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

Last change on this file since 2308 was 2308, checked in by Gubaer, 15 years ago

fixed #3762: Deleted relation still referenced when deleting former member
Clean up of Delete command. New: only one confirmation dialog for all parent relations of deleted objects, see help.
Improved infrastructure for context-sensitive help, improved internal help browser.

  • Property svn:eol-style set to native
File size: 21.3 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.tr;
5import static org.openstreetmap.josm.tools.I18n.trn;
6
7import java.awt.GridBagLayout;
8import java.awt.geom.Area;
9import java.util.ArrayList;
10import java.util.Collection;
11import java.util.Collections;
12import java.util.HashSet;
13import java.util.Iterator;
14import java.util.LinkedList;
15import java.util.List;
16import java.util.Set;
17
18import javax.swing.JLabel;
19import javax.swing.JOptionPane;
20import javax.swing.JPanel;
21import javax.swing.tree.DefaultMutableTreeNode;
22import javax.swing.tree.MutableTreeNode;
23
24import org.openstreetmap.josm.Main;
25import org.openstreetmap.josm.data.osm.BackreferencedDataSet;
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.Relation;
30import org.openstreetmap.josm.data.osm.RelationMember;
31import org.openstreetmap.josm.data.osm.Way;
32import org.openstreetmap.josm.data.osm.WaySegment;
33import org.openstreetmap.josm.data.osm.BackreferencedDataSet.RelationToChildReference;
34import org.openstreetmap.josm.data.osm.visitor.CollectBackReferencesVisitor;
35import org.openstreetmap.josm.gui.ConditionalOptionPaneUtil;
36import org.openstreetmap.josm.gui.DefaultNameFormatter;
37import org.openstreetmap.josm.gui.ExtendedDialog;
38import org.openstreetmap.josm.gui.actionsupport.DeleteFromRelationConfirmationDialog;
39import org.openstreetmap.josm.gui.layer.OsmDataLayer;
40import org.openstreetmap.josm.tools.ImageProvider;
41
42import sun.swing.BakedArrayList;
43
44/**
45 * A command to delete a number of primitives from the dataset.
46
47 */
48public class DeleteCommand extends Command {
49 /**
50 * The primitives that get deleted.
51 */
52 private final Collection<? extends OsmPrimitive> toDelete;
53
54 /**
55 * Constructor. Deletes a collection of primitives in the current edit layer.
56 *
57 * @param data the primitives to delete
58 */
59 public DeleteCommand(Collection<? extends OsmPrimitive> data) {
60 if (data == null) {
61 data = Collections.emptyList();
62 }
63 this.toDelete = data;
64 }
65
66 /**
67 * Constructor. Deletes a single primitive in the current edit layer.
68 *
69 * @param data the primitive to delete. Must not be null.
70 * @throws IllegalArgumentException thrown if data is null
71 */
72 public DeleteCommand(OsmPrimitive data) throws IllegalArgumentException {
73 if (data == null)
74 throw new IllegalArgumentException(tr("Parameter ''{0}'' must not be null", "data"));
75 this.toDelete = Collections.singleton(data);
76 }
77
78 /**
79 * Constructor for a single data item. Use the collection constructor to delete multiple
80 * objects.
81 *
82 * @param layer the layer context for deleting this primitive. Must not be null.
83 * @param data the primitive to delete. Must not be null.
84 * @throws IllegalArgumentException thrown if data is null
85 * @throws IllegalArgumentException thrown if layer is null
86 */
87 public DeleteCommand(OsmDataLayer layer, OsmPrimitive data) throws IllegalArgumentException {
88 super(layer);
89 if (data == null)
90 throw new IllegalArgumentException(tr("Parameter ''{0}'' must not be null", "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
100 * @throws IllegalArgumentException thrown if layer is null
101 */
102 public DeleteCommand(OsmDataLayer layer, Collection<? extends OsmPrimitive> data) throws IllegalArgumentException{
103 super(layer);
104 if (data == null) {
105 data = Collections.emptyList();
106 }
107 this.toDelete = data;
108 }
109
110 protected void removeNewNodesFromDeletedWay(Way w) {
111 // #2707: ways to be deleted can include new nodes (with node.id == 0).
112 // Remove them from the way before the way is deleted. Otherwise the
113 // deleted way is saved (or sent to the API) with a dangling reference to a node
114 // Example:
115 // <node id='2' action='delete' visible='true' version='1' ... />
116 // <node id='1' action='delete' visible='true' version='1' ... />
117 // <!-- missing node with id -1 because new deleted nodes are not persisted -->
118 // <way id='3' action='delete' visible='true' version='1'>
119 // <nd ref='1' />
120 // <nd ref='-1' /> <!-- here's the problem -->
121 // <nd ref='2' />
122 // </way>
123 if (w.isNew())
124 return; // process existing ways only
125 List<Node> nodesToKeep = new ArrayList<Node>();
126 // lookup new nodes which have been added to the set of deleted
127 // nodes ...
128 Iterator<Node> it = nodesToKeep.iterator();
129 while(it.hasNext()) {
130 Node n = it.next();
131 if (n.isNew()) {
132 it.remove();
133 }
134 }
135 w.setNodes(nodesToKeep);
136 }
137
138 @Override
139 public boolean executeCommand() {
140 super.executeCommand();
141 for (OsmPrimitive osm : toDelete) {
142 osm.setDeleted(true);
143 if (osm instanceof Way) {
144 removeNewNodesFromDeletedWay((Way)osm);
145 }
146 }
147 return true;
148 }
149
150 @Override
151 public void fillModifiedData(Collection<OsmPrimitive> modified, Collection<OsmPrimitive> deleted,
152 Collection<OsmPrimitive> added) {
153 deleted.addAll(toDelete);
154 }
155
156 @Override
157 public MutableTreeNode description() {
158 if (toDelete.size() == 1) {
159 OsmPrimitive primitive = toDelete.iterator().next();
160 String msg = "";
161 switch(OsmPrimitiveType.from(primitive)) {
162 case NODE: msg = "Delete node {0}"; break;
163 case WAY: msg = "Delete way {0}"; break;
164 case RELATION:msg = "Delete relation {0}"; break;
165 }
166
167 return new DefaultMutableTreeNode(new JLabel(tr(msg, primitive.getDisplayName(DefaultNameFormatter.getInstance())),
168 ImageProvider.get(OsmPrimitiveType.from(primitive)), JLabel.HORIZONTAL));
169 }
170
171 Set<OsmPrimitiveType> typesToDelete = new HashSet<OsmPrimitiveType>();
172 for (OsmPrimitive osm : toDelete) {
173 typesToDelete.add(OsmPrimitiveType.from(osm));
174 }
175 String msg = "";
176 String apiname = "object";
177 if (typesToDelete.size() > 1) {
178 msg = trn("Delete {0} object", "Delete {0} objects", toDelete.size(), toDelete.size());
179 } else {
180 OsmPrimitiveType t = typesToDelete.iterator().next();
181 apiname = t.getAPIName();
182 switch(t) {
183 case NODE: msg = trn("Delete {0} node", "Delete {0} nodes", toDelete.size(), toDelete.size()); break;
184 case WAY: msg = trn("Delete {0} way", "Delete {0} ways", toDelete.size(), toDelete.size()); break;
185 case RELATION: msg = trn("Delete {0} relation", "Delete {0} relations", toDelete.size(), toDelete.size()); break;
186 }
187 }
188 DefaultMutableTreeNode root = new DefaultMutableTreeNode(
189 new JLabel(msg, ImageProvider.get("data", apiname), JLabel.HORIZONTAL)
190 );
191 for (OsmPrimitive osm : toDelete) {
192 root.add(new DefaultMutableTreeNode(new JLabel(
193 osm.getDisplayName(DefaultNameFormatter.getInstance()),
194 ImageProvider.get(OsmPrimitiveType.from(osm)), JLabel.HORIZONTAL)));
195 }
196 return root;
197 }
198
199 /**
200 * Delete the primitives and everything they reference.
201 *
202 * If a node is deleted, the node and all ways and relations the node is part of are deleted as
203 * well.
204 *
205 * If a way is deleted, all relations the way is member of are also deleted.
206 *
207 * If a way is deleted, only the way and no nodes are deleted.
208 *
209 * @param layer the {@see OsmDataLayer} in whose context primitives are deleted. Must not be null.
210 * @param selection The list of all object to be deleted.
211 * @param silent Set to true if the user should not be bugged with additional dialogs
212 * @return command A command to perform the deletions, or null of there is nothing to delete.
213 * @throws IllegalArgumentException thrown if layer is null
214 */
215 public static Command deleteWithReferences(OsmDataLayer layer, Collection<? extends OsmPrimitive> selection, boolean silent) throws IllegalArgumentException {
216 if (layer == null)
217 throw new IllegalArgumentException(tr("Parameter ''{0}'' must not be null", "layer"));
218 if (selection == null || selection.isEmpty()) return null;
219 CollectBackReferencesVisitor v = new CollectBackReferencesVisitor(layer.data);
220 v.initialize();
221 for (OsmPrimitive osm : selection) {
222 osm.visit(v);
223 }
224 v.getData().addAll(selection);
225 if (v.getData().isEmpty())
226 return null;
227 if (!checkAndConfirmOutlyingDeletes(layer,v.getData()) && !silent)
228 return null;
229 return new DeleteCommand(layer,v.getData());
230 }
231
232 public static Command deleteWithReferences(OsmDataLayer layer, Collection<? extends OsmPrimitive> selection) {
233 return deleteWithReferences(layer, selection, false);
234 }
235
236 private static int testRelation(Relation ref, OsmPrimitive osm, boolean simulate) {
237 // If this delete action is simulated, do not bug the user with dialogs
238 // and assume the relations should be deleted
239 if(simulate)
240 return 1;
241
242 String role = "";
243 for (RelationMember m : ref.getMembers()) {
244 if (m.getMember() == osm) {
245 role = m.getRole();
246 break;
247 }
248 }
249 ExtendedDialog dialog = new ExtendedDialog(
250 Main.parent,
251 tr("Conflicting relation"),
252 new String[] { tr("Delete from relation"),tr("Cancel") }
253 );
254 dialog.setButtonIcons( new String[] { "dialogs/delete.png", "cancel.png" });
255 if (role.length() > 0) {
256 dialog.setContent(
257 tr(
258 "<html>Selection \"{0}\" is used by relation \"{1}\" with role {2}.<br>Delete from relation?</html>",
259 osm.getDisplayName(DefaultNameFormatter.getInstance()),
260 ref.getDisplayName(DefaultNameFormatter.getInstance()),
261 role
262 )
263 );
264 dialog.showDialog();
265 return dialog.getValue();
266 } else {
267 dialog.setContent(
268 tr(
269 "<html>Selection \"{0}\" is used by relation \"{1}\".<br>Delete from relation?</html>",
270 osm.getDisplayName(DefaultNameFormatter.getInstance()),
271 ref.getDisplayName(DefaultNameFormatter.getInstance())
272 )
273 );
274 dialog.showDialog();
275 return dialog.getValue();
276 }
277 }
278
279 public static Command delete(OsmDataLayer layer, Collection<? extends OsmPrimitive> selection) {
280 return delete(layer, selection, true, false);
281 }
282
283 /**
284 * Replies the collection of nodes referred to by primitives in <code>primitivesToDelete</code> which
285 * can be deleted too. A node can be deleted if
286 * <ul>
287 * <li>it is untagged (see {@see Node#isTagged()}</li>
288 * <li>it is not referred to by other non-deleted primitives outside of <code>primitivesToDelete</code></li>
289 * <ul>
290 * @param backreferences backreference data structure
291 * @param layer the layer in whose context primitives are deleted
292 * @param primitivesToDelete the primitives to delete
293 * @return the collection of nodes referred to by primitives in <code>primitivesToDelete</code> which
294 * can be deleted too
295 */
296 protected static Collection<Node> computeNodesToDelete(BackreferencedDataSet backreferences, OsmDataLayer layer, Collection<OsmPrimitive> primitivesToDelete) {
297 Collection<Node> nodesToDelete = new HashSet<Node>();
298 //CollectBackReferencesVisitor v = new CollectBackReferencesVisitor(layer.data, false);
299 for (Way way : OsmPrimitive.getFilteredList(primitivesToDelete, Way.class)) {
300 for (Node n : way.getNodes()) {
301 if (n.isTagged()) {
302 continue;
303 }
304 //v.initialize();
305 //n.visit(v);
306 Collection<OsmPrimitive> referringPrimitives = backreferences.getParents(n);
307 referringPrimitives.removeAll(primitivesToDelete);
308 int count = 0;
309 for (OsmPrimitive p : referringPrimitives) {
310 if (!p.isDeleted()) {
311 count++;
312 }
313 }
314 if (count == 0) {
315 nodesToDelete.add(n);
316 }
317 }
318 }
319 return nodesToDelete;
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 {@see 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 * @return command a command to perform the deletions, or null if there is nothing to delete.
335 */
336 public static Command delete(OsmDataLayer layer, Collection<? extends OsmPrimitive> selection,
337 boolean alsoDeleteNodesInWay) {
338 return delete(layer, selection, alsoDeleteNodesInWay, false /* not silent */);
339 }
340
341 /**
342 * Try to delete all given primitives.
343 *
344 * If a node is used by a way, it's removed from that way. If a node or a way is used by a
345 * relation, inform the user and do not delete.
346 *
347 * If this would cause ways with less than 2 nodes to be created, delete these ways instead. If
348 * they are part of a relation, inform the user and do not delete.
349 *
350 * @param layer the {@see OsmDataLayer} in whose context the primitives are deleted
351 * @param selection the objects to delete.
352 * @param alsoDeleteNodesInWay <code>true</code> if nodes should be deleted as well
353 * @param silent set to true if the user should not be bugged with additional questions
354 * @return command a command to perform the deletions, or null if there is nothing to delete.
355 */
356 public static Command delete(OsmDataLayer layer, Collection<? extends OsmPrimitive> selection,
357 boolean alsoDeleteNodesInWay, boolean silent) {
358 if (selection == null || selection.isEmpty())
359 return null;
360
361 BackreferencedDataSet backreferences = new BackreferencedDataSet(layer.data);
362 backreferences.build();
363
364 Set<OsmPrimitive> primitivesToDelete = new HashSet<OsmPrimitive>(selection);
365 Collection<Way> waysToBeChanged = new HashSet<Way>();
366
367 if (alsoDeleteNodesInWay) {
368 // delete untagged nodes only referenced by primitives in primitivesToDelete,
369 // too
370 Collection<Node> nodesToDelete = computeNodesToDelete(backreferences, layer, primitivesToDelete);
371 primitivesToDelete.addAll(nodesToDelete);
372 }
373
374 if (!silent && !checkAndConfirmOutlyingDeletes(layer,primitivesToDelete))
375 return null;
376
377 waysToBeChanged.addAll(OsmPrimitive.getFilteredSet(backreferences.getParents(primitivesToDelete), Way.class));
378
379 Collection<Command> cmds = new LinkedList<Command>();
380 for (Way w : waysToBeChanged) {
381 Way wnew = new Way(w);
382 wnew.removeNodes(primitivesToDelete);
383 if (wnew.getNodesCount() < 2) {
384 primitivesToDelete.add(w);
385 } else {
386 cmds.add(new ChangeCommand(w, wnew));
387 }
388 }
389
390 // get a confirmation that the objects to delete can be removed from their parent
391 // relations
392 //
393 if (!silent) {
394 Set<RelationToChildReference> references = backreferences.getRelationToChildReferences(primitivesToDelete);
395 Iterator<RelationToChildReference> it = references.iterator();
396 while(it.hasNext()) {
397 RelationToChildReference ref = it.next();
398 if (ref.getParent().isDeleted()) {
399 it.remove();
400 }
401 }
402 if (!references.isEmpty()) {
403 DeleteFromRelationConfirmationDialog dialog = DeleteFromRelationConfirmationDialog.getInstance();
404 dialog.getModel().populate(references);
405 dialog.setVisible(true);
406 if (dialog.isCanceled())
407 return null;
408 }
409 }
410
411 // remove the objects from their parent relations
412 //
413 Iterator<Relation> iterator = OsmPrimitive.getFilteredSet(backreferences.getParents(primitivesToDelete), Relation.class).iterator();
414 while (iterator.hasNext()) {
415 Relation cur = iterator.next();
416 Relation rel = new Relation(cur);
417 rel.removeMembersFor(primitivesToDelete);
418 cmds.add(new ChangeCommand(cur, rel));
419 }
420
421 // build the delete command
422 //
423 if (!primitivesToDelete.isEmpty()) {
424 cmds.add(new DeleteCommand(layer,primitivesToDelete));
425 }
426
427 return new SequenceCommand(tr("Delete"), cmds);
428 }
429
430 public static Command deleteWaySegment(OsmDataLayer layer, WaySegment ws) {
431 if (ws.way.getNodesCount() < 3)
432 return new DeleteCommand(layer, Collections.singleton(ws.way));
433
434 if (ws.way.firstNode() == ws.way.lastNode()) {
435 // If the way is circular (first and last nodes are the same),
436 // the way shouldn't be splitted
437
438 List<Node> n = new ArrayList<Node>();
439
440 n.addAll(ws.way.getNodes().subList(ws.lowerIndex + 1, ws.way.getNodesCount() - 1));
441 n.addAll(ws.way.getNodes().subList(0, ws.lowerIndex + 1));
442
443 Way wnew = new Way(ws.way);
444 wnew.setNodes(n);
445
446 return new ChangeCommand(ws.way, wnew);
447 }
448
449 List<Node> n1 = new ArrayList<Node>(), n2 = new ArrayList<Node>();
450
451 n1.addAll(ws.way.getNodes().subList(0, ws.lowerIndex + 1));
452 n2.addAll(ws.way.getNodes().subList(ws.lowerIndex + 1, ws.way.getNodesCount()));
453
454 Way wnew = new Way(ws.way);
455
456 if (n1.size() < 2) {
457 wnew.setNodes(n2);
458 return new ChangeCommand(ws.way, wnew);
459 } else if (n2.size() < 2) {
460 wnew.setNodes(n1);
461 return new ChangeCommand(ws.way, wnew);
462 } else {
463 Collection<Command> cmds = new LinkedList<Command>();
464
465 wnew.setNodes(n1);
466 cmds.add(new ChangeCommand(ws.way, wnew));
467
468 Way wnew2 = new Way();
469 wnew2.setKeys(wnew.getKeys());
470 wnew2.setNodes(n2);
471 cmds.add(new AddCommand(wnew2));
472
473 // FIXME: relation memberships are not handled
474
475 return new SequenceCommand(tr("Split way segment"), cmds);
476 }
477 }
478
479 /**
480 * Check whether user is about to delete data outside of the download area. Request confirmation
481 * if he is.
482 *
483 * @param layer the layer in whose context data is deleted
484 * @param primitivesToDelete the primitives to delete
485 * @return true, if deleting outlying primitives is OK; false, otherwise
486 */
487 private static boolean checkAndConfirmOutlyingDeletes(OsmDataLayer layer, Collection<OsmPrimitive> primitivesToDelete) {
488 Area a = layer.data.getDataSourceArea();
489 if (a != null) {
490 for (OsmPrimitive osm : primitivesToDelete) {
491 if (osm instanceof Node && !osm.isNew()) {
492 Node n = (Node) osm;
493 if (!a.contains(n.getCoor())) {
494 JPanel msg = new JPanel(new GridBagLayout());
495 msg.add(new JLabel(
496 "<html>" +
497 // leave message in one tr() as there is a grammatical
498 // connection.
499 tr("You are about to delete nodes outside of the area you have downloaded."
500 + "<br>"
501 + "This can cause problems because other objects (that you don't see) might use them."
502 + "<br>" + "Do you really want to delete?") + "</html>"));
503 return ConditionalOptionPaneUtil.showConfirmationDialog(
504 "delete_outside_nodes",
505 Main.parent,
506 msg,
507 tr("Delete confirmation"),
508 JOptionPane.YES_NO_OPTION,
509 JOptionPane.QUESTION_MESSAGE,
510 JOptionPane.YES_OPTION
511 );
512 }
513 }
514 }
515 }
516 return true;
517 }
518}
Note: See TracBrowser for help on using the repository browser.