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

Last change on this file since 5132 was 5132, checked in by simon04, 12 years ago

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

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