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