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