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