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}