source: josm/trunk/src/org/openstreetmap/josm/data/osm/DefaultNameFormatter.java

Last change on this file was 14680, checked in by simon04, 9 days ago

fix #14856 - Add naming tags for relations: water/waterway/wetland

Based on https://taginfo.openstreetmap.org/tags/type=multipolygon#combinations

  • Property svn:eol-style set to native
File size: 24.9 KB
Line 
1// License: GPL. For details, see LICENSE file.
2package org.openstreetmap.josm.data.osm;
3
4import static org.openstreetmap.josm.tools.I18n.tr;
5import static org.openstreetmap.josm.tools.I18n.trc;
6import static org.openstreetmap.josm.tools.I18n.trcLazy;
7import static org.openstreetmap.josm.tools.I18n.trn;
8
9import java.awt.ComponentOrientation;
10import java.util.ArrayList;
11import java.util.Arrays;
12import java.util.Collection;
13import java.util.Collections;
14import java.util.Comparator;
15import java.util.HashSet;
16import java.util.LinkedList;
17import java.util.List;
18import java.util.Locale;
19import java.util.Map;
20import java.util.Set;
21import java.util.stream.Collectors;
22
23import org.openstreetmap.josm.data.coor.LatLon;
24import org.openstreetmap.josm.data.coor.conversion.CoordinateFormatManager;
25import org.openstreetmap.josm.data.osm.history.HistoryNameFormatter;
26import org.openstreetmap.josm.data.osm.history.HistoryNode;
27import org.openstreetmap.josm.data.osm.history.HistoryOsmPrimitive;
28import org.openstreetmap.josm.data.osm.history.HistoryRelation;
29import org.openstreetmap.josm.data.osm.history.HistoryWay;
30import org.openstreetmap.josm.gui.tagging.presets.TaggingPreset;
31import org.openstreetmap.josm.gui.tagging.presets.TaggingPresetNameTemplateList;
32import org.openstreetmap.josm.spi.preferences.Config;
33import org.openstreetmap.josm.tools.AlphanumComparator;
34import org.openstreetmap.josm.tools.I18n;
35import org.openstreetmap.josm.tools.Utils;
36import org.openstreetmap.josm.tools.template_engine.TemplateEngineDataProvider;
37
38/**
39 * This is the default implementation of a {@link NameFormatter} for names of {@link IPrimitive}s
40 * and {@link HistoryOsmPrimitive}s.
41 * @since 12663 (moved from {@code gui} package)
42 * @since 1990
43 */
44public class DefaultNameFormatter implements NameFormatter, HistoryNameFormatter {
45
46    private static DefaultNameFormatter instance;
47
48    private static final List<NameFormatterHook> formatHooks = new LinkedList<>();
49
50    /**
51     * Replies the unique instance of this formatter
52     *
53     * @return the unique instance of this formatter
54     */
55    public static synchronized DefaultNameFormatter getInstance() {
56        if (instance == null) {
57            instance = new DefaultNameFormatter();
58        }
59        return instance;
60    }
61
62    /**
63     * Registers a format hook. Adds the hook at the first position of the format hooks.
64     * (for plugins)
65     *
66     * @param hook the format hook. Ignored if null.
67     */
68    public static void registerFormatHook(NameFormatterHook hook) {
69        if (hook == null) return;
70        if (!formatHooks.contains(hook)) {
71            formatHooks.add(0, hook);
72        }
73    }
74
75    /**
76     * Unregisters a format hook. Removes the hook from the list of format hooks.
77     *
78     * @param hook the format hook. Ignored if null.
79     */
80    public static void unregisterFormatHook(NameFormatterHook hook) {
81        if (hook == null) return;
82        if (formatHooks.contains(hook)) {
83            formatHooks.remove(hook);
84        }
85    }
86
87    /** The default list of tags which are used as naming tags in relations.
88     * A ? prefix indicates a boolean value, for which the key (instead of the value) is used.
89     */
90    private static final String[] DEFAULT_NAMING_TAGS_FOR_RELATIONS = {
91            "name",
92            "ref",
93            //
94            "amenity",
95            "landuse",
96            "leisure",
97            "natural",
98            "public_transport",
99            "restriction",
100            "water",
101            "waterway",
102            "wetland",
103            //
104            ":LocationCode",
105            "note",
106            "?building",
107            "?building:part",
108    };
109
110    /** the current list of tags used as naming tags in relations */
111    private static List<String> namingTagsForRelations;
112
113    /**
114     * Replies the list of naming tags used in relations. The list is given (in this order) by:
115     * <ul>
116     *   <li>by the tag names in the preference <code>relation.nameOrder</code></li>
117     *   <li>by the default tags in {@link #DEFAULT_NAMING_TAGS_FOR_RELATIONS}
118     * </ul>
119     *
120     * @return the list of naming tags used in relations
121     */
122    public static synchronized List<String> getNamingtagsForRelations() {
123        if (namingTagsForRelations == null) {
124            namingTagsForRelations = new ArrayList<>(
125                    Config.getPref().getList("relation.nameOrder", Arrays.asList(DEFAULT_NAMING_TAGS_FOR_RELATIONS))
126                    );
127        }
128        return namingTagsForRelations;
129    }
130
131    /**
132     * Decorates the name of primitive with its id and version, if the preferences
133     * <code>osm-primitives.showid</code> and <code>osm-primitives.showversion</code> are set.
134     * Shows unique id if <code>osm-primitives.showid.new-primitives</code> is set
135     *
136     * @param name the name without the id
137     * @param primitive the primitive
138     */
139    protected void decorateNameWithId(StringBuilder name, IPrimitive primitive) {
140        int version = primitive.getVersion();
141        if (Config.getPref().getBoolean("osm-primitives.showid")) {
142            long id = Config.getPref().getBoolean("osm-primitives.showid.new-primitives") ?
143                    primitive.getUniqueId() : primitive.getId();
144            if (Config.getPref().getBoolean("osm-primitives.showversion") && version > 0) {
145                name.append(tr(" [id: {0}, v{1}]", id, version));
146            } else {
147                name.append(tr(" [id: {0}]", id));
148            }
149        } else if (Config.getPref().getBoolean("osm-primitives.showversion")) {
150            name.append(tr(" [v{0}]", version));
151        }
152    }
153
154    /**
155     * Formats a name for an {@link IPrimitive}.
156     *
157     * @param osm the primitive
158     * @return the name
159     * @since 10991
160     * @since 13564 (signature)
161     */
162    public String format(IPrimitive osm) {
163        if (osm instanceof INode) {
164            return format((INode) osm);
165        } else if (osm instanceof IWay) {
166            return format((IWay<?>) osm);
167        } else if (osm instanceof IRelation) {
168            return format((IRelation<?>) osm);
169        }
170        return null;
171    }
172
173    @Override
174    public String format(INode node) {
175        StringBuilder name = new StringBuilder();
176        if (node.isIncomplete()) {
177            name.append(tr("incomplete"));
178        } else {
179            TaggingPreset preset = TaggingPresetNameTemplateList.getInstance().findPresetTemplate(node);
180            if (preset == null || !(node instanceof TemplateEngineDataProvider)) {
181                String n;
182                if (Config.getPref().getBoolean("osm-primitives.localize-name", true)) {
183                    n = node.getLocalName();
184                } else {
185                    n = node.getName();
186                }
187                if (n == null) {
188                    String s = node.get("addr:housename");
189                    if (s != null) {
190                        /* I18n: name of house as parameter */
191                        n = tr("House {0}", s);
192                    }
193                    if (n == null && (s = node.get("addr:housenumber")) != null) {
194                        String t = node.get("addr:street");
195                        if (t != null) {
196                            /* I18n: house number, street as parameter, number should remain
197                        before street for better visibility */
198                            n = tr("House number {0} at {1}", s, t);
199                        } else {
200                            /* I18n: house number as parameter */
201                            n = tr("House number {0}", s);
202                        }
203                    }
204                }
205
206                if (n == null) {
207                    n = node.isNew() ? tr("node") : Long.toString(node.getId());
208                }
209                name.append(n);
210            } else {
211                preset.nameTemplate.appendText(name, (TemplateEngineDataProvider) node);
212            }
213            if (node.isLatLonKnown() && Config.getPref().getBoolean("osm-primitives.showcoor")) {
214                name.append(" \u200E(")
215                    .append(CoordinateFormatManager.getDefaultFormat().latToString(node)).append(", ")
216                    .append(CoordinateFormatManager.getDefaultFormat().lonToString(node)).append(')');
217            }
218        }
219        decorateNameWithId(name, node);
220
221        String result = name.toString();
222        for (NameFormatterHook hook: formatHooks) {
223            String hookResult = hook.checkFormat(node, result);
224            if (hookResult != null)
225                return hookResult;
226        }
227
228        return result;
229    }
230
231    private final Comparator<INode> nodeComparator = (n1, n2) -> format(n1).compareTo(format(n2));
232
233    @Override
234    public Comparator<INode> getNodeComparator() {
235        return nodeComparator;
236    }
237
238    @Override
239    public String format(IWay<?> way) {
240        StringBuilder name = new StringBuilder();
241
242        char mark;
243        // If current language is left-to-right (almost all languages)
244        if (ComponentOrientation.getOrientation(Locale.getDefault()).isLeftToRight()) {
245            // will insert Left-To-Right Mark to ensure proper display of text in the case when object name is right-to-left
246            mark = '\u200E';
247        } else {
248            // otherwise will insert Right-To-Left Mark to ensure proper display in the opposite case
249            mark = '\u200F';
250        }
251        // Initialize base direction of the string
252        name.append(mark);
253
254        if (way.isIncomplete()) {
255            name.append(tr("incomplete"));
256        } else {
257            TaggingPreset preset = TaggingPresetNameTemplateList.getInstance().findPresetTemplate(way);
258            if (preset == null || !(way instanceof TemplateEngineDataProvider)) {
259                String n;
260                if (Config.getPref().getBoolean("osm-primitives.localize-name", true)) {
261                    n = way.getLocalName();
262                } else {
263                    n = way.getName();
264                }
265                if (n == null) {
266                    n = way.get("ref");
267                }
268                if (n == null) {
269                    n = way.hasKey("highway") ? tr("highway") :
270                        way.hasKey("railway") ? tr("railway") :
271                        way.hasKey("waterway") ? tr("waterway") :
272                        way.hasKey("landuse") ? tr("landuse") : null;
273                }
274                if (n == null) {
275                    String s = way.get("addr:housename");
276                    if (s != null) {
277                        /* I18n: name of house as parameter */
278                        n = tr("House {0}", s);
279                    }
280                    if (n == null && (s = way.get("addr:housenumber")) != null) {
281                        String t = way.get("addr:street");
282                        if (t != null) {
283                            /* I18n: house number, street as parameter, number should remain
284                        before street for better visibility */
285                            n = tr("House number {0} at {1}", s, t);
286                        } else {
287                            /* I18n: house number as parameter */
288                            n = tr("House number {0}", s);
289                        }
290                    }
291                }
292                if (n == null && way.hasKey("building")) {
293                    n = tr("building");
294                }
295                if (n == null || n.isEmpty()) {
296                    n = String.valueOf(way.getId());
297                }
298
299                name.append(n);
300            } else {
301                preset.nameTemplate.appendText(name, (TemplateEngineDataProvider) way);
302            }
303
304            int nodesNo = way.getRealNodesCount();
305            /* note: length == 0 should no longer happen, but leave the bracket code
306               nevertheless, who knows what future brings */
307            /* I18n: count of nodes as parameter */
308            String nodes = trn("{0} node", "{0} nodes", nodesNo, nodesNo);
309            name.append(mark).append(" (").append(nodes).append(')');
310        }
311        decorateNameWithId(name, way);
312
313        String result = name.toString();
314        for (NameFormatterHook hook: formatHooks) {
315            String hookResult = hook.checkFormat(way, result);
316            if (hookResult != null)
317                return hookResult;
318        }
319
320        return result;
321    }
322
323    private final Comparator<IWay<?>> wayComparator = (w1, w2) -> format(w1).compareTo(format(w2));
324
325    @Override
326    public Comparator<IWay<?>> getWayComparator() {
327        return wayComparator;
328    }
329
330    @Override
331    public String format(IRelation<?> relation) {
332        StringBuilder name = new StringBuilder();
333        if (relation.isIncomplete()) {
334            name.append(tr("incomplete"));
335        } else {
336            TaggingPreset preset = TaggingPresetNameTemplateList.getInstance().findPresetTemplate(relation);
337
338            formatRelationNameAndType(relation, name, preset);
339
340            int mbno = relation.getMembersCount();
341            name.append(trn("{0} member", "{0} members", mbno, mbno));
342
343            if (relation.hasIncompleteMembers()) {
344                name.append(", ").append(tr("incomplete"));
345            }
346
347            name.append(')');
348        }
349        decorateNameWithId(name, relation);
350
351        String result = name.toString();
352        for (NameFormatterHook hook: formatHooks) {
353            String hookResult = hook.checkFormat(relation, result);
354            if (hookResult != null)
355                return hookResult;
356        }
357
358        return result;
359    }
360
361    private static StringBuilder formatRelationNameAndType(IRelation<?> relation, StringBuilder result, TaggingPreset preset) {
362        if (preset == null || !(relation instanceof TemplateEngineDataProvider)) {
363            result.append(getRelationTypeName(relation));
364            String relationName = getRelationName(relation);
365            if (relationName == null) {
366                relationName = Long.toString(relation.getId());
367            } else {
368                relationName = '\"' + relationName + '\"';
369            }
370            result.append(" (").append(relationName).append(", ");
371        } else {
372            preset.nameTemplate.appendText(result, (TemplateEngineDataProvider) relation);
373            result.append('(');
374        }
375        return result;
376    }
377
378    private final Comparator<IRelation<?>> relationComparator = (r1, r2) -> {
379        //TODO This doesn't work correctly with formatHooks
380
381        TaggingPreset preset1 = TaggingPresetNameTemplateList.getInstance().findPresetTemplate(r1);
382        TaggingPreset preset2 = TaggingPresetNameTemplateList.getInstance().findPresetTemplate(r2);
383
384        if (preset1 != null || preset2 != null) {
385            String name11 = formatRelationNameAndType(r1, new StringBuilder(), preset1).toString();
386            String name21 = formatRelationNameAndType(r2, new StringBuilder(), preset2).toString();
387
388            int comp1 = AlphanumComparator.getInstance().compare(name11, name21);
389            if (comp1 != 0)
390                return comp1;
391        } else {
392
393            String type1 = getRelationTypeName(r1);
394            String type2 = getRelationTypeName(r2);
395
396            int comp2 = AlphanumComparator.getInstance().compare(type1, type2);
397            if (comp2 != 0)
398                return comp2;
399
400            String name12 = getRelationName(r1);
401            String name22 = getRelationName(r2);
402
403            comp2 = AlphanumComparator.getInstance().compare(name12, name22);
404            if (comp2 != 0)
405                return comp2;
406        }
407
408        int comp3 = Integer.compare(r1.getMembersCount(), r2.getMembersCount());
409        if (comp3 != 0)
410            return comp3;
411
412
413        comp3 = Boolean.compare(r1.hasIncompleteMembers(), r2.hasIncompleteMembers());
414        if (comp3 != 0)
415            return comp3;
416
417        return Long.compare(r1.getUniqueId(), r2.getUniqueId());
418    };
419
420    @Override
421    public Comparator<IRelation<?>> getRelationComparator() {
422        return relationComparator;
423    }
424
425    private static String getRelationTypeName(IRelation<?> relation) {
426        String name = trc("Relation type", relation.get("type"));
427        if (name == null) {
428            name = relation.hasKey("public_transport") ? tr("public transport") : null;
429        }
430        if (name == null) {
431            String building = relation.get("building");
432            if (OsmUtils.isTrue(building)) {
433                name = tr("building");
434            } else if (building != null) {
435                name = tr(building); // translate tag!
436            }
437        }
438        if (name == null) {
439            name = trc("Place type", relation.get("place"));
440        }
441        if (name == null) {
442            name = tr("relation");
443        }
444        String adminLevel = relation.get("admin_level");
445        if (adminLevel != null) {
446            name += '['+adminLevel+']';
447        }
448
449        for (NameFormatterHook hook: formatHooks) {
450            String hookResult = hook.checkRelationTypeName(relation, name);
451            if (hookResult != null)
452                return hookResult;
453        }
454
455        return name;
456    }
457
458    private static String getNameTagValue(IRelation<?> relation, String nameTag) {
459        if ("name".equals(nameTag)) {
460            if (Config.getPref().getBoolean("osm-primitives.localize-name", true))
461                return relation.getLocalName();
462            else
463                return relation.getName();
464        } else if (":LocationCode".equals(nameTag)) {
465            for (String m : relation.keySet()) {
466                if (m.endsWith(nameTag))
467                    return relation.get(m);
468            }
469            return null;
470        } else if (nameTag.startsWith("?") && OsmUtils.isTrue(relation.get(nameTag.substring(1)))) {
471            return tr(nameTag.substring(1));
472        } else if (nameTag.startsWith("?") && OsmUtils.isFalse(relation.get(nameTag.substring(1)))) {
473            return null;
474        } else if (nameTag.startsWith("?")) {
475            return trcLazy(nameTag, I18n.escape(relation.get(nameTag.substring(1))));
476        } else {
477            return trcLazy(nameTag, I18n.escape(relation.get(nameTag)));
478        }
479    }
480
481    private static String getRelationName(IRelation<?> relation) {
482        String nameTag;
483        for (String n : getNamingtagsForRelations()) {
484            nameTag = getNameTagValue(relation, n);
485            if (nameTag != null)
486                return nameTag;
487        }
488        return null;
489    }
490
491    @Override
492    public String format(Changeset changeset) {
493        return tr("Changeset {0}", changeset.getId());
494    }
495
496    /**
497     * Builds a default tooltip text for the primitive <code>primitive</code>.
498     *
499     * @param primitive the primitmive
500     * @return the tooltip text
501     */
502    public String buildDefaultToolTip(IPrimitive primitive) {
503        return buildDefaultToolTip(primitive.getId(), primitive.getKeys());
504    }
505
506    private static String buildDefaultToolTip(long id, Map<String, String> tags) {
507        StringBuilder sb = new StringBuilder(128);
508        sb.append("<html><strong>id</strong>=")
509          .append(id)
510          .append("<br>");
511        List<String> keyList = new ArrayList<>(tags.keySet());
512        Collections.sort(keyList);
513        for (int i = 0; i < keyList.size(); i++) {
514            if (i > 0) {
515                sb.append("<br>");
516            }
517            String key = keyList.get(i);
518            sb.append("<strong>")
519              .append(Utils.escapeReservedCharactersHTML(key))
520              .append("</strong>=");
521            String value = tags.get(key);
522            while (!value.isEmpty()) {
523                sb.append(Utils.escapeReservedCharactersHTML(value.substring(0, Math.min(50, value.length()))));
524                if (value.length() > 50) {
525                    sb.append("<br>");
526                    value = value.substring(50);
527                } else {
528                    value = "";
529                }
530            }
531        }
532        sb.append("</html>");
533        return sb.toString();
534    }
535
536    /**
537     * Decorates the name of primitive with its id, if the preference
538     * <code>osm-primitives.showid</code> is set.
539     *
540     * The id is append to the {@link StringBuilder} passed in <code>name</code>.
541     *
542     * @param name  the name without the id
543     * @param primitive the primitive
544     */
545    protected void decorateNameWithId(StringBuilder name, HistoryOsmPrimitive primitive) {
546        if (Config.getPref().getBoolean("osm-primitives.showid")) {
547            name.append(tr(" [id: {0}]", primitive.getId()));
548        }
549    }
550
551    @Override
552    public String format(HistoryNode node) {
553        StringBuilder sb = new StringBuilder();
554        String name;
555        if (Config.getPref().getBoolean("osm-primitives.localize-name", true)) {
556            name = node.getLocalName();
557        } else {
558            name = node.getName();
559        }
560        if (name == null) {
561            sb.append(node.getId());
562        } else {
563            sb.append(name);
564        }
565        LatLon coord = node.getCoords();
566        if (coord != null) {
567            sb.append(" (")
568            .append(CoordinateFormatManager.getDefaultFormat().latToString(coord))
569            .append(", ")
570            .append(CoordinateFormatManager.getDefaultFormat().lonToString(coord))
571            .append(')');
572        }
573        decorateNameWithId(sb, node);
574        return sb.toString();
575    }
576
577    @Override
578    public String format(HistoryWay way) {
579        StringBuilder sb = new StringBuilder();
580        String name;
581        if (Config.getPref().getBoolean("osm-primitives.localize-name", true)) {
582            name = way.getLocalName();
583        } else {
584            name = way.getName();
585        }
586        if (name != null) {
587            sb.append(name);
588        }
589        if (sb.length() == 0 && way.get("ref") != null) {
590            sb.append(way.get("ref"));
591        }
592        if (sb.length() == 0) {
593            sb.append(
594                    way.hasKey("highway") ? tr("highway") :
595                    way.hasKey("railway") ? tr("railway") :
596                    way.hasKey("waterway") ? tr("waterway") :
597                    way.hasKey("landuse") ? tr("landuse") : ""
598                    );
599        }
600
601        int nodesNo = way.isClosed() ? (way.getNumNodes() -1) : way.getNumNodes();
602        String nodes = trn("{0} node", "{0} nodes", nodesNo, nodesNo);
603        if (sb.length() == 0) {
604            sb.append(way.getId());
605        }
606        /* note: length == 0 should no longer happen, but leave the bracket code
607           nevertheless, who knows what future brings */
608        sb.append((sb.length() > 0) ? (" ("+nodes+')') : nodes);
609        decorateNameWithId(sb, way);
610        return sb.toString();
611    }
612
613    @Override
614    public String format(HistoryRelation relation) {
615        StringBuilder sb = new StringBuilder();
616        String type = relation.get("type");
617        if (type != null) {
618            sb.append(type);
619        } else {
620            sb.append(tr("relation"));
621        }
622        sb.append(" (");
623        String nameTag = null;
624        Set<String> namingTags = new HashSet<>(getNamingtagsForRelations());
625        for (String n : relation.getTags().keySet()) {
626            // #3328: "note " and " note" are name tags too
627            if (namingTags.contains(n.trim())) {
628                if (Config.getPref().getBoolean("osm-primitives.localize-name", true)) {
629                    nameTag = relation.getLocalName();
630                } else {
631                    nameTag = relation.getName();
632                }
633                if (nameTag == null) {
634                    nameTag = relation.get(n);
635                }
636            }
637            if (nameTag != null) {
638                break;
639            }
640        }
641        if (nameTag == null) {
642            sb.append(Long.toString(relation.getId())).append(", ");
643        } else {
644            sb.append('\"').append(nameTag).append("\", ");
645        }
646
647        int mbno = relation.getNumMembers();
648        sb.append(trn("{0} member", "{0} members", mbno, mbno)).append(')');
649
650        decorateNameWithId(sb, relation);
651        return sb.toString();
652    }
653
654    /**
655     * Builds a default tooltip text for an HistoryOsmPrimitive <code>primitive</code>.
656     *
657     * @param primitive the primitmive
658     * @return the tooltip text
659     */
660    public String buildDefaultToolTip(HistoryOsmPrimitive primitive) {
661        return buildDefaultToolTip(primitive.getId(), primitive.getTags());
662    }
663
664    /**
665     * Formats the given collection of primitives as an HTML unordered list.
666     * @param primitives collection of primitives to format
667     * @param maxElements the maximum number of elements to display
668     * @return HTML unordered list
669     */
670    public String formatAsHtmlUnorderedList(Collection<? extends OsmPrimitive> primitives, int maxElements) {
671        Collection<String> displayNames = primitives.stream().map(x -> x.getDisplayName(this)).collect(Collectors.toList());
672        return Utils.joinAsHtmlUnorderedList(Utils.limit(displayNames, maxElements, "..."));
673    }
674
675    /**
676     * Formats the given primitive as an HTML unordered list.
677     * @param primitive primitive to format
678     * @return HTML unordered list
679     */
680    public String formatAsHtmlUnorderedList(OsmPrimitive primitive) {
681        return formatAsHtmlUnorderedList(Collections.singletonList(primitive), 1);
682    }
683}
Note: See TracBrowser for help on using the repository browser.