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