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