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