001// License: GPL. For details, see LICENSE file.
002package org.openstreetmap.josm.data.osm;
003
004import static org.openstreetmap.josm.tools.I18n.tr;
005
006import java.io.Serializable;
007import java.util.ArrayList;
008import java.util.Arrays;
009import java.util.Collection;
010import java.util.HashMap;
011import java.util.HashSet;
012import java.util.Iterator;
013import java.util.LinkedHashMap;
014import java.util.LinkedHashSet;
015import java.util.List;
016import java.util.Map;
017import java.util.Map.Entry;
018import java.util.Objects;
019import java.util.Set;
020import java.util.regex.Pattern;
021import java.util.stream.Collectors;
022import java.util.stream.Stream;
023
024import org.openstreetmap.josm.Main;
025import org.openstreetmap.josm.tools.Utils;
026
027/**
028 * TagCollection is a collection of tags which can be used to manipulate
029 * tags managed by {@link org.openstreetmap.josm.data.osm.OsmPrimitive}s.
030 *
031 * A TagCollection can be created:
032 * <ul>
033 *  <li>from the tags managed by a specific {@link org.openstreetmap.josm.data.osm.OsmPrimitive}
034 *  with {@link #from(org.openstreetmap.josm.data.osm.Tagged)}</li>
035 *  <li>from the union of all tags managed by a collection of {@link org.openstreetmap.josm.data.osm.OsmPrimitive}s
036 *  with {@link #unionOfAllPrimitives(java.util.Collection)}</li>
037 *  <li>from the union of all tags managed by a {@link org.openstreetmap.josm.data.osm.DataSet}
038 *  with {@link #unionOfAllPrimitives(org.openstreetmap.josm.data.osm.DataSet)}</li>
039 *  <li>from the intersection of all tags managed by a collection of primitives
040 *  with {@link #commonToAllPrimitives(java.util.Collection)}</li>
041 * </ul>
042 *
043 * It  provides methods to query the collection, like {@link #size()}, {@link #hasTagsFor(String)}, etc.
044 *
045 * Basic set operations allow to create the union, the intersection and  the difference
046 * of tag collections, see {@link #union(org.openstreetmap.josm.data.osm.TagCollection)},
047 * {@link #intersect(org.openstreetmap.josm.data.osm.TagCollection)}, and {@link #minus(org.openstreetmap.josm.data.osm.TagCollection)}.
048 *
049 * @since 2008
050 */
051public class TagCollection implements Iterable<Tag>, Serializable {
052
053    private static final long serialVersionUID = 1;
054
055    /**
056     * Creates a tag collection from the tags managed by a specific
057     * {@link org.openstreetmap.josm.data.osm.OsmPrimitive}. If <code>primitive</code> is null, replies
058     * an empty tag collection.
059     *
060     * @param primitive  the primitive
061     * @return a tag collection with the tags managed by a specific
062     * {@link org.openstreetmap.josm.data.osm.OsmPrimitive}
063     */
064    public static TagCollection from(Tagged primitive) {
065        TagCollection tags = new TagCollection();
066        if (primitive != null) {
067            for (String key: primitive.keySet()) {
068                tags.add(new Tag(key, primitive.get(key)));
069            }
070        }
071        return tags;
072    }
073
074    /**
075     * Creates a tag collection from a map of key/value-pairs. Replies
076     * an empty tag collection if {@code tags} is null.
077     *
078     * @param tags  the key/value-pairs
079     * @return the tag collection
080     */
081    public static TagCollection from(Map<String, String> tags) {
082        TagCollection ret = new TagCollection();
083        if (tags == null) return ret;
084        for (Entry<String, String> entry: tags.entrySet()) {
085            String key = entry.getKey() == null ? "" : entry.getKey();
086            String value = entry.getValue() == null ? "" : entry.getValue();
087            ret.add(new Tag(key, value));
088        }
089        return ret;
090    }
091
092    /**
093     * Creates a tag collection from the union of the tags managed by
094     * a collection of primitives. Replies an empty tag collection,
095     * if <code>primitives</code> is null.
096     *
097     * @param primitives the primitives
098     * @return  a tag collection with the union of the tags managed by
099     * 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).filter(t -> !t.getValue().isEmpty()).findAny().isPresent();
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                Main.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}