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 14302 219 */ 220 public int getTagOccurrence(Tag tag) { 221 return tags.getOrDefault(tag, 0); 222 } 223 224 /** 225 * Adds a collection of tags to the tag collection. If <code>tags</code> is null, nothing 226 * is added. null values in the collection are ignored. 227 * 228 * @param tags the collection of tags 229 */ 230 public final void add(Collection<Tag> tags) { 231 if (tags == null) return; 232 for (Tag tag: tags) { 233 add(tag); 234 } 235 } 236 237 /** 238 * Adds the tags of another tag collection to this collection. Adds nothing, if 239 * <code>tags</code> is null. 240 * 241 * @param tags the other tag collection 242 */ 243 public final void add(TagCollection tags) { 244 if (tags != null) { 245 for (Entry<Tag, Integer> entry : tags.tags.entrySet()) { 246 this.tags.merge(entry.getKey(), entry.getValue(), (i, j) -> i + j); 247 } 248 } 249 } 250 251 /** 252 * Removes a specific tag from the tag collection. Does nothing if <code>tag</code> is 253 * null. 254 * 255 * @param tag the tag to be removed 256 */ 257 public void remove(Tag tag) { 258 if (tag == null) return; 259 tags.remove(tag); 260 } 261 262 /** 263 * Removes a collection of tags from the tag collection. Does nothing if <code>tags</code> is 264 * null. 265 * 266 * @param tags the tags to be removed 267 */ 268 public void remove(Collection<Tag> tags) { 269 if (tags != null) { 270 tags.stream().forEach(this::remove); 271 } 272 } 273 274 /** 275 * Removes all tags in the tag collection <code>tags</code> from the current tag collection. 276 * Does nothing if <code>tags</code> is null. 277 * 278 * @param tags the tag collection to be removed. 279 */ 280 public void remove(TagCollection tags) { 281 if (tags != null) { 282 tags.tags.keySet().stream().forEach(this::remove); 283 } 284 } 285 286 /** 287 * Removes all tags whose keys are equal to <code>key</code>. Does nothing if <code>key</code> 288 * is null. 289 * 290 * @param key the key to be removed 291 */ 292 public void removeByKey(String key) { 293 if (key != null) { 294 tags.keySet().removeIf(tag -> tag.matchesKey(key)); 295 } 296 } 297 298 /** 299 * Removes all tags whose key is in the collection <code>keys</code>. Does nothing if 300 * <code>keys</code> is null. 301 * 302 * @param keys the collection of keys to be removed 303 */ 304 public void removeByKey(Collection<String> keys) { 305 if (keys == null) return; 306 for (String key: keys) { 307 removeByKey(key); 308 } 309 } 310 311 /** 312 * Replies true if the this tag collection contains <code>tag</code>. 313 * 314 * @param tag the tag to look up 315 * @return true if the this tag collection contains <code>tag</code>; false, otherwise 316 */ 317 public boolean contains(Tag tag) { 318 return tags.containsKey(tag); 319 } 320 321 /** 322 * Replies true if this tag collection contains all tags in <code>tags</code>. Replies 323 * false, if tags is null. 324 * 325 * @param tags the tags to look up 326 * @return true if this tag collection contains all tags in <code>tags</code>. Replies 327 * false, if tags is null. 328 */ 329 public boolean containsAll(Collection<Tag> tags) { 330 if (tags == null) { 331 return false; 332 } else { 333 return this.tags.keySet().containsAll(tags); 334 } 335 } 336 337 /** 338 * Replies true if this tag collection at least one tag for every key in <code>keys</code>. 339 * Replies false, if <code>keys</code> is null. null values in <code>keys</code> are ignored. 340 * 341 * @param keys the keys to lookup 342 * @return true if this tag collection at least one tag for every key in <code>keys</code>. 343 */ 344 public boolean containsAllKeys(Collection<String> keys) { 345 if (keys == null) { 346 return false; 347 } else { 348 return keys.stream().filter(Objects::nonNull).allMatch(this::hasTagsFor); 349 } 350 } 351 352 /** 353 * Replies the number of tags with key <code>key</code> 354 * 355 * @param key the key to look up 356 * @return the number of tags with key <code>key</code>, including the empty "" value. 0, if key is null. 357 */ 358 public int getNumTagsFor(String key) { 359 return (int) generateStreamForKey(key).count(); 360 } 361 362 /** 363 * Replies true if there is at least one tag for the given key. 364 * 365 * @param key the key to look up 366 * @return true if there is at least one tag for the given key. false, if key is null. 367 */ 368 public boolean hasTagsFor(String key) { 369 return getNumTagsFor(key) > 0; 370 } 371 372 /** 373 * Replies true it there is at least one tag with a non empty value for key. 374 * Replies false if key is null. 375 * 376 * @param key the key 377 * @return true it there is at least one tag with a non empty value for key. 378 */ 379 public boolean hasValuesFor(String key) { 380 return generateStreamForKey(key).anyMatch(t -> !t.getValue().isEmpty()); 381 } 382 383 /** 384 * Replies true if there is exactly one tag for <code>key</code> and 385 * if the value of this tag is not empty. Replies false if key is 386 * null. 387 * 388 * @param key the key 389 * @return true if there is exactly one tag for <code>key</code> and 390 * if the value of this tag is not empty 391 */ 392 public boolean hasUniqueNonEmptyValue(String key) { 393 return generateStreamForKey(key).filter(t -> !t.getValue().isEmpty()).count() == 1; 394 } 395 396 /** 397 * Replies true if there is a tag with an empty value for <code>key</code>. 398 * Replies false, if key is null. 399 * 400 * @param key the key 401 * @return true if there is a tag with an empty value for <code>key</code> 402 */ 403 public boolean hasEmptyValue(String key) { 404 return generateStreamForKey(key).anyMatch(t -> t.getValue().isEmpty()); 405 } 406 407 /** 408 * Replies true if there is exactly one tag for <code>key</code> and if 409 * the value for this tag is empty. Replies false if key is null. 410 * 411 * @param key the key 412 * @return true if there is exactly one tag for <code>key</code> and if 413 * the value for this tag is empty 414 */ 415 public boolean hasUniqueEmptyValue(String key) { 416 Set<String> values = getValues(key); 417 return values.size() == 1 && values.contains(""); 418 } 419 420 /** 421 * Replies a tag collection with the tags for a given key. Replies an empty collection 422 * if key is null. 423 * 424 * @param key the key to look up 425 * @return a tag collection with the tags for a given key. Replies an empty collection 426 * if key is null. 427 */ 428 public TagCollection getTagsFor(String key) { 429 TagCollection ret = new TagCollection(); 430 generateStreamForKey(key).forEach(ret::add); 431 return ret; 432 } 433 434 /** 435 * Replies a tag collection with all tags whose key is equal to one of the keys in 436 * <code>keys</code>. Replies an empty collection if keys is null. 437 * 438 * @param keys the keys to look up 439 * @return a tag collection with all tags whose key is equal to one of the keys in 440 * <code>keys</code> 441 */ 442 public TagCollection getTagsFor(Collection<String> keys) { 443 TagCollection ret = new TagCollection(); 444 if (keys == null) 445 return ret; 446 for (String key : keys) { 447 if (key != null) { 448 ret.add(getTagsFor(key)); 449 } 450 } 451 return ret; 452 } 453 454 /** 455 * Replies the tags of this tag collection as set 456 * 457 * @return the tags of this tag collection as set 458 */ 459 public Set<Tag> asSet() { 460 return new HashSet<>(tags.keySet()); 461 } 462 463 /** 464 * Replies the tags of this tag collection as list. 465 * Note that the order of the list is not preserved between method invocations. 466 * 467 * @return the tags of this tag collection as list. There are no dupplicate values. 468 */ 469 public List<Tag> asList() { 470 return new ArrayList<>(tags.keySet()); 471 } 472 473 /** 474 * Replies an iterator to iterate over the tags in this collection 475 * 476 * @return the iterator 477 */ 478 @Override 479 public Iterator<Tag> iterator() { 480 return tags.keySet().iterator(); 481 } 482 483 /** 484 * Replies the set of keys of this tag collection. 485 * 486 * @return the set of keys of this tag collection 487 */ 488 public Set<String> getKeys() { 489 return generateKeyStream().collect(Collectors.toCollection(HashSet::new)); 490 } 491 492 /** 493 * Replies the set of keys which have at least 2 matching tags. 494 * 495 * @return the set of keys which have at least 2 matching tags. 496 */ 497 public Set<String> getKeysWithMultipleValues() { 498 HashSet<String> singleKeys = new HashSet<>(); 499 return generateKeyStream().filter(key -> !singleKeys.add(key)).collect(Collectors.toSet()); 500 } 501 502 /** 503 * Sets a unique tag for the key of this tag. All other tags with the same key are 504 * removed from the collection. Does nothing if tag is null. 505 * 506 * @param tag the tag to set 507 */ 508 public void setUniqueForKey(Tag tag) { 509 if (tag == null) return; 510 removeByKey(tag.getKey()); 511 add(tag); 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. Assume the empty string for key and value if either 517 * key or value is null. 518 * 519 * @param key the key 520 * @param value the value 521 */ 522 public void setUniqueForKey(String key, String value) { 523 Tag tag = new Tag(key, value); 524 setUniqueForKey(tag); 525 } 526 527 /** 528 * Replies the set of values in this tag collection 529 * 530 * @return the set of values 531 */ 532 public Set<String> getValues() { 533 return tags.keySet().stream().map(Tag::getValue).collect(Collectors.toSet()); 534 } 535 536 /** 537 * Replies the set of values for a given key. Replies an empty collection if there 538 * are no values for the given key. 539 * 540 * @param key the key to look up 541 * @return the set of values for a given key. Replies an empty collection if there 542 * are no values for the given key 543 */ 544 public Set<String> getValues(String key) { 545 // null-safe 546 return generateStreamForKey(key).map(Tag::getValue).collect(Collectors.toSet()); 547 } 548 549 /** 550 * Replies true if for every key there is one tag only, i.e. exactly one value. 551 * 552 * @return {@code true} if for every key there is one tag only 553 */ 554 public boolean isApplicableToPrimitive() { 555 return getKeysWithMultipleValues().isEmpty(); 556 } 557 558 /** 559 * Applies this tag collection to an {@link org.openstreetmap.josm.data.osm.OsmPrimitive}. Does nothing if 560 * primitive is null 561 * 562 * @param primitive the primitive 563 * @throws IllegalStateException if this tag collection can't be applied 564 * because there are keys with multiple values 565 */ 566 public void applyTo(Tagged primitive) { 567 if (primitive == null) return; 568 ensureApplicableToPrimitive(); 569 for (Tag tag: tags.keySet()) { 570 if (tag.getValue() == null || tag.getValue().isEmpty()) { 571 primitive.remove(tag.getKey()); 572 } else { 573 primitive.put(tag.getKey(), tag.getValue()); 574 } 575 } 576 } 577 578 /** 579 * Applies this tag collection to a collection of {@link org.openstreetmap.josm.data.osm.OsmPrimitive}s. Does nothing if 580 * primitives is null 581 * 582 * @param primitives the collection of primitives 583 * @throws IllegalStateException if this tag collection can't be applied 584 * because there are keys with multiple values 585 */ 586 public void applyTo(Collection<? extends Tagged> primitives) { 587 if (primitives == null) return; 588 ensureApplicableToPrimitive(); 589 for (Tagged primitive: primitives) { 590 applyTo(primitive); 591 } 592 } 593 594 /** 595 * Replaces the tags of an {@link org.openstreetmap.josm.data.osm.OsmPrimitive} by the tags in this collection . Does nothing if 596 * primitive is null 597 * 598 * @param primitive the primitive 599 * @throws IllegalStateException if this tag collection can't be applied 600 * because there are keys with multiple values 601 */ 602 public void replaceTagsOf(Tagged primitive) { 603 if (primitive == null) return; 604 ensureApplicableToPrimitive(); 605 primitive.removeAll(); 606 for (Tag tag: tags.keySet()) { 607 primitive.put(tag.getKey(), tag.getValue()); 608 } 609 } 610 611 /** 612 * Replaces the tags of a collection of{@link org.openstreetmap.josm.data.osm.OsmPrimitive}s by the tags in this collection. 613 * Does nothing if primitives is null 614 * 615 * @param primitives the collection of primitives 616 * @throws IllegalStateException if this tag collection can't be applied 617 * because there are keys with multiple values 618 */ 619 public void replaceTagsOf(Collection<? extends Tagged> primitives) { 620 if (primitives == null) return; 621 ensureApplicableToPrimitive(); 622 for (Tagged primitive: primitives) { 623 replaceTagsOf(primitive); 624 } 625 } 626 627 private void ensureApplicableToPrimitive() { 628 if (!isApplicableToPrimitive()) 629 throw new IllegalStateException(tr("Tag collection cannot be applied to a primitive because there are keys with multiple values.")); 630 } 631 632 /** 633 * Builds the intersection of this tag collection and another tag collection 634 * 635 * @param other the other tag collection. If null, replies an empty tag collection. 636 * @return the intersection of this tag collection and another tag collection. All counts are set to 1. 637 */ 638 public TagCollection intersect(TagCollection other) { 639 TagCollection ret = new TagCollection(); 640 if (other != null) { 641 tags.keySet().stream().filter(other::contains).forEach(ret::add); 642 } 643 return ret; 644 } 645 646 /** 647 * Replies the difference of this tag collection and another tag collection 648 * 649 * @param other the other tag collection. May be null. 650 * @return the difference of this tag collection and another tag collection 651 */ 652 public TagCollection minus(TagCollection other) { 653 TagCollection ret = new TagCollection(this); 654 if (other != null) { 655 ret.remove(other); 656 } 657 return ret; 658 } 659 660 /** 661 * Replies the union of this tag collection and another tag collection 662 * 663 * @param other the other tag collection. May be null. 664 * @return the union of this tag collection and another tag collection. The tag count is summed. 665 */ 666 public TagCollection union(TagCollection other) { 667 TagCollection ret = new TagCollection(this); 668 if (other != null) { 669 ret.add(other); 670 } 671 return ret; 672 } 673 674 public TagCollection emptyTagsForKeysMissingIn(TagCollection other) { 675 TagCollection ret = new TagCollection(); 676 for (String key: this.minus(other).getKeys()) { 677 ret.add(new Tag(key)); 678 } 679 return ret; 680 } 681 682 private static final Pattern SPLIT_VALUES_PATTERN = Pattern.compile(";\\s*"); 683 684 /** 685 * Replies the concatenation of all tag values (concatenated by a semicolon) 686 * @param key the key to look up 687 * 688 * @return the concatenation of all tag values 689 */ 690 public String getJoinedValues(String key) { 691 692 // See #7201 combining ways screws up the order of ref tags 693 Set<String> originalValues = getValues(key); 694 if (originalValues.size() == 1) { 695 return originalValues.iterator().next(); 696 } 697 698 Set<String> values = new LinkedHashSet<>(); 699 Map<String, Collection<String>> originalSplitValues = new LinkedHashMap<>(); 700 for (String v : originalValues) { 701 List<String> vs = Arrays.asList(SPLIT_VALUES_PATTERN.split(v)); 702 originalSplitValues.put(v, vs); 703 values.addAll(vs); 704 } 705 values.remove(""); 706 // try to retain an already existing key if it contains all needed values (remove this if it causes performance problems) 707 for (Entry<String, Collection<String>> i : originalSplitValues.entrySet()) { 708 if (i.getValue().containsAll(values)) { 709 return i.getKey(); 710 } 711 } 712 return Utils.join(";", values); 713 } 714 715 /** 716 * Replies the sum of all numeric tag values. Ignores dupplicates. 717 * @param key the key to look up 718 * 719 * @return the sum of all numeric tag values, as string. 720 * @since 7743 721 */ 722 public String getSummedValues(String key) { 723 int result = 0; 724 for (String value : getValues(key)) { 725 try { 726 result += Integer.parseInt(value); 727 } catch (NumberFormatException e) { 728 Logging.trace(e); 729 } 730 } 731 return Integer.toString(result); 732 } 733 734 private Stream<String> generateKeyStream() { 735 return tags.keySet().stream().map(Tag::getKey); 736 } 737 738 /** 739 * Get a stram for the given key. 740 * @param key The key 741 * @return The stream. An empty stream if key is <code>null</code> 742 */ 743 private Stream<Tag> generateStreamForKey(String key) { 744 return tags.keySet().stream().filter(e -> e.matchesKey(key)); 745 } 746 747 @Override 748 public String toString() { 749 return tags.toString(); 750 } 751}