source: josm/trunk/src/org/openstreetmap/josm/gui/DefaultNameFormatter.java @ 5241

Revision 5132, 24.5 KB checked in by simon04, 8 weeks ago (diff)

fix #7513 - Warn non-experts when combining ways with conflicting tags or ways being part of relations

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