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