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