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

Last change on this file since 4067 was 3431, checked in by bastiK, 14 years ago

added purge action (some testing would be welcome)

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