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