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

Last change on this file was 18871, checked in by taylor.smock, 6 months ago

See #23218: Use newer error_prone versions when compiling on Java 11+

error_prone 2.11 dropped support for compiling with Java 8, although it still
supports compiling for Java 8. The "major" new check for us is NotJavadoc since
we used /** in quite a few places which were not javadoc.

Other "new" checks that are of interest:

  • AlreadyChecked: if (foo) { doFoo(); } else if (!foo) { doBar(); }
  • UnnecessaryStringBuilder: Avoid StringBuilder (Java converts + to StringBuilder behind-the-scenes, but may also do something else if it performs better)
  • NonApiType: Avoid specific interface types in function definitions
  • NamedLikeContextualKeyword: Avoid using restricted names for classes and methods
  • UnusedMethod: Unused private methods should be removed

This fixes most of the new error_prone issues and some SonarLint issues.

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