source: josm/trunk/src/org/openstreetmap/josm/data/osm/AbstractPrimitive.java

Last change on this file was 18646, checked in by stoecker, 6 days ago

see #18258 - add a note why current_id is here

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