001// License: GPL. For details, see LICENSE file.
002package org.openstreetmap.josm.gui;
003
004import static org.openstreetmap.josm.tools.I18n.tr;
005import static org.openstreetmap.josm.tools.I18n.trc;
006import static org.openstreetmap.josm.tools.I18n.trc_lazy;
007import static org.openstreetmap.josm.tools.I18n.trn;
008
009import java.awt.ComponentOrientation;
010import java.util.ArrayList;
011import java.util.Arrays;
012import java.util.Collection;
013import java.util.Collections;
014import java.util.Comparator;
015import java.util.HashSet;
016import java.util.LinkedList;
017import java.util.List;
018import java.util.Locale;
019import java.util.Map;
020import java.util.Set;
021
022import org.openstreetmap.josm.Main;
023import org.openstreetmap.josm.data.coor.CoordinateFormat;
024import org.openstreetmap.josm.data.coor.LatLon;
025import org.openstreetmap.josm.data.osm.Changeset;
026import org.openstreetmap.josm.data.osm.IPrimitive;
027import org.openstreetmap.josm.data.osm.IRelation;
028import org.openstreetmap.josm.data.osm.NameFormatter;
029import org.openstreetmap.josm.data.osm.Node;
030import org.openstreetmap.josm.data.osm.OsmPrimitive;
031import org.openstreetmap.josm.data.osm.OsmUtils;
032import org.openstreetmap.josm.data.osm.Relation;
033import org.openstreetmap.josm.data.osm.Way;
034import org.openstreetmap.josm.data.osm.history.HistoryNameFormatter;
035import org.openstreetmap.josm.data.osm.history.HistoryNode;
036import org.openstreetmap.josm.data.osm.history.HistoryOsmPrimitive;
037import org.openstreetmap.josm.data.osm.history.HistoryRelation;
038import org.openstreetmap.josm.data.osm.history.HistoryWay;
039import org.openstreetmap.josm.gui.tagging.presets.TaggingPreset;
040import org.openstreetmap.josm.gui.tagging.presets.TaggingPresetNameTemplateList;
041import org.openstreetmap.josm.tools.AlphanumComparator;
042import org.openstreetmap.josm.tools.I18n;
043import org.openstreetmap.josm.tools.Utils;
044import org.openstreetmap.josm.tools.Utils.Function;
045
046/**
047 * This is the default implementation of a {@link NameFormatter} for names of {@link OsmPrimitive}s.
048 *
049 */
050public class DefaultNameFormatter implements NameFormatter, HistoryNameFormatter {
051
052    private static DefaultNameFormatter instance;
053
054    private static final List<NameFormatterHook> formatHooks = new LinkedList<>();
055
056    /**
057     * Replies the unique instance of this formatter
058     *
059     * @return the unique instance of this formatter
060     */
061    public static synchronized DefaultNameFormatter getInstance() {
062        if (instance == null) {
063            instance = new DefaultNameFormatter();
064        }
065        return instance;
066    }
067
068    /**
069     * Registers a format hook. Adds the hook at the first position of the format hooks.
070     * (for plugins)
071     *
072     * @param hook the format hook. Ignored if null.
073     */
074    public static void registerFormatHook(NameFormatterHook hook) {
075        if (hook == null) return;
076        if (!formatHooks.contains(hook)) {
077            formatHooks.add(0, hook);
078        }
079    }
080
081    /**
082     * Unregisters a format hook. Removes the hook from the list of format hooks.
083     *
084     * @param hook the format hook. Ignored if null.
085     */
086    public static void unregisterFormatHook(NameFormatterHook hook) {
087        if (hook == null) return;
088        if (formatHooks.contains(hook)) {
089            formatHooks.remove(hook);
090        }
091    }
092
093    /** The default list of tags which are used as naming tags in relations.
094     * A ? prefix indicates a boolean value, for which the key (instead of the value) is used.
095     */
096    protected static final String[] DEFAULT_NAMING_TAGS_FOR_RELATIONS = {"name", "ref", "restriction", "landuse", "natural",
097        "public_transport", ":LocationCode", "note", "?building"};
098
099    /** the current list of tags used as naming tags in relations */
100    private static List<String> namingTagsForRelations;
101
102    /**
103     * Replies the list of naming tags used in relations. The list is given (in this order) by:
104     * <ul>
105     *   <li>by the tag names in the preference <tt>relation.nameOrder</tt></li>
106     *   <li>by the default tags in {@link #DEFAULT_NAMING_TAGS_FOR_RELATIONS}
107     * </ul>
108     *
109     * @return the list of naming tags used in relations
110     */
111    public static synchronized List<String> getNamingtagsForRelations() {
112        if (namingTagsForRelations == null) {
113            namingTagsForRelations = new ArrayList<>(
114                    Main.pref.getCollection("relation.nameOrder", Arrays.asList(DEFAULT_NAMING_TAGS_FOR_RELATIONS))
115                    );
116        }
117        return namingTagsForRelations;
118    }
119
120    /**
121     * Decorates the name of primitive with its id, if the preference
122     * <tt>osm-primitives.showid</tt> is set. Shows unique id if osm-primitives.showid.new-primitives is set
123     *
124     * @param name  the name without the id
125     * @param primitive the primitive
126     */
127    protected void decorateNameWithId(StringBuilder name, IPrimitive primitive) {
128        if (Main.pref.getBoolean("osm-primitives.showid")) {
129            if (Main.pref.getBoolean("osm-primitives.showid.new-primitives")) {
130                name.append(tr(" [id: {0}]", primitive.getUniqueId()));
131            } else {
132                name.append(tr(" [id: {0}]", primitive.getId()));
133            }
134        }
135    }
136
137    /**
138     * Formats a name for a node
139     *
140     * @param node the node
141     * @return the name
142     */
143    @Override
144    public String format(Node node) {
145        StringBuilder name = new StringBuilder();
146        if (node.isIncomplete()) {
147            name.append(tr("incomplete"));
148        } else {
149            TaggingPreset preset = TaggingPresetNameTemplateList.getInstance().findPresetTemplate(node);
150            if (preset == null) {
151                String n;
152                if (Main.pref.getBoolean("osm-primitives.localize-name", true)) {
153                    n = node.getLocalName();
154                } else {
155                    n = node.getName();
156                }
157                if (n == null) {
158                    String s;
159                    if ((s = node.get("addr:housename")) != null) {
160                        /* I18n: name of house as parameter */
161                        n = tr("House {0}", s);
162                    }
163                    if (n == null && (s = node.get("addr:housenumber")) != null) {
164                        String t = node.get("addr:street");
165                        if (t != null) {
166                            /* I18n: house number, street as parameter, number should remain
167                        before street for better visibility */
168                            n =  tr("House number {0} at {1}", s, t);
169                        } else {
170                            /* I18n: house number as parameter */
171                            n = tr("House number {0}", s);
172                        }
173                    }
174                }
175
176                if (n == null) {
177                    n = node.isNew() ? tr("node") : Long.toString(node.getId());
178                }
179                name.append(n);
180            } else {
181                preset.nameTemplate.appendText(name, node);
182            }
183            if (node.getCoor() != null) {
184                name.append(" \u200E(").append(node.getCoor().latToString(CoordinateFormat.getDefaultFormat())).append(", ")
185                    .append(node.getCoor().lonToString(CoordinateFormat.getDefaultFormat())).append(')');
186            }
187        }
188        decorateNameWithId(name, node);
189
190
191        String result = name.toString();
192        for (NameFormatterHook hook: formatHooks) {
193            String hookResult = hook.checkFormat(node, result);
194            if (hookResult != null)
195                return hookResult;
196        }
197
198        return result;
199    }
200
201    private final Comparator<Node> nodeComparator = new Comparator<Node>() {
202        @Override
203        public int compare(Node n1, Node n2) {
204            return format(n1).compareTo(format(n2));
205        }
206    };
207
208    @Override
209    public Comparator<Node> getNodeComparator() {
210        return nodeComparator;
211    }
212
213
214    /**
215     * Formats a name for a way
216     *
217     * @param way the way
218     * @return the name
219     */
220    @Override
221    public String format(Way way) {
222        StringBuilder name = new StringBuilder();
223
224        char mark = 0;
225        // If current language is left-to-right (almost all languages)
226        if (ComponentOrientation.getOrientation(Locale.getDefault()).isLeftToRight()) {
227            // will insert Left-To-Right Mark to ensure proper display of text in the case when object name is right-to-left
228            mark = '\u200E';
229        } else {
230            // otherwise will insert Right-To-Left Mark to ensure proper display in the opposite case
231            mark = '\u200F';
232        }
233        // Initialize base direction of the string
234        name.append(mark);
235
236        if (way.isIncomplete()) {
237            name.append(tr("incomplete"));
238        } else {
239            TaggingPreset preset = TaggingPresetNameTemplateList.getInstance().findPresetTemplate(way);
240            if (preset == null) {
241                String n;
242                if (Main.pref.getBoolean("osm-primitives.localize-name", true)) {
243                    n = way.getLocalName();
244                } else {
245                    n = way.getName();
246                }
247                if (n == null) {
248                    n = way.get("ref");
249                }
250                if (n == null) {
251                    n =     (way.get("highway") != null) ? tr("highway") :
252                                (way.get("railway") != null) ? tr("railway") :
253                                    (way.get("waterway") != null) ? tr("waterway") :
254                                            (way.get("landuse") != null) ? tr("landuse") : null;
255                }
256                if (n == null) {
257                    String s;
258                    if ((s = way.get("addr:housename")) != null) {
259                        /* I18n: name of house as parameter */
260                        n = tr("House {0}", s);
261                    }
262                    if (n == null && (s = way.get("addr:housenumber")) != null) {
263                        String t = way.get("addr:street");
264                        if (t != null) {
265                            /* I18n: house number, street as parameter, number should remain
266                        before street for better visibility */
267                            n =  tr("House number {0} at {1}", s, t);
268                        } else {
269                            /* I18n: house number as parameter */
270                            n = tr("House number {0}", s);
271                        }
272                    }
273                }
274                if (n == null && way.get("building") != null) n = tr("building");
275                if (n == null || n.isEmpty()) {
276                    n = String.valueOf(way.getId());
277                }
278
279                name.append(n);
280            } else {
281                preset.nameTemplate.appendText(name, way);
282            }
283
284            int nodesNo = way.getRealNodesCount();
285            /* note: length == 0 should no longer happen, but leave the bracket code
286               nevertheless, who knows what future brings */
287            /* I18n: count of nodes as parameter */
288            String nodes = trn("{0} node", "{0} nodes", nodesNo, nodesNo);
289            name.append(mark).append(" (").append(nodes).append(')');
290        }
291        decorateNameWithId(name, way);
292
293        String result = name.toString();
294        for (NameFormatterHook hook: formatHooks) {
295            String hookResult = hook.checkFormat(way, result);
296            if (hookResult != null)
297                return hookResult;
298        }
299
300        return result;
301    }
302
303    private final Comparator<Way> wayComparator = new Comparator<Way>() {
304        @Override
305        public int compare(Way w1, Way w2) {
306            return format(w1).compareTo(format(w2));
307        }
308    };
309
310    @Override
311    public Comparator<Way> getWayComparator() {
312        return wayComparator;
313    }
314
315
316    /**
317     * Formats a name for a relation
318     *
319     * @param relation the relation
320     * @return the name
321     */
322    @Override
323    public String format(Relation relation) {
324        StringBuilder name = new StringBuilder();
325        if (relation.isIncomplete()) {
326            name.append(tr("incomplete"));
327        } else {
328            TaggingPreset preset = TaggingPresetNameTemplateList.getInstance().findPresetTemplate(relation);
329
330            formatRelationNameAndType(relation, name, preset);
331
332            int mbno = relation.getMembersCount();
333            name.append(trn("{0} member", "{0} members", mbno, mbno));
334
335            if (relation.hasIncompleteMembers()) {
336                name.append(", ").append(tr("incomplete"));
337            }
338
339            name.append(')');
340        }
341        decorateNameWithId(name, relation);
342
343        String result = name.toString();
344        for (NameFormatterHook hook: formatHooks) {
345            String hookResult = hook.checkFormat(relation, result);
346            if (hookResult != null)
347                return hookResult;
348        }
349
350        return result;
351    }
352
353    private void formatRelationNameAndType(Relation relation, StringBuilder result, TaggingPreset preset) {
354        if (preset == null) {
355            result.append(getRelationTypeName(relation));
356            String relationName = getRelationName(relation);
357            if (relationName == null) {
358                relationName = Long.toString(relation.getId());
359            } else {
360                relationName = '\"' + relationName + '\"';
361            }
362            result.append(" (").append(relationName).append(", ");
363        } else {
364            preset.nameTemplate.appendText(result, relation);
365            result.append('(');
366        }
367    }
368
369    private final Comparator<Relation> relationComparator = new Comparator<Relation>() {
370        @Override
371        public int compare(Relation r1, Relation r2) {
372            //TODO This doesn't work correctly with formatHooks
373
374            TaggingPreset preset1 = TaggingPresetNameTemplateList.getInstance().findPresetTemplate(r1);
375            TaggingPreset preset2 = TaggingPresetNameTemplateList.getInstance().findPresetTemplate(r2);
376
377            if (preset1 != null || preset2 != null) {
378                StringBuilder name1 = new StringBuilder();
379                formatRelationNameAndType(r1, name1, preset1);
380                StringBuilder name2 = new StringBuilder();
381                formatRelationNameAndType(r2, name2, preset2);
382
383                int comp = AlphanumComparator.getInstance().compare(name1.toString(), name2.toString());
384                if (comp != 0)
385                    return comp;
386            } else {
387
388                String type1 = getRelationTypeName(r1);
389                String type2 = getRelationTypeName(r2);
390
391                int comp = AlphanumComparator.getInstance().compare(type1, type2);
392                if (comp != 0)
393                    return comp;
394
395                String name1 = getRelationName(r1);
396                String name2 = getRelationName(r2);
397
398                comp = AlphanumComparator.getInstance().compare(name1, name2);
399                if (comp != 0)
400                    return comp;
401            }
402
403            if (r1.getMembersCount() != r2.getMembersCount())
404                return (r1.getMembersCount() > r2.getMembersCount()) ? 1 : -1;
405
406            int comp = Boolean.valueOf(r1.hasIncompleteMembers()).compareTo(Boolean.valueOf(r2.hasIncompleteMembers()));
407            if (comp != 0)
408                return comp;
409
410            if (r1.getUniqueId() > r2.getUniqueId())
411                return 1;
412            else if (r1.getUniqueId() < r2.getUniqueId())
413                return -1;
414            else
415                return 0;
416        }
417    };
418
419    @Override
420    public Comparator<Relation> getRelationComparator() {
421        return relationComparator;
422    }
423
424    private static String getRelationTypeName(IRelation relation) {
425        String name = trc("Relation type", relation.get("type"));
426        if (name == null) {
427            name = (relation.get("public_transport") != null) ? tr("public transport") : null;
428        }
429        if (name == null) {
430            String building  = relation.get("building");
431            if (OsmUtils.isTrue(building)) {
432                name = tr("building");
433            } else if (building != null) {
434                name = tr(building); // translate tag!
435            }
436        }
437        if (name == null) {
438            name = trc("Place type", relation.get("place"));
439        }
440        if (name == null) {
441            name = tr("relation");
442        }
443        String admin_level = relation.get("admin_level");
444        if (admin_level != null) {
445            name += '['+admin_level+']';
446        }
447
448        for (NameFormatterHook hook: formatHooks) {
449            String hookResult = hook.checkRelationTypeName(relation, name);
450            if (hookResult != null)
451                return hookResult;
452        }
453
454        return name;
455    }
456
457    private static String getNameTagValue(IRelation relation, String nameTag) {
458        if ("name".equals(nameTag)) {
459            if (Main.pref.getBoolean("osm-primitives.localize-name", true))
460                return relation.getLocalName();
461            else
462                return relation.getName();
463        } else if (":LocationCode".equals(nameTag)) {
464            for (String m : relation.keySet()) {
465                if (m.endsWith(nameTag))
466                    return relation.get(m);
467            }
468            return null;
469        } else if (nameTag.startsWith("?") && OsmUtils.isTrue(relation.get(nameTag.substring(1)))) {
470            return tr(nameTag.substring(1));
471        } else if (nameTag.startsWith("?") && OsmUtils.isFalse(relation.get(nameTag.substring(1)))) {
472            return null;
473        } else if (nameTag.startsWith("?")) {
474            return trc_lazy(nameTag, I18n.escape(relation.get(nameTag.substring(1))));
475        } else {
476            return trc_lazy(nameTag, I18n.escape(relation.get(nameTag)));
477        }
478    }
479
480    private String getRelationName(IRelation relation) {
481        String nameTag = null;
482        for (String n : getNamingtagsForRelations()) {
483            nameTag = getNameTagValue(relation, n);
484            if (nameTag != null)
485                return nameTag;
486        }
487        return null;
488    }
489
490    /**
491     * Formats a name for a changeset
492     *
493     * @param changeset the changeset
494     * @return the name
495     */
496    @Override
497    public String format(Changeset changeset) {
498        return tr("Changeset {0}", changeset.getId());
499    }
500
501    /**
502     * Builds a default tooltip text for the primitive <code>primitive</code>.
503     *
504     * @param primitive the primitmive
505     * @return the tooltip text
506     */
507    public String buildDefaultToolTip(IPrimitive primitive) {
508        return buildDefaultToolTip(primitive.getId(), primitive.getKeys());
509    }
510
511    private static String buildDefaultToolTip(long id, Map<String, String> tags) {
512        StringBuilder sb = new StringBuilder();
513        sb.append("<html><strong>id</strong>=")
514          .append(id)
515          .append("<br>");
516        List<String> keyList = new ArrayList<>(tags.keySet());
517        Collections.sort(keyList);
518        for (int i = 0; i < keyList.size(); i++) {
519            if (i > 0) {
520                sb.append("<br>");
521            }
522            String key = keyList.get(i);
523            sb.append("<strong>")
524              .append(key)
525              .append("</strong>=");
526            String value = tags.get(key);
527            while (!value.isEmpty()) {
528                sb.append(value.substring(0, Math.min(50, value.length())));
529                if (value.length() > 50) {
530                    sb.append("<br>");
531                    value = value.substring(50);
532                } else {
533                    value = "";
534                }
535            }
536        }
537        sb.append("</html>");
538        return sb.toString();
539    }
540
541    /**
542     * Decorates the name of primitive with its id, if the preference
543     * <tt>osm-primitives.showid</tt> is set.
544     *
545     * The id is append to the {@link StringBuilder} passed in <code>name</code>.
546     *
547     * @param name  the name without the id
548     * @param primitive the primitive
549     */
550    protected void decorateNameWithId(StringBuilder name, HistoryOsmPrimitive primitive) {
551        if (Main.pref.getBoolean("osm-primitives.showid")) {
552            name.append(tr(" [id: {0}]", primitive.getId()));
553        }
554    }
555
556    /**
557     * Formats a name for a history node
558     *
559     * @param node the node
560     * @return the name
561     */
562    @Override
563    public String format(HistoryNode node) {
564        StringBuilder sb = new StringBuilder();
565        String name;
566        if (Main.pref.getBoolean("osm-primitives.localize-name", true)) {
567            name = node.getLocalName();
568        } else {
569            name = node.getName();
570        }
571        if (name == null) {
572            sb.append(node.getId());
573        } else {
574            sb.append(name);
575        }
576        LatLon coord = node.getCoords();
577        if (coord != null) {
578            sb.append(" (")
579            .append(coord.latToString(CoordinateFormat.getDefaultFormat()))
580            .append(", ")
581            .append(coord.lonToString(CoordinateFormat.getDefaultFormat()))
582            .append(')');
583        }
584        decorateNameWithId(sb, node);
585        return sb.toString();
586    }
587
588    /**
589     * Formats a name for a way
590     *
591     * @param way the way
592     * @return the name
593     */
594    @Override
595    public String format(HistoryWay way) {
596        StringBuilder sb = new StringBuilder();
597        String name;
598        if (Main.pref.getBoolean("osm-primitives.localize-name", true)) {
599            name = way.getLocalName();
600        } else {
601            name = way.getName();
602        }
603        if (name != null) {
604            sb.append(name);
605        }
606        if (sb.length() == 0 && way.get("ref") != null) {
607            sb.append(way.get("ref"));
608        }
609        if (sb.length() == 0) {
610            sb.append(
611                    (way.get("highway") != null) ? tr("highway") :
612                        (way.get("railway") != null) ? tr("railway") :
613                            (way.get("waterway") != null) ? tr("waterway") :
614                                (way.get("landuse") != null) ? tr("landuse") : ""
615                    );
616        }
617
618        int nodesNo = way.isClosed() ? way.getNumNodes() -1 : way.getNumNodes();
619        String nodes = trn("{0} node", "{0} nodes", nodesNo, nodesNo);
620        if (sb.length() == 0) {
621            sb.append(way.getId());
622        }
623        /* note: length == 0 should no longer happen, but leave the bracket code
624           nevertheless, who knows what future brings */
625        sb.append((sb.length() > 0) ? " ("+nodes+')' : nodes);
626        decorateNameWithId(sb, way);
627        return sb.toString();
628    }
629
630    /**
631     * Formats a name for a {@link HistoryRelation})
632     *
633     * @param relation the relation
634     * @return the name
635     */
636    @Override
637    public String format(HistoryRelation relation) {
638        StringBuilder sb = new StringBuilder();
639        if (relation.get("type") != null) {
640            sb.append(relation.get("type"));
641        } else {
642            sb.append(tr("relation"));
643        }
644        sb.append(" (");
645        String nameTag = null;
646        Set<String> namingTags = new HashSet<>(getNamingtagsForRelations());
647        for (String n : relation.getTags().keySet()) {
648            // #3328: "note " and " note" are name tags too
649            if (namingTags.contains(n.trim())) {
650                if (Main.pref.getBoolean("osm-primitives.localize-name", true)) {
651                    nameTag = relation.getLocalName();
652                } else {
653                    nameTag = relation.getName();
654                }
655                if (nameTag == null) {
656                    nameTag = relation.get(n);
657                }
658            }
659            if (nameTag != null) {
660                break;
661            }
662        }
663        if (nameTag == null) {
664            sb.append(Long.toString(relation.getId())).append(", ");
665        } else {
666            sb.append('\"').append(nameTag).append("\", ");
667        }
668
669        int mbno = relation.getNumMembers();
670        sb.append(trn("{0} member", "{0} members", mbno, mbno)).append(')');
671
672        decorateNameWithId(sb, relation);
673        return sb.toString();
674    }
675
676    /**
677     * Builds a default tooltip text for an HistoryOsmPrimitive <code>primitive</code>.
678     *
679     * @param primitive the primitmive
680     * @return the tooltip text
681     */
682    public String buildDefaultToolTip(HistoryOsmPrimitive primitive) {
683        return buildDefaultToolTip(primitive.getId(), primitive.getTags());
684    }
685
686    public String formatAsHtmlUnorderedList(Collection<? extends OsmPrimitive> primitives) {
687        return Utils.joinAsHtmlUnorderedList(Utils.transform(primitives, new Function<OsmPrimitive, String>() {
688
689            @Override
690            public String apply(OsmPrimitive x) {
691                return x.getDisplayName(DefaultNameFormatter.this);
692            }
693        }));
694    }
695
696    public String formatAsHtmlUnorderedList(OsmPrimitive... primitives) {
697        return formatAsHtmlUnorderedList(Arrays.asList(primitives));
698    }
699}