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