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

Last change on this file since 2602 was 2565, checked in by Gubaer, 14 years ago

Removed BackReferenceDataSet and CollectBackReferenceVisitor because we now have referrer support in OsmPrimitive.
This could cause some problems in the next few days. I'm sure I didn't test every possibly affected feature.

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