source: josm/trunk/src/org/openstreetmap/josm/data/osm/DefaultNameFormatter.java@ 17825

Last change on this file since 17825 was 17825, checked in by simon04, 3 years ago

see #20467 - Checkstyle

  • Property svn:eol-style set to native
File size: 24.0 KB
Line 
1// License: GPL. For details, see LICENSE file.
2package org.openstreetmap.josm.data.osm;
3
4import static org.openstreetmap.josm.tools.I18n.marktr;
5import static org.openstreetmap.josm.tools.I18n.tr;
6import static org.openstreetmap.josm.tools.I18n.trc;
7import static org.openstreetmap.josm.tools.I18n.trcLazy;
8import static org.openstreetmap.josm.tools.I18n.trn;
9
10import java.awt.ComponentOrientation;
11import java.util.ArrayList;
12import java.util.Arrays;
13import java.util.Collection;
14import java.util.Collections;
15import java.util.Comparator;
16import java.util.HashSet;
17import java.util.LinkedList;
18import java.util.List;
19import java.util.Locale;
20import java.util.Map;
21import java.util.Objects;
22import java.util.Set;
23import java.util.stream.Collectors;
24
25import org.openstreetmap.josm.data.coor.LatLon;
26import org.openstreetmap.josm.data.coor.conversion.CoordinateFormatManager;
27import org.openstreetmap.josm.data.osm.history.HistoryNameFormatter;
28import org.openstreetmap.josm.data.osm.history.HistoryNode;
29import org.openstreetmap.josm.data.osm.history.HistoryOsmPrimitive;
30import org.openstreetmap.josm.data.osm.history.HistoryRelation;
31import org.openstreetmap.josm.data.osm.history.HistoryWay;
32import org.openstreetmap.josm.gui.tagging.presets.TaggingPreset;
33import org.openstreetmap.josm.gui.tagging.presets.TaggingPresetNameTemplateList;
34import org.openstreetmap.josm.spi.preferences.Config;
35import org.openstreetmap.josm.tools.AlphanumComparator;
36import org.openstreetmap.josm.tools.I18n;
37import org.openstreetmap.josm.tools.Utils;
38import org.openstreetmap.josm.tools.template_engine.TemplateEngineDataProvider;
39
40/**
41 * This is the default implementation of a {@link NameFormatter} for names of {@link IPrimitive}s
42 * and {@link HistoryOsmPrimitive}s.
43 * @since 12663 (moved from {@code gui} package)
44 * @since 1990
45 */
46public class DefaultNameFormatter implements NameFormatter, HistoryNameFormatter {
47
48 private static DefaultNameFormatter instance;
49
50 private static final List<NameFormatterHook> formatHooks = new LinkedList<>();
51
52 private static final List<String> HIGHWAY_RAILWAY_WATERWAY_LANDUSE_BUILDING = Arrays.asList(
53 marktr("highway"), marktr("railway"), marktr("waterway"), marktr("landuse"), marktr("building"));
54
55 /**
56 * Replies the unique instance of this formatter
57 *
58 * @return the unique instance of this formatter
59 */
60 public static synchronized DefaultNameFormatter getInstance() {
61 if (instance == null) {
62 instance = new DefaultNameFormatter();
63 }
64 return instance;
65 }
66
67 /**
68 * Registers a format hook. Adds the hook at the first position of the format hooks.
69 * (for plugins)
70 *
71 * @param hook the format hook. Ignored if null.
72 */
73 public static void registerFormatHook(NameFormatterHook hook) {
74 if (hook == null) return;
75 if (!formatHooks.contains(hook)) {
76 formatHooks.add(0, hook);
77 }
78 }
79
80 /**
81 * Unregisters a format hook. Removes the hook from the list of format hooks.
82 *
83 * @param hook the format hook. Ignored if null.
84 */
85 public static void unregisterFormatHook(NameFormatterHook hook) {
86 if (hook == null) return;
87 formatHooks.remove(hook);
88 }
89
90 /** The default list of tags which are used as naming tags in relations.
91 * A ? prefix indicates a boolean value, for which the key (instead of the value) is used.
92 */
93 private static final String[] DEFAULT_NAMING_TAGS_FOR_RELATIONS = {
94 "name",
95 "ref",
96 //
97 "amenity",
98 "landuse",
99 "leisure",
100 "natural",
101 "public_transport",
102 "restriction",
103 "water",
104 "waterway",
105 "wetland",
106 //
107 ":LocationCode",
108 "note",
109 "?building",
110 "?building:part",
111 };
112
113 /** the current list of tags used as naming tags in relations */
114 private static List<String> namingTagsForRelations;
115
116 /**
117 * Replies the list of naming tags used in relations. The list is given (in this order) by:
118 * <ul>
119 * <li>by the tag names in the preference <code>relation.nameOrder</code></li>
120 * <li>by the default tags in {@link #DEFAULT_NAMING_TAGS_FOR_RELATIONS}
121 * </ul>
122 *
123 * @return the list of naming tags used in relations
124 */
125 public static synchronized List<String> getNamingtagsForRelations() {
126 if (namingTagsForRelations == null) {
127 namingTagsForRelations = new ArrayList<>(
128 Config.getPref().getList("relation.nameOrder", Arrays.asList(DEFAULT_NAMING_TAGS_FOR_RELATIONS))
129 );
130 }
131 return namingTagsForRelations;
132 }
133
134 /**
135 * Decorates the name of primitive with its id and version, if the preferences
136 * <code>osm-primitives.showid</code> and <code>osm-primitives.showversion</code> are set.
137 * Shows unique id if <code>osm-primitives.showid.new-primitives</code> is set
138 *
139 * @param name the name without the id
140 * @param primitive the primitive
141 */
142 protected void decorateNameWithId(StringBuilder name, IPrimitive primitive) {
143 int version = primitive.getVersion();
144 if (Config.getPref().getBoolean("osm-primitives.showid")) {
145 long id = Config.getPref().getBoolean("osm-primitives.showid.new-primitives") ?
146 primitive.getUniqueId() : primitive.getId();
147 if (Config.getPref().getBoolean("osm-primitives.showversion") && version > 0) {
148 name.append(tr(" [id: {0}, v{1}]", id, version));
149 } else {
150 name.append(tr(" [id: {0}]", id));
151 }
152 } else if (Config.getPref().getBoolean("osm-primitives.showversion")) {
153 name.append(tr(" [v{0}]", version));
154 }
155 }
156
157 /**
158 * Formats a name for an {@link IPrimitive}.
159 *
160 * @param osm the primitive
161 * @return the name
162 * @since 10991
163 * @since 13564 (signature)
164 */
165 public String format(IPrimitive osm) {
166 return osm.getDisplayName(this);
167 }
168
169 @Override
170 public String format(INode node) {
171 StringBuilder name = new StringBuilder();
172 if (node.isIncomplete()) {
173 name.append(tr("incomplete"));
174 } else {
175 TaggingPreset preset = TaggingPresetNameTemplateList.getInstance().findPresetTemplate(node);
176 if (preset == null || !(node instanceof TemplateEngineDataProvider)) {
177 String n = formatLocalName(node);
178 if (n == null) {
179 n = formatAddress(node);
180 }
181
182 if (n == null) {
183 n = node.isNew() ? tr("node") : Long.toString(node.getId());
184 }
185 name.append(n);
186 } else {
187 preset.nameTemplate.appendText(name, (TemplateEngineDataProvider) node);
188 }
189 if (node.isLatLonKnown() && Config.getPref().getBoolean("osm-primitives.showcoor")) {
190 name.append(" \u200E(");
191 name.append(CoordinateFormatManager.getDefaultFormat().toString(node, ", "));
192 name.append(")\u200C");
193 }
194 }
195 decorateNameWithId(name, node);
196
197 String result = name.toString();
198 return formatHooks.stream().map(hook -> hook.checkFormat(node, result))
199 .filter(Objects::nonNull)
200 .findFirst().orElse(result);
201
202 }
203
204 private final Comparator<INode> nodeComparator = Comparator.comparing(this::format);
205
206 @Override
207 public Comparator<INode> getNodeComparator() {
208 return nodeComparator;
209 }
210
211 @Override
212 public String format(IWay<?> way) {
213 StringBuilder name = new StringBuilder();
214
215 char mark;
216 // If current language is left-to-right (almost all languages)
217 if (ComponentOrientation.getOrientation(Locale.getDefault()).isLeftToRight()) {
218 // will insert Left-To-Right Mark to ensure proper display of text in the case when object name is right-to-left
219 mark = '\u200E';
220 } else {
221 // otherwise will insert Right-To-Left Mark to ensure proper display in the opposite case
222 mark = '\u200F';
223 }
224 // Initialize base direction of the string
225 name.append(mark);
226
227 if (way.isIncomplete()) {
228 name.append(tr("incomplete"));
229 } else {
230 TaggingPreset preset = TaggingPresetNameTemplateList.getInstance().findPresetTemplate(way);
231 if (preset == null || !(way instanceof TemplateEngineDataProvider)) {
232 String n;
233 n = formatLocalName(way);
234 if (n == null) {
235 n = way.get("ref");
236 }
237 if (n == null) {
238 n = formatAddress(way);
239 }
240 if (n == null) {
241 for (String key : HIGHWAY_RAILWAY_WATERWAY_LANDUSE_BUILDING) {
242 if (way.hasKey(key) && !way.isKeyFalse(key)) {
243 /* I18N: first is highway, railway, waterway, landuse or building type, second is the type itself */
244 n = way.isKeyTrue(key) ? tr(key) : tr("{0} ({1})", trcLazy(key, way.get(key)), tr(key));
245 break;
246 }
247 }
248 }
249 if (n == null || n.isEmpty()) {
250 n = String.valueOf(way.getId());
251 }
252
253 name.append(n);
254 } else {
255 preset.nameTemplate.appendText(name, (TemplateEngineDataProvider) way);
256 }
257
258 int nodesNo = way.getRealNodesCount();
259 /* note: length == 0 should no longer happen, but leave the bracket code
260 nevertheless, who knows what future brings */
261 /* I18n: count of nodes as parameter */
262 String nodes = trn("{0} node", "{0} nodes", nodesNo, nodesNo);
263 name.append(mark).append(" (").append(nodes).append(')');
264 }
265 decorateNameWithId(name, way);
266 name.append('\u200C');
267
268 String result = name.toString();
269 return formatHooks.stream().map(hook -> hook.checkFormat(way, result))
270 .filter(Objects::nonNull)
271 .findFirst().orElse(result);
272
273 }
274
275 private static String formatLocalName(IPrimitive osm) {
276 if (Config.getPref().getBoolean("osm-primitives.localize-name", true)) {
277 return osm.getLocalName();
278 } else {
279 return osm.getName();
280 }
281 }
282
283 private static String formatLocalName(HistoryOsmPrimitive osm) {
284 if (Config.getPref().getBoolean("osm-primitives.localize-name", true)) {
285 return osm.getLocalName();
286 } else {
287 return osm.getName();
288 }
289 }
290
291 private static String formatAddress(Tagged osm) {
292 String n = null;
293 String s = osm.get("addr:housename");
294 if (s != null) {
295 /* I18n: name of house as parameter */
296 n = tr("House {0}", s);
297 }
298 if (n == null && (s = osm.get("addr:housenumber")) != null) {
299 String t = osm.get("addr:street");
300 if (t != null) {
301 /* I18n: house number, street as parameter, number should remain
302 before street for better visibility */
303 n = tr("House number {0} at {1}", s, t);
304 } else {
305 /* I18n: house number as parameter */
306 n = tr("House number {0}", s);
307 }
308 }
309 return n;
310 }
311
312 private final Comparator<IWay<?>> wayComparator = Comparator.comparing(this::format);
313
314 @Override
315 public Comparator<IWay<?>> getWayComparator() {
316 return wayComparator;
317 }
318
319 @Override
320 public String format(IRelation<?> relation) {
321 StringBuilder name = new StringBuilder();
322 if (relation.isIncomplete()) {
323 name.append(tr("incomplete"));
324 } else {
325 TaggingPreset preset = TaggingPresetNameTemplateList.getInstance().findPresetTemplate(relation);
326
327 formatRelationNameAndType(relation, name, preset);
328
329 int mbno = relation.getMembersCount();
330 name.append(trn("{0} member", "{0} members", mbno, mbno));
331
332 if (relation.hasIncompleteMembers()) {
333 name.append(", ").append(tr("incomplete"));
334 }
335
336 name.append(')');
337 }
338 decorateNameWithId(name, relation);
339
340 String result = name.toString();
341 return formatHooks.stream().map(hook -> hook.checkFormat(relation, result))
342 .filter(Objects::nonNull)
343 .findFirst().orElse(result);
344
345 }
346
347 private static StringBuilder formatRelationNameAndType(IRelation<?> relation, StringBuilder result, TaggingPreset preset) {
348 if (preset == null || !(relation instanceof TemplateEngineDataProvider)) {
349 result.append(getRelationTypeName(relation));
350 String relationName = getRelationName(relation);
351 if (relationName == null) {
352 relationName = Long.toString(relation.getId());
353 } else {
354 relationName = '\"' + relationName + '\"';
355 }
356 result.append(" (").append(relationName).append(", ");
357 } else {
358 preset.nameTemplate.appendText(result, (TemplateEngineDataProvider) relation);
359 result.append('(');
360 }
361 return result;
362 }
363
364 private final Comparator<IRelation<?>> relationComparator = (r1, r2) -> {
365 //TODO This doesn't work correctly with formatHooks
366
367 TaggingPreset preset1 = TaggingPresetNameTemplateList.getInstance().findPresetTemplate(r1);
368 TaggingPreset preset2 = TaggingPresetNameTemplateList.getInstance().findPresetTemplate(r2);
369
370 if (preset1 != null || preset2 != null) {
371 String name11 = formatRelationNameAndType(r1, new StringBuilder(), preset1).toString();
372 String name21 = formatRelationNameAndType(r2, new StringBuilder(), preset2).toString();
373
374 int comp1 = AlphanumComparator.getInstance().compare(name11, name21);
375 if (comp1 != 0)
376 return comp1;
377 } else {
378
379 String type1 = getRelationTypeName(r1);
380 String type2 = getRelationTypeName(r2);
381
382 int comp2 = AlphanumComparator.getInstance().compare(type1, type2);
383 if (comp2 != 0)
384 return comp2;
385
386 String name12 = getRelationName(r1);
387 String name22 = getRelationName(r2);
388
389 comp2 = AlphanumComparator.getInstance().compare(name12, name22);
390 if (comp2 != 0)
391 return comp2;
392 }
393
394 int comp3 = Integer.compare(r1.getMembersCount(), r2.getMembersCount());
395 if (comp3 != 0)
396 return comp3;
397
398
399 comp3 = Boolean.compare(r1.hasIncompleteMembers(), r2.hasIncompleteMembers());
400 if (comp3 != 0)
401 return comp3;
402
403 return Long.compare(r1.getUniqueId(), r2.getUniqueId());
404 };
405
406 @Override
407 public Comparator<IRelation<?>> getRelationComparator() {
408 return relationComparator;
409 }
410
411 private static String getRelationTypeName(IRelation<?> relation) {
412 // see https://josm.openstreetmap.de/browser/osm/applications/editors/josm/i18n/specialmessages.java
413 String name = trc("Relation type", relation.get("type"));
414 if (name == null) {
415 name = relation.hasKey("public_transport") ? 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 name = tr(building); // translate tag!
423 }
424 }
425 if (name == null) {
426 // see https://josm.openstreetmap.de/browser/osm/applications/editors/josm/i18n/specialmessages.java
427 name = trc("Place type", relation.get("place"));
428 }
429 if (name == null) {
430 name = tr("relation");
431 }
432 String adminLevel = relation.get("admin_level");
433 if (adminLevel != null) {
434 name += '['+adminLevel+']';
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 static String getNameTagValue(IRelation<?> relation, String nameTag) {
447 if ("name".equals(nameTag)) {
448 return formatLocalName(relation);
449 } else if (":LocationCode".equals(nameTag)) {
450 return relation.keys()
451 .filter(m -> m.endsWith(nameTag))
452 .findFirst()
453 .map(relation::get)
454 .orElse(null);
455 } else if (nameTag.startsWith("?") && OsmUtils.isTrue(relation.get(nameTag.substring(1)))) {
456 return tr(nameTag.substring(1));
457 } else if (nameTag.startsWith("?") && OsmUtils.isFalse(relation.get(nameTag.substring(1)))) {
458 return null;
459 } else if (nameTag.startsWith("?")) {
460 return trcLazy(nameTag, I18n.escape(relation.get(nameTag.substring(1))));
461 } else {
462 return trcLazy(nameTag, I18n.escape(relation.get(nameTag)));
463 }
464 }
465
466 private static String getRelationName(IRelation<?> relation) {
467 String nameTag;
468 for (String n : getNamingtagsForRelations()) {
469 nameTag = getNameTagValue(relation, n);
470 if (nameTag != null)
471 return nameTag;
472 }
473 return null;
474 }
475
476 @Override
477 public String format(Changeset changeset) {
478 return tr("Changeset {0}", changeset.getId());
479 }
480
481 /**
482 * Builds a default tooltip text for the primitive <code>primitive</code>.
483 *
484 * @param primitive the primitive
485 * @return the tooltip text
486 */
487 public String buildDefaultToolTip(IPrimitive primitive) {
488 return buildDefaultToolTip(primitive.getId(), primitive.getKeys());
489 }
490
491 private static String buildDefaultToolTip(long id, Map<String, String> tags) {
492 StringBuilder sb = new StringBuilder(128);
493 sb.append("<html><strong>id</strong>=")
494 .append(id)
495 .append("<br>");
496 List<String> keyList = new ArrayList<>(tags.keySet());
497 Collections.sort(keyList);
498 for (int i = 0; i < keyList.size(); i++) {
499 if (i > 0) {
500 sb.append("<br>");
501 }
502 String key = keyList.get(i);
503 sb.append("<strong>")
504 .append(Utils.escapeReservedCharactersHTML(key))
505 .append("</strong>=");
506 String value = tags.get(key);
507 while (!value.isEmpty()) {
508 sb.append(Utils.escapeReservedCharactersHTML(value.substring(0, Math.min(50, value.length()))));
509 if (value.length() > 50) {
510 sb.append("<br>");
511 value = value.substring(50);
512 } else {
513 value = "";
514 }
515 }
516 }
517 sb.append("</html>");
518 return sb.toString();
519 }
520
521 /**
522 * Decorates the name of primitive with its id, if the preference
523 * <code>osm-primitives.showid</code> is set.
524 *
525 * The id is append to the {@link StringBuilder} passed in <code>name</code>.
526 *
527 * @param name the name without the id
528 * @param primitive the primitive
529 */
530 protected void decorateNameWithId(StringBuilder name, HistoryOsmPrimitive primitive) {
531 if (Config.getPref().getBoolean("osm-primitives.showid")) {
532 name.append(tr(" [id: {0}]", primitive.getId()));
533 }
534 }
535
536 @Override
537 public String format(HistoryNode node) {
538 StringBuilder sb = new StringBuilder();
539 String name = formatLocalName(node);
540 if (name == null) {
541 sb.append(node.getId());
542 } else {
543 sb.append(name);
544 }
545 LatLon coord = node.getCoords();
546 if (coord != null) {
547 sb.append(" (")
548 .append(CoordinateFormatManager.getDefaultFormat().latToString(coord))
549 .append(", ")
550 .append(CoordinateFormatManager.getDefaultFormat().lonToString(coord))
551 .append(')');
552 }
553 decorateNameWithId(sb, node);
554 return sb.toString();
555 }
556
557 @Override
558 public String format(HistoryWay way) {
559 StringBuilder sb = new StringBuilder();
560 String name = formatLocalName(way);
561 if (name != null) {
562 sb.append(name);
563 }
564 if (sb.length() == 0 && way.get("ref") != null) {
565 sb.append(way.get("ref"));
566 }
567 if (sb.length() == 0) {
568 sb.append(
569 way.hasKey("highway") ? tr("highway") :
570 way.hasKey("railway") ? tr("railway") :
571 way.hasKey("waterway") ? tr("waterway") :
572 way.hasKey("landuse") ? tr("landuse") : ""
573 );
574 }
575
576 int nodesNo = way.isClosed() ? (way.getNumNodes() -1) : way.getNumNodes();
577 String nodes = trn("{0} node", "{0} nodes", nodesNo, nodesNo);
578 if (sb.length() == 0) {
579 sb.append(way.getId());
580 }
581 /* note: length == 0 should no longer happen, but leave the bracket code
582 nevertheless, who knows what future brings */
583 sb.append((sb.length() > 0) ? (" ("+nodes+')') : nodes);
584 decorateNameWithId(sb, way);
585 return sb.toString();
586 }
587
588 @Override
589 public String format(HistoryRelation relation) {
590 StringBuilder sb = new StringBuilder();
591 String type = relation.get("type");
592 if (type != null) {
593 sb.append(type);
594 } else {
595 sb.append(tr("relation"));
596 }
597 sb.append(" (");
598 String nameTag = null;
599 Set<String> namingTags = new HashSet<>(getNamingtagsForRelations());
600 for (String n : relation.getTags().keySet()) {
601 // #3328: "note " and " note" are name tags too
602 if (namingTags.contains(n.trim())) {
603 nameTag = formatLocalName(relation);
604 if (nameTag == null) {
605 nameTag = relation.get(n);
606 }
607 }
608 if (nameTag != null) {
609 break;
610 }
611 }
612 if (nameTag == null) {
613 sb.append(Long.toString(relation.getId())).append(", ");
614 } else {
615 sb.append('\"').append(nameTag).append("\", ");
616 }
617
618 int mbno = relation.getNumMembers();
619 sb.append(trn("{0} member", "{0} members", mbno, mbno)).append(')');
620
621 decorateNameWithId(sb, relation);
622 return sb.toString();
623 }
624
625 /**
626 * Builds a default tooltip text for an HistoryOsmPrimitive <code>primitive</code>.
627 *
628 * @param primitive the primitive
629 * @return the tooltip text
630 */
631 public String buildDefaultToolTip(HistoryOsmPrimitive primitive) {
632 return buildDefaultToolTip(primitive.getId(), primitive.getTags());
633 }
634
635 /**
636 * Formats the given collection of primitives as an HTML unordered list.
637 * @param primitives collection of primitives to format
638 * @param maxElements the maximum number of elements to display
639 * @return HTML unordered list
640 */
641 public String formatAsHtmlUnorderedList(Collection<? extends OsmPrimitive> primitives, int maxElements) {
642 Collection<String> displayNames = primitives.stream().map(x -> x.getDisplayName(this)).collect(Collectors.toList());
643 return Utils.joinAsHtmlUnorderedList(Utils.limit(displayNames, maxElements, "..."));
644 }
645
646 /**
647 * Formats the given primitive as an HTML unordered list.
648 * @param primitive primitive to format
649 * @return HTML unordered list
650 */
651 public String formatAsHtmlUnorderedList(OsmPrimitive primitive) {
652 return formatAsHtmlUnorderedList(Collections.singletonList(primitive), 1);
653 }
654
655 /**
656 * Removes the bidirectional text characters U+200C, U+200E, U+200F from the string
657 * @param string the string
658 * @return the string with the bidirectional text characters removed
659 */
660 public static String removeBiDiCharacters(String string) {
661 return string.replaceAll("[\\u200C\\u200E\\u200F]", "");
662 }
663}
Note: See TracBrowser for help on using the repository browser.