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

Last change on this file since 13434 was 12620, checked in by Don-vip, 7 years ago

see #15182 - deprecate all Main logging methods and introduce suitable replacements in Logging for most of them

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