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.&nbsp;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 &lt;= 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}