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.Collection;
009import java.util.Collections;
010import java.util.Date;
011import java.util.HashSet;
012import java.util.LinkedHashSet;
013import java.util.LinkedList;
014import java.util.List;
015import java.util.Locale;
016import java.util.Map;
017import java.util.Objects;
018import java.util.Set;
019import java.util.function.Consumer;
020
021import org.openstreetmap.josm.data.osm.search.SearchCompiler;
022import org.openstreetmap.josm.data.osm.search.SearchCompiler.Match;
023import org.openstreetmap.josm.data.osm.search.SearchParseError;
024import org.openstreetmap.josm.data.osm.visitor.OsmPrimitiveVisitor;
025import org.openstreetmap.josm.data.osm.visitor.PrimitiveVisitor;
026import org.openstreetmap.josm.gui.mappaint.StyleCache;
027import org.openstreetmap.josm.spi.preferences.Config;
028import org.openstreetmap.josm.tools.CheckParameterUtil;
029import org.openstreetmap.josm.tools.Logging;
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 TemplateEngineDataProvider {
045    private static final String SPECIAL_VALUE_ID = "id";
046    private static final String SPECIAL_VALUE_LOCAL_NAME = "localname";
047
048    /**
049     * A tagged way that matches this pattern has a direction.
050     * @see #FLAG_HAS_DIRECTIONS
051     */
052    static volatile Match directionKeys;
053
054    /**
055     * A tagged way that matches this pattern has a direction that is reversed.
056     * <p>
057     * This pattern should be a subset of {@link #directionKeys}
058     * @see #FLAG_DIRECTION_REVERSED
059     */
060    private static volatile Match reversedDirectionKeys;
061
062    static {
063        String reversedDirectionDefault = "oneway=\"-1\"";
064
065        String directionDefault = "oneway? | "+
066                "(aerialway=chair_lift & -oneway=no) | "+
067                "(aerialway=rope_tow & -oneway=no) | "+
068                "(aerialway=magic_carpet & -oneway=no) | "+
069                "(aerialway=zip_line & -oneway=no) | "+
070                "(aerialway=drag_lift & -oneway=no) | "+
071                "(aerialway=t-bar & -oneway=no) | "+
072                "(aerialway=j-bar & -oneway=no) | "+
073                "(aerialway=platter & -oneway=no) | "+
074                "waterway=stream | waterway=river | waterway=ditch | waterway=drain | "+
075                "(\"piste:type\"=downhill & -area=yes) | (\"piste:type\"=sled & -area=yes) | (man_made=\"piste:halfpipe\" & -area=yes) | "+
076                "junction=roundabout | (highway=motorway & -oneway=no & -oneway=reversible) | "+
077                "(highway=motorway_link & -oneway=no & -oneway=reversible)";
078
079        reversedDirectionKeys = compileDirectionKeys("tags.reversed_direction", reversedDirectionDefault);
080        directionKeys = compileDirectionKeys("tags.direction", directionDefault);
081    }
082
083    /**
084     * Replies the sub-collection of {@link OsmPrimitive}s of type <code>type</code> present in
085     * another collection of {@link OsmPrimitive}s. The result collection is a list.
086     *
087     * If <code>list</code> is null, replies an empty list.
088     *
089     * @param <T> type of data (must be one of the {@link OsmPrimitive} types
090     * @param list  the original list
091     * @param type the type to filter for
092     * @return the sub-list of OSM primitives of type <code>type</code>
093     */
094    public static <T extends OsmPrimitive> List<T> getFilteredList(Collection<OsmPrimitive> list, Class<T> type) {
095        if (list == null) return Collections.emptyList();
096        List<T> ret = new LinkedList<>();
097        for (OsmPrimitive p: list) {
098            if (type.isInstance(p)) {
099                ret.add(type.cast(p));
100            }
101        }
102        return ret;
103    }
104
105    /**
106     * Replies the sub-collection of {@link OsmPrimitive}s of type <code>type</code> present in
107     * another collection of {@link OsmPrimitive}s. The result collection is a set.
108     *
109     * If <code>list</code> is null, replies an empty set.
110     *
111     * @param <T> type of data (must be one of the {@link OsmPrimitive} types
112     * @param set  the original collection
113     * @param type the type to filter for
114     * @return the sub-set of OSM primitives of type <code>type</code>
115     */
116    public static <T extends OsmPrimitive> Set<T> getFilteredSet(Collection<OsmPrimitive> set, Class<T> type) {
117        Set<T> ret = new LinkedHashSet<>();
118        if (set != null) {
119            for (OsmPrimitive p: set) {
120                if (type.isInstance(p)) {
121                    ret.add(type.cast(p));
122                }
123            }
124        }
125        return ret;
126    }
127
128    /**
129     * Replies the collection of referring primitives for the primitives in <code>primitives</code>.
130     *
131     * @param primitives the collection of primitives.
132     * @return the collection of referring primitives for the primitives in <code>primitives</code>;
133     * empty set if primitives is null or if there are no referring primitives
134     */
135    public static Set<OsmPrimitive> getReferrer(Collection<? extends OsmPrimitive> primitives) {
136        Set<OsmPrimitive> ret = new HashSet<>();
137        if (primitives == null || primitives.isEmpty()) return ret;
138        for (OsmPrimitive p: primitives) {
139            ret.addAll(p.getReferrers());
140        }
141        return ret;
142    }
143
144    /**
145     * Creates a new primitive for the given id.
146     *
147     * If allowNegativeId is set, provided id can be &lt; 0 and will be set to primitive without any processing.
148     * If allowNegativeId is not set, then id will have to be 0 (in that case new unique id will be generated) or
149     * positive number.
150     *
151     * @param id the id
152     * @param allowNegativeId {@code true} to allow negative id
153     * @throws IllegalArgumentException if id &lt; 0 and allowNegativeId is false
154     */
155    protected OsmPrimitive(long id, boolean allowNegativeId) {
156        if (allowNegativeId) {
157            this.id = id;
158        } else {
159            if (id < 0)
160                throw new IllegalArgumentException(MessageFormat.format("Expected ID >= 0. Got {0}.", id));
161            else if (id == 0) {
162                this.id = generateUniqueId();
163            } else {
164                this.id = id;
165            }
166
167        }
168        this.version = 0;
169        this.setIncomplete(id > 0);
170    }
171
172    /**
173     * Creates a new primitive for the given id and version.
174     *
175     * If allowNegativeId is set, provided id can be &lt; 0 and will be set to primitive without any processing.
176     * If allowNegativeId is not set, then id will have to be 0 (in that case new unique id will be generated) or
177     * positive number.
178     *
179     * If id is not &gt; 0 version is ignored and set to 0.
180     *
181     * @param id the id
182     * @param version the version (positive integer)
183     * @param allowNegativeId {@code true} to allow negative id
184     * @throws IllegalArgumentException if id &lt; 0 and allowNegativeId is false
185     */
186    protected OsmPrimitive(long id, int version, boolean allowNegativeId) {
187        this(id, allowNegativeId);
188        this.version = id > 0 ? version : 0;
189        setIncomplete(id > 0 && version == 0);
190    }
191
192    /*----------
193     * MAPPAINT
194     *--------*/
195    private StyleCache mappaintStyle;
196    private short mappaintCacheIdx;
197
198    @Override
199    public final StyleCache getCachedStyle() {
200        return mappaintStyle;
201    }
202
203    @Override
204    public final void setCachedStyle(StyleCache mappaintStyle) {
205        this.mappaintStyle = mappaintStyle;
206    }
207
208    @Override
209    public final boolean isCachedStyleUpToDate() {
210        return mappaintStyle != null && mappaintCacheIdx == dataSet.getMappaintCacheIndex();
211    }
212
213    @Override
214    public final void declareCachedStyleUpToDate() {
215        this.mappaintCacheIdx = dataSet.getMappaintCacheIndex();
216    }
217
218    /* end of mappaint data */
219
220    /*---------
221     * DATASET
222     *---------*/
223
224    /** the parent dataset */
225    private DataSet dataSet;
226
227    /**
228     * This method should never ever by called from somewhere else than Dataset.addPrimitive or removePrimitive methods
229     * @param dataSet the parent dataset
230     */
231    void setDataset(DataSet dataSet) {
232        if (this.dataSet != null && dataSet != null && this.dataSet != dataSet)
233            throw new DataIntegrityProblemException("Primitive cannot be included in more than one Dataset");
234        this.dataSet = dataSet;
235    }
236
237    @Override
238    public DataSet getDataSet() {
239        return dataSet;
240    }
241
242    /**
243     * Throws exception if primitive is not part of the dataset
244     */
245    public void checkDataset() {
246        if (dataSet == null)
247            throw new DataIntegrityProblemException("Primitive must be part of the dataset: " + toString());
248    }
249
250    /**
251     * Throws exception if primitive is in a read-only dataset
252     */
253    protected final void checkDatasetNotReadOnly() {
254        if (dataSet != null && dataSet.isLocked())
255            throw new DataIntegrityProblemException("Primitive cannot be modified in read-only dataset: " + toString());
256    }
257
258    protected boolean writeLock() {
259        if (dataSet != null) {
260            dataSet.beginUpdate();
261            return true;
262        } else
263            return false;
264    }
265
266    protected void writeUnlock(boolean locked) {
267        if (locked && dataSet != null) {
268            // It shouldn't be possible for dataset to become null because
269            // method calling setDataset would need write lock which is owned by this thread
270            dataSet.endUpdate();
271        }
272    }
273
274    /**
275     * Sets the id and the version of this primitive if it is known to the OSM API.
276     *
277     * Since we know the id and its version it can't be incomplete anymore. incomplete
278     * is set to false.
279     *
280     * @param id the id. &gt; 0 required
281     * @param version the version &gt; 0 required
282     * @throws IllegalArgumentException if id &lt;= 0
283     * @throws IllegalArgumentException if version &lt;= 0
284     * @throws DataIntegrityProblemException if id is changed and primitive was already added to the dataset
285     */
286    @Override
287    public void setOsmId(long id, int version) {
288        checkDatasetNotReadOnly();
289        boolean locked = writeLock();
290        try {
291            if (id <= 0)
292                throw new IllegalArgumentException(tr("ID > 0 expected. Got {0}.", id));
293            if (version <= 0)
294                throw new IllegalArgumentException(tr("Version > 0 expected. Got {0}.", version));
295            if (dataSet != null && id != this.id) {
296                DataSet datasetCopy = dataSet;
297                // Reindex primitive
298                datasetCopy.removePrimitive(this);
299                this.id = id;
300                datasetCopy.addPrimitive(this);
301            }
302            super.setOsmId(id, version);
303        } finally {
304            writeUnlock(locked);
305        }
306    }
307
308    /**
309     * Clears the metadata, including id and version known to the OSM API.
310     * The id is a new unique id. The version, changeset and timestamp are set to 0.
311     * incomplete and deleted are set to false. It's preferred to use copy constructor with clearMetadata set to true instead
312     *
313     * <strong>Caution</strong>: Do not use this method on primitives which are already added to a {@link DataSet}.
314     *
315     * @throws DataIntegrityProblemException If primitive was already added to the dataset
316     * @since 6140
317     */
318    @Override
319    public void clearOsmMetadata() {
320        if (dataSet != null)
321            throw new DataIntegrityProblemException("Method cannot be called after primitive was added to the dataset");
322        super.clearOsmMetadata();
323    }
324
325    @Override
326    public void setUser(User user) {
327        checkDatasetNotReadOnly();
328        boolean locked = writeLock();
329        try {
330            super.setUser(user);
331        } finally {
332            writeUnlock(locked);
333        }
334    }
335
336    @Override
337    public void setChangesetId(int changesetId) {
338        checkDatasetNotReadOnly();
339        boolean locked = writeLock();
340        try {
341            int old = this.changesetId;
342            super.setChangesetId(changesetId);
343            if (dataSet != null) {
344                dataSet.fireChangesetIdChanged(this, old, changesetId);
345            }
346        } finally {
347            writeUnlock(locked);
348        }
349    }
350
351    @Override
352    public void setTimestamp(Date timestamp) {
353        checkDatasetNotReadOnly();
354        boolean locked = writeLock();
355        try {
356            super.setTimestamp(timestamp);
357        } finally {
358            writeUnlock(locked);
359        }
360    }
361
362
363    /* -------
364    /* FLAGS
365    /* ------*/
366
367    private void updateFlagsNoLock(short flag, boolean value) {
368        super.updateFlags(flag, value);
369    }
370
371    @Override
372    protected final void updateFlags(short flag, boolean value) {
373        boolean locked = writeLock();
374        try {
375            updateFlagsNoLock(flag, value);
376        } finally {
377            writeUnlock(locked);
378        }
379    }
380
381    /**
382     * Make the primitive disabled (e.g.&nbsp;if a filter applies).
383     *
384     * To enable the primitive again, use unsetDisabledState.
385     * @param hidden if the primitive should be completely hidden from view or
386     *             just shown in gray color.
387     * @return true, any flag has changed; false if you try to set the disabled
388     * state to the value that is already preset
389     */
390    public boolean setDisabledState(boolean hidden) {
391        boolean locked = writeLock();
392        try {
393            int oldFlags = flags;
394            updateFlagsNoLock(FLAG_DISABLED, true);
395            updateFlagsNoLock(FLAG_HIDE_IF_DISABLED, hidden);
396            return oldFlags != flags;
397        } finally {
398            writeUnlock(locked);
399        }
400    }
401
402    /**
403     * Remove the disabled flag from the primitive.
404     * Afterwards, the primitive is displayed normally and can be selected again.
405     * @return {@code true} if a change occurred
406     */
407    public boolean unsetDisabledState() {
408        boolean locked = writeLock();
409        try {
410            int oldFlags = flags;
411            updateFlagsNoLock(FLAG_DISABLED, false);
412            updateFlagsNoLock(FLAG_HIDE_IF_DISABLED, false);
413            return oldFlags != flags;
414        } finally {
415            writeUnlock(locked);
416        }
417    }
418
419    /**
420     * Set binary property used internally by the filter mechanism.
421     * @param isExplicit new "disabled type" flag value
422     */
423    public void setDisabledType(boolean isExplicit) {
424        updateFlags(FLAG_DISABLED_TYPE, isExplicit);
425    }
426
427    /**
428     * Set binary property used internally by the filter mechanism.
429     * @param isExplicit new "hidden type" flag value
430     */
431    public void setHiddenType(boolean isExplicit) {
432        updateFlags(FLAG_HIDDEN_TYPE, isExplicit);
433    }
434
435    /**
436     * Set binary property used internally by the filter mechanism.
437     * @param isPreserved new "preserved" flag value
438     * @since 13309
439     */
440    public void setPreserved(boolean isPreserved) {
441        updateFlags(FLAG_PRESERVED, isPreserved);
442    }
443
444    @Override
445    public boolean isDisabled() {
446        return (flags & FLAG_DISABLED) != 0;
447    }
448
449    @Override
450    public boolean isDisabledAndHidden() {
451        return ((flags & FLAG_DISABLED) != 0) && ((flags & FLAG_HIDE_IF_DISABLED) != 0);
452    }
453
454    /**
455     * Get binary property used internally by the filter mechanism.
456     * @return {@code true} if this object has the "hidden type" flag enabled
457     */
458    public boolean getHiddenType() {
459        return (flags & FLAG_HIDDEN_TYPE) != 0;
460    }
461
462    /**
463     * Get binary property used internally by the filter mechanism.
464     * @return {@code true} if this object has the "disabled type" flag enabled
465     */
466    public boolean getDisabledType() {
467        return (flags & FLAG_DISABLED_TYPE) != 0;
468    }
469
470    @Override
471    public boolean isPreserved() {
472        return (flags & FLAG_PRESERVED) != 0;
473    }
474
475    @Override
476    public boolean isSelectable() {
477        // not synchronized -> check disabled twice just to be sure we did not have a race condition.
478        return !isDisabled() && isDrawable() && !isDisabled();
479    }
480
481    @Override
482    public boolean isDrawable() {
483        return (flags & (FLAG_DELETED + FLAG_INCOMPLETE + FLAG_HIDE_IF_DISABLED)) == 0;
484    }
485
486    @Override
487    public void setModified(boolean modified) {
488        checkDatasetNotReadOnly();
489        boolean locked = writeLock();
490        try {
491            super.setModified(modified);
492            if (dataSet != null) {
493                dataSet.firePrimitiveFlagsChanged(this);
494            }
495            clearCachedStyle();
496        } finally {
497            writeUnlock(locked);
498        }
499    }
500
501    @Override
502    public void setVisible(boolean visible) {
503        checkDatasetNotReadOnly();
504        boolean locked = writeLock();
505        try {
506            super.setVisible(visible);
507            clearCachedStyle();
508        } finally {
509            writeUnlock(locked);
510        }
511    }
512
513    @Override
514    public void setDeleted(boolean deleted) {
515        checkDatasetNotReadOnly();
516        boolean locked = writeLock();
517        try {
518            super.setDeleted(deleted);
519            if (dataSet != null) {
520                if (deleted) {
521                    dataSet.firePrimitivesRemoved(Collections.singleton(this), false);
522                } else {
523                    dataSet.firePrimitivesAdded(Collections.singleton(this), false);
524                }
525            }
526            clearCachedStyle();
527        } finally {
528            writeUnlock(locked);
529        }
530    }
531
532    @Override
533    protected final void setIncomplete(boolean incomplete) {
534        checkDatasetNotReadOnly();
535        boolean locked = writeLock();
536        try {
537            if (dataSet != null && incomplete != this.isIncomplete()) {
538                if (incomplete) {
539                    dataSet.firePrimitivesRemoved(Collections.singletonList(this), true);
540                } else {
541                    dataSet.firePrimitivesAdded(Collections.singletonList(this), true);
542                }
543            }
544            super.setIncomplete(incomplete);
545        } finally {
546            writeUnlock(locked);
547        }
548    }
549
550    @Override
551    public boolean isSelected() {
552        return dataSet != null && dataSet.isSelected(this);
553    }
554
555    @Override
556    public boolean isMemberOfSelected() {
557        if (referrers == null)
558            return false;
559        if (referrers instanceof OsmPrimitive)
560            return referrers instanceof Relation && ((OsmPrimitive) referrers).isSelected();
561        for (OsmPrimitive ref : (OsmPrimitive[]) referrers) {
562            if (ref instanceof Relation && ref.isSelected())
563                return true;
564        }
565        return false;
566    }
567
568    @Override
569    public boolean isOuterMemberOfSelected() {
570        if (referrers == null)
571            return false;
572        if (referrers instanceof OsmPrimitive) {
573            return isOuterMemberOfMultipolygon((OsmPrimitive) referrers);
574        }
575        for (OsmPrimitive ref : (OsmPrimitive[]) referrers) {
576            if (isOuterMemberOfMultipolygon(ref))
577                return true;
578        }
579        return false;
580    }
581
582    private boolean isOuterMemberOfMultipolygon(OsmPrimitive ref) {
583        if (ref instanceof Relation && ref.isSelected() && ((Relation) ref).isMultipolygon()) {
584            for (RelationMember rm : ((Relation) ref).getMembersFor(Collections.singleton(this))) {
585                if ("outer".equals(rm.getRole())) {
586                    return true;
587                }
588            }
589        }
590        return false;
591    }
592
593    @Override
594    public void setHighlighted(boolean highlighted) {
595        if (isHighlighted() != highlighted) {
596            updateFlags(FLAG_HIGHLIGHTED, highlighted);
597            if (dataSet != null) {
598                dataSet.fireHighlightingChanged();
599            }
600        }
601    }
602
603    @Override
604    public boolean isHighlighted() {
605        return (flags & FLAG_HIGHLIGHTED) != 0;
606    }
607
608    /*---------------
609     * DIRECTION KEYS
610     *---------------*/
611
612    private static Match compileDirectionKeys(String prefName, String defaultValue) throws AssertionError {
613        try {
614            return SearchCompiler.compile(Config.getPref().get(prefName, defaultValue));
615        } catch (SearchParseError e) {
616            Logging.log(Logging.LEVEL_ERROR, "Unable to compile pattern for " + prefName + ", trying default pattern:", e);
617        }
618
619        try {
620            return SearchCompiler.compile(defaultValue);
621        } catch (SearchParseError e2) {
622            throw new AssertionError("Unable to compile default pattern for direction keys: " + e2.getMessage(), e2);
623        }
624    }
625
626    private void updateTagged() {
627        for (String key: keySet()) {
628            // 'area' is not really uninteresting (putting it in that list may have unpredictable side effects)
629            // but it's clearly not enough to consider an object as tagged (see #9261)
630            if (!isUninterestingKey(key) && !"area".equals(key)) {
631                updateFlagsNoLock(FLAG_TAGGED, true);
632                return;
633            }
634        }
635        updateFlagsNoLock(FLAG_TAGGED, false);
636    }
637
638    private void updateAnnotated() {
639        for (String key: keySet()) {
640            if (getWorkInProgressKeys().contains(key)) {
641                updateFlagsNoLock(FLAG_ANNOTATED, true);
642                return;
643            }
644        }
645        updateFlagsNoLock(FLAG_ANNOTATED, false);
646    }
647
648    @Override
649    public boolean isTagged() {
650        return (flags & FLAG_TAGGED) != 0;
651    }
652
653    @Override
654    public boolean isAnnotated() {
655        return (flags & FLAG_ANNOTATED) != 0;
656    }
657
658    private void updateDirectionFlags() {
659        boolean hasDirections = false;
660        boolean directionReversed = false;
661        if (reversedDirectionKeys.match(this)) {
662            hasDirections = true;
663            directionReversed = true;
664        }
665        if (directionKeys.match(this)) {
666            hasDirections = true;
667        }
668
669        updateFlagsNoLock(FLAG_DIRECTION_REVERSED, directionReversed);
670        updateFlagsNoLock(FLAG_HAS_DIRECTIONS, hasDirections);
671    }
672
673    @Override
674    public boolean hasDirectionKeys() {
675        return (flags & FLAG_HAS_DIRECTIONS) != 0;
676    }
677
678    @Override
679    public boolean reversedDirection() {
680        return (flags & FLAG_DIRECTION_REVERSED) != 0;
681    }
682
683    /*------------
684     * Keys handling
685     ------------*/
686
687    @Override
688    public final void setKeys(TagMap keys) {
689        checkDatasetNotReadOnly();
690        boolean locked = writeLock();
691        try {
692            super.setKeys(keys);
693        } finally {
694            writeUnlock(locked);
695        }
696    }
697
698    @Override
699    public final void setKeys(Map<String, String> keys) {
700        checkDatasetNotReadOnly();
701        boolean locked = writeLock();
702        try {
703            super.setKeys(keys);
704        } finally {
705            writeUnlock(locked);
706        }
707    }
708
709    @Override
710    public final void put(String key, String value) {
711        checkDatasetNotReadOnly();
712        boolean locked = writeLock();
713        try {
714            super.put(key, value);
715        } finally {
716            writeUnlock(locked);
717        }
718    }
719
720    @Override
721    public final void remove(String key) {
722        checkDatasetNotReadOnly();
723        boolean locked = writeLock();
724        try {
725            super.remove(key);
726        } finally {
727            writeUnlock(locked);
728        }
729    }
730
731    @Override
732    public final void removeAll() {
733        checkDatasetNotReadOnly();
734        boolean locked = writeLock();
735        try {
736            super.removeAll();
737        } finally {
738            writeUnlock(locked);
739        }
740    }
741
742    @Override
743    protected void keysChangedImpl(Map<String, String> originalKeys) {
744        clearCachedStyle();
745        if (dataSet != null) {
746            for (OsmPrimitive ref : getReferrers()) {
747                ref.clearCachedStyle();
748            }
749        }
750        updateDirectionFlags();
751        updateTagged();
752        updateAnnotated();
753        if (dataSet != null) {
754            dataSet.fireTagsChanged(this, originalKeys);
755        }
756    }
757
758    /*------------
759     * Referrers
760     ------------*/
761
762    private Object referrers;
763
764    /**
765     * Add new referrer. If referrer is already included then no action is taken
766     * @param referrer The referrer to add
767     */
768    protected void addReferrer(OsmPrimitive referrer) {
769        checkDatasetNotReadOnly();
770        if (referrers == null) {
771            referrers = referrer;
772        } else if (referrers instanceof OsmPrimitive) {
773            if (referrers != referrer) {
774                referrers = new OsmPrimitive[] {(OsmPrimitive) referrers, referrer};
775            }
776        } else {
777            for (OsmPrimitive primitive:(OsmPrimitive[]) referrers) {
778                if (primitive == referrer)
779                    return;
780            }
781            referrers = Utils.addInArrayCopy((OsmPrimitive[]) referrers, referrer);
782        }
783    }
784
785    /**
786     * Remove referrer. No action is taken if referrer is not registered
787     * @param referrer The referrer to remove
788     */
789    protected void removeReferrer(OsmPrimitive referrer) {
790        checkDatasetNotReadOnly();
791        if (referrers instanceof OsmPrimitive) {
792            if (referrers == referrer) {
793                referrers = null;
794            }
795        } else if (referrers instanceof OsmPrimitive[]) {
796            OsmPrimitive[] orig = (OsmPrimitive[]) referrers;
797            int idx = -1;
798            for (int i = 0; i < orig.length; i++) {
799                if (orig[i] == referrer) {
800                    idx = i;
801                    break;
802                }
803            }
804            if (idx == -1)
805                return;
806
807            if (orig.length == 2) {
808                referrers = orig[1-idx]; // idx is either 0 or 1, take the other
809            } else { // downsize the array
810                OsmPrimitive[] smaller = new OsmPrimitive[orig.length-1];
811                System.arraycopy(orig, 0, smaller, 0, idx);
812                System.arraycopy(orig, idx+1, smaller, idx, smaller.length-idx);
813                referrers = smaller;
814            }
815        }
816    }
817
818    @Override
819    public final List<OsmPrimitive> getReferrers(boolean allowWithoutDataset) {
820        // Returns only referrers that are members of the same dataset (primitive can have some fake references, for example
821        // when way is cloned
822
823        if (dataSet == null && allowWithoutDataset)
824            return Collections.emptyList();
825
826        checkDataset();
827        Object referrers = this.referrers;
828        List<OsmPrimitive> result = new ArrayList<>();
829        if (referrers != null) {
830            if (referrers instanceof OsmPrimitive) {
831                OsmPrimitive ref = (OsmPrimitive) referrers;
832                if (ref.dataSet == dataSet) {
833                    result.add(ref);
834                }
835            } else {
836                for (OsmPrimitive o:(OsmPrimitive[]) referrers) {
837                    if (dataSet == o.dataSet) {
838                        result.add(o);
839                    }
840                }
841            }
842        }
843        return result;
844    }
845
846    @Override
847    public final List<OsmPrimitive> getReferrers() {
848        return getReferrers(false);
849    }
850
851    /**
852     * <p>Visits {@code visitor} for all referrers.</p>
853     *
854     * @param visitor the visitor. Ignored, if null.
855     * @since 12809
856     */
857    public void visitReferrers(OsmPrimitiveVisitor visitor) {
858        if (visitor != null)
859            doVisitReferrers(o -> o.accept(visitor));
860    }
861
862    @Override
863    public void visitReferrers(PrimitiveVisitor visitor) {
864        if (visitor != null)
865            doVisitReferrers(o -> o.accept(visitor));
866    }
867
868    private void doVisitReferrers(Consumer<OsmPrimitive> visitor) {
869        if (this.referrers == null)
870            return;
871        else if (this.referrers instanceof OsmPrimitive) {
872            OsmPrimitive ref = (OsmPrimitive) this.referrers;
873            if (ref.dataSet == dataSet) {
874                visitor.accept(ref);
875            }
876        } else if (this.referrers instanceof OsmPrimitive[]) {
877            OsmPrimitive[] refs = (OsmPrimitive[]) this.referrers;
878            for (OsmPrimitive ref: refs) {
879                if (ref.dataSet == dataSet) {
880                    visitor.accept(ref);
881                }
882            }
883        }
884    }
885
886    /**
887     * Return true, if this primitive is a node referred by at least n ways
888     * @param n Minimal number of ways to return true. Must be positive
889     * @return {@code true} if this primitive is referred by at least n ways
890     */
891    protected final boolean isNodeReferredByWays(int n) {
892        // Count only referrers that are members of the same dataset (primitive can have some fake references, for example
893        // when way is cloned
894        Object referrers = this.referrers;
895        if (referrers == null) return false;
896        checkDataset();
897        if (referrers instanceof OsmPrimitive)
898            return n <= 1 && referrers instanceof Way && ((OsmPrimitive) referrers).dataSet == dataSet;
899        else {
900            int counter = 0;
901            for (OsmPrimitive o : (OsmPrimitive[]) referrers) {
902                if (dataSet == o.dataSet && o instanceof Way && ++counter >= n)
903                    return true;
904            }
905            return false;
906        }
907    }
908
909    /*-----------------
910     * OTHER METHODS
911     *----------------*/
912
913    /**
914     * Implementation of the visitor scheme. Subclasses have to call the correct
915     * visitor function.
916     * @param visitor The visitor from which the visit() function must be called.
917     * @since 12809
918     */
919    public abstract void accept(OsmPrimitiveVisitor visitor);
920
921    /**
922     * Get and write all attributes from the parameter. Does not fire any listener, so
923     * use this only in the data initializing phase
924     * @param other other primitive
925     */
926    public void cloneFrom(OsmPrimitive other) {
927        // write lock is provided by subclasses
928        if (id != other.id && dataSet != null)
929            throw new DataIntegrityProblemException("Osm id cannot be changed after primitive was added to the dataset");
930
931        super.cloneFrom(other);
932        clearCachedStyle();
933    }
934
935    /**
936     * Merges the technical and semantical attributes from <code>other</code> onto this.
937     *
938     * Both this and other must be new, or both must be assigned an OSM ID. If both this and <code>other</code>
939     * have an assigned OSM id, the IDs have to be the same.
940     *
941     * @param other the other primitive. Must not be null.
942     * @throws IllegalArgumentException if other is null.
943     * @throws DataIntegrityProblemException if either this is new and other is not, or other is new and this is not
944     * @throws DataIntegrityProblemException if other isn't new and other.getId() != this.getId()
945     */
946    public void mergeFrom(OsmPrimitive other) {
947        checkDatasetNotReadOnly();
948        boolean locked = writeLock();
949        try {
950            CheckParameterUtil.ensureParameterNotNull(other, "other");
951            if (other.isNew() ^ isNew())
952                throw new DataIntegrityProblemException(
953                        tr("Cannot merge because either of the participating primitives is new and the other is not"));
954            if (!other.isNew() && other.getId() != id)
955                throw new DataIntegrityProblemException(
956                        tr("Cannot merge primitives with different ids. This id is {0}, the other is {1}", id, other.getId()));
957
958            setKeys(other.hasKeys() ? other.getKeys() : null);
959            timestamp = other.timestamp;
960            version = other.version;
961            setIncomplete(other.isIncomplete());
962            flags = other.flags;
963            user = other.user;
964            changesetId = other.changesetId;
965        } finally {
966            writeUnlock(locked);
967        }
968    }
969
970    /**
971     * Replies true if this primitive and other are equal with respect to their semantic attributes.
972     * <ol>
973     *   <li>equal id</li>
974     *   <li>both are complete or both are incomplete</li>
975     *   <li>both have the same tags</li>
976     * </ol>
977     * @param other other primitive to compare
978     * @return true if this primitive and other are equal with respect to their semantic attributes.
979     */
980    public final boolean hasEqualSemanticAttributes(OsmPrimitive other) {
981        return hasEqualSemanticAttributes(other, true);
982    }
983
984    boolean hasEqualSemanticFlags(final OsmPrimitive other) {
985        if (!isNew() && id != other.id)
986            return false;
987        return !(isIncomplete() ^ other.isIncomplete()); // exclusive or operator for performance (see #7159)
988    }
989
990    boolean hasEqualSemanticAttributes(final OsmPrimitive other, final boolean testInterestingTagsOnly) {
991        return hasEqualSemanticFlags(other)
992                && (testInterestingTagsOnly ? hasSameInterestingTags(other) : getKeys().equals(other.getKeys()));
993    }
994
995    /**
996     * Replies true if this primitive and other are equal with respect to their technical attributes.
997     * The attributes:
998     * <ol>
999     *   <li>deleted</li>
1000     *   <li>modified</li>
1001     *   <li>timestamp</li>
1002     *   <li>version</li>
1003     *   <li>visible</li>
1004     *   <li>user</li>
1005     * </ol>
1006     * have to be equal
1007     * @param other the other primitive
1008     * @return true if this primitive and other are equal with respect to their technical attributes
1009     */
1010    public boolean hasEqualTechnicalAttributes(OsmPrimitive other) {
1011        // CHECKSTYLE.OFF: BooleanExpressionComplexity
1012        return other != null
1013            && timestamp == other.timestamp
1014            && version == other.version
1015            && changesetId == other.changesetId
1016            && isDeleted() == other.isDeleted()
1017            && isModified() == other.isModified()
1018            && isVisible() == other.isVisible()
1019            && Objects.equals(user, other.user);
1020        // CHECKSTYLE.ON: BooleanExpressionComplexity
1021    }
1022
1023    /**
1024     * Loads (clone) this primitive from provided PrimitiveData
1025     * @param data The object which should be cloned
1026     */
1027    public void load(PrimitiveData data) {
1028        checkDatasetNotReadOnly();
1029        // Write lock is provided by subclasses
1030        setKeys(data.hasKeys() ? data.getKeys() : null);
1031        setRawTimestamp(data.getRawTimestamp());
1032        user = data.getUser();
1033        setChangesetId(data.getChangesetId());
1034        setDeleted(data.isDeleted());
1035        setModified(data.isModified());
1036        setVisible(data.isVisible());
1037        setIncomplete(data.isIncomplete());
1038        version = data.getVersion();
1039    }
1040
1041    /**
1042     * Save parameters of this primitive to the transport object
1043     * @return The saved object data
1044     */
1045    public abstract PrimitiveData save();
1046
1047    /**
1048     * Save common parameters of primitives to the transport object
1049     * @param data The object to save the data into
1050     */
1051    protected void saveCommonAttributes(PrimitiveData data) {
1052        data.setId(id);
1053        data.setKeys(hasKeys() ? getKeys() : null);
1054        data.setRawTimestamp(getRawTimestamp());
1055        data.setUser(user);
1056        data.setDeleted(isDeleted());
1057        data.setModified(isModified());
1058        data.setVisible(isVisible());
1059        data.setIncomplete(isIncomplete());
1060        data.setChangesetId(changesetId);
1061        data.setVersion(version);
1062    }
1063
1064    /**
1065     * Called by Dataset to update cached position information of primitive (bbox, cached EarthNorth, ...)
1066     */
1067    public abstract void updatePosition();
1068
1069    /*----------------
1070     * OBJECT METHODS
1071     *---------------*/
1072
1073    @Override
1074    protected String getFlagsAsString() {
1075        StringBuilder builder = new StringBuilder(super.getFlagsAsString());
1076
1077        if (isDisabled()) {
1078            if (isDisabledAndHidden()) {
1079                builder.append('h');
1080            } else {
1081                builder.append('d');
1082            }
1083        }
1084        if (isTagged()) {
1085            builder.append('T');
1086        }
1087        if (hasDirectionKeys()) {
1088            if (reversedDirection()) {
1089                builder.append('<');
1090            } else {
1091                builder.append('>');
1092            }
1093        }
1094        return builder.toString();
1095    }
1096
1097    /**
1098     * Equal, if the id (and class) is equal.
1099     *
1100     * An primitive is equal to its incomplete counter part.
1101     */
1102    @Override
1103    public boolean equals(Object obj) {
1104        if (this == obj) {
1105            return true;
1106        } else if (obj == null || getClass() != obj.getClass()) {
1107            return false;
1108        } else {
1109            OsmPrimitive that = (OsmPrimitive) obj;
1110            return id == that.id;
1111        }
1112    }
1113
1114    /**
1115     * Return the id plus the class type encoded as hashcode or super's hashcode if id is 0.
1116     *
1117     * An primitive has the same hashcode as its incomplete counterpart.
1118     */
1119    @Override
1120    public int hashCode() {
1121        return Long.hashCode(id);
1122    }
1123
1124    @Override
1125    public Collection<String> getTemplateKeys() {
1126        Collection<String> keySet = keySet();
1127        List<String> result = new ArrayList<>(keySet.size() + 2);
1128        result.add(SPECIAL_VALUE_ID);
1129        result.add(SPECIAL_VALUE_LOCAL_NAME);
1130        result.addAll(keySet);
1131        return result;
1132    }
1133
1134    @Override
1135    public Object getTemplateValue(String name, boolean special) {
1136        if (special) {
1137            String lc = name.toLowerCase(Locale.ENGLISH);
1138            if (SPECIAL_VALUE_ID.equals(lc))
1139                return getId();
1140            else if (SPECIAL_VALUE_LOCAL_NAME.equals(lc))
1141                return getLocalName();
1142            else
1143                return null;
1144
1145        } else
1146            return getIgnoreCase(name);
1147    }
1148
1149    @Override
1150    public boolean evaluateCondition(Match condition) {
1151        return condition.match(this);
1152    }
1153
1154    /**
1155     * Replies the set of referring relations
1156     * @param primitives primitives to fetch relations from
1157     *
1158     * @return the set of referring relations
1159     */
1160    public static Set<Relation> getParentRelations(Collection<? extends OsmPrimitive> primitives) {
1161        Set<Relation> ret = new HashSet<>();
1162        for (OsmPrimitive w : primitives) {
1163            ret.addAll(OsmPrimitive.getFilteredList(w.getReferrers(), Relation.class));
1164        }
1165        return ret;
1166    }
1167
1168    /**
1169     * Determines if this primitive has tags denoting an area.
1170     * @return {@code true} if this primitive has tags denoting an area, {@code false} otherwise.
1171     * @since 6491
1172     */
1173    public final boolean hasAreaTags() {
1174        return hasKey("landuse", "amenity", "building", "building:part")
1175                || hasTag("area", OsmUtils.TRUE_VALUE)
1176                || hasTag("waterway", "riverbank")
1177                || hasTagDifferent("leisure", "picnic_table", "slipway", "firepit")
1178                || hasTag("natural", "water", "wood", "scrub", "wetland", "grassland", "heath", "rock", "bare_rock",
1179                                     "sand", "beach", "scree", "bay", "glacier", "shingle", "fell", "reef", "stone",
1180                                     "mud", "landslide", "sinkhole", "crevasse", "desert");
1181    }
1182
1183    /**
1184     * Determines if this primitive semantically concerns an area.
1185     * @return {@code true} if this primitive semantically concerns an area, according to its type, geometry and tags, {@code false} otherwise.
1186     * @since 6491
1187     */
1188    public abstract boolean concernsArea();
1189
1190    /**
1191     * Tests if this primitive lies outside of the downloaded area of its {@link DataSet}.
1192     * @return {@code true} if this primitive lies outside of the downloaded area
1193     */
1194    public abstract boolean isOutsideDownloadArea();
1195
1196    /**
1197     * If necessary, extend the bbox to contain this primitive
1198     * @param box a bbox instance
1199     * @param visited a set of visited members  or null
1200     * @since 11269
1201     */
1202    protected abstract void addToBBox(BBox box, Set<PrimitiveId> visited);
1203}