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