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