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 String[] ICON_KEYS = {"icon-image", "icon-width", "icon-height", "icon-opacity"};
017    protected static final String[] REPEAT_IMAGE_KEYS = {"repeat-image", "repeat-image-width", "repeat-image-height", "repeat-image-opacity"};
018
019    public float major_z_index;
020    public float z_index;
021    public float object_z_index;
022    public boolean isModifier;  // false, if style can serve as main style for the
023    // primitive; true, if it is a highlight or modifier
024
025    public ElemStyle(float major_z_index, float z_index, float object_z_index, boolean isModifier) {
026        this.major_z_index = major_z_index;
027        this.z_index = z_index;
028        this.object_z_index = object_z_index;
029        this.isModifier = isModifier;
030    }
031
032    protected ElemStyle(Cascade c, float default_major_z_index) {
033        major_z_index = c.get("major-z-index", default_major_z_index, Float.class);
034        z_index = c.get(Z_INDEX, 0f, Float.class);
035        object_z_index = c.get(OBJECT_Z_INDEX, 0f, Float.class);
036        isModifier = c.get(MODIFIER, false, Boolean.class);
037    }
038
039    /**
040     * draws a primitive
041     * @param primitive
042     * @param paintSettings
043     * @param painter
044     * @param selected true, if primitive is selected
045     * @param outermember true, if primitive is not selected and outer member of a selected multipolygon relation
046     * @param member true, if primitive is not selected and member of a selected relation
047     */
048    public abstract void paintPrimitive(OsmPrimitive primitive, MapPaintSettings paintSettings, StyledMapRenderer painter,
049            boolean selected, boolean outermember, boolean member);
050
051    public boolean isProperLineStyle() {
052        return false;
053    }
054
055    /**
056     * Get a property value of type Width
057     * @param c the cascade
058     * @param key property key for the width value
059     * @param relativeTo reference width. Only needed, when relative width syntax
060     *              is used, e.g. "+4".
061     */
062    protected static Float getWidth(Cascade c, String key, Float relativeTo) {
063        Float width = c.get(key, null, Float.class, true);
064        if (width != null) {
065            if (width > 0)
066                return width;
067        } else {
068            Keyword widthKW = c.get(key, null, Keyword.class, true);
069            if (Keyword.THINNEST.equals(widthKW))
070                return 0f;
071            if (Keyword.DEFAULT.equals(widthKW))
072                return (float) MapPaintSettings.INSTANCE.getDefaultSegmentWidth();
073            if (relativeTo != null) {
074                RelativeFloat width_rel = c.get(key, null, RelativeFloat.class, true);
075                if (width_rel != null)
076                    return relativeTo + width_rel.val;
077            }
078        }
079        return null;
080    }
081
082    /* ------------------------------------------------------------------------------- */
083    /* cached values                                                                   */
084    /* ------------------------------------------------------------------------------- */
085    /*
086     * Two preference values and the set of created fonts are cached in order to avoid
087     * expensive lookups and to avoid too many font objects
088     *
089     * FIXME: cached preference values are not updated if the user changes them during
090     * a JOSM session. Should have a listener listening to preference changes.
091     */
092    private static volatile String DEFAULT_FONT_NAME = null;
093    private static volatile Float DEFAULT_FONT_SIZE = null;
094    private static final Object lock = new Object();
095
096    // thread save access (double-checked locking)
097    private static Float getDefaultFontSize() {
098        Float s = DEFAULT_FONT_SIZE;
099        if (s == null) {
100            synchronized (lock) {
101                s = DEFAULT_FONT_SIZE;
102                if (s == null) {
103                    DEFAULT_FONT_SIZE = s = (float) Main.pref.getInteger("mappaint.fontsize", 8);
104                }
105            }
106        }
107        return s;
108    }
109
110    private static String getDefaultFontName() {
111        String n = DEFAULT_FONT_NAME;
112        if (n == null) {
113            synchronized (lock) {
114                n = DEFAULT_FONT_NAME;
115                if (n == null) {
116                    DEFAULT_FONT_NAME = n = Main.pref.get("mappaint.font", "Droid Sans");
117                }
118            }
119        }
120        return n;
121    }
122
123    private static class FontDescriptor {
124        public String name;
125        public int style;
126        public int size;
127
128        public FontDescriptor(String name, int style, int size){
129            this.name = name;
130            this.style = style;
131            this.size = size;
132        }
133
134        @Override
135        public int hashCode() {
136            final int prime = 31;
137            int result = 1;
138            result = prime * result + ((name == null) ? 0 : name.hashCode());
139            result = prime * result + size;
140            result = prime * result + style;
141            return result;
142        }
143        @Override
144        public boolean equals(Object obj) {
145            if (this == obj)
146                return true;
147            if (obj == null)
148                return false;
149            if (getClass() != obj.getClass())
150                return false;
151            FontDescriptor other = (FontDescriptor) obj;
152            if (name == null) {
153                if (other.name != null)
154                    return false;
155            } else if (!name.equals(other.name))
156                return false;
157            if (size != other.size)
158                return false;
159            if (style != other.style)
160                return false;
161            return true;
162        }
163    }
164
165    private static final Map<FontDescriptor, Font> FONT_MAP = new HashMap<>();
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 (!(o instanceof ElemStyle))
202            return false;
203        ElemStyle s = (ElemStyle) o;
204        return major_z_index == s.major_z_index &&
205                z_index == s.z_index &&
206                object_z_index == s.object_z_index &&
207                isModifier == s.isModifier;
208    }
209
210    @Override
211    public int hashCode() {
212        int hash = 5;
213        hash = 41 * hash + Float.floatToIntBits(this.major_z_index);
214        hash = 41 * hash + Float.floatToIntBits(this.z_index);
215        hash = 41 * hash + Float.floatToIntBits(this.object_z_index);
216        hash = 41 * hash + (isModifier ? 1 : 0);
217        return hash;
218    }
219
220    @Override
221    public String toString() {
222        return String.format("z_idx=[%s/%s/%s] ", major_z_index, z_index, object_z_index) + (isModifier ? "modifier " : "");
223    }
224}