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

Last change on this file since 3102 was 3055, checked in by jttt, 14 years ago

In DeleteCommand remove references first and then do the deleting (to prevent delete referenced inconsistency problem while command is being executed)

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