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