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