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