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