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