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

Last change on this file since 2349 was 2333, checked in by Gubaer, 14 years ago

fixed #3753: Merging the ends of two node ways produces corrupt way

  • Property svn:eol-style set to native
File size: 20.0 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.Way;
31import org.openstreetmap.josm.data.osm.WaySegment;
32import org.openstreetmap.josm.data.osm.BackreferencedDataSet.RelationToChildReference;
33import org.openstreetmap.josm.data.osm.visitor.CollectBackReferencesVisitor;
34import org.openstreetmap.josm.gui.ConditionalOptionPaneUtil;
35import org.openstreetmap.josm.gui.DefaultNameFormatter;
36import org.openstreetmap.josm.gui.actionsupport.DeleteFromRelationConfirmationDialog;
37import org.openstreetmap.josm.gui.layer.OsmDataLayer;
38import org.openstreetmap.josm.tools.ImageProvider;
39
40/**
41 * A command to delete a number of primitives from the dataset.
42 *
43 */
44public class DeleteCommand extends Command {
45 /**
46 * The primitives that get deleted.
47 */
48 private final Collection<? extends OsmPrimitive> toDelete;
49
50 /**
51 * Constructor. Deletes a collection of primitives in the current edit layer.
52 *
53 * @param data the primitives to delete. Must neither be null nor empty.
54 * @throws IllegalArgumentException thrown if data is null or empty
55 */
56 public DeleteCommand(Collection<? extends OsmPrimitive> data) throws IllegalArgumentException {
57 if (data == null)
58 throw new IllegalArgumentException(tr("Parameter ''{0}'' must not be empty"));
59 if (data.isEmpty())
60 throw new IllegalArgumentException(tr("At least one object to delete requird, got empty collection"));
61 this.toDelete = data;
62 }
63
64 /**
65 * Constructor. Deletes a single primitive in the current edit layer.
66 *
67 * @param data the primitive to delete. Must not be null.
68 * @throws IllegalArgumentException thrown if data is null
69 */
70 public DeleteCommand(OsmPrimitive data) throws IllegalArgumentException {
71 if (data == null)
72 throw new IllegalArgumentException(tr("Parameter ''{0}'' must not be null", "data"));
73 this.toDelete = Collections.singleton(data);
74 }
75
76 /**
77 * Constructor for a single data item. Use the collection constructor to delete multiple
78 * objects.
79 *
80 * @param layer the layer context for deleting this primitive. Must not be null.
81 * @param data the primitive to delete. Must not be null.
82 * @throws IllegalArgumentException thrown if data is null
83 * @throws IllegalArgumentException thrown if layer is null
84 */
85 public DeleteCommand(OsmDataLayer layer, OsmPrimitive data) throws IllegalArgumentException {
86 super(layer);
87 if (data == null)
88 throw new IllegalArgumentException(tr("Parameter ''{0}'' must not be null", "data"));
89 this.toDelete = Collections.singleton(data);
90 }
91
92 /**
93 * Constructor for a collection of data to be deleted in the context of
94 * a specific layer
95 *
96 * @param layer the layer context for deleting these primitives. Must not be null.
97 * @param data the primitives to delete. Must neither be null nor empty.
98 * @throws IllegalArgumentException thrown if layer is null
99 * @throws IllegalArgumentException thrown if data is null or empty
100 */
101 public DeleteCommand(OsmDataLayer layer, Collection<? extends OsmPrimitive> data) throws IllegalArgumentException{
102 super(layer);
103 if (data == null)
104 throw new IllegalArgumentException(tr("Parameter ''{0}'' must not be empty"));
105 if (data.isEmpty())
106 throw new IllegalArgumentException(tr("At least one object to delete requird, got empty collection"));
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 public static Command delete(OsmDataLayer layer, Collection<? extends OsmPrimitive> selection) {
237 return delete(layer, selection, true, false);
238 }
239
240 /**
241 * Replies the collection of nodes referred to by primitives in <code>primitivesToDelete</code> which
242 * can be deleted too. A node can be deleted if
243 * <ul>
244 * <li>it is untagged (see {@see Node#isTagged()}</li>
245 * <li>it is not referred to by other non-deleted primitives outside of <code>primitivesToDelete</code></li>
246 * <ul>
247 * @param backreferences backreference data structure
248 * @param layer the layer in whose context primitives are deleted
249 * @param primitivesToDelete the primitives to delete
250 * @return the collection of nodes referred to by primitives in <code>primitivesToDelete</code> which
251 * can be deleted too
252 */
253 protected static Collection<Node> computeNodesToDelete(BackreferencedDataSet backreferences, OsmDataLayer layer, Collection<OsmPrimitive> primitivesToDelete) {
254 Collection<Node> nodesToDelete = new HashSet<Node>();
255 //CollectBackReferencesVisitor v = new CollectBackReferencesVisitor(layer.data, false);
256 for (Way way : OsmPrimitive.getFilteredList(primitivesToDelete, Way.class)) {
257 for (Node n : way.getNodes()) {
258 if (n.isTagged()) {
259 continue;
260 }
261 //v.initialize();
262 //n.visit(v);
263 Collection<OsmPrimitive> referringPrimitives = backreferences.getParents(n);
264 referringPrimitives.removeAll(primitivesToDelete);
265 int count = 0;
266 for (OsmPrimitive p : referringPrimitives) {
267 if (!p.isDeleted()) {
268 count++;
269 }
270 }
271 if (count == 0) {
272 nodesToDelete.add(n);
273 }
274 }
275 }
276 return nodesToDelete;
277 }
278
279 /**
280 * Try to delete all given primitives.
281 *
282 * If a node is used by a way, it's removed from that way. If a node or a way is used by a
283 * relation, inform the user and do not delete.
284 *
285 * If this would cause ways with less than 2 nodes to be created, delete these ways instead. If
286 * they are part of a relation, inform the user and do not delete.
287 *
288 * @param layer the {@see OsmDataLayer} in whose context the primitives are deleted
289 * @param selection the objects to delete.
290 * @param alsoDeleteNodesInWay <code>true</code> if nodes should be deleted as well
291 * @return command a command to perform the deletions, or null if there is nothing to delete.
292 */
293 public static Command delete(OsmDataLayer layer, Collection<? extends OsmPrimitive> selection,
294 boolean alsoDeleteNodesInWay) {
295 return delete(layer, selection, alsoDeleteNodesInWay, false /* not silent */);
296 }
297
298 /**
299 * Try to delete all given primitives.
300 *
301 * If a node is used by a way, it's removed from that way. If a node or a way is used by a
302 * relation, inform the user and do not delete.
303 *
304 * If this would cause ways with less than 2 nodes to be created, delete these ways instead. If
305 * they are part of a relation, inform the user and do not delete.
306 *
307 * @param layer the {@see OsmDataLayer} in whose context the primitives are deleted
308 * @param selection the objects to delete.
309 * @param alsoDeleteNodesInWay <code>true</code> if nodes should be deleted as well
310 * @param silent set to true if the user should not be bugged with additional questions
311 * @return command a command to perform the deletions, or null if there is nothing to delete.
312 */
313 public static Command delete(OsmDataLayer layer, Collection<? extends OsmPrimitive> selection,
314 boolean alsoDeleteNodesInWay, boolean silent) {
315 if (selection == null || selection.isEmpty())
316 return null;
317
318 BackreferencedDataSet backreferences = new BackreferencedDataSet(layer.data);
319 backreferences.build();
320
321 Set<OsmPrimitive> primitivesToDelete = new HashSet<OsmPrimitive>(selection);
322 Collection<Way> waysToBeChanged = new HashSet<Way>();
323
324 if (alsoDeleteNodesInWay) {
325 // delete untagged nodes only referenced by primitives in primitivesToDelete,
326 // too
327 Collection<Node> nodesToDelete = computeNodesToDelete(backreferences, layer, primitivesToDelete);
328 primitivesToDelete.addAll(nodesToDelete);
329 }
330
331 if (!silent && !checkAndConfirmOutlyingDeletes(layer,primitivesToDelete))
332 return null;
333
334 waysToBeChanged.addAll(OsmPrimitive.getFilteredSet(backreferences.getParents(primitivesToDelete), Way.class));
335
336 Collection<Command> cmds = new LinkedList<Command>();
337 for (Way w : waysToBeChanged) {
338 Way wnew = new Way(w);
339 wnew.removeNodes(primitivesToDelete);
340 if (wnew.getNodesCount() < 2) {
341 primitivesToDelete.add(w);
342 } else {
343 cmds.add(new ChangeCommand(w, wnew));
344 }
345 }
346
347 // get a confirmation that the objects to delete can be removed from their parent
348 // relations
349 //
350 if (!silent) {
351 Set<RelationToChildReference> references = backreferences.getRelationToChildReferences(primitivesToDelete);
352 Iterator<RelationToChildReference> it = references.iterator();
353 while(it.hasNext()) {
354 RelationToChildReference ref = it.next();
355 if (ref.getParent().isDeleted()) {
356 it.remove();
357 }
358 }
359 if (!references.isEmpty()) {
360 DeleteFromRelationConfirmationDialog dialog = DeleteFromRelationConfirmationDialog.getInstance();
361 dialog.getModel().populate(references);
362 dialog.setVisible(true);
363 if (dialog.isCanceled())
364 return null;
365 }
366 }
367
368 // remove the objects from their parent relations
369 //
370 Iterator<Relation> iterator = OsmPrimitive.getFilteredSet(backreferences.getParents(primitivesToDelete), Relation.class).iterator();
371 while (iterator.hasNext()) {
372 Relation cur = iterator.next();
373 Relation rel = new Relation(cur);
374 rel.removeMembersFor(primitivesToDelete);
375 cmds.add(new ChangeCommand(cur, rel));
376 }
377
378 // build the delete command
379 //
380 if (!primitivesToDelete.isEmpty()) {
381 cmds.add(new DeleteCommand(layer,primitivesToDelete));
382 }
383
384 return new SequenceCommand(tr("Delete"), cmds);
385 }
386
387 public static Command deleteWaySegment(OsmDataLayer layer, WaySegment ws) {
388 if (ws.way.getNodesCount() < 3)
389 return new DeleteCommand(layer, Collections.singleton(ws.way));
390
391 if (ws.way.firstNode() == ws.way.lastNode()) {
392 // If the way is circular (first and last nodes are the same),
393 // the way shouldn't be splitted
394
395 List<Node> n = new ArrayList<Node>();
396
397 n.addAll(ws.way.getNodes().subList(ws.lowerIndex + 1, ws.way.getNodesCount() - 1));
398 n.addAll(ws.way.getNodes().subList(0, ws.lowerIndex + 1));
399
400 Way wnew = new Way(ws.way);
401 wnew.setNodes(n);
402
403 return new ChangeCommand(ws.way, wnew);
404 }
405
406 List<Node> n1 = new ArrayList<Node>(), n2 = new ArrayList<Node>();
407
408 n1.addAll(ws.way.getNodes().subList(0, ws.lowerIndex + 1));
409 n2.addAll(ws.way.getNodes().subList(ws.lowerIndex + 1, ws.way.getNodesCount()));
410
411 Way wnew = new Way(ws.way);
412
413 if (n1.size() < 2) {
414 wnew.setNodes(n2);
415 return new ChangeCommand(ws.way, wnew);
416 } else if (n2.size() < 2) {
417 wnew.setNodes(n1);
418 return new ChangeCommand(ws.way, wnew);
419 } else {
420 Collection<Command> cmds = new LinkedList<Command>();
421
422 wnew.setNodes(n1);
423 cmds.add(new ChangeCommand(ws.way, wnew));
424
425 Way wnew2 = new Way();
426 wnew2.setKeys(wnew.getKeys());
427 wnew2.setNodes(n2);
428 cmds.add(new AddCommand(wnew2));
429
430 // FIXME: relation memberships are not handled
431
432 return new SequenceCommand(tr("Split way segment"), cmds);
433 }
434 }
435
436 /**
437 * Check whether user is about to delete data outside of the download area. Request confirmation
438 * if he is.
439 *
440 * @param layer the layer in whose context data is deleted
441 * @param primitivesToDelete the primitives to delete
442 * @return true, if deleting outlying primitives is OK; false, otherwise
443 */
444 private static boolean checkAndConfirmOutlyingDeletes(OsmDataLayer layer, Collection<OsmPrimitive> primitivesToDelete) {
445 Area a = layer.data.getDataSourceArea();
446 if (a != null) {
447 for (OsmPrimitive osm : primitivesToDelete) {
448 if (osm instanceof Node && !osm.isNew()) {
449 Node n = (Node) osm;
450 if (!a.contains(n.getCoor())) {
451 JPanel msg = new JPanel(new GridBagLayout());
452 msg.add(new JLabel(
453 "<html>" +
454 // leave message in one tr() as there is a grammatical
455 // connection.
456 tr("You are about to delete nodes outside of the area you have downloaded."
457 + "<br>"
458 + "This can cause problems because other objects (that you don't see) might use them."
459 + "<br>" + "Do you really want to delete?") + "</html>"));
460 return ConditionalOptionPaneUtil.showConfirmationDialog(
461 "delete_outside_nodes",
462 Main.parent,
463 msg,
464 tr("Delete confirmation"),
465 JOptionPane.YES_NO_OPTION,
466 JOptionPane.QUESTION_MESSAGE,
467 JOptionPane.YES_OPTION
468 );
469 }
470 }
471 }
472 }
473 return true;
474 }
475}
Note: See TracBrowser for help on using the repository browser.