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.TaggingPreset;
040import org.openstreetmap.josm.gui.tagging.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 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    public 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 =  null;
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 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                {
159                    String s;
160                    if((s = node.get("addr:housename")) != null) {
161                        /* I18n: name of house as parameter */
162                        n = tr("House {0}", s);
163                    }
164                    if(n == null && (s = node.get("addr:housenumber")) != null) {
165                        String t = node.get("addr:street");
166                        if(t != null) {
167                            /* I18n: house number, street as parameter, number should remain
168                        before street for better visibility */
169                            n =  tr("House number {0} at {1}", s, t);
170                        }
171                        else {
172                            /* I18n: house number as parameter */
173                            n = tr("House number {0}", s);
174                        }
175                    }
176                }
177
178                if (n == null) {
179                    n = node.isNew() ? tr("node") : Long.toString(node.getId());
180                }
181                name.append(n);
182            } else {
183                preset.nameTemplate.appendText(name, node);
184            }
185            if (node.getCoor() != null) {
186                name.append(" \u200E(").append(node.getCoor().latToString(CoordinateFormat.getDefaultFormat())).append(", ").append(node.getCoor().lonToString(CoordinateFormat.getDefaultFormat())).append(")");
187            }
188        }
189        decorateNameWithId(name, node);
190
191
192        String result = name.toString();
193        for (NameFormatterHook hook: formatHooks) {
194            String hookResult = hook.checkFormat(node, result);
195            if (hookResult != null)
196                return hookResult;
197        }
198
199        return result;
200    }
201
202    private final Comparator<Node> nodeComparator = new Comparator<Node>() {
203        @Override
204        public int compare(Node n1, Node n2) {
205            return format(n1).compareTo(format(n2));
206        }
207    };
208
209    @Override
210    public Comparator<Node> getNodeComparator() {
211        return nodeComparator;
212    }
213
214
215    /**
216     * Formats a name for a way
217     *
218     * @param way the way
219     * @return the name
220     */
221    @Override
222    public String format(Way way) {
223        StringBuilder name = new StringBuilder();
224
225        char mark = 0;
226        // If current language is left-to-right (almost all languages)
227        if (ComponentOrientation.getOrientation(Locale.getDefault()).isLeftToRight()) {
228            // will insert Left-To-Right Mark to ensure proper display of text in the case when object name is right-to-left
229            mark = '\u200E';
230        } else {
231            // otherwise will insert Right-To-Left Mark to ensure proper display in the opposite case
232            mark = '\u200F';
233        }
234        // Initialize base direction of the string
235        name.append(mark);
236
237        if (way.isIncomplete()) {
238            name.append(tr("incomplete"));
239        } else {
240            TaggingPreset preset = TaggingPresetNameTemplateList.getInstance().findPresetTemplate(way);
241            if (preset == null) {
242                String n;
243                if (Main.pref.getBoolean("osm-primitives.localize-name", true)) {
244                    n = way.getLocalName();
245                } else {
246                    n = way.getName();
247                }
248                if (n == null) {
249                    n = way.get("ref");
250                }
251                if (n == null) {
252                    n =     (way.get("highway") != null) ? tr("highway") :
253                                (way.get("railway") != null) ? tr("railway") :
254                                    (way.get("waterway") != null) ? tr("waterway") :
255                                            (way.get("landuse") != null) ? tr("landuse") : null;
256                }
257                if (n == null) {
258                    String s;
259                    if((s = way.get("addr:housename")) != null) {
260                        /* I18n: name of house as parameter */
261                        n = tr("House {0}", s);
262                    }
263                    if(n == null && (s = way.get("addr:housenumber")) != null) {
264                        String t = way.get("addr:street");
265                        if(t != null) {
266                            /* I18n: house number, street as parameter, number should remain
267                        before street for better visibility */
268                            n =  tr("House number {0} at {1}", s, t);
269                        }
270                        else {
271                            /* I18n: house number as parameter */
272                            n = tr("House number {0}", s);
273                        }
274                    }
275                }
276                if(n == null && way.get("building") != null) n = tr("building");
277                if(n == null || n.length() == 0) {
278                    n = String.valueOf(way.getId());
279                }
280
281                name.append(n);
282            } else {
283                preset.nameTemplate.appendText(name, way);
284            }
285
286            int nodesNo = way.getRealNodesCount();
287            /* note: length == 0 should no longer happen, but leave the bracket code
288               nevertheless, who knows what future brings */
289            /* I18n: count of nodes as parameter */
290            String nodes = trn("{0} node", "{0} nodes", nodesNo, nodesNo);
291            name.append(mark).append(" (").append(nodes).append(")");
292        }
293        decorateNameWithId(name, way);
294
295        String result = name.toString();
296        for (NameFormatterHook hook: formatHooks) {
297            String hookResult = hook.checkFormat(way, result);
298            if (hookResult != null)
299                return hookResult;
300        }
301
302        return result;
303    }
304
305    private final Comparator<Way> wayComparator = new Comparator<Way>() {
306        @Override
307        public int compare(Way w1, Way w2) {
308            return format(w1).compareTo(format(w2));
309        }
310    };
311
312    @Override
313    public Comparator<Way> getWayComparator() {
314        return wayComparator;
315    }
316
317
318    /**
319     * Formats a name for a relation
320     *
321     * @param relation the relation
322     * @return the name
323     */
324    @Override
325    public String format(Relation relation) {
326        StringBuilder name = new StringBuilder();
327        if (relation.isIncomplete()) {
328            name.append(tr("incomplete"));
329        } else {
330            TaggingPreset preset = TaggingPresetNameTemplateList.getInstance().findPresetTemplate(relation);
331
332            formatRelationNameAndType(relation, name, preset);
333
334            int mbno = relation.getMembersCount();
335            name.append(trn("{0} member", "{0} members", mbno, mbno));
336
337            if (relation.hasIncompleteMembers()) {
338                name.append(", ").append(tr("incomplete"));
339            }
340
341            name.append(")");
342        }
343        decorateNameWithId(name, relation);
344
345        String result = name.toString();
346        for (NameFormatterHook hook: formatHooks) {
347            String hookResult = hook.checkFormat(relation, result);
348            if (hookResult != null)
349                return hookResult;
350        }
351
352        return result;
353    }
354
355    private void formatRelationNameAndType(Relation relation, StringBuilder result, TaggingPreset preset) {
356        if (preset == null) {
357            result.append(getRelationTypeName(relation));
358            String relationName = getRelationName(relation);
359            if (relationName == null) {
360                relationName = Long.toString(relation.getId());
361            } else {
362                relationName = "\"" + relationName + "\"";
363            }
364            result.append(" (").append(relationName).append(", ");
365        } else {
366            preset.nameTemplate.appendText(result, relation);
367            result.append("(");
368        }
369    }
370
371    private final Comparator<Relation> relationComparator = new Comparator<Relation>() {
372        @Override
373        public int compare(Relation r1, Relation r2) {
374            //TODO This doesn't work correctly with formatHooks
375
376            TaggingPreset preset1 = TaggingPresetNameTemplateList.getInstance().findPresetTemplate(r1);
377            TaggingPreset preset2 = TaggingPresetNameTemplateList.getInstance().findPresetTemplate(r2);
378
379            if (preset1 != null || preset2 != null) {
380                StringBuilder name1 = new StringBuilder();
381                formatRelationNameAndType(r1, name1, preset1);
382                StringBuilder name2 = new StringBuilder();
383                formatRelationNameAndType(r2, name2, preset2);
384
385                int comp = AlphanumComparator.getInstance().compare(name1.toString(), name2.toString());
386                if (comp != 0)
387                    return comp;
388            } else {
389
390                String type1 = getRelationTypeName(r1);
391                String type2 = getRelationTypeName(r2);
392
393                int comp = AlphanumComparator.getInstance().compare(type1, type2);
394                if (comp != 0)
395                    return comp;
396
397                String name1 = getRelationName(r1);
398                String name2 = getRelationName(r2);
399
400                comp = AlphanumComparator.getInstance().compare(name1, name2);
401                if (comp != 0)
402                    return comp;
403            }
404
405            if (r1.getMembersCount() != r2.getMembersCount())
406                return (r1.getMembersCount() > r2.getMembersCount())?1:-1;
407
408            int comp = Boolean.valueOf(r1.hasIncompleteMembers()).compareTo(Boolean.valueOf(r2.hasIncompleteMembers()));
409            if (comp != 0)
410                return comp;
411
412            if (r1.getUniqueId() > r2.getUniqueId())
413                return 1;
414            else if (r1.getUniqueId() < r2.getUniqueId())
415                return -1;
416            else
417                return 0;
418        }
419    };
420
421    @Override
422    public Comparator<Relation> getRelationComparator() {
423        return relationComparator;
424    }
425
426    private String getRelationTypeName(IRelation relation) {
427        String name = trc("Relation type", relation.get("type"));
428        if (name == null) {
429            name = (relation.get("public_transport") != null) ? tr("public transport") : null;
430        }
431        if (name == null) {
432            String building  = relation.get("building");
433            if (OsmUtils.isTrue(building)) {
434                name = tr("building");
435            } else if(building != null)
436            {
437                name = tr(building); // translate tag!
438            }
439        }
440        if (name == null) {
441            name = trc("Place type", relation.get("place"));
442        }
443        if (name == null) {
444            name = tr("relation");
445        }
446        String admin_level = relation.get("admin_level");
447        if (admin_level != null) {
448            name += "["+admin_level+"]";
449        }
450
451        for (NameFormatterHook hook: formatHooks) {
452            String hookResult = hook.checkRelationTypeName(relation, name);
453            if (hookResult != null)
454                return hookResult;
455        }
456
457        return name;
458    }
459
460    private String getNameTagValue(IRelation relation, String nameTag) {
461        if ("name".equals(nameTag)) {
462            if (Main.pref.getBoolean("osm-primitives.localize-name", true))
463                return relation.getLocalName();
464            else
465                return relation.getName();
466        } else if (":LocationCode".equals(nameTag)) {
467            for (String m : relation.keySet()) {
468                if (m.endsWith(nameTag))
469                    return relation.get(m);
470            }
471            return null;
472        } else if (nameTag.startsWith("?") && OsmUtils.isTrue(relation.get(nameTag.substring(1)))) {
473            return tr(nameTag.substring(1));
474        } else if (nameTag.startsWith("?") && OsmUtils.isFalse(relation.get(nameTag.substring(1)))) {
475            return null;
476        } else if (nameTag.startsWith("?")) {
477            return trc_lazy(nameTag, I18n.escape(relation.get(nameTag.substring(1))));
478        } else {
479            return trc_lazy(nameTag, I18n.escape(relation.get(nameTag)));
480        }
481    }
482
483    private String getRelationName(IRelation relation) {
484        String nameTag = null;
485        for (String n : getNamingtagsForRelations()) {
486            nameTag = getNameTagValue(relation, n);
487            if (nameTag != null)
488                return nameTag;
489        }
490        return null;
491    }
492
493    /**
494     * Formats a name for a changeset
495     *
496     * @param changeset the changeset
497     * @return the name
498     */
499    @Override
500    public String format(Changeset changeset) {
501        return tr("Changeset {0}",changeset.getId());
502    }
503
504    /**
505     * Builds a default tooltip text for the primitive <code>primitive</code>.
506     *
507     * @param primitive the primitmive
508     * @return the tooltip text
509     */
510    public String buildDefaultToolTip(IPrimitive primitive) {
511        return buildDefaultToolTip(primitive.getId(), primitive.getKeys());
512    }
513
514    private String buildDefaultToolTip(long id, Map<String, String> tags) {
515        StringBuilder sb = new StringBuilder();
516        sb.append("<html>");
517        sb.append("<strong>id</strong>=")
518        .append(id)
519        .append("<br>");
520        List<String> keyList = new ArrayList<>(tags.keySet());
521        Collections.sort(keyList);
522        for (int i = 0; i < keyList.size(); i++) {
523            if (i > 0) {
524                sb.append("<br>");
525            }
526            String key = keyList.get(i);
527            sb.append("<strong>")
528            .append(key)
529            .append("</strong>")
530            .append("=");
531            String value = tags.get(key);
532            while(value.length() != 0) {
533                sb.append(value.substring(0,Math.min(50, value.length())));
534                if (value.length() > 50) {
535                    sb.append("<br>");
536                    value = value.substring(50);
537                } else {
538                    value = "";
539                }
540            }
541        }
542        sb.append("</html>");
543        return sb.toString();
544    }
545
546    /**
547     * Decorates the name of primitive with its id, if the preference
548     * <tt>osm-primitives.showid</tt> is set.
549     *
550     * The id is append to the {@link StringBuilder} passed in <code>name</code>.
551     *
552     * @param name  the name without the id
553     * @param primitive the primitive
554     */
555    protected void decorateNameWithId(StringBuilder name, HistoryOsmPrimitive primitive) {
556        if (Main.pref.getBoolean("osm-primitives.showid")) {
557            name.append(tr(" [id: {0}]", primitive.getId()));
558        }
559    }
560
561    /**
562     * Formats a name for a history node
563     *
564     * @param node the node
565     * @return the name
566     */
567    @Override
568    public String format(HistoryNode node) {
569        StringBuilder sb = new StringBuilder();
570        String name;
571        if (Main.pref.getBoolean("osm-primitives.localize-name", true)) {
572            name = node.getLocalName();
573        } else {
574            name = node.getName();
575        }
576        if (name == null) {
577            sb.append(node.getId());
578        } else {
579            sb.append(name);
580        }
581        LatLon coord = node.getCoords();
582        if (coord != null) {
583            sb.append(" (")
584            .append(coord.latToString(CoordinateFormat.getDefaultFormat()))
585            .append(", ")
586            .append(coord.lonToString(CoordinateFormat.getDefaultFormat()))
587            .append(")");
588        }
589        decorateNameWithId(sb, node);
590        return sb.toString();
591    }
592
593    /**
594     * Formats a name for a way
595     *
596     * @param way the way
597     * @return the name
598     */
599    @Override
600    public String format(HistoryWay way) {
601        StringBuilder sb = new StringBuilder();
602        String name;
603        if (Main.pref.getBoolean("osm-primitives.localize-name", true)) {
604            name = way.getLocalName();
605        } else {
606            name = way.getName();
607        }
608        if (name != null) {
609            sb.append(name);
610        }
611        if (sb.length() == 0 && way.get("ref") != null) {
612            sb.append(way.get("ref"));
613        }
614        if (sb.length() == 0) {
615            sb.append(
616                    (way.get("highway") != null) ? tr("highway") :
617                        (way.get("railway") != null) ? tr("railway") :
618                            (way.get("waterway") != null) ? tr("waterway") :
619                                (way.get("landuse") != null) ? tr("landuse") : ""
620                    );
621        }
622
623        int nodesNo = way.isClosed() ? way.getNumNodes() -1 : way.getNumNodes();
624        String nodes = trn("{0} node", "{0} nodes", nodesNo, nodesNo);
625        if(sb.length() == 0 ) {
626            sb.append(way.getId());
627        }
628        /* note: length == 0 should no longer happen, but leave the bracket code
629           nevertheless, who knows what future brings */
630        sb.append((sb.length() > 0) ? " ("+nodes+")" : nodes);
631        decorateNameWithId(sb, way);
632        return sb.toString();
633    }
634
635    /**
636     * Formats a name for a {@link HistoryRelation})
637     *
638     * @param relation the relation
639     * @return the name
640     */
641    @Override
642    public String format(HistoryRelation relation) {
643        StringBuilder sb = new StringBuilder();
644        if (relation.get("type") != null) {
645            sb.append(relation.get("type"));
646        } else {
647            sb.append(tr("relation"));
648        }
649        sb.append(" (");
650        String nameTag = null;
651        Set<String> namingTags = new HashSet<>(getNamingtagsForRelations());
652        for (String n : relation.getTags().keySet()) {
653            // #3328: "note " and " note" are name tags too
654            if (namingTags.contains(n.trim())) {
655                if (Main.pref.getBoolean("osm-primitives.localize-name", true)) {
656                    nameTag = relation.getLocalName();
657                } else {
658                    nameTag = relation.getName();
659                }
660                if (nameTag == null) {
661                    nameTag = relation.get(n);
662                }
663            }
664            if (nameTag != null) {
665                break;
666            }
667        }
668        if (nameTag == null) {
669            sb.append(Long.toString(relation.getId())).append(", ");
670        } else {
671            sb.append("\"").append(nameTag).append("\", ");
672        }
673
674        int mbno = relation.getNumMembers();
675        sb.append(trn("{0} member", "{0} members", mbno, mbno)).append(")");
676
677        decorateNameWithId(sb, relation);
678        return sb.toString();
679    }
680
681    /**
682     * Builds a default tooltip text for an HistoryOsmPrimitive <code>primitive</code>.
683     *
684     * @param primitive the primitmive
685     * @return the tooltip text
686     */
687    public String buildDefaultToolTip(HistoryOsmPrimitive primitive) {
688        return buildDefaultToolTip(primitive.getId(), primitive.getTags());
689    }
690
691    public String formatAsHtmlUnorderedList(Collection<? extends OsmPrimitive> primitives) {
692        return Utils.joinAsHtmlUnorderedList(Utils.transform(primitives, new Function<OsmPrimitive, String>() {
693
694            @Override
695            public String apply(OsmPrimitive x) {
696                return x.getDisplayName(DefaultNameFormatter.this);
697            }
698        }));
699    }
700
701    public String formatAsHtmlUnorderedList(OsmPrimitive... primitives) {
702        return formatAsHtmlUnorderedList(Arrays.asList(primitives));
703    }
704}