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.text.MessageFormat; 007import java.time.Instant; 008import java.util.Arrays; 009import java.util.Collection; 010import java.util.Collections; 011import java.util.Date; 012import java.util.HashMap; 013import java.util.HashSet; 014import java.util.LinkedList; 015import java.util.List; 016import java.util.Map; 017import java.util.Map.Entry; 018import java.util.Objects; 019import java.util.Set; 020import java.util.concurrent.TimeUnit; 021import java.util.function.BiPredicate; 022import java.util.stream.IntStream; 023import java.util.stream.Stream; 024 025import org.openstreetmap.josm.data.gpx.GpxConstants; 026import org.openstreetmap.josm.spi.preferences.Config; 027import org.openstreetmap.josm.tools.Utils; 028 029/** 030 * Abstract class to represent common features of the datatypes primitives. 031 * 032 * @since 4099 033 */ 034public abstract class AbstractPrimitive implements IPrimitive, IFilterablePrimitive { 035 036 /** 037 * This flag shows, that the properties have been changed by the user 038 * and on upload the object will be send to the server. 039 */ 040 protected static final short FLAG_MODIFIED = 1 << 0; 041 042 /** 043 * This flag is false, if the object is marked 044 * as deleted on the server. 045 */ 046 protected static final short FLAG_VISIBLE = 1 << 1; 047 048 /** 049 * An object that was deleted by the user. 050 * Deleted objects are usually hidden on the map and a request 051 * for deletion will be send to the server on upload. 052 * An object usually cannot be deleted if it has non-deleted 053 * objects still referring to it. 054 */ 055 protected static final short FLAG_DELETED = 1 << 2; 056 057 /** 058 * A primitive is incomplete if we know its id and type, but nothing more. 059 * Typically some members of a relation are incomplete until they are 060 * fetched from the server. 061 */ 062 protected static final short FLAG_INCOMPLETE = 1 << 3; 063 064 /** 065 * An object can be disabled by the filter mechanism. 066 * Then it will show in a shade of gray on the map or it is completely 067 * hidden from the view. 068 * Disabled objects usually cannot be selected or modified 069 * while the filter is active. 070 */ 071 protected static final short FLAG_DISABLED = 1 << 4; 072 073 /** 074 * This flag is only relevant if an object is disabled by the 075 * filter mechanism (i.e. FLAG_DISABLED is set). 076 * Then it indicates, whether it is completely hidden or 077 * just shown in gray color. 078 * 079 * When the primitive is not disabled, this flag should be 080 * unset as well (for efficient access). 081 */ 082 protected static final short FLAG_HIDE_IF_DISABLED = 1 << 5; 083 084 /** 085 * Flag used internally by the filter mechanism. 086 */ 087 protected static final short FLAG_DISABLED_TYPE = 1 << 6; 088 089 /** 090 * Flag used internally by the filter mechanism. 091 */ 092 protected static final short FLAG_HIDDEN_TYPE = 1 << 7; 093 094 /** 095 * This flag is set if the primitive is a way and 096 * according to the tags, the direction of the way is important. 097 * (e.g. one way street.) 098 */ 099 protected static final short FLAG_HAS_DIRECTIONS = 1 << 8; 100 101 /** 102 * If the primitive is tagged. 103 * Some trivial tags like source=* are ignored here. 104 */ 105 protected static final short FLAG_TAGGED = 1 << 9; 106 107 /** 108 * This flag is only relevant if FLAG_HAS_DIRECTIONS is set. 109 * It shows, that direction of the arrows should be reversed. 110 * (E.g. oneway=-1.) 111 */ 112 protected static final short FLAG_DIRECTION_REVERSED = 1 << 10; 113 114 /** 115 * When hovering over ways and nodes in add mode, the 116 * "target" objects are visually highlighted. This flag indicates 117 * that the primitive is currently highlighted. 118 */ 119 protected static final short FLAG_HIGHLIGHTED = 1 << 11; 120 121 /** 122 * If the primitive is annotated with a tag such as note, fixme, etc. 123 * Match the "work in progress" tags in default map style. 124 */ 125 protected static final short FLAG_ANNOTATED = 1 << 12; 126 127 /** 128 * Determines if the primitive is preserved from the filter mechanism. 129 */ 130 protected static final short FLAG_PRESERVED = 1 << 13; 131 132 /** 133 * Put several boolean flags to one short int field to save memory. 134 * Other bits of this field are used in subclasses. 135 */ 136 protected volatile short flags = FLAG_VISIBLE; // visible per default 137 138 /** 139 * The mappaint cache index for this primitive. 140 * This field belongs to {@code OsmPrimitive}, but due to Java's memory layout alignment, see #20830. 141 */ 142 protected short mappaintCacheIdx; 143 144 /*------------------- 145 * OTHER PROPERTIES 146 *-------------------*/ 147 148 /** 149 * Unique identifier in OSM. This is used to identify objects on the server. 150 * An id of 0 means an unknown id. The object has not been uploaded yet to 151 * know what id it will get. 152 */ 153 protected long id; 154 155 /** 156 * User that last modified this primitive, as specified by the server. 157 * Never changed by JOSM. 158 */ 159 protected User user; 160 161 /** 162 * Contains the version number as returned by the API. Needed to 163 * ensure update consistency 164 */ 165 protected int version; 166 167 /** 168 * The id of the changeset this primitive was last uploaded to. 169 * 0 if it wasn't uploaded to a changeset yet of if the changeset 170 * id isn't known. 171 */ 172 protected int changesetId; 173 174 /** 175 * A time value, measured in seconds from the epoch, or in other words, 176 * a number of seconds that have passed since 1970-01-01T00:00:00Z 177 */ 178 protected int timestamp; 179 180 /** 181 * Get and write all attributes from the parameter. Does not fire any listener, so 182 * use this only in the data initializing phase 183 * @param other the primitive to clone data from 184 */ 185 public void cloneFrom(AbstractPrimitive other) { 186 setKeys(other.getKeys()); 187 id = other.id; 188 if (id <= 0) { 189 // reset version and changeset id 190 version = 0; 191 changesetId = 0; 192 } 193 timestamp = other.timestamp; 194 if (id > 0) { 195 version = other.version; 196 } 197 flags = other.flags; 198 user = other.user; 199 if (id > 0 && other.changesetId > 0) { 200 // #4208: sometimes we cloned from other with id < 0 *and* 201 // an assigned changeset id. Don't know why yet. For primitives 202 // with id < 0 we don't propagate the changeset id any more. 203 // 204 setChangesetId(other.changesetId); 205 } 206 } 207 208 @Override 209 public int getVersion() { 210 return version; 211 } 212 213 @Override 214 public long getId() { 215 return id >= 0 ? id : 0; 216 } 217 218 /** 219 * Gets a unique id representing this object. 220 * 221 * @return Osm id if primitive already exists on the server. Unique negative value if primitive is new 222 */ 223 @Override 224 public long getUniqueId() { 225 return id; 226 } 227 228 /** 229 * Determines if this primitive is new. 230 * @return {@code true} if this primitive is new (not yet uploaded the server, id <= 0) 231 */ 232 @Override 233 public boolean isNew() { 234 return id <= 0; 235 } 236 237 @Override 238 public boolean isNewOrUndeleted() { 239 return isNew() || ((flags & (FLAG_VISIBLE + FLAG_DELETED)) == 0); 240 } 241 242 @Override 243 public void setOsmId(long id, int version) { 244 if (id <= 0) 245 throw new IllegalArgumentException(tr("ID > 0 expected. Got {0}.", id)); 246 if (version <= 0) 247 throw new IllegalArgumentException(tr("Version > 0 expected. Got {0}.", version)); 248 this.id = id; 249 this.version = version; 250 this.setIncomplete(false); 251 } 252 253 /** 254 * Clears the metadata, including id and version known to the OSM API. 255 * The id is a new unique id. The version, changeset and timestamp are set to 0. 256 * incomplete and deleted are set to false. It's preferred to use copy constructor with clearMetadata set to true instead 257 * of calling this method. 258 * @since 6140 259 */ 260 public void clearOsmMetadata() { 261 // Not part of dataset - no lock necessary 262 this.id = getIdGenerator().generateUniqueId(); 263 this.version = 0; 264 this.user = null; 265 this.changesetId = 0; // reset changeset id on a new object 266 this.timestamp = 0; 267 this.setIncomplete(false); 268 this.setDeleted(false); 269 this.setVisible(true); 270 } 271 272 /** 273 * Returns the unique identifier generator. 274 * @return the unique identifier generator 275 * @since 15820 276 */ 277 public abstract UniqueIdGenerator getIdGenerator(); 278 279 @Override 280 public User getUser() { 281 return user; 282 } 283 284 @Override 285 public void setUser(User user) { 286 this.user = user; 287 } 288 289 @Override 290 public int getChangesetId() { 291 return changesetId; 292 } 293 294 @Override 295 public void setChangesetId(int changesetId) { 296 if (this.changesetId == changesetId) 297 return; 298 if (changesetId < 0) 299 throw new IllegalArgumentException(MessageFormat.format("Parameter ''{0}'' >= 0 expected, got {1}", "changesetId", changesetId)); 300 if (changesetId > 0 && isNew()) 301 throw new IllegalStateException(tr("Cannot assign a changesetId > 0 to a new primitive. Value of changesetId is {0}", changesetId)); 302 303 this.changesetId = changesetId; 304 } 305 306 @Deprecated 307 @Override 308 public void setTimestamp(Date timestamp) { 309 this.timestamp = (int) TimeUnit.MILLISECONDS.toSeconds(timestamp.getTime()); 310 } 311 312 @Override 313 public void setInstant(Instant timestamp) { 314 this.timestamp = (int) timestamp.getEpochSecond(); 315 } 316 317 @Override 318 public void setRawTimestamp(int timestamp) { 319 this.timestamp = timestamp; 320 } 321 322 @Deprecated 323 @Override 324 public Date getTimestamp() { 325 return Date.from(getInstant()); 326 } 327 328 @Override 329 public Instant getInstant() { 330 return Instant.ofEpochSecond(Integer.toUnsignedLong(timestamp)); 331 } 332 333 @Override 334 public int getRawTimestamp() { 335 return timestamp; 336 } 337 338 @Override 339 public boolean isTimestampEmpty() { 340 return timestamp == 0; 341 } 342 343 /* ------- 344 /* FLAGS 345 /* ------*/ 346 347 protected void updateFlags(short flag, boolean value) { 348 if (value) { 349 flags |= flag; 350 } else { 351 flags &= (short) ~flag; 352 } 353 } 354 355 /** 356 * Update flags 357 * @param flag The flag to update 358 * @param value The value to set 359 * @return {@code true} if the flags have changed 360 */ 361 protected boolean updateFlagsChanged(short flag, boolean value) { 362 int oldFlags = flags; 363 updateFlags(flag, value); 364 return oldFlags != flags; 365 } 366 367 @Override 368 public void setModified(boolean modified) { 369 updateFlags(FLAG_MODIFIED, modified); 370 } 371 372 @Override 373 public boolean isModified() { 374 return (flags & FLAG_MODIFIED) != 0; 375 } 376 377 @Override 378 public boolean isDeleted() { 379 return (flags & FLAG_DELETED) != 0; 380 } 381 382 @Override 383 public boolean isUndeleted() { 384 return (flags & (FLAG_VISIBLE + FLAG_DELETED)) == 0; 385 } 386 387 @Override 388 public boolean isUsable() { 389 return (flags & (FLAG_DELETED + FLAG_INCOMPLETE)) == 0; 390 } 391 392 @Override 393 public boolean isVisible() { 394 return (flags & FLAG_VISIBLE) != 0; 395 } 396 397 @Override 398 public void setVisible(boolean visible) { 399 if (!visible && isNew()) 400 throw new IllegalStateException(tr("A primitive with ID = 0 cannot be invisible.")); 401 updateFlags(FLAG_VISIBLE, visible); 402 } 403 404 @Override 405 public void setDeleted(boolean deleted) { 406 updateFlags(FLAG_DELETED, deleted); 407 setModified(deleted ^ !isVisible()); 408 } 409 410 /** 411 * If set to true, this object is incomplete, which means only the id 412 * and type is known (type is the objects instance class) 413 * @param incomplete incomplete flag value 414 */ 415 protected void setIncomplete(boolean incomplete) { 416 updateFlags(FLAG_INCOMPLETE, incomplete); 417 } 418 419 @Override 420 public boolean isIncomplete() { 421 return (flags & FLAG_INCOMPLETE) != 0; 422 } 423 424 @Override 425 public boolean getHiddenType() { 426 return (flags & FLAG_HIDDEN_TYPE) != 0; 427 } 428 429 @Override 430 public boolean getDisabledType() { 431 return (flags & FLAG_DISABLED_TYPE) != 0; 432 } 433 434 @Override 435 public boolean setDisabledState(boolean hidden) { 436 // Store as variables to avoid short circuit boolean return 437 final boolean flagDisabled = updateFlagsChanged(FLAG_DISABLED, true); 438 final boolean flagHideIfDisabled = updateFlagsChanged(FLAG_HIDE_IF_DISABLED, hidden); 439 return flagDisabled || flagHideIfDisabled; 440 } 441 442 @Override 443 public boolean unsetDisabledState() { 444 // Store as variables to avoid short circuit boolean return 445 final boolean flagDisabled = updateFlagsChanged(FLAG_DISABLED, false); 446 final boolean flagHideIfDisabled = updateFlagsChanged(FLAG_HIDE_IF_DISABLED, false); 447 return flagDisabled || flagHideIfDisabled; 448 } 449 450 @Override 451 public void setDisabledType(boolean isExplicit) { 452 updateFlags(FLAG_DISABLED_TYPE, isExplicit); 453 } 454 455 @Override 456 public void setHiddenType(boolean isExplicit) { 457 updateFlags(FLAG_HIDDEN_TYPE, isExplicit); 458 } 459 460 protected String getFlagsAsString() { 461 StringBuilder builder = new StringBuilder(); 462 463 if (isIncomplete()) { 464 builder.append('I'); 465 } 466 if (isModified()) { 467 builder.append('M'); 468 } 469 if (isVisible()) { 470 builder.append('V'); 471 } 472 if (isDeleted()) { 473 builder.append('D'); 474 } 475 return builder.toString(); 476 } 477 478 /*------------ 479 * Keys handling 480 ------------*/ 481 482 /** 483 * The key/value list for this primitive. 484 * <p> 485 * Note that the keys field is synchronized using RCU. 486 * Writes to it are not synchronized by this object, the writers have to synchronize writes themselves. 487 * <p> 488 * In short this means that you should not rely on this variable being the same value when read again and your should always 489 * copy it on writes. 490 * <p> 491 * Further reading: 492 * <ul> 493 * <li>{@link java.util.concurrent.CopyOnWriteArrayList}</li> 494 * <li> <a href="http://stackoverflow.com/questions/2950871/how-can-copyonwritearraylist-be-thread-safe"> 495 * http://stackoverflow.com/questions/2950871/how-can-copyonwritearraylist-be-thread-safe</a></li> 496 * <li> <a href="https://en.wikipedia.org/wiki/Read-copy-update"> 497 * https://en.wikipedia.org/wiki/Read-copy-update</a> (mind that we have a Garbage collector, 498 * {@code rcu_assign_pointer} and {@code rcu_dereference} are ensured by the {@code volatile} keyword)</li> 499 * </ul> 500 */ 501 protected volatile String[] keys; 502 503 /** 504 * Replies the map of key/value pairs. Never replies null. The map can be empty, though. 505 * 506 * @return tags of this primitive. Changes made in returned map are not mapped 507 * back to the primitive, use setKeys() to modify the keys 508 * @see #visitKeys(KeyValueVisitor) 509 */ 510 @Override 511 public TagMap getKeys() { 512 return new TagMap(keys); 513 } 514 515 @Override 516 public void visitKeys(KeyValueVisitor visitor) { 517 String[] keys = this.keys; 518 if (keys != null) { 519 for (int i = 0; i < keys.length; i += 2) { 520 visitor.visitKeyValue(this, keys[i], keys[i + 1]); 521 } 522 } 523 } 524 525 /** 526 * Sets the keys of this primitives to the key/value pairs in <code>keys</code>. 527 * Old key/value pairs are removed. 528 * If <code>keys</code> is null, clears existing key/value pairs. 529 * <p> 530 * Note that this method, like all methods that modify keys, is not synchronized and may lead to data corruption when being used 531 * from multiple threads. 532 * 533 * @param keys the key/value pairs to set. If null, removes all existing key/value pairs. 534 */ 535 @Override 536 public void setKeys(Map<String, String> keys) { 537 Map<String, String> originalKeys = getKeys(); 538 if (Utils.isEmpty(keys)) { 539 this.keys = null; 540 keysChangedImpl(originalKeys); 541 return; 542 } 543 String[] newKeys = new String[keys.size() * 2]; 544 int index = 0; 545 for (Entry<String, String> entry:keys.entrySet()) { 546 newKeys[index++] = Objects.requireNonNull(entry.getKey()); 547 newKeys[index++] = Objects.requireNonNull(entry.getValue()); 548 } 549 this.keys = newKeys; 550 keysChangedImpl(originalKeys); 551 } 552 553 /** 554 * Copy the keys from a TagMap. 555 * @param keys The new key map. 556 */ 557 public void setKeys(TagMap keys) { 558 Map<String, String> originalKeys = getKeys(); 559 if (keys == null) { 560 this.keys = null; 561 } else { 562 String[] arr = keys.getTagsArray(); 563 if (arr.length == 0) { 564 this.keys = null; 565 } else { 566 this.keys = arr; 567 } 568 } 569 keysChangedImpl(originalKeys); 570 } 571 572 /** 573 * Set the given value to the given key. If key is null, does nothing. If value is null, 574 * removes the key and behaves like {@link #remove(String)}. 575 * <p> 576 * Note that this method, like all methods that modify keys, is not synchronized and may lead to data corruption when being used 577 * from multiple threads. 578 * 579 * @param key The key, for which the value is to be set. Can be null or empty, does nothing in this case. 580 * @param value The value for the key. If null, removes the respective key/value pair. 581 * 582 * @see #remove(String) 583 */ 584 @Override 585 public void put(String key, String value) { 586 Map<String, String> originalKeys = getKeys(); 587 if (key == null || Utils.isStripEmpty(key)) 588 return; 589 else if (value == null) { 590 remove(key); 591 } else if (keys == null) { 592 keys = new String[] {key, value}; 593 keysChangedImpl(originalKeys); 594 } else { 595 int keyIndex = indexOfKey(keys, key); 596 int tagArrayLength = keys.length; 597 if (keyIndex < 0) { 598 keyIndex = tagArrayLength; 599 tagArrayLength += 2; 600 } 601 602 // Do not try to optimize this array creation if the key already exists. 603 // We would need to convert the keys array to be an AtomicReferenceArray 604 // Or we would at least need a volatile write after the array was modified to 605 // ensure that changes are visible by other threads. 606 String[] newKeys = Arrays.copyOf(keys, tagArrayLength); 607 newKeys[keyIndex] = key; 608 newKeys[keyIndex + 1] = value; 609 keys = newKeys; 610 keysChangedImpl(originalKeys); 611 } 612 } 613 614 /** 615 * Scans a key/value array for a given key. 616 * @param keys The key array. It is not modified. It may be null to indicate an empty array. 617 * @param key The key to search for. 618 * @return The position of that key in the keys array - which is always a multiple of 2 - or -1 if it was not found. 619 */ 620 private static int indexOfKey(String[] keys, String key) { 621 if (keys == null) { 622 return -1; 623 } 624 for (int i = 0; i < keys.length; i += 2) { 625 if (keys[i].equals(key)) { 626 return i; 627 } 628 } 629 return -1; 630 } 631 632 /** 633 * Remove the given key from the list 634 * <p> 635 * Note that this method, like all methods that modify keys, is not synchronized and may lead to data corruption when being used 636 * from multiple threads. 637 * 638 * @param key the key to be removed. Ignored, if key is null. 639 */ 640 @Override 641 public void remove(String key) { 642 if (key == null || keys == null) return; 643 if (!hasKey(key)) 644 return; 645 Map<String, String> originalKeys = getKeys(); 646 if (keys.length == 2) { 647 keys = null; 648 keysChangedImpl(originalKeys); 649 return; 650 } 651 String[] newKeys = new String[keys.length - 2]; 652 int j = 0; 653 for (int i = 0; i < keys.length; i += 2) { 654 if (!keys[i].equals(key)) { 655 newKeys[j++] = keys[i]; 656 newKeys[j++] = keys[i+1]; 657 } 658 } 659 keys = newKeys; 660 keysChangedImpl(originalKeys); 661 } 662 663 /** 664 * Removes all keys from this primitive. 665 * <p> 666 * Note that this method, like all methods that modify keys, is not synchronized and may lead to data corruption when being used 667 * from multiple threads. 668 */ 669 @Override 670 public void removeAll() { 671 if (keys != null) { 672 Map<String, String> originalKeys = getKeys(); 673 keys = null; 674 keysChangedImpl(originalKeys); 675 } 676 } 677 678 protected final String doGet(String key, BiPredicate<String, String> predicate) { 679 if (key == null) 680 return null; 681 if (keys == null) 682 return null; 683 for (int i = 0; i < keys.length; i += 2) { 684 if (predicate.test(keys[i], key)) return keys[i+1]; 685 } 686 return null; 687 } 688 689 /** 690 * Replies the value for key <code>key</code>. Replies null, if <code>key</code> is null. 691 * Replies null, if there is no value for the given key. 692 * 693 * @param key the key. Can be null, replies null in this case. 694 * @return the value for key <code>key</code>. 695 */ 696 @Override 697 public final String get(String key) { 698 return doGet(key, String::equals); 699 } 700 701 /** 702 * Gets a key ignoring the case of the key 703 * @param key The key to get 704 * @return The value for a key that matches the given key ignoring case. 705 */ 706 public final String getIgnoreCase(String key) { 707 return doGet(key, String::equalsIgnoreCase); 708 } 709 710 @Override 711 public final int getNumKeys() { 712 return keys == null ? 0 : keys.length / 2; 713 } 714 715 @Override 716 public final Collection<String> keySet() { 717 String[] keys = this.keys; 718 if (keys == null) { 719 return Collections.emptySet(); 720 } 721 if (keys.length == 2) { 722 return Collections.singleton(keys[0]); 723 } 724 725 final Set<String> result = new HashSet<>(Utils.hashMapInitialCapacity(keys.length / 2)); 726 for (int i = 0; i < keys.length; i += 2) { 727 result.add(keys[i]); 728 } 729 return result; 730 } 731 732 @Override 733 public Stream<String> keys() { 734 final String[] k = this.keys; 735 if (k == null) { 736 return Stream.empty(); 737 } else if (k.length == 2) { 738 return Stream.of(k[0]); 739 } else { 740 return IntStream.range(0, k.length / 2).mapToObj(i -> k[i * 2]); 741 } 742 } 743 744 /** 745 * Replies true, if the map of key/value pairs of this primitive is not empty. 746 * 747 * @return true, if the map of key/value pairs of this primitive is not empty; false otherwise 748 */ 749 @Override 750 public final boolean hasKeys() { 751 return keys != null; 752 } 753 754 /** 755 * Replies true if this primitive has a tag with key <code>key</code>. 756 * 757 * @param key the key 758 * @return true, if this primitive has a tag with key <code>key</code> 759 */ 760 @Override 761 public boolean hasKey(String key) { 762 return key != null && indexOfKey(keys, key) >= 0; 763 } 764 765 /** 766 * Replies true if this primitive has a tag any of the <code>keys</code>. 767 * 768 * @param keys the keys 769 * @return true, if this primitive has a tag with any of the <code>keys</code> 770 * @since 11587 771 */ 772 public boolean hasKey(String... keys) { 773 return keys != null && Arrays.stream(keys).anyMatch(this::hasKey); 774 } 775 776 /** 777 * What to do, when the tags have changed by one of the tag-changing methods. 778 * @param originalKeys original tags 779 */ 780 protected abstract void keysChangedImpl(Map<String, String> originalKeys); 781 782 /*------------------------------------- 783 * WORK IN PROGRESS, UNINTERESTING KEYS 784 *-------------------------------------*/ 785 786 private static volatile Collection<String> workinprogress; 787 private static volatile Collection<String> uninteresting; 788 private static volatile Collection<String> discardable; 789 790 /** 791 * Returns a list of "uninteresting" keys that do not make an object 792 * "tagged". Entries that end with ':' are causing a whole namespace to be considered 793 * "uninteresting". Only the first level namespace is considered. 794 * Initialized by isUninterestingKey() 795 * @return The list of uninteresting keys. 796 */ 797 public static Collection<String> getUninterestingKeys() { 798 if (uninteresting == null) { 799 List<String> l = new LinkedList<>(Arrays.asList( 800 "source", "source_ref", "source:", "comment", 801 "watch", "watch:", "description", "attribution", GpxConstants.GPX_PREFIX)); 802 l.addAll(getDiscardableKeys()); 803 l.addAll(getWorkInProgressKeys()); 804 uninteresting = new HashSet<>(Config.getPref().getList("tags.uninteresting", l)); 805 } 806 return uninteresting; 807 } 808 809 /** 810 * Returns a list of keys which have been deemed uninteresting to the point 811 * that they can be silently removed from data which is being edited. 812 * @return The list of discardable keys. 813 */ 814 public static Collection<String> getDiscardableKeys() { 815 if (discardable == null) { 816 discardable = new HashSet<>(Config.getPref().getList("tags.discardable", 817 Arrays.asList( 818 "created_by", 819 "converted_by", 820 "current_id", 821 "geobase:datasetName", 822 "geobase:uuid", 823 "KSJ2:ADS", 824 "KSJ2:ARE", 825 "KSJ2:AdminArea", 826 "KSJ2:COP_label", 827 "KSJ2:DFD", 828 "KSJ2:INT", 829 "KSJ2:INT_label", 830 "KSJ2:LOC", 831 "KSJ2:LPN", 832 "KSJ2:OPC", 833 "KSJ2:PubFacAdmin", 834 "KSJ2:RAC", 835 "KSJ2:RAC_label", 836 "KSJ2:RIC", 837 "KSJ2:RIN", 838 "KSJ2:WSC", 839 "KSJ2:coordinate", 840 "KSJ2:curve_id", 841 "KSJ2:curve_type", 842 "KSJ2:filename", 843 "KSJ2:lake_id", 844 "KSJ2:lat", 845 "KSJ2:long", 846 "KSJ2:river_id", 847 "odbl", 848 "odbl:note", 849 "osmarender:nameDirection", 850 "osmarender:renderName", 851 "osmarender:renderRef", 852 "osmarender:rendernames", 853 "SK53_bulk:load", 854 "sub_sea:type", 855 "tiger:source", 856 "tiger:separated", 857 "tiger:tlid", 858 "tiger:upload_uuid", 859 "import_uuid", 860 "gnis:import_uuid", 861 "yh:LINE_NAME", 862 "yh:LINE_NUM", 863 "yh:STRUCTURE", 864 "yh:TOTYUMONO", 865 "yh:TYPE", 866 "yh:WIDTH", 867 "yh:WIDTH_RANK" 868 ))); 869 } 870 return discardable; 871 } 872 873 /** 874 * Returns a list of "work in progress" keys that do not make an object 875 * "tagged" but "annotated". 876 * @return The list of work in progress keys. 877 * @since 5754 878 */ 879 public static Collection<String> getWorkInProgressKeys() { 880 if (workinprogress == null) { 881 workinprogress = new HashSet<>(Config.getPref().getList("tags.workinprogress", 882 Arrays.asList("note", "fixme", "FIXME"))); 883 } 884 return workinprogress; 885 } 886 887 /** 888 * Determines if key is considered "uninteresting". 889 * @param key The key to check 890 * @return true if key is considered "uninteresting". 891 */ 892 public static boolean isUninterestingKey(String key) { 893 getUninterestingKeys(); 894 if (uninteresting.contains(key)) 895 return true; 896 int pos = key.indexOf(':'); 897 if (pos > 0) 898 return uninteresting.contains(key.substring(0, pos + 1)); 899 return false; 900 } 901 902 @Override 903 public Map<String, String> getInterestingTags() { 904 Map<String, String> result = new HashMap<>(); 905 if (keys != null) { 906 for (int i = 0; i < keys.length; i += 2) { 907 if (!isUninterestingKey(keys[i])) { 908 result.put(keys[i], keys[i + 1]); 909 } 910 } 911 } 912 return result; 913 } 914}