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