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}