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