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        if (referrers == null) {
820            return Stream.empty();
821        }
822        final Stream<OsmPrimitive> stream = referrers instanceof OsmPrimitive
823                ? Stream.of((OsmPrimitive) referrers)
824                : Arrays.stream((OsmPrimitive[]) referrers);
825        return stream
826                .filter(p -> p.dataSet == dataSet)
827                .filter(filter::isInstance)
828                .map(filter::cast);
829    }
830
831    /**
832     * Gets all primitives in the current dataset that reference this primitive.
833     * @param filter restrict primitives to subclasses
834     * @param <T> type of primitives
835     * @return the referrers as Stream
836     * @since 14654
837     */
838    public final <T extends OsmPrimitive> Stream<T> referrers(Class<T> filter) {
839        return referrers(false, filter);
840    }
841
842    @Override
843    public final List<OsmPrimitive> getReferrers(boolean allowWithoutDataset) {
844        return referrers(allowWithoutDataset, OsmPrimitive.class)
845                .collect(Collectors.toList());
846    }
847
848    @Override
849    public final List<OsmPrimitive> getReferrers() {
850        return getReferrers(false);
851    }
852
853    /**
854     * <p>Visits {@code visitor} for all referrers.</p>
855     *
856     * @param visitor the visitor. Ignored, if null.
857     * @since 12809
858     */
859    public void visitReferrers(OsmPrimitiveVisitor visitor) {
860        if (visitor != null)
861            doVisitReferrers(o -> o.accept(visitor));
862    }
863
864    @Override
865    public void visitReferrers(PrimitiveVisitor visitor) {
866        if (visitor != null)
867            doVisitReferrers(o -> o.accept(visitor));
868    }
869
870    private void doVisitReferrers(Consumer<OsmPrimitive> visitor) {
871        if (this.referrers == null)
872            return;
873        else if (this.referrers instanceof OsmPrimitive) {
874            OsmPrimitive ref = (OsmPrimitive) this.referrers;
875            if (ref.dataSet == dataSet) {
876                visitor.accept(ref);
877            }
878        } else if (this.referrers instanceof OsmPrimitive[]) {
879            OsmPrimitive[] refs = (OsmPrimitive[]) this.referrers;
880            for (OsmPrimitive ref: refs) {
881                if (ref.dataSet == dataSet) {
882                    visitor.accept(ref);
883                }
884            }
885        }
886    }
887
888    /**
889     * Return true, if this primitive is a node referred by at least n ways
890     * @param n Minimal number of ways to return true. Must be positive
891     * @return {@code true} if this primitive is referred by at least n ways
892     */
893    protected final boolean isNodeReferredByWays(int n) {
894        // Count only referrers that are members of the same dataset (primitive can have some fake references, for example
895        // when way is cloned
896        if (referrers == null) return false;
897        checkDataset();
898        if (referrers instanceof OsmPrimitive)
899            return n <= 1 && referrers instanceof Way && ((OsmPrimitive) referrers).dataSet == dataSet;
900        else {
901            int counter = 0;
902            for (OsmPrimitive o : (OsmPrimitive[]) referrers) {
903                if (dataSet == o.dataSet && o instanceof Way && ++counter >= n)
904                    return true;
905            }
906            return false;
907        }
908    }
909
910    /*-----------------
911     * OTHER METHODS
912     *----------------*/
913
914    /**
915     * Implementation of the visitor scheme. Subclasses have to call the correct
916     * visitor function.
917     * @param visitor The visitor from which the visit() function must be called.
918     * @since 12809
919     */
920    public abstract void accept(OsmPrimitiveVisitor visitor);
921
922    /**
923     * Get and write all attributes from the parameter. Does not fire any listener, so
924     * use this only in the data initializing phase
925     * @param other other primitive
926     */
927    public void cloneFrom(OsmPrimitive other) {
928        // write lock is provided by subclasses
929        if (id != other.id && dataSet != null)
930            throw new DataIntegrityProblemException("Osm id cannot be changed after primitive was added to the dataset");
931
932        super.cloneFrom(other);
933        clearCachedStyle();
934    }
935
936    /**
937     * Merges the technical and semantical attributes from <code>other</code> onto this.
938     *
939     * Both this and other must be new, or both must be assigned an OSM ID. If both this and <code>other</code>
940     * have an assigned OSM id, the IDs have to be the same.
941     *
942     * @param other the other primitive. Must not be null.
943     * @throws IllegalArgumentException if other is null.
944     * @throws DataIntegrityProblemException if either this is new and other is not, or other is new and this is not
945     * @throws DataIntegrityProblemException if other isn't new and other.getId() != this.getId()
946     */
947    public void mergeFrom(OsmPrimitive other) {
948        checkDatasetNotReadOnly();
949        boolean locked = writeLock();
950        try {
951            CheckParameterUtil.ensureParameterNotNull(other, "other");
952            if (other.isNew() ^ isNew())
953                throw new DataIntegrityProblemException(
954                        tr("Cannot merge because either of the participating primitives is new and the other is not"));
955            if (!other.isNew() && other.getId() != id)
956                throw new DataIntegrityProblemException(
957                        tr("Cannot merge primitives with different ids. This id is {0}, the other is {1}", id, other.getId()));
958
959            setKeys(other.hasKeys() ? other.getKeys() : null);
960            timestamp = other.timestamp;
961            version = other.version;
962            setIncomplete(other.isIncomplete());
963            flags = other.flags;
964            user = other.user;
965            changesetId = other.changesetId;
966        } finally {
967            writeUnlock(locked);
968        }
969    }
970
971    /**
972     * Replies true if this primitive and other are equal with respect to their semantic attributes.
973     * <ol>
974     *   <li>equal id</li>
975     *   <li>both are complete or both are incomplete</li>
976     *   <li>both have the same tags</li>
977     * </ol>
978     * @param other other primitive to compare
979     * @return true if this primitive and other are equal with respect to their semantic attributes.
980     */
981    public final boolean hasEqualSemanticAttributes(OsmPrimitive other) {
982        return hasEqualSemanticAttributes(other, true);
983    }
984
985    boolean hasEqualSemanticFlags(final OsmPrimitive other) {
986        if (!isNew() && id != other.id)
987            return false;
988        return !(isIncomplete() ^ other.isIncomplete()); // exclusive or operator for performance (see #7159)
989    }
990
991    boolean hasEqualSemanticAttributes(final OsmPrimitive other, final boolean testInterestingTagsOnly) {
992        return hasEqualSemanticFlags(other)
993                && (testInterestingTagsOnly ? hasSameInterestingTags(other) : getKeys().equals(other.getKeys()));
994    }
995
996    /**
997     * Replies true if this primitive and other are equal with respect to their technical attributes.
998     * The attributes:
999     * <ol>
1000     *   <li>deleted</li>
1001     *   <li>modified</li>
1002     *   <li>timestamp</li>
1003     *   <li>version</li>
1004     *   <li>visible</li>
1005     *   <li>user</li>
1006     * </ol>
1007     * have to be equal
1008     * @param other the other primitive
1009     * @return true if this primitive and other are equal with respect to their technical attributes
1010     */
1011    public boolean hasEqualTechnicalAttributes(OsmPrimitive other) {
1012        // CHECKSTYLE.OFF: BooleanExpressionComplexity
1013        return other != null
1014            && timestamp == other.timestamp
1015            && version == other.version
1016            && changesetId == other.changesetId
1017            && isDeleted() == other.isDeleted()
1018            && isModified() == other.isModified()
1019            && isVisible() == other.isVisible()
1020            && Objects.equals(user, other.user);
1021        // CHECKSTYLE.ON: BooleanExpressionComplexity
1022    }
1023
1024    /**
1025     * Loads (clone) this primitive from provided PrimitiveData
1026     * @param data The object which should be cloned
1027     */
1028    public void load(PrimitiveData data) {
1029        checkDatasetNotReadOnly();
1030        // Write lock is provided by subclasses
1031        setKeys(data.hasKeys() ? data.getKeys() : null);
1032        setRawTimestamp(data.getRawTimestamp());
1033        user = data.getUser();
1034        setChangesetId(data.getChangesetId());
1035        setDeleted(data.isDeleted());
1036        setModified(data.isModified());
1037        setVisible(data.isVisible());
1038        setIncomplete(data.isIncomplete());
1039        version = data.getVersion();
1040    }
1041
1042    /**
1043     * Save parameters of this primitive to the transport object
1044     * @return The saved object data
1045     */
1046    public abstract PrimitiveData save();
1047
1048    /**
1049     * Save common parameters of primitives to the transport object
1050     * @param data The object to save the data into
1051     */
1052    protected void saveCommonAttributes(PrimitiveData data) {
1053        data.setId(id);
1054        data.setKeys(hasKeys() ? getKeys() : null);
1055        data.setRawTimestamp(getRawTimestamp());
1056        data.setUser(user);
1057        data.setDeleted(isDeleted());
1058        data.setModified(isModified());
1059        data.setVisible(isVisible());
1060        data.setIncomplete(isIncomplete());
1061        data.setChangesetId(changesetId);
1062        data.setVersion(version);
1063    }
1064
1065    /**
1066     * Called by Dataset to update cached position information of primitive (bbox, cached EarthNorth, ...)
1067     */
1068    public abstract void updatePosition();
1069
1070    /*----------------
1071     * OBJECT METHODS
1072     *---------------*/
1073
1074    @Override
1075    protected String getFlagsAsString() {
1076        StringBuilder builder = new StringBuilder(super.getFlagsAsString());
1077
1078        if (isDisabled()) {
1079            if (isDisabledAndHidden()) {
1080                builder.append('h');
1081            } else {
1082                builder.append('d');
1083            }
1084        }
1085        if (isTagged()) {
1086            builder.append('T');
1087        }
1088        if (hasDirectionKeys()) {
1089            if (reversedDirection()) {
1090                builder.append('<');
1091            } else {
1092                builder.append('>');
1093            }
1094        }
1095        return builder.toString();
1096    }
1097
1098    /**
1099     * Equal, if the id (and class) is equal.
1100     *
1101     * An primitive is equal to its incomplete counter part.
1102     */
1103    @Override
1104    public boolean equals(Object obj) {
1105        if (this == obj) {
1106            return true;
1107        } else if (obj == null || getClass() != obj.getClass()) {
1108            return false;
1109        } else {
1110            OsmPrimitive that = (OsmPrimitive) obj;
1111            return id == that.id;
1112        }
1113    }
1114
1115    /**
1116     * Return the id plus the class type encoded as hashcode or super's hashcode if id is 0.
1117     *
1118     * An primitive has the same hashcode as its incomplete counterpart.
1119     */
1120    @Override
1121    public int hashCode() {
1122        return Long.hashCode(id);
1123    }
1124
1125    @Override
1126    public Collection<String> getTemplateKeys() {
1127        Collection<String> keySet = keySet();
1128        List<String> result = new ArrayList<>(keySet.size() + 2);
1129        result.add(SPECIAL_VALUE_ID);
1130        result.add(SPECIAL_VALUE_LOCAL_NAME);
1131        result.addAll(keySet);
1132        return result;
1133    }
1134
1135    @Override
1136    public Object getTemplateValue(String name, boolean special) {
1137        if (special) {
1138            String lc = name.toLowerCase(Locale.ENGLISH);
1139            if (SPECIAL_VALUE_ID.equals(lc))
1140                return getId();
1141            else if (SPECIAL_VALUE_LOCAL_NAME.equals(lc))
1142                return getLocalName();
1143            else
1144                return null;
1145
1146        } else
1147            return getIgnoreCase(name);
1148    }
1149
1150    @Override
1151    public boolean evaluateCondition(Match condition) {
1152        return condition.match(this);
1153    }
1154
1155    /**
1156     * Replies the set of referring relations
1157     * @param primitives primitives to fetch relations from
1158     *
1159     * @return the set of referring relations
1160     */
1161    public static Set<Relation> getParentRelations(Collection<? extends OsmPrimitive> primitives) {
1162        return primitives.stream()
1163                .flatMap(p -> p.referrers(Relation.class))
1164                .collect(Collectors.toSet());
1165    }
1166
1167    /**
1168     * Determines if this primitive has tags denoting an area.
1169     * @return {@code true} if this primitive has tags denoting an area, {@code false} otherwise.
1170     * @since 6491
1171     */
1172    public final boolean hasAreaTags() {
1173        return hasKey("landuse", "amenity", "building", "building:part")
1174                || hasTag("area", OsmUtils.TRUE_VALUE)
1175                || hasTag("waterway", "riverbank")
1176                || hasTagDifferent("leisure", "picnic_table", "slipway", "firepit")
1177                || hasTag("natural", "water", "wood", "scrub", "wetland", "grassland", "heath", "rock", "bare_rock",
1178                                     "sand", "beach", "scree", "bay", "glacier", "shingle", "fell", "reef", "stone",
1179                                     "mud", "landslide", "sinkhole", "crevasse", "desert");
1180    }
1181
1182    /**
1183     * Determines if this primitive semantically concerns an area.
1184     * @return {@code true} if this primitive semantically concerns an area, according to its type, geometry and tags, {@code false} otherwise.
1185     * @since 6491
1186     */
1187    public abstract boolean concernsArea();
1188
1189    /**
1190     * Tests if this primitive lies outside of the downloaded area of its {@link DataSet}.
1191     * @return {@code true} if this primitive lies outside of the downloaded area
1192     */
1193    public abstract boolean isOutsideDownloadArea();
1194
1195    /**
1196     * If necessary, extend the bbox to contain this primitive
1197     * @param box a bbox instance
1198     * @param visited a set of visited members  or null
1199     * @since 11269
1200     */
1201    protected abstract void addToBBox(BBox box, Set<PrimitiveId> visited);
1202}