// License: GPL. For details, see LICENSE file. package org.openstreetmap.josm.data.osm; import static org.openstreetmap.josm.tools.I18n.tr; import java.awt.geom.Area; import java.util.ArrayList; import java.util.Arrays; import java.util.Collection; import java.util.Collections; import java.util.HashMap; import java.util.HashSet; import java.util.Iterator; import java.util.LinkedHashSet; import java.util.LinkedList; import java.util.List; import java.util.Map; import java.util.Objects; import java.util.Set; import java.util.concurrent.CopyOnWriteArrayList; import java.util.concurrent.locks.Lock; import java.util.concurrent.locks.ReadWriteLock; import java.util.concurrent.locks.ReentrantReadWriteLock; import java.util.function.Predicate; import java.util.stream.Collectors; import org.openstreetmap.josm.Main; import org.openstreetmap.josm.data.Bounds; import org.openstreetmap.josm.data.Data; import org.openstreetmap.josm.data.DataSource; import org.openstreetmap.josm.data.ProjectionBounds; import org.openstreetmap.josm.data.SelectionChangedListener; import org.openstreetmap.josm.data.coor.EastNorth; import org.openstreetmap.josm.data.coor.LatLon; import org.openstreetmap.josm.data.osm.event.AbstractDatasetChangedEvent; import org.openstreetmap.josm.data.osm.event.ChangesetIdChangedEvent; import org.openstreetmap.josm.data.osm.event.DataChangedEvent; import org.openstreetmap.josm.data.osm.event.DataSetListener; import org.openstreetmap.josm.data.osm.event.NodeMovedEvent; import org.openstreetmap.josm.data.osm.event.PrimitiveFlagsChangedEvent; import org.openstreetmap.josm.data.osm.event.PrimitivesAddedEvent; import org.openstreetmap.josm.data.osm.event.PrimitivesRemovedEvent; import org.openstreetmap.josm.data.osm.event.RelationMembersChangedEvent; import org.openstreetmap.josm.data.osm.event.TagsChangedEvent; import org.openstreetmap.josm.data.osm.event.WayNodesChangedEvent; import org.openstreetmap.josm.data.osm.visitor.BoundingXYVisitor; import org.openstreetmap.josm.data.projection.Projection; import org.openstreetmap.josm.data.projection.ProjectionChangeListener; import org.openstreetmap.josm.gui.progress.ProgressMonitor; import org.openstreetmap.josm.gui.tagging.ac.AutoCompletionManager; import org.openstreetmap.josm.tools.JosmRuntimeException; import org.openstreetmap.josm.tools.SubclassFilteredCollection; import org.openstreetmap.josm.tools.Utils; /** * DataSet is the data behind the application. It can consists of only a few points up to the whole * osm database. DataSet's can be merged together, saved, (up/down/disk)loaded etc. * * Note that DataSet is not an osm-primitive and so has no key association but a few members to * store some information. * * Dataset is threadsafe - accessing Dataset simultaneously from different threads should never * lead to data corruption or ConcurrentModificationException. However when for example one thread * removes primitive and other thread try to add another primitive referring to the removed primitive, * DataIntegrityException will occur. * * To prevent such situations, read/write lock is provided. While read lock is used, it's guaranteed that * Dataset will not change. Sample usage: * <code> * ds.getReadLock().lock(); * try { * // .. do something with dataset * } finally { * ds.getReadLock().unlock(); * } * </code> * * Write lock should be used in case of bulk operations. In addition to ensuring that other threads can't * use dataset in the middle of modifications it also stops sending of dataset events. That's good for performance * reasons - GUI can be updated after all changes are done. * Sample usage: * <code> * ds.beginUpdate() * try { * // .. do modifications * } finally { * ds.endUpdate(); * } * </code> * * Note that it is not necessary to call beginUpdate/endUpdate for every dataset modification - dataset will get locked * automatically. * * Note that locks cannot be upgraded - if one threads use read lock and and then write lock, dead lock will occur - see #5814 for * sample ticket * * @author imi */ public final class DataSet implements Data, ProjectionChangeListener { /** * Upload policy. * * Determines if upload to the OSM server is intended, discouraged, or * disabled / blocked. */ public enum UploadPolicy { /** * Normal dataset, upload intended. */ NORMAL("true"), /** * Upload discouraged, for example when using or distributing a private dataset. */ DISCOURAGED("false"), /** * Upload blocked. * Upload options completely disabled. Intended for special cases * where a warning dialog is not enough, see #12731. * * For the user, it shouldn't be too easy to disable this flag. */ BLOCKED("never"); final String xmlFlag; UploadPolicy(String xmlFlag) { this.xmlFlag = xmlFlag; } /** * Get the corresponding value of the <code>upload='...'</code> XML-attribute * in the .osm file. * @return value of the <code>upload</code> attribute */ public String getXmlFlag() { return xmlFlag; } } /** * Maximum number of events that can be fired between beginUpdate/endUpdate to be send as single events (ie without DatasetChangedEvent) */ private static final int MAX_SINGLE_EVENTS = 30; /** * Maximum number of events to kept between beginUpdate/endUpdate. When more events are created, that simple DatasetChangedEvent is sent) */ private static final int MAX_EVENTS = 1000; private final Storage<OsmPrimitive> allPrimitives = new Storage<>(new Storage.PrimitiveIdHash(), true); private final Map<PrimitiveId, OsmPrimitive> primitivesMap = allPrimitives.foreignKey(new Storage.PrimitiveIdHash()); private final CopyOnWriteArrayList<DataSetListener> listeners = new CopyOnWriteArrayList<>(); // provide means to highlight map elements that are not osm primitives private Collection<WaySegment> highlightedVirtualNodes = new LinkedList<>(); private Collection<WaySegment> highlightedWaySegments = new LinkedList<>(); // Number of open calls to beginUpdate private int updateCount; // Events that occurred while dataset was locked but should be fired after write lock is released private final List<AbstractDatasetChangedEvent> cachedEvents = new ArrayList<>(); private int highlightUpdateCount; private UploadPolicy uploadPolicy; private final ReadWriteLock lock = new ReentrantReadWriteLock(); private final Object selectionLock = new Object(); private Area cachedDataSourceArea; private List<Bounds> cachedDataSourceBounds; /** * All data sources of this DataSet. */ private final Collection<DataSource> dataSources = new LinkedList<>(); /** * Constructs a new {@code DataSet}. */ public DataSet() { // Transparently register as projection change listener. No need to explicitly remove // the listener, projection change listeners are managed as WeakReferences. Main.addProjectionChangeListener(this); } /** * Creates a new {@link DataSet}. * @param copyFrom An other {@link DataSet} to copy the contents of this dataset from. * @since 10346 */ public DataSet(DataSet copyFrom) { this(); copyFrom.getReadLock().lock(); try { Map<OsmPrimitive, OsmPrimitive> primMap = new HashMap<>(); for (Node n : copyFrom.nodes) { Node newNode = new Node(n); primMap.put(n, newNode); addPrimitive(newNode); } for (Way w : copyFrom.ways) { Way newWay = new Way(w); primMap.put(w, newWay); List<Node> newNodes = new ArrayList<>(); for (Node n: w.getNodes()) { newNodes.add((Node) primMap.get(n)); } newWay.setNodes(newNodes); addPrimitive(newWay); } // Because relations can have other relations as members we first clone all relations // and then get the cloned members for (Relation r : copyFrom.relations) { Relation newRelation = new Relation(r, r.isNew()); newRelation.setMembers(null); primMap.put(r, newRelation); addPrimitive(newRelation); } for (Relation r : copyFrom.relations) { Relation newRelation = (Relation) primMap.get(r); List<RelationMember> newMembers = new ArrayList<>(); for (RelationMember rm: r.getMembers()) { newMembers.add(new RelationMember(rm.getRole(), primMap.get(rm.getMember()))); } newRelation.setMembers(newMembers); } for (DataSource source : copyFrom.dataSources) { dataSources.add(new DataSource(source)); } version = copyFrom.version; } finally { copyFrom.getReadLock().unlock(); } } /** * Adds a new data source. * @param source data source to add * @return {@code true} if the collection changed as a result of the call * @since 11626 */ public synchronized boolean addDataSource(DataSource source) { return addDataSources(Collections.singleton(source)); } /** * Adds new data sources. * @param sources data sources to add * @return {@code true} if the collection changed as a result of the call * @since 11626 */ public synchronized boolean addDataSources(Collection<DataSource> sources) { boolean changed = dataSources.addAll(sources); if (changed) { cachedDataSourceArea = null; cachedDataSourceBounds = null; } return changed; } /** * Returns the lock used for reading. * @return the lock used for reading */ public Lock getReadLock() { return lock.readLock(); } /** * This method can be used to detect changes in highlight state of primitives. If highlighting was changed * then the method will return different number. * @return the current highlight counter */ public int getHighlightUpdateCount() { return highlightUpdateCount; } /** * History of selections - shared by plugins and SelectionListDialog */ private final LinkedList<Collection<? extends OsmPrimitive>> selectionHistory = new LinkedList<>(); /** * Replies the history of JOSM selections * * @return list of history entries */ public LinkedList<Collection<? extends OsmPrimitive>> getSelectionHistory() { return selectionHistory; } /** * Clears selection history list */ public void clearSelectionHistory() { selectionHistory.clear(); } /** * Maintains a list of used tags for autocompletion. */ private AutoCompletionManager autocomplete; /** * Returns the autocompletion manager, which maintains a list of used tags for autocompletion. * @return the autocompletion manager */ public AutoCompletionManager getAutoCompletionManager() { if (autocomplete == null) { autocomplete = new AutoCompletionManager(this); addDataSetListener(autocomplete); } return autocomplete; } /** * The API version that created this data set, if any. */ private String version; /** * Replies the API version this dataset was created from. May be null. * * @return the API version this dataset was created from. May be null. */ public String getVersion() { return version; } /** * Sets the API version this dataset was created from. * * @param version the API version, i.e. "0.6" */ public void setVersion(String version) { this.version = version; } /** * Determines if upload is being discouraged. * (i.e. this dataset contains private data which should not be uploaded) * @return {@code true} if upload is being discouraged, {@code false} otherwise * @see #setUploadDiscouraged * @deprecated use {@link #getUploadPolicy()} */ @Deprecated public boolean isUploadDiscouraged() { return uploadPolicy == UploadPolicy.DISCOURAGED || uploadPolicy == UploadPolicy.BLOCKED; } /** * Sets the "upload discouraged" flag. * @param uploadDiscouraged {@code true} if this dataset contains private data which should not be uploaded * @see #isUploadDiscouraged * @deprecated use {@link #setUploadPolicy(UploadPolicy)} */ @Deprecated public void setUploadDiscouraged(boolean uploadDiscouraged) { if (uploadPolicy != UploadPolicy.BLOCKED) { this.uploadPolicy = uploadDiscouraged ? UploadPolicy.DISCOURAGED : UploadPolicy.NORMAL; } } /** * Get the upload policy. * @return the upload policy * @see #setUploadPolicy(UploadPolicy) */ public UploadPolicy getUploadPolicy() { return this.uploadPolicy; } /** * Sets the upload policy. * @param uploadPolicy the upload policy * @see #getUploadPolicy() */ public void setUploadPolicy(UploadPolicy uploadPolicy) { this.uploadPolicy = uploadPolicy; } /** * Holding bin for changeset tag information, to be applied when or if this is ever uploaded. */ private final Map<String, String> changeSetTags = new HashMap<>(); /** * Replies the set of changeset tags to be applied when or if this is ever uploaded. * @return the set of changeset tags * @see #addChangeSetTag */ public Map<String, String> getChangeSetTags() { return changeSetTags; } /** * Adds a new changeset tag. * @param k Key * @param v Value * @see #getChangeSetTags */ public void addChangeSetTag(String k, String v) { this.changeSetTags.put(k, v); } /** * All nodes goes here, even when included in other data (ways etc). This enables the instant * conversion of the whole DataSet by iterating over this data structure. */ private final QuadBuckets<Node> nodes = new QuadBuckets<>(); /** * Gets a filtered collection of primitives matching the given predicate. * @param <T> The primitive type. * @param predicate The predicate to match * @return The list of primtives. * @since 10590 */ public <T extends OsmPrimitive> Collection<T> getPrimitives(Predicate<? super OsmPrimitive> predicate) { return new SubclassFilteredCollection<>(allPrimitives, predicate); } /** * Replies an unmodifiable collection of nodes in this dataset * * @return an unmodifiable collection of nodes in this dataset */ public Collection<Node> getNodes() { return getPrimitives(Node.class::isInstance); } /** * Searches for nodes in the given bounding box. * @param bbox the bounding box * @return List of nodes in the given bbox. Can be empty but not null */ public List<Node> searchNodes(BBox bbox) { lock.readLock().lock(); try { return nodes.search(bbox); } finally { lock.readLock().unlock(); } } /** * Determines if the given node can be retrieved in the data set through its bounding box. Useful for dataset consistency test. * For efficiency reasons this method does not lock the dataset, you have to lock it manually. * * @param n The node to search * @return {@code true} if {@code n} ban be retrieved in this data set, {@code false} otherwise * @since 7501 */ public boolean containsNode(Node n) { return nodes.contains(n); } /** * All ways (Streets etc.) in the DataSet. * * The way nodes are stored only in the way list. */ private final QuadBuckets<Way> ways = new QuadBuckets<>(); /** * Replies an unmodifiable collection of ways in this dataset * * @return an unmodifiable collection of ways in this dataset */ public Collection<Way> getWays() { return getPrimitives(Way.class::isInstance); } /** * Searches for ways in the given bounding box. * @param bbox the bounding box * @return List of ways in the given bbox. Can be empty but not null */ public List<Way> searchWays(BBox bbox) { lock.readLock().lock(); try { return ways.search(bbox); } finally { lock.readLock().unlock(); } } /** * Determines if the given way can be retrieved in the data set through its bounding box. Useful for dataset consistency test. * For efficiency reasons this method does not lock the dataset, you have to lock it manually. * * @param w The way to search * @return {@code true} if {@code w} ban be retrieved in this data set, {@code false} otherwise * @since 7501 */ public boolean containsWay(Way w) { return ways.contains(w); } /** * All relations/relationships */ private final Collection<Relation> relations = new ArrayList<>(); /** * Replies an unmodifiable collection of relations in this dataset * * @return an unmodifiable collection of relations in this dataset */ public Collection<Relation> getRelations() { return getPrimitives(Relation.class::isInstance); } /** * Searches for relations in the given bounding box. * @param bbox the bounding box * @return List of relations in the given bbox. Can be empty but not null */ public List<Relation> searchRelations(BBox bbox) { lock.readLock().lock(); try { // QuadBuckets might be useful here (don't forget to do reindexing after some of rm is changed) return relations.stream() .filter(r -> r.getBBox().intersects(bbox)) .collect(Collectors.toList()); } finally { lock.readLock().unlock(); } } /** * Determines if the given relation can be retrieved in the data set through its bounding box. Useful for dataset consistency test. * For efficiency reasons this method does not lock the dataset, you have to lock it manually. * * @param r The relation to search * @return {@code true} if {@code r} ban be retrieved in this data set, {@code false} otherwise * @since 7501 */ public boolean containsRelation(Relation r) { return relations.contains(r); } /** * Returns a collection containing all primitives of the dataset. * @return A collection containing all primitives of the dataset. Data is not ordered */ public Collection<OsmPrimitive> allPrimitives() { return getPrimitives(o -> true); } /** * Returns a collection containing all not-deleted primitives. * @return A collection containing all not-deleted primitives. * @see OsmPrimitive#isDeleted */ public Collection<OsmPrimitive> allNonDeletedPrimitives() { return getPrimitives(p -> !p.isDeleted()); } /** * Returns a collection containing all not-deleted complete primitives. * @return A collection containing all not-deleted complete primitives. * @see OsmPrimitive#isDeleted * @see OsmPrimitive#isIncomplete */ public Collection<OsmPrimitive> allNonDeletedCompletePrimitives() { return getPrimitives(primitive -> !primitive.isDeleted() && !primitive.isIncomplete()); } /** * Returns a collection containing all not-deleted complete physical primitives. * @return A collection containing all not-deleted complete physical primitives (nodes and ways). * @see OsmPrimitive#isDeleted * @see OsmPrimitive#isIncomplete */ public Collection<OsmPrimitive> allNonDeletedPhysicalPrimitives() { return getPrimitives(primitive -> !primitive.isDeleted() && !primitive.isIncomplete() && !(primitive instanceof Relation)); } /** * Returns a collection containing all modified primitives. * @return A collection containing all modified primitives. * @see OsmPrimitive#isModified */ public Collection<OsmPrimitive> allModifiedPrimitives() { return getPrimitives(OsmPrimitive::isModified); } /** * Adds a primitive to the dataset. * * @param primitive the primitive. */ public void addPrimitive(OsmPrimitive primitive) { Objects.requireNonNull(primitive, "primitive"); beginUpdate(); try { if (getPrimitiveById(primitive) != null) throw new DataIntegrityProblemException( tr("Unable to add primitive {0} to the dataset because it is already included", primitive.toString())); allPrimitives.add(primitive); primitive.setDataset(this); primitive.updatePosition(); // Set cached bbox for way and relation (required for reindexWay and reindexRelation to work properly) boolean success = false; if (primitive instanceof Node) { success = nodes.add((Node) primitive); } else if (primitive instanceof Way) { success = ways.add((Way) primitive); } else if (primitive instanceof Relation) { success = relations.add((Relation) primitive); } if (!success) throw new JosmRuntimeException("failed to add primitive: "+primitive); firePrimitivesAdded(Collections.singletonList(primitive), false); } finally { endUpdate(); } } /** * Removes a primitive from the dataset. This method only removes the * primitive form the respective collection of primitives managed * by this dataset, i.e. from {@link #nodes}, {@link #ways}, or * {@link #relations}. References from other primitives to this * primitive are left unchanged. * * @param primitiveId the id of the primitive */ public void removePrimitive(PrimitiveId primitiveId) { beginUpdate(); try { OsmPrimitive primitive = getPrimitiveByIdChecked(primitiveId); if (primitive == null) return; boolean success = false; if (primitive instanceof Node) { success = nodes.remove(primitive); } else if (primitive instanceof Way) { success = ways.remove(primitive); } else if (primitive instanceof Relation) { success = relations.remove(primitive); } if (!success) throw new JosmRuntimeException("failed to remove primitive: "+primitive); synchronized (selectionLock) { selectedPrimitives.remove(primitive); selectionSnapshot = null; } allPrimitives.remove(primitive); primitive.setDataset(null); firePrimitivesRemoved(Collections.singletonList(primitive), false); } finally { endUpdate(); } } /*--------------------------------------------------- * SELECTION HANDLING *---------------------------------------------------*/ /** * A list of listeners to selection changed events. The list is static, as listeners register * themselves for any dataset selection changes that occur, regardless of the current active * dataset. (However, the selection does only change in the active layer) */ private static final Collection<SelectionChangedListener> selListeners = new CopyOnWriteArrayList<>(); /** * Adds a new selection listener. * @param listener The selection listener to add */ public static void addSelectionListener(SelectionChangedListener listener) { ((CopyOnWriteArrayList<SelectionChangedListener>) selListeners).addIfAbsent(listener); } /** * Removes a selection listener. * @param listener The selection listener to remove */ public static void removeSelectionListener(SelectionChangedListener listener) { selListeners.remove(listener); } /** * Notifies all registered {@link SelectionChangedListener} about the current selection in * this dataset. * */ public void fireSelectionChanged() { Collection<? extends OsmPrimitive> currentSelection = getAllSelected(); for (SelectionChangedListener l : selListeners) { l.selectionChanged(currentSelection); } } private Set<OsmPrimitive> selectedPrimitives = new LinkedHashSet<>(); private Collection<OsmPrimitive> selectionSnapshot; /** * Returns selected nodes and ways. * @return selected nodes and ways */ public Collection<OsmPrimitive> getSelectedNodesAndWays() { return new SubclassFilteredCollection<>(getSelected(), primitive -> primitive instanceof Node || primitive instanceof Way); } /** * Returns an unmodifiable collection of *WaySegments* whose virtual * nodes should be highlighted. WaySegments are used to avoid having * to create a VirtualNode class that wouldn't have much purpose otherwise. * * @return unmodifiable collection of WaySegments */ public Collection<WaySegment> getHighlightedVirtualNodes() { return Collections.unmodifiableCollection(highlightedVirtualNodes); } /** * Returns an unmodifiable collection of WaySegments that should be highlighted. * * @return unmodifiable collection of WaySegments */ public Collection<WaySegment> getHighlightedWaySegments() { return Collections.unmodifiableCollection(highlightedWaySegments); } /** * Replies an unmodifiable collection of primitives currently selected * in this dataset, except deleted ones. May be empty, but not null. * * @return unmodifiable collection of primitives */ public Collection<OsmPrimitive> getSelected() { return new SubclassFilteredCollection<>(getAllSelected(), p -> !p.isDeleted()); } /** * Replies an unmodifiable collection of primitives currently selected * in this dataset, including deleted ones. May be empty, but not null. * * @return unmodifiable collection of primitives */ public Collection<OsmPrimitive> getAllSelected() { Collection<OsmPrimitive> currentList; synchronized (selectionLock) { if (selectionSnapshot == null) { selectionSnapshot = Collections.unmodifiableList(new ArrayList<>(selectedPrimitives)); } currentList = selectionSnapshot; } return currentList; } /** * Returns selected nodes. * @return selected nodes */ public Collection<Node> getSelectedNodes() { return new SubclassFilteredCollection<>(getSelected(), Node.class::isInstance); } /** * Returns selected ways. * @return selected ways */ public Collection<Way> getSelectedWays() { return new SubclassFilteredCollection<>(getSelected(), Way.class::isInstance); } /** * Returns selected relations. * @return selected relations */ public Collection<Relation> getSelectedRelations() { return new SubclassFilteredCollection<>(getSelected(), Relation.class::isInstance); } /** * Determines whether the selection is empty or not * @return whether the selection is empty or not */ public boolean selectionEmpty() { return selectedPrimitives.isEmpty(); } /** * Determines whether the given primitive is selected or not * @param osm the primitive * @return whether {@code osm} is selected or not */ public boolean isSelected(OsmPrimitive osm) { return selectedPrimitives.contains(osm); } /** * Toggles the selected state of the given collection of primitives. * @param osm The primitives to toggle */ public void toggleSelected(Collection<? extends PrimitiveId> osm) { boolean changed = false; synchronized (selectionLock) { for (PrimitiveId o : osm) { changed = changed | this.dotoggleSelected(o); } if (changed) { selectionSnapshot = null; } } if (changed) { fireSelectionChanged(); } } /** * Toggles the selected state of the given collection of primitives. * @param osm The primitives to toggle */ public void toggleSelected(PrimitiveId... osm) { toggleSelected(Arrays.asList(osm)); } private boolean dotoggleSelected(PrimitiveId primitiveId) { OsmPrimitive primitive = getPrimitiveByIdChecked(primitiveId); if (primitive == null) return false; if (!selectedPrimitives.remove(primitive)) { selectedPrimitives.add(primitive); } selectionSnapshot = null; return true; } /** * set what virtual nodes should be highlighted. Requires a Collection of * *WaySegments* to avoid a VirtualNode class that wouldn't have much use * otherwise. * @param waySegments Collection of way segments */ public void setHighlightedVirtualNodes(Collection<WaySegment> waySegments) { if (highlightedVirtualNodes.isEmpty() && waySegments.isEmpty()) return; highlightedVirtualNodes = waySegments; fireHighlightingChanged(); } /** * set what virtual ways should be highlighted. * @param waySegments Collection of way segments */ public void setHighlightedWaySegments(Collection<WaySegment> waySegments) { if (highlightedWaySegments.isEmpty() && waySegments.isEmpty()) return; highlightedWaySegments = waySegments; fireHighlightingChanged(); } /** * Sets the current selection to the primitives in <code>selection</code>. * Notifies all {@link SelectionChangedListener} if <code>fireSelectionChangeEvent</code> is true. * * @param selection the selection * @param fireSelectionChangeEvent true, if the selection change listeners are to be notified; false, otherwise */ public void setSelected(Collection<? extends PrimitiveId> selection, boolean fireSelectionChangeEvent) { boolean changed; synchronized (selectionLock) { Set<OsmPrimitive> oldSelection = new LinkedHashSet<>(selectedPrimitives); selectedPrimitives = new LinkedHashSet<>(); addSelected(selection, false); changed = !oldSelection.equals(selectedPrimitives); if (changed) { selectionSnapshot = null; } } if (changed && fireSelectionChangeEvent) { // If selection is not empty then event was already fired in addSelecteds fireSelectionChanged(); } } /** * Sets the current selection to the primitives in <code>selection</code> * and notifies all {@link SelectionChangedListener}. * * @param selection the selection */ public void setSelected(Collection<? extends PrimitiveId> selection) { setSelected(selection, true /* fire selection change event */); } /** * Sets the current selection to the primitives in <code>osm</code> * and notifies all {@link SelectionChangedListener}. * * @param osm the primitives to set */ public void setSelected(PrimitiveId... osm) { if (osm.length == 1 && osm[0] == null) { setSelected(); return; } List<PrimitiveId> list = Arrays.asList(osm); setSelected(list); } /** * Adds the primitives in <code>selection</code> to the current selection * and notifies all {@link SelectionChangedListener}. * * @param selection the selection */ public void addSelected(Collection<? extends PrimitiveId> selection) { addSelected(selection, true /* fire selection change event */); } /** * Adds the primitives in <code>osm</code> to the current selection * and notifies all {@link SelectionChangedListener}. * * @param osm the primitives to add */ public void addSelected(PrimitiveId... osm) { addSelected(Arrays.asList(osm)); } /** * Adds the primitives in <code>selection</code> to the current selection. * Notifies all {@link SelectionChangedListener} if <code>fireSelectionChangeEvent</code> is true. * * @param selection the selection * @param fireSelectionChangeEvent true, if the selection change listeners are to be notified; false, otherwise * @return if the selection was changed in the process */ private boolean addSelected(Collection<? extends PrimitiveId> selection, boolean fireSelectionChangeEvent) { boolean changed = false; synchronized (selectionLock) { for (PrimitiveId id: selection) { OsmPrimitive primitive = getPrimitiveByIdChecked(id); if (primitive != null) { changed = changed | selectedPrimitives.add(primitive); } } if (changed) { selectionSnapshot = null; } } if (fireSelectionChangeEvent && changed) { fireSelectionChanged(); } return changed; } /** * clear all highlights of virtual nodes */ public void clearHighlightedVirtualNodes() { setHighlightedVirtualNodes(new ArrayList<WaySegment>()); } /** * clear all highlights of way segments */ public void clearHighlightedWaySegments() { setHighlightedWaySegments(new ArrayList<WaySegment>()); } /** * Removes the selection from every value in the collection. * @param osm The collection of ids to remove the selection from. */ public void clearSelection(PrimitiveId... osm) { clearSelection(Arrays.asList(osm)); } /** * Removes the selection from every value in the collection. * @param list The collection of ids to remove the selection from. */ public void clearSelection(Collection<? extends PrimitiveId> list) { boolean changed = false; synchronized (selectionLock) { for (PrimitiveId id:list) { OsmPrimitive primitive = getPrimitiveById(id); if (primitive != null) { changed = changed | selectedPrimitives.remove(primitive); } } if (changed) { selectionSnapshot = null; } } if (changed) { fireSelectionChanged(); } } /** * Clears the current selection. */ public void clearSelection() { if (!selectedPrimitives.isEmpty()) { synchronized (selectionLock) { selectedPrimitives.clear(); selectionSnapshot = null; } fireSelectionChanged(); } } @Override public synchronized Area getDataSourceArea() { if (cachedDataSourceArea == null) { cachedDataSourceArea = Data.super.getDataSourceArea(); } return cachedDataSourceArea; } @Override public synchronized List<Bounds> getDataSourceBounds() { if (cachedDataSourceBounds == null) { cachedDataSourceBounds = Data.super.getDataSourceBounds(); } return Collections.unmodifiableList(cachedDataSourceBounds); } @Override public synchronized Collection<DataSource> getDataSources() { return Collections.unmodifiableCollection(dataSources); } /** * Returns a primitive with a given id from the data set. null, if no such primitive exists * * @param id uniqueId of the primitive. Might be < 0 for newly created primitives * @param type the type of the primitive. Must not be null. * @return the primitive * @throws NullPointerException if type is null */ public OsmPrimitive getPrimitiveById(long id, OsmPrimitiveType type) { return getPrimitiveById(new SimplePrimitiveId(id, type)); } /** * Returns a primitive with a given id from the data set. null, if no such primitive exists * * @param primitiveId type and uniqueId of the primitive. Might be < 0 for newly created primitives * @return the primitive */ public OsmPrimitive getPrimitiveById(PrimitiveId primitiveId) { return primitiveId != null ? primitivesMap.get(primitiveId) : null; } /** * Show message and stack trace in log in case primitive is not found * @param primitiveId primitive id to look for * @return Primitive by id. */ private OsmPrimitive getPrimitiveByIdChecked(PrimitiveId primitiveId) { OsmPrimitive result = getPrimitiveById(primitiveId); if (result == null && primitiveId != null) { Main.warn(tr("JOSM expected to find primitive [{0} {1}] in dataset but it is not there. Please report this " + "at {2}. This is not a critical error, it should be safe to continue in your work.", primitiveId.getType(), Long.toString(primitiveId.getUniqueId()), Main.getJOSMWebsite())); Main.error(new Exception()); } return result; } private static void deleteWay(Way way) { way.setNodes(null); way.setDeleted(true); } /** * Removes all references from ways in this dataset to a particular node. * * @param node the node * @return The set of ways that have been modified */ public Set<Way> unlinkNodeFromWays(Node node) { Set<Way> result = new HashSet<>(); beginUpdate(); try { for (Way way : OsmPrimitive.getFilteredList(node.getReferrers(), Way.class)) { List<Node> wayNodes = way.getNodes(); if (wayNodes.remove(node)) { if (wayNodes.size() < 2) { deleteWay(way); } else { way.setNodes(wayNodes); } result.add(way); } } } finally { endUpdate(); } return result; } /** * removes all references from relations in this dataset to this primitive * * @param primitive the primitive * @return The set of relations that have been modified */ public Set<Relation> unlinkPrimitiveFromRelations(OsmPrimitive primitive) { Set<Relation> result = new HashSet<>(); beginUpdate(); try { for (Relation relation : relations) { List<RelationMember> members = relation.getMembers(); Iterator<RelationMember> it = members.iterator(); boolean removed = false; while (it.hasNext()) { RelationMember member = it.next(); if (member.getMember().equals(primitive)) { it.remove(); removed = true; } } if (removed) { relation.setMembers(members); result.add(relation); } } } finally { endUpdate(); } return result; } /** * Removes all references from other primitives to the referenced primitive. * * @param referencedPrimitive the referenced primitive * @return The set of primitives that have been modified */ public Set<OsmPrimitive> unlinkReferencesToPrimitive(OsmPrimitive referencedPrimitive) { Set<OsmPrimitive> result = new HashSet<>(); beginUpdate(); try { if (referencedPrimitive instanceof Node) { result.addAll(unlinkNodeFromWays((Node) referencedPrimitive)); } result.addAll(unlinkPrimitiveFromRelations(referencedPrimitive)); } finally { endUpdate(); } return result; } /** * Replies true if there is at least one primitive in this dataset with * {@link OsmPrimitive#isModified()} == <code>true</code>. * * @return true if there is at least one primitive in this dataset with * {@link OsmPrimitive#isModified()} == <code>true</code>. */ public boolean isModified() { for (OsmPrimitive p: allPrimitives) { if (p.isModified()) return true; } return false; } private void reindexNode(Node node, LatLon newCoor, EastNorth eastNorth) { if (!nodes.remove(node)) throw new JosmRuntimeException("Reindexing node failed to remove"); node.setCoorInternal(newCoor, eastNorth); if (!nodes.add(node)) throw new JosmRuntimeException("Reindexing node failed to add"); for (OsmPrimitive primitive: node.getReferrers()) { if (primitive instanceof Way) { reindexWay((Way) primitive); } else { reindexRelation((Relation) primitive); } } } private void reindexWay(Way way) { BBox before = way.getBBox(); if (!ways.remove(way)) throw new JosmRuntimeException("Reindexing way failed to remove"); way.updatePosition(); if (!ways.add(way)) throw new JosmRuntimeException("Reindexing way failed to add"); if (!way.getBBox().equals(before)) { for (OsmPrimitive primitive: way.getReferrers()) { reindexRelation((Relation) primitive); } } } private static void reindexRelation(Relation relation) { BBox before = relation.getBBox(); relation.updatePosition(); if (!before.equals(relation.getBBox())) { for (OsmPrimitive primitive: relation.getReferrers()) { reindexRelation((Relation) primitive); } } } /** * Adds a new data set listener. * @param dsl The data set listener to add */ public void addDataSetListener(DataSetListener dsl) { listeners.addIfAbsent(dsl); } /** * Removes a data set listener. * @param dsl The data set listener to remove */ public void removeDataSetListener(DataSetListener dsl) { listeners.remove(dsl); } /** * Can be called before bigger changes on dataset. Events are disabled until {@link #endUpdate()}. * {@link DataSetListener#dataChanged(DataChangedEvent event)} event is triggered after end of changes * <br> * Typical usecase should look like this: * <pre> * ds.beginUpdate(); * try { * ... * } finally { * ds.endUpdate(); * } * </pre> */ public void beginUpdate() { lock.writeLock().lock(); updateCount++; } /** * @see DataSet#beginUpdate() */ public void endUpdate() { if (updateCount > 0) { updateCount--; List<AbstractDatasetChangedEvent> eventsToFire = Collections.emptyList(); if (updateCount == 0) { eventsToFire = new ArrayList<>(cachedEvents); cachedEvents.clear(); } if (!eventsToFire.isEmpty()) { lock.readLock().lock(); lock.writeLock().unlock(); try { if (eventsToFire.size() < MAX_SINGLE_EVENTS) { for (AbstractDatasetChangedEvent event: eventsToFire) { fireEventToListeners(event); } } else if (eventsToFire.size() == MAX_EVENTS) { fireEventToListeners(new DataChangedEvent(this)); } else { fireEventToListeners(new DataChangedEvent(this, eventsToFire)); } } finally { lock.readLock().unlock(); } } else { lock.writeLock().unlock(); } } else throw new AssertionError("endUpdate called without beginUpdate"); } private void fireEventToListeners(AbstractDatasetChangedEvent event) { for (DataSetListener listener: listeners) { event.fire(listener); } } private void fireEvent(AbstractDatasetChangedEvent event) { if (updateCount == 0) throw new AssertionError("dataset events can be fired only when dataset is locked"); if (cachedEvents.size() < MAX_EVENTS) { cachedEvents.add(event); } } void firePrimitivesAdded(Collection<? extends OsmPrimitive> added, boolean wasIncomplete) { fireEvent(new PrimitivesAddedEvent(this, added, wasIncomplete)); } void firePrimitivesRemoved(Collection<? extends OsmPrimitive> removed, boolean wasComplete) { fireEvent(new PrimitivesRemovedEvent(this, removed, wasComplete)); } void fireTagsChanged(OsmPrimitive prim, Map<String, String> originalKeys) { fireEvent(new TagsChangedEvent(this, prim, originalKeys)); } void fireRelationMembersChanged(Relation r) { reindexRelation(r); fireEvent(new RelationMembersChangedEvent(this, r)); } void fireNodeMoved(Node node, LatLon newCoor, EastNorth eastNorth) { reindexNode(node, newCoor, eastNorth); fireEvent(new NodeMovedEvent(this, node)); } void fireWayNodesChanged(Way way) { reindexWay(way); fireEvent(new WayNodesChangedEvent(this, way)); } void fireChangesetIdChanged(OsmPrimitive primitive, int oldChangesetId, int newChangesetId) { fireEvent(new ChangesetIdChangedEvent(this, Collections.singletonList(primitive), oldChangesetId, newChangesetId)); } void firePrimitiveFlagsChanged(OsmPrimitive primitive) { fireEvent(new PrimitiveFlagsChangedEvent(this, primitive)); } void fireHighlightingChanged() { highlightUpdateCount++; } /** * Invalidates the internal cache of projected east/north coordinates. * * This method can be invoked after the globally configured projection method * changed. */ public void invalidateEastNorthCache() { if (Main.getProjection() == null) return; // sanity check try { beginUpdate(); for (Node n: Utils.filteredCollection(allPrimitives, Node.class)) { n.invalidateEastNorthCache(); } } finally { endUpdate(); } } /** * Cleanups all deleted primitives (really delete them from the dataset). */ public void cleanupDeletedPrimitives() { beginUpdate(); try { boolean changed = cleanupDeleted(nodes.iterator()); if (cleanupDeleted(ways.iterator())) { changed = true; } if (cleanupDeleted(relations.iterator())) { changed = true; } if (changed) { fireSelectionChanged(); } } finally { endUpdate(); } } private boolean cleanupDeleted(Iterator<? extends OsmPrimitive> it) { boolean changed = false; synchronized (selectionLock) { while (it.hasNext()) { OsmPrimitive primitive = it.next(); if (primitive.isDeleted() && (!primitive.isVisible() || primitive.isNew())) { selectedPrimitives.remove(primitive); selectionSnapshot = null; allPrimitives.remove(primitive); primitive.setDataset(null); changed = true; it.remove(); } } if (changed) { selectionSnapshot = null; } } return changed; } /** * Removes all primitives from the dataset and resets the currently selected primitives * to the empty collection. Also notifies selection change listeners if necessary. * */ public void clear() { beginUpdate(); try { clearSelection(); for (OsmPrimitive primitive:allPrimitives) { primitive.setDataset(null); } nodes.clear(); ways.clear(); relations.clear(); allPrimitives.clear(); } finally { endUpdate(); } } /** * Marks all "invisible" objects as deleted. These objects should be always marked as * deleted when downloaded from the server. They can be undeleted later if necessary. * */ public void deleteInvisible() { for (OsmPrimitive primitive:allPrimitives) { if (!primitive.isVisible()) { primitive.setDeleted(true); } } } /** * Moves all primitives and datasources from DataSet "from" to this DataSet. * @param from The source DataSet */ public void mergeFrom(DataSet from) { mergeFrom(from, null); } /** * Moves all primitives and datasources from DataSet "from" to this DataSet. * @param from The source DataSet * @param progressMonitor The progress monitor */ public synchronized void mergeFrom(DataSet from, ProgressMonitor progressMonitor) { if (from != null) { new DataSetMerger(this, from).merge(progressMonitor); synchronized (from) { if (!from.dataSources.isEmpty()) { if (dataSources.addAll(from.dataSources)) { cachedDataSourceArea = null; cachedDataSourceBounds = null; } from.dataSources.clear(); from.cachedDataSourceArea = null; from.cachedDataSourceBounds = null; } } } } /* --------------------------------------------------------------------------------- */ /* interface ProjectionChangeListner */ /* --------------------------------------------------------------------------------- */ @Override public void projectionChanged(Projection oldValue, Projection newValue) { invalidateEastNorthCache(); } /** * Returns the data sources bounding box. * @return the data sources bounding box */ public synchronized ProjectionBounds getDataSourceBoundingBox() { BoundingXYVisitor bbox = new BoundingXYVisitor(); for (DataSource source : dataSources) { bbox.visit(source.bounds); } if (bbox.hasExtend()) { return bbox.getBounds(); } return null; } }