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