waySegments) {
if (highlightedWaySegments.isEmpty() && waySegments.isEmpty())
return;
highlightedWaySegments = waySegments;
fireHighlightingChanged();
}
@Override
public void setSelected(Collection extends PrimitiveId> selection) {
setSelected(selection.stream());
}
@Override
public void setSelected(PrimitiveId... osm) {
setSelected(Stream.of(osm).filter(Objects::nonNull));
}
private void setSelected(Stream extends PrimitiveId> stream) {
doSelectionChange(old -> new SelectionReplaceEvent(this, old,
stream.map(this::getPrimitiveByIdChecked).filter(Objects::nonNull)));
}
@Override
public void addSelected(Collection extends PrimitiveId> selection) {
addSelected(selection.stream());
}
@Override
public void addSelected(PrimitiveId... osm) {
addSelected(Stream.of(osm));
}
private void addSelected(Stream extends PrimitiveId> stream) {
doSelectionChange(old -> new SelectionAddEvent(this, old,
stream.map(this::getPrimitiveByIdChecked).filter(Objects::nonNull)));
}
@Override
public void clearSelection(PrimitiveId... osm) {
clearSelection(Stream.of(osm));
}
@Override
public void clearSelection(Collection extends PrimitiveId> list) {
clearSelection(list.stream());
}
@Override
public void clearSelection() {
setSelected(Stream.empty());
}
private void clearSelection(Stream extends PrimitiveId> stream) {
doSelectionChange(old -> new SelectionRemoveEvent(this, old,
stream.map(this::getPrimitiveByIdChecked).filter(Objects::nonNull)));
}
@Override
public void toggleSelected(Collection extends PrimitiveId> osm) {
toggleSelected(osm.stream());
}
@Override
public void toggleSelected(PrimitiveId... osm) {
toggleSelected(Stream.of(osm));
}
private void toggleSelected(Stream extends PrimitiveId> stream) {
doSelectionChange(old -> new SelectionToggleEvent(this, old,
stream.map(this::getPrimitiveByIdChecked).filter(Objects::nonNull)));
}
/**
* Do a selection change.
*
* This is the only method that changes the current selection state.
* @param command A generator that generates the {@link SelectionChangeEvent} for the given base set of currently selected primitives.
* @return true iff the command did change the selection.
* @since 12048
*/
private boolean doSelectionChange(Function, SelectionChangeEvent> command) {
synchronized (selectionLock) {
SelectionChangeEvent event = command.apply(currentSelectedPrimitives);
if (event.isNop()) {
return false;
}
currentSelectedPrimitives = event.getSelection();
selectionListeners.fireEvent(l -> l.selectionChanged(event));
return true;
}
}
@Override
public synchronized Area getDataSourceArea() {
if (cachedDataSourceArea == null) {
cachedDataSourceArea = OsmData.super.getDataSourceArea();
}
return cachedDataSourceArea;
}
@Override
public synchronized List getDataSourceBounds() {
if (cachedDataSourceBounds == null) {
cachedDataSourceBounds = OsmData.super.getDataSourceBounds();
}
return Collections.unmodifiableList(cachedDataSourceBounds);
}
@Override
public synchronized Collection getDataSources() {
return Collections.unmodifiableCollection(dataSources);
}
@Override
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) {
Logging.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()), Config.getUrls().getJOSMWebsite()));
Logging.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
* @throws IllegalStateException if the dataset is read-only
*/
public Set unlinkNodeFromWays(Node node) {
checkModifiable();
Set result = new HashSet<>();
beginUpdate();
try {
for (Way way : node.getParentWays()) {
List 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
* @throws IllegalStateException if the dataset is read-only
*/
public Set unlinkPrimitiveFromRelations(OsmPrimitive primitive) {
checkModifiable();
Set result = new HashSet<>();
beginUpdate();
try {
for (Relation relation : getRelations()) {
List members = relation.getMembers();
Iterator 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
* @throws IllegalStateException if the dataset is read-only
*/
public Set unlinkReferencesToPrimitive(OsmPrimitive referencedPrimitive) {
checkModifiable();
Set result = new HashSet<>();
beginUpdate();
try {
if (referencedPrimitive instanceof Node) {
result.addAll(unlinkNodeFromWays((Node) referencedPrimitive));
}
result.addAll(unlinkPrimitiveFromRelations(referencedPrimitive));
} finally {
endUpdate();
}
return result;
}
@Override
public boolean isModified() {
for (OsmPrimitive p : allPrimitives) {
if (p.isModified())
return true;
}
return false;
}
/**
* Replies true if there is at least one primitive in this dataset which requires to be uploaded to server.
* @return true if there is at least one primitive in this dataset which requires to be uploaded to server
* @since 13161
*/
public boolean requiresUploadToServer() {
for (OsmPrimitive p : allPrimitives) {
if (APIOperation.of(p) != null)
return true;
}
return false;
}
/**
* 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
*
* Typical usecase should look like this:
*
* ds.beginUpdate();
* try {
* ...
* } finally {
* ds.endUpdate();
* }
*
* @see #endUpdate()
*/
public void beginUpdate() {
lock.writeLock().lock();
updateCount++;
}
/**
* Must be called after a previous call to {@link #beginUpdate()} to fire change events.
*
* Typical usecase should look like this:
*
* ds.beginUpdate();
* try {
* ...
* } finally {
* ds.endUpdate();
* }
*
* @see DataSet#beginUpdate()
*/
public void endUpdate() {
if (updateCount > 0) {
updateCount--;
List 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 originalKeys) {
fireEvent(new TagsChangedEvent(this, prim, originalKeys));
}
void fireRelationMembersChanged(Relation r) {
store.reindexRelation(r, Relation::updatePosition);
fireEvent(new RelationMembersChangedEvent(this, r));
}
void fireNodeMoved(Node node, LatLon newCoor, EastNorth eastNorth) {
store.reindexNode(node, n -> n.setCoorInternal(newCoor, eastNorth), Way::updatePosition, Relation::updatePosition);
fireEvent(new NodeMovedEvent(this, node));
}
void fireWayNodesChanged(Way way) {
if (way.getNodesCount() > 0) {
store.reindexWay(way, Way::updatePosition, Relation::updatePosition);
}
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 fireFilterChanged() {
fireEvent(new DataChangedEvent(this));
}
void fireHighlightingChanged() {
HighlightUpdateListener.HighlightUpdateEvent e = new HighlightUpdateListener.HighlightUpdateEvent(this);
highlightUpdateListeners.fireEvent(l -> l.highlightUpdated(e));
}
/**
* 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 (ProjectionRegistry.getProjection() == null)
return; // sanity check
beginUpdate();
try {
for (Node n : getNodes()) {
n.invalidateEastNorthCache();
}
} finally {
endUpdate();
}
}
/**
* Cleanups all deleted primitives (really delete them from the dataset).
*/
public void cleanupDeletedPrimitives() {
beginUpdate();
try {
Collection toCleanUp = getPrimitives(
primitive -> primitive.isDeleted() && (!primitive.isVisible() || primitive.isNew()));
if (!toCleanUp.isEmpty()) {
// We unselect them in advance to not fire a selection change for every primitive
clearSelection(toCleanUp.stream().map(OsmPrimitive::getPrimitiveId));
for (OsmPrimitive primitive : toCleanUp) {
removePrimitiveImpl(primitive);
}
firePrimitivesRemoved(toCleanUp, false);
}
} finally {
endUpdate();
}
}
/**
* Removes all primitives from the dataset and resets the currently selected primitives
* to the empty collection. Also notifies selection change listeners if necessary.
* @throws IllegalStateException if the dataset is read-only
*/
@Override
public void clear() {
checkModifiable();
beginUpdate();
try {
clearSelection();
for (OsmPrimitive primitive : allPrimitives) {
primitive.setDataset(null);
}
store.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.
* @throws IllegalStateException if the dataset is read-only
*/
public void deleteInvisible() {
checkModifiable();
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
* @throws IllegalStateException if the dataset is read-only
*/
public synchronized void mergeFrom(DataSet from, ProgressMonitor progressMonitor) {
if (from != null) {
checkModifiable();
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;
}
}
}
}
/**
* Replies the set of conflicts currently managed in this layer.
*
* @return the set of conflicts currently managed in this layer
* @since 12672
*/
public ConflictCollection getConflicts() {
return conflicts;
}
@Override
public String getName() {
return name;
}
@Override
public void setName(String name) {
this.name = name;
}
/* --------------------------------------------------------------------------------- */
/* interface ProjectionChangeListner */
/* --------------------------------------------------------------------------------- */
@Override
public void projectionChanged(Projection oldValue, Projection newValue) {
invalidateEastNorthCache();
}
@Override
public synchronized ProjectionBounds getDataSourceBoundingBox() {
BoundingXYVisitor bbox = new BoundingXYVisitor();
for (DataSource source : dataSources) {
bbox.visit(source.bounds);
}
if (bbox.hasExtend()) {
return bbox.getBounds();
}
return null;
}
/**
* Returns mappaint cache index for this DataSet.
*
* If the {@link OsmPrimitive#mappaintCacheIdx} is not equal to the DataSet mappaint
* cache index, this means the cache for that primitive is out of date.
* @return mappaint cache index
* @since 13420
*/
public short getMappaintCacheIndex() {
return mappaintCacheIdx;
}
@Override
public void clearMappaintCache() {
mappaintCacheIdx++;
}
@Override
public void lock() {
if (!isReadOnly.compareAndSet(false, true)) {
Logging.warn("Trying to set readOnly flag on a readOnly dataset ", getName());
}
}
@Override
public void unlock() {
if (!isReadOnly.compareAndSet(true, false)) {
Logging.warn("Trying to unset readOnly flag on a non-readOnly dataset ", getName());
}
}
@Override
public boolean isLocked() {
return isReadOnly.get();
}
/**
* Checks the dataset is modifiable (not read-only).
* @throws IllegalStateException if the dataset is read-only
*/
private void checkModifiable() {
if (isLocked()) {
throw new IllegalStateException("DataSet is read-only");
}
}
}