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