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