source: josm/trunk/src/org/openstreetmap/josm/data/osm/DataSet.java @ 13161

Last change on this file since 13161 was 13161, checked in by Don-vip, 15 months ago

fix #13153 - Should not warn to upload/save "modified" layers with 0 objects

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