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