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}