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

Last change on this file since 5674 was 5612, checked in by Don-vip, 11 years ago

see #6178 - Makes DeleteCommand use ChangeNodesCommand instead of ChangeCommand when deleting a node being part of a way.

The previous use of ChangeCommand prevented this use case to work as expected:

  • SequenceCommand of:
    • ChangePropertyCommands adding tags to a way
    • DeleteCommand of a node of this way


-> Running the DeleteCommand did cancel the previously run ChangePropertyCommands as the entire way was replaced (including its tags), based on the way state at the creation of the SequenceCommand and not its real current state.

By the way, this could also be more efficient.

  • Property svn:eol-style set to native
File size: 20.6 KB
Line 
1// License: GPL. Copyright 2007 by Immanuel Scholz and others
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.awt.GridBagLayout;
9import java.awt.geom.Area;
10import java.util.ArrayList;
11import java.util.Collection;
12import java.util.Collections;
13import java.util.HashMap;
14import java.util.HashSet;
15import java.util.Iterator;
16import java.util.LinkedList;
17import java.util.List;
18import java.util.Map;
19import java.util.Set;
20import java.util.Map.Entry;
21
22import javax.swing.Icon;
23import javax.swing.JLabel;
24import javax.swing.JOptionPane;
25import javax.swing.JPanel;
26
27import org.openstreetmap.josm.Main;
28import org.openstreetmap.josm.actions.SplitWayAction;
29import org.openstreetmap.josm.data.osm.Node;
30import org.openstreetmap.josm.data.osm.OsmPrimitive;
31import org.openstreetmap.josm.data.osm.OsmPrimitiveType;
32import org.openstreetmap.josm.data.osm.PrimitiveData;
33import org.openstreetmap.josm.data.osm.Relation;
34import org.openstreetmap.josm.data.osm.RelationToChildReference;
35import org.openstreetmap.josm.data.osm.Way;
36import org.openstreetmap.josm.data.osm.WaySegment;
37import org.openstreetmap.josm.gui.ConditionalOptionPaneUtil;
38import org.openstreetmap.josm.gui.DefaultNameFormatter;
39import org.openstreetmap.josm.gui.actionsupport.DeleteFromRelationConfirmationDialog;
40import org.openstreetmap.josm.gui.layer.OsmDataLayer;
41import org.openstreetmap.josm.tools.CheckParameterUtil;
42import org.openstreetmap.josm.tools.ImageProvider;
43import org.openstreetmap.josm.tools.Utils;
44
45/**
46 * A command to delete a number of primitives from the dataset.
47 *
48 */
49public class DeleteCommand extends Command {
50 /**
51 * The primitives that get deleted.
52 */
53 private final Collection<? extends OsmPrimitive> toDelete;
54 private final Map<OsmPrimitive, PrimitiveData> clonedPrimitives = new HashMap<OsmPrimitive, PrimitiveData>();
55
56 /**
57 * Constructor. Deletes a collection of primitives in the current edit layer.
58 *
59 * @param data the primitives to delete. Must neither be null nor empty.
60 * @throws IllegalArgumentException thrown if data is null or empty
61 */
62 public DeleteCommand(Collection<? extends OsmPrimitive> data) throws IllegalArgumentException {
63 if (data == null)
64 throw new IllegalArgumentException("Parameter 'data' must not be empty");
65 if (data.isEmpty())
66 throw new IllegalArgumentException(tr("At least one object to delete required, got empty collection"));
67 this.toDelete = data;
68 }
69
70 /**
71 * Constructor. Deletes a single primitive in the current edit layer.
72 *
73 * @param data the primitive to delete. Must not be null.
74 * @throws IllegalArgumentException thrown if data is null
75 */
76 public DeleteCommand(OsmPrimitive data) throws IllegalArgumentException {
77 CheckParameterUtil.ensureParameterNotNull(data, "data");
78 this.toDelete = Collections.singleton(data);
79 }
80
81 /**
82 * Constructor for a single data item. Use the collection constructor to delete multiple
83 * objects.
84 *
85 * @param layer the layer context for deleting this primitive. Must not be null.
86 * @param data the primitive to delete. Must not be null.
87 * @throws IllegalArgumentException thrown if data is null
88 * @throws IllegalArgumentException thrown if layer is null
89 */
90 public DeleteCommand(OsmDataLayer layer, OsmPrimitive data) throws IllegalArgumentException {
91 super(layer);
92 CheckParameterUtil.ensureParameterNotNull(data, "data");
93 this.toDelete = Collections.singleton(data);
94 }
95
96 /**
97 * Constructor for a collection of data to be deleted in the context of
98 * a specific layer
99 *
100 * @param layer the layer context for deleting these primitives. Must not be null.
101 * @param data the primitives to delete. Must neither be null nor empty.
102 * @throws IllegalArgumentException thrown if layer is null
103 * @throws IllegalArgumentException thrown if data is null or empty
104 */
105 public DeleteCommand(OsmDataLayer layer, Collection<? extends OsmPrimitive> data) throws IllegalArgumentException{
106 super(layer);
107 if (data == null)
108 throw new IllegalArgumentException("Parameter 'data' must not be empty");
109 if (data.isEmpty())
110 throw new IllegalArgumentException(tr("At least one object to delete required, got empty collection"));
111 this.toDelete = data;
112 }
113
114 @Override
115 public boolean executeCommand() {
116 // Make copy and remove all references (to prevent inconsistent dataset (delete referenced) while command is executed)
117 for (OsmPrimitive osm: toDelete) {
118 if (osm.isDeleted())
119 throw new IllegalArgumentException(osm.toString() + " is already deleted");
120 clonedPrimitives.put(osm, osm.save());
121
122 if (osm instanceof Way) {
123 ((Way) osm).setNodes(null);
124 } else if (osm instanceof Relation) {
125 ((Relation) osm).setMembers(null);
126 }
127 }
128
129 for (OsmPrimitive osm: toDelete) {
130 osm.setDeleted(true);
131 }
132
133 return true;
134 }
135
136 @Override
137 public void undoCommand() {
138 for (OsmPrimitive osm: toDelete) {
139 osm.setDeleted(false);
140 }
141
142 for (Entry<OsmPrimitive, PrimitiveData> entry: clonedPrimitives.entrySet()) {
143 entry.getKey().load(entry.getValue());
144 }
145 }
146
147 @Override
148 public void fillModifiedData(Collection<OsmPrimitive> modified, Collection<OsmPrimitive> deleted,
149 Collection<OsmPrimitive> added) {
150 }
151
152 private Set<OsmPrimitiveType> getTypesToDelete() {
153 Set<OsmPrimitiveType> typesToDelete = new HashSet<OsmPrimitiveType>();
154 for (OsmPrimitive osm : toDelete) {
155 typesToDelete.add(OsmPrimitiveType.from(osm));
156 }
157 return typesToDelete;
158 }
159
160 @Override
161 public String getDescriptionText() {
162 if (toDelete.size() == 1) {
163 OsmPrimitive primitive = toDelete.iterator().next();
164 String msg = "";
165 switch(OsmPrimitiveType.from(primitive)) {
166 case NODE: msg = marktr("Delete node {0}"); break;
167 case WAY: msg = marktr("Delete way {0}"); break;
168 case RELATION:msg = marktr("Delete relation {0}"); break;
169 }
170
171 return tr(msg, primitive.getDisplayName(DefaultNameFormatter.getInstance()));
172 } else {
173 Set<OsmPrimitiveType> typesToDelete = getTypesToDelete();
174 String msg = "";
175 if (typesToDelete.size() > 1) {
176 msg = trn("Delete {0} object", "Delete {0} objects", toDelete.size(), toDelete.size());
177 } else {
178 OsmPrimitiveType t = typesToDelete.iterator().next();
179 switch(t) {
180 case NODE: msg = trn("Delete {0} node", "Delete {0} nodes", toDelete.size(), toDelete.size()); break;
181 case WAY: msg = trn("Delete {0} way", "Delete {0} ways", toDelete.size(), toDelete.size()); break;
182 case RELATION: msg = trn("Delete {0} relation", "Delete {0} relations", toDelete.size(), toDelete.size()); break;
183 }
184 }
185 return msg;
186 }
187 }
188
189 @Override
190 public Icon getDescriptionIcon() {
191 if (toDelete.size() == 1)
192 return ImageProvider.get(toDelete.iterator().next().getDisplayType());
193 Set<OsmPrimitiveType> typesToDelete = getTypesToDelete();
194 if (typesToDelete.size() > 1)
195 return ImageProvider.get("data", "object");
196 else
197 return ImageProvider.get(typesToDelete.iterator().next());
198 }
199
200 @Override public Collection<PseudoCommand> getChildren() {
201 if (toDelete.size() == 1)
202 return null;
203 else {
204 List<PseudoCommand> children = new ArrayList<PseudoCommand>();
205 for (final OsmPrimitive osm : toDelete) {
206 children.add(new PseudoCommand() {
207
208 @Override public String getDescriptionText() {
209 return tr("Deleted ''{0}''", osm.getDisplayName(DefaultNameFormatter.getInstance()));
210 }
211
212 @Override public Icon getDescriptionIcon() {
213 return ImageProvider.get(osm.getDisplayType());
214 }
215
216 @Override public Collection<? extends OsmPrimitive> getParticipatingPrimitives() {
217 return Collections.singleton(osm);
218 }
219
220 });
221 }
222 return children;
223
224 }
225 }
226
227 @Override public Collection<? extends OsmPrimitive> getParticipatingPrimitives() {
228 return toDelete;
229 }
230
231 /**
232 * Delete the primitives and everything they reference.
233 *
234 * If a node is deleted, the node and all ways and relations the node is part of are deleted as
235 * well.
236 *
237 * If a way is deleted, all relations the way is member of are also deleted.
238 *
239 * If a way is deleted, only the way and no nodes are deleted.
240 *
241 * @param layer the {@link OsmDataLayer} in whose context primitives are deleted. Must not be null.
242 * @param selection The list of all object to be deleted.
243 * @param silent Set to true if the user should not be bugged with additional dialogs
244 * @return command A command to perform the deletions, or null of there is nothing to delete.
245 * @throws IllegalArgumentException thrown if layer is null
246 */
247 public static Command deleteWithReferences(OsmDataLayer layer, Collection<? extends OsmPrimitive> selection, boolean silent) throws IllegalArgumentException {
248 CheckParameterUtil.ensureParameterNotNull(layer, "layer");
249 if (selection == null || selection.isEmpty()) return null;
250 Set<OsmPrimitive> parents = OsmPrimitive.getReferrer(selection);
251 parents.addAll(selection);
252
253 if (parents.isEmpty())
254 return null;
255 if (!silent && !checkAndConfirmOutlyingDelete(layer, parents, null))
256 return null;
257 return new DeleteCommand(layer,parents);
258 }
259
260 public static Command deleteWithReferences(OsmDataLayer layer, Collection<? extends OsmPrimitive> selection) {
261 return deleteWithReferences(layer, selection, false);
262 }
263
264 public static Command delete(OsmDataLayer layer, Collection<? extends OsmPrimitive> selection) {
265 return delete(layer, selection, true, false);
266 }
267
268 /**
269 * Replies the collection of nodes referred to by primitives in <code>primitivesToDelete</code> which
270 * can be deleted too. A node can be deleted if
271 * <ul>
272 * <li>it is untagged (see {@link Node#isTagged()}</li>
273 * <li>it is not referred to by other non-deleted primitives outside of <code>primitivesToDelete</code></li>
274 * <ul>
275 * @param layer the layer in whose context primitives are deleted
276 * @param primitivesToDelete the primitives to delete
277 * @return the collection of nodes referred to by primitives in <code>primitivesToDelete</code> which
278 * can be deleted too
279 */
280 protected static Collection<Node> computeNodesToDelete(OsmDataLayer layer, Collection<OsmPrimitive> primitivesToDelete) {
281 Collection<Node> nodesToDelete = new HashSet<Node>();
282 for (Way way : OsmPrimitive.getFilteredList(primitivesToDelete, Way.class)) {
283 for (Node n : way.getNodes()) {
284 if (n.isTagged()) {
285 continue;
286 }
287 Collection<OsmPrimitive> referringPrimitives = n.getReferrers();
288 referringPrimitives.removeAll(primitivesToDelete);
289 int count = 0;
290 for (OsmPrimitive p : referringPrimitives) {
291 if (!p.isDeleted()) {
292 count++;
293 }
294 }
295 if (count == 0) {
296 nodesToDelete.add(n);
297 }
298 }
299 }
300 return nodesToDelete;
301 }
302
303 /**
304 * Try to delete all given primitives.
305 *
306 * If a node is used by a way, it's removed from that way. If a node or a way is used by a
307 * relation, inform the user and do not delete.
308 *
309 * If this would cause ways with less than 2 nodes to be created, delete these ways instead. If
310 * they are part of a relation, inform the user and do not delete.
311 *
312 * @param layer the {@link OsmDataLayer} in whose context the primitives are deleted
313 * @param selection the objects to delete.
314 * @param alsoDeleteNodesInWay <code>true</code> if nodes should be deleted as well
315 * @return command a command to perform the deletions, or null if there is nothing to delete.
316 */
317 public static Command delete(OsmDataLayer layer, Collection<? extends OsmPrimitive> selection,
318 boolean alsoDeleteNodesInWay) {
319 return delete(layer, selection, alsoDeleteNodesInWay, false /* not silent */);
320 }
321
322 /**
323 * Try to delete all given primitives.
324 *
325 * If a node is used by a way, it's removed from that way. If a node or a way is used by a
326 * relation, inform the user and do not delete.
327 *
328 * If this would cause ways with less than 2 nodes to be created, delete these ways instead. If
329 * they are part of a relation, inform the user and do not delete.
330 *
331 * @param layer the {@link OsmDataLayer} in whose context the primitives are deleted
332 * @param selection the objects to delete.
333 * @param alsoDeleteNodesInWay <code>true</code> if nodes should be deleted as well
334 * @param silent set to true if the user should not be bugged with additional questions
335 * @return command a command to perform the deletions, or null if there is nothing to delete.
336 */
337 public static Command delete(OsmDataLayer layer, Collection<? extends OsmPrimitive> selection,
338 boolean alsoDeleteNodesInWay, boolean silent) {
339 if (selection == null || selection.isEmpty())
340 return null;
341
342 Set<OsmPrimitive> primitivesToDelete = new HashSet<OsmPrimitive>(selection);
343
344 Collection<Relation> relationsToDelete = Utils.filteredCollection(primitivesToDelete, Relation.class);
345 if (!relationsToDelete.isEmpty() && !silent && !confirmRelationDeletion(relationsToDelete))
346 return null;
347
348 Collection<Way> waysToBeChanged = new HashSet<Way>();
349
350 if (alsoDeleteNodesInWay) {
351 // delete untagged nodes only referenced by primitives in primitivesToDelete,
352 // too
353 Collection<Node> nodesToDelete = computeNodesToDelete(layer, primitivesToDelete);
354 primitivesToDelete.addAll(nodesToDelete);
355 }
356
357 if (!silent && !checkAndConfirmOutlyingDelete(layer,
358 primitivesToDelete, Utils.filteredCollection(primitivesToDelete, Way.class)))
359 return null;
360
361 waysToBeChanged.addAll(OsmPrimitive.getFilteredSet(OsmPrimitive.getReferrer(primitivesToDelete), Way.class));
362
363 Collection<Command> cmds = new LinkedList<Command>();
364 for (Way w : waysToBeChanged) {
365 Way wnew = new Way(w);
366 wnew.removeNodes(OsmPrimitive.getFilteredSet(primitivesToDelete, Node.class));
367 if (wnew.getNodesCount() < 2) {
368 primitivesToDelete.add(w);
369 } else {
370 cmds.add(new ChangeNodesCommand(w, wnew.getNodes()));
371 }
372 }
373
374 // get a confirmation that the objects to delete can be removed from their parent
375 // relations
376 //
377 if (!silent) {
378 Set<RelationToChildReference> references = RelationToChildReference.getRelationToChildReferences(primitivesToDelete);
379 Iterator<RelationToChildReference> it = references.iterator();
380 while(it.hasNext()) {
381 RelationToChildReference ref = it.next();
382 if (ref.getParent().isDeleted()) {
383 it.remove();
384 }
385 }
386 if (!references.isEmpty()) {
387 DeleteFromRelationConfirmationDialog dialog = DeleteFromRelationConfirmationDialog.getInstance();
388 dialog.getModel().populate(references);
389 dialog.setVisible(true);
390 if (dialog.isCanceled())
391 return null;
392 }
393 }
394
395 // remove the objects from their parent relations
396 //
397 Iterator<Relation> iterator = OsmPrimitive.getFilteredSet(OsmPrimitive.getReferrer(primitivesToDelete), Relation.class).iterator();
398 while (iterator.hasNext()) {
399 Relation cur = iterator.next();
400 Relation rel = new Relation(cur);
401 rel.removeMembersFor(primitivesToDelete);
402 cmds.add(new ChangeCommand(cur, rel));
403 }
404
405 // build the delete command
406 //
407 if (!primitivesToDelete.isEmpty()) {
408 cmds.add(new DeleteCommand(layer,primitivesToDelete));
409 }
410
411 return new SequenceCommand(tr("Delete"), cmds);
412 }
413
414 public static Command deleteWaySegment(OsmDataLayer layer, WaySegment ws) {
415 if (ws.way.getNodesCount() < 3)
416 return delete(layer, Collections.singleton(ws.way), false);
417
418 if (ws.way.firstNode() == ws.way.lastNode()) {
419 // If the way is circular (first and last nodes are the same),
420 // the way shouldn't be splitted
421
422 List<Node> n = new ArrayList<Node>();
423
424 n.addAll(ws.way.getNodes().subList(ws.lowerIndex + 1, ws.way.getNodesCount() - 1));
425 n.addAll(ws.way.getNodes().subList(0, ws.lowerIndex + 1));
426
427 Way wnew = new Way(ws.way);
428 wnew.setNodes(n);
429
430 return new ChangeCommand(ws.way, wnew);
431 }
432
433 List<Node> n1 = new ArrayList<Node>(), n2 = new ArrayList<Node>();
434
435 n1.addAll(ws.way.getNodes().subList(0, ws.lowerIndex + 1));
436 n2.addAll(ws.way.getNodes().subList(ws.lowerIndex + 1, ws.way.getNodesCount()));
437
438 Way wnew = new Way(ws.way);
439
440 if (n1.size() < 2) {
441 wnew.setNodes(n2);
442 return new ChangeCommand(ws.way, wnew);
443 } else if (n2.size() < 2) {
444 wnew.setNodes(n1);
445 return new ChangeCommand(ws.way, wnew);
446 } else {
447 List<List<Node>> chunks = new ArrayList<List<Node>>(2);
448 chunks.add(n1);
449 chunks.add(n2);
450 return SplitWayAction.splitWay(layer,ws.way, chunks, Collections.<OsmPrimitive>emptyList()).getCommand();
451 }
452 }
453
454 public static boolean checkAndConfirmOutlyingDelete(OsmDataLayer layer, Collection<? extends OsmPrimitive> primitives, Collection<? extends OsmPrimitive> ignore) {
455 return checkAndConfirmOutlyingDelete(layer.data.getDataSourceArea(), primitives, ignore);
456 }
457
458 public static boolean checkAndConfirmOutlyingDelete(Area area, Collection<? extends OsmPrimitive> primitives, Collection<? extends OsmPrimitive> ignore) {
459 return Command.checkAndConfirmOutlyingOperation("delete",
460 tr("Delete confirmation"),
461 tr("You are about to delete nodes outside of the area you have downloaded."
462 + "<br>"
463 + "This can cause problems because other objects (that you do not see) might use them."
464 + "<br>"
465 + "Do you really want to delete?"),
466 tr("You are about to delete incomplete objects."
467 + "<br>"
468 + "This will cause problems because you don''t see the real object."
469 + "<br>" + "Do you really want to delete?"),
470 area, primitives, ignore);
471 }
472
473 private static boolean confirmRelationDeletion(Collection<Relation> relations) {
474 JPanel msg = new JPanel(new GridBagLayout());
475 msg.add(new JLabel("<html>" + trn(
476 "You are about to delete {0} relation: {1}"
477 + "<br/>"
478 + "This step is rarely necessary and cannot be undone easily after being uploaded to the server."
479 + "<br/>"
480 + "Do you really want to delete?",
481 "You are about to delete {0} relations: {1}"
482 + "<br/>"
483 + "This step is rarely necessary and cannot be undone easily after being uploaded to the server."
484 + "<br/>"
485 + "Do you really want to delete?",
486 relations.size(), relations.size(), DefaultNameFormatter.getInstance().formatAsHtmlUnorderedList(relations))
487 + "</html>"));
488 boolean answer = ConditionalOptionPaneUtil.showConfirmationDialog(
489 "delete_relations",
490 Main.parent,
491 msg,
492 tr("Delete relation?"),
493 JOptionPane.YES_NO_OPTION,
494 JOptionPane.QUESTION_MESSAGE,
495 JOptionPane.YES_OPTION);
496 return answer;
497 }
498}
Note: See TracBrowser for help on using the repository browser.