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.util.ArrayList;
007import java.util.Arrays;
008import java.util.Collection;
009import java.util.HashMap;
010import java.util.HashSet;
011import java.util.Iterator;
012import java.util.LinkedHashMap;
013import java.util.LinkedHashSet;
014import java.util.List;
015import java.util.Map;
016import java.util.Map.Entry;
017import java.util.Set;
018import java.util.regex.Pattern;
019
020import org.openstreetmap.josm.Main;
021import org.openstreetmap.josm.tools.Utils;
022
023/**
024 * TagCollection is a collection of tags which can be used to manipulate
025 * tags managed by {@link org.openstreetmap.josm.data.osm.OsmPrimitive}s.
026 *
027 * A TagCollection can be created:
028 * <ul>
029 *  <li>from the tags managed by a specific {@link org.openstreetmap.josm.data.osm.OsmPrimitive}
030 *  with {@link #from(org.openstreetmap.josm.data.osm.Tagged)}</li>
031 *  <li>from the union of all tags managed by a collection of {@link org.openstreetmap.josm.data.osm.OsmPrimitive}s
032 *  with {@link #unionOfAllPrimitives(java.util.Collection)}</li>
033 *  <li>from the union of all tags managed by a {@link org.openstreetmap.josm.data.osm.DataSet}
034 *  with {@link #unionOfAllPrimitives(org.openstreetmap.josm.data.osm.DataSet)}</li>
035 *  <li>from the intersection of all tags managed by a collection of primitives
036 *  with {@link #commonToAllPrimitives(java.util.Collection)}</li>
037 * </ul>
038 *
039 * It  provides methods to query the collection, like {@link #size()}, {@link #hasTagsFor(String)}, etc.
040 *
041 * Basic set operations allow to create the union, the intersection and  the difference
042 * of tag collections, see {@link #union(org.openstreetmap.josm.data.osm.TagCollection)},
043 * {@link #intersect(org.openstreetmap.josm.data.osm.TagCollection)}, and {@link #minus(org.openstreetmap.josm.data.osm.TagCollection)}.
044 *
045 * @since 2008
046 */
047public class TagCollection implements Iterable<Tag> {
048
049    /**
050     * Creates a tag collection from the tags managed by a specific
051     * {@link org.openstreetmap.josm.data.osm.OsmPrimitive}. If <code>primitive</code> is null, replies
052     * an empty tag collection.
053     *
054     * @param primitive  the primitive
055     * @return a tag collection with the tags managed by a specific
056     * {@link org.openstreetmap.josm.data.osm.OsmPrimitive}
057     */
058    public static TagCollection from(Tagged primitive) {
059        TagCollection tags = new TagCollection();
060        if (primitive != null) {
061            for (String key: primitive.keySet()) {
062                tags.add(new Tag(key, primitive.get(key)));
063            }
064        }
065        return tags;
066    }
067
068    /**
069     * Creates a tag collection from a map of key/value-pairs. Replies
070     * an empty tag collection if {@code tags} is null.
071     *
072     * @param tags  the key/value-pairs
073     * @return the tag collection
074     */
075    public static TagCollection from(Map<String, String> tags) {
076        TagCollection ret = new TagCollection();
077        if (tags == null) return ret;
078        for (Entry<String, String> entry: tags.entrySet()) {
079            String key = entry.getKey() == null ? "" : entry.getKey();
080            String value = entry.getValue() == null ? "" : entry.getValue();
081            ret.add(new Tag(key, value));
082        }
083        return ret;
084    }
085
086    /**
087     * Creates a tag collection from the union of the tags managed by
088     * a collection of primitives. Replies an empty tag collection,
089     * if <code>primitives</code> is null.
090     *
091     * @param primitives the primitives
092     * @return  a tag collection with the union of the tags managed by
093     * a collection of primitives
094     */
095    public static TagCollection unionOfAllPrimitives(Collection<? extends Tagged> primitives) {
096        TagCollection tags = new TagCollection();
097        if (primitives == null) return tags;
098        for (Tagged primitive: primitives) {
099            if (primitive == null) {
100                continue;
101            }
102            tags.add(TagCollection.from(primitive));
103        }
104        return tags;
105    }
106
107    /**
108     * Replies a tag collection with the tags which are common to all primitives in in
109     * <code>primitives</code>. Replies an empty tag collection of <code>primitives</code>
110     * is null.
111     *
112     * @param primitives the primitives
113     * @return  a tag collection with the tags which are common to all primitives
114     */
115    public static TagCollection commonToAllPrimitives(Collection<? extends Tagged> primitives) {
116        TagCollection tags = new TagCollection();
117        if (primitives == null || primitives.isEmpty()) return tags;
118        // initialize with the first
119        //
120        tags.add(TagCollection.from(primitives.iterator().next()));
121
122        // intersect with the others
123        //
124        for (Tagged primitive: primitives) {
125            if (primitive == null) {
126                continue;
127            }
128            tags.add(tags.intersect(TagCollection.from(primitive)));
129        }
130        return tags;
131    }
132
133    /**
134     * Replies a tag collection with the union of the tags which are common to all primitives in
135     * the dataset <code>ds</code>. Returns an empty tag collection of <code>ds</code> is null.
136     *
137     * @param ds the dataset
138     * @return a tag collection with the union of the tags which are common to all primitives in
139     * the dataset <code>ds</code>
140     */
141    public static TagCollection unionOfAllPrimitives(DataSet ds) {
142        TagCollection tags = new TagCollection();
143        if (ds == null) return tags;
144        tags.add(TagCollection.unionOfAllPrimitives(ds.allPrimitives()));
145        return tags;
146    }
147
148    private final Set<Tag> tags = new HashSet<>();
149
150    /**
151     * Creates an empty tag collection.
152     */
153    public TagCollection() {
154        // contents can be set later with add()
155    }
156
157    /**
158     * Creates a clone of the tag collection <code>other</code>. Creats an empty
159     * tag collection if <code>other</code> is null.
160     *
161     * @param other the other collection
162     */
163    public TagCollection(TagCollection other) {
164        if (other != null) {
165            tags.addAll(other.tags);
166        }
167    }
168
169    /**
170     * Creates a tag collection from <code>tags</code>.
171     * @param tags the collection of tags
172     * @since 5724
173     */
174    public TagCollection(Collection<Tag> tags) {
175        add(tags);
176    }
177
178    /**
179     * Replies the number of tags in this tag collection
180     *
181     * @return the number of tags in this tag collection
182     */
183    public int size() {
184        return tags.size();
185    }
186
187    /**
188     * Replies true if this tag collection is empty
189     *
190     * @return true if this tag collection is empty; false, otherwise
191     */
192    public boolean isEmpty() {
193        return size() == 0;
194    }
195
196    /**
197     * Adds a tag to the tag collection. If <code>tag</code> is null, nothing is added.
198     *
199     * @param tag the tag to add
200     */
201    public final void add(Tag tag) {
202        if (tag == null) return;
203        if (tags.contains(tag)) return;
204        tags.add(tag);
205    }
206
207    /**
208     * Adds a collection of tags to the tag collection. If <code>tags</code> is null, nothing
209     * is added. null values in the collection are ignored.
210     *
211     * @param tags the collection of tags
212     */
213    public final void add(Collection<Tag> tags) {
214        if (tags == null) return;
215        for (Tag tag: tags) {
216            add(tag);
217        }
218    }
219
220    /**
221     * Adds the tags of another tag collection to this collection. Adds nothing, if
222     * <code>tags</code> is null.
223     *
224     * @param tags the other tag collection
225     */
226    public final void add(TagCollection tags) {
227        if (tags == null) return;
228        this.tags.addAll(tags.tags);
229    }
230
231    /**
232     * Removes a specific tag from the tag collection. Does nothing if <code>tag</code> is
233     * null.
234     *
235     * @param tag the tag to be removed
236     */
237    public void remove(Tag tag) {
238        if (tag == null) return;
239        tags.remove(tag);
240    }
241
242    /**
243     * Removes a collection of tags from the tag collection. Does nothing if <code>tags</code> is
244     * null.
245     *
246     * @param tags the tags to be removed
247     */
248    public void remove(Collection<Tag> tags) {
249        if (tags == null) return;
250        this.tags.removeAll(tags);
251    }
252
253    /**
254     * Removes all tags in the tag collection <code>tags</code> from the current tag collection.
255     * Does nothing if <code>tags</code> is null.
256     *
257     * @param tags the tag collection to be removed.
258     */
259    public void remove(TagCollection tags) {
260        if (tags == null) return;
261        this.tags.removeAll(tags.tags);
262    }
263
264    /**
265     * Removes all tags whose keys are equal to  <code>key</code>. Does nothing if <code>key</code>
266     * is null.
267     *
268     * @param key the key to be removed
269     */
270    public void removeByKey(String key) {
271        if (key == null) return;
272        Iterator<Tag> it = tags.iterator();
273        while (it.hasNext()) {
274            if (it.next().matchesKey(key)) {
275                it.remove();
276            }
277        }
278    }
279
280    /**
281     * Removes all tags whose key is in the collection <code>keys</code>. Does nothing if
282     * <code>keys</code> is null.
283     *
284     * @param keys the collection of keys to be removed
285     */
286    public void removeByKey(Collection<String> keys) {
287        if (keys == null) return;
288        for (String key: keys) {
289            removeByKey(key);
290        }
291    }
292
293    /**
294     * Replies true if the this tag collection contains <code>tag</code>.
295     *
296     * @param tag the tag to look up
297     * @return true if the this tag collection contains <code>tag</code>; false, otherwise
298     */
299    public boolean contains(Tag tag) {
300        return tags.contains(tag);
301    }
302
303    /**
304     * Replies true if this tag collection contains at least one tag with key <code>key</code>.
305     *
306     * @param key the key to look up
307     * @return true if this tag collection contains at least one tag with key <code>key</code>; false, otherwise
308     */
309    public boolean containsKey(String key) {
310        if (key == null) return false;
311        for (Tag tag: tags) {
312            if (tag.matchesKey(key)) return true;
313        }
314        return false;
315    }
316
317    /**
318     * Replies true if this tag collection contains all tags in <code>tags</code>. Replies
319     * false, if tags is null.
320     *
321     * @param tags the tags to look up
322     * @return true if this tag collection contains all tags in <code>tags</code>. Replies
323     * false, if tags is null.
324     */
325    public boolean containsAll(Collection<Tag> tags) {
326        if (tags == null) return false;
327        return this.tags.containsAll(tags);
328    }
329
330    /**
331     * Replies true if this tag collection at least one tag for every key in <code>keys</code>.
332     * Replies false, if <code>keys</code> is null. null values in <code>keys</code> are ignored.
333     *
334     * @param keys the keys to lookup
335     * @return true if this tag collection at least one tag for every key in <code>keys</code>.
336     */
337    public boolean containsAllKeys(Collection<String> keys) {
338        if (keys == null) return false;
339        for (String key: keys) {
340            if (key == null) {
341                continue;
342            }
343            if (!containsKey(key)) return false;
344        }
345        return true;
346    }
347
348    /**
349     * Replies the number of tags with key <code>key</code>
350     *
351     * @param key the key to look up
352     * @return the number of tags with key <code>key</code>. 0, if key is null.
353     */
354    public int getNumTagsFor(String key) {
355        if (key == null) return 0;
356        int count = 0;
357        for (Tag tag: tags) {
358            if (tag.matchesKey(key)) {
359                count++;
360            }
361        }
362        return count;
363    }
364
365    /**
366     * Replies true if there is at least one tag for the given key.
367     *
368     * @param key the key to look up
369     * @return true if there is at least one tag for the given key. false, if key is null.
370     */
371    public boolean hasTagsFor(String key) {
372        return getNumTagsFor(key) > 0;
373    }
374
375    /**
376     * Replies true it there is at least one tag with a non empty value for key.
377     * Replies false if key is null.
378     *
379     * @param key the key
380     * @return true it there is at least one tag with a non empty value for key.
381     */
382    public boolean hasValuesFor(String key) {
383        if (key == null) return false;
384        Set<String> values = getTagsFor(key).getValues();
385        values.remove("");
386        return !values.isEmpty();
387    }
388
389    /**
390     * Replies true if there is exactly one tag for <code>key</code> and
391     * if the value of this tag is not empty. Replies false if key is
392     * null.
393     *
394     * @param key the key
395     * @return true if there is exactly one tag for <code>key</code> and
396     * if the value of this tag is not empty
397     */
398    public boolean hasUniqueNonEmptyValue(String key) {
399        if (key == null) return false;
400        Set<String> values = getTagsFor(key).getValues();
401        return values.size() == 1 && !values.contains("");
402    }
403
404    /**
405     * Replies true if there is a tag with an empty value for <code>key</code>.
406     * Replies false, if key is null.
407     *
408     * @param key the key
409     * @return true if there is a tag with an empty value for <code>key</code>
410     */
411    public boolean hasEmptyValue(String key) {
412        if (key == null) return false;
413        Set<String> values = getTagsFor(key).getValues();
414        return values.contains("");
415    }
416
417    /**
418     * Replies true if there is exactly one tag for <code>key</code> and if
419     * the value for this tag is empty. Replies false if key is null.
420     *
421     * @param key the key
422     * @return  true if there is exactly one tag for <code>key</code> and if
423     * the value for this tag is empty
424     */
425    public boolean hasUniqueEmptyValue(String key) {
426        if (key == null) return false;
427        Set<String> values = getTagsFor(key).getValues();
428        return values.size() == 1 && values.contains("");
429    }
430
431    /**
432     * Replies a tag collection with the tags for a given key. Replies an empty collection
433     * if key is null.
434     *
435     * @param key the key to look up
436     * @return a tag collection with the tags for a given key. Replies an empty collection
437     * if key is null.
438     */
439    public TagCollection getTagsFor(String key) {
440        TagCollection ret = new TagCollection();
441        if (key == null)
442            return ret;
443        for (Tag tag: tags) {
444            if (tag.matchesKey(key)) {
445                ret.add(tag);
446            }
447        }
448        return ret;
449    }
450
451    /**
452     * Replies a tag collection with all tags whose key is equal to one of the keys in
453     * <code>keys</code>. Replies an empty collection if keys is null.
454     *
455     * @param keys the keys to look up
456     * @return a tag collection with all tags whose key is equal to one of the keys in
457     * <code>keys</code>
458     */
459    public TagCollection getTagsFor(Collection<String> keys) {
460        TagCollection ret = new TagCollection();
461        if (keys == null)
462            return ret;
463        for (String key : keys) {
464            if (key != null) {
465                ret.add(getTagsFor(key));
466            }
467        }
468        return ret;
469    }
470
471    /**
472     * Replies the tags of this tag collection as set
473     *
474     * @return the tags of this tag collection as set
475     */
476    public Set<Tag> asSet() {
477        return new HashSet<>(tags);
478    }
479
480    /**
481     * Replies the tags of this tag collection as list.
482     * Note that the order of the list is not preserved between method invocations.
483     *
484     * @return the tags of this tag collection as list.
485     */
486    public List<Tag> asList() {
487        return new ArrayList<>(tags);
488    }
489
490    /**
491     * Replies an iterator to iterate over the tags in this collection
492     *
493     * @return the iterator
494     */
495    @Override
496    public Iterator<Tag> iterator() {
497        return tags.iterator();
498    }
499
500    /**
501     * Replies the set of keys of this tag collection.
502     *
503     * @return the set of keys of this tag collection
504     */
505    public Set<String> getKeys() {
506        Set<String> ret = new HashSet<>();
507        for (Tag tag: tags) {
508            ret.add(tag.getKey());
509        }
510        return ret;
511    }
512
513    /**
514     * Replies the set of keys which have at least 2 matching tags.
515     *
516     * @return the set of keys which have at least 2 matching tags.
517     */
518    public Set<String> getKeysWithMultipleValues() {
519        Map<String, Integer> counters = new HashMap<>();
520        for (Tag tag: tags) {
521            Integer v = counters.get(tag.getKey());
522            counters.put(tag.getKey(), (v == null) ? 1 : v+1);
523        }
524        Set<String> ret = new HashSet<>();
525        for (Entry<String, Integer> e : counters.entrySet()) {
526            if (e.getValue() > 1) {
527                ret.add(e.getKey());
528            }
529        }
530        return ret;
531    }
532
533    /**
534     * Sets a unique tag for the key of this tag. All other tags with the same key are
535     * removed from the collection. Does nothing if tag is null.
536     *
537     * @param tag the tag to set
538     */
539    public void setUniqueForKey(Tag tag) {
540        if (tag == null) return;
541        removeByKey(tag.getKey());
542        add(tag);
543    }
544
545    /**
546     * Sets a unique tag for the key of this tag. All other tags with the same key are
547     * removed from the collection. Assume the empty string for key and value if either
548     * key or value is null.
549     *
550     * @param key the key
551     * @param value the value
552     */
553    public void setUniqueForKey(String key, String value) {
554        Tag tag = new Tag(key, value);
555        setUniqueForKey(tag);
556    }
557
558    /**
559     * Replies the set of values in this tag collection
560     *
561     * @return the set of values
562     */
563    public Set<String> getValues() {
564        Set<String> ret = new HashSet<>();
565        for (Tag tag: tags) {
566            ret.add(tag.getValue());
567        }
568        return ret;
569    }
570
571    /**
572     * Replies the set of values for a given key. Replies an empty collection if there
573     * are no values for the given key.
574     *
575     * @param key the key to look up
576     * @return the set of values for a given key. Replies an empty collection if there
577     * are no values for the given key
578     */
579    public Set<String> getValues(String key) {
580        Set<String> ret = new HashSet<>();
581        if (key == null) return ret;
582        for (Tag tag: tags) {
583            if (tag.matchesKey(key)) {
584                ret.add(tag.getValue());
585            }
586        }
587        return ret;
588    }
589
590    /**
591     * Replies true if for every key there is one tag only, i.e. exactly one value.
592     *
593     * @return {@code true} if for every key there is one tag only
594     */
595    public boolean isApplicableToPrimitive() {
596        return size() == getKeys().size();
597    }
598
599    /**
600     * Applies this tag collection to an {@link org.openstreetmap.josm.data.osm.OsmPrimitive}. Does nothing if
601     * primitive is null
602     *
603     * @param primitive  the primitive
604     * @throws IllegalStateException if this tag collection can't be applied
605     * because there are keys with multiple values
606     */
607    public void applyTo(Tagged primitive) {
608        if (primitive == null) return;
609        if (!isApplicableToPrimitive())
610            throw new IllegalStateException(tr("Tag collection cannot be applied to a primitive because there are keys with multiple values."));
611        for (Tag tag: tags) {
612            if (tag.getValue() == null || tag.getValue().isEmpty()) {
613                primitive.remove(tag.getKey());
614            } else {
615                primitive.put(tag.getKey(), tag.getValue());
616            }
617        }
618    }
619
620    /**
621     * Applies this tag collection to a collection of {@link org.openstreetmap.josm.data.osm.OsmPrimitive}s. Does nothing if
622     * primitives is null
623     *
624     * @param primitives the collection of primitives
625     * @throws IllegalStateException if this tag collection can't be applied
626     * because there are keys with multiple values
627     */
628    public void applyTo(Collection<? extends Tagged> primitives) {
629        if (primitives == null) return;
630        if (!isApplicableToPrimitive())
631            throw new IllegalStateException(tr("Tag collection cannot be applied to a primitive because there are keys with multiple values."));
632        for (Tagged primitive: primitives) {
633            applyTo(primitive);
634        }
635    }
636
637    /**
638     * Replaces the tags of an {@link org.openstreetmap.josm.data.osm.OsmPrimitive} by the tags in this collection . Does nothing if
639     * primitive is null
640     *
641     * @param primitive  the primitive
642     * @throws IllegalStateException if this tag collection can't be applied
643     * because there are keys with multiple values
644     */
645    public void replaceTagsOf(Tagged primitive) {
646        if (primitive == null) return;
647        if (!isApplicableToPrimitive())
648            throw new IllegalStateException(tr("Tag collection cannot be applied to a primitive because there are keys with multiple values."));
649        primitive.removeAll();
650        for (Tag tag: tags) {
651            primitive.put(tag.getKey(), tag.getValue());
652        }
653    }
654
655    /**
656     * Replaces the tags of a collection of{@link org.openstreetmap.josm.data.osm.OsmPrimitive}s by the tags in this collection.
657     * Does nothing if primitives is null
658     *
659     * @param primitives the collection of primitives
660     * @throws IllegalStateException if this tag collection can't be applied
661     * because there are keys with multiple values
662     */
663    public void replaceTagsOf(Collection<? extends Tagged> primitives) {
664        if (primitives == null) return;
665        if (!isApplicableToPrimitive())
666            throw new IllegalStateException(tr("Tag collection cannot be applied to a primitive because there are keys with multiple values."));
667        for (Tagged primitive: primitives) {
668            replaceTagsOf(primitive);
669        }
670    }
671
672    /**
673     * Builds the intersection of this tag collection and another tag collection
674     *
675     * @param other the other tag collection. If null, replies an empty tag collection.
676     * @return the intersection of this tag collection and another tag collection
677     */
678    public TagCollection intersect(TagCollection other) {
679        TagCollection ret = new TagCollection();
680        if (other != null) {
681            for (Tag tag: tags) {
682                if (other.contains(tag)) {
683                    ret.add(tag);
684                }
685            }
686        }
687        return ret;
688    }
689
690    /**
691     * Replies the difference of this tag collection and another tag collection
692     *
693     * @param other the other tag collection. May be null.
694     * @return the difference of this tag collection and another tag collection
695     */
696    public TagCollection minus(TagCollection other) {
697        TagCollection ret = new TagCollection(this);
698        if (other != null) {
699            ret.remove(other);
700        }
701        return ret;
702    }
703
704    /**
705     * Replies the union of this tag collection and another tag collection
706     *
707     * @param other the other tag collection. May be null.
708     * @return the union of this tag collection and another tag collection
709     */
710    public TagCollection union(TagCollection other) {
711        TagCollection ret = new TagCollection(this);
712        if (other != null) {
713            ret.add(other);
714        }
715        return ret;
716    }
717
718    public TagCollection emptyTagsForKeysMissingIn(TagCollection other) {
719        TagCollection ret = new TagCollection();
720        for (String key: this.minus(other).getKeys()) {
721            ret.add(new Tag(key));
722        }
723        return ret;
724    }
725
726    private static final Pattern SPLIT_VALUES_PATTERN = Pattern.compile(";\\s*");
727
728    /**
729     * Replies the concatenation of all tag values (concatenated by a semicolon)
730     * @param key the key to look up
731     *
732     * @return the concatenation of all tag values
733     */
734    public String getJoinedValues(String key) {
735
736        // See #7201 combining ways screws up the order of ref tags
737        Set<String> originalValues = getValues(key);
738        if (originalValues.size() == 1) {
739            return originalValues.iterator().next();
740        }
741
742        Set<String> values = new LinkedHashSet<>();
743        Map<String, Collection<String>> originalSplitValues = new LinkedHashMap<>();
744        for (String v : originalValues) {
745            List<String> vs = Arrays.asList(SPLIT_VALUES_PATTERN.split(v));
746            originalSplitValues.put(v, vs);
747            values.addAll(vs);
748        }
749        values.remove("");
750        // try to retain an already existing key if it contains all needed values (remove this if it causes performance problems)
751        for (Entry<String, Collection<String>> i : originalSplitValues.entrySet()) {
752            if (i.getValue().containsAll(values)) {
753                return i.getKey();
754            }
755        }
756        return Utils.join(";", values);
757    }
758
759    /**
760     * Replies the sum of all numeric tag values.
761     * @param key the key to look up
762     *
763     * @return the sum of all numeric tag values, as string
764     * @since 7743
765     */
766    public String getSummedValues(String key) {
767        int result = 0;
768        for (String value : getValues(key)) {
769            try {
770                result += Integer.parseInt(value);
771            } catch (NumberFormatException e) {
772                if (Main.isTraceEnabled()) {
773                    Main.trace(e.getMessage());
774                }
775            }
776        }
777        return Integer.toString(result);
778    }
779
780    @Override
781    public String toString() {
782        return tags.toString();
783    }
784}