001// License: GPL. For details, see LICENSE file.
002package org.openstreetmap.josm.gui.mappaint.styleelement;
003
004import java.util.ArrayList;
005import java.util.Arrays;
006import java.util.Collections;
007import java.util.List;
008import java.util.Objects;
009
010import org.openstreetmap.josm.data.osm.IPrimitive;
011import org.openstreetmap.josm.spi.preferences.Config;
012import org.openstreetmap.josm.spi.preferences.PreferenceChangeEvent;
013import org.openstreetmap.josm.spi.preferences.PreferenceChangedListener;
014import org.openstreetmap.josm.tools.LanguageInfo;
015
016/**
017 * <p>Provides an abstract parent class and three concrete sub classes for various
018 * strategies on how to compose the text label which can be rendered close to a node
019 * or within an area in an OSM map.</p>
020 *
021 * <p>The three strategies below support three rules for composing a label:
022 * <ul>
023 *   <li>{@link StaticLabelCompositionStrategy} - the label is given by a static text
024 *   specified in the MapCSS style file</li>
025 *
026 *   <li>{@link TagLookupCompositionStrategy} - the label is given by the content of a
027 *   tag whose name specified in the MapCSS style file</li>
028 *
029 *   <li>{@link DeriveLabelFromNameTagsCompositionStrategy} - the label is given by the value
030 *   of one of the configured "name tags". The list of relevant name tags can be configured
031 *   in the JOSM preferences
032 *   see the preference options <code>mappaint.nameOrder</code> and <code>mappaint.nameComplementOrder</code>.</li>
033 * </ul>
034 * @since  3987 (creation)
035 * @since 10599 (functional interface)
036 */
037@FunctionalInterface
038public interface LabelCompositionStrategy {
039
040    /**
041     * Replies the text value to be rendered as label for the primitive {@code primitive}.
042     *
043     * @param primitive the primitive
044     *
045     * @return the text value to be rendered or null, if primitive is null or
046     * if no suitable value could be composed
047     */
048    String compose(IPrimitive primitive);
049
050    /**
051     * Strategy where the label is given by a static text specified in the MapCSS style file.
052     */
053    class StaticLabelCompositionStrategy implements LabelCompositionStrategy {
054        private final String defaultLabel;
055
056        public StaticLabelCompositionStrategy(String defaultLabel) {
057            this.defaultLabel = defaultLabel;
058        }
059
060        @Override
061        public String compose(IPrimitive primitive) {
062            return defaultLabel;
063        }
064
065        public String getDefaultLabel() {
066            return defaultLabel;
067        }
068
069        @Override
070        public String toString() {
071            return '{' + getClass().getSimpleName() + " defaultLabel=" + defaultLabel + '}';
072        }
073
074        @Override
075        public int hashCode() {
076            return Objects.hash(defaultLabel);
077        }
078
079        @Override
080        public boolean equals(Object obj) {
081            if (this == obj) return true;
082            if (obj == null || getClass() != obj.getClass()) return false;
083            StaticLabelCompositionStrategy that = (StaticLabelCompositionStrategy) obj;
084            return Objects.equals(defaultLabel, that.defaultLabel);
085        }
086    }
087
088    /**
089     * Strategy where the label is given by the content of a tag whose name specified in the MapCSS style file.
090     */
091    class TagLookupCompositionStrategy implements LabelCompositionStrategy {
092
093        private final String defaultLabelTag;
094
095        public TagLookupCompositionStrategy(String defaultLabelTag) {
096            if (defaultLabelTag != null) {
097                defaultLabelTag = defaultLabelTag.trim();
098                if (defaultLabelTag.isEmpty()) {
099                    defaultLabelTag = null;
100                }
101            }
102            this.defaultLabelTag = defaultLabelTag;
103        }
104
105        @Override
106        public String compose(IPrimitive primitive) {
107            if (defaultLabelTag == null) return null;
108            if (primitive == null) return null;
109            return primitive.get(defaultLabelTag);
110        }
111
112        public String getDefaultLabelTag() {
113            return defaultLabelTag;
114        }
115
116        @Override
117        public String toString() {
118            return '{' + getClass().getSimpleName() + " defaultLabelTag=" + defaultLabelTag + '}';
119        }
120
121        @Override
122        public int hashCode() {
123            return Objects.hash(defaultLabelTag);
124        }
125
126        @Override
127        public boolean equals(Object obj) {
128            if (this == obj) return true;
129            if (obj == null || getClass() != obj.getClass()) return false;
130            TagLookupCompositionStrategy that = (TagLookupCompositionStrategy) obj;
131            return Objects.equals(defaultLabelTag, that.defaultLabelTag);
132        }
133    }
134
135    /**
136     * Strategy where the label is given by the value of one of the configured "name tags".
137     * The list of relevant name tags can be configured in the JOSM preferences
138     * see the preference options <code>mappaint.nameOrder</code> and <code>mappaint.nameComplementOrder</code>
139     */
140    class DeriveLabelFromNameTagsCompositionStrategy implements LabelCompositionStrategy, PreferenceChangedListener {
141
142        /**
143         * The list of default name tags from which a label candidate is derived.
144         */
145        private static final String[] DEFAULT_NAME_TAGS = {
146            "name:" + LanguageInfo.getJOSMLocaleCode(),
147            "name",
148            "int_name",
149            "distance",
150            "ref",
151            "operator",
152            "brand",
153            "addr:housenumber"
154        };
155
156        /**
157         * The list of default name complement tags from which a label candidate is derived.
158         */
159        private static final String[] DEFAULT_NAME_COMPLEMENT_TAGS = {
160            "capacity"
161        };
162
163        private List<String> nameTags = new ArrayList<>();
164        private List<String> nameComplementTags = new ArrayList<>();
165
166        /**
167         * <p>Creates the strategy and initializes its name tags from the preferences.</p>
168         */
169        public DeriveLabelFromNameTagsCompositionStrategy() {
170            Config.getPref().addPreferenceChangeListener(this);
171            initNameTagsFromPreferences();
172        }
173
174        private static List<String> buildNameTags(List<String> nameTags) {
175            List<String> result = new ArrayList<>();
176            if (nameTags != null) {
177                for (String tag: nameTags) {
178                    if (tag == null) {
179                        continue;
180                    }
181                    tag = tag.trim();
182                    if (tag.isEmpty()) {
183                        continue;
184                    }
185                    result.add(tag);
186                }
187            }
188            return result;
189        }
190
191        /**
192         * Sets the name tags to be looked up in order to build up the label.
193         *
194         * @param nameTags the name tags. null values are ignored.
195         */
196        public void setNameTags(List<String> nameTags) {
197            this.nameTags = buildNameTags(nameTags);
198        }
199
200        /**
201         * Sets the name complement tags to be looked up in order to build up the label.
202         *
203         * @param nameComplementTags the name complement tags. null values are ignored.
204         * @since 6541
205         */
206        public void setNameComplementTags(List<String> nameComplementTags) {
207            this.nameComplementTags = buildNameTags(nameComplementTags);
208        }
209
210        /**
211         * Replies an unmodifiable list of the name tags used to compose the label.
212         *
213         * @return the list of name tags
214         */
215        public List<String> getNameTags() {
216            return Collections.unmodifiableList(nameTags);
217        }
218
219        /**
220         * Replies an unmodifiable list of the name complement tags used to compose the label.
221         *
222         * @return the list of name complement tags
223         * @since 6541
224         */
225        public List<String> getNameComplementTags() {
226            return Collections.unmodifiableList(nameComplementTags);
227        }
228
229        /**
230         * Initializes the name tags to use from a list of default name tags (see
231         * {@link #DEFAULT_NAME_TAGS} and {@link #DEFAULT_NAME_COMPLEMENT_TAGS})
232         * and from name tags configured in the preferences using the keys
233         * <code>mappaint.nameOrder</code> and <code>mappaint.nameComplementOrder</code>.
234         */
235        public final void initNameTagsFromPreferences() {
236            if (Config.getPref() == null) {
237                this.nameTags = new ArrayList<>(Arrays.asList(DEFAULT_NAME_TAGS));
238                this.nameComplementTags = new ArrayList<>(Arrays.asList(DEFAULT_NAME_COMPLEMENT_TAGS));
239            } else {
240                this.nameTags = new ArrayList<>(
241                        Config.getPref().getList("mappaint.nameOrder", Arrays.asList(DEFAULT_NAME_TAGS))
242                );
243                this.nameComplementTags = new ArrayList<>(
244                        Config.getPref().getList("mappaint.nameComplementOrder", Arrays.asList(DEFAULT_NAME_COMPLEMENT_TAGS))
245                );
246            }
247        }
248
249        private String getPrimitiveName(IPrimitive n) {
250            StringBuilder name = new StringBuilder();
251            if (!n.hasKeys()) return null;
252            for (String rn : nameTags) {
253                String val = n.get(rn);
254                if (val != null) {
255                    name.append(val);
256                    break;
257                }
258            }
259            for (String rn : nameComplementTags) {
260                String comp = n.get(rn);
261                if (comp != null) {
262                    if (name.length() == 0) {
263                        name.append(comp);
264                    } else {
265                        name.append(" (").append(comp).append(')');
266                    }
267                    break;
268                }
269            }
270            return name.toString();
271        }
272
273        @Override
274        public String compose(IPrimitive primitive) {
275            if (primitive == null) return null;
276            return getPrimitiveName(primitive);
277        }
278
279        @Override
280        public String toString() {
281            return '{' + getClass().getSimpleName() + '}';
282        }
283
284        @Override
285        public void preferenceChanged(PreferenceChangeEvent e) {
286            if (e.getKey() != null && e.getKey().startsWith("mappaint.name")) {
287                initNameTagsFromPreferences();
288            }
289        }
290    }
291}