001// License: GPL. For details, see LICENSE file.
002package org.openstreetmap.josm.gui.mappaint;
003
004import java.awt.Color;
005import java.awt.Rectangle;
006
007import org.openstreetmap.josm.data.osm.Node;
008import org.openstreetmap.josm.data.osm.OsmPrimitive;
009import org.openstreetmap.josm.data.osm.visitor.paint.MapPaintSettings;
010import org.openstreetmap.josm.data.osm.visitor.paint.PaintColors;
011import org.openstreetmap.josm.data.osm.visitor.paint.StyledMapRenderer;
012import org.openstreetmap.josm.tools.CheckParameterUtil;
013
014/**
015 * Text style attached to a style with a bounding box, like an icon or a symbol.
016 */
017public class BoxTextElemStyle extends ElemStyle {
018
019    public enum HorizontalTextAlignment { LEFT, CENTER, RIGHT }
020    public enum VerticalTextAlignment { ABOVE, TOP, CENTER, BOTTOM, BELOW }
021
022    public static interface BoxProvider {
023        BoxProviderResult get();
024    }
025
026    public static class BoxProviderResult {
027        private Rectangle box;
028        private boolean temporary;
029
030        public BoxProviderResult(Rectangle box, boolean temporary) {
031            this.box = box;
032            this.temporary = temporary;
033        }
034
035        /**
036         * The box
037         */
038        public Rectangle getBox() {
039            return box;
040        }
041
042        /**
043         * True, if the box can change in future calls of the BoxProvider get() method
044         */
045        public boolean isTemporary() {
046            return temporary;
047        }
048    }
049
050    public static class SimpleBoxProvider implements BoxProvider {
051        private Rectangle box;
052
053        public SimpleBoxProvider(Rectangle box) {
054            this.box = box;
055        }
056
057        @Override
058        public BoxProviderResult get() {
059            return new BoxProviderResult(box, false);
060        }
061
062        @Override
063        public int hashCode() {
064            return box.hashCode();
065        }
066
067        @Override
068        public boolean equals(Object obj) {
069            if (!(obj instanceof BoxProvider))
070                return false;
071            final BoxProvider other = (BoxProvider) obj;
072            BoxProviderResult resultOther = other.get();
073            if (resultOther.isTemporary()) return false;
074            return box.equals(resultOther.getBox());
075        }
076    }
077
078    public static final Rectangle ZERO_BOX = new Rectangle(0, 0, 0, 0);
079
080    public TextElement text;
081    // Either boxProvider or box is not null. If boxProvider is different from
082    // null, this means, that the box can still change in future, otherwise
083    // it is fixed.
084    protected BoxProvider boxProvider;
085    protected Rectangle box;
086    public HorizontalTextAlignment hAlign;
087    public VerticalTextAlignment vAlign;
088
089    public BoxTextElemStyle(Cascade c, TextElement text, BoxProvider boxProvider, Rectangle box, HorizontalTextAlignment hAlign, VerticalTextAlignment vAlign) {
090        super(c, 5f);
091        CheckParameterUtil.ensureParameterNotNull(text);
092        CheckParameterUtil.ensureParameterNotNull(hAlign);
093        CheckParameterUtil.ensureParameterNotNull(vAlign);
094        this.text = text;
095        this.boxProvider = boxProvider;
096        this.box = box == null ? ZERO_BOX : box;
097        this.hAlign = hAlign;
098        this.vAlign = vAlign;
099    }
100
101    public static BoxTextElemStyle create(Environment env, BoxProvider boxProvider) {
102        return create(env, boxProvider, null);
103    }
104
105    public static BoxTextElemStyle create(Environment env, Rectangle box) {
106        return create(env, null, box);
107    }
108
109    public static BoxTextElemStyle create(Environment env, BoxProvider boxProvider, Rectangle box) {
110        initDefaultParameters();
111        Cascade c = env.mc.getCascade(env.layer);
112
113        TextElement text = TextElement.create(c, DEFAULT_TEXT_COLOR, false);
114        if (text == null) return null;
115        // Skip any primitives that don't have text to draw. (Styles are recreated for any tag change.)
116        // The concrete text to render is not cached in this object, but computed for each
117        // repaint. This way, one BoxTextElemStyle object can be used by multiple primitives (to save memory).
118        if (text.labelCompositionStrategy.compose(env.osm) == null) return null;
119
120        HorizontalTextAlignment hAlign = HorizontalTextAlignment.RIGHT;
121        Keyword hAlignKW = c.get("text-anchor-horizontal", Keyword.RIGHT, Keyword.class);
122        if ("left".equals(hAlignKW.val)) {
123            hAlign = HorizontalTextAlignment.LEFT;
124        } else if ("center".equals(hAlignKW.val)) {
125            hAlign = HorizontalTextAlignment.CENTER;
126        } else if ("right".equals(hAlignKW.val)) {
127            hAlign = HorizontalTextAlignment.RIGHT;
128        }
129        VerticalTextAlignment vAlign = VerticalTextAlignment.BOTTOM;
130        String vAlignStr = c.get("text-anchor-vertical", Keyword.BOTTOM, Keyword.class).val;
131        if ("above".equals(vAlignStr)) {
132            vAlign = VerticalTextAlignment.ABOVE;
133        } else if ("top".equals(vAlignStr)) {
134            vAlign = VerticalTextAlignment.TOP;
135        } else if ("center".equals(vAlignStr)) {
136            vAlign = VerticalTextAlignment.CENTER;
137        } else if ("bottom".equals(vAlignStr)) {
138            vAlign = VerticalTextAlignment.BOTTOM;
139        } else if ("below".equals(vAlignStr)) {
140            vAlign = VerticalTextAlignment.BELOW;
141        }
142
143        return new BoxTextElemStyle(c, text, boxProvider, box, hAlign, vAlign);
144    }
145
146    public Rectangle getBox() {
147        if (boxProvider != null) {
148            BoxProviderResult result = boxProvider.get();
149            if (!result.isTemporary()) {
150                box = result.getBox();
151                boxProvider = null;
152            }
153            return result.getBox();
154        }
155        return box;
156    }
157
158    public static final BoxTextElemStyle SIMPLE_NODE_TEXT_ELEMSTYLE;
159    static {
160        MultiCascade mc = new MultiCascade();
161        Cascade c = mc.getOrCreateCascade("default");
162        c.put(TEXT, Keyword.AUTO);
163        Node n = new Node();
164        n.put("name", "dummy");
165        SIMPLE_NODE_TEXT_ELEMSTYLE = create(new Environment(n, mc, "default", null), NodeElemStyle.SIMPLE_NODE_ELEMSTYLE.getBoxProvider());
166        if (SIMPLE_NODE_TEXT_ELEMSTYLE == null) throw new AssertionError();
167    }
168    /*
169     * Caches the default text color from the preferences.
170     *
171     * FIXME: the cache isn't updated if the user changes the preference during a JOSM
172     * session. There should be preference listener updating this cache.
173     */
174    private static Color DEFAULT_TEXT_COLOR = null;
175    private static void initDefaultParameters() {
176        if (DEFAULT_TEXT_COLOR != null) return;
177        DEFAULT_TEXT_COLOR = PaintColors.TEXT.get();
178    }
179
180    @Override
181    public void paintPrimitive(OsmPrimitive osm, MapPaintSettings settings, StyledMapRenderer painter, boolean selected, boolean member) {
182        if (osm instanceof Node) {
183            painter.drawBoxText((Node) osm, this);
184        }
185    }
186
187    @Override
188    public boolean equals(Object obj) {
189        if (!super.equals(obj))
190            return false;
191        if (obj == null || getClass() != obj.getClass())
192            return false;
193        final BoxTextElemStyle other = (BoxTextElemStyle) obj;
194        if (!text.equals(other.text)) return false;
195        if (boxProvider != null) {
196            if (!boxProvider.equals(other.boxProvider)) return false;
197        } else if (other.boxProvider != null)
198            return false;
199        else {
200            if (!box.equals(other.box)) return false;
201        }
202        if (hAlign != other.hAlign) return false;
203        if (vAlign != other.vAlign) return false;
204        return true;
205    }
206
207    @Override
208    public int hashCode() {
209        int hash = super.hashCode();
210        hash = 97 * hash + text.hashCode();
211        if (boxProvider != null) {
212            hash = 97 * hash + boxProvider.hashCode();
213        } else {
214            hash = 97 * hash + box.hashCode();
215        }
216        hash = 97 * hash + hAlign.hashCode();
217        hash = 97 * hash + vAlign.hashCode();
218        return hash;
219    }
220
221    @Override
222    public String toString() {
223        return "BoxTextElemStyle{" + super.toString() + " " + text.toStringImpl() + " box=" + box + " hAlign=" + hAlign + " vAlign=" + vAlign + '}';
224    }
225
226}