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            initNameTagsFromPreferences();
171        }
172
173        private static List<String> buildNameTags(List<String> nameTags) {
174            List<String> result = new ArrayList<>();
175            if (nameTags != null) {
176                for (String tag: nameTags) {
177                    if (tag == null) {
178                        continue;
179                    }
180                    tag = tag.trim();
181                    if (tag.isEmpty()) {
182                        continue;
183                    }
184                    result.add(tag);
185                }
186            }
187            return result;
188        }
189
190        /**
191         * Sets the name tags to be looked up in order to build up the label.
192         *
193         * @param nameTags the name tags. null values are ignored.
194         */
195        public void setNameTags(List<String> nameTags) {
196            this.nameTags = buildNameTags(nameTags);
197        }
198
199        /**
200         * Sets the name complement tags to be looked up in order to build up the label.
201         *
202         * @param nameComplementTags the name complement tags. null values are ignored.
203         * @since 6541
204         */
205        public void setNameComplementTags(List<String> nameComplementTags) {
206            this.nameComplementTags = buildNameTags(nameComplementTags);
207        }
208
209        /**
210         * Replies an unmodifiable list of the name tags used to compose the label.
211         *
212         * @return the list of name tags
213         */
214        public List<String> getNameTags() {
215            return Collections.unmodifiableList(nameTags);
216        }
217
218        /**
219         * Replies an unmodifiable list of the name complement tags used to compose the label.
220         *
221         * @return the list of name complement tags
222         * @since 6541
223         */
224        public List<String> getNameComplementTags() {
225            return Collections.unmodifiableList(nameComplementTags);
226        }
227
228        /**
229         * Initializes the name tags to use from a list of default name tags (see
230         * {@link #DEFAULT_NAME_TAGS} and {@link #DEFAULT_NAME_COMPLEMENT_TAGS})
231         * and from name tags configured in the preferences using the keys
232         * <code>mappaint.nameOrder</code> and <code>mappaint.nameComplementOrder</code>.
233         */
234        public final void initNameTagsFromPreferences() {
235            if (Config.getPref() == null) {
236                this.nameTags = new ArrayList<>(Arrays.asList(DEFAULT_NAME_TAGS));
237                this.nameComplementTags = new ArrayList<>(Arrays.asList(DEFAULT_NAME_COMPLEMENT_TAGS));
238            } else {
239                this.nameTags = new ArrayList<>(
240                        Config.getPref().getList("mappaint.nameOrder", Arrays.asList(DEFAULT_NAME_TAGS))
241                );
242                this.nameComplementTags = new ArrayList<>(
243                        Config.getPref().getList("mappaint.nameComplementOrder", Arrays.asList(DEFAULT_NAME_COMPLEMENT_TAGS))
244                );
245            }
246        }
247
248        private String getPrimitiveName(IPrimitive n) {
249            StringBuilder name = new StringBuilder();
250            if (!n.hasKeys()) return null;
251            for (String rn : nameTags) {
252                String val = n.get(rn);
253                if (val != null) {
254                    name.append(val);
255                    break;
256                }
257            }
258            for (String rn : nameComplementTags) {
259                String comp = n.get(rn);
260                if (comp != null) {
261                    if (name.length() == 0) {
262                        name.append(comp);
263                    } else {
264                        name.append(" (").append(comp).append(')');
265                    }
266                    break;
267                }
268            }
269            return name.toString();
270        }
271
272        @Override
273        public String compose(IPrimitive primitive) {
274            if (primitive == null) return null;
275            return getPrimitiveName(primitive);
276        }
277
278        @Override
279        public String toString() {
280            return '{' + getClass().getSimpleName() + '}';
281        }
282
283        @Override
284        public void preferenceChanged(PreferenceChangeEvent e) {
285            if (e.getKey() != null && e.getKey().startsWith("mappaint.name")) {
286                initNameTagsFromPreferences();
287            }
288        }
289    }
290}