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.ArrayList;
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.LinkedHashSet;
015import java.util.LinkedList;
016import java.util.List;
017import java.util.Locale;
018import java.util.Map;
019import java.util.Objects;
020import java.util.Set;
021
022import org.openstreetmap.josm.Main;
023import org.openstreetmap.josm.actions.search.SearchCompiler;
024import org.openstreetmap.josm.actions.search.SearchCompiler.Match;
025import org.openstreetmap.josm.actions.search.SearchCompiler.ParseError;
026import org.openstreetmap.josm.data.osm.visitor.Visitor;
027import org.openstreetmap.josm.gui.mappaint.StyleCache;
028import org.openstreetmap.josm.tools.CheckParameterUtil;
029import org.openstreetmap.josm.tools.Predicate;
030import org.openstreetmap.josm.tools.Predicates;
031import org.openstreetmap.josm.tools.Utils;
032import org.openstreetmap.josm.tools.template_engine.TemplateEngineDataProvider;
033
034/**
035 * The base class for OSM objects ({@link Node}, {@link Way}, {@link Relation}).
036 *
037 * It can be created, deleted and uploaded to the OSM-Server.
038 *
039 * Although OsmPrimitive is designed as a base class, it is not to be meant to subclass
040 * it by any other than from the package {@link org.openstreetmap.josm.data.osm}. The available primitives are a fixed set that are given
041 * by the server environment and not an extendible data stuff.
042 *
043 * @author imi
044 */
045public abstract class OsmPrimitive extends AbstractPrimitive implements Comparable<OsmPrimitive>, TemplateEngineDataProvider {
046    private static final String SPECIAL_VALUE_ID = "id";
047    private static final String SPECIAL_VALUE_LOCAL_NAME = "localname";
048
049    /**
050     * An object can be disabled by the filter mechanism.
051     * Then it will show in a shade of gray on the map or it is completely
052     * hidden from the view.
053     * Disabled objects usually cannot be selected or modified
054     * while the filter is active.
055     */
056    protected static final int FLAG_DISABLED = 1 << 4;
057
058    /**
059     * This flag is only relevant if an object is disabled by the
060     * filter mechanism (i.e.&nbsp;FLAG_DISABLED is set).
061     * Then it indicates, whether it is completely hidden or
062     * just shown in gray color.
063     *
064     * When the primitive is not disabled, this flag should be
065     * unset as well (for efficient access).
066     */
067    protected static final int FLAG_HIDE_IF_DISABLED = 1 << 5;
068
069    /**
070     * Flag used internally by the filter mechanism.
071     */
072    protected static final int FLAG_DISABLED_TYPE = 1 << 6;
073
074    /**
075     * Flag used internally by the filter mechanism.
076     */
077    protected static final int FLAG_HIDDEN_TYPE = 1 << 7;
078
079    /**
080     * This flag is set if the primitive is a way and
081     * according to the tags, the direction of the way is important.
082     * (e.g. one way street.)
083     */
084    protected static final int FLAG_HAS_DIRECTIONS = 1 << 8;
085
086    /**
087     * If the primitive is tagged.
088     * Some trivial tags like source=* are ignored here.
089     */
090    protected static final int FLAG_TAGGED = 1 << 9;
091
092    /**
093     * This flag is only relevant if FLAG_HAS_DIRECTIONS is set.
094     * It shows, that direction of the arrows should be reversed.
095     * (E.g. oneway=-1.)
096     */
097    protected static final int FLAG_DIRECTION_REVERSED = 1 << 10;
098
099    /**
100     * When hovering over ways and nodes in add mode, the
101     * "target" objects are visually highlighted. This flag indicates
102     * that the primitive is currently highlighted.
103     */
104    protected static final int FLAG_HIGHLIGHTED = 1 << 11;
105
106    /**
107     * If the primitive is annotated with a tag such as note, fixme, etc.
108     * Match the "work in progress" tags in default map style.
109     */
110    protected static final int FLAG_ANNOTATED = 1 << 12;
111
112    /**
113     * Replies the sub-collection of {@link OsmPrimitive}s of type <code>type</code> present in
114     * another collection of {@link OsmPrimitive}s. The result collection is a list.
115     *
116     * If <code>list</code> is null, replies an empty list.
117     *
118     * @param <T> type of data (must be one of the {@link OsmPrimitive} types
119     * @param list  the original list
120     * @param type the type to filter for
121     * @return the sub-list of OSM primitives of type <code>type</code>
122     */
123    public static <T extends OsmPrimitive> List<T> getFilteredList(Collection<OsmPrimitive> list, Class<T> type) {
124        if (list == null) return Collections.emptyList();
125        List<T> ret = new LinkedList<>();
126        for (OsmPrimitive p: list) {
127            if (type.isInstance(p)) {
128                ret.add(type.cast(p));
129            }
130        }
131        return ret;
132    }
133
134    /**
135     * Replies the sub-collection of {@link OsmPrimitive}s of type <code>type</code> present in
136     * another collection of {@link OsmPrimitive}s. The result collection is a set.
137     *
138     * If <code>list</code> is null, replies an empty set.
139     *
140     * @param <T> type of data (must be one of the {@link OsmPrimitive} types
141     * @param set  the original collection
142     * @param type the type to filter for
143     * @return the sub-set of OSM primitives of type <code>type</code>
144     */
145    public static <T extends OsmPrimitive> Set<T> getFilteredSet(Collection<OsmPrimitive> set, Class<T> type) {
146        Set<T> ret = new LinkedHashSet<>();
147        if (set != null) {
148            for (OsmPrimitive p: set) {
149                if (type.isInstance(p)) {
150                    ret.add(type.cast(p));
151                }
152            }
153        }
154        return ret;
155    }
156
157    /**
158     * Replies the collection of referring primitives for the primitives in <code>primitives</code>.
159     *
160     * @param primitives the collection of primitives.
161     * @return the collection of referring primitives for the primitives in <code>primitives</code>;
162     * empty set if primitives is null or if there are no referring primitives
163     */
164    public static Set<OsmPrimitive> getReferrer(Collection<? extends OsmPrimitive> primitives) {
165        Set<OsmPrimitive> ret = new HashSet<>();
166        if (primitives == null || primitives.isEmpty()) return ret;
167        for (OsmPrimitive p: primitives) {
168            ret.addAll(p.getReferrers());
169        }
170        return ret;
171    }
172
173    /**
174     * A predicate that filters primitives that are usable.
175     * @see OsmPrimitive#isUsable()
176     */
177    public static final Predicate<OsmPrimitive> isUsablePredicate = new Predicate<OsmPrimitive>() {
178        @Override
179        public boolean evaluate(OsmPrimitive primitive) {
180            return primitive.isUsable();
181        }
182    };
183
184    /**
185     * A predicate filtering primitives that are selectable.
186     */
187    public static final Predicate<OsmPrimitive> isSelectablePredicate = new Predicate<OsmPrimitive>() {
188        @Override
189        public boolean evaluate(OsmPrimitive primitive) {
190            return primitive.isSelectable();
191        }
192    };
193
194    /**
195     * A predicate filtering primitives that are not deleted.
196     */
197    public static final Predicate<OsmPrimitive> nonDeletedPredicate = new Predicate<OsmPrimitive>() {
198        @Override public boolean evaluate(OsmPrimitive primitive) {
199            return !primitive.isDeleted();
200        }
201    };
202
203    /**
204     * A predicate filtering primitives that are not deleted and not incomplete.
205     */
206    public static final Predicate<OsmPrimitive> nonDeletedCompletePredicate = new Predicate<OsmPrimitive>() {
207        @Override public boolean evaluate(OsmPrimitive primitive) {
208            return !primitive.isDeleted() && !primitive.isIncomplete();
209        }
210    };
211
212    /**
213     * A predicate filtering primitives that are not deleted and not incomplete and that are not a relation.
214     */
215    public static final Predicate<OsmPrimitive> nonDeletedPhysicalPredicate = new Predicate<OsmPrimitive>() {
216        @Override public boolean evaluate(OsmPrimitive primitive) {
217            return !primitive.isDeleted() && !primitive.isIncomplete() && !(primitive instanceof Relation);
218        }
219    };
220
221    /**
222     * A predicate filtering primitives that are modified
223     */
224    public static final Predicate<OsmPrimitive> modifiedPredicate = new Predicate<OsmPrimitive>() {
225        @Override public boolean evaluate(OsmPrimitive primitive) {
226            return primitive.isModified();
227        }
228    };
229
230    /**
231     * A predicate filtering nodes.
232     */
233    public static final Predicate<OsmPrimitive> nodePredicate = Predicates.<OsmPrimitive>isOfClass(Node.class);
234
235    /**
236     * A predicate filtering ways.
237     */
238    public static final Predicate<OsmPrimitive> wayPredicate = Predicates.<OsmPrimitive>isOfClass(Way.class);
239
240    /**
241     * A predicate filtering relations.
242     */
243    public static final Predicate<OsmPrimitive> relationPredicate = Predicates.<OsmPrimitive>isOfClass(Relation.class);
244
245    /**
246     * A predicate filtering multipolygon relations.
247     */
248    public static final Predicate<OsmPrimitive> multipolygonPredicate = new Predicate<OsmPrimitive>() {
249        @Override public boolean evaluate(OsmPrimitive primitive) {
250            return primitive.getClass() == Relation.class && ((Relation) primitive).isMultipolygon();
251        }
252    };
253
254    /**
255     * This matches all ways that have a direction
256     *
257     * @see #FLAG_HAS_DIRECTIONS
258     */
259    public static final Predicate<Tag> directionalKeyPredicate = new Predicate<Tag>() {
260        @Override
261        public boolean evaluate(Tag tag) {
262            return directionKeys.match(tag);
263        }
264    };
265
266    /**
267     * Creates a new primitive for the given id.
268     *
269     * If allowNegativeId is set, provided id can be &lt; 0 and will be set to primitive without any processing.
270     * If allowNegativeId is not set, then id will have to be 0 (in that case new unique id will be generated) or
271     * positive number.
272     *
273     * @param id the id
274     * @param allowNegativeId {@code true} to allow negative id
275     * @throws IllegalArgumentException if id &lt; 0 and allowNegativeId is false
276     */
277    protected OsmPrimitive(long id, boolean allowNegativeId) {
278        if (allowNegativeId) {
279            this.id = id;
280        } else {
281            if (id < 0)
282                throw new IllegalArgumentException(MessageFormat.format("Expected ID >= 0. Got {0}.", id));
283            else if (id == 0) {
284                this.id = generateUniqueId();
285            } else {
286                this.id = id;
287            }
288
289        }
290        this.version = 0;
291        this.setIncomplete(id > 0);
292    }
293
294    /**
295     * Creates a new primitive for the given id and version.
296     *
297     * If allowNegativeId is set, provided id can be &lt; 0 and will be set to primitive without any processing.
298     * If allowNegativeId is not set, then id will have to be 0 (in that case new unique id will be generated) or
299     * positive number.
300     *
301     * If id is not &gt; 0 version is ignored and set to 0.
302     *
303     * @param id the id
304     * @param version the version (positive integer)
305     * @param allowNegativeId {@code true} to allow negative id
306     * @throws IllegalArgumentException if id &lt; 0 and allowNegativeId is false
307     */
308    protected OsmPrimitive(long id, int version, boolean allowNegativeId) {
309        this(id, allowNegativeId);
310        this.version = id > 0 ? version : 0;
311        setIncomplete(id > 0 && version == 0);
312    }
313
314    /*----------
315     * MAPPAINT
316     *--------*/
317    public StyleCache mappaintStyle;
318    public int mappaintCacheIdx;
319
320    /* This should not be called from outside. Fixing the UI to add relevant
321       get/set functions calling this implicitely is preferred, so we can have
322       transparent cache handling in the future. */
323    public void clearCachedStyle() {
324        mappaintStyle = null;
325    }
326    /* end of mappaint data */
327
328    /*---------
329     * DATASET
330     *---------*/
331
332    /** the parent dataset */
333    private DataSet dataSet;
334
335    /**
336     * This method should never ever by called from somewhere else than Dataset.addPrimitive or removePrimitive methods
337     * @param dataSet the parent dataset
338     */
339    void setDataset(DataSet dataSet) {
340        if (this.dataSet != null && dataSet != null && this.dataSet != dataSet)
341            throw new DataIntegrityProblemException("Primitive cannot be included in more than one Dataset");
342        this.dataSet = dataSet;
343    }
344
345    /**
346     *
347     * @return DataSet this primitive is part of.
348     */
349    public DataSet getDataSet() {
350        return dataSet;
351    }
352
353    /**
354     * Throws exception if primitive is not part of the dataset
355     */
356    public void checkDataset() {
357        if (dataSet == null)
358            throw new DataIntegrityProblemException("Primitive must be part of the dataset: " + toString());
359    }
360
361    protected boolean writeLock() {
362        if (dataSet != null) {
363            dataSet.beginUpdate();
364            return true;
365        } else
366            return false;
367    }
368
369    protected void writeUnlock(boolean locked) {
370        if (locked) {
371            // It shouldn't be possible for dataset to become null because
372            // method calling setDataset would need write lock which is owned by this thread
373            dataSet.endUpdate();
374        }
375    }
376
377    /**
378     * Sets the id and the version of this primitive if it is known to the OSM API.
379     *
380     * Since we know the id and its version it can't be incomplete anymore. incomplete
381     * is set to false.
382     *
383     * @param id the id. &gt; 0 required
384     * @param version the version &gt; 0 required
385     * @throws IllegalArgumentException if id &lt;= 0
386     * @throws IllegalArgumentException if version &lt;= 0
387     * @throws DataIntegrityProblemException if id is changed and primitive was already added to the dataset
388     */
389    @Override
390    public void setOsmId(long id, int version) {
391        boolean locked = writeLock();
392        try {
393            if (id <= 0)
394                throw new IllegalArgumentException(tr("ID > 0 expected. Got {0}.", id));
395            if (version <= 0)
396                throw new IllegalArgumentException(tr("Version > 0 expected. Got {0}.", version));
397            if (dataSet != null && id != this.id) {
398                DataSet datasetCopy = dataSet;
399                // Reindex primitive
400                datasetCopy.removePrimitive(this);
401                this.id = id;
402                datasetCopy.addPrimitive(this);
403            }
404            super.setOsmId(id, version);
405        } finally {
406            writeUnlock(locked);
407        }
408    }
409
410    /**
411     * Clears the metadata, including id and version known to the OSM API.
412     * The id is a new unique id. The version, changeset and timestamp are set to 0.
413     * incomplete and deleted are set to false. It's preferred to use copy constructor with clearMetadata set to true instead
414     *
415     * <strong>Caution</strong>: Do not use this method on primitives which are already added to a {@link DataSet}.
416     *
417     * @throws DataIntegrityProblemException If primitive was already added to the dataset
418     * @since 6140
419     */
420    @Override
421    public void clearOsmMetadata() {
422        if (dataSet != null)
423            throw new DataIntegrityProblemException("Method cannot be called after primitive was added to the dataset");
424        super.clearOsmMetadata();
425    }
426
427    @Override
428    public void setUser(User user) {
429        boolean locked = writeLock();
430        try {
431            super.setUser(user);
432        } finally {
433            writeUnlock(locked);
434        }
435    }
436
437    @Override
438    public void setChangesetId(int changesetId) {
439        boolean locked = writeLock();
440        try {
441            int old = this.changesetId;
442            super.setChangesetId(changesetId);
443            if (dataSet != null) {
444                dataSet.fireChangesetIdChanged(this, old, changesetId);
445            }
446        } finally {
447            writeUnlock(locked);
448        }
449    }
450
451    @Override
452    public void setTimestamp(Date timestamp) {
453        boolean locked = writeLock();
454        try {
455            super.setTimestamp(timestamp);
456        } finally {
457            writeUnlock(locked);
458        }
459    }
460
461
462    /* -------
463    /* FLAGS
464    /* ------*/
465
466    private void updateFlagsNoLock(int flag, boolean value) {
467        super.updateFlags(flag, value);
468    }
469
470    @Override
471    protected final void updateFlags(int flag, boolean value) {
472        boolean locked = writeLock();
473        try {
474            updateFlagsNoLock(flag, value);
475        } finally {
476            writeUnlock(locked);
477        }
478    }
479
480    /**
481     * Make the primitive disabled (e.g.&nbsp;if a filter applies).
482     *
483     * To enable the primitive again, use unsetDisabledState.
484     * @param hidden if the primitive should be completely hidden from view or
485     *             just shown in gray color.
486     * @return true, any flag has changed; false if you try to set the disabled
487     * state to the value that is already preset
488     */
489    public boolean setDisabledState(boolean hidden) {
490        boolean locked = writeLock();
491        try {
492            int oldFlags = flags;
493            updateFlagsNoLock(FLAG_DISABLED, true);
494            updateFlagsNoLock(FLAG_HIDE_IF_DISABLED, hidden);
495            return oldFlags != flags;
496        } finally {
497            writeUnlock(locked);
498        }
499    }
500
501    /**
502     * Remove the disabled flag from the primitive.
503     * Afterwards, the primitive is displayed normally and can be selected again.
504     * @return {@code true} if a change occurred
505     */
506    public boolean unsetDisabledState() {
507        boolean locked = writeLock();
508        try {
509            int oldFlags = flags;
510            updateFlagsNoLock(FLAG_DISABLED + FLAG_HIDE_IF_DISABLED, false);
511            return oldFlags != flags;
512        } finally {
513            writeUnlock(locked);
514        }
515    }
516
517    /**
518     * Set binary property used internally by the filter mechanism.
519     * @param isExplicit new "disabled type" flag value
520     */
521    public void setDisabledType(boolean isExplicit) {
522        updateFlags(FLAG_DISABLED_TYPE, isExplicit);
523    }
524
525    /**
526     * Set binary property used internally by the filter mechanism.
527     * @param isExplicit new "hidden type" flag value
528     */
529    public void setHiddenType(boolean isExplicit) {
530        updateFlags(FLAG_HIDDEN_TYPE, isExplicit);
531    }
532
533    /**
534     * Replies true, if this primitive is disabled. (E.g. a filter applies)
535     * @return {@code true} if this object has the "disabled" flag enabled
536     */
537    public boolean isDisabled() {
538        return (flags & FLAG_DISABLED) != 0;
539    }
540
541    /**
542     * Replies true, if this primitive is disabled and marked as completely hidden on the map.
543     * @return {@code true} if this object has both the "disabled" and "hide if disabled" flags enabled
544     */
545    public boolean isDisabledAndHidden() {
546        return ((flags & FLAG_DISABLED) != 0) && ((flags & FLAG_HIDE_IF_DISABLED) != 0);
547    }
548
549    /**
550     * Get binary property used internally by the filter mechanism.
551     * @return {@code true} if this object has the "hidden type" flag enabled
552     */
553    public boolean getHiddenType() {
554        return (flags & FLAG_HIDDEN_TYPE) != 0;
555    }
556
557    /**
558     * Get binary property used internally by the filter mechanism.
559     * @return {@code true} if this object has the "disabled type" flag enabled
560     */
561    public boolean getDisabledType() {
562        return (flags & FLAG_DISABLED_TYPE) != 0;
563    }
564
565    /**
566     * Determines if this object is selectable.
567     * @return {@code true} if this object is selectable
568     */
569    public boolean isSelectable() {
570        return (flags & (FLAG_DELETED + FLAG_INCOMPLETE + FLAG_DISABLED + FLAG_HIDE_IF_DISABLED)) == 0;
571    }
572
573    /**
574     * Determines if this object is drawable.
575     * @return {@code true} if this object is drawable
576     */
577    public boolean isDrawable() {
578        return (flags & (FLAG_DELETED + FLAG_INCOMPLETE + FLAG_HIDE_IF_DISABLED)) == 0;
579    }
580
581    @Override
582    public void setModified(boolean modified) {
583        boolean locked = writeLock();
584        try {
585            super.setModified(modified);
586            if (dataSet != null) {
587                dataSet.firePrimitiveFlagsChanged(this);
588            }
589            clearCachedStyle();
590        } finally {
591            writeUnlock(locked);
592        }
593    }
594
595    @Override
596    public void setVisible(boolean visible) {
597        boolean locked = writeLock();
598        try {
599            super.setVisible(visible);
600            clearCachedStyle();
601        } finally {
602            writeUnlock(locked);
603        }
604    }
605
606    @Override
607    public void setDeleted(boolean deleted) {
608        boolean locked = writeLock();
609        try {
610            super.setDeleted(deleted);
611            if (dataSet != null) {
612                if (deleted) {
613                    dataSet.firePrimitivesRemoved(Collections.singleton(this), false);
614                } else {
615                    dataSet.firePrimitivesAdded(Collections.singleton(this), false);
616                }
617            }
618            clearCachedStyle();
619        } finally {
620            writeUnlock(locked);
621        }
622    }
623
624    @Override
625    protected final void setIncomplete(boolean incomplete) {
626        boolean locked = writeLock();
627        try {
628            if (dataSet != null && incomplete != this.isIncomplete()) {
629                if (incomplete) {
630                    dataSet.firePrimitivesRemoved(Collections.singletonList(this), true);
631                } else {
632                    dataSet.firePrimitivesAdded(Collections.singletonList(this), true);
633                }
634            }
635            super.setIncomplete(incomplete);
636        } finally {
637            writeUnlock(locked);
638        }
639    }
640
641    /**
642     * Determines whether the primitive is selected
643     * @return whether the primitive is selected
644     * @see DataSet#isSelected(OsmPrimitive)
645     */
646    public boolean isSelected() {
647        return dataSet != null && dataSet.isSelected(this);
648    }
649
650    /**
651     * Determines if this primitive is a member of a selected relation.
652     * @return {@code true} if this primitive is a member of a selected relation, {@code false} otherwise
653     */
654    public boolean isMemberOfSelected() {
655        if (referrers == null)
656            return false;
657        if (referrers instanceof OsmPrimitive)
658            return referrers instanceof Relation && ((OsmPrimitive) referrers).isSelected();
659        for (OsmPrimitive ref : (OsmPrimitive[]) referrers) {
660            if (ref instanceof Relation && ref.isSelected())
661                return true;
662        }
663        return false;
664    }
665
666    /**
667     * Determines if this primitive is an outer member of a selected multipolygon relation.
668     * @return {@code true} if this primitive is an outer member of a selected multipolygon relation, {@code false} otherwise
669     * @since 7621
670     */
671    public boolean isOuterMemberOfSelected() {
672        if (referrers == null)
673            return false;
674        if (referrers instanceof OsmPrimitive) {
675            return isOuterMemberOfMultipolygon((OsmPrimitive) referrers);
676        }
677        for (OsmPrimitive ref : (OsmPrimitive[]) referrers) {
678            if (isOuterMemberOfMultipolygon(ref))
679                return true;
680        }
681        return false;
682    }
683
684    private boolean isOuterMemberOfMultipolygon(OsmPrimitive ref) {
685        if (ref instanceof Relation && ref.isSelected() && ((Relation) ref).isMultipolygon()) {
686            for (RelationMember rm : ((Relation) ref).getMembersFor(Collections.singleton(this))) {
687                if ("outer".equals(rm.getRole())) {
688                    return true;
689                }
690            }
691        }
692        return false;
693    }
694
695    /**
696     * Updates the highlight flag for this primitive.
697     * @param highlighted The new highlight flag.
698     */
699    public void setHighlighted(boolean highlighted) {
700        if (isHighlighted() != highlighted) {
701            updateFlags(FLAG_HIGHLIGHTED, highlighted);
702            if (dataSet != null) {
703                dataSet.fireHighlightingChanged();
704            }
705        }
706    }
707
708    /**
709     * Checks if the highlight flag for this primitive was set
710     * @return The highlight flag.
711     */
712    public boolean isHighlighted() {
713        return (flags & FLAG_HIGHLIGHTED) != 0;
714    }
715
716    /*---------------------------------------------------
717     * WORK IN PROGRESS, UNINTERESTING AND DIRECTION KEYS
718     *--------------------------------------------------*/
719
720    private static volatile Collection<String> workinprogress;
721    private static volatile Collection<String> uninteresting;
722    private static volatile Collection<String> discardable;
723
724    /**
725     * Returns a list of "uninteresting" keys that do not make an object
726     * "tagged".  Entries that end with ':' are causing a whole namespace to be considered
727     * "uninteresting".  Only the first level namespace is considered.
728     * Initialized by isUninterestingKey()
729     * @return The list of uninteresting keys.
730     */
731    public static Collection<String> getUninterestingKeys() {
732        if (uninteresting == null) {
733            List<String> l = new LinkedList<>(Arrays.asList(
734                "source", "source_ref", "source:", "comment",
735                "converted_by", "watch", "watch:",
736                "description", "attribution"));
737            l.addAll(getDiscardableKeys());
738            l.addAll(getWorkInProgressKeys());
739            uninteresting = Main.pref.getCollection("tags.uninteresting", l);
740        }
741        return uninteresting;
742    }
743
744    /**
745     * Returns a list of keys which have been deemed uninteresting to the point
746     * that they can be silently removed from data which is being edited.
747     * @return The list of discardable keys.
748     */
749    public static Collection<String> getDiscardableKeys() {
750        if (discardable == null) {
751            discardable = Main.pref.getCollection("tags.discardable",
752                    Arrays.asList(
753                            "created_by",
754                            "geobase:datasetName",
755                            "geobase:uuid",
756                            "KSJ2:ADS",
757                            "KSJ2:ARE",
758                            "KSJ2:AdminArea",
759                            "KSJ2:COP_label",
760                            "KSJ2:DFD",
761                            "KSJ2:INT",
762                            "KSJ2:INT_label",
763                            "KSJ2:LOC",
764                            "KSJ2:LPN",
765                            "KSJ2:OPC",
766                            "KSJ2:PubFacAdmin",
767                            "KSJ2:RAC",
768                            "KSJ2:RAC_label",
769                            "KSJ2:RIC",
770                            "KSJ2:RIN",
771                            "KSJ2:WSC",
772                            "KSJ2:coordinate",
773                            "KSJ2:curve_id",
774                            "KSJ2:curve_type",
775                            "KSJ2:filename",
776                            "KSJ2:lake_id",
777                            "KSJ2:lat",
778                            "KSJ2:long",
779                            "KSJ2:river_id",
780                            "odbl",
781                            "odbl:note",
782                            "SK53_bulk:load",
783                            "sub_sea:type",
784                            "tiger:source",
785                            "tiger:separated",
786                            "tiger:tlid",
787                            "tiger:upload_uuid",
788                            "yh:LINE_NAME",
789                            "yh:LINE_NUM",
790                            "yh:STRUCTURE",
791                            "yh:TOTYUMONO",
792                            "yh:TYPE",
793                            "yh:WIDTH",
794                            "yh:WIDTH_RANK"
795                        ));
796        }
797        return discardable;
798    }
799
800    /**
801     * Returns a list of "work in progress" keys that do not make an object
802     * "tagged" but "annotated".
803     * @return The list of work in progress keys.
804     * @since 5754
805     */
806    public static Collection<String> getWorkInProgressKeys() {
807        if (workinprogress == null) {
808            workinprogress = Main.pref.getCollection("tags.workinprogress",
809                    Arrays.asList("note", "fixme", "FIXME"));
810        }
811        return workinprogress;
812    }
813
814    /**
815     * Determines if key is considered "uninteresting".
816     * @param key The key to check
817     * @return true if key is considered "uninteresting".
818     */
819    public static boolean isUninterestingKey(String key) {
820        getUninterestingKeys();
821        if (uninteresting.contains(key))
822            return true;
823        int pos = key.indexOf(':');
824        if (pos > 0)
825            return uninteresting.contains(key.substring(0, pos + 1));
826        return false;
827    }
828
829    /**
830     * Returns {@link #getKeys()} for which {@code key} does not fulfill {@link #isUninterestingKey}.
831     * @return A map of interesting tags
832     */
833    public Map<String, String> getInterestingTags() {
834        Map<String, String> result = new HashMap<>();
835        String[] keys = this.keys;
836        if (keys != null) {
837            for (int i = 0; i < keys.length; i += 2) {
838                if (!isUninterestingKey(keys[i])) {
839                    result.put(keys[i], keys[i + 1]);
840                }
841            }
842        }
843        return result;
844    }
845
846    /**
847     * A tagged way that matches this pattern has a direction.
848     * @see #FLAG_HAS_DIRECTIONS
849     */
850    private static volatile Match directionKeys;
851
852    /**
853     * A tagged way that matches this pattern has a direction that is reversed.
854     * <p>
855     * This pattern should be a subset of {@link #directionKeys}
856     * @see #FLAG_DIRECTION_REVERSED
857     */
858    private static volatile Match reversedDirectionKeys;
859
860    static {
861        String reversedDirectionDefault = "oneway=\"-1\"";
862
863        String directionDefault = "oneway? | (aerialway=* -aerialway=station) | "+
864                "waterway=stream | waterway=river | waterway=ditch | waterway=drain | "+
865                "\"piste:type\"=downhill | \"piste:type\"=sled | man_made=\"piste:halfpipe\" | "+
866                "junction=roundabout | (highway=motorway & -oneway=no & -oneway=reversible) | "+
867                "(highway=motorway_link & -oneway=no & -oneway=reversible)";
868
869        reversedDirectionKeys = compileDirectionKeys("tags.reversed_direction", reversedDirectionDefault);
870        directionKeys = compileDirectionKeys("tags.direction", directionDefault);
871    }
872
873    private static Match compileDirectionKeys(String prefName, String defaultValue) throws AssertionError {
874        try {
875            return SearchCompiler.compile(Main.pref.get(prefName, defaultValue));
876        } catch (ParseError e) {
877            Main.error(e, "Unable to compile pattern for " + prefName + ", trying default pattern:");
878        }
879
880        try {
881            return SearchCompiler.compile(defaultValue);
882        } catch (ParseError e2) {
883            throw new AssertionError("Unable to compile default pattern for direction keys: " + e2.getMessage(), e2);
884        }
885    }
886
887    private void updateTagged() {
888        for (String key: keySet()) {
889            // 'area' is not really uninteresting (putting it in that list may have unpredictable side effects)
890            // but it's clearly not enough to consider an object as tagged (see #9261)
891            if (!isUninterestingKey(key) && !"area".equals(key)) {
892                updateFlagsNoLock(FLAG_TAGGED, true);
893                return;
894            }
895        }
896        updateFlagsNoLock(FLAG_TAGGED, false);
897    }
898
899    private void updateAnnotated() {
900        for (String key: keySet()) {
901            if (getWorkInProgressKeys().contains(key)) {
902                updateFlagsNoLock(FLAG_ANNOTATED, true);
903                return;
904            }
905        }
906        updateFlagsNoLock(FLAG_ANNOTATED, false);
907    }
908
909    /**
910     * Determines if this object is considered "tagged". To be "tagged", an object
911     * must have one or more "interesting" tags. "created_by" and "source"
912     * are typically considered "uninteresting" and do not make an object
913     * "tagged".
914     * @return true if this object is considered "tagged"
915     */
916    public boolean isTagged() {
917        return (flags & FLAG_TAGGED) != 0;
918    }
919
920    /**
921     * Determines if this object is considered "annotated". To be "annotated", an object
922     * must have one or more "work in progress" tags, such as "note" or "fixme".
923     * @return true if this object is considered "annotated"
924     * @since 5754
925     */
926    public boolean isAnnotated() {
927        return (flags & FLAG_ANNOTATED) != 0;
928    }
929
930    private void updateDirectionFlags() {
931        boolean hasDirections = false;
932        boolean directionReversed = false;
933        if (reversedDirectionKeys.match(this)) {
934            hasDirections = true;
935            directionReversed = true;
936        }
937        if (directionKeys.match(this)) {
938            hasDirections = true;
939        }
940
941        updateFlagsNoLock(FLAG_DIRECTION_REVERSED, directionReversed);
942        updateFlagsNoLock(FLAG_HAS_DIRECTIONS, hasDirections);
943    }
944
945    /**
946     * true if this object has direction dependent tags (e.g. oneway)
947     * @return {@code true} if this object has direction dependent tags
948     */
949    public boolean hasDirectionKeys() {
950        return (flags & FLAG_HAS_DIRECTIONS) != 0;
951    }
952
953    /**
954     * true if this object has the "reversed diretion" flag enabled
955     * @return {@code true} if this object has the "reversed diretion" flag enabled
956     */
957    public boolean reversedDirection() {
958        return (flags & FLAG_DIRECTION_REVERSED) != 0;
959    }
960
961    /*------------
962     * Keys handling
963     ------------*/
964
965    @Override
966    public final void setKeys(TagMap keys) {
967        boolean locked = writeLock();
968        try {
969            super.setKeys(keys);
970        } finally {
971            writeUnlock(locked);
972        }
973    }
974
975    @Override
976    public final void setKeys(Map<String, String> keys) {
977        boolean locked = writeLock();
978        try {
979            super.setKeys(keys);
980        } finally {
981            writeUnlock(locked);
982        }
983    }
984
985    @Override
986    public final void put(String key, String value) {
987        boolean locked = writeLock();
988        try {
989            super.put(key, value);
990        } finally {
991            writeUnlock(locked);
992        }
993    }
994
995    @Override
996    public final void remove(String key) {
997        boolean locked = writeLock();
998        try {
999            super.remove(key);
1000        } finally {
1001            writeUnlock(locked);
1002        }
1003    }
1004
1005    @Override
1006    public final void removeAll() {
1007        boolean locked = writeLock();
1008        try {
1009            super.removeAll();
1010        } finally {
1011            writeUnlock(locked);
1012        }
1013    }
1014
1015    @Override
1016    protected void keysChangedImpl(Map<String, String> originalKeys) {
1017        clearCachedStyle();
1018        if (dataSet != null) {
1019            for (OsmPrimitive ref : getReferrers()) {
1020                ref.clearCachedStyle();
1021            }
1022        }
1023        updateDirectionFlags();
1024        updateTagged();
1025        updateAnnotated();
1026        if (dataSet != null) {
1027            dataSet.fireTagsChanged(this, originalKeys);
1028        }
1029    }
1030
1031    /*------------
1032     * Referrers
1033     ------------*/
1034
1035    private Object referrers;
1036
1037    /**
1038     * Add new referrer. If referrer is already included then no action is taken
1039     * @param referrer The referrer to add
1040     */
1041    protected void addReferrer(OsmPrimitive referrer) {
1042        if (referrers == null) {
1043            referrers = referrer;
1044        } else if (referrers instanceof OsmPrimitive) {
1045            if (referrers != referrer) {
1046                referrers = new OsmPrimitive[] {(OsmPrimitive) referrers, referrer};
1047            }
1048        } else {
1049            for (OsmPrimitive primitive:(OsmPrimitive[]) referrers) {
1050                if (primitive == referrer)
1051                    return;
1052            }
1053            referrers = Utils.addInArrayCopy((OsmPrimitive[]) referrers, referrer);
1054        }
1055    }
1056
1057    /**
1058     * Remove referrer. No action is taken if referrer is not registered
1059     * @param referrer The referrer to remove
1060     */
1061    protected void removeReferrer(OsmPrimitive referrer) {
1062        if (referrers instanceof OsmPrimitive) {
1063            if (referrers == referrer) {
1064                referrers = null;
1065            }
1066        } else if (referrers instanceof OsmPrimitive[]) {
1067            OsmPrimitive[] orig = (OsmPrimitive[]) referrers;
1068            int idx = -1;
1069            for (int i = 0; i < orig.length; i++) {
1070                if (orig[i] == referrer) {
1071                    idx = i;
1072                    break;
1073                }
1074            }
1075            if (idx == -1)
1076                return;
1077
1078            if (orig.length == 2) {
1079                referrers = orig[1-idx]; // idx is either 0 or 1, take the other
1080            } else { // downsize the array
1081                OsmPrimitive[] smaller = new OsmPrimitive[orig.length-1];
1082                System.arraycopy(orig, 0, smaller, 0, idx);
1083                System.arraycopy(orig, idx+1, smaller, idx, smaller.length-idx);
1084                referrers = smaller;
1085            }
1086        }
1087    }
1088
1089    /**
1090     * Find primitives that reference this primitive. Returns only primitives that are included in the same
1091     * dataset as this primitive. <br>
1092     *
1093     * For example following code will add wnew as referer to all nodes of existingWay, but this method will
1094     * not return wnew because it's not part of the dataset <br>
1095     *
1096     * <code>Way wnew = new Way(existingWay)</code>
1097     *
1098     * @param allowWithoutDataset If true, method will return empty list if primitive is not part of the dataset. If false,
1099     * exception will be thrown in this case
1100     *
1101     * @return a collection of all primitives that reference this primitive.
1102     */
1103    public final List<OsmPrimitive> getReferrers(boolean allowWithoutDataset) {
1104        // Returns only referrers that are members of the same dataset (primitive can have some fake references, for example
1105        // when way is cloned
1106
1107        if (dataSet == null && allowWithoutDataset)
1108            return Collections.emptyList();
1109
1110        checkDataset();
1111        Object referrers = this.referrers;
1112        List<OsmPrimitive> result = new ArrayList<>();
1113        if (referrers != null) {
1114            if (referrers instanceof OsmPrimitive) {
1115                OsmPrimitive ref = (OsmPrimitive) referrers;
1116                if (ref.dataSet == dataSet) {
1117                    result.add(ref);
1118                }
1119            } else {
1120                for (OsmPrimitive o:(OsmPrimitive[]) referrers) {
1121                    if (dataSet == o.dataSet) {
1122                        result.add(o);
1123                    }
1124                }
1125            }
1126        }
1127        return result;
1128    }
1129
1130    public final List<OsmPrimitive> getReferrers() {
1131        return getReferrers(false);
1132    }
1133
1134    /**
1135     * <p>Visits {@code visitor} for all referrers.</p>
1136     *
1137     * @param visitor the visitor. Ignored, if null.
1138     */
1139    public void visitReferrers(Visitor visitor) {
1140        if (visitor == null) return;
1141        if (this.referrers == null)
1142            return;
1143        else if (this.referrers instanceof OsmPrimitive) {
1144            OsmPrimitive ref = (OsmPrimitive) this.referrers;
1145            if (ref.dataSet == dataSet) {
1146                ref.accept(visitor);
1147            }
1148        } else if (this.referrers instanceof OsmPrimitive[]) {
1149            OsmPrimitive[] refs = (OsmPrimitive[]) this.referrers;
1150            for (OsmPrimitive ref: refs) {
1151                if (ref.dataSet == dataSet) {
1152                    ref.accept(visitor);
1153                }
1154            }
1155        }
1156    }
1157
1158    /**
1159      Return true, if this primitive is referred by at least n ways
1160      @param n Minimal number of ways to return true. Must be positive
1161     * @return {@code true} if this primitive is referred by at least n ways
1162     */
1163    public final boolean isReferredByWays(int n) {
1164        // Count only referrers that are members of the same dataset (primitive can have some fake references, for example
1165        // when way is cloned
1166        Object referrers = this.referrers;
1167        if (referrers == null) return false;
1168        checkDataset();
1169        if (referrers instanceof OsmPrimitive)
1170            return n <= 1 && referrers instanceof Way && ((OsmPrimitive) referrers).dataSet == dataSet;
1171        else {
1172            int counter = 0;
1173            for (OsmPrimitive o : (OsmPrimitive[]) referrers) {
1174                if (dataSet == o.dataSet && o instanceof Way) {
1175                    if (++counter >= n)
1176                        return true;
1177                }
1178            }
1179            return false;
1180        }
1181    }
1182
1183    /*-----------------
1184     * OTHER METHODS
1185     *----------------*/
1186
1187    /**
1188     * Implementation of the visitor scheme. Subclasses have to call the correct
1189     * visitor function.
1190     * @param visitor The visitor from which the visit() function must be called.
1191     */
1192    public abstract void accept(Visitor visitor);
1193
1194    /**
1195     * Get and write all attributes from the parameter. Does not fire any listener, so
1196     * use this only in the data initializing phase
1197     * @param other other primitive
1198     */
1199    public void cloneFrom(OsmPrimitive other) {
1200        // write lock is provided by subclasses
1201        if (id != other.id && dataSet != null)
1202            throw new DataIntegrityProblemException("Osm id cannot be changed after primitive was added to the dataset");
1203
1204        super.cloneFrom(other);
1205        clearCachedStyle();
1206    }
1207
1208    /**
1209     * Merges the technical and semantical attributes from <code>other</code> onto this.
1210     *
1211     * Both this and other must be new, or both must be assigned an OSM ID. If both this and <code>other</code>
1212     * have an assigend OSM id, the IDs have to be the same.
1213     *
1214     * @param other the other primitive. Must not be null.
1215     * @throws IllegalArgumentException if other is null.
1216     * @throws DataIntegrityProblemException if either this is new and other is not, or other is new and this is not
1217     * @throws DataIntegrityProblemException if other isn't new and other.getId() != this.getId()
1218     */
1219    public void mergeFrom(OsmPrimitive other) {
1220        boolean locked = writeLock();
1221        try {
1222            CheckParameterUtil.ensureParameterNotNull(other, "other");
1223            if (other.isNew() ^ isNew())
1224                throw new DataIntegrityProblemException(
1225                        tr("Cannot merge because either of the participating primitives is new and the other is not"));
1226            if (!other.isNew() && other.getId() != id)
1227                throw new DataIntegrityProblemException(
1228                        tr("Cannot merge primitives with different ids. This id is {0}, the other is {1}", id, other.getId()));
1229
1230            setKeys(other.hasKeys() ? other.getKeys() : null);
1231            timestamp = other.timestamp;
1232            version = other.version;
1233            setIncomplete(other.isIncomplete());
1234            flags = other.flags;
1235            user = other.user;
1236            changesetId = other.changesetId;
1237        } finally {
1238            writeUnlock(locked);
1239        }
1240    }
1241
1242    /**
1243     * Replies true if other isn't null and has the same interesting tags (key/value-pairs) as this.
1244     *
1245     * @param other the other object primitive
1246     * @return true if other isn't null and has the same interesting tags (key/value-pairs) as this.
1247     */
1248    public boolean hasSameInterestingTags(OsmPrimitive other) {
1249        return (keys == null && other.keys == null)
1250                || getInterestingTags().equals(other.getInterestingTags());
1251    }
1252
1253    /**
1254     * Replies true if this primitive and other are equal with respect to their semantic attributes.
1255     * <ol>
1256     *   <li>equal id</li>
1257     *   <li>both are complete or both are incomplete</li>
1258     *   <li>both have the same tags</li>
1259     * </ol>
1260     * @param other other primitive to compare
1261     * @return true if this primitive and other are equal with respect to their semantic attributes.
1262     */
1263    public final boolean hasEqualSemanticAttributes(OsmPrimitive other) {
1264        return hasEqualSemanticAttributes(other, true);
1265    }
1266
1267    boolean hasEqualSemanticAttributes(final OsmPrimitive other, final boolean testInterestingTagsOnly) {
1268        if (!isNew() && id != other.id)
1269            return false;
1270        if (isIncomplete() ^ other.isIncomplete()) // exclusive or operator for performance (see #7159)
1271            return false;
1272        return testInterestingTagsOnly ? hasSameInterestingTags(other) : getKeys().equals(other.getKeys());
1273    }
1274
1275    /**
1276     * Replies true if this primitive and other are equal with respect to their technical attributes.
1277     * The attributes:
1278     * <ol>
1279     *   <li>deleted</li>
1280     *   <li>modified</li>
1281     *   <li>timestamp</li>
1282     *   <li>version</li>
1283     *   <li>visible</li>
1284     *   <li>user</li>
1285     * </ol>
1286     * have to be equal
1287     * @param other the other primitive
1288     * @return true if this primitive and other are equal with respect to their technical attributes
1289     */
1290    public boolean hasEqualTechnicalAttributes(OsmPrimitive other) {
1291        if (other == null) return false;
1292
1293        return isDeleted() == other.isDeleted()
1294                && isModified() == other.isModified()
1295                && timestamp == other.timestamp
1296                && version == other.version
1297                && isVisible() == other.isVisible()
1298                && (user == null ? other.user == null : user == other.user)
1299                && changesetId == other.changesetId;
1300    }
1301
1302    /**
1303     * Loads (clone) this primitive from provided PrimitiveData
1304     * @param data The object which should be cloned
1305     */
1306    public void load(PrimitiveData data) {
1307        // Write lock is provided by subclasses
1308        setKeys(data.hasKeys() ? data.getKeys() : null);
1309        setRawTimestamp(data.getRawTimestamp());
1310        user = data.getUser();
1311        setChangesetId(data.getChangesetId());
1312        setDeleted(data.isDeleted());
1313        setModified(data.isModified());
1314        setIncomplete(data.isIncomplete());
1315        version = data.getVersion();
1316    }
1317
1318    /**
1319     * Save parameters of this primitive to the transport object
1320     * @return The saved object data
1321     */
1322    public abstract PrimitiveData save();
1323
1324    /**
1325     * Save common parameters of primitives to the transport object
1326     * @param data The object to save the data into
1327     */
1328    protected void saveCommonAttributes(PrimitiveData data) {
1329        data.setId(id);
1330        data.setKeys(hasKeys() ? getKeys() : null);
1331        data.setRawTimestamp(getRawTimestamp());
1332        data.setUser(user);
1333        data.setDeleted(isDeleted());
1334        data.setModified(isModified());
1335        data.setVisible(isVisible());
1336        data.setIncomplete(isIncomplete());
1337        data.setChangesetId(changesetId);
1338        data.setVersion(version);
1339    }
1340
1341    /**
1342     * Fetch the bounding box of the primitive
1343     * @return Bounding box of the object
1344     */
1345    public abstract BBox getBBox();
1346
1347    /**
1348     * Called by Dataset to update cached position information of primitive (bbox, cached EarthNorth, ...)
1349     */
1350    public abstract void updatePosition();
1351
1352    /*----------------
1353     * OBJECT METHODS
1354     *---------------*/
1355
1356    @Override
1357    protected String getFlagsAsString() {
1358        StringBuilder builder = new StringBuilder(super.getFlagsAsString());
1359
1360        if (isDisabled()) {
1361            if (isDisabledAndHidden()) {
1362                builder.append('h');
1363            } else {
1364                builder.append('d');
1365            }
1366        }
1367        if (isTagged()) {
1368            builder.append('T');
1369        }
1370        if (hasDirectionKeys()) {
1371            if (reversedDirection()) {
1372                builder.append('<');
1373            } else {
1374                builder.append('>');
1375            }
1376        }
1377        return builder.toString();
1378    }
1379
1380    /**
1381     * Equal, if the id (and class) is equal.
1382     *
1383     * An primitive is equal to its incomplete counter part.
1384     */
1385    @Override
1386    public boolean equals(Object obj) {
1387        if (this == obj) return true;
1388        if (obj == null || getClass() != obj.getClass()) return false;
1389        OsmPrimitive that = (OsmPrimitive) obj;
1390        return Objects.equals(id, that.id);
1391    }
1392
1393    /**
1394     * Return the id plus the class type encoded as hashcode or super's hashcode if id is 0.
1395     *
1396     * An primitive has the same hashcode as its incomplete counterpart.
1397     */
1398    @Override
1399    public int hashCode() {
1400        return Objects.hash(id);
1401    }
1402
1403    /**
1404     * Replies the display name of a primitive formatted by <code>formatter</code>
1405     * @param formatter formatter to use
1406     *
1407     * @return the display name
1408     */
1409    public abstract String getDisplayName(NameFormatter formatter);
1410
1411    @Override
1412    public Collection<String> getTemplateKeys() {
1413        Collection<String> keySet = keySet();
1414        List<String> result = new ArrayList<>(keySet.size() + 2);
1415        result.add(SPECIAL_VALUE_ID);
1416        result.add(SPECIAL_VALUE_LOCAL_NAME);
1417        result.addAll(keySet);
1418        return result;
1419    }
1420
1421    @Override
1422    public Object getTemplateValue(String name, boolean special) {
1423        if (special) {
1424            String lc = name.toLowerCase(Locale.ENGLISH);
1425            if (SPECIAL_VALUE_ID.equals(lc))
1426                return getId();
1427            else if (SPECIAL_VALUE_LOCAL_NAME.equals(lc))
1428                return getLocalName();
1429            else
1430                return null;
1431
1432        } else
1433            return getIgnoreCase(name);
1434    }
1435
1436    @Override
1437    public boolean evaluateCondition(Match condition) {
1438        return condition.match(this);
1439    }
1440
1441    /**
1442     * Replies the set of referring relations
1443     * @param primitives primitives to fetch relations from
1444     *
1445     * @return the set of referring relations
1446     */
1447    public static Set<Relation> getParentRelations(Collection<? extends OsmPrimitive> primitives) {
1448        Set<Relation> ret = new HashSet<>();
1449        for (OsmPrimitive w : primitives) {
1450            ret.addAll(OsmPrimitive.getFilteredList(w.getReferrers(), Relation.class));
1451        }
1452        return ret;
1453    }
1454
1455    /**
1456     * Determines if this primitive has tags denoting an area.
1457     * @return {@code true} if this primitive has tags denoting an area, {@code false} otherwise.
1458     * @since 6491
1459     */
1460    public final boolean hasAreaTags() {
1461        return hasKey("landuse")
1462                || "yes".equals(get("area"))
1463                || "riverbank".equals(get("waterway"))
1464                || hasKey("natural")
1465                || hasKey("amenity")
1466                || hasKey("leisure")
1467                || hasKey("building")
1468                || hasKey("building:part");
1469    }
1470
1471    /**
1472     * Determines if this primitive semantically concerns an area.
1473     * @return {@code true} if this primitive semantically concerns an area, according to its type, geometry and tags, {@code false} otherwise.
1474     * @since 6491
1475     */
1476    public abstract boolean concernsArea();
1477
1478    /**
1479     * Tests if this primitive lies outside of the downloaded area of its {@link DataSet}.
1480     * @return {@code true} if this primitive lies outside of the downloaded area
1481     */
1482    public abstract boolean isOutsideDownloadArea();
1483}