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.data.APIDataSet.APIOperation;
028import org.openstreetmap.josm.data.Bounds;
029import org.openstreetmap.josm.data.DataSource;
030import org.openstreetmap.josm.data.ProjectionBounds;
031import org.openstreetmap.josm.data.conflict.ConflictCollection;
032import org.openstreetmap.josm.data.coor.EastNorth;
033import org.openstreetmap.josm.data.coor.LatLon;
034import org.openstreetmap.josm.data.osm.DataSelectionListener.SelectionAddEvent;
035import org.openstreetmap.josm.data.osm.DataSelectionListener.SelectionChangeEvent;
036import org.openstreetmap.josm.data.osm.DataSelectionListener.SelectionRemoveEvent;
037import org.openstreetmap.josm.data.osm.DataSelectionListener.SelectionReplaceEvent;
038import org.openstreetmap.josm.data.osm.DataSelectionListener.SelectionToggleEvent;
039import org.openstreetmap.josm.data.osm.event.AbstractDatasetChangedEvent;
040import org.openstreetmap.josm.data.osm.event.ChangesetIdChangedEvent;
041import org.openstreetmap.josm.data.osm.event.DataChangedEvent;
042import org.openstreetmap.josm.data.osm.event.DataSetListener;
043import org.openstreetmap.josm.data.osm.event.FilterChangedEvent;
044import org.openstreetmap.josm.data.osm.event.NodeMovedEvent;
045import org.openstreetmap.josm.data.osm.event.PrimitiveFlagsChangedEvent;
046import org.openstreetmap.josm.data.osm.event.PrimitivesAddedEvent;
047import org.openstreetmap.josm.data.osm.event.PrimitivesRemovedEvent;
048import org.openstreetmap.josm.data.osm.event.RelationMembersChangedEvent;
049import org.openstreetmap.josm.data.osm.event.TagsChangedEvent;
050import org.openstreetmap.josm.data.osm.event.WayNodesChangedEvent;
051import org.openstreetmap.josm.data.osm.visitor.BoundingXYVisitor;
052import org.openstreetmap.josm.data.projection.Projection;
053import org.openstreetmap.josm.data.projection.ProjectionChangeListener;
054import org.openstreetmap.josm.data.projection.ProjectionRegistry;
055import org.openstreetmap.josm.gui.progress.ProgressMonitor;
056import org.openstreetmap.josm.spi.preferences.Config;
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 = DownloadPolicy.NORMAL;
136    private UploadPolicy uploadPolicy = UploadPolicy.NORMAL;
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    private String remark;
170
171    /**
172     * Constructs a new {@code DataSet}.
173     */
174    public DataSet() {
175        // Transparently register as projection change listener. No need to explicitly remove
176        // the listener, projection change listeners are managed as WeakReferences.
177        ProjectionRegistry.addProjectionChangeListener(this);
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 = Objects.requireNonNull(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 = Objects.requireNonNull(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     * Returns selected nodes and ways.
544     * @return selected nodes and ways
545     */
546    public Collection<OsmPrimitive> getSelectedNodesAndWays() {
547        return new SubclassFilteredCollection<>(getSelected(),
548                primitive -> primitive instanceof Node || primitive instanceof Way);
549    }
550
551    @Override
552    public Collection<WaySegment> getHighlightedVirtualNodes() {
553        return Collections.unmodifiableCollection(highlightedVirtualNodes);
554    }
555
556    @Override
557    public Collection<WaySegment> getHighlightedWaySegments() {
558        return Collections.unmodifiableCollection(highlightedWaySegments);
559    }
560
561    @Override
562    public void addHighlightUpdateListener(HighlightUpdateListener listener) {
563        highlightUpdateListeners.addListener(listener);
564    }
565
566    @Override
567    public void removeHighlightUpdateListener(HighlightUpdateListener listener) {
568        highlightUpdateListeners.removeListener(listener);
569    }
570
571    @Override
572    public Collection<OsmPrimitive> getAllSelected() {
573        return currentSelectedPrimitives;
574    }
575
576    @Override
577    public boolean selectionEmpty() {
578        return currentSelectedPrimitives.isEmpty();
579    }
580
581    @Override
582    public boolean isSelected(OsmPrimitive osm) {
583        return currentSelectedPrimitives.contains(osm);
584    }
585
586    @Override
587    public void setHighlightedVirtualNodes(Collection<WaySegment> waySegments) {
588        if (highlightedVirtualNodes.isEmpty() && waySegments.isEmpty())
589            return;
590
591        highlightedVirtualNodes = waySegments;
592        fireHighlightingChanged();
593    }
594
595    @Override
596    public void setHighlightedWaySegments(Collection<WaySegment> waySegments) {
597        if (highlightedWaySegments.isEmpty() && waySegments.isEmpty())
598            return;
599
600        highlightedWaySegments = waySegments;
601        fireHighlightingChanged();
602    }
603
604    @Override
605    public void setSelected(Collection<? extends PrimitiveId> selection) {
606        setSelected(selection.stream());
607    }
608
609    @Override
610    public void setSelected(PrimitiveId... osm) {
611        setSelected(Stream.of(osm).filter(Objects::nonNull));
612    }
613
614    private void setSelected(Stream<? extends PrimitiveId> stream) {
615        doSelectionChange(old -> new SelectionReplaceEvent(this, old,
616                stream.map(this::getPrimitiveByIdChecked).filter(Objects::nonNull)));
617    }
618
619    @Override
620    public void addSelected(Collection<? extends PrimitiveId> selection) {
621        addSelected(selection.stream());
622    }
623
624    @Override
625    public void addSelected(PrimitiveId... osm) {
626        addSelected(Stream.of(osm));
627    }
628
629    private void addSelected(Stream<? extends PrimitiveId> stream) {
630        doSelectionChange(old -> new SelectionAddEvent(this, old,
631                stream.map(this::getPrimitiveByIdChecked).filter(Objects::nonNull)));
632    }
633
634    @Override
635    public void clearSelection(PrimitiveId... osm) {
636        clearSelection(Stream.of(osm));
637    }
638
639    @Override
640    public void clearSelection(Collection<? extends PrimitiveId> list) {
641        clearSelection(list.stream());
642    }
643
644    @Override
645    public void clearSelection() {
646        setSelected(Stream.empty());
647    }
648
649    private void clearSelection(Stream<? extends PrimitiveId> stream) {
650        doSelectionChange(old -> new SelectionRemoveEvent(this, old,
651                stream.map(this::getPrimitiveByIdChecked).filter(Objects::nonNull)));
652    }
653
654    @Override
655    public void toggleSelected(Collection<? extends PrimitiveId> osm) {
656        toggleSelected(osm.stream());
657    }
658
659    @Override
660    public void toggleSelected(PrimitiveId... osm) {
661        toggleSelected(Stream.of(osm));
662    }
663
664    private void toggleSelected(Stream<? extends PrimitiveId> stream) {
665        doSelectionChange(old -> new SelectionToggleEvent(this, old,
666                stream.map(this::getPrimitiveByIdChecked).filter(Objects::nonNull)));
667    }
668
669    /**
670     * Do a selection change.
671     * <p>
672     * This is the only method that changes the current selection state.
673     * @param command A generator that generates the {@link SelectionChangeEvent} for the given base set of currently selected primitives.
674     * @return true iff the command did change the selection.
675     * @since 12048
676     */
677    private boolean doSelectionChange(Function<Set<OsmPrimitive>, SelectionChangeEvent> command) {
678        synchronized (selectionLock) {
679            SelectionChangeEvent event = command.apply(currentSelectedPrimitives);
680            if (event.isNop()) {
681                return false;
682            }
683            currentSelectedPrimitives = event.getSelection();
684            selectionListeners.fireEvent(l -> l.selectionChanged(event));
685            return true;
686        }
687    }
688
689    @Override
690    public synchronized Area getDataSourceArea() {
691        if (cachedDataSourceArea == null) {
692            cachedDataSourceArea = OsmData.super.getDataSourceArea();
693        }
694        return cachedDataSourceArea;
695    }
696
697    @Override
698    public synchronized List<Bounds> getDataSourceBounds() {
699        if (cachedDataSourceBounds == null) {
700            cachedDataSourceBounds = OsmData.super.getDataSourceBounds();
701        }
702        return Collections.unmodifiableList(cachedDataSourceBounds);
703    }
704
705    @Override
706    public synchronized Collection<DataSource> getDataSources() {
707        return Collections.unmodifiableCollection(dataSources);
708    }
709
710    @Override
711    public OsmPrimitive getPrimitiveById(PrimitiveId primitiveId) {
712        return primitiveId != null ? primitivesMap.get(primitiveId) : null;
713    }
714
715    /**
716     * Show message and stack trace in log in case primitive is not found
717     * @param primitiveId primitive id to look for
718     * @return Primitive by id.
719     */
720    private OsmPrimitive getPrimitiveByIdChecked(PrimitiveId primitiveId) {
721        OsmPrimitive result = getPrimitiveById(primitiveId);
722        if (result == null && primitiveId != null) {
723            Logging.warn(tr(
724                    "JOSM expected to find primitive [{0} {1}] in dataset but it is not there. Please report this "
725                            + "at {2}. This is not a critical error, it should be safe to continue in your work.",
726                    primitiveId.getType(), Long.toString(primitiveId.getUniqueId()), Config.getUrls().getJOSMWebsite()));
727            Logging.error(new Exception());
728        }
729
730        return result;
731    }
732
733    private static void deleteWay(Way way) {
734        way.setNodes(null);
735        way.setDeleted(true);
736    }
737
738    /**
739     * Removes all references from ways in this dataset to a particular node.
740     *
741     * @param node the node
742     * @return The set of ways that have been modified
743     * @throws IllegalStateException if the dataset is read-only
744     */
745    public Set<Way> unlinkNodeFromWays(Node node) {
746        checkModifiable();
747        Set<Way> result = new HashSet<>();
748        beginUpdate();
749        try {
750            for (Way way : node.getParentWays()) {
751                List<Node> wayNodes = way.getNodes();
752                if (wayNodes.remove(node)) {
753                    if (wayNodes.size() < 2) {
754                        deleteWay(way);
755                    } else {
756                        way.setNodes(wayNodes);
757                    }
758                    result.add(way);
759                }
760            }
761        } finally {
762            endUpdate();
763        }
764        return result;
765    }
766
767    /**
768     * removes all references from relations in this dataset  to this primitive
769     *
770     * @param primitive the primitive
771     * @return The set of relations that have been modified
772     * @throws IllegalStateException if the dataset is read-only
773     */
774    public Set<Relation> unlinkPrimitiveFromRelations(OsmPrimitive primitive) {
775        checkModifiable();
776        Set<Relation> result = new HashSet<>();
777        beginUpdate();
778        try {
779            for (Relation relation : getRelations()) {
780                List<RelationMember> members = relation.getMembers();
781
782                Iterator<RelationMember> it = members.iterator();
783                boolean removed = false;
784                while (it.hasNext()) {
785                    RelationMember member = it.next();
786                    if (member.getMember().equals(primitive)) {
787                        it.remove();
788                        removed = true;
789                    }
790                }
791
792                if (removed) {
793                    relation.setMembers(members);
794                    result.add(relation);
795                }
796            }
797        } finally {
798            endUpdate();
799        }
800        return result;
801    }
802
803    /**
804     * Removes all references from other primitives to the referenced primitive.
805     *
806     * @param referencedPrimitive the referenced primitive
807     * @return The set of primitives that have been modified
808     * @throws IllegalStateException if the dataset is read-only
809     */
810    public Set<OsmPrimitive> unlinkReferencesToPrimitive(OsmPrimitive referencedPrimitive) {
811        checkModifiable();
812        Set<OsmPrimitive> result = new HashSet<>();
813        beginUpdate();
814        try {
815            if (referencedPrimitive instanceof Node) {
816                result.addAll(unlinkNodeFromWays((Node) referencedPrimitive));
817            }
818            result.addAll(unlinkPrimitiveFromRelations(referencedPrimitive));
819        } finally {
820            endUpdate();
821        }
822        return result;
823    }
824
825    @Override
826    public boolean isModified() {
827        for (OsmPrimitive p : allPrimitives) {
828            if (p.isModified())
829                return true;
830        }
831        return false;
832    }
833
834    /**
835     * Replies true if there is at least one primitive in this dataset which requires to be uploaded to server.
836     * @return true if there is at least one primitive in this dataset which requires to be uploaded to server
837     * @since 13161
838     */
839    public boolean requiresUploadToServer() {
840        for (OsmPrimitive p : allPrimitives) {
841            if (APIOperation.of(p) != null)
842                return true;
843        }
844        return false;
845    }
846
847    /**
848     * Adds a new data set listener.
849     * @param dsl The data set listener to add
850     */
851    public void addDataSetListener(DataSetListener dsl) {
852        listeners.addIfAbsent(dsl);
853    }
854
855    /**
856     * Removes a data set listener.
857     * @param dsl The data set listener to remove
858     */
859    public void removeDataSetListener(DataSetListener dsl) {
860        listeners.remove(dsl);
861    }
862
863    /**
864     * Can be called before bigger changes on dataset. Events are disabled until {@link #endUpdate()}.
865     * {@link DataSetListener#dataChanged(DataChangedEvent event)} event is triggered after end of changes
866     * <br>
867     * Typical usecase should look like this:
868     * <pre>
869     * ds.beginUpdate();
870     * try {
871     *   ...
872     * } finally {
873     *   ds.endUpdate();
874     * }
875     * </pre>
876     * @see #endUpdate()
877     */
878    public void beginUpdate() {
879        lock.writeLock().lock();
880        updateCount++;
881    }
882
883    /**
884     * Must be called after a previous call to {@link #beginUpdate()} to fire change events.
885     * <br>
886     * Typical usecase should look like this:
887     * <pre>
888     * ds.beginUpdate();
889     * try {
890     *   ...
891     * } finally {
892     *   ds.endUpdate();
893     * }
894     * </pre>
895     * @see DataSet#beginUpdate()
896     */
897    public void endUpdate() {
898        if (updateCount > 0) {
899            updateCount--;
900            List<AbstractDatasetChangedEvent> eventsToFire = Collections.emptyList();
901            if (updateCount == 0) {
902                eventsToFire = new ArrayList<>(cachedEvents);
903                cachedEvents.clear();
904            }
905
906            if (!eventsToFire.isEmpty()) {
907                lock.readLock().lock();
908                try {
909                    lock.writeLock().unlock();
910                    if (eventsToFire.size() < MAX_SINGLE_EVENTS) {
911                        for (AbstractDatasetChangedEvent event : eventsToFire) {
912                            fireEventToListeners(event);
913                        }
914                    } else if (eventsToFire.size() == MAX_EVENTS) {
915                        fireEventToListeners(new DataChangedEvent(this));
916                    } else {
917                        fireEventToListeners(new DataChangedEvent(this, eventsToFire));
918                    }
919                } finally {
920                    lock.readLock().unlock();
921                }
922            } else {
923                lock.writeLock().unlock();
924            }
925
926        } else
927            throw new AssertionError("endUpdate called without beginUpdate");
928    }
929
930    private void fireEventToListeners(AbstractDatasetChangedEvent event) {
931        for (DataSetListener listener : listeners) {
932            event.fire(listener);
933        }
934    }
935
936    private void fireEvent(AbstractDatasetChangedEvent event) {
937        if (updateCount == 0)
938            throw new AssertionError("dataset events can be fired only when dataset is locked");
939        if (cachedEvents.size() < MAX_EVENTS) {
940            cachedEvents.add(event);
941        }
942    }
943
944    void firePrimitivesAdded(Collection<? extends OsmPrimitive> added, boolean wasIncomplete) {
945        fireEvent(new PrimitivesAddedEvent(this, added, wasIncomplete));
946    }
947
948    void firePrimitivesRemoved(Collection<? extends OsmPrimitive> removed, boolean wasComplete) {
949        fireEvent(new PrimitivesRemovedEvent(this, removed, wasComplete));
950    }
951
952    void fireTagsChanged(OsmPrimitive prim, Map<String, String> originalKeys) {
953        fireEvent(new TagsChangedEvent(this, prim, originalKeys));
954    }
955
956    void fireRelationMembersChanged(Relation r) {
957        store.reindexRelation(r, Relation::updatePosition);
958        fireEvent(new RelationMembersChangedEvent(this, r));
959    }
960
961    void fireNodeMoved(Node node, LatLon newCoor, EastNorth eastNorth) {
962        store.reindexNode(node, n -> n.setCoorInternal(newCoor, eastNorth), Way::updatePosition, Relation::updatePosition);
963        fireEvent(new NodeMovedEvent(this, node));
964    }
965
966    void fireWayNodesChanged(Way way) {
967        if (way.getNodesCount() > 0) {
968            store.reindexWay(way, Way::updatePosition, Relation::updatePosition);
969        }
970        fireEvent(new WayNodesChangedEvent(this, way));
971    }
972
973    void fireChangesetIdChanged(OsmPrimitive primitive, int oldChangesetId, int newChangesetId) {
974        fireEvent(new ChangesetIdChangedEvent(this, Collections.singletonList(primitive), oldChangesetId,
975                newChangesetId));
976    }
977
978    void firePrimitiveFlagsChanged(OsmPrimitive primitive) {
979        fireEvent(new PrimitiveFlagsChangedEvent(this, primitive));
980    }
981
982    void fireFilterChanged() {
983        fireEvent(new FilterChangedEvent(this));
984    }
985
986    void fireHighlightingChanged() {
987        HighlightUpdateListener.HighlightUpdateEvent e = new HighlightUpdateListener.HighlightUpdateEvent(this);
988        highlightUpdateListeners.fireEvent(l -> l.highlightUpdated(e));
989    }
990
991    /**
992     * Invalidates the internal cache of projected east/north coordinates.
993     *
994     * This method can be invoked after the globally configured projection method
995     * changed.
996     */
997    public void invalidateEastNorthCache() {
998        if (ProjectionRegistry.getProjection() == null)
999            return; // sanity check
1000        beginUpdate();
1001        try {
1002            for (Node n : getNodes()) {
1003                n.invalidateEastNorthCache();
1004            }
1005        } finally {
1006            endUpdate();
1007        }
1008    }
1009
1010    /**
1011     * Cleanups all deleted primitives (really delete them from the dataset).
1012     */
1013    public void cleanupDeletedPrimitives() {
1014        beginUpdate();
1015        try {
1016            Collection<OsmPrimitive> toCleanUp = getPrimitives(
1017                    primitive -> primitive.isDeleted() && (!primitive.isVisible() || primitive.isNew()));
1018            if (!toCleanUp.isEmpty()) {
1019                // We unselect them in advance to not fire a selection change for every primitive
1020                clearSelection(toCleanUp.stream().map(OsmPrimitive::getPrimitiveId));
1021                for (OsmPrimitive primitive : toCleanUp) {
1022                    removePrimitiveImpl(primitive);
1023                }
1024                firePrimitivesRemoved(toCleanUp, false);
1025            }
1026        } finally {
1027            endUpdate();
1028        }
1029    }
1030
1031    /**
1032     * Removes all primitives from the dataset and resets the currently selected primitives
1033     * to the empty collection. Also notifies selection change listeners if necessary.
1034     * @throws IllegalStateException if the dataset is read-only
1035     */
1036    @Override
1037    public void clear() {
1038        //TODO: Why can't we clear a dataset that is locked?
1039        //TODO: Report listeners that are still active (should be none)
1040        checkModifiable();
1041        beginUpdate();
1042        try {
1043            clearSelection();
1044            for (OsmPrimitive primitive : allPrimitives) {
1045                primitive.setDataset(null);
1046            }
1047            store.clear();
1048            allPrimitives.clear();
1049        } finally {
1050            endUpdate();
1051        }
1052    }
1053
1054    /**
1055     * Marks all "invisible" objects as deleted. These objects should be always marked as
1056     * deleted when downloaded from the server. They can be undeleted later if necessary.
1057     * @throws IllegalStateException if the dataset is read-only
1058     */
1059    public void deleteInvisible() {
1060        checkModifiable();
1061        for (OsmPrimitive primitive : allPrimitives) {
1062            if (!primitive.isVisible()) {
1063                primitive.setDeleted(true);
1064            }
1065        }
1066    }
1067
1068    /**
1069     * Moves all primitives and datasources from DataSet "from" to this DataSet.
1070     * @param from The source DataSet
1071     */
1072    public void mergeFrom(DataSet from) {
1073        mergeFrom(from, null);
1074    }
1075
1076    /**
1077     * Moves all primitives and datasources from DataSet "from" to this DataSet.
1078     * @param from The source DataSet
1079     * @param progressMonitor The progress monitor
1080     * @throws IllegalStateException if the dataset is read-only
1081     */
1082    public synchronized void mergeFrom(DataSet from, ProgressMonitor progressMonitor) {
1083        if (from != null) {
1084            checkModifiable();
1085            new DataSetMerger(this, from).merge(progressMonitor);
1086            synchronized (from) {
1087                if (!from.dataSources.isEmpty()) {
1088                    if (dataSources.addAll(from.dataSources)) {
1089                        cachedDataSourceArea = null;
1090                        cachedDataSourceBounds = null;
1091                    }
1092                    from.dataSources.clear();
1093                    from.cachedDataSourceArea = null;
1094                    from.cachedDataSourceBounds = null;
1095                }
1096            }
1097        }
1098    }
1099
1100    /**
1101     * Replies the set of conflicts currently managed in this layer.
1102     *
1103     * @return the set of conflicts currently managed in this layer
1104     * @since 12672
1105     */
1106    public ConflictCollection getConflicts() {
1107        return conflicts;
1108    }
1109
1110    @Override
1111    public String getName() {
1112        return name;
1113    }
1114
1115    @Override
1116    public void setName(String name) {
1117        this.name = name;
1118    }
1119
1120    /* --------------------------------------------------------------------------------- */
1121    /* interface ProjectionChangeListner                                                 */
1122    /* --------------------------------------------------------------------------------- */
1123    @Override
1124    public void projectionChanged(Projection oldValue, Projection newValue) {
1125        invalidateEastNorthCache();
1126    }
1127
1128    @Override
1129    public synchronized ProjectionBounds getDataSourceBoundingBox() {
1130        BoundingXYVisitor bbox = new BoundingXYVisitor();
1131        for (DataSource source : dataSources) {
1132            bbox.visit(source.bounds);
1133        }
1134        if (bbox.hasExtend()) {
1135            return bbox.getBounds();
1136        }
1137        return null;
1138    }
1139
1140    /**
1141     * Returns mappaint cache index for this DataSet.
1142     *
1143     * If the {@link OsmPrimitive#mappaintCacheIdx} is not equal to the DataSet mappaint
1144     * cache index, this means the cache for that primitive is out of date.
1145     * @return mappaint cache index
1146     * @since 13420
1147     */
1148    public short getMappaintCacheIndex() {
1149        return mappaintCacheIdx;
1150    }
1151
1152    @Override
1153    public void clearMappaintCache() {
1154        mappaintCacheIdx++;
1155    }
1156
1157    @Override
1158    public void lock() {
1159        if (!isReadOnly.compareAndSet(false, true)) {
1160            Logging.warn("Trying to set readOnly flag on a readOnly dataset ", getName());
1161        }
1162    }
1163
1164    @Override
1165    public void unlock() {
1166        if (!isReadOnly.compareAndSet(true, false)) {
1167            Logging.warn("Trying to unset readOnly flag on a non-readOnly dataset ", getName());
1168        }
1169    }
1170
1171    @Override
1172    public boolean isLocked() {
1173        return isReadOnly.get();
1174    }
1175
1176    /**
1177     * Checks the dataset is modifiable (not read-only).
1178     * @throws IllegalStateException if the dataset is read-only
1179     */
1180    private void checkModifiable() {
1181        if (isLocked()) {
1182            throw new IllegalStateException("DataSet is read-only");
1183        }
1184    }
1185
1186    /**
1187     * Returns an optional remark about this data set (used by Overpass API).
1188     * @return a remark about this data set, or {@code null}
1189     * @since 14219
1190     */
1191    public String getRemark() {
1192        return remark;
1193    }
1194
1195    /**
1196     * Sets an optional remark about this data set (used by Overpass API).
1197     * @param remark a remark about this data set, or {@code null}
1198     * @since 14219
1199     */
1200    public void setRemark(String remark) {
1201        this.remark = remark;
1202    }
1203
1204    /**
1205     * @return true if this Dataset contains no primitives
1206     * @since 14835
1207     */
1208    public boolean isEmpty() {
1209        return allPrimitives.isEmpty();
1210    }
1211}