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