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            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}