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

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

fixed #2707: Deleting a way with new nodes produces invalid osm file

  • Property svn:eol-style set to native
File size: 15.2 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;
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.Node;
26import org.openstreetmap.josm.data.osm.OsmPrimitive;
27import org.openstreetmap.josm.data.osm.Relation;
28import org.openstreetmap.josm.data.osm.RelationMember;
29import org.openstreetmap.josm.data.osm.Way;
30import org.openstreetmap.josm.data.osm.WaySegment;
31import org.openstreetmap.josm.data.osm.visitor.CollectBackReferencesVisitor;
32import org.openstreetmap.josm.data.osm.visitor.NameVisitor;
33import org.openstreetmap.josm.gui.ExtendedDialog;
34import org.openstreetmap.josm.tools.DontShowAgainInfo;
35import org.openstreetmap.josm.tools.ImageProvider;
36
37/**
38 * A command to delete a number of primitives from the dataset.
39 * @author imi
40 */
41public class DeleteCommand extends Command {
42
43 /**
44 * The primitive that get deleted.
45 */
46 private final Collection<? extends OsmPrimitive> data;
47
48 /**
49 * Constructor for a collection of data
50 */
51 public DeleteCommand(Collection<? extends OsmPrimitive> data) {
52 this.data = data;
53 }
54
55 /**
56 * Constructor for a single data item. Use the collection constructor to delete multiple
57 * objects.
58 */
59 public DeleteCommand(OsmPrimitive data) {
60 this.data = Collections.singleton(data);
61 }
62
63 @Override public boolean executeCommand() {
64 super.executeCommand();
65 for (OsmPrimitive osm : data) {
66 osm.delete(true);
67 }
68 return true;
69 }
70
71 @Override public void fillModifiedData(Collection<OsmPrimitive> modified, Collection<OsmPrimitive> deleted,
72 Collection<OsmPrimitive> added) {
73 deleted.addAll(data);
74 }
75
76 @Override public MutableTreeNode description() {
77 NameVisitor v = new NameVisitor();
78
79 if (data.size() == 1) {
80 data.iterator().next().visit(v);
81 return new DefaultMutableTreeNode(new JLabel(tr("Delete {1} {0}", v.name, tr(v.className)), v.icon,
82 JLabel.HORIZONTAL));
83 }
84
85 String cname = null;
86 String cnamem = null;
87 for (OsmPrimitive osm : data) {
88 osm.visit(v);
89 if (cname == null) {
90 cname = v.className;
91 cnamem = v.classNamePlural;
92 } else if (!cname.equals(v.className)) {
93 cname = "object";
94 cnamem = trn("object", "objects", 2);
95 }
96 }
97 DefaultMutableTreeNode root = new DefaultMutableTreeNode(new JLabel(tr("Delete {0} {1}", data.size(), trn(
98 cname, cnamem, data.size())), ImageProvider.get("data", cname), JLabel.HORIZONTAL));
99 for (OsmPrimitive osm : data) {
100 osm.visit(v);
101 root.add(new DefaultMutableTreeNode(v.toLabel()));
102 }
103 return root;
104 }
105
106 /**
107 * Delete the primitives and everything they reference.
108 *
109 * If a node is deleted, the node and all ways and relations the node is part of are deleted as
110 * well.
111 *
112 * If a way is deleted, all relations the way is member of are also deleted.
113 *
114 * If a way is deleted, only the way and no nodes are deleted.
115 *
116 * @param selection The list of all object to be deleted.
117 * @return command A command to perform the deletions, or null of there is nothing to delete.
118 */
119 public static Command deleteWithReferences(Collection<? extends OsmPrimitive> selection) {
120 CollectBackReferencesVisitor v = new CollectBackReferencesVisitor(Main.ds);
121 for (OsmPrimitive osm : selection) {
122 osm.visit(v);
123 }
124 v.data.addAll(selection);
125 if (v.data.isEmpty())
126 return null;
127 if (!checkAndConfirmOutlyingDeletes(v.data))
128 return null;
129 return new DeleteCommand(v.data);
130 }
131
132 private static int testRelation(Relation ref, OsmPrimitive osm) {
133 NameVisitor n = new NameVisitor();
134 ref.visit(n);
135 NameVisitor s = new NameVisitor();
136 osm.visit(s);
137 String role = new String();
138 for (RelationMember m : ref.members) {
139 if (m.member == osm) {
140 role = m.role;
141 break;
142 }
143 }
144 if (role.length() > 0)
145 return new ExtendedDialog(Main.parent,
146 tr("Conflicting relation"),
147 tr("Selection \"{0}\" is used by relation \"{1}\" with role {2}.\nDelete from relation?",
148 s.name, n.name, role),
149 new String[] {tr("Delete from relation"), tr("Cancel")},
150 new String[] {"dialogs/delete.png", "cancel.png"}).getValue();
151 else
152 return new ExtendedDialog(Main.parent,
153 tr("Conflicting relation"),
154 tr("Selection \"{0}\" is used by relation \"{1}\".\nDelete from relation?",
155 s.name, n.name),
156 new String[] {tr("Delete from relation"), tr("Cancel")},
157 new String[] {"dialogs/delete.png", "cancel.png"}).getValue();
158 }
159
160 public static Command delete(Collection<? extends OsmPrimitive> selection) {
161 return delete(selection, true);
162 }
163
164 /**
165 * Try to delete all given primitives.
166 *
167 * If a node is used by a way, it's removed from that way. If a node or a way is used by a
168 * relation, inform the user and do not delete.
169 *
170 * If this would cause ways with less than 2 nodes to be created, delete these ways instead. If
171 * they are part of a relation, inform the user and do not delete.
172 *
173 * @param selection The objects to delete.
174 * @param alsoDeleteNodesInWay <code>true</code> if nodes should be deleted as well
175 * @return command A command to perform the deletions, or null of there is nothing to delete.
176 */
177 public static Command delete(Collection<? extends OsmPrimitive> selection, boolean alsoDeleteNodesInWay) {
178 if (selection.isEmpty())
179 return null;
180
181 Collection<OsmPrimitive> del = new HashSet<OsmPrimitive>(selection);
182 Collection<Way> waysToBeChanged = new HashSet<Way>();
183 HashMap<OsmPrimitive, Collection<OsmPrimitive>> relationsToBeChanged = new HashMap<OsmPrimitive, Collection<OsmPrimitive>>();
184
185 if (alsoDeleteNodesInWay) {
186 // Delete untagged nodes that are to be unreferenced.
187 Collection<OsmPrimitive> delNodes = new HashSet<OsmPrimitive>();
188 for (OsmPrimitive osm : del) {
189 if (osm instanceof Way) {
190 for (Node n : ((Way) osm).nodes) {
191 if (!n.isTagged()) {
192 CollectBackReferencesVisitor v = new CollectBackReferencesVisitor(Main.ds, false);
193 n.visit(v);
194 v.data.removeAll(del);
195 if (v.data.isEmpty()) {
196 delNodes.add(n);
197 }
198 }
199 }
200 }
201 }
202 del.addAll(delNodes);
203 }
204
205 if (!checkAndConfirmOutlyingDeletes(del))
206 return null;
207
208 for (OsmPrimitive osm : del) {
209 CollectBackReferencesVisitor v = new CollectBackReferencesVisitor(Main.ds, false);
210 osm.visit(v);
211 for (OsmPrimitive ref : v.data) {
212 if (del.contains(ref)) {
213 continue;
214 }
215 if (ref instanceof Way) {
216 waysToBeChanged.add((Way) ref);
217 } else if (ref instanceof Relation) {
218 if (testRelation((Relation) ref, osm) == 1) {
219 Collection<OsmPrimitive> relset = relationsToBeChanged.get(ref);
220 if (relset == null) {
221 relset = new HashSet<OsmPrimitive>();
222 }
223 relset.add(osm);
224 relationsToBeChanged.put(ref, relset);
225 } else
226 return null;
227 } else
228 return null;
229 }
230 }
231
232 Collection<Command> cmds = new LinkedList<Command>();
233 for (Way w : waysToBeChanged) {
234 Way wnew = new Way(w);
235 wnew.removeNodes(del);
236 if (wnew.nodes.size() < 2) {
237 del.add(w);
238
239 CollectBackReferencesVisitor v = new CollectBackReferencesVisitor(Main.ds, false);
240 w.visit(v);
241 for (OsmPrimitive ref : v.data) {
242 if (del.contains(ref)) {
243 continue;
244 }
245 if (ref instanceof Relation) {
246 Boolean found = false;
247 Collection<OsmPrimitive> relset = relationsToBeChanged.get(ref);
248 if (relset == null) {
249 relset = new HashSet<OsmPrimitive>();
250 } else {
251 for (OsmPrimitive m : relset) {
252 if (m == w) {
253 found = true;
254 break;
255 }
256 }
257 }
258 if (!found) {
259 if (testRelation((Relation) ref, w) == 1) {
260 relset.add(w);
261 relationsToBeChanged.put(ref, relset);
262 } else
263 return null;
264 }
265 } else
266 return null;
267 }
268 } else {
269 cmds.add(new ChangeCommand(w, wnew));
270 }
271 }
272
273 Iterator<OsmPrimitive> iterator = relationsToBeChanged.keySet().iterator();
274 while (iterator.hasNext()) {
275 Relation cur = (Relation) iterator.next();
276 Relation rel = new Relation(cur);
277 for (OsmPrimitive osm : relationsToBeChanged.get(cur)) {
278 for (RelationMember rm : rel.members) {
279 if (rm.member == osm) {
280 RelationMember mem = new RelationMember();
281 mem.role = rm.role;
282 mem.member = rm.member;
283 rel.members.remove(mem);
284 break;
285 }
286 }
287 }
288 cmds.add(new ChangeCommand(cur, rel));
289 }
290
291 // #2707: ways to be deleted can include new nodes (with node.id == 0).
292 // Remove them from the way before the way is deleted. Otherwise the
293 // deleted way is saved (or sent to the API) with a dangling reference to a node
294 // Example:
295 // <node id='2' action='delete' visible='true' version='1' ... />
296 // <node id='1' action='delete' visible='true' version='1' ... />
297 // <!-- missing node with id -1 because new deleted nodes are not persisted -->
298 // <way id='3' action='delete' visible='true' version='1'>
299 // <nd ref='1' />
300 // <nd ref='-1' /> <!-- heres the problem -->
301 // <nd ref='2' />
302 // </way>
303 for (OsmPrimitive primitive : del) {
304 if (! (primitive instanceof Way)) {
305 continue;
306 }
307 Way w = (Way)primitive;
308 if (w.id == 0) { // new ways with id == 0 are fine,
309 continue; // process existing ways only
310 }
311 Way wnew = new Way(w);
312 ArrayList<Node> nodesToStrip = new ArrayList<Node>();
313 // lookup new nodes which have been added to the set of deleted
314 // nodes ...
315 for (Node n : wnew.nodes) {
316 if (n.id == 0 && del.contains(n)) {
317 nodesToStrip.add(n);
318 }
319 }
320 // .. and remove them from the way
321 //
322 wnew.nodes.removeAll(nodesToStrip);
323 if (!nodesToStrip.isEmpty()) {
324 cmds.add(new ChangeCommand(w,wnew));
325 }
326 }
327
328 if (!del.isEmpty()) {
329 cmds.add(new DeleteCommand(del));
330 }
331
332 return new SequenceCommand(tr("Delete"), cmds);
333 }
334
335 public static Command deleteWaySegment(WaySegment ws) {
336 List<Node> n1 = new ArrayList<Node>(), n2 = new ArrayList<Node>();
337
338 n1.addAll(ws.way.nodes.subList(0, ws.lowerIndex + 1));
339 n2.addAll(ws.way.nodes.subList(ws.lowerIndex + 1, ws.way.nodes.size()));
340
341 if (n1.size() < 2 && n2.size() < 2)
342 return new DeleteCommand(Collections.singleton(ws.way));
343
344 Way wnew = new Way(ws.way);
345 wnew.nodes.clear();
346
347 if (n1.size() < 2) {
348 wnew.nodes.addAll(n2);
349 return new ChangeCommand(ws.way, wnew);
350 } else if (n2.size() < 2) {
351 wnew.nodes.addAll(n1);
352 return new ChangeCommand(ws.way, wnew);
353 } else {
354 Collection<Command> cmds = new LinkedList<Command>();
355
356 wnew.nodes.addAll(n1);
357 cmds.add(new ChangeCommand(ws.way, wnew));
358
359 Way wnew2 = new Way();
360 if (wnew.keys != null) {
361 wnew2.keys = new HashMap<String, String>(wnew.keys);
362 }
363 wnew2.nodes.addAll(n2);
364 cmds.add(new AddCommand(wnew2));
365
366 return new SequenceCommand(tr("Split way segment"), cmds);
367 }
368 }
369
370 /**
371 * Check whether user is about to delete data outside of the download area.
372 * Request confirmation if he is.
373 */
374 private static boolean checkAndConfirmOutlyingDeletes(Collection<OsmPrimitive> del) {
375 Area a = Main.ds.getDataSourceArea();
376 if (a != null) {
377 for (OsmPrimitive osm : del) {
378 if (osm instanceof Node && osm.id != 0) {
379 Node n = (Node) osm;
380 if (!a.contains(n.getCoor())) {
381 JPanel msg = new JPanel(new GridBagLayout());
382 msg.add(new JLabel(
383 "<html>" +
384 // leave message in one tr() as there is a grammatical connection.
385 tr("You are about to delete nodes outside of the area you have downloaded." +
386 "<br>" +
387 "This can cause problems because other objects (that you don't see) might use them." +
388 "<br>" +
389 "Do you really want to delete?") + "</html>"));
390 return DontShowAgainInfo.show("delete_outside_nodes", msg, false, JOptionPane.YES_NO_OPTION, JOptionPane.YES_OPTION);
391 }
392
393 }
394 }
395 }
396 return true;
397 }
398}
Note: See TracBrowser for help on using the repository browser.