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    /**
025     * MapCSS text-anchor-horizontal
026     */
027    public enum HorizontalTextAlignment { LEFT, CENTER, RIGHT }
028
029    /**
030     * MapCSS text-anchor-vertical
031     */
032    public enum VerticalTextAlignment { ABOVE, TOP, CENTER, BOTTOM, BELOW }
033
034    /**
035     * Something that provides us with a {@link BoxProviderResult}
036     * @since 10600 (functional interface)
037     */
038    @FunctionalInterface
039    public interface BoxProvider {
040        /**
041         * Compute and get the {@link BoxProviderResult}. The temporary flag is set if the result of the computation may change in the future.
042         * @return The result of the computation.
043         */
044        BoxProviderResult get();
045    }
046
047    /**
048     * A box rectangle with a flag if it is temporary.
049     */
050    public static class BoxProviderResult {
051        private final Rectangle box;
052        private final boolean temporary;
053
054        public BoxProviderResult(Rectangle box, boolean temporary) {
055            this.box = box;
056            this.temporary = temporary;
057        }
058
059        /**
060         * Returns the box.
061         * @return the box
062         */
063        public Rectangle getBox() {
064            return box;
065        }
066
067        /**
068         * Determines if the box can change in future calls of the {@link BoxProvider#get()} method
069         * @return {@code true} if the box can change in future calls of the {@code BoxProvider#get()} method
070         */
071        public boolean isTemporary() {
072            return temporary;
073        }
074    }
075
076    /**
077     * A {@link BoxProvider} that always returns the same non-temporary rectangle
078     */
079    public static class SimpleBoxProvider implements BoxProvider {
080        private final Rectangle box;
081
082        /**
083         * Constructs a new {@code SimpleBoxProvider}.
084         * @param box the box
085         */
086        public SimpleBoxProvider(Rectangle box) {
087            this.box = box;
088        }
089
090        @Override
091        public BoxProviderResult get() {
092            return new BoxProviderResult(box, false);
093        }
094
095        @Override
096        public int hashCode() {
097            return Objects.hash(box);
098        }
099
100        @Override
101        public boolean equals(Object obj) {
102            if (this == obj) return true;
103            if (obj == null || getClass() != obj.getClass()) return false;
104            SimpleBoxProvider that = (SimpleBoxProvider) obj;
105            return Objects.equals(box, that.box);
106        }
107    }
108
109    /**
110     * A rectangle with size 0x0
111     */
112    public static final Rectangle ZERO_BOX = new Rectangle(0, 0, 0, 0);
113
114    /**
115     * The default style a simple node should use for it's text
116     */
117    public static final BoxTextElement SIMPLE_NODE_TEXT_ELEMSTYLE;
118    static {
119        MultiCascade mc = new MultiCascade();
120        Cascade c = mc.getOrCreateCascade("default");
121        c.put(TEXT, Keyword.AUTO);
122        Node n = new Node();
123        n.put("name", "dummy");
124        SIMPLE_NODE_TEXT_ELEMSTYLE = create(new Environment(n, mc, "default", null), NodeElement.SIMPLE_NODE_ELEMSTYLE.getBoxProvider());
125        if (SIMPLE_NODE_TEXT_ELEMSTYLE == null) throw new AssertionError();
126    }
127
128    /**
129     * Caches the default text color from the preferences.
130     *
131     * FIXME: the cache isn't updated if the user changes the preference during a JOSM
132     * session. There should be preference listener updating this cache.
133     */
134    private static volatile Color defaultTextColorCache;
135
136    /**
137     * The text this element should display.
138     */
139    public TextLabel text;
140    // Either boxProvider or box is not null. If boxProvider is different from
141    // null, this means, that the box can still change in future, otherwise
142    // it is fixed.
143    protected BoxProvider boxProvider;
144    protected Rectangle box;
145    /**
146     * The {@link HorizontalTextAlignment} for this text.
147     */
148    public HorizontalTextAlignment hAlign;
149    /**
150     * The {@link VerticalTextAlignment} for this text.
151     */
152    public VerticalTextAlignment vAlign;
153
154    /**
155     * Create a new {@link BoxTextElement}
156     * @param c The current cascade
157     * @param text The text to display
158     * @param boxProvider The box provider to use
159     * @param box The initial box to use.
160     * @param hAlign The {@link HorizontalTextAlignment}
161     * @param vAlign The {@link VerticalTextAlignment}
162     */
163    public BoxTextElement(Cascade c, TextLabel text, BoxProvider boxProvider, Rectangle box,
164            HorizontalTextAlignment hAlign, VerticalTextAlignment vAlign) {
165        super(c, 5f);
166        CheckParameterUtil.ensureParameterNotNull(text);
167        CheckParameterUtil.ensureParameterNotNull(hAlign);
168        CheckParameterUtil.ensureParameterNotNull(vAlign);
169        this.text = text;
170        this.boxProvider = boxProvider;
171        this.box = box == null ? ZERO_BOX : box;
172        this.hAlign = hAlign;
173        this.vAlign = vAlign;
174    }
175
176    /**
177     * Create a new {@link BoxTextElement} with a dynamic box
178     * @param env The MapCSS environment
179     * @param boxProvider The box provider that computes the box.
180     * @return A new {@link BoxTextElement} or <code>null</code> if the creation failed.
181     */
182    public static BoxTextElement create(Environment env, BoxProvider boxProvider) {
183        return create(env, boxProvider, null);
184    }
185
186    /**
187     * Create a new {@link BoxTextElement} with a fixed box
188     * @param env The MapCSS environment
189     * @param box The box
190     * @return A new {@link BoxTextElement} or <code>null</code> if the creation failed.
191     */
192    public static BoxTextElement create(Environment env, Rectangle box) {
193        return create(env, null, box);
194    }
195
196    /**
197     * Create a new {@link BoxTextElement} with a boxprovider and a box.
198     * @param env The MapCSS environment
199     * @param boxProvider The box provider.
200     * @param box The box. Only considered if boxProvider is null.
201     * @return A new {@link BoxTextElement} or <code>null</code> if the creation failed.
202     */
203    public static BoxTextElement create(Environment env, BoxProvider boxProvider, Rectangle box) {
204        initDefaultParameters();
205
206        TextLabel text = TextLabel.create(env, defaultTextColorCache, false);
207        if (text == null) return null;
208        // Skip any primitives that don't have text to draw. (Styles are recreated for any tag change.)
209        // The concrete text to render is not cached in this object, but computed for each
210        // repaint. This way, one BoxTextElement object can be used by multiple primitives (to save memory).
211        if (text.labelCompositionStrategy.compose(env.osm) == null) return null;
212
213        Cascade c = env.mc.getCascade(env.layer);
214
215        HorizontalTextAlignment hAlign;
216        switch (c.get(TEXT_ANCHOR_HORIZONTAL, Keyword.RIGHT, Keyword.class).val) {
217            case "left":
218                hAlign = HorizontalTextAlignment.LEFT;
219                break;
220            case "center":
221                hAlign = HorizontalTextAlignment.CENTER;
222                break;
223            case "right":
224            default:
225                hAlign = HorizontalTextAlignment.RIGHT;
226        }
227        VerticalTextAlignment vAlign;
228        switch (c.get(TEXT_ANCHOR_VERTICAL, Keyword.BOTTOM, Keyword.class).val) {
229            case "above":
230                vAlign = VerticalTextAlignment.ABOVE;
231                break;
232            case "top":
233                vAlign = VerticalTextAlignment.TOP;
234                break;
235            case "center":
236                vAlign = VerticalTextAlignment.CENTER;
237                break;
238            case "below":
239                vAlign = VerticalTextAlignment.BELOW;
240                break;
241            case "bottom":
242            default:
243                vAlign = VerticalTextAlignment.BOTTOM;
244        }
245
246        return new BoxTextElement(c, text, boxProvider, box, hAlign, vAlign);
247    }
248
249    /**
250     * Get the box in which the content should be drawn.
251     * @return The box.
252     */
253    public Rectangle getBox() {
254        if (boxProvider != null) {
255            BoxProviderResult result = boxProvider.get();
256            if (!result.isTemporary()) {
257                box = result.getBox();
258                boxProvider = null;
259            }
260            return result.getBox();
261        }
262        return box;
263    }
264
265    private static void initDefaultParameters() {
266        if (defaultTextColorCache != null) return;
267        defaultTextColorCache = PaintColors.TEXT.get();
268    }
269
270    @Override
271    public void paintPrimitive(OsmPrimitive osm, MapPaintSettings settings, StyledMapRenderer painter,
272            boolean selected, boolean outermember, boolean member) {
273        if (osm instanceof Node) {
274            painter.drawBoxText((Node) osm, this);
275        }
276    }
277
278    @Override
279    public boolean equals(Object obj) {
280        if (this == obj) return true;
281        if (obj == null || getClass() != obj.getClass()) return false;
282        if (!super.equals(obj)) return false;
283        BoxTextElement that = (BoxTextElement) obj;
284        return Objects.equals(text, that.text) &&
285                Objects.equals(boxProvider, that.boxProvider) &&
286                Objects.equals(box, that.box) &&
287                hAlign == that.hAlign &&
288                vAlign == that.vAlign;
289    }
290
291    @Override
292    public int hashCode() {
293        return Objects.hash(super.hashCode(), text, boxProvider, box, hAlign, vAlign);
294    }
295
296    @Override
297    public String toString() {
298        return "BoxTextElemStyle{" + super.toString() + ' ' + text.toStringImpl()
299                + " box=" + box + " hAlign=" + hAlign + " vAlign=" + vAlign + '}';
300    }
301}