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

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

see #19885: memory leak with "temporary" objects in validator and actions
Start to use ChangeMembersCommand instead of ChangeCommand in those places where the cloned relation was only created for the ChangeCommand and not referenced elsewhere.

  • Property svn:eol-style set to native
File size: 20.6 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.HashMap;
13import java.util.HashSet;
14import java.util.LinkedList;
15import java.util.List;
16import java.util.Map;
17import java.util.Map.Entry;
18import java.util.Objects;
19import java.util.Set;
20import java.util.stream.Collectors;
21
22import javax.swing.Icon;
23
24import org.openstreetmap.josm.data.osm.DataSet;
25import org.openstreetmap.josm.data.osm.DefaultNameFormatter;
26import org.openstreetmap.josm.data.osm.Node;
27import org.openstreetmap.josm.data.osm.OsmPrimitive;
28import org.openstreetmap.josm.data.osm.OsmPrimitiveType;
29import org.openstreetmap.josm.data.osm.PrimitiveData;
30import org.openstreetmap.josm.data.osm.Relation;
31import org.openstreetmap.josm.data.osm.RelationMember;
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
189 getAffectedDataSet().update(() -> {
190 // Make copy and remove all references (to prevent inconsistent dataset (delete referenced) while command is executed)
191 for (OsmPrimitive osm : toDelete) {
192 if (osm.isDeleted())
193 throw new IllegalArgumentException(osm + " is already deleted");
194 clonedPrimitives.put(osm, osm.save());
195
196 if (osm instanceof Way) {
197 ((Way) osm).setNodes(null);
198 } else if (osm instanceof Relation) {
199 ((Relation) osm).setMembers(null);
200 }
201 }
202
203 for (OsmPrimitive osm : toDelete) {
204 osm.setDeleted(true);
205 }
206 });
207 return true;
208 }
209
210 @Override
211 public void undoCommand() {
212 ensurePrimitivesAreInDataset();
213
214 getAffectedDataSet().update(() -> {
215 for (OsmPrimitive osm : toDelete) {
216 osm.setDeleted(false);
217 }
218
219 for (Entry<OsmPrimitive, PrimitiveData> entry : clonedPrimitives.entrySet()) {
220 entry.getKey().load(entry.getValue());
221 }
222 });
223 }
224
225 @Override
226 public void fillModifiedData(Collection<OsmPrimitive> modified, Collection<OsmPrimitive> deleted, Collection<OsmPrimitive> added) {
227 // Do nothing
228 }
229
230 private Set<OsmPrimitiveType> getTypesToDelete() {
231 return toDelete.stream().map(OsmPrimitiveType::from).collect(Collectors.toSet());
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 return toDelete.stream().map(DeleteChildCommand::new).collect(Collectors.toList());
281 }
282 }
283
284 @Override public Collection<? extends OsmPrimitive> getParticipatingPrimitives() {
285 return toDelete;
286 }
287
288 /**
289 * Delete the primitives and everything they reference.
290 *
291 * If a node is deleted, the node and all ways and relations the node is part of are deleted as well.
292 * If a way is deleted, all relations the way is member of are also deleted.
293 * If a way is deleted, only the way and no nodes are deleted.
294 *
295 * @param selection The list of all object to be deleted.
296 * @param silent Set to true if the user should not be bugged with additional dialogs
297 * @return command A command to perform the deletions, or null of there is nothing to delete.
298 * @throws IllegalArgumentException if layer is null
299 * @since 12718
300 */
301 public static Command deleteWithReferences(Collection<? extends OsmPrimitive> selection, boolean silent) {
302 if (selection == null || selection.isEmpty()) return null;
303 Set<OsmPrimitive> parents = OsmPrimitive.getReferrer(selection);
304 parents.addAll(selection);
305
306 if (parents.isEmpty())
307 return null;
308 if (!silent && !callback.checkAndConfirmOutlyingDelete(parents, null))
309 return null;
310 return new DeleteCommand(parents);
311 }
312
313 /**
314 * Delete the primitives and everything they reference.
315 *
316 * If a node is deleted, the node and all ways and relations the node is part of are deleted as well.
317 * If a way is deleted, all relations the way is member of are also deleted.
318 * If a way is deleted, only the way and no nodes are deleted.
319 *
320 * @param selection The list of all object to be deleted.
321 * @return command A command to perform the deletions, or null of there is nothing to delete.
322 * @throws IllegalArgumentException if layer is null
323 * @since 12718
324 */
325 public static Command deleteWithReferences(Collection<? extends OsmPrimitive> selection) {
326 return deleteWithReferences(selection, false);
327 }
328
329 /**
330 * Try to delete all given primitives.
331 *
332 * If a node is used by a way, it's removed from that way. If a node or a way is used by a
333 * relation, inform the user and do not delete.
334 *
335 * If this would cause ways with less than 2 nodes to be created, delete these ways instead. If
336 * they are part of a relation, inform the user and do not delete.
337 *
338 * @param selection the objects to delete.
339 * @return command a command to perform the deletions, or null if there is nothing to delete.
340 * @since 12718
341 */
342 public static Command delete(Collection<? extends OsmPrimitive> selection) {
343 return delete(selection, true, false);
344 }
345
346 /**
347 * Replies the collection of nodes referred to by primitives in <code>primitivesToDelete</code> which
348 * can be deleted too. A node can be deleted if
349 * <ul>
350 * <li>it is untagged (see {@link Node#isTagged()}</li>
351 * <li>it is not referred to by other non-deleted primitives outside of <code>primitivesToDelete</code></li>
352 * </ul>
353 * @param primitivesToDelete the primitives to delete
354 * @return the collection of nodes referred to by primitives in <code>primitivesToDelete</code> which
355 * can be deleted too
356 */
357 protected static Collection<Node> computeNodesToDelete(Collection<OsmPrimitive> primitivesToDelete) {
358 Collection<Node> nodesToDelete = new HashSet<>();
359 for (Way way : Utils.filteredCollection(primitivesToDelete, Way.class)) {
360 for (Node n : way.getNodes()) {
361 if (n.isTagged()) {
362 continue;
363 }
364 Collection<OsmPrimitive> referringPrimitives = n.getReferrers();
365 referringPrimitives.removeAll(primitivesToDelete);
366 if (referringPrimitives.stream().allMatch(OsmPrimitive::isDeleted)) {
367 nodesToDelete.add(n);
368 }
369 }
370 }
371 return nodesToDelete;
372 }
373
374 /**
375 * Try to delete all given primitives.
376 *
377 * If a node is used by a way, it's removed from that way. If a node or a way is used by a
378 * relation, inform the user and do not delete.
379 *
380 * If this would cause ways with less than 2 nodes to be created, delete these ways instead. If
381 * they are part of a relation, inform the user and do not delete.
382 *
383 * @param selection the objects to delete.
384 * @param alsoDeleteNodesInWay <code>true</code> if nodes should be deleted as well
385 * @return command a command to perform the deletions, or null if there is nothing to delete.
386 * @since 12718
387 */
388 public static Command delete(Collection<? extends OsmPrimitive> selection, boolean alsoDeleteNodesInWay) {
389 return delete(selection, alsoDeleteNodesInWay, false /* not silent */);
390 }
391
392 /**
393 * Try to delete all given primitives.
394 *
395 * If a node is used by a way, it's removed from that way. If a node or a way is used by a
396 * relation, inform the user and do not delete.
397 *
398 * If this would cause ways with less than 2 nodes to be created, delete these ways instead. If
399 * they are part of a relation, inform the user and do not delete.
400 *
401 * @param selection the objects to delete.
402 * @param alsoDeleteNodesInWay <code>true</code> if nodes should be deleted as well
403 * @param silent set to true if the user should not be bugged with additional questions
404 * @return command a command to perform the deletions, or null if there is nothing to delete.
405 * @since 12718
406 */
407 public static Command delete(Collection<? extends OsmPrimitive> selection, boolean alsoDeleteNodesInWay, boolean silent) {
408 if (selection == null || selection.isEmpty())
409 return null;
410
411 Set<OsmPrimitive> primitivesToDelete = new HashSet<>(selection);
412
413 Collection<Relation> relationsToDelete = Utils.filteredCollection(primitivesToDelete, Relation.class);
414 if (!relationsToDelete.isEmpty() && !silent && !callback.confirmRelationDeletion(relationsToDelete))
415 return null;
416
417 if (alsoDeleteNodesInWay) {
418 // delete untagged nodes only referenced by primitives in primitivesToDelete, too
419 Collection<Node> nodesToDelete = computeNodesToDelete(primitivesToDelete);
420 primitivesToDelete.addAll(nodesToDelete);
421 }
422
423 if (!silent && !callback.checkAndConfirmOutlyingDelete(
424 primitivesToDelete, Utils.filteredCollection(primitivesToDelete, Way.class)))
425 return null;
426
427 Collection<Way> waysToBeChanged = primitivesToDelete.stream()
428 .flatMap(p -> p.referrers(Way.class))
429 .collect(Collectors.toSet());
430
431 Collection<Command> cmds = new LinkedList<>();
432 Set<Node> nodesToRemove = new HashSet<>(Utils.filteredCollection(primitivesToDelete, Node.class));
433 for (Way w : waysToBeChanged) {
434 if (primitivesToDelete.contains(w))
435 continue;
436 List<Node> remainingNodes = w.calculateRemoveNodes(nodesToRemove);
437 if (remainingNodes.size() < 2) {
438 primitivesToDelete.add(w);
439 } else {
440 cmds.add(new ChangeNodesCommand(w, remainingNodes));
441 }
442 }
443
444 // get a confirmation that the objects to delete can be removed from their parent relations
445 //
446 if (!silent) {
447 Set<RelationToChildReference> references = RelationToChildReference.getRelationToChildReferences(primitivesToDelete);
448 references.removeIf(ref -> ref.getParent().isDeleted());
449 if (!references.isEmpty() && !callback.confirmDeletionFromRelation(references)) {
450 return null;
451 }
452 }
453
454 // remove the objects from their parent relations
455 //
456 final Set<Relation> relationsToBeChanged = primitivesToDelete.stream()
457 .flatMap(p -> p.referrers(Relation.class))
458 .collect(Collectors.toSet());
459 for (Relation cur : relationsToBeChanged) {
460 List<RelationMember> newMembers = cur.getMembers();
461 cur.getMembersFor(primitivesToDelete).forEach(newMembers::remove);
462 cmds.add(new ChangeMembersCommand(cur, newMembers));
463 }
464
465 // build the delete command
466 //
467 if (!primitivesToDelete.isEmpty()) {
468 cmds.add(new DeleteCommand(primitivesToDelete));
469 }
470
471 return SequenceCommand.wrapIfNeeded(tr("Delete"), cmds);
472 }
473
474 /**
475 * Create a command that deletes a single way segment. The way may be split by this.
476 * @param ws The way segment that should be deleted
477 * @return A matching command to safely delete that segment.
478 * @since 12718
479 */
480 public static Command deleteWaySegment(WaySegment ws) {
481 if (ws.way.getNodesCount() < 3)
482 return delete(Collections.singleton(ws.way), false);
483
484 if (ws.way.isClosed()) {
485 // If the way is circular (first and last nodes are the same), the way shouldn't be splitted
486
487 List<Node> n = new ArrayList<>();
488
489 n.addAll(ws.way.getNodes().subList(ws.lowerIndex + 1, ws.way.getNodesCount() - 1));
490 n.addAll(ws.way.getNodes().subList(0, ws.lowerIndex + 1));
491
492 return new ChangeNodesCommand(ws.way, n);
493 }
494
495 List<Node> n1 = new ArrayList<>();
496 List<Node> n2 = new ArrayList<>();
497
498 n1.addAll(ws.way.getNodes().subList(0, ws.lowerIndex + 1));
499 n2.addAll(ws.way.getNodes().subList(ws.lowerIndex + 1, ws.way.getNodesCount()));
500
501 if (n1.size() < 2) {
502 return new ChangeNodesCommand(ws.way, n2);
503 } else if (n2.size() < 2) {
504 return new ChangeNodesCommand(ws.way, n1);
505 } else {
506 return SplitWayCommand.splitWay(ws.way, Arrays.asList(n1, n2), Collections.<OsmPrimitive>emptyList());
507 }
508 }
509
510 @Override
511 public int hashCode() {
512 return Objects.hash(super.hashCode(), toDelete, clonedPrimitives);
513 }
514
515 @Override
516 public boolean equals(Object obj) {
517 if (this == obj) return true;
518 if (obj == null || getClass() != obj.getClass()) return false;
519 if (!super.equals(obj)) return false;
520 DeleteCommand that = (DeleteCommand) obj;
521 return Objects.equals(toDelete, that.toDelete) &&
522 Objects.equals(clonedPrimitives, that.clonedPrimitives);
523 }
524}
Note: See TracBrowser for help on using the repository browser.