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