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