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