source: josm/trunk/src/org/openstreetmap/josm/command/DeleteCommand.java@ 15874

Last change on this file since 15874 was 15874, checked in by GerdP, 4 years ago

see #18728: Join overlapping Areas is slow when combining many complex ways
Improve performance of DeleteCommand.delete() when thousands of ways are removed

Don't calculate set of nodes inside the loop which adds delete commands for the ways, the set doesn't change in this loop.

  • Property svn:eol-style set to native
File size: 21.0 KB
Line 
1// License: GPL. For details, see LICENSE file.
2package org.openstreetmap.josm.command;
3
4import static org.openstreetmap.josm.tools.I18n.marktr;
5import static org.openstreetmap.josm.tools.I18n.tr;
6import static org.openstreetmap.josm.tools.I18n.trn;
7
8import java.util.ArrayList;
9import java.util.Arrays;
10import java.util.Collection;
11import java.util.Collections;
12import java.util.EnumSet;
13import java.util.HashMap;
14import java.util.HashSet;
15import java.util.LinkedList;
16import java.util.List;
17import java.util.Map;
18import java.util.Map.Entry;
19import java.util.Objects;
20import java.util.Set;
21import java.util.stream.Collectors;
22
23import javax.swing.Icon;
24
25import org.openstreetmap.josm.data.osm.DataSet;
26import org.openstreetmap.josm.data.osm.DefaultNameFormatter;
27import org.openstreetmap.josm.data.osm.Node;
28import org.openstreetmap.josm.data.osm.OsmPrimitive;
29import org.openstreetmap.josm.data.osm.OsmPrimitiveType;
30import org.openstreetmap.josm.data.osm.PrimitiveData;
31import org.openstreetmap.josm.data.osm.Relation;
32import org.openstreetmap.josm.data.osm.RelationToChildReference;
33import org.openstreetmap.josm.data.osm.Way;
34import org.openstreetmap.josm.data.osm.WaySegment;
35import org.openstreetmap.josm.tools.CheckParameterUtil;
36import org.openstreetmap.josm.tools.ImageProvider;
37import org.openstreetmap.josm.tools.Utils;
38
39/**
40 * A command to delete a number of primitives from the dataset.
41 * To be used correctly, this class requires an initial call to {@link #setDeletionCallback(DeletionCallback)} to
42 * allow interactive confirmation actions.
43 * @since 23
44 */
45public class DeleteCommand extends Command {
46 private static final class DeleteChildCommand implements PseudoCommand {
47 private final OsmPrimitive osm;
48
49 private DeleteChildCommand(OsmPrimitive osm) {
50 this.osm = osm;
51 }
52
53 @Override
54 public String getDescriptionText() {
55 return tr("Deleted ''{0}''", osm.getDisplayName(DefaultNameFormatter.getInstance()));
56 }
57
58 @Override
59 public Icon getDescriptionIcon() {
60 return ImageProvider.get(osm.getDisplayType());
61 }
62
63 @Override
64 public Collection<? extends OsmPrimitive> getParticipatingPrimitives() {
65 return Collections.singleton(osm);
66 }
67
68 @Override
69 public String toString() {
70 return "DeleteChildCommand [osm=" + osm + ']';
71 }
72 }
73
74 /**
75 * Called when a deletion operation must be checked and confirmed by user.
76 * @since 12749
77 */
78 public interface DeletionCallback {
79 /**
80 * Check whether user is about to delete data outside of the download area.
81 * Request confirmation if he is.
82 * @param primitives the primitives to operate on
83 * @param ignore {@code null} or a primitive to be ignored
84 * @return true, if operating on outlying primitives is OK; false, otherwise
85 */
86 boolean checkAndConfirmOutlyingDelete(Collection<? extends OsmPrimitive> primitives, Collection<? extends OsmPrimitive> ignore);
87
88 /**
89 * Confirm before deleting a relation, as it is a common newbie error.
90 * @param relations relation to check for deletion
91 * @return {@code true} if user confirms the deletion
92 * @since 12760
93 */
94 boolean confirmRelationDeletion(Collection<Relation> relations);
95
96 /**
97 * Confirm before removing a collection of primitives from their parent relations.
98 * @param references the list of relation-to-child references
99 * @return {@code true} if user confirms the deletion
100 * @since 12763
101 */
102 boolean confirmDeletionFromRelation(Collection<RelationToChildReference> references);
103 }
104
105 private static volatile DeletionCallback callback;
106
107 /**
108 * Sets the global {@link DeletionCallback}.
109 * @param deletionCallback the new {@code DeletionCallback}. Must not be null
110 * @throws NullPointerException if {@code deletionCallback} is null
111 * @since 12749
112 */
113 public static void setDeletionCallback(DeletionCallback deletionCallback) {
114 callback = Objects.requireNonNull(deletionCallback);
115 }
116
117 /**
118 * The primitives that get deleted.
119 */
120 private final Collection<? extends OsmPrimitive> toDelete;
121 private final Map<OsmPrimitive, PrimitiveData> clonedPrimitives = new HashMap<>();
122
123 /**
124 * Constructor. Deletes a collection of primitives in the current edit layer.
125 *
126 * @param data the primitives to delete. Must neither be null nor empty, and belong to a data set
127 * @throws IllegalArgumentException if data is null or empty
128 */
129 public DeleteCommand(Collection<? extends OsmPrimitive> data) {
130 this(data.iterator().next().getDataSet(), data);
131 }
132
133 /**
134 * Constructor. Deletes a single primitive in the current edit layer.
135 *
136 * @param data the primitive to delete. Must not be null.
137 * @throws IllegalArgumentException if data is null
138 */
139 public DeleteCommand(OsmPrimitive data) {
140 this(Collections.singleton(data));
141 }
142
143 /**
144 * Constructor for a single data item. Use the collection constructor to delete multiple objects.
145 *
146 * @param dataset the data set context for deleting this primitive. Must not be null.
147 * @param data the primitive to delete. Must not be null.
148 * @throws IllegalArgumentException if data is null
149 * @throws IllegalArgumentException if layer is null
150 * @since 12718
151 */
152 public DeleteCommand(DataSet dataset, OsmPrimitive data) {
153 this(dataset, Collections.singleton(data));
154 }
155
156 /**
157 * Constructor for a collection of data to be deleted in the context of a specific data set
158 *
159 * @param dataset the dataset context for deleting these primitives. Must not be null.
160 * @param data the primitives to delete. Must neither be null nor empty.
161 * @throws IllegalArgumentException if dataset is null
162 * @throws IllegalArgumentException if data is null or empty
163 * @since 11240
164 */
165 public DeleteCommand(DataSet dataset, Collection<? extends OsmPrimitive> data) {
166 super(dataset);
167 CheckParameterUtil.ensureParameterNotNull(data, "data");
168 this.toDelete = data;
169 checkConsistency();
170 }
171
172 private void checkConsistency() {
173 if (toDelete.isEmpty()) {
174 throw new IllegalArgumentException(tr("At least one object to delete required, got empty collection"));
175 }
176 for (OsmPrimitive p : toDelete) {
177 if (p == null) {
178 throw new IllegalArgumentException("Primitive to delete must not be null");
179 } else if (p.getDataSet() == null) {
180 throw new IllegalArgumentException("Primitive to delete must be in a dataset");
181 }
182 }
183 }
184
185 @Override
186 public boolean executeCommand() {
187 ensurePrimitivesAreInDataset();
188 // Make copy and remove all references (to prevent inconsistent dataset (delete referenced) while command is executed)
189 for (OsmPrimitive osm: toDelete) {
190 if (osm.isDeleted())
191 throw new IllegalArgumentException(osm + " is already deleted");
192 clonedPrimitives.put(osm, osm.save());
193
194 if (osm instanceof Way) {
195 ((Way) osm).setNodes(null);
196 } else if (osm instanceof Relation) {
197 ((Relation) osm).setMembers(null);
198 }
199 }
200
201 for (OsmPrimitive osm: toDelete) {
202 osm.setDeleted(true);
203 }
204
205 return true;
206 }
207
208 @Override
209 public void undoCommand() {
210 ensurePrimitivesAreInDataset();
211
212 for (OsmPrimitive osm: toDelete) {
213 osm.setDeleted(false);
214 }
215
216 for (Entry<OsmPrimitive, PrimitiveData> entry: clonedPrimitives.entrySet()) {
217 entry.getKey().load(entry.getValue());
218 }
219 }
220
221 @Override
222 public void fillModifiedData(Collection<OsmPrimitive> modified, Collection<OsmPrimitive> deleted, Collection<OsmPrimitive> added) {
223 // Do nothing
224 }
225
226 private EnumSet<OsmPrimitiveType> getTypesToDelete() {
227 EnumSet<OsmPrimitiveType> typesToDelete = EnumSet.noneOf(OsmPrimitiveType.class);
228 for (OsmPrimitive osm : toDelete) {
229 typesToDelete.add(OsmPrimitiveType.from(osm));
230 }
231 return typesToDelete;
232 }
233
234 @Override
235 public String getDescriptionText() {
236 if (toDelete.size() == 1) {
237 OsmPrimitive primitive = toDelete.iterator().next();
238 String msg;
239 switch(OsmPrimitiveType.from(primitive)) {
240 case NODE: msg = marktr("Delete node {0}"); break;
241 case WAY: msg = marktr("Delete way {0}"); break;
242 case RELATION:msg = marktr("Delete relation {0}"); break;
243 default: throw new AssertionError();
244 }
245
246 return tr(msg, primitive.getDisplayName(DefaultNameFormatter.getInstance()));
247 } else {
248 Set<OsmPrimitiveType> typesToDelete = getTypesToDelete();
249 String msg;
250 if (typesToDelete.size() > 1) {
251 msg = trn("Delete {0} object", "Delete {0} objects", toDelete.size(), toDelete.size());
252 } else {
253 OsmPrimitiveType t = typesToDelete.iterator().next();
254 switch(t) {
255 case NODE: msg = trn("Delete {0} node", "Delete {0} nodes", toDelete.size(), toDelete.size()); break;
256 case WAY: msg = trn("Delete {0} way", "Delete {0} ways", toDelete.size(), toDelete.size()); break;
257 case RELATION: msg = trn("Delete {0} relation", "Delete {0} relations", toDelete.size(), toDelete.size()); break;
258 default: throw new AssertionError();
259 }
260 }
261 return msg;
262 }
263 }
264
265 @Override
266 public Icon getDescriptionIcon() {
267 if (toDelete.size() == 1)
268 return ImageProvider.get(toDelete.iterator().next().getDisplayType());
269 Set<OsmPrimitiveType> typesToDelete = getTypesToDelete();
270 if (typesToDelete.size() > 1)
271 return ImageProvider.get("data", "object");
272 else
273 return ImageProvider.get(typesToDelete.iterator().next());
274 }
275
276 @Override public Collection<PseudoCommand> getChildren() {
277 if (toDelete.size() == 1)
278 return null;
279 else {
280 List<PseudoCommand> children = new ArrayList<>(toDelete.size());
281 for (final OsmPrimitive osm : toDelete) {
282 children.add(new DeleteChildCommand(osm));
283 }
284 return children;
285
286 }
287 }
288
289 @Override public Collection<? extends OsmPrimitive> getParticipatingPrimitives() {
290 return toDelete;
291 }
292
293 /**
294 * Delete the primitives and everything they reference.
295 *
296 * If a node is deleted, the node and all ways and relations the node is part of are deleted as well.
297 * If a way is deleted, all relations the way is member of are also deleted.
298 * If a way is deleted, only the way and no nodes are deleted.
299 *
300 * @param selection The list of all object to be deleted.
301 * @param silent Set to true if the user should not be bugged with additional dialogs
302 * @return command A command to perform the deletions, or null of there is nothing to delete.
303 * @throws IllegalArgumentException if layer is null
304 * @since 12718
305 */
306 public static Command deleteWithReferences(Collection<? extends OsmPrimitive> selection, boolean silent) {
307 if (selection == null || selection.isEmpty()) return null;
308 Set<OsmPrimitive> parents = OsmPrimitive.getReferrer(selection);
309 parents.addAll(selection);
310
311 if (parents.isEmpty())
312 return null;
313 if (!silent && !callback.checkAndConfirmOutlyingDelete(parents, null))
314 return null;
315 return new DeleteCommand(parents.iterator().next().getDataSet(), parents);
316 }
317
318 /**
319 * Delete the primitives and everything they reference.
320 *
321 * If a node is deleted, the node and all ways and relations the node is part of are deleted as well.
322 * If a way is deleted, all relations the way is member of are also deleted.
323 * If a way is deleted, only the way and no nodes are deleted.
324 *
325 * @param selection The list of all object to be deleted.
326 * @return command A command to perform the deletions, or null of there is nothing to delete.
327 * @throws IllegalArgumentException if layer is null
328 * @since 12718
329 */
330 public static Command deleteWithReferences(Collection<? extends OsmPrimitive> selection) {
331 return deleteWithReferences(selection, false);
332 }
333
334 /**
335 * Try to delete all given primitives.
336 *
337 * If a node is used by a way, it's removed from that way. If a node or a way is used by a
338 * relation, inform the user and do not delete.
339 *
340 * If this would cause ways with less than 2 nodes to be created, delete these ways instead. If
341 * they are part of a relation, inform the user and do not delete.
342 *
343 * @param selection the objects to delete.
344 * @return command a command to perform the deletions, or null if there is nothing to delete.
345 * @since 12718
346 */
347 public static Command delete(Collection<? extends OsmPrimitive> selection) {
348 return delete(selection, true, false);
349 }
350
351 /**
352 * Replies the collection of nodes referred to by primitives in <code>primitivesToDelete</code> which
353 * can be deleted too. A node can be deleted if
354 * <ul>
355 * <li>it is untagged (see {@link Node#isTagged()}</li>
356 * <li>it is not referred to by other non-deleted primitives outside of <code>primitivesToDelete</code></li>
357 * </ul>
358 * @param primitivesToDelete the primitives to delete
359 * @return the collection of nodes referred to by primitives in <code>primitivesToDelete</code> which
360 * can be deleted too
361 */
362 protected static Collection<Node> computeNodesToDelete(Collection<OsmPrimitive> primitivesToDelete) {
363 Collection<Node> nodesToDelete = new HashSet<>();
364 for (Way way : Utils.filteredCollection(primitivesToDelete, Way.class)) {
365 for (Node n : way.getNodes()) {
366 if (n.isTagged()) {
367 continue;
368 }
369 Collection<OsmPrimitive> referringPrimitives = n.getReferrers();
370 referringPrimitives.removeAll(primitivesToDelete);
371 int count = 0;
372 for (OsmPrimitive p : referringPrimitives) {
373 if (!p.isDeleted()) {
374 count++;
375 }
376 }
377 if (count == 0) {
378 nodesToDelete.add(n);
379 }
380 }
381 }
382 return nodesToDelete;
383 }
384
385 /**
386 * Try to delete all given primitives.
387 *
388 * If a node is used by a way, it's removed from that way. If a node or a way is used by a
389 * relation, inform the user and do not delete.
390 *
391 * If this would cause ways with less than 2 nodes to be created, delete these ways instead. If
392 * they are part of a relation, inform the user and do not delete.
393 *
394 * @param selection the objects to delete.
395 * @param alsoDeleteNodesInWay <code>true</code> if nodes should be deleted as well
396 * @return command a command to perform the deletions, or null if there is nothing to delete.
397 * @since 12718
398 */
399 public static Command delete(Collection<? extends OsmPrimitive> selection, boolean alsoDeleteNodesInWay) {
400 return delete(selection, alsoDeleteNodesInWay, false /* not silent */);
401 }
402
403 /**
404 * Try to delete all given primitives.
405 *
406 * If a node is used by a way, it's removed from that way. If a node or a way is used by a
407 * relation, inform the user and do not delete.
408 *
409 * If this would cause ways with less than 2 nodes to be created, delete these ways instead. If
410 * they are part of a relation, inform the user and do not delete.
411 *
412 * @param selection the objects to delete.
413 * @param alsoDeleteNodesInWay <code>true</code> if nodes should be deleted as well
414 * @param silent set to true if the user should not be bugged with additional questions
415 * @return command a command to perform the deletions, or null if there is nothing to delete.
416 * @since 12718
417 */
418 public static Command delete(Collection<? extends OsmPrimitive> selection, boolean alsoDeleteNodesInWay, boolean silent) {
419 if (selection == null || selection.isEmpty())
420 return null;
421
422 Set<OsmPrimitive> primitivesToDelete = new HashSet<>(selection);
423
424 Collection<Relation> relationsToDelete = Utils.filteredCollection(primitivesToDelete, Relation.class);
425 if (!relationsToDelete.isEmpty() && !silent && !callback.confirmRelationDeletion(relationsToDelete))
426 return null;
427
428 if (alsoDeleteNodesInWay) {
429 // delete untagged nodes only referenced by primitives in primitivesToDelete, too
430 Collection<Node> nodesToDelete = computeNodesToDelete(primitivesToDelete);
431 primitivesToDelete.addAll(nodesToDelete);
432 }
433
434 if (!silent && !callback.checkAndConfirmOutlyingDelete(
435 primitivesToDelete, Utils.filteredCollection(primitivesToDelete, Way.class)))
436 return null;
437
438 Collection<Way> waysToBeChanged = primitivesToDelete.stream()
439 .flatMap(p -> p.referrers(Way.class))
440 .collect(Collectors.toSet());
441
442 Collection<Command> cmds = new LinkedList<>();
443 Set<Node> nodesToRemove = new HashSet<>(Utils.filteredCollection(primitivesToDelete, Node.class));
444 for (Way w : waysToBeChanged) {
445 Way wnew = new Way(w);
446 wnew.removeNodes(nodesToRemove);
447 if (wnew.getNodesCount() < 2) {
448 primitivesToDelete.add(w);
449 } else {
450 cmds.add(new ChangeNodesCommand(w, wnew.getNodes()));
451 }
452 }
453
454 // get a confirmation that the objects to delete can be removed from their parent relations
455 //
456 if (!silent) {
457 Set<RelationToChildReference> references = RelationToChildReference.getRelationToChildReferences(primitivesToDelete);
458 references.removeIf(ref -> ref.getParent().isDeleted());
459 if (!references.isEmpty() && !callback.confirmDeletionFromRelation(references)) {
460 return null;
461 }
462 }
463
464 // remove the objects from their parent relations
465 //
466 final Set<Relation> relationsToBeChanged = primitivesToDelete.stream()
467 .flatMap(p -> p.referrers(Relation.class))
468 .collect(Collectors.toSet());
469 for (Relation cur : relationsToBeChanged) {
470 Relation rel = new Relation(cur);
471 rel.removeMembersFor(primitivesToDelete);
472 cmds.add(new ChangeCommand(cur, rel));
473 }
474
475 // build the delete command
476 //
477 if (!primitivesToDelete.isEmpty()) {
478 cmds.add(new DeleteCommand(primitivesToDelete.iterator().next().getDataSet(), primitivesToDelete));
479 }
480
481 return new SequenceCommand(tr("Delete"), cmds);
482 }
483
484 /**
485 * Create a command that deletes a single way segment. The way may be split by this.
486 * @param ws The way segment that should be deleted
487 * @return A matching command to safely delete that segment.
488 * @since 12718
489 */
490 public static Command deleteWaySegment(WaySegment ws) {
491 if (ws.way.getNodesCount() < 3)
492 return delete(Collections.singleton(ws.way), false);
493
494 if (ws.way.isClosed()) {
495 // If the way is circular (first and last nodes are the same), the way shouldn't be splitted
496
497 List<Node> n = new ArrayList<>();
498
499 n.addAll(ws.way.getNodes().subList(ws.lowerIndex + 1, ws.way.getNodesCount() - 1));
500 n.addAll(ws.way.getNodes().subList(0, ws.lowerIndex + 1));
501
502 Way wnew = new Way(ws.way);
503 wnew.setNodes(n);
504
505 return new ChangeCommand(ws.way, wnew);
506 }
507
508 List<Node> n1 = new ArrayList<>();
509 List<Node> n2 = new ArrayList<>();
510
511 n1.addAll(ws.way.getNodes().subList(0, ws.lowerIndex + 1));
512 n2.addAll(ws.way.getNodes().subList(ws.lowerIndex + 1, ws.way.getNodesCount()));
513
514 Way wnew = new Way(ws.way);
515
516 if (n1.size() < 2) {
517 wnew.setNodes(n2);
518 return new ChangeCommand(ws.way, wnew);
519 } else if (n2.size() < 2) {
520 wnew.setNodes(n1);
521 return new ChangeCommand(ws.way, wnew);
522 } else {
523 return SplitWayCommand.splitWay(ws.way, Arrays.asList(n1, n2), Collections.<OsmPrimitive>emptyList());
524 }
525 }
526
527 @Override
528 public int hashCode() {
529 return Objects.hash(super.hashCode(), toDelete, clonedPrimitives);
530 }
531
532 @Override
533 public boolean equals(Object obj) {
534 if (this == obj) return true;
535 if (obj == null || getClass() != obj.getClass()) return false;
536 if (!super.equals(obj)) return false;
537 DeleteCommand that = (DeleteCommand) obj;
538 return Objects.equals(toDelete, that.toDelete) &&
539 Objects.equals(clonedPrimitives, that.clonedPrimitives);
540 }
541}
Note: See TracBrowser for help on using the repository browser.