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