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