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

Last change on this file since 11277 was 11097, checked in by Don-vip, 8 years ago

sonar - squid:S1612 - Lambdas should be replaced with method references

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