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