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