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
123        TextLabel text = TextLabel.create(env, DEFAULT_TEXT_COLOR, false);
124        if (text == null) return null;
125        // Skip any primitives that don't have text to draw. (Styles are recreated for any tag change.)
126        // The concrete text to render is not cached in this object, but computed for each
127        // repaint. This way, one BoxTextElement object can be used by multiple primitives (to save memory).
128        if (text.labelCompositionStrategy.compose(env.osm) == null) return null;
129
130        Cascade c = env.mc.getCascade(env.layer);
131
132        HorizontalTextAlignment hAlign;
133        switch (c.get(TEXT_ANCHOR_HORIZONTAL, Keyword.RIGHT, Keyword.class).val) {
134            case "left":
135                hAlign = HorizontalTextAlignment.LEFT;
136                break;
137            case "center":
138                hAlign = HorizontalTextAlignment.CENTER;
139                break;
140            case "right":
141            default:
142                hAlign = HorizontalTextAlignment.RIGHT;
143        }
144        VerticalTextAlignment vAlign;
145        switch (c.get(TEXT_ANCHOR_VERTICAL, Keyword.BOTTOM, Keyword.class).val) {
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                break;
158            case "bottom":
159            default:
160                vAlign = VerticalTextAlignment.BOTTOM;
161        }
162
163        return new BoxTextElement(c, text, boxProvider, box, hAlign, vAlign);
164    }
165
166    public Rectangle getBox() {
167        if (boxProvider != null) {
168            BoxProviderResult result = boxProvider.get();
169            if (!result.isTemporary()) {
170                box = result.getBox();
171                boxProvider = null;
172            }
173            return result.getBox();
174        }
175        return box;
176    }
177
178    public static final BoxTextElement SIMPLE_NODE_TEXT_ELEMSTYLE;
179    static {
180        MultiCascade mc = new MultiCascade();
181        Cascade c = mc.getOrCreateCascade("default");
182        c.put(TEXT, Keyword.AUTO);
183        Node n = new Node();
184        n.put("name", "dummy");
185        SIMPLE_NODE_TEXT_ELEMSTYLE = create(new Environment(n, mc, "default", null), NodeElement.SIMPLE_NODE_ELEMSTYLE.getBoxProvider());
186        if (SIMPLE_NODE_TEXT_ELEMSTYLE == null) throw new AssertionError();
187    }
188
189    /*
190     * Caches the default text color from the preferences.
191     *
192     * FIXME: the cache isn't updated if the user changes the preference during a JOSM
193     * session. There should be preference listener updating this cache.
194     */
195    private static volatile Color DEFAULT_TEXT_COLOR;
196
197    private static void initDefaultParameters() {
198        if (DEFAULT_TEXT_COLOR != null) return;
199        DEFAULT_TEXT_COLOR = PaintColors.TEXT.get();
200    }
201
202    @Override
203    public void paintPrimitive(OsmPrimitive osm, MapPaintSettings settings, StyledMapRenderer painter,
204            boolean selected, boolean outermember, boolean member) {
205        if (osm instanceof Node) {
206            painter.drawBoxText((Node) osm, this);
207        }
208    }
209
210    @Override
211    public boolean equals(Object obj) {
212        if (this == obj) return true;
213        if (obj == null || getClass() != obj.getClass()) return false;
214        if (!super.equals(obj)) return false;
215        BoxTextElement that = (BoxTextElement) obj;
216        return Objects.equals(text, that.text) &&
217                Objects.equals(boxProvider, that.boxProvider) &&
218                Objects.equals(box, that.box) &&
219                hAlign == that.hAlign &&
220                vAlign == that.vAlign;
221    }
222
223    @Override
224    public int hashCode() {
225        return Objects.hash(super.hashCode(), text, boxProvider, box, hAlign, vAlign);
226    }
227
228    @Override
229    public String toString() {
230        return "BoxTextElemStyle{" + super.toString() + ' ' + text.toStringImpl()
231                + " box=" + box + " hAlign=" + hAlign + " vAlign=" + vAlign + '}';
232    }
233}