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}