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