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