001    // License: GPL. For details, see LICENSE file.
002    package org.openstreetmap.josm.gui;
003    
004    import static org.openstreetmap.josm.tools.I18n.tr;
005    import static org.openstreetmap.josm.tools.I18n.trc;
006    import static org.openstreetmap.josm.tools.I18n.trc_lazy;
007    import static org.openstreetmap.josm.tools.I18n.trn;
008    
009    import java.util.ArrayList;
010    import java.util.Arrays;
011    import java.util.Collection;
012    import java.util.Collections;
013    import java.util.Comparator;
014    import java.util.HashSet;
015    import java.util.LinkedList;
016    import java.util.List;
017    import java.util.Set;
018    
019    import org.openstreetmap.josm.Main;
020    import org.openstreetmap.josm.data.coor.CoordinateFormat;
021    import org.openstreetmap.josm.data.coor.LatLon;
022    import org.openstreetmap.josm.data.osm.Changeset;
023    import org.openstreetmap.josm.data.osm.IPrimitive;
024    import org.openstreetmap.josm.data.osm.IRelation;
025    import org.openstreetmap.josm.data.osm.NameFormatter;
026    import org.openstreetmap.josm.data.osm.Node;
027    import org.openstreetmap.josm.data.osm.OsmPrimitive;
028    import org.openstreetmap.josm.data.osm.OsmUtils;
029    import org.openstreetmap.josm.data.osm.Relation;
030    import org.openstreetmap.josm.data.osm.Way;
031    import org.openstreetmap.josm.data.osm.history.HistoryNameFormatter;
032    import org.openstreetmap.josm.data.osm.history.HistoryNode;
033    import org.openstreetmap.josm.data.osm.history.HistoryOsmPrimitive;
034    import org.openstreetmap.josm.data.osm.history.HistoryRelation;
035    import org.openstreetmap.josm.data.osm.history.HistoryWay;
036    import org.openstreetmap.josm.gui.tagging.TaggingPreset;
037    import org.openstreetmap.josm.tools.AlphanumComparator;
038    import org.openstreetmap.josm.tools.I18n;
039    import org.openstreetmap.josm.tools.TaggingPresetNameTemplateList;
040    import org.openstreetmap.josm.tools.Utils;
041    import 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     */
047    public class DefaultNameFormatter implements NameFormatter, HistoryNameFormatter {
048    
049        static private DefaultNameFormatter instance;
050    
051        private static final LinkedList<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         * @return the decorated name
124         */
125        protected void decorateNameWithId(StringBuilder name, IPrimitive primitive) {
126            if (Main.pref.getBoolean("osm-primitives.showid")) {
127                if (Main.pref.getBoolean("osm-primitives.showid.new-primitives")) {
128                    name.append(tr(" [id: {0}]", primitive.getUniqueId()));
129                } else {
130                    name.append(tr(" [id: {0}]", primitive.getId()));
131                }
132            }
133        }
134    
135        /**
136         * Formats a name for a node
137         *
138         * @param node the node
139         * @return the name
140         */
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        public Comparator<Node> getNodeComparator() {
207            return nodeComparator;
208        }
209    
210    
211        /**
212         * Formats a name for a way
213         *
214         * @param way the way
215         * @return the name
216         */
217        public String format(Way way) {
218            StringBuilder name = new StringBuilder();
219            if (way.isIncomplete()) {
220                name.append(tr("incomplete"));
221            } else {
222                TaggingPreset preset = TaggingPresetNameTemplateList.getInstance().findPresetTemplate(way);
223                if (preset == null) {
224                    String n;
225                    if (Main.pref.getBoolean("osm-primitives.localize-name", true)) {
226                        n = way.getLocalName();
227                    } else {
228                        n = way.getName();
229                    }
230                    if (n == null) {
231                        n = way.get("ref");
232                    }
233                    if (n == null) {
234                        n =
235                                (way.get("highway") != null) ? tr("highway") :
236                                    (way.get("railway") != null) ? tr("railway") :
237                                        (way.get("waterway") != null) ? tr("waterway") :
238                                                (way.get("landuse") != null) ? tr("landuse") : null;
239                    }
240                    if(n == null)
241                    {
242                        String s;
243                        if((s = way.get("addr:housename")) != null) {
244                            /* I18n: name of house as parameter */
245                            n = tr("House {0}", s);
246                        }
247                        if(n == null && (s = way.get("addr:housenumber")) != null) {
248                            String t = way.get("addr:street");
249                            if(t != null) {
250                                /* I18n: house number, street as parameter, number should remain
251                            before street for better visibility */
252                                n =  tr("House number {0} at {1}", s, t);
253                            }
254                            else {
255                                /* I18n: house number as parameter */
256                                n = tr("House number {0}", s);
257                            }
258                        }
259                    }
260                    if(n == null && way.get("building") != null) n = tr("building");
261                    if(n == null || n.length() == 0) {
262                        n = String.valueOf(way.getId());
263                    }
264    
265                    name.append(n);
266                } else {
267                    preset.nameTemplate.appendText(name, way);
268                }
269    
270                int nodesNo = way.getNodesCount();
271                if (nodesNo > 1 && way.isClosed()) {
272                    nodesNo--;
273                }
274                /* note: length == 0 should no longer happen, but leave the bracket code
275                   nevertheless, who knows what future brings */
276                /* I18n: count of nodes as parameter */
277                String nodes = trn("{0} node", "{0} nodes", nodesNo, nodesNo);
278                name.append(" (").append(nodes).append(")");
279            }
280            decorateNameWithId(name, way);
281    
282            String result = name.toString();
283            for (NameFormatterHook hook: formatHooks) {
284                String hookResult = hook.checkFormat(way, result);
285                if (hookResult != null)
286                    return hookResult;
287            }
288    
289            return result;
290        }
291    
292        private final Comparator<Way> wayComparator = new Comparator<Way>() {
293            @Override
294            public int compare(Way w1, Way w2) {
295                return format(w1).compareTo(format(w2));
296            }
297        };
298    
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        public String format(Relation relation) {
311            StringBuilder name = new StringBuilder();
312            if (relation.isIncomplete()) {
313                name.append(tr("incomplete"));
314            } else {
315                TaggingPreset preset = TaggingPresetNameTemplateList.getInstance().findPresetTemplate(relation);
316    
317                formatRelationNameAndType(relation, name, preset);
318    
319                int mbno = relation.getMembersCount();
320                name.append(trn("{0} member", "{0} members", mbno, mbno));
321    
322                if (relation.hasIncompleteMembers()) {
323                    name.append(", ").append(tr("incomplete"));
324                }
325    
326                name.append(")");
327            }
328            decorateNameWithId(name, relation);
329    
330            String result = name.toString();
331            for (NameFormatterHook hook: formatHooks) {
332                String hookResult = hook.checkFormat(relation, result);
333                if (hookResult != null)
334                    return hookResult;
335            }
336    
337            return result;
338        }
339    
340        private void formatRelationNameAndType(Relation relation, StringBuilder result, TaggingPreset preset) {
341            if (preset == null) {
342                result.append(getRelationTypeName(relation));
343                String relationName = getRelationName(relation);
344                if (relationName == null) {
345                    relationName = Long.toString(relation.getId());
346                } else {
347                    relationName = "\"" + relationName + "\"";
348                }
349                result.append(" (").append(relationName).append(", ");
350            } else {
351                preset.nameTemplate.appendText(result, relation);
352                result.append("(");
353            }
354        }
355    
356        private final Comparator<Relation> relationComparator = new Comparator<Relation>() {
357            private final AlphanumComparator ALPHANUM_COMPARATOR = new AlphanumComparator();
358            @Override
359            public int compare(Relation r1, Relation r2) {
360                //TODO This doesn't work correctly with formatHooks
361    
362                TaggingPreset preset1 = TaggingPresetNameTemplateList.getInstance().findPresetTemplate(r1);
363                TaggingPreset preset2 = TaggingPresetNameTemplateList.getInstance().findPresetTemplate(r2);
364    
365                if (preset1 != null || preset2 != null) {
366                    StringBuilder name1 = new StringBuilder();
367                    formatRelationNameAndType(r1, name1, preset1);
368                    StringBuilder name2 = new StringBuilder();
369                    formatRelationNameAndType(r2, name2, preset2);
370    
371                    int comp = name1.toString().compareTo(name2.toString());
372                    if (comp != 0)
373                        return comp;
374                } else {
375    
376                    String type1 = getRelationTypeName(r1);
377                    String type2 = getRelationTypeName(r2);
378    
379                    int comp = ALPHANUM_COMPARATOR.compare(type1, type2);
380                    if (comp != 0)
381                        return comp;
382    
383                    String name1 = getRelationName(r1);
384                    String name2 = getRelationName(r2);
385    
386                    return ALPHANUM_COMPARATOR.compare(name1, name2);
387                }
388    
389                if (r1.getMembersCount() != r2.getMembersCount())
390                    return (r1.getMembersCount() > r2.getMembersCount())?1:-1;
391    
392                int comp = Boolean.valueOf(r1.hasIncompleteMembers()).compareTo(Boolean.valueOf(r2.hasIncompleteMembers()));
393                if (comp != 0)
394                    return comp;
395    
396                return r1.getUniqueId() > r2.getUniqueId()?1:-1;
397            }
398        };
399    
400        public Comparator<Relation> getRelationComparator() {
401            return relationComparator;
402        }
403    
404        private String getLeadingNumber(String s) {
405            int i = 0;
406            while (i < s.length() && Character.isDigit(s.charAt(i))) {
407                i++;
408            }
409            return s.substring(0, i);
410        }
411    
412        private String getRelationTypeName(IRelation relation) {
413            String name = trc("Relation type", relation.get("type"));
414            if (name == null) {
415                name = (relation.get("public_transport") != null) ? tr("public transport") : null;
416            }
417            if (name == null) {
418                String building  = relation.get("building");
419                if (OsmUtils.isTrue(building)) {
420                    name = tr("building");
421                } else if(building != null)
422                {
423                    name = tr(building); // translate tag!
424                }
425            }
426            if (name == null) {
427                name = trc("Place type", relation.get("place"));
428            }
429            if (name == null) {
430                name = tr("relation");
431            }
432            String admin_level = relation.get("admin_level");
433            if (admin_level != null) {
434                name += "["+admin_level+"]";
435            }
436    
437            for (NameFormatterHook hook: formatHooks) {
438                String hookResult = hook.checkRelationTypeName(relation, name);
439                if (hookResult != null)
440                    return hookResult;
441            }
442    
443            return name;
444        }
445    
446        private String getNameTagValue(IRelation relation, String nameTag) {
447            if (nameTag.equals("name")) {
448                if (Main.pref.getBoolean("osm-primitives.localize-name", true))
449                    return relation.getLocalName();
450                else
451                    return relation.getName();
452            } else if (nameTag.equals(":LocationCode")) {
453                for (String m : relation.keySet()) {
454                    if (m.endsWith(nameTag))
455                        return relation.get(m);
456                }
457                return null;
458            } else if (nameTag.startsWith("?") && OsmUtils.isTrue(relation.get(nameTag.substring(1)))) {
459                return tr(nameTag.substring(1));
460            } else if (nameTag.startsWith("?") && OsmUtils.isFalse(relation.get(nameTag.substring(1)))) {
461                return null;
462            } else {
463                return trc_lazy(nameTag, I18n.escape(relation.get(nameTag)));
464            }
465        }
466    
467        private String getRelationName(IRelation relation) {
468            String nameTag = null;
469            for (String n : getNamingtagsForRelations()) {
470                nameTag = getNameTagValue(relation, n);
471                if (nameTag != null)
472                    return nameTag;
473            }
474            return null;
475        }
476    
477        /**
478         * Formats a name for a changeset
479         *
480         * @param changeset the changeset
481         * @return the name
482         */
483        public String format(Changeset changeset) {
484            return tr("Changeset {0}",changeset.getId());
485        }
486    
487        /**
488         * Builds a default tooltip text for the primitive <code>primitive</code>.
489         *
490         * @param primitive the primitmive
491         * @return the tooltip text
492         */
493        public String buildDefaultToolTip(IPrimitive primitive) {
494            StringBuilder sb = new StringBuilder();
495            sb.append("<html>");
496            sb.append("<strong>id</strong>=")
497            .append(primitive.getId())
498            .append("<br>");
499            ArrayList<String> keyList = new ArrayList<String>(primitive.keySet());
500            Collections.sort(keyList);
501            for (int i = 0; i < keyList.size(); i++) {
502                if (i > 0) {
503                    sb.append("<br>");
504                }
505                String key = keyList.get(i);
506                sb.append("<strong>")
507                .append(key)
508                .append("</strong>")
509                .append("=");
510                String value = primitive.get(key);
511                while(value.length() != 0) {
512                    sb.append(value.substring(0,Math.min(50, value.length())));
513                    if (value.length() > 50) {
514                        sb.append("<br>");
515                        value = value.substring(50);
516                    } else {
517                        value = "";
518                    }
519                }
520            }
521            sb.append("</html>");
522            return sb.toString();
523        }
524    
525        /**
526         * Decorates the name of primitive with its id, if the preference
527         * <tt>osm-primitives.showid</tt> is set.
528         *
529         * The id is append to the {@link StringBuilder} passed in in <code>name</code>.
530         *
531         * @param name  the name without the id
532         * @param primitive the primitive
533         */
534        protected void decorateNameWithId(StringBuilder name, HistoryOsmPrimitive primitive) {
535            if (Main.pref.getBoolean("osm-primitives.showid")) {
536                name.append(tr(" [id: {0}]", primitive.getId()));
537            }
538        }
539    
540        /**
541         * Formats a name for a history node
542         *
543         * @param node the node
544         * @return the name
545         */
546        public String format(HistoryNode node) {
547            StringBuilder sb = new StringBuilder();
548            String name;
549            if (Main.pref.getBoolean("osm-primitives.localize-name", true)) {
550                name = node.getLocalName();
551            } else {
552                name = node.getName();
553            }
554            if (name == null) {
555                sb.append(node.getId());
556            } else {
557                sb.append(name);
558            }
559            LatLon coord = node.getCoords();
560            if (coord != null) {
561                sb.append(" (")
562                .append(coord.latToString(CoordinateFormat.getDefaultFormat()))
563                .append(", ")
564                .append(coord.lonToString(CoordinateFormat.getDefaultFormat()))
565                .append(")");
566            }
567            decorateNameWithId(sb, node);
568            return sb.toString();
569        }
570    
571        /**
572         * Formats a name for a way
573         *
574         * @param way the way
575         * @return the name
576         */
577        public String format(HistoryWay way) {
578            StringBuilder sb = new StringBuilder();
579            String name;
580            if (Main.pref.getBoolean("osm-primitives.localize-name", true)) {
581                name = way.getLocalName();
582            } else {
583                name = way.getName();
584            }
585            if (name != null) {
586                sb.append(name);
587            }
588            if (sb.length() == 0 && way.get("ref") != null) {
589                sb.append(way.get("ref"));
590            }
591            if (sb.length() == 0) {
592                sb.append(
593                        (way.get("highway") != null) ? tr("highway") :
594                            (way.get("railway") != null) ? tr("railway") :
595                                (way.get("waterway") != null) ? tr("waterway") :
596                                    (way.get("landuse") != null) ? tr("landuse") : ""
597                        );
598            }
599    
600            int nodesNo = way.isClosed() ? way.getNumNodes() -1 : way.getNumNodes();
601            String nodes = trn("{0} node", "{0} nodes", nodesNo, nodesNo);
602            if(sb.length() == 0 ) {
603                sb.append(way.getId());
604            }
605            /* note: length == 0 should no longer happen, but leave the bracket code
606               nevertheless, who knows what future brings */
607            sb.append((sb.length() > 0) ? " ("+nodes+")" : nodes);
608            decorateNameWithId(sb, way);
609            return sb.toString();
610        }
611    
612        /**
613         * Formats a name for a {@link HistoryRelation})
614         *
615         * @param relation the relation
616         * @return the name
617         */
618        public String format(HistoryRelation relation) {
619            StringBuilder sb = new StringBuilder();
620            if (relation.get("type") != null) {
621                sb.append(relation.get("type"));
622            } else {
623                sb.append(tr("relation"));
624            }
625            sb.append(" (");
626            String nameTag = null;
627            Set<String> namingTags = new HashSet<String>(getNamingtagsForRelations());
628            for (String n : relation.getTags().keySet()) {
629                // #3328: "note " and " note" are name tags too
630                if (namingTags.contains(n.trim())) {
631                    if (Main.pref.getBoolean("osm-primitives.localize-name", true)) {
632                        nameTag = relation.getLocalName();
633                    } else {
634                        nameTag = relation.getName();
635                    }
636                    if (nameTag == null) {
637                        nameTag = relation.get(n);
638                    }
639                }
640                if (nameTag != null) {
641                    break;
642                }
643            }
644            if (nameTag == null) {
645                sb.append(Long.toString(relation.getId())).append(", ");
646            } else {
647                sb.append("\"").append(nameTag).append("\", ");
648            }
649    
650            int mbno = relation.getNumMembers();
651            sb.append(trn("{0} member", "{0} members", mbno, mbno)).append(")");
652    
653            decorateNameWithId(sb, relation);
654            return sb.toString();
655        }
656    
657        /**
658         * Builds a default tooltip text for an HistoryOsmPrimitive <code>primitive</code>.
659         *
660         * @param primitive the primitmive
661         * @return the tooltip text
662         */
663        public String buildDefaultToolTip(HistoryOsmPrimitive primitive) {
664            StringBuilder sb = new StringBuilder();
665            sb.append("<html>");
666            sb.append("<strong>id</strong>=")
667            .append(primitive.getId())
668            .append("<br>");
669            ArrayList<String> keyList = new ArrayList<String>(primitive.getTags().keySet());
670            Collections.sort(keyList);
671            for (int i = 0; i < keyList.size(); i++) {
672                if (i > 0) {
673                    sb.append("<br>");
674                }
675                String key = keyList.get(i);
676                sb.append("<strong>")
677                .append(key)
678                .append("</strong>")
679                .append("=");
680                String value = primitive.get(key);
681                while(value.length() != 0) {
682                    sb.append(value.substring(0,Math.min(50, value.length())));
683                    if (value.length() > 50) {
684                        sb.append("<br>");
685                        value = value.substring(50);
686                    } else {
687                        value = "";
688                    }
689                }
690            }
691            sb.append("</html>");
692            return sb.toString();
693        }
694    
695        public String formatAsHtmlUnorderedList(Collection<? extends OsmPrimitive> primitives) {
696            return Utils.joinAsHtmlUnorderedList(Utils.transform(primitives, new Function<OsmPrimitive, String>() {
697    
698                @Override
699                public String apply(OsmPrimitive x) {
700                    return x.getDisplayName(DefaultNameFormatter.this);
701                }
702            }));
703        }
704    
705        public String formatAsHtmlUnorderedList(OsmPrimitive... primitives) {
706            return formatAsHtmlUnorderedList(Arrays.asList(primitives));
707        }
708    }