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}