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