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