001// License: GPL. For details, see LICENSE file. 002package org.openstreetmap.josm.gui.mappaint.styleelement; 003 004import java.awt.Font; 005import java.util.HashMap; 006import java.util.Map; 007import java.util.Objects; 008 009import org.openstreetmap.josm.Main; 010import org.openstreetmap.josm.data.osm.OsmPrimitive; 011import org.openstreetmap.josm.data.osm.visitor.paint.MapPaintSettings; 012import org.openstreetmap.josm.data.osm.visitor.paint.StyledMapRenderer; 013import org.openstreetmap.josm.gui.mappaint.Cascade; 014import org.openstreetmap.josm.gui.mappaint.Keyword; 015import org.openstreetmap.josm.gui.mappaint.StyleKeys; 016import org.openstreetmap.josm.gui.mappaint.mapcss.Instruction.RelativeFloat; 017 018public abstract class StyleElement implements StyleKeys { 019 020 protected static final int ICON_IMAGE_IDX = 0; 021 protected static final int ICON_WIDTH_IDX = 1; 022 protected static final int ICON_HEIGHT_IDX = 2; 023 protected static final int ICON_OPACITY_IDX = 3; 024 protected static final int ICON_OFFSET_X_IDX = 4; 025 protected static final int ICON_OFFSET_Y_IDX = 5; 026 protected static final String[] ICON_KEYS = {ICON_IMAGE, ICON_WIDTH, ICON_HEIGHT, ICON_OPACITY, ICON_OFFSET_X, ICON_OFFSET_Y}; 027 protected static final String[] REPEAT_IMAGE_KEYS = {REPEAT_IMAGE, REPEAT_IMAGE_WIDTH, REPEAT_IMAGE_HEIGHT, REPEAT_IMAGE_OPACITY, 028 null, null}; 029 030 public float majorZIndex; 031 public float zIndex; 032 public float objectZIndex; 033 public boolean isModifier; // false, if style can serve as main style for the 034 // primitive; true, if it is a highlight or modifier 035 public boolean defaultSelectedHandling; 036 037 public StyleElement(float major_z_index, float z_index, float object_z_index, boolean isModifier, boolean defaultSelectedHandling) { 038 this.majorZIndex = major_z_index; 039 this.zIndex = z_index; 040 this.objectZIndex = object_z_index; 041 this.isModifier = isModifier; 042 this.defaultSelectedHandling = defaultSelectedHandling; 043 } 044 045 protected StyleElement(Cascade c, float default_major_z_index) { 046 majorZIndex = c.get(MAJOR_Z_INDEX, default_major_z_index, Float.class); 047 zIndex = c.get(Z_INDEX, 0f, Float.class); 048 objectZIndex = c.get(OBJECT_Z_INDEX, 0f, Float.class); 049 isModifier = c.get(MODIFIER, Boolean.FALSE, Boolean.class); 050 defaultSelectedHandling = c.isDefaultSelectedHandling(); 051 } 052 053 /** 054 * draws a primitive 055 * @param primitive primitive to draw 056 * @param paintSettings paint settings 057 * @param painter painter 058 * @param selected true, if primitive is selected 059 * @param outermember true, if primitive is not selected and outer member of a selected multipolygon relation 060 * @param member true, if primitive is not selected and member of a selected relation 061 */ 062 public abstract void paintPrimitive(OsmPrimitive primitive, MapPaintSettings paintSettings, StyledMapRenderer painter, 063 boolean selected, boolean outermember, boolean member); 064 065 public boolean isProperLineStyle() { 066 return false; 067 } 068 069 /** 070 * Get a property value of type Width 071 * @param c the cascade 072 * @param key property key for the width value 073 * @param relativeTo reference width. Only needed, when relative width syntax is used, e.g. "+4". 074 * @return width 075 */ 076 protected static Float getWidth(Cascade c, String key, Float relativeTo) { 077 Float width = c.get(key, null, Float.class, true); 078 if (width != null) { 079 if (width > 0) 080 return width; 081 } else { 082 Keyword widthKW = c.get(key, null, Keyword.class, true); 083 if (Keyword.THINNEST.equals(widthKW)) 084 return 0f; 085 if (Keyword.DEFAULT.equals(widthKW)) 086 return (float) MapPaintSettings.INSTANCE.getDefaultSegmentWidth(); 087 if (relativeTo != null) { 088 RelativeFloat width_rel = c.get(key, null, RelativeFloat.class, true); 089 if (width_rel != null) 090 return relativeTo + width_rel.val; 091 } 092 } 093 return null; 094 } 095 096 /* ------------------------------------------------------------------------------- */ 097 /* cached values */ 098 /* ------------------------------------------------------------------------------- */ 099 /* 100 * Two preference values and the set of created fonts are cached in order to avoid 101 * expensive lookups and to avoid too many font objects 102 * 103 * FIXME: cached preference values are not updated if the user changes them during 104 * a JOSM session. Should have a listener listening to preference changes. 105 */ 106 private static volatile String DEFAULT_FONT_NAME; 107 private static volatile Float DEFAULT_FONT_SIZE; 108 private static final Object lock = new Object(); 109 110 // thread save access (double-checked locking) 111 private static Float getDefaultFontSize() { 112 Float s = DEFAULT_FONT_SIZE; 113 if (s == null) { 114 synchronized (lock) { 115 s = DEFAULT_FONT_SIZE; 116 if (s == null) { 117 DEFAULT_FONT_SIZE = s = (float) Main.pref.getInteger("mappaint.fontsize", 8); 118 } 119 } 120 } 121 return s; 122 } 123 124 private static String getDefaultFontName() { 125 String n = DEFAULT_FONT_NAME; 126 if (n == null) { 127 synchronized (lock) { 128 n = DEFAULT_FONT_NAME; 129 if (n == null) { 130 DEFAULT_FONT_NAME = n = Main.pref.get("mappaint.font", "Droid Sans"); 131 } 132 } 133 } 134 return n; 135 } 136 137 private static class FontDescriptor { 138 public String name; 139 public int style; 140 public int size; 141 142 FontDescriptor(String name, int style, int size) { 143 this.name = name; 144 this.style = style; 145 this.size = size; 146 } 147 148 @Override 149 public int hashCode() { 150 return Objects.hash(name, style, size); 151 } 152 153 @Override 154 public boolean equals(Object obj) { 155 if (this == obj) return true; 156 if (obj == null || getClass() != obj.getClass()) return false; 157 FontDescriptor that = (FontDescriptor) obj; 158 return style == that.style && 159 size == that.size && 160 Objects.equals(name, that.name); 161 } 162 } 163 164 private static final Map<FontDescriptor, Font> FONT_MAP = new HashMap<>(); 165 166 private static Font getCachedFont(FontDescriptor fd) { 167 Font f = FONT_MAP.get(fd); 168 if (f != null) return f; 169 f = new Font(fd.name, fd.style, fd.size); 170 FONT_MAP.put(fd, f); 171 return f; 172 } 173 174 private static Font getCachedFont(String name, int style, int size) { 175 return getCachedFont(new FontDescriptor(name, style, size)); 176 } 177 178 protected static Font getFont(Cascade c, String s) { 179 String name = c.get(FONT_FAMILY, getDefaultFontName(), String.class); 180 float size = c.get(FONT_SIZE, getDefaultFontSize(), Float.class); 181 int weight = Font.PLAIN; 182 if ("bold".equalsIgnoreCase(c.get(FONT_WEIGHT, null, String.class))) { 183 weight = Font.BOLD; 184 } 185 int style = Font.PLAIN; 186 if ("italic".equalsIgnoreCase(c.get(FONT_STYLE, null, String.class))) { 187 style = Font.ITALIC; 188 } 189 Font f = getCachedFont(name, style | weight, Math.round(size)); 190 if (f.canDisplayUpTo(s) == -1) 191 return f; 192 else { 193 // fallback if the string contains characters that cannot be 194 // rendered by the selected font 195 return getCachedFont("SansSerif", style | weight, Math.round(size)); 196 } 197 } 198 199 @Override 200 public boolean equals(Object o) { 201 if (this == o) return true; 202 if (o == null || getClass() != o.getClass()) return false; 203 StyleElement that = (StyleElement) o; 204 return Float.compare(that.majorZIndex, majorZIndex) == 0 && 205 Float.compare(that.zIndex, zIndex) == 0 && 206 Float.compare(that.objectZIndex, objectZIndex) == 0 && 207 isModifier == that.isModifier; 208 } 209 210 @Override 211 public int hashCode() { 212 return Objects.hash(majorZIndex, zIndex, objectZIndex, isModifier); 213 } 214 215 @Override 216 public String toString() { 217 return String.format("z_idx=[%s/%s/%s] ", majorZIndex, zIndex, objectZIndex) + (isModifier ? "modifier " : ""); 218 } 219}