source: josm/trunk/src/org/openstreetmap/josm/data/osm/TagCollection.java@ 17585

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

see #20613 - Use Tagged.visitKeys()

Avoids creating a temporary HashMap just for iteration purposes

  • Property svn:eol-style set to native
File size: 24.3 KB
Line 
1// License: GPL. For details, see LICENSE file.
2package org.openstreetmap.josm.data.osm;
3
4import static org.openstreetmap.josm.tools.I18n.tr;
5
6import java.io.Serializable;
7import java.util.ArrayList;
8import java.util.Arrays;
9import java.util.Collection;
10import java.util.HashMap;
11import java.util.HashSet;
12import java.util.Iterator;
13import java.util.LinkedHashMap;
14import java.util.LinkedHashSet;
15import java.util.List;
16import java.util.Map;
17import java.util.Map.Entry;
18import java.util.Objects;
19import java.util.Set;
20import java.util.regex.Pattern;
21import java.util.stream.Collectors;
22import java.util.stream.Stream;
23
24import org.openstreetmap.josm.tools.Logging;
25
26/**
27 * TagCollection is a collection of tags which can be used to manipulate
28 * tags managed by {@link org.openstreetmap.josm.data.osm.OsmPrimitive}s.
29 *
30 * A TagCollection can be created:
31 * <ul>
32 * <li>from the tags managed by a specific {@link org.openstreetmap.josm.data.osm.OsmPrimitive}
33 * with {@link #from(org.openstreetmap.josm.data.osm.Tagged)}</li>
34 * <li>from the union of all tags managed by a collection of {@link org.openstreetmap.josm.data.osm.OsmPrimitive}s
35 * with {@link #unionOfAllPrimitives(java.util.Collection)}</li>
36 * <li>from the union of all tags managed by a {@link org.openstreetmap.josm.data.osm.DataSet}
37 * with {@link #unionOfAllPrimitives(org.openstreetmap.josm.data.osm.DataSet)}</li>
38 * <li>from the intersection of all tags managed by a collection of primitives
39 * with {@link #commonToAllPrimitives(java.util.Collection)}</li>
40 * </ul>
41 *
42 * It provides methods to query the collection, like {@link #size()}, {@link #hasTagsFor(String)}, etc.
43 *
44 * Basic set operations allow to create the union, the intersection and the difference
45 * of tag collections, see {@link #union(org.openstreetmap.josm.data.osm.TagCollection)},
46 * {@link #intersect(org.openstreetmap.josm.data.osm.TagCollection)}, and {@link #minus(org.openstreetmap.josm.data.osm.TagCollection)}.
47 *
48 * @since 2008
49 */
50public class TagCollection implements Iterable<Tag>, Serializable {
51
52 private static final long serialVersionUID = 1;
53
54 /**
55 * Creates a tag collection from the tags managed by a specific
56 * {@link org.openstreetmap.josm.data.osm.OsmPrimitive}. If <code>primitive</code> is null, replies
57 * an empty tag collection.
58 *
59 * @param primitive the primitive
60 * @return a tag collection with the tags managed by a specific
61 * {@link org.openstreetmap.josm.data.osm.OsmPrimitive}
62 */
63 public static TagCollection from(Tagged primitive) {
64 TagCollection tags = new TagCollection();
65 if (primitive != null) {
66 primitive.visitKeys((p, key, value) -> tags.add(new Tag(key, value)));
67 }
68 return tags;
69 }
70
71 /**
72 * Creates a tag collection from a map of key/value-pairs. Replies
73 * an empty tag collection if {@code tags} is null.
74 *
75 * @param tags the key/value-pairs
76 * @return the tag collection
77 */
78 public static TagCollection from(Map<String, String> tags) {
79 TagCollection ret = new TagCollection();
80 if (tags == null) return ret;
81 for (Entry<String, String> entry: tags.entrySet()) {
82 String key = entry.getKey() == null ? "" : entry.getKey();
83 String value = entry.getValue() == null ? "" : entry.getValue();
84 ret.add(new Tag(key, value));
85 }
86 return ret;
87 }
88
89 /**
90 * Creates a tag collection from the union of the tags managed by
91 * a collection of primitives. Replies an empty tag collection,
92 * if <code>primitives</code> is null.
93 *
94 * @param primitives the primitives
95 * @return a tag collection with the union of the tags managed by
96 * a collection of primitives
97 */
98 public static TagCollection unionOfAllPrimitives(Collection<? extends Tagged> primitives) {
99 TagCollection tags = new TagCollection();
100 if (primitives == null) return tags;
101 for (Tagged primitive: primitives) {
102 if (primitive == null) {
103 continue;
104 }
105 tags.add(TagCollection.from(primitive));
106 }
107 return tags;
108 }
109
110 /**
111 * Replies a tag collection with the tags which are common to all primitives in in
112 * <code>primitives</code>. Replies an empty tag collection of <code>primitives</code>
113 * is null.
114 *
115 * @param primitives the primitives
116 * @return a tag collection with the tags which are common to all primitives
117 */
118 public static TagCollection commonToAllPrimitives(Collection<? extends Tagged> primitives) {
119 TagCollection tags = new TagCollection();
120 if (primitives == null || primitives.isEmpty()) return tags;
121 // initialize with the first
122 tags.add(TagCollection.from(primitives.iterator().next()));
123
124 // intersect with the others
125 //
126 for (Tagged primitive: primitives) {
127 if (primitive == null) {
128 continue;
129 }
130 tags = tags.intersect(TagCollection.from(primitive));
131 if (tags.isEmpty())
132 break;
133 }
134 return tags;
135 }
136
137 /**
138 * Replies a tag collection with the union of the tags which are common to all primitives in
139 * the dataset <code>ds</code>. Returns an empty tag collection of <code>ds</code> is null.
140 *
141 * @param ds the dataset
142 * @return a tag collection with the union of the tags which are common to all primitives in
143 * the dataset <code>ds</code>
144 */
145 public static TagCollection unionOfAllPrimitives(DataSet ds) {
146 TagCollection tags = new TagCollection();
147 if (ds == null) return tags;
148 tags.add(TagCollection.unionOfAllPrimitives(ds.allPrimitives()));
149 return tags;
150 }
151
152 private final Map<Tag, Integer> tags = new HashMap<>();
153
154 /**
155 * Creates an empty tag collection.
156 */
157 public TagCollection() {
158 // contents can be set later with add()
159 }
160
161 /**
162 * Creates a clone of the tag collection <code>other</code>. Creates an empty
163 * tag collection if <code>other</code> is null.
164 *
165 * @param other the other collection
166 */
167 public TagCollection(TagCollection other) {
168 if (other != null) {
169 tags.putAll(other.tags);
170 }
171 }
172
173 /**
174 * Creates a tag collection from <code>tags</code>.
175 * @param tags the collection of tags
176 * @since 5724
177 */
178 public TagCollection(Collection<Tag> tags) {
179 add(tags);
180 }
181
182 /**
183 * Replies the number of tags in this tag collection
184 *
185 * @return the number of tags in this tag collection
186 */
187 public int size() {
188 return tags.size();
189 }
190
191 /**
192 * Replies true if this tag collection is empty
193 *
194 * @return true if this tag collection is empty; false, otherwise
195 */
196 public boolean isEmpty() {
197 return size() == 0;
198 }
199
200 /**
201 * Adds a tag to the tag collection. If <code>tag</code> is null, nothing is added.
202 *
203 * @param tag the tag to add
204 */
205 public final void add(Tag tag) {
206 if (tag != null) {
207 tags.merge(tag, 1, (i, j) -> i + j);
208 }
209 }
210
211 /**
212 * Gets the number of times this tag was added to the collection.
213 * @param tag The tag
214 * @return The number of times this tag is used in this collection.
215 * @since 14302
216 */
217 public int getTagOccurrence(Tag tag) {
218 return tags.getOrDefault(tag, 0);
219 }
220
221 /**
222 * Adds a collection of tags to the tag collection. If <code>tags</code> is null, nothing
223 * is added. null values in the collection are ignored.
224 *
225 * @param tags the collection of tags
226 */
227 public final void add(Collection<Tag> tags) {
228 if (tags == null) return;
229 for (Tag tag: tags) {
230 add(tag);
231 }
232 }
233
234 /**
235 * Adds the tags of another tag collection to this collection. Adds nothing, if
236 * <code>tags</code> is null.
237 *
238 * @param tags the other tag collection
239 */
240 public final void add(TagCollection tags) {
241 if (tags != null) {
242 for (Entry<Tag, Integer> entry : tags.tags.entrySet()) {
243 this.tags.merge(entry.getKey(), entry.getValue(), (i, j) -> i + j);
244 }
245 }
246 }
247
248 /**
249 * Removes a specific tag from the tag collection. Does nothing if <code>tag</code> is
250 * null.
251 *
252 * @param tag the tag to be removed
253 */
254 public void remove(Tag tag) {
255 if (tag == null) return;
256 tags.remove(tag);
257 }
258
259 /**
260 * Removes a collection of tags from the tag collection. Does nothing if <code>tags</code> is
261 * null.
262 *
263 * @param tags the tags to be removed
264 */
265 public void remove(Collection<Tag> tags) {
266 if (tags != null) {
267 tags.forEach(this::remove);
268 }
269 }
270
271 /**
272 * Removes all tags in the tag collection <code>tags</code> from the current tag collection.
273 * Does nothing if <code>tags</code> is null.
274 *
275 * @param tags the tag collection to be removed.
276 */
277 public void remove(TagCollection tags) {
278 if (tags != null) {
279 tags.tags.keySet().forEach(this::remove);
280 }
281 }
282
283 /**
284 * Removes all tags whose keys are equal to <code>key</code>. Does nothing if <code>key</code>
285 * is null.
286 *
287 * @param key the key to be removed
288 */
289 public void removeByKey(String key) {
290 if (key != null) {
291 tags.keySet().removeIf(tag -> tag.matchesKey(key));
292 }
293 }
294
295 /**
296 * Removes all tags whose key is in the collection <code>keys</code>. Does nothing if
297 * <code>keys</code> is null.
298 *
299 * @param keys the collection of keys to be removed
300 */
301 public void removeByKey(Collection<String> keys) {
302 if (keys == null) return;
303 for (String key: keys) {
304 removeByKey(key);
305 }
306 }
307
308 /**
309 * Replies true if the this tag collection contains <code>tag</code>.
310 *
311 * @param tag the tag to look up
312 * @return true if the this tag collection contains <code>tag</code>; false, otherwise
313 */
314 public boolean contains(Tag tag) {
315 return tags.containsKey(tag);
316 }
317
318 /**
319 * Replies true if this tag collection contains all tags in <code>tags</code>. Replies
320 * false, if tags is null.
321 *
322 * @param tags the tags to look up
323 * @return true if this tag collection contains all tags in <code>tags</code>. Replies
324 * false, if tags is null.
325 */
326 public boolean containsAll(Collection<Tag> tags) {
327 if (tags == null) {
328 return false;
329 } else {
330 return this.tags.keySet().containsAll(tags);
331 }
332 }
333
334 /**
335 * Replies true if this tag collection at least one tag for every key in <code>keys</code>.
336 * Replies false, if <code>keys</code> is null. null values in <code>keys</code> are ignored.
337 *
338 * @param keys the keys to lookup
339 * @return true if this tag collection at least one tag for every key in <code>keys</code>.
340 */
341 public boolean containsAllKeys(Collection<String> keys) {
342 if (keys == null) {
343 return false;
344 } else {
345 return keys.stream().filter(Objects::nonNull).allMatch(this::hasTagsFor);
346 }
347 }
348
349 /**
350 * Replies the number of tags with key <code>key</code>
351 *
352 * @param key the key to look up
353 * @return the number of tags with key <code>key</code>, including the empty "" value. 0, if key is null.
354 */
355 public int getNumTagsFor(String key) {
356 return (int) generateStreamForKey(key).count();
357 }
358
359 /**
360 * Replies true if there is at least one tag for the given key.
361 *
362 * @param key the key to look up
363 * @return true if there is at least one tag for the given key. false, if key is null.
364 */
365 public boolean hasTagsFor(String key) {
366 return getNumTagsFor(key) > 0;
367 }
368
369 /**
370 * Replies true it there is at least one tag with a non empty value for key.
371 * Replies false if key is null.
372 *
373 * @param key the key
374 * @return true it there is at least one tag with a non empty value for key.
375 */
376 public boolean hasValuesFor(String key) {
377 return generateStreamForKey(key).anyMatch(t -> !t.getValue().isEmpty());
378 }
379
380 /**
381 * Replies true if there is exactly one tag for <code>key</code> and
382 * if the value of this tag is not empty. Replies false if key is
383 * null.
384 *
385 * @param key the key
386 * @return true if there is exactly one tag for <code>key</code> and
387 * if the value of this tag is not empty
388 */
389 public boolean hasUniqueNonEmptyValue(String key) {
390 return generateStreamForKey(key).filter(t -> !t.getValue().isEmpty()).count() == 1;
391 }
392
393 /**
394 * Replies true if there is a tag with an empty value for <code>key</code>.
395 * Replies false, if key is null.
396 *
397 * @param key the key
398 * @return true if there is a tag with an empty value for <code>key</code>
399 */
400 public boolean hasEmptyValue(String key) {
401 return generateStreamForKey(key).anyMatch(t -> t.getValue().isEmpty());
402 }
403
404 /**
405 * Replies true if there is exactly one tag for <code>key</code> and if
406 * the value for this tag is empty. Replies false if key is null.
407 *
408 * @param key the key
409 * @return true if there is exactly one tag for <code>key</code> and if
410 * the value for this tag is empty
411 */
412 public boolean hasUniqueEmptyValue(String key) {
413 Set<String> values = getValues(key);
414 return values.size() == 1 && values.contains("");
415 }
416
417 /**
418 * Replies a tag collection with the tags for a given key. Replies an empty collection
419 * if key is null.
420 *
421 * @param key the key to look up
422 * @return a tag collection with the tags for a given key. Replies an empty collection
423 * if key is null.
424 */
425 public TagCollection getTagsFor(String key) {
426 TagCollection ret = new TagCollection();
427 generateStreamForKey(key).forEach(ret::add);
428 return ret;
429 }
430
431 /**
432 * Replies a tag collection with all tags whose key is equal to one of the keys in
433 * <code>keys</code>. Replies an empty collection if keys is null.
434 *
435 * @param keys the keys to look up
436 * @return a tag collection with all tags whose key is equal to one of the keys in
437 * <code>keys</code>
438 */
439 public TagCollection getTagsFor(Collection<String> keys) {
440 TagCollection ret = new TagCollection();
441 if (keys == null)
442 return ret;
443 for (String key : keys) {
444 if (key != null) {
445 ret.add(getTagsFor(key));
446 }
447 }
448 return ret;
449 }
450
451 /**
452 * Replies the tags of this tag collection as set
453 *
454 * @return the tags of this tag collection as set
455 */
456 public Set<Tag> asSet() {
457 return new HashSet<>(tags.keySet());
458 }
459
460 /**
461 * Replies the tags of this tag collection as list.
462 * Note that the order of the list is not preserved between method invocations.
463 *
464 * @return the tags of this tag collection as list. There are no duplicate values.
465 */
466 public List<Tag> asList() {
467 return new ArrayList<>(tags.keySet());
468 }
469
470 /**
471 * Replies an iterator to iterate over the tags in this collection
472 *
473 * @return the iterator
474 */
475 @Override
476 public Iterator<Tag> iterator() {
477 return tags.keySet().iterator();
478 }
479
480 /**
481 * Replies the set of keys of this tag collection.
482 *
483 * @return the set of keys of this tag collection
484 */
485 public Set<String> getKeys() {
486 return generateKeyStream().collect(Collectors.toCollection(HashSet::new));
487 }
488
489 /**
490 * Replies the set of keys which have at least 2 matching tags.
491 *
492 * @return the set of keys which have at least 2 matching tags.
493 */
494 public Set<String> getKeysWithMultipleValues() {
495 HashSet<String> singleKeys = new HashSet<>();
496 return generateKeyStream().filter(key -> !singleKeys.add(key)).collect(Collectors.toSet());
497 }
498
499 /**
500 * Sets a unique tag for the key of this tag. All other tags with the same key are
501 * removed from the collection. Does nothing if tag is null.
502 *
503 * @param tag the tag to set
504 */
505 public void setUniqueForKey(Tag tag) {
506 if (tag == null) return;
507 removeByKey(tag.getKey());
508 add(tag);
509 }
510
511 /**
512 * Sets a unique tag for the key of this tag. All other tags with the same key are
513 * removed from the collection. Assume the empty string for key and value if either
514 * key or value is null.
515 *
516 * @param key the key
517 * @param value the value
518 */
519 public void setUniqueForKey(String key, String value) {
520 Tag tag = new Tag(key, value);
521 setUniqueForKey(tag);
522 }
523
524 /**
525 * Replies the set of values in this tag collection
526 *
527 * @return the set of values
528 */
529 public Set<String> getValues() {
530 return tags.keySet().stream().map(Tag::getValue).collect(Collectors.toSet());
531 }
532
533 /**
534 * Replies the set of values for a given key. Replies an empty collection if there
535 * are no values for the given key.
536 *
537 * @param key the key to look up
538 * @return the set of values for a given key. Replies an empty collection if there
539 * are no values for the given key
540 */
541 public Set<String> getValues(String key) {
542 // null-safe
543 return generateStreamForKey(key).map(Tag::getValue).collect(Collectors.toSet());
544 }
545
546 /**
547 * Replies true if for every key there is one tag only, i.e. exactly one value.
548 *
549 * @return {@code true} if for every key there is one tag only
550 */
551 public boolean isApplicableToPrimitive() {
552 return getKeysWithMultipleValues().isEmpty();
553 }
554
555 /**
556 * Applies this tag collection to an {@link org.openstreetmap.josm.data.osm.OsmPrimitive}. Does nothing if
557 * primitive is null
558 *
559 * @param primitive the primitive
560 * @throws IllegalStateException if this tag collection can't be applied
561 * because there are keys with multiple values
562 */
563 public void applyTo(Tagged primitive) {
564 if (primitive == null) return;
565 ensureApplicableToPrimitive();
566 for (Tag tag: tags.keySet()) {
567 if (tag.getValue() == null || tag.getValue().isEmpty()) {
568 primitive.remove(tag.getKey());
569 } else {
570 primitive.put(tag.getKey(), tag.getValue());
571 }
572 }
573 }
574
575 /**
576 * Applies this tag collection to a collection of {@link org.openstreetmap.josm.data.osm.OsmPrimitive}s. Does nothing if
577 * primitives is null
578 *
579 * @param primitives the collection of primitives
580 * @throws IllegalStateException if this tag collection can't be applied
581 * because there are keys with multiple values
582 */
583 public void applyTo(Collection<? extends Tagged> primitives) {
584 if (primitives == null) return;
585 ensureApplicableToPrimitive();
586 for (Tagged primitive: primitives) {
587 applyTo(primitive);
588 }
589 }
590
591 /**
592 * Replaces the tags of an {@link org.openstreetmap.josm.data.osm.OsmPrimitive} by the tags in this collection . Does nothing if
593 * primitive is null
594 *
595 * @param primitive the primitive
596 * @throws IllegalStateException if this tag collection can't be applied
597 * because there are keys with multiple values
598 */
599 public void replaceTagsOf(Tagged primitive) {
600 if (primitive == null) return;
601 ensureApplicableToPrimitive();
602 primitive.removeAll();
603 for (Tag tag: tags.keySet()) {
604 primitive.put(tag.getKey(), tag.getValue());
605 }
606 }
607
608 /**
609 * Replaces the tags of a collection of{@link org.openstreetmap.josm.data.osm.OsmPrimitive}s by the tags in this collection.
610 * Does nothing if primitives is null
611 *
612 * @param primitives the collection of primitives
613 * @throws IllegalStateException if this tag collection can't be applied
614 * because there are keys with multiple values
615 */
616 public void replaceTagsOf(Collection<? extends Tagged> primitives) {
617 if (primitives == null) return;
618 ensureApplicableToPrimitive();
619 for (Tagged primitive: primitives) {
620 replaceTagsOf(primitive);
621 }
622 }
623
624 private void ensureApplicableToPrimitive() {
625 if (!isApplicableToPrimitive())
626 throw new IllegalStateException(tr("Tag collection cannot be applied to a primitive because there are keys with multiple values."));
627 }
628
629 /**
630 * Builds the intersection of this tag collection and another tag collection
631 *
632 * @param other the other tag collection. If null, replies an empty tag collection.
633 * @return the intersection of this tag collection and another tag collection. All counts are set to 1.
634 */
635 public TagCollection intersect(TagCollection other) {
636 TagCollection ret = new TagCollection();
637 if (other != null) {
638 tags.keySet().stream().filter(other::contains).forEach(ret::add);
639 }
640 return ret;
641 }
642
643 /**
644 * Replies the difference of this tag collection and another tag collection
645 *
646 * @param other the other tag collection. May be null.
647 * @return the difference of this tag collection and another tag collection
648 */
649 public TagCollection minus(TagCollection other) {
650 TagCollection ret = new TagCollection(this);
651 if (other != null) {
652 ret.remove(other);
653 }
654 return ret;
655 }
656
657 /**
658 * Replies the union of this tag collection and another tag collection
659 *
660 * @param other the other tag collection. May be null.
661 * @return the union of this tag collection and another tag collection. The tag count is summed.
662 */
663 public TagCollection union(TagCollection other) {
664 TagCollection ret = new TagCollection(this);
665 if (other != null) {
666 ret.add(other);
667 }
668 return ret;
669 }
670
671 public TagCollection emptyTagsForKeysMissingIn(TagCollection other) {
672 TagCollection ret = new TagCollection();
673 for (String key: this.minus(other).getKeys()) {
674 ret.add(new Tag(key));
675 }
676 return ret;
677 }
678
679 private static final Pattern SPLIT_VALUES_PATTERN = Pattern.compile(";\\s*");
680
681 /**
682 * Replies the concatenation of all tag values (concatenated by a semicolon)
683 * @param key the key to look up
684 *
685 * @return the concatenation of all tag values
686 */
687 public String getJoinedValues(String key) {
688
689 // See #7201 combining ways screws up the order of ref tags
690 Set<String> originalValues = getValues(key);
691 if (originalValues.size() == 1) {
692 return originalValues.iterator().next();
693 }
694
695 Set<String> values = new LinkedHashSet<>();
696 Map<String, Collection<String>> originalSplitValues = new LinkedHashMap<>();
697 for (String v : originalValues) {
698 List<String> vs = Arrays.asList(SPLIT_VALUES_PATTERN.split(v, -1));
699 originalSplitValues.put(v, vs);
700 values.addAll(vs);
701 }
702 values.remove("");
703 // try to retain an already existing key if it contains all needed values (remove this if it causes performance problems)
704 for (Entry<String, Collection<String>> i : originalSplitValues.entrySet()) {
705 if (i.getValue().containsAll(values)) {
706 return i.getKey();
707 }
708 }
709 return String.join(";", values);
710 }
711
712 /**
713 * Replies the sum of all numeric tag values. Ignores duplicates.
714 * @param key the key to look up
715 *
716 * @return the sum of all numeric tag values, as string.
717 * @since 7743
718 */
719 public String getSummedValues(String key) {
720 int result = 0;
721 for (String value : getValues(key)) {
722 try {
723 result += Integer.parseInt(value);
724 } catch (NumberFormatException e) {
725 Logging.trace(e);
726 }
727 }
728 return Integer.toString(result);
729 }
730
731 private Stream<String> generateKeyStream() {
732 return tags.keySet().stream().map(Tag::getKey);
733 }
734
735 /**
736 * Get a stream for the given key.
737 * @param key The key
738 * @return The stream. An empty stream if key is <code>null</code>
739 */
740 private Stream<Tag> generateStreamForKey(String key) {
741 return tags.keySet().stream().filter(e -> e.matchesKey(key));
742 }
743
744 @Override
745 public String toString() {
746 return tags.toString();
747 }
748}
Note: See TracBrowser for help on using the repository browser.