001// License: GPL. For details, see LICENSE file.
002package org.openstreetmap.josm.gui.mappaint.styleelement;
003
004import java.awt.Color;
005import java.awt.Font;
006import java.awt.geom.Point2D;
007import java.util.Objects;
008
009import org.openstreetmap.josm.data.osm.IPrimitive;
010import org.openstreetmap.josm.data.osm.OsmPrimitive;
011import org.openstreetmap.josm.gui.mappaint.Cascade;
012import org.openstreetmap.josm.gui.mappaint.Environment;
013import org.openstreetmap.josm.gui.mappaint.Keyword;
014import org.openstreetmap.josm.gui.mappaint.MapPaintStyles.TagKeyReference;
015import org.openstreetmap.josm.gui.mappaint.StyleKeys;
016import org.openstreetmap.josm.gui.mappaint.styleelement.LabelCompositionStrategy.DeriveLabelFromNameTagsCompositionStrategy;
017import org.openstreetmap.josm.gui.mappaint.styleelement.LabelCompositionStrategy.StaticLabelCompositionStrategy;
018import org.openstreetmap.josm.gui.mappaint.styleelement.LabelCompositionStrategy.TagLookupCompositionStrategy;
019import org.openstreetmap.josm.tools.CheckParameterUtil;
020import org.openstreetmap.josm.tools.ColorHelper;
021import org.openstreetmap.josm.tools.RotationAngle;
022
023/**
024 * Represents the rendering style for a textual label placed somewhere on the map.
025 * @since 3880
026 */
027public class TextLabel implements StyleKeys {
028    /**
029     * The default strategy to use when determining the label of a element.
030     */
031    public static final LabelCompositionStrategy AUTO_LABEL_COMPOSITION_STRATEGY = new DeriveLabelFromNameTagsCompositionStrategy();
032
033    /**
034     * The strategy for building the actual label value for a given a {@link OsmPrimitive}.
035     * Check for null before accessing.
036     */
037    public LabelCompositionStrategy labelCompositionStrategy;
038    /**
039     * the font to be used when rendering
040     */
041    public Font font;
042    /**
043     * The rotation angle to be used when rendering
044     */
045    public RotationAngle rotationAngle;
046    /**
047     * The color to draw the text in, includes alpha.
048     */
049    public Color color;
050    /**
051     * The radius of the halo effect.
052     */
053    public Float haloRadius;
054    /**
055     * The color of the halo effect.
056     */
057    public Color haloColor;
058
059    /**
060     * Creates a new text element
061     *
062     * @param strategy the strategy indicating how the text is composed for a specific {@link OsmPrimitive} to be rendered.
063     * If null, no label is rendered.
064     * @param font the font to be used. Must not be null.
065     * @param rotationAngle the rotation angle to be used. Must not be null.
066     * @param color the color to be used. Must not be null
067     * @param haloRadius halo radius
068     * @param haloColor halo color
069     */
070    protected TextLabel(LabelCompositionStrategy strategy, Font font, RotationAngle rotationAngle,
071                        Color color, Float haloRadius, Color haloColor) {
072        this.labelCompositionStrategy = strategy;
073        this.font = Objects.requireNonNull(font, "font");
074        this.rotationAngle = Objects.requireNonNull(rotationAngle, "rotationAngle");
075        this.color = Objects.requireNonNull(color, "color");
076        this.haloRadius = haloRadius;
077        this.haloColor = haloColor;
078    }
079
080    /**
081     * Copy constructor
082     *
083     * @param other the other element.
084     */
085    public TextLabel(TextLabel other) {
086        this.labelCompositionStrategy = other.labelCompositionStrategy;
087        this.font = other.font;
088        this.rotationAngle = other.rotationAngle;
089        this.color = other.color;
090        this.haloColor = other.haloColor;
091        this.haloRadius = other.haloRadius;
092    }
093
094    /**
095     * Derives a suitable label composition strategy from the style properties in {@code c}.
096     *
097     * @param c the style properties
098     * @param defaultAnnotate whether to return {@link #AUTO_LABEL_COMPOSITION_STRATEGY} if not strategy is found
099     * @return the label composition strategy, or {@code null}
100     */
101    protected static LabelCompositionStrategy buildLabelCompositionStrategy(Cascade c, boolean defaultAnnotate) {
102        /*
103         * If the cascade includes a TagKeyReference we will lookup the rendered label
104         * from a tag value.
105         */
106        TagKeyReference tkr = c.get(TEXT, null, TagKeyReference.class, true);
107        if (tkr != null)
108            return new TagLookupCompositionStrategy(tkr.key);
109
110        /*
111         * Check whether the label composition strategy is given by a keyword
112         */
113        Keyword keyword = c.get(TEXT, null, Keyword.class, true);
114        if (Keyword.AUTO.equals(keyword))
115            return AUTO_LABEL_COMPOSITION_STRATEGY;
116
117        /*
118         * Do we have a static text label?
119         */
120        String text = c.get(TEXT, null, String.class, true);
121        if (text != null)
122            return new StaticLabelCompositionStrategy(text);
123        return defaultAnnotate ? AUTO_LABEL_COMPOSITION_STRATEGY : null;
124    }
125
126    /**
127     * Builds a text element from style properties in {@code c} and the
128     * default text color {@code defaultTextColor}
129     *
130     * @param env the environment
131     * @param defaultTextColor the default text color. Must not be null.
132     * @param defaultAnnotate true, if a text label shall be rendered by default, even if the style sheet
133     *   doesn't include respective style declarations
134     * @return the text element or null, if the style properties don't include
135     * properties for text rendering
136     * @throws IllegalArgumentException if {@code defaultTextColor} is null
137     */
138    public static TextLabel create(Environment env, Color defaultTextColor, boolean defaultAnnotate) {
139        CheckParameterUtil.ensureParameterNotNull(defaultTextColor);
140        Cascade c = env.getCascade();
141
142        LabelCompositionStrategy strategy = buildLabelCompositionStrategy(c, defaultAnnotate);
143        if (strategy == null) return null;
144        String s = strategy.compose(env.osm);
145        if (s == null) return null;
146        Font font = StyleElement.getFont(c, s);
147        RotationAngle rotationAngle = NodeElement.createTextRotationAngle(env);
148
149        Color color = c.get(TEXT_COLOR, defaultTextColor, Color.class);
150        float alpha = c.get(TEXT_OPACITY, 1f, Float.class);
151        color = ColorHelper.alphaMultiply(color, alpha);
152
153        Float haloRadius = c.get(TEXT_HALO_RADIUS, null, Float.class);
154        if (haloRadius != null && haloRadius <= 0) {
155            haloRadius = null;
156        }
157        Color haloColor = null;
158        if (haloRadius != null) {
159            haloColor = c.get(TEXT_HALO_COLOR, ColorHelper.complement(color), Color.class);
160            float haloAlphaFactor = c.get(TEXT_HALO_OPACITY, 1f, Float.class);
161            haloColor = ColorHelper.alphaMultiply(haloColor, haloAlphaFactor);
162        }
163
164        return new TextLabel(strategy, font, rotationAngle, color, haloRadius, haloColor);
165    }
166
167    /**
168     * Gets the text-offset property from a cascade
169     * @param c The cascade
170     * @return The text offset property
171     */
172    public static Point2D getTextOffset(Cascade c) {
173        float xOffset = 0;
174        float yOffset = 0;
175        float[] offset = c.get(TEXT_OFFSET, null, float[].class);
176        if (offset != null) {
177            if (offset.length == 1) {
178                yOffset = offset[0];
179            } else if (offset.length >= 2) {
180                xOffset = offset[0];
181                yOffset = offset[1];
182            }
183        }
184        xOffset = c.get(TEXT_OFFSET_X, xOffset, Float.class);
185        yOffset = c.get(TEXT_OFFSET_Y, yOffset, Float.class);
186        return new Point2D.Double(xOffset, yOffset);
187    }
188
189    /**
190     * Replies the label to be rendered for the primitive {@code osm}.
191     *
192     * @param osm the OSM object
193     * @return the label, or null, if {@code osm} is null or if no label can be
194     * derived for {@code osm}
195     */
196    public String getString(IPrimitive osm) {
197        if (labelCompositionStrategy == null) return null;
198        return labelCompositionStrategy.compose(osm);
199    }
200
201    @Override
202    public String toString() {
203        return "TextLabel{" + toStringImpl() + '}';
204    }
205
206    protected String toStringImpl() {
207        StringBuilder sb = new StringBuilder(96);
208        sb.append("labelCompositionStrategy=").append(labelCompositionStrategy)
209          .append(" font=").append(font)
210          .append(" rotationAngle=").append(rotationAngle)
211          .append(" color=").append(ColorHelper.color2html(color));
212        if (haloRadius != null) {
213            sb.append(" haloRadius=").append(haloRadius)
214              .append(" haloColor=").append(haloColor);
215        }
216        return sb.toString();
217    }
218
219    @Override
220    public int hashCode() {
221        return Objects.hash(labelCompositionStrategy, font, rotationAngle, color, haloRadius, haloColor);
222    }
223
224    @Override
225    public boolean equals(Object obj) {
226        if (this == obj) return true;
227        if (obj == null || getClass() != obj.getClass()) return false;
228        TextLabel textLabel = (TextLabel) obj;
229        return Objects.equals(labelCompositionStrategy, textLabel.labelCompositionStrategy) &&
230                Objects.equals(font, textLabel.font) &&
231                Objects.equals(rotationAngle, textLabel.rotationAngle) &&
232                Objects.equals(color, textLabel.color) &&
233                Objects.equals(haloRadius, textLabel.haloRadius) &&
234                Objects.equals(haloColor, textLabel.haloColor);
235    }
236}