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.awt.geom.Area;
007import java.util.ArrayList;
008import java.util.Collection;
009import java.util.Collections;
010import java.util.HashMap;
011import java.util.HashSet;
012import java.util.Iterator;
013import java.util.LinkedList;
014import java.util.List;
015import java.util.Map;
016import java.util.Objects;
017import java.util.Set;
018import java.util.concurrent.CopyOnWriteArrayList;
019import java.util.concurrent.atomic.AtomicBoolean;
020import java.util.concurrent.locks.Lock;
021import java.util.concurrent.locks.ReadWriteLock;
022import java.util.concurrent.locks.ReentrantReadWriteLock;
023import java.util.function.Function;
024import java.util.function.Predicate;
025import java.util.stream.Stream;
026
027import org.openstreetmap.josm.Main;
028import org.openstreetmap.josm.data.APIDataSet.APIOperation;
029import org.openstreetmap.josm.data.Bounds;
030import org.openstreetmap.josm.data.Data;
031import org.openstreetmap.josm.data.DataSource;
032import org.openstreetmap.josm.data.ProjectionBounds;
033import org.openstreetmap.josm.data.SelectionChangedListener;
034import org.openstreetmap.josm.data.conflict.ConflictCollection;
035import org.openstreetmap.josm.data.coor.EastNorth;
036import org.openstreetmap.josm.data.coor.LatLon;
037import org.openstreetmap.josm.data.osm.DataSelectionListener.SelectionAddEvent;
038import org.openstreetmap.josm.data.osm.DataSelectionListener.SelectionChangeEvent;
039import org.openstreetmap.josm.data.osm.DataSelectionListener.SelectionRemoveEvent;
040import org.openstreetmap.josm.data.osm.DataSelectionListener.SelectionReplaceEvent;
041import org.openstreetmap.josm.data.osm.DataSelectionListener.SelectionToggleEvent;
042import org.openstreetmap.josm.data.osm.event.AbstractDatasetChangedEvent;
043import org.openstreetmap.josm.data.osm.event.ChangesetIdChangedEvent;
044import org.openstreetmap.josm.data.osm.event.DataChangedEvent;
045import org.openstreetmap.josm.data.osm.event.DataSetListener;
046import org.openstreetmap.josm.data.osm.event.NodeMovedEvent;
047import org.openstreetmap.josm.data.osm.event.PrimitiveFlagsChangedEvent;
048import org.openstreetmap.josm.data.osm.event.PrimitivesAddedEvent;
049import org.openstreetmap.josm.data.osm.event.PrimitivesRemovedEvent;
050import org.openstreetmap.josm.data.osm.event.RelationMembersChangedEvent;
051import org.openstreetmap.josm.data.osm.event.SelectionEventManager;
052import org.openstreetmap.josm.data.osm.event.TagsChangedEvent;
053import org.openstreetmap.josm.data.osm.event.WayNodesChangedEvent;
054import org.openstreetmap.josm.data.osm.visitor.BoundingXYVisitor;
055import org.openstreetmap.josm.data.projection.Projection;
056import org.openstreetmap.josm.data.projection.ProjectionChangeListener;
057import org.openstreetmap.josm.gui.progress.ProgressMonitor;
058import org.openstreetmap.josm.tools.ListenerList;
059import org.openstreetmap.josm.tools.Logging;
060import org.openstreetmap.josm.tools.SubclassFilteredCollection;
061
062/**
063 * DataSet is the data behind the application. It can consists of only a few points up to the whole
064 * osm database. DataSet's can be merged together, saved, (up/down/disk)loaded etc.
065 *
066 * Note that DataSet is not an osm-primitive and so has no key association but a few members to
067 * store some information.
068 *
069 * Dataset is threadsafe - accessing Dataset simultaneously from different threads should never
070 * lead to data corruption or ConcurrentModificationException. However when for example one thread
071 * removes primitive and other thread try to add another primitive referring to the removed primitive,
072 * DataIntegrityException will occur.
073 *
074 * To prevent such situations, read/write lock is provided. While read lock is used, it's guaranteed that
075 * Dataset will not change. Sample usage:
076 * <code>
077 *   ds.getReadLock().lock();
078 *   try {
079 *     // .. do something with dataset
080 *   } finally {
081 *     ds.getReadLock().unlock();
082 *   }
083 * </code>
084 *
085 * Write lock should be used in case of bulk operations. In addition to ensuring that other threads can't
086 * use dataset in the middle of modifications it also stops sending of dataset events. That's good for performance
087 * reasons - GUI can be updated after all changes are done.
088 * Sample usage:
089 * <code>
090 * ds.beginUpdate()
091 * try {
092 *   // .. do modifications
093 * } finally {
094 *  ds.endUpdate();
095 * }
096 * </code>
097 *
098 * Note that it is not necessary to call beginUpdate/endUpdate for every dataset modification - dataset will get locked
099 * automatically.
100 *
101 * Note that locks cannot be upgraded - if one threads use read lock and and then write lock, dead lock will occur - see #5814 for
102 * sample ticket
103 *
104 * @author imi
105 */
106public final class DataSet extends QuadBucketPrimitiveStore implements Data, ProjectionChangeListener, Lockable {
107
108    /**
109     * Maximum number of events that can be fired between beginUpdate/endUpdate to be send as single events (ie without DatasetChangedEvent)
110     */
111    private static final int MAX_SINGLE_EVENTS = 30;
112
113    /**
114     * Maximum number of events to kept between beginUpdate/endUpdate. When more events are created, that simple DatasetChangedEvent is sent)
115     */
116    private static final int MAX_EVENTS = 1000;
117
118    private final Storage<OsmPrimitive> allPrimitives = new Storage<>(new Storage.PrimitiveIdHash(), true);
119    private final Map<PrimitiveId, OsmPrimitive> primitivesMap = allPrimitives
120            .foreignKey(new Storage.PrimitiveIdHash());
121    private final CopyOnWriteArrayList<DataSetListener> listeners = new CopyOnWriteArrayList<>();
122
123    // provide means to highlight map elements that are not osm primitives
124    private Collection<WaySegment> highlightedVirtualNodes = new LinkedList<>();
125    private Collection<WaySegment> highlightedWaySegments = new LinkedList<>();
126    private final ListenerList<HighlightUpdateListener> highlightUpdateListeners = ListenerList.create();
127
128    // Number of open calls to beginUpdate
129    private int updateCount;
130    // Events that occurred while dataset was locked but should be fired after write lock is released
131    private final List<AbstractDatasetChangedEvent> cachedEvents = new ArrayList<>();
132
133    private String name;
134    private DownloadPolicy downloadPolicy;
135    private UploadPolicy uploadPolicy;
136    /** Flag used to know if the dataset should not be editable */
137    private final AtomicBoolean isReadOnly = new AtomicBoolean(false);
138
139    private final ReadWriteLock lock = new ReentrantReadWriteLock();
140
141    /**
142     * The mutex lock that is used to synchronize selection changes.
143     */
144    private final Object selectionLock = new Object();
145    /**
146     * The current selected primitives. This is always a unmodifiable set.
147     *
148     * The set should be ordered in the order in which the primitives have been added to the selection.
149     */
150    private Set<OsmPrimitive> currentSelectedPrimitives = Collections.emptySet();
151
152    /**
153     * A list of listeners that listen to selection changes on this layer.
154     */
155    private final ListenerList<DataSelectionListener> selectionListeners = ListenerList.create();
156
157    private Area cachedDataSourceArea;
158    private List<Bounds> cachedDataSourceBounds;
159
160    /**
161     * All data sources of this DataSet.
162     */
163    private final Collection<DataSource> dataSources = new LinkedList<>();
164
165    private final ConflictCollection conflicts = new ConflictCollection();
166
167    private short mappaintCacheIdx = 1;
168
169    /**
170     * Constructs a new {@code DataSet}.
171     */
172    public DataSet() {
173        // Transparently register as projection change listener. No need to explicitly remove
174        // the listener, projection change listeners are managed as WeakReferences.
175        Main.addProjectionChangeListener(this);
176        addSelectionListener((DataSelectionListener) e -> fireSelectionChange(e.getSelection()));
177    }
178
179    /**
180     * Creates a new {@link DataSet}.
181     * @param copyFrom An other {@link DataSet} to copy the contents of this dataset from.
182     * @since 10346
183     */
184    public DataSet(DataSet copyFrom) {
185        this();
186        copyFrom.getReadLock().lock();
187        try {
188            Map<OsmPrimitive, OsmPrimitive> primMap = new HashMap<>();
189            for (Node n : copyFrom.getNodes()) {
190                Node newNode = new Node(n);
191                primMap.put(n, newNode);
192                addPrimitive(newNode);
193            }
194            for (Way w : copyFrom.getWays()) {
195                Way newWay = new Way(w);
196                primMap.put(w, newWay);
197                List<Node> newNodes = new ArrayList<>();
198                for (Node n : w.getNodes()) {
199                    newNodes.add((Node) primMap.get(n));
200                }
201                newWay.setNodes(newNodes);
202                addPrimitive(newWay);
203            }
204            // Because relations can have other relations as members we first clone all relations
205            // and then get the cloned members
206            Collection<Relation> relations = copyFrom.getRelations();
207            for (Relation r : relations) {
208                Relation newRelation = new Relation(r);
209                newRelation.setMembers(null);
210                primMap.put(r, newRelation);
211                addPrimitive(newRelation);
212            }
213            for (Relation r : relations) {
214                Relation newRelation = (Relation) primMap.get(r);
215                List<RelationMember> newMembers = new ArrayList<>();
216                for (RelationMember rm : r.getMembers()) {
217                    newMembers.add(new RelationMember(rm.getRole(), primMap.get(rm.getMember())));
218                }
219                newRelation.setMembers(newMembers);
220            }
221            for (DataSource source : copyFrom.dataSources) {
222                dataSources.add(new DataSource(source));
223            }
224            version = copyFrom.version;
225            uploadPolicy = copyFrom.uploadPolicy;
226            downloadPolicy = copyFrom.downloadPolicy;
227            isReadOnly.set(copyFrom.isReadOnly.get());
228        } finally {
229            copyFrom.getReadLock().unlock();
230        }
231    }
232
233    /**
234     * Constructs a new {@code DataSet} initially filled with the given primitives.
235     * @param osmPrimitives primitives to add to this data set
236     * @since 12726
237     */
238    public DataSet(OsmPrimitive... osmPrimitives) {
239        this();
240        beginUpdate();
241        try {
242            for (OsmPrimitive o : osmPrimitives) {
243                addPrimitive(o);
244            }
245        } finally {
246            endUpdate();
247        }
248    }
249
250    /**
251     * Adds a new data source.
252     * @param source data source to add
253     * @return {@code true} if the collection changed as a result of the call
254     * @since 11626
255     */
256    public synchronized boolean addDataSource(DataSource source) {
257        return addDataSources(Collections.singleton(source));
258    }
259
260    /**
261     * Adds new data sources.
262     * @param sources data sources to add
263     * @return {@code true} if the collection changed as a result of the call
264     * @since 11626
265     */
266    public synchronized boolean addDataSources(Collection<DataSource> sources) {
267        boolean changed = dataSources.addAll(sources);
268        if (changed) {
269            cachedDataSourceArea = null;
270            cachedDataSourceBounds = null;
271        }
272        return changed;
273    }
274
275    /**
276     * Returns the lock used for reading.
277     * @return the lock used for reading
278     */
279    public Lock getReadLock() {
280        return lock.readLock();
281    }
282
283    /**
284     * History of selections - shared by plugins and SelectionListDialog
285     */
286    private final LinkedList<Collection<? extends OsmPrimitive>> selectionHistory = new LinkedList<>();
287
288    /**
289     * Replies the history of JOSM selections
290     *
291     * @return list of history entries
292     */
293    public LinkedList<Collection<? extends OsmPrimitive>> getSelectionHistory() {
294        return selectionHistory;
295    }
296
297    /**
298     * Clears selection history list
299     */
300    public void clearSelectionHistory() {
301        selectionHistory.clear();
302    }
303
304    /**
305     * The API version that created this data set, if any.
306     */
307    private String version;
308
309    /**
310     * Replies the API version this dataset was created from. May be null.
311     *
312     * @return the API version this dataset was created from. May be null.
313     */
314    public String getVersion() {
315        return version;
316    }
317
318    /**
319     * Sets the API version this dataset was created from.
320     *
321     * @param version the API version, i.e. "0.6"
322     * @throws IllegalStateException if the dataset is read-only
323     */
324    public void setVersion(String version) {
325        checkModifiable();
326        this.version = version;
327    }
328
329    /**
330     * Get the download policy.
331     * @return the download policy
332     * @see #setDownloadPolicy(DownloadPolicy)
333     * @since 13453
334     */
335    public DownloadPolicy getDownloadPolicy() {
336        return this.downloadPolicy;
337    }
338
339    /**
340     * Sets the download policy.
341     * @param downloadPolicy the download policy
342     * @see #getUploadPolicy()
343     * @since 13453
344     */
345    public void setDownloadPolicy(DownloadPolicy downloadPolicy) {
346        this.downloadPolicy = downloadPolicy;
347    }
348
349    /**
350     * Get the upload policy.
351     * @return the upload policy
352     * @see #setUploadPolicy(UploadPolicy)
353     */
354    public UploadPolicy getUploadPolicy() {
355        return this.uploadPolicy;
356    }
357
358    /**
359     * Sets the upload policy.
360     * @param uploadPolicy the upload policy
361     * @see #getUploadPolicy()
362     */
363    public void setUploadPolicy(UploadPolicy uploadPolicy) {
364        this.uploadPolicy = uploadPolicy;
365    }
366
367    /**
368     * Holding bin for changeset tag information, to be applied when or if this is ever uploaded.
369     */
370    private final Map<String, String> changeSetTags = new HashMap<>();
371
372    /**
373     * Replies the set of changeset tags to be applied when or if this is ever uploaded.
374     * @return the set of changeset tags
375     * @see #addChangeSetTag
376     */
377    public Map<String, String> getChangeSetTags() {
378        return changeSetTags;
379    }
380
381    /**
382     * Adds a new changeset tag.
383     * @param k Key
384     * @param v Value
385     * @see #getChangeSetTags
386     */
387    public void addChangeSetTag(String k, String v) {
388        this.changeSetTags.put(k, v);
389    }
390
391    /**
392     * Gets a filtered collection of primitives matching the given predicate.
393     * @param <T> The primitive type.
394     * @param predicate The predicate to match
395     * @return The list of primtives.
396     * @since 10590
397     */
398    public <T extends OsmPrimitive> Collection<T> getPrimitives(Predicate<? super OsmPrimitive> predicate) {
399        return new SubclassFilteredCollection<>(allPrimitives, predicate);
400    }
401
402    /**
403     * Replies an unmodifiable collection of nodes in this dataset
404     *
405     * @return an unmodifiable collection of nodes in this dataset
406     */
407    public Collection<Node> getNodes() {
408        return getPrimitives(Node.class::isInstance);
409    }
410
411    @Override
412    public List<Node> searchNodes(BBox bbox) {
413        lock.readLock().lock();
414        try {
415            return super.searchNodes(bbox);
416        } finally {
417            lock.readLock().unlock();
418        }
419    }
420
421    /**
422     * Replies an unmodifiable collection of ways in this dataset
423     *
424     * @return an unmodifiable collection of ways in this dataset
425     */
426    public Collection<Way> getWays() {
427        return getPrimitives(Way.class::isInstance);
428    }
429
430    @Override
431    public List<Way> searchWays(BBox bbox) {
432        lock.readLock().lock();
433        try {
434            return super.searchWays(bbox);
435        } finally {
436            lock.readLock().unlock();
437        }
438    }
439
440    /**
441     * Searches for relations in the given bounding box.
442     * @param bbox the bounding box
443     * @return List of relations in the given bbox. Can be empty but not null
444     */
445    @Override
446    public List<Relation> searchRelations(BBox bbox) {
447        lock.readLock().lock();
448        try {
449            return super.searchRelations(bbox);
450        } finally {
451            lock.readLock().unlock();
452        }
453    }
454
455    /**
456     * Replies an unmodifiable collection of relations in this dataset
457     *
458     * @return an unmodifiable collection of relations in this dataset
459     */
460    public Collection<Relation> getRelations() {
461        return getPrimitives(Relation.class::isInstance);
462    }
463
464    /**
465     * Returns a collection containing all primitives of the dataset.
466     * @return A collection containing all primitives of the dataset. Data is not ordered
467     */
468    public Collection<OsmPrimitive> allPrimitives() {
469        return getPrimitives(o -> true);
470    }
471
472    /**
473     * Returns a collection containing all not-deleted primitives.
474     * @return A collection containing all not-deleted primitives.
475     * @see OsmPrimitive#isDeleted
476     */
477    public Collection<OsmPrimitive> allNonDeletedPrimitives() {
478        return getPrimitives(p -> !p.isDeleted());
479    }
480
481    /**
482     * Returns a collection containing all not-deleted complete primitives.
483     * @return A collection containing all not-deleted complete primitives.
484     * @see OsmPrimitive#isDeleted
485     * @see OsmPrimitive#isIncomplete
486     */
487    public Collection<OsmPrimitive> allNonDeletedCompletePrimitives() {
488        return getPrimitives(primitive -> !primitive.isDeleted() && !primitive.isIncomplete());
489    }
490
491    /**
492     * Returns a collection containing all not-deleted complete physical primitives.
493     * @return A collection containing all not-deleted complete physical primitives (nodes and ways).
494     * @see OsmPrimitive#isDeleted
495     * @see OsmPrimitive#isIncomplete
496     */
497    public Collection<OsmPrimitive> allNonDeletedPhysicalPrimitives() {
498        return getPrimitives(
499                primitive -> !primitive.isDeleted() && !primitive.isIncomplete() && !(primitive instanceof Relation));
500    }
501
502    /**
503     * Returns a collection containing all modified primitives.
504     * @return A collection containing all modified primitives.
505     * @see OsmPrimitive#isModified
506     */
507    public Collection<OsmPrimitive> allModifiedPrimitives() {
508        return getPrimitives(OsmPrimitive::isModified);
509    }
510
511    /**
512     * Returns a collection containing all primitives preserved from filtering.
513     * @return A collection containing all primitives preserved from filtering.
514     * @see OsmPrimitive#isPreserved
515     * @since 13309
516     */
517    public Collection<OsmPrimitive> allPreservedPrimitives() {
518        return getPrimitives(OsmPrimitive::isPreserved);
519    }
520
521    /**
522     * Adds a primitive to the dataset.
523     *
524     * @param primitive the primitive.
525     * @throws IllegalStateException if the dataset is read-only
526     */
527    @Override
528    public void addPrimitive(OsmPrimitive primitive) {
529        Objects.requireNonNull(primitive, "primitive");
530        checkModifiable();
531        beginUpdate();
532        try {
533            if (getPrimitiveById(primitive) != null)
534                throw new DataIntegrityProblemException(
535                        tr("Unable to add primitive {0} to the dataset because it is already included",
536                                primitive.toString()));
537
538            allPrimitives.add(primitive);
539            primitive.setDataset(this);
540            primitive.updatePosition(); // Set cached bbox for way and relation (required for reindexWay and reindexRelation to work properly)
541            super.addPrimitive(primitive);
542            firePrimitivesAdded(Collections.singletonList(primitive), false);
543        } finally {
544            endUpdate();
545        }
546    }
547
548    /**
549     * Removes a primitive from the dataset. This method only removes the
550     * primitive form the respective collection of primitives managed
551     * by this dataset, i.e. from {@link #nodes}, {@link #ways}, or
552     * {@link #relations}. References from other primitives to this
553     * primitive are left unchanged.
554     *
555     * @param primitiveId the id of the primitive
556     * @throws IllegalStateException if the dataset is read-only
557     */
558    public void removePrimitive(PrimitiveId primitiveId) {
559        checkModifiable();
560        beginUpdate();
561        try {
562            OsmPrimitive primitive = getPrimitiveByIdChecked(primitiveId);
563            if (primitive == null)
564                return;
565            removePrimitiveImpl(primitive);
566            firePrimitivesRemoved(Collections.singletonList(primitive), false);
567        } finally {
568            endUpdate();
569        }
570    }
571
572    private void removePrimitiveImpl(OsmPrimitive primitive) {
573        clearSelection(primitive.getPrimitiveId());
574        if (primitive.isSelected()) {
575            throw new DataIntegrityProblemException("Primitive was re-selected by a selection listener: " + primitive);
576        }
577        super.removePrimitive(primitive);
578        allPrimitives.remove(primitive);
579        primitive.setDataset(null);
580    }
581
582    @Override
583    protected void removePrimitive(OsmPrimitive primitive) {
584        checkModifiable();
585        beginUpdate();
586        try {
587            removePrimitiveImpl(primitive);
588            firePrimitivesRemoved(Collections.singletonList(primitive), false);
589        } finally {
590            endUpdate();
591        }
592    }
593
594    /*---------------------------------------------------
595     *   SELECTION HANDLING
596     *---------------------------------------------------*/
597
598    /**
599     * Add a listener that listens to selection changes in this specific data set.
600     * @param listener The listener.
601     * @see #removeSelectionListener(DataSelectionListener)
602     * @see SelectionEventManager#addSelectionListener(SelectionChangedListener,
603     *      org.openstreetmap.josm.data.osm.event.DatasetEventManager.FireMode)
604     *      To add a global listener.
605     */
606    public void addSelectionListener(DataSelectionListener listener) {
607        selectionListeners.addListener(listener);
608    }
609
610    /**
611     * Remove a listener that listens to selection changes in this specific data set.
612     * @param listener The listener.
613     * @see #addSelectionListener(DataSelectionListener)
614     */
615    public void removeSelectionListener(DataSelectionListener listener) {
616        selectionListeners.removeListener(listener);
617    }
618
619    /*---------------------------------------------------
620     *   OLD SELECTION HANDLING
621     *---------------------------------------------------*/
622
623    /**
624     * A list of listeners to selection changed events. The list is static, as listeners register
625     * themselves for any dataset selection changes that occur, regardless of the current active
626     * dataset. (However, the selection does only change in the active layer)
627     */
628    private static final Collection<SelectionChangedListener> selListeners = new CopyOnWriteArrayList<>();
629
630    /**
631     * Adds a new selection listener.
632     * @param listener The selection listener to add
633     * @see #addSelectionListener(DataSelectionListener)
634     * @see SelectionEventManager#removeSelectionListener(SelectionChangedListener)
635     */
636    public static void addSelectionListener(SelectionChangedListener listener) {
637        ((CopyOnWriteArrayList<SelectionChangedListener>) selListeners).addIfAbsent(listener);
638    }
639
640    /**
641     * Removes a selection listener.
642     * @param listener The selection listener to remove
643     * @see #removeSelectionListener(DataSelectionListener)
644     * @see SelectionEventManager#removeSelectionListener(SelectionChangedListener)
645     */
646    public static void removeSelectionListener(SelectionChangedListener listener) {
647        selListeners.remove(listener);
648    }
649
650    private static void fireSelectionChange(Collection<? extends OsmPrimitive> currentSelection) {
651        for (SelectionChangedListener l : selListeners) {
652            l.selectionChanged(currentSelection);
653        }
654    }
655
656    /**
657     * Returns selected nodes and ways.
658     * @return selected nodes and ways
659     */
660    public Collection<OsmPrimitive> getSelectedNodesAndWays() {
661        return new SubclassFilteredCollection<>(getSelected(),
662                primitive -> primitive instanceof Node || primitive instanceof Way);
663    }
664
665    /**
666     * Returns an unmodifiable collection of *WaySegments* whose virtual
667     * nodes should be highlighted. WaySegments are used to avoid having
668     * to create a VirtualNode class that wouldn't have much purpose otherwise.
669     *
670     * @return unmodifiable collection of WaySegments
671     */
672    public Collection<WaySegment> getHighlightedVirtualNodes() {
673        return Collections.unmodifiableCollection(highlightedVirtualNodes);
674    }
675
676    /**
677     * Returns an unmodifiable collection of WaySegments that should be highlighted.
678     *
679     * @return unmodifiable collection of WaySegments
680     */
681    public Collection<WaySegment> getHighlightedWaySegments() {
682        return Collections.unmodifiableCollection(highlightedWaySegments);
683    }
684
685    /**
686     * Adds a listener that gets notified whenever way segment / virtual nodes highlights change.
687     * @param listener The Listener
688     * @since 12014
689     */
690    public void addHighlightUpdateListener(HighlightUpdateListener listener) {
691        highlightUpdateListeners.addListener(listener);
692    }
693
694    /**
695     * Removes a listener that was added with {@link #addHighlightUpdateListener(HighlightUpdateListener)}
696     * @param listener The Listener
697     * @since 12014
698     */
699    public void removeHighlightUpdateListener(HighlightUpdateListener listener) {
700        highlightUpdateListeners.removeListener(listener);
701    }
702
703    /**
704     * Replies an unmodifiable collection of primitives currently selected
705     * in this dataset, except deleted ones. May be empty, but not null.
706     *
707     * When iterating through the set it is ordered by the order in which the primitives were added to the selection.
708     *
709     * @return unmodifiable collection of primitives
710     */
711    public Collection<OsmPrimitive> getSelected() {
712        return new SubclassFilteredCollection<>(getAllSelected(), p -> !p.isDeleted());
713    }
714
715    /**
716     * Replies an unmodifiable collection of primitives currently selected
717     * in this dataset, including deleted ones. May be empty, but not null.
718     *
719     * When iterating through the set it is ordered by the order in which the primitives were added to the selection.
720     *
721     * @return unmodifiable collection of primitives
722     */
723    public Collection<OsmPrimitive> getAllSelected() {
724        return currentSelectedPrimitives;
725    }
726
727    /**
728     * Returns selected nodes.
729     * @return selected nodes
730     */
731    public Collection<Node> getSelectedNodes() {
732        return new SubclassFilteredCollection<>(getSelected(), Node.class::isInstance);
733    }
734
735    /**
736     * Returns selected ways.
737     * @return selected ways
738     */
739    public Collection<Way> getSelectedWays() {
740        return new SubclassFilteredCollection<>(getSelected(), Way.class::isInstance);
741    }
742
743    /**
744     * Returns selected relations.
745     * @return selected relations
746     */
747    public Collection<Relation> getSelectedRelations() {
748        return new SubclassFilteredCollection<>(getSelected(), Relation.class::isInstance);
749    }
750
751    /**
752     * Determines whether the selection is empty or not
753     * @return whether the selection is empty or not
754     */
755    public boolean selectionEmpty() {
756        return currentSelectedPrimitives.isEmpty();
757    }
758
759    /**
760     * Determines whether the given primitive is selected or not
761     * @param osm the primitive
762     * @return whether {@code osm} is selected or not
763     */
764    public boolean isSelected(OsmPrimitive osm) {
765        return currentSelectedPrimitives.contains(osm);
766    }
767
768    /**
769     * set what virtual nodes should be highlighted. Requires a Collection of
770     * *WaySegments* to avoid a VirtualNode class that wouldn't have much use
771     * otherwise.
772     * @param waySegments Collection of way segments
773     */
774    public void setHighlightedVirtualNodes(Collection<WaySegment> waySegments) {
775        if (highlightedVirtualNodes.isEmpty() && waySegments.isEmpty())
776            return;
777
778        highlightedVirtualNodes = waySegments;
779        fireHighlightingChanged();
780    }
781
782    /**
783     * set what virtual ways should be highlighted.
784     * @param waySegments Collection of way segments
785     */
786    public void setHighlightedWaySegments(Collection<WaySegment> waySegments) {
787        if (highlightedWaySegments.isEmpty() && waySegments.isEmpty())
788            return;
789
790        highlightedWaySegments = waySegments;
791        fireHighlightingChanged();
792    }
793
794    /**
795     * Sets the current selection to the primitives in <code>selection</code>
796     * and notifies all {@link SelectionChangedListener}.
797     *
798     * @param selection the selection
799     */
800    public void setSelected(Collection<? extends PrimitiveId> selection) {
801        setSelected(selection.stream());
802    }
803
804    /**
805     * Sets the current selection to the primitives in <code>osm</code>
806     * and notifies all {@link SelectionChangedListener}.
807     *
808     * @param osm the primitives to set. <code>null</code> values are ignored for now, but this may be removed in the future.
809     */
810    public void setSelected(PrimitiveId... osm) {
811        setSelected(Stream.of(osm).filter(Objects::nonNull));
812    }
813
814    private void setSelected(Stream<? extends PrimitiveId> stream) {
815        doSelectionChange(old -> new SelectionReplaceEvent(this, old,
816                stream.map(this::getPrimitiveByIdChecked).filter(Objects::nonNull)));
817    }
818
819    /**
820     * Adds the primitives in <code>selection</code> to the current selection
821     * and notifies all {@link SelectionChangedListener}.
822     *
823     * @param selection the selection
824     */
825    public void addSelected(Collection<? extends PrimitiveId> selection) {
826        addSelected(selection.stream());
827    }
828
829    /**
830     * Adds the primitives in <code>osm</code> to the current selection
831     * and notifies all {@link SelectionChangedListener}.
832     *
833     * @param osm the primitives to add
834     */
835    public void addSelected(PrimitiveId... osm) {
836        addSelected(Stream.of(osm));
837    }
838
839    private void addSelected(Stream<? extends PrimitiveId> stream) {
840        doSelectionChange(old -> new SelectionAddEvent(this, old,
841                stream.map(this::getPrimitiveByIdChecked).filter(Objects::nonNull)));
842    }
843
844    /**
845     * Removes the selection from every value in the collection.
846     * @param osm The collection of ids to remove the selection from.
847     */
848    public void clearSelection(PrimitiveId... osm) {
849        clearSelection(Stream.of(osm));
850    }
851
852    /**
853     * Removes the selection from every value in the collection.
854     * @param list The collection of ids to remove the selection from.
855     */
856    public void clearSelection(Collection<? extends PrimitiveId> list) {
857        clearSelection(list.stream());
858    }
859
860    /**
861     * Clears the current selection.
862     */
863    public void clearSelection() {
864        setSelected(Stream.empty());
865    }
866
867    private void clearSelection(Stream<? extends PrimitiveId> stream) {
868        doSelectionChange(old -> new SelectionRemoveEvent(this, old,
869                stream.map(this::getPrimitiveByIdChecked).filter(Objects::nonNull)));
870    }
871
872    /**
873     * Toggles the selected state of the given collection of primitives.
874     * @param osm The primitives to toggle
875     */
876    public void toggleSelected(Collection<? extends PrimitiveId> osm) {
877        toggleSelected(osm.stream());
878    }
879
880    /**
881     * Toggles the selected state of the given collection of primitives.
882     * @param osm The primitives to toggle
883     */
884    public void toggleSelected(PrimitiveId... osm) {
885        toggleSelected(Stream.of(osm));
886    }
887
888    private void toggleSelected(Stream<? extends PrimitiveId> stream) {
889        doSelectionChange(old -> new SelectionToggleEvent(this, old,
890                stream.map(this::getPrimitiveByIdChecked).filter(Objects::nonNull)));
891    }
892
893    /**
894     * Do a selection change.
895     * <p>
896     * This is the only method that changes the current selection state.
897     * @param command A generator that generates the {@link SelectionChangeEvent} for the given base set of currently selected primitives.
898     * @return true iff the command did change the selection.
899     * @since 12048
900     */
901    private boolean doSelectionChange(Function<Set<OsmPrimitive>, SelectionChangeEvent> command) {
902        synchronized (selectionLock) {
903            SelectionChangeEvent event = command.apply(currentSelectedPrimitives);
904            if (event.isNop()) {
905                return false;
906            }
907            currentSelectedPrimitives = event.getSelection();
908            selectionListeners.fireEvent(l -> l.selectionChanged(event));
909            return true;
910        }
911    }
912
913    /**
914     * clear all highlights of virtual nodes
915     */
916    public void clearHighlightedVirtualNodes() {
917        setHighlightedVirtualNodes(new ArrayList<WaySegment>());
918    }
919
920    /**
921     * clear all highlights of way segments
922     */
923    public void clearHighlightedWaySegments() {
924        setHighlightedWaySegments(new ArrayList<WaySegment>());
925    }
926
927    @Override
928    public synchronized Area getDataSourceArea() {
929        if (cachedDataSourceArea == null) {
930            cachedDataSourceArea = Data.super.getDataSourceArea();
931        }
932        return cachedDataSourceArea;
933    }
934
935    @Override
936    public synchronized List<Bounds> getDataSourceBounds() {
937        if (cachedDataSourceBounds == null) {
938            cachedDataSourceBounds = Data.super.getDataSourceBounds();
939        }
940        return Collections.unmodifiableList(cachedDataSourceBounds);
941    }
942
943    @Override
944    public synchronized Collection<DataSource> getDataSources() {
945        return Collections.unmodifiableCollection(dataSources);
946    }
947
948    /**
949     * Returns a primitive with a given id from the data set. null, if no such primitive exists
950     *
951     * @param id  uniqueId of the primitive. Might be &lt; 0 for newly created primitives
952     * @param type the type of  the primitive. Must not be null.
953     * @return the primitive
954     * @throws NullPointerException if type is null
955     */
956    public OsmPrimitive getPrimitiveById(long id, OsmPrimitiveType type) {
957        return getPrimitiveById(new SimplePrimitiveId(id, type));
958    }
959
960    /**
961     * Returns a primitive with a given id from the data set. null, if no such primitive exists
962     *
963     * @param primitiveId type and uniqueId of the primitive. Might be &lt; 0 for newly created primitives
964     * @return the primitive
965     */
966    public OsmPrimitive getPrimitiveById(PrimitiveId primitiveId) {
967        return primitiveId != null ? primitivesMap.get(primitiveId) : null;
968    }
969
970    /**
971     * Show message and stack trace in log in case primitive is not found
972     * @param primitiveId primitive id to look for
973     * @return Primitive by id.
974     */
975    private OsmPrimitive getPrimitiveByIdChecked(PrimitiveId primitiveId) {
976        OsmPrimitive result = getPrimitiveById(primitiveId);
977        if (result == null && primitiveId != null) {
978            Logging.warn(tr(
979                    "JOSM expected to find primitive [{0} {1}] in dataset but it is not there. Please report this "
980                            + "at {2}. This is not a critical error, it should be safe to continue in your work.",
981                    primitiveId.getType(), Long.toString(primitiveId.getUniqueId()), Main.getJOSMWebsite()));
982            Logging.error(new Exception());
983        }
984
985        return result;
986    }
987
988    private static void deleteWay(Way way) {
989        way.setNodes(null);
990        way.setDeleted(true);
991    }
992
993    /**
994     * Removes all references from ways in this dataset to a particular node.
995     *
996     * @param node the node
997     * @return The set of ways that have been modified
998     * @throws IllegalStateException if the dataset is read-only
999     */
1000    public Set<Way> unlinkNodeFromWays(Node node) {
1001        checkModifiable();
1002        Set<Way> result = new HashSet<>();
1003        beginUpdate();
1004        try {
1005            for (Way way : node.getParentWays()) {
1006                List<Node> wayNodes = way.getNodes();
1007                if (wayNodes.remove(node)) {
1008                    if (wayNodes.size() < 2) {
1009                        deleteWay(way);
1010                    } else {
1011                        way.setNodes(wayNodes);
1012                    }
1013                    result.add(way);
1014                }
1015            }
1016        } finally {
1017            endUpdate();
1018        }
1019        return result;
1020    }
1021
1022    /**
1023     * removes all references from relations in this dataset  to this primitive
1024     *
1025     * @param primitive the primitive
1026     * @return The set of relations that have been modified
1027     * @throws IllegalStateException if the dataset is read-only
1028     */
1029    public Set<Relation> unlinkPrimitiveFromRelations(OsmPrimitive primitive) {
1030        checkModifiable();
1031        Set<Relation> result = new HashSet<>();
1032        beginUpdate();
1033        try {
1034            for (Relation relation : getRelations()) {
1035                List<RelationMember> members = relation.getMembers();
1036
1037                Iterator<RelationMember> it = members.iterator();
1038                boolean removed = false;
1039                while (it.hasNext()) {
1040                    RelationMember member = it.next();
1041                    if (member.getMember().equals(primitive)) {
1042                        it.remove();
1043                        removed = true;
1044                    }
1045                }
1046
1047                if (removed) {
1048                    relation.setMembers(members);
1049                    result.add(relation);
1050                }
1051            }
1052        } finally {
1053            endUpdate();
1054        }
1055        return result;
1056    }
1057
1058    /**
1059     * Removes all references from other primitives to the referenced primitive.
1060     *
1061     * @param referencedPrimitive the referenced primitive
1062     * @return The set of primitives that have been modified
1063     * @throws IllegalStateException if the dataset is read-only
1064     */
1065    public Set<OsmPrimitive> unlinkReferencesToPrimitive(OsmPrimitive referencedPrimitive) {
1066        checkModifiable();
1067        Set<OsmPrimitive> result = new HashSet<>();
1068        beginUpdate();
1069        try {
1070            if (referencedPrimitive instanceof Node) {
1071                result.addAll(unlinkNodeFromWays((Node) referencedPrimitive));
1072            }
1073            result.addAll(unlinkPrimitiveFromRelations(referencedPrimitive));
1074        } finally {
1075            endUpdate();
1076        }
1077        return result;
1078    }
1079
1080    /**
1081     * Replies true if there is at least one primitive in this dataset with
1082     * {@link OsmPrimitive#isModified()} == <code>true</code>.
1083     *
1084     * @return true if there is at least one primitive in this dataset with
1085     * {@link OsmPrimitive#isModified()} == <code>true</code>.
1086     */
1087    public boolean isModified() {
1088        for (OsmPrimitive p : allPrimitives) {
1089            if (p.isModified())
1090                return true;
1091        }
1092        return false;
1093    }
1094
1095    /**
1096     * Replies true if there is at least one primitive in this dataset which requires to be uploaded to server.
1097     * @return true if there is at least one primitive in this dataset which requires to be uploaded to server
1098     * @since 13161
1099     */
1100    public boolean requiresUploadToServer() {
1101        for (OsmPrimitive p : allPrimitives) {
1102            if (APIOperation.of(p) != null)
1103                return true;
1104        }
1105        return false;
1106    }
1107
1108    /**
1109     * Adds a new data set listener.
1110     * @param dsl The data set listener to add
1111     */
1112    public void addDataSetListener(DataSetListener dsl) {
1113        listeners.addIfAbsent(dsl);
1114    }
1115
1116    /**
1117     * Removes a data set listener.
1118     * @param dsl The data set listener to remove
1119     */
1120    public void removeDataSetListener(DataSetListener dsl) {
1121        listeners.remove(dsl);
1122    }
1123
1124    /**
1125     * Can be called before bigger changes on dataset. Events are disabled until {@link #endUpdate()}.
1126     * {@link DataSetListener#dataChanged(DataChangedEvent event)} event is triggered after end of changes
1127     * <br>
1128     * Typical usecase should look like this:
1129     * <pre>
1130     * ds.beginUpdate();
1131     * try {
1132     *   ...
1133     * } finally {
1134     *   ds.endUpdate();
1135     * }
1136     * </pre>
1137     * @see #endUpdate()
1138     */
1139    public void beginUpdate() {
1140        lock.writeLock().lock();
1141        updateCount++;
1142    }
1143
1144    /**
1145     * Must be called after a previous call to {@link #beginUpdate()} to fire change events.
1146     * <br>
1147     * Typical usecase should look like this:
1148     * <pre>
1149     * ds.beginUpdate();
1150     * try {
1151     *   ...
1152     * } finally {
1153     *   ds.endUpdate();
1154     * }
1155     * </pre>
1156     * @see DataSet#beginUpdate()
1157     */
1158    public void endUpdate() {
1159        if (updateCount > 0) {
1160            updateCount--;
1161            List<AbstractDatasetChangedEvent> eventsToFire = Collections.emptyList();
1162            if (updateCount == 0) {
1163                eventsToFire = new ArrayList<>(cachedEvents);
1164                cachedEvents.clear();
1165            }
1166
1167            if (!eventsToFire.isEmpty()) {
1168                lock.readLock().lock();
1169                lock.writeLock().unlock();
1170                try {
1171                    if (eventsToFire.size() < MAX_SINGLE_EVENTS) {
1172                        for (AbstractDatasetChangedEvent event : eventsToFire) {
1173                            fireEventToListeners(event);
1174                        }
1175                    } else if (eventsToFire.size() == MAX_EVENTS) {
1176                        fireEventToListeners(new DataChangedEvent(this));
1177                    } else {
1178                        fireEventToListeners(new DataChangedEvent(this, eventsToFire));
1179                    }
1180                } finally {
1181                    lock.readLock().unlock();
1182                }
1183            } else {
1184                lock.writeLock().unlock();
1185            }
1186
1187        } else
1188            throw new AssertionError("endUpdate called without beginUpdate");
1189    }
1190
1191    private void fireEventToListeners(AbstractDatasetChangedEvent event) {
1192        for (DataSetListener listener : listeners) {
1193            event.fire(listener);
1194        }
1195    }
1196
1197    private void fireEvent(AbstractDatasetChangedEvent event) {
1198        if (updateCount == 0)
1199            throw new AssertionError("dataset events can be fired only when dataset is locked");
1200        if (cachedEvents.size() < MAX_EVENTS) {
1201            cachedEvents.add(event);
1202        }
1203    }
1204
1205    void firePrimitivesAdded(Collection<? extends OsmPrimitive> added, boolean wasIncomplete) {
1206        fireEvent(new PrimitivesAddedEvent(this, added, wasIncomplete));
1207    }
1208
1209    void firePrimitivesRemoved(Collection<? extends OsmPrimitive> removed, boolean wasComplete) {
1210        fireEvent(new PrimitivesRemovedEvent(this, removed, wasComplete));
1211    }
1212
1213    void fireTagsChanged(OsmPrimitive prim, Map<String, String> originalKeys) {
1214        fireEvent(new TagsChangedEvent(this, prim, originalKeys));
1215    }
1216
1217    void fireRelationMembersChanged(Relation r) {
1218        reindexRelation(r);
1219        fireEvent(new RelationMembersChangedEvent(this, r));
1220    }
1221
1222    void fireNodeMoved(Node node, LatLon newCoor, EastNorth eastNorth) {
1223        reindexNode(node, newCoor, eastNorth);
1224        fireEvent(new NodeMovedEvent(this, node));
1225    }
1226
1227    void fireWayNodesChanged(Way way) {
1228        reindexWay(way);
1229        fireEvent(new WayNodesChangedEvent(this, way));
1230    }
1231
1232    void fireChangesetIdChanged(OsmPrimitive primitive, int oldChangesetId, int newChangesetId) {
1233        fireEvent(new ChangesetIdChangedEvent(this, Collections.singletonList(primitive), oldChangesetId,
1234                newChangesetId));
1235    }
1236
1237    void firePrimitiveFlagsChanged(OsmPrimitive primitive) {
1238        fireEvent(new PrimitiveFlagsChangedEvent(this, primitive));
1239    }
1240
1241    void fireFilterChanged() {
1242        fireEvent(new DataChangedEvent(this));
1243    }
1244
1245    void fireHighlightingChanged() {
1246        HighlightUpdateListener.HighlightUpdateEvent e = new HighlightUpdateListener.HighlightUpdateEvent(this);
1247        highlightUpdateListeners.fireEvent(l -> l.highlightUpdated(e));
1248    }
1249
1250    /**
1251     * Invalidates the internal cache of projected east/north coordinates.
1252     *
1253     * This method can be invoked after the globally configured projection method
1254     * changed.
1255     */
1256    public void invalidateEastNorthCache() {
1257        if (Main.getProjection() == null)
1258            return; // sanity check
1259        beginUpdate();
1260        try {
1261            for (Node n : getNodes()) {
1262                n.invalidateEastNorthCache();
1263            }
1264        } finally {
1265            endUpdate();
1266        }
1267    }
1268
1269    /**
1270     * Cleanups all deleted primitives (really delete them from the dataset).
1271     */
1272    public void cleanupDeletedPrimitives() {
1273        beginUpdate();
1274        try {
1275            Collection<OsmPrimitive> toCleanUp = getPrimitives(
1276                    primitive -> primitive.isDeleted() && (!primitive.isVisible() || primitive.isNew()));
1277            if (!toCleanUp.isEmpty()) {
1278                // We unselect them in advance to not fire a selection change for every primitive
1279                clearSelection(toCleanUp.stream().map(OsmPrimitive::getPrimitiveId));
1280                for (OsmPrimitive primitive : toCleanUp) {
1281                    removePrimitiveImpl(primitive);
1282                }
1283                firePrimitivesRemoved(toCleanUp, false);
1284            }
1285        } finally {
1286            endUpdate();
1287        }
1288    }
1289
1290    /**
1291     * Removes all primitives from the dataset and resets the currently selected primitives
1292     * to the empty collection. Also notifies selection change listeners if necessary.
1293     * @throws IllegalStateException if the dataset is read-only
1294     */
1295    @Override
1296    public void clear() {
1297        checkModifiable();
1298        beginUpdate();
1299        try {
1300            clearSelection();
1301            for (OsmPrimitive primitive : allPrimitives) {
1302                primitive.setDataset(null);
1303            }
1304            super.clear();
1305            allPrimitives.clear();
1306        } finally {
1307            endUpdate();
1308        }
1309    }
1310
1311    /**
1312     * Marks all "invisible" objects as deleted. These objects should be always marked as
1313     * deleted when downloaded from the server. They can be undeleted later if necessary.
1314     * @throws IllegalStateException if the dataset is read-only
1315     */
1316    public void deleteInvisible() {
1317        checkModifiable();
1318        for (OsmPrimitive primitive : allPrimitives) {
1319            if (!primitive.isVisible()) {
1320                primitive.setDeleted(true);
1321            }
1322        }
1323    }
1324
1325    /**
1326     * Moves all primitives and datasources from DataSet "from" to this DataSet.
1327     * @param from The source DataSet
1328     */
1329    public void mergeFrom(DataSet from) {
1330        mergeFrom(from, null);
1331    }
1332
1333    /**
1334     * Moves all primitives and datasources from DataSet "from" to this DataSet.
1335     * @param from The source DataSet
1336     * @param progressMonitor The progress monitor
1337     * @throws IllegalStateException if the dataset is read-only
1338     */
1339    public synchronized void mergeFrom(DataSet from, ProgressMonitor progressMonitor) {
1340        if (from != null) {
1341            checkModifiable();
1342            new DataSetMerger(this, from).merge(progressMonitor);
1343            synchronized (from) {
1344                if (!from.dataSources.isEmpty()) {
1345                    if (dataSources.addAll(from.dataSources)) {
1346                        cachedDataSourceArea = null;
1347                        cachedDataSourceBounds = null;
1348                    }
1349                    from.dataSources.clear();
1350                    from.cachedDataSourceArea = null;
1351                    from.cachedDataSourceBounds = null;
1352                }
1353            }
1354        }
1355    }
1356
1357    /**
1358     * Replies the set of conflicts currently managed in this layer.
1359     *
1360     * @return the set of conflicts currently managed in this layer
1361     * @since 12672
1362     */
1363    public ConflictCollection getConflicts() {
1364        return conflicts;
1365    }
1366
1367    /**
1368     * Returns the name of this data set (optional).
1369     * @return the name of this data set. Can be {@code null}
1370     * @since 12718
1371     */
1372    public String getName() {
1373        return name;
1374    }
1375
1376    /**
1377     * Sets the name of this data set.
1378     * @param name the new name of this data set. Can be {@code null} to reset it
1379     * @since 12718
1380     */
1381    public void setName(String name) {
1382        this.name = name;
1383    }
1384
1385    /* --------------------------------------------------------------------------------- */
1386    /* interface ProjectionChangeListner                                                 */
1387    /* --------------------------------------------------------------------------------- */
1388    @Override
1389    public void projectionChanged(Projection oldValue, Projection newValue) {
1390        invalidateEastNorthCache();
1391    }
1392
1393    /**
1394     * Returns the data sources bounding box.
1395     * @return the data sources bounding box
1396     */
1397    public synchronized ProjectionBounds getDataSourceBoundingBox() {
1398        BoundingXYVisitor bbox = new BoundingXYVisitor();
1399        for (DataSource source : dataSources) {
1400            bbox.visit(source.bounds);
1401        }
1402        if (bbox.hasExtend()) {
1403            return bbox.getBounds();
1404        }
1405        return null;
1406    }
1407
1408    /**
1409     * Returns mappaint cache index for this DataSet.
1410     *
1411     * If the {@link OsmPrimitive#mappaintCacheIdx} is not equal to the DataSet mappaint
1412     * cache index, this means the cache for that primitive is out of date.
1413     * @return mappaint cache index
1414     * @since 13420
1415     */
1416    public short getMappaintCacheIndex() {
1417        return mappaintCacheIdx;
1418    }
1419
1420    /**
1421     * Clear the mappaint cache for this DataSet.
1422     * @since 13420
1423     */
1424    public void clearMappaintCache() {
1425        mappaintCacheIdx++;
1426    }
1427
1428    @Override
1429    public void lock() {
1430        if (!isReadOnly.compareAndSet(false, true)) {
1431            Logging.warn("Trying to set readOnly flag on a readOnly dataset ", getName());
1432        }
1433    }
1434
1435    @Override
1436    public void unlock() {
1437        if (!isReadOnly.compareAndSet(true, false)) {
1438            Logging.warn("Trying to unset readOnly flag on a non-readOnly dataset ", getName());
1439        }
1440    }
1441
1442    @Override
1443    public boolean isLocked() {
1444        return isReadOnly.get();
1445    }
1446
1447    /**
1448     * Checks the dataset is modifiable (not read-only).
1449     * @throws IllegalStateException if the dataset is read-only
1450     */
1451    private void checkModifiable() {
1452        if (isLocked()) {
1453            throw new IllegalStateException("DataSet is read-only");
1454        }
1455    }
1456}