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

Last change on this file since 12735 was 12735, checked in by bastiK, 6 weeks ago

see #15229 - move CoordinateFormat code out of LatLon class

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