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

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

fix #13271 - Make TagCollection count the occurence of tags (patch by michael2402) - gsoc-core

  • Property svn:eol-style set to native
File size: 25.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.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 at least one tag with key <code>key</code>.
327 *
328 * @param key the key to look up
329 * @return true if this tag collection contains at least one tag with key <code>key</code>; false, otherwise
330 * @deprecated Use {@link #hasTagsFor(String)} instead.
331 */
332 @Deprecated
333 public boolean containsKey(String key) {
334 return generateStreamForKey(key).findAny().isPresent();
335 }
336
337 /**
338 * Replies true if this tag collection contains all tags in <code>tags</code>. Replies
339 * false, if tags is null.
340 *
341 * @param tags the tags to look up
342 * @return true if this tag collection contains all tags in <code>tags</code>. Replies
343 * false, if tags is null.
344 */
345 public boolean containsAll(Collection<Tag> tags) {
346 if (tags == null) {
347 return false;
348 } else {
349 return this.tags.keySet().containsAll(tags);
350 }
351 }
352
353 /**
354 * Replies true if this tag collection at least one tag for every key in <code>keys</code>.
355 * Replies false, if <code>keys</code> is null. null values in <code>keys</code> are ignored.
356 *
357 * @param keys the keys to lookup
358 * @return true if this tag collection at least one tag for every key in <code>keys</code>.
359 */
360 public boolean containsAllKeys(Collection<String> keys) {
361 if (keys == null) {
362 return false;
363 } else {
364 return keys.stream().filter(Objects::nonNull).allMatch(this::hasTagsFor);
365 }
366 }
367
368 /**
369 * Replies the number of tags with key <code>key</code>
370 *
371 * @param key the key to look up
372 * @return the number of tags with key <code>key</code>, including the empty "" value. 0, if key is null.
373 */
374 public int getNumTagsFor(String key) {
375 return (int) generateStreamForKey(key).count();
376 }
377
378 /**
379 * Replies true if there is at least one tag for the given key.
380 *
381 * @param key the key to look up
382 * @return true if there is at least one tag for the given key. false, if key is null.
383 */
384 public boolean hasTagsFor(String key) {
385 return getNumTagsFor(key) > 0;
386 }
387
388 /**
389 * Replies true it there is at least one tag with a non empty value for key.
390 * Replies false if key is null.
391 *
392 * @param key the key
393 * @return true it there is at least one tag with a non empty value for key.
394 */
395 public boolean hasValuesFor(String key) {
396 return generateStreamForKey(key).filter(t -> !t.getValue().isEmpty()).findAny().isPresent();
397 }
398
399 /**
400 * Replies true if there is exactly one tag for <code>key</code> and
401 * if the value of this tag is not empty. Replies false if key is
402 * null.
403 *
404 * @param key the key
405 * @return true if there is exactly one tag for <code>key</code> and
406 * if the value of this tag is not empty
407 */
408 public boolean hasUniqueNonEmptyValue(String key) {
409 return generateStreamForKey(key).filter(t -> !t.getValue().isEmpty()).count() == 1;
410 }
411
412 /**
413 * Replies true if there is a tag with an empty value for <code>key</code>.
414 * Replies false, if key is null.
415 *
416 * @param key the key
417 * @return true if there is a tag with an empty value for <code>key</code>
418 */
419 public boolean hasEmptyValue(String key) {
420 return generateStreamForKey(key).anyMatch(t -> t.getValue().isEmpty());
421 }
422
423 /**
424 * Replies true if there is exactly one tag for <code>key</code> and if
425 * the value for this tag is empty. Replies false if key is null.
426 *
427 * @param key the key
428 * @return true if there is exactly one tag for <code>key</code> and if
429 * the value for this tag is empty
430 */
431 public boolean hasUniqueEmptyValue(String key) {
432 Set<String> values = getValues(key);
433 return values.size() == 1 && values.contains("");
434 }
435
436 /**
437 * Replies a tag collection with the tags for a given key. Replies an empty collection
438 * if key is null.
439 *
440 * @param key the key to look up
441 * @return a tag collection with the tags for a given key. Replies an empty collection
442 * if key is null.
443 */
444 public TagCollection getTagsFor(String key) {
445 TagCollection ret = new TagCollection();
446 generateStreamForKey(key).forEach(ret::add);
447 return ret;
448 }
449
450 /**
451 * Replies a tag collection with all tags whose key is equal to one of the keys in
452 * <code>keys</code>. Replies an empty collection if keys is null.
453 *
454 * @param keys the keys to look up
455 * @return a tag collection with all tags whose key is equal to one of the keys in
456 * <code>keys</code>
457 */
458 public TagCollection getTagsFor(Collection<String> keys) {
459 TagCollection ret = new TagCollection();
460 if (keys == null)
461 return ret;
462 for (String key : keys) {
463 if (key != null) {
464 ret.add(getTagsFor(key));
465 }
466 }
467 return ret;
468 }
469
470 /**
471 * Replies the tags of this tag collection as set
472 *
473 * @return the tags of this tag collection as set
474 */
475 public Set<Tag> asSet() {
476 return new HashSet<>(tags.keySet());
477 }
478
479 /**
480 * Replies the tags of this tag collection as list.
481 * Note that the order of the list is not preserved between method invocations.
482 *
483 * @return the tags of this tag collection as list. There are no dupplicate values.
484 */
485 public List<Tag> asList() {
486 return new ArrayList<>(tags.keySet());
487 }
488
489 /**
490 * Replies an iterator to iterate over the tags in this collection
491 *
492 * @return the iterator
493 */
494 @Override
495 public Iterator<Tag> iterator() {
496 return tags.keySet().iterator();
497 }
498
499 /**
500 * Replies the set of keys of this tag collection.
501 *
502 * @return the set of keys of this tag collection
503 */
504 public Set<String> getKeys() {
505 return generateKeyStream().collect(Collectors.toCollection(HashSet::new));
506 }
507
508 /**
509 * Replies the set of keys which have at least 2 matching tags.
510 *
511 * @return the set of keys which have at least 2 matching tags.
512 */
513 public Set<String> getKeysWithMultipleValues() {
514 HashSet<String> singleKeys = new HashSet<>();
515 return generateKeyStream().filter(key -> !singleKeys.add(key)).collect(Collectors.toSet());
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. Does nothing if tag is null.
521 *
522 * @param tag the tag to set
523 */
524 public void setUniqueForKey(Tag tag) {
525 if (tag == null) return;
526 removeByKey(tag.getKey());
527 add(tag);
528 }
529
530 /**
531 * Sets a unique tag for the key of this tag. All other tags with the same key are
532 * removed from the collection. Assume the empty string for key and value if either
533 * key or value is null.
534 *
535 * @param key the key
536 * @param value the value
537 */
538 public void setUniqueForKey(String key, String value) {
539 Tag tag = new Tag(key, value);
540 setUniqueForKey(tag);
541 }
542
543 /**
544 * Replies the set of values in this tag collection
545 *
546 * @return the set of values
547 */
548 public Set<String> getValues() {
549 return tags.keySet().stream().map(e -> e.getValue()).collect(Collectors.toSet());
550 }
551
552 /**
553 * Replies the set of values for a given key. Replies an empty collection if there
554 * are no values for the given key.
555 *
556 * @param key the key to look up
557 * @return the set of values for a given key. Replies an empty collection if there
558 * are no values for the given key
559 */
560 public Set<String> getValues(String key) {
561 // null-safe
562 return generateStreamForKey(key).map(e -> e.getValue()).collect(Collectors.toSet());
563 }
564
565 /**
566 * Replies true if for every key there is one tag only, i.e. exactly one value.
567 *
568 * @return {@code true} if for every key there is one tag only
569 */
570 public boolean isApplicableToPrimitive() {
571 return getKeysWithMultipleValues().isEmpty();
572 }
573
574 /**
575 * Applies this tag collection to an {@link org.openstreetmap.josm.data.osm.OsmPrimitive}. Does nothing if
576 * primitive is null
577 *
578 * @param primitive the primitive
579 * @throws IllegalStateException if this tag collection can't be applied
580 * because there are keys with multiple values
581 */
582 public void applyTo(Tagged primitive) {
583 if (primitive == null) return;
584 ensureApplicableToPrimitive();
585 for (Tag tag: tags.keySet()) {
586 if (tag.getValue() == null || tag.getValue().isEmpty()) {
587 primitive.remove(tag.getKey());
588 } else {
589 primitive.put(tag.getKey(), tag.getValue());
590 }
591 }
592 }
593
594 /**
595 * Applies this tag collection to a collection of {@link org.openstreetmap.josm.data.osm.OsmPrimitive}s. Does nothing if
596 * primitives is null
597 *
598 * @param primitives the collection of primitives
599 * @throws IllegalStateException if this tag collection can't be applied
600 * because there are keys with multiple values
601 */
602 public void applyTo(Collection<? extends Tagged> primitives) {
603 if (primitives == null) return;
604 ensureApplicableToPrimitive();
605 for (Tagged primitive: primitives) {
606 applyTo(primitive);
607 }
608 }
609
610 /**
611 * Replaces the tags of an {@link org.openstreetmap.josm.data.osm.OsmPrimitive} by the tags in this collection . Does nothing if
612 * primitive is null
613 *
614 * @param primitive the primitive
615 * @throws IllegalStateException if this tag collection can't be applied
616 * because there are keys with multiple values
617 */
618 public void replaceTagsOf(Tagged primitive) {
619 if (primitive == null) return;
620 ensureApplicableToPrimitive();
621 primitive.removeAll();
622 for (Tag tag: tags.keySet()) {
623 primitive.put(tag.getKey(), tag.getValue());
624 }
625 }
626
627 /**
628 * Replaces the tags of a collection of{@link org.openstreetmap.josm.data.osm.OsmPrimitive}s by the tags in this collection.
629 * Does nothing if primitives is null
630 *
631 * @param primitives the collection of primitives
632 * @throws IllegalStateException if this tag collection can't be applied
633 * because there are keys with multiple values
634 */
635 public void replaceTagsOf(Collection<? extends Tagged> primitives) {
636 if (primitives == null) return;
637 ensureApplicableToPrimitive();
638 for (Tagged primitive: primitives) {
639 replaceTagsOf(primitive);
640 }
641 }
642
643 private void ensureApplicableToPrimitive() {
644 if (!isApplicableToPrimitive())
645 throw new IllegalStateException(tr("Tag collection cannot be applied to a primitive because there are keys with multiple values."));
646 }
647
648 /**
649 * Builds the intersection of this tag collection and another tag collection
650 *
651 * @param other the other tag collection. If null, replies an empty tag collection.
652 * @return the intersection of this tag collection and another tag collection. All counts are set to 1.
653 */
654 public TagCollection intersect(TagCollection other) {
655 TagCollection ret = new TagCollection();
656 if (other != null) {
657 tags.keySet().stream().filter(other::contains).forEach(ret::add);
658 }
659 return ret;
660 }
661
662 /**
663 * Replies the difference of this tag collection and another tag collection
664 *
665 * @param other the other tag collection. May be null.
666 * @return the difference of this tag collection and another tag collection
667 */
668 public TagCollection minus(TagCollection other) {
669 TagCollection ret = new TagCollection(this);
670 if (other != null) {
671 ret.remove(other);
672 }
673 return ret;
674 }
675
676 /**
677 * Replies the union of this tag collection and another tag collection
678 *
679 * @param other the other tag collection. May be null.
680 * @return the union of this tag collection and another tag collection. The tag count is summed.
681 */
682 public TagCollection union(TagCollection other) {
683 TagCollection ret = new TagCollection(this);
684 if (other != null) {
685 ret.add(other);
686 }
687 return ret;
688 }
689
690 public TagCollection emptyTagsForKeysMissingIn(TagCollection other) {
691 TagCollection ret = new TagCollection();
692 for (String key: this.minus(other).getKeys()) {
693 ret.add(new Tag(key));
694 }
695 return ret;
696 }
697
698 private static final Pattern SPLIT_VALUES_PATTERN = Pattern.compile(";\\s*");
699
700 /**
701 * Replies the concatenation of all tag values (concatenated by a semicolon)
702 * @param key the key to look up
703 *
704 * @return the concatenation of all tag values
705 */
706 public String getJoinedValues(String key) {
707
708 // See #7201 combining ways screws up the order of ref tags
709 Set<String> originalValues = getValues(key);
710 if (originalValues.size() == 1) {
711 return originalValues.iterator().next();
712 }
713
714 Set<String> values = new LinkedHashSet<>();
715 Map<String, Collection<String>> originalSplitValues = new LinkedHashMap<>();
716 for (String v : originalValues) {
717 List<String> vs = Arrays.asList(SPLIT_VALUES_PATTERN.split(v));
718 originalSplitValues.put(v, vs);
719 values.addAll(vs);
720 }
721 values.remove("");
722 // try to retain an already existing key if it contains all needed values (remove this if it causes performance problems)
723 for (Entry<String, Collection<String>> i : originalSplitValues.entrySet()) {
724 if (i.getValue().containsAll(values)) {
725 return i.getKey();
726 }
727 }
728 return Utils.join(";", values);
729 }
730
731 /**
732 * Replies the sum of all numeric tag values. Ignores dupplicates.
733 * @param key the key to look up
734 *
735 * @return the sum of all numeric tag values, as string.
736 * @since 7743
737 */
738 public String getSummedValues(String key) {
739 int result = 0;
740 for (String value : getValues(key)) {
741 try {
742 result += Integer.parseInt(value);
743 } catch (NumberFormatException e) {
744 Main.trace(e);
745 }
746 }
747 return Integer.toString(result);
748 }
749
750 private Stream<String> generateKeyStream() {
751 return tags.keySet().stream().map(tag -> tag.getKey());
752 }
753
754 /**
755 * Get a stram for the given key.
756 * @param key The key
757 * @return The stream. An empty stream if key is <code>null</code>
758 */
759 private Stream<Tag> generateStreamForKey(String key) {
760 return tags.keySet().stream().filter(e -> e.matchesKey(key));
761 }
762
763 @Override
764 public String toString() {
765 return tags.toString();
766 }
767}
Note: See TracBrowser for help on using the repository browser.