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