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