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