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