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