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
027    public float majorZIndex;
028    public float zIndex;
029    public float objectZIndex;
030    public boolean isModifier;  // false, if style can serve as main style for the
031    // primitive; true, if it is a highlight or modifier
032    public boolean defaultSelectedHandling;
033
034    public StyleElement(float majorZindex, float zIndex, float objectZindex, boolean isModifier, boolean defaultSelectedHandling) {
035        this.majorZIndex = majorZindex;
036        this.zIndex = zIndex;
037        this.objectZIndex = objectZindex;
038        this.isModifier = isModifier;
039        this.defaultSelectedHandling = defaultSelectedHandling;
040    }
041
042    protected StyleElement(Cascade c, float defaultMajorZindex) {
043        majorZIndex = c.get(MAJOR_Z_INDEX, defaultMajorZindex, Float.class);
044        zIndex = c.get(Z_INDEX, 0f, Float.class);
045        objectZIndex = c.get(OBJECT_Z_INDEX, 0f, Float.class);
046        isModifier = c.get(MODIFIER, Boolean.FALSE, Boolean.class);
047        defaultSelectedHandling = c.isDefaultSelectedHandling();
048    }
049
050    /**
051     * draws a primitive
052     * @param primitive primitive to draw
053     * @param paintSettings paint settings
054     * @param painter painter
055     * @param selected true, if primitive is selected
056     * @param outermember true, if primitive is not selected and outer member of a selected multipolygon relation
057     * @param member true, if primitive is not selected and member of a selected relation
058     */
059    public abstract void paintPrimitive(OsmPrimitive primitive, MapPaintSettings paintSettings, StyledMapRenderer painter,
060            boolean selected, boolean outermember, boolean member);
061
062    public boolean isProperLineStyle() {
063        return false;
064    }
065
066    /**
067     * Get a property value of type Width
068     * @param c the cascade
069     * @param key property key for the width value
070     * @param relativeTo reference width. Only needed, when relative width syntax is used, e.g. "+4".
071     * @return width
072     */
073    protected static Float getWidth(Cascade c, String key, Float relativeTo) {
074        Float width = c.get(key, null, Float.class, true);
075        if (width != null) {
076            if (width > 0)
077                return width;
078        } else {
079            Keyword widthKW = c.get(key, null, Keyword.class, true);
080            if (Keyword.THINNEST.equals(widthKW))
081                return 0f;
082            if (Keyword.DEFAULT.equals(widthKW))
083                return (float) MapPaintSettings.INSTANCE.getDefaultSegmentWidth();
084            if (relativeTo != null) {
085                RelativeFloat widthRel = c.get(key, null, RelativeFloat.class, true);
086                if (widthRel != null)
087                    return relativeTo + widthRel.val;
088            }
089        }
090        return null;
091    }
092
093    /* ------------------------------------------------------------------------------- */
094    /* cached values                                                                   */
095    /* ------------------------------------------------------------------------------- */
096    /*
097     * Two preference values and the set of created fonts are cached in order to avoid
098     * expensive lookups and to avoid too many font objects
099     *
100     * FIXME: cached preference values are not updated if the user changes them during
101     * a JOSM session. Should have a listener listening to preference changes.
102     */
103    private static volatile String DEFAULT_FONT_NAME;
104    private static volatile Float DEFAULT_FONT_SIZE;
105    private static final Object lock = new Object();
106
107    // thread save access (double-checked locking)
108    private static Float getDefaultFontSize() {
109        Float s = DEFAULT_FONT_SIZE;
110        if (s == null) {
111            synchronized (lock) {
112                s = DEFAULT_FONT_SIZE;
113                if (s == null) {
114                    DEFAULT_FONT_SIZE = s = (float) Main.pref.getInteger("mappaint.fontsize", 8);
115                }
116            }
117        }
118        return s;
119    }
120
121    private static String getDefaultFontName() {
122        String n = DEFAULT_FONT_NAME;
123        if (n == null) {
124            synchronized (lock) {
125                n = DEFAULT_FONT_NAME;
126                if (n == null) {
127                    DEFAULT_FONT_NAME = n = Main.pref.get("mappaint.font", "Droid Sans");
128                }
129            }
130        }
131        return n;
132    }
133
134    private static class FontDescriptor {
135        public String name;
136        public int style;
137        public int size;
138
139        FontDescriptor(String name, int style, int size) {
140            this.name = name;
141            this.style = style;
142            this.size = size;
143        }
144
145        @Override
146        public int hashCode() {
147            return Objects.hash(name, style, size);
148        }
149
150        @Override
151        public boolean equals(Object obj) {
152            if (this == obj) return true;
153            if (obj == null || getClass() != obj.getClass()) return false;
154            FontDescriptor that = (FontDescriptor) obj;
155            return style == that.style &&
156                    size == that.size &&
157                    Objects.equals(name, that.name);
158        }
159    }
160
161    private static final Map<FontDescriptor, Font> FONT_MAP = new HashMap<>();
162
163    private static Font getCachedFont(FontDescriptor fd) {
164        Font f = FONT_MAP.get(fd);
165        if (f != null) return f;
166        f = new Font(fd.name, fd.style, fd.size);
167        FONT_MAP.put(fd, f);
168        return f;
169    }
170
171    private static Font getCachedFont(String name, int style, int size) {
172        return getCachedFont(new FontDescriptor(name, style, size));
173    }
174
175    protected static Font getFont(Cascade c, String s) {
176        String name = c.get(FONT_FAMILY, getDefaultFontName(), String.class);
177        float size = c.get(FONT_SIZE, getDefaultFontSize(), Float.class);
178        int weight = Font.PLAIN;
179        if ("bold".equalsIgnoreCase(c.get(FONT_WEIGHT, null, String.class))) {
180            weight = Font.BOLD;
181        }
182        int style = Font.PLAIN;
183        if ("italic".equalsIgnoreCase(c.get(FONT_STYLE, null, String.class))) {
184            style = Font.ITALIC;
185        }
186        Font f = getCachedFont(name, style | weight, Math.round(size));
187        if (f.canDisplayUpTo(s) == -1)
188            return f;
189        else {
190            // fallback if the string contains characters that cannot be
191            // rendered by the selected font
192            return getCachedFont("SansSerif", style | weight, Math.round(size));
193        }
194    }
195
196    @Override
197    public boolean equals(Object o) {
198        if (this == o) return true;
199        if (o == null || getClass() != o.getClass()) return false;
200        StyleElement that = (StyleElement) o;
201        return Float.compare(that.majorZIndex, majorZIndex) == 0 &&
202                Float.compare(that.zIndex, zIndex) == 0 &&
203                Float.compare(that.objectZIndex, objectZIndex) == 0 &&
204                isModifier == that.isModifier;
205    }
206
207    @Override
208    public int hashCode() {
209        return Objects.hash(majorZIndex, zIndex, objectZIndex, isModifier);
210    }
211
212    @Override
213    public String toString() {
214        return String.format("z_idx=[%s/%s/%s] ", majorZIndex, zIndex, objectZIndex) + (isModifier ? "modifier " : "");
215    }
216}