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

Last change on this file since 2296 was 2296, checked in by stoecker, 15 years ago

see #3703 - patch by Daeron - deleting way segment cleanup

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