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