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