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 }