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