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.data.osm.IPrimitive; 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; 017import org.openstreetmap.josm.spi.preferences.Config; 018 019/** 020 * Class that defines how objects ({@link OsmPrimitive}) should be drawn on the map. 021 * 022 * Several subclasses of this abstract class implement different drawing features, 023 * like icons for a node or area fill. This class and all its subclasses are immutable 024 * and tend to get shared when multiple objects have the same style (in order to 025 * save memory, see {@link org.openstreetmap.josm.gui.mappaint.StyleCache#intern()}). 026 */ 027public abstract class StyleElement implements StyleKeys { 028 029 protected static final int ICON_IMAGE_IDX = 0; 030 protected static final int ICON_WIDTH_IDX = 1; 031 protected static final int ICON_HEIGHT_IDX = 2; 032 protected static final int ICON_OPACITY_IDX = 3; 033 protected static final int ICON_OFFSET_X_IDX = 4; 034 protected static final int ICON_OFFSET_Y_IDX = 5; 035 036 /** 037 * The major z index of this style element 038 */ 039 public float majorZIndex; 040 /** 041 * The z index as set by the user 042 */ 043 public float zIndex; 044 /** 045 * The object z index 046 */ 047 public float objectZIndex; 048 /** 049 * false, if style can serve as main style for the primitive; 050 * true, if it is a highlight or modifier 051 */ 052 public boolean isModifier; 053 /** 054 * A flag indicating that the selection color handling should be done automatically 055 */ 056 public boolean defaultSelectedHandling; 057 058 /** 059 * Construct a new StyleElement 060 * @param majorZIndex like z-index, but higher priority 061 * @param zIndex order the objects are drawn 062 * @param objectZIndex like z-index, but lower priority 063 * @param isModifier if false, a default line or node symbol is generated 064 * @param defaultSelectedHandling true if default behavior for selected objects 065 * is enabled, false if a style for selected state is given explicitly 066 */ 067 public StyleElement(float majorZIndex, float zIndex, float objectZIndex, boolean isModifier, boolean defaultSelectedHandling) { 068 this.majorZIndex = majorZIndex; 069 this.zIndex = zIndex; 070 this.objectZIndex = objectZIndex; 071 this.isModifier = isModifier; 072 this.defaultSelectedHandling = defaultSelectedHandling; 073 } 074 075 protected StyleElement(Cascade c, float defaultMajorZindex) { 076 majorZIndex = c.get(MAJOR_Z_INDEX, defaultMajorZindex, Float.class); 077 zIndex = c.get(Z_INDEX, 0f, Float.class); 078 objectZIndex = c.get(OBJECT_Z_INDEX, 0f, Float.class); 079 isModifier = c.get(MODIFIER, Boolean.FALSE, Boolean.class); 080 defaultSelectedHandling = c.isDefaultSelectedHandling(); 081 } 082 083 /** 084 * draws a primitive 085 * @param primitive primitive to draw 086 * @param paintSettings paint settings 087 * @param painter painter 088 * @param selected true, if primitive is selected 089 * @param outermember true, if primitive is not selected and outer member of a selected multipolygon relation 090 * @param member true, if primitive is not selected and member of a selected relation 091 * @since 13662 (signature) 092 */ 093 public abstract void paintPrimitive(IPrimitive primitive, MapPaintSettings paintSettings, StyledMapRenderer painter, 094 boolean selected, boolean outermember, boolean member); 095 096 /** 097 * Check if this is a style that makes the line visible to the user 098 * @return <code>true</code> for line styles 099 */ 100 public boolean isProperLineStyle() { 101 return false; 102 } 103 104 /** 105 * Get a property value of type Width 106 * @param c the cascade 107 * @param key property key for the width value 108 * @param relativeTo reference width. Only needed, when relative width syntax is used, e.g. "+4". 109 * @return width 110 */ 111 protected static Float getWidth(Cascade c, String key, Float relativeTo) { 112 Float width = c.get(key, null, Float.class, true); 113 if (width != null) { 114 if (width > 0) 115 return width; 116 } else { 117 Keyword widthKW = c.get(key, null, Keyword.class, true); 118 if (Keyword.THINNEST.equals(widthKW)) 119 return 0f; 120 if (Keyword.DEFAULT.equals(widthKW)) 121 return (float) MapPaintSettings.INSTANCE.getDefaultSegmentWidth(); 122 if (relativeTo != null) { 123 RelativeFloat widthRel = c.get(key, null, RelativeFloat.class, true); 124 if (widthRel != null) 125 return relativeTo + widthRel.val; 126 } 127 } 128 return null; 129 } 130 131 /* ------------------------------------------------------------------------------- */ 132 /* cached values */ 133 /* ------------------------------------------------------------------------------- */ 134 /* 135 * Two preference values and the set of created fonts are cached in order to avoid 136 * expensive lookups and to avoid too many font objects 137 * 138 * FIXME: cached preference values are not updated if the user changes them during 139 * a JOSM session. Should have a listener listening to preference changes. 140 */ 141 private static volatile String defaultFontName; 142 private static volatile Float defaultFontSize; 143 private static final Object lock = new Object(); 144 145 // thread save access (double-checked locking) 146 private static Float getDefaultFontSize() { 147 Float s = defaultFontSize; 148 if (s == null) { 149 synchronized (lock) { 150 s = defaultFontSize; 151 if (s == null) { 152 defaultFontSize = s = (float) Config.getPref().getInt("mappaint.fontsize", 8); 153 } 154 } 155 } 156 return s; 157 } 158 159 private static String getDefaultFontName() { 160 String n = defaultFontName; 161 if (n == null) { 162 synchronized (lock) { 163 n = defaultFontName; 164 if (n == null) { 165 defaultFontName = n = Config.getPref().get("mappaint.font", "Droid Sans"); 166 } 167 } 168 } 169 return n; 170 } 171 172 private static class FontDescriptor { 173 public String name; 174 public int style; 175 public int size; 176 177 FontDescriptor(String name, int style, int size) { 178 this.name = name; 179 this.style = style; 180 this.size = size; 181 } 182 183 @Override 184 public int hashCode() { 185 return Objects.hash(name, style, size); 186 } 187 188 @Override 189 public boolean equals(Object obj) { 190 if (this == obj) return true; 191 if (obj == null || getClass() != obj.getClass()) return false; 192 FontDescriptor that = (FontDescriptor) obj; 193 return style == that.style && 194 size == that.size && 195 Objects.equals(name, that.name); 196 } 197 } 198 199 private static final Map<FontDescriptor, Font> FONT_MAP = new HashMap<>(); 200 201 private static Font getCachedFont(FontDescriptor fd) { 202 Font f = FONT_MAP.get(fd); 203 if (f != null) return f; 204 f = new Font(fd.name, fd.style, fd.size); 205 FONT_MAP.put(fd, f); 206 return f; 207 } 208 209 private static Font getCachedFont(String name, int style, int size) { 210 return getCachedFont(new FontDescriptor(name, style, size)); 211 } 212 213 protected static Font getFont(Cascade c, String s) { 214 String name = c.get(FONT_FAMILY, getDefaultFontName(), String.class); 215 float size = c.get(FONT_SIZE, getDefaultFontSize(), Float.class); 216 int weight = Font.PLAIN; 217 if ("bold".equalsIgnoreCase(c.get(FONT_WEIGHT, null, String.class))) { 218 weight = Font.BOLD; 219 } 220 int style = Font.PLAIN; 221 if ("italic".equalsIgnoreCase(c.get(FONT_STYLE, null, String.class))) { 222 style = Font.ITALIC; 223 } 224 Font f = getCachedFont(name, style | weight, Math.round(size)); 225 if (f.canDisplayUpTo(s) == -1) 226 return f; 227 else { 228 // fallback if the string contains characters that cannot be 229 // rendered by the selected font 230 return getCachedFont("SansSerif", style | weight, Math.round(size)); 231 } 232 } 233 234 @Override 235 public boolean equals(Object o) { 236 if (this == o) return true; 237 if (o == null || getClass() != o.getClass()) return false; 238 StyleElement that = (StyleElement) o; 239 return isModifier == that.isModifier && 240 Float.compare(that.majorZIndex, majorZIndex) == 0 && 241 Float.compare(that.zIndex, zIndex) == 0 && 242 Float.compare(that.objectZIndex, objectZIndex) == 0; 243 } 244 245 @Override 246 public int hashCode() { 247 return Objects.hash(majorZIndex, zIndex, objectZIndex, isModifier); 248 } 249 250 @Override 251 public String toString() { 252 return String.format("z_idx=[%s/%s/%s] ", majorZIndex, zIndex, objectZIndex) + (isModifier ? "modifier " : ""); 253 } 254}