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