source: josm/trunk/src/org/openstreetmap/josm/actions/UnGlueAction.java@ 4458

Last change on this file since 4458 was 4458, checked in by simon04, 13 years ago

fix #3951 - user should be warned when unglue-ing two ways outside the download area

  • Property svn:eol-style set to native
File size: 16.0 KB
Line 
1// License: GPL. Copyright 2007 by Immanuel Scholz and others
2package org.openstreetmap.josm.actions;
3
4import static org.openstreetmap.josm.gui.help.HelpUtil.ht;
5import static org.openstreetmap.josm.tools.I18n.tr;
6import static org.openstreetmap.josm.tools.I18n.trn;
7
8import java.awt.event.ActionEvent;
9import java.awt.event.KeyEvent;
10import java.util.ArrayList;
11import java.util.Collection;
12import java.util.Collections;
13import java.util.HashSet;
14import java.util.LinkedList;
15import java.util.List;
16
17import javax.swing.JOptionPane;
18import javax.swing.JPanel;
19
20import org.openstreetmap.josm.Main;
21import org.openstreetmap.josm.command.AddCommand;
22import org.openstreetmap.josm.command.ChangeCommand;
23import org.openstreetmap.josm.command.Command;
24import org.openstreetmap.josm.command.SequenceCommand;
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.gui.MapView;
31import org.openstreetmap.josm.tools.Shortcut;
32
33/**
34 * Duplicate nodes that are used by multiple ways.
35 *
36 * Resulting nodes are identical, up to their position.
37 *
38 * This is the opposite of the MergeNodesAction.
39 *
40 * If a single node is selected, it will copy that node and remove all tags from the old one
41 */
42
43public class UnGlueAction extends JosmAction {
44
45 private Node selectedNode;
46 private Way selectedWay;
47 private ArrayList<Node> selectedNodes;
48
49 /**
50 * Create a new UnGlueAction.
51 */
52 public UnGlueAction() {
53 super(tr("UnGlue Ways"), "unglueways", tr("Duplicate nodes that are used by multiple ways."),
54 Shortcut.registerShortcut("tools:unglue", tr("Tool: {0}", tr("UnGlue Ways")), KeyEvent.VK_G, Shortcut.GROUP_EDIT), true);
55 putValue("help", ht("/Action/UnGlue"));
56 }
57
58 /**
59 * Called when the action is executed.
60 *
61 * This method does some checking on the selection and calls the matching unGlueWay method.
62 */
63 @Override
64 public void actionPerformed(ActionEvent e) {
65
66 Collection<OsmPrimitive> selection = getCurrentDataSet().getSelected();
67
68 String errMsg = null;
69 if (checkSelection(selection)) {
70 if (!checkAndConfirmOutlyingUnglue()) {
71 return;
72 }
73 int count = 0;
74 for (Way w : OsmPrimitive.getFilteredList(selectedNode.getReferrers(), Way.class)) {
75 if (!w.isUsable() || w.getNodesCount() < 1) {
76 continue;
77 }
78 count++;
79 }
80 if (count < 2) {
81 // If there aren't enough ways, maybe the user wanted to unglue the nodes
82 // (= copy tags to a new node)
83 if (checkForUnglueNode(selection)) {
84 unglueNode(e);
85 } else {
86 errMsg = tr("This node is not glued to anything else.");
87 }
88 } else {
89 // and then do the work.
90 unglueWays();
91 }
92 } else if (checkSelection2(selection)) {
93 if (!checkAndConfirmOutlyingUnglue()) {
94 return;
95 }
96 ArrayList<Node> tmpNodes = new ArrayList<Node>();
97 for (Node n : selectedNodes) {
98 int count = 0;
99 for (Way w : OsmPrimitive.getFilteredList(n.getReferrers(), Way.class)) {
100 if (!w.isUsable()) {
101 continue;
102 }
103 count++;
104 }
105 if (count >= 2) {
106 tmpNodes.add(n);
107 }
108 }
109 if (tmpNodes.size() < 1) {
110 if (selection.size() > 1) {
111 errMsg = tr("None of these nodes are glued to anything else.");
112 } else {
113 errMsg = tr("None of this way''s nodes are glued to anything else.");
114 }
115 } else {
116 // and then do the work.
117 selectedNodes = tmpNodes;
118 unglueWays2();
119 }
120 } else {
121 errMsg =
122 tr("The current selection cannot be used for unglueing.")+"\n"+
123 "\n"+
124 tr("Select either:")+"\n"+
125 tr("* One tagged node, or")+"\n"+
126 tr("* One node that is used by more than one way, or")+"\n"+
127 tr("* One node that is used by more than one way and one of those ways, or")+"\n"+
128 tr("* One way that has one or more nodes that are used by more than one way, or")+"\n"+
129 tr("* One way and one or more of its nodes that are used by more than one way.")+"\n"+
130 "\n"+
131 tr("Note: If a way is selected, this way will get fresh copies of the unglued\n"+
132 "nodes and the new nodes will be selected. Otherwise, all ways will get their\n"+
133 "own copy and all nodes will be selected.");
134 }
135
136 if(errMsg != null) {
137 JOptionPane.showMessageDialog(
138 Main.parent,
139 errMsg,
140 tr("Error"),
141 JOptionPane.ERROR_MESSAGE);
142 }
143
144 selectedNode = null;
145 selectedWay = null;
146 selectedNodes = null;
147 }
148
149 /**
150 * Assumes there is one tagged Node stored in selectedNode that it will try to unglue
151 * (= copy node and remove all tags from the old one. Relations will not be removed)
152 */
153 private void unglueNode(ActionEvent e) {
154 LinkedList<Command> cmds = new LinkedList<Command>();
155
156 Node c = new Node(selectedNode);
157 c.removeAll();
158 getCurrentDataSet().clearSelection(c);
159 cmds.add(new ChangeCommand(selectedNode, c));
160
161 Node n = new Node(selectedNode, true);
162
163 // If this wasn't called from menu, place it where the cursor is/was
164 if(e.getSource() instanceof JPanel) {
165 MapView mv = Main.map.mapView;
166 n.setCoor(mv.getLatLon(mv.lastMEvent.getX(), mv.lastMEvent.getY()));
167 }
168
169 cmds.add(new AddCommand(n));
170
171 fixRelations(selectedNode, cmds, Collections.singletonList(n));
172
173 Main.main.undoRedo.add(new SequenceCommand(tr("Unglued Node"), cmds));
174 getCurrentDataSet().setSelected(n);
175 Main.map.mapView.repaint();
176 }
177
178 /**
179 * Checks if selection is suitable for ungluing. This is the case when there's a single,
180 * tagged node selected that's part of at least one way (ungluing an unconnected node does
181 * not make sense. Due to the call order in actionPerformed, this is only called when the
182 * node is only part of one or less ways.
183 *
184 * @param The selection to check against
185 * @return Selection is suitable
186 */
187 private boolean checkForUnglueNode(Collection<? extends OsmPrimitive> selection) {
188 if (selection.size() != 1)
189 return false;
190 OsmPrimitive n = (OsmPrimitive) selection.toArray()[0];
191 if (!(n instanceof Node))
192 return false;
193 if (OsmPrimitive.getFilteredList(n.getReferrers(), Way.class).isEmpty())
194 return false;
195
196 selectedNode = (Node) n;
197 return selectedNode.isTagged();
198 }
199
200 /**
201 * Checks if the selection consists of something we can work with.
202 * Checks only if the number and type of items selected looks good.
203 *
204 * If this method returns "true", selectedNode and selectedWay will
205 * be set.
206 *
207 * Returns true if either one node is selected or one node and one
208 * way are selected and the node is part of the way.
209 *
210 * The way will be put into the object variable "selectedWay", the
211 * node into "selectedNode".
212 */
213 private boolean checkSelection(Collection<? extends OsmPrimitive> selection) {
214
215 int size = selection.size();
216 if (size < 1 || size > 2)
217 return false;
218
219 selectedNode = null;
220 selectedWay = null;
221
222 for (OsmPrimitive p : selection) {
223 if (p instanceof Node) {
224 selectedNode = (Node) p;
225 if (size == 1 || selectedWay != null)
226 return size == 1 || selectedWay.containsNode(selectedNode);
227 } else if (p instanceof Way) {
228 selectedWay = (Way) p;
229 if (size == 2 && selectedNode != null)
230 return selectedWay.containsNode(selectedNode);
231 }
232 }
233
234 return false;
235 }
236
237 /**
238 * Checks if the selection consists of something we can work with.
239 * Checks only if the number and type of items selected looks good.
240 *
241 * Returns true if one way and any number of nodes that are part of
242 * that way are selected. Note: "any" can be none, then all nodes of
243 * the way are used.
244 *
245 * The way will be put into the object variable "selectedWay", the
246 * nodes into "selectedNodes".
247 */
248 private boolean checkSelection2(Collection<? extends OsmPrimitive> selection) {
249 if (selection.size() < 1)
250 return false;
251
252 selectedWay = null;
253 for (OsmPrimitive p : selection) {
254 if (p instanceof Way) {
255 if (selectedWay != null)
256 return false;
257 selectedWay = (Way) p;
258 }
259 }
260 if (selectedWay == null)
261 return false;
262
263 selectedNodes = new ArrayList<Node>();
264 for (OsmPrimitive p : selection) {
265 if (p instanceof Node) {
266 Node n = (Node) p;
267 if (!selectedWay.containsNode(n))
268 return false;
269 selectedNodes.add(n);
270 }
271 }
272
273 if (selectedNodes.size() < 1) {
274 selectedNodes.addAll(selectedWay.getNodes());
275 }
276
277 return true;
278 }
279
280 /**
281 * dupe the given node of the given way
282 *
283 * assume that OrginalNode is in the way
284 *
285 * -> the new node will be put into the parameter newNodes.
286 * -> the add-node command will be put into the parameter cmds.
287 * -> the changed way will be returned and must be put into cmds by the caller!
288 */
289 private Way modifyWay(Node originalNode, Way w, List<Command> cmds, List<Node> newNodes) {
290 // clone the node for the way
291 Node newNode = new Node(originalNode, true /* clear OSM ID */);
292 newNodes.add(newNode);
293 cmds.add(new AddCommand(newNode));
294
295 ArrayList<Node> nn = new ArrayList<Node>();
296 for (Node pushNode : w.getNodes()) {
297 if (originalNode == pushNode) {
298 pushNode = newNode;
299 }
300 nn.add(pushNode);
301 }
302 Way newWay = new Way(w);
303 newWay.setNodes(nn);
304
305 return newWay;
306 }
307
308 /**
309 * put all newNodes into the same relation(s) that originalNode is in
310 */
311 private void fixRelations(Node originalNode, List<Command> cmds, List<Node> newNodes) {
312 // modify all relations containing the node
313 Relation newRel = null;
314 HashSet<String> rolesToReAdd = null;
315 for (Relation r : OsmPrimitive.getFilteredList(originalNode.getReferrers(), Relation.class)) {
316 if (r.isDeleted()) {
317 continue;
318 }
319 newRel = null;
320 rolesToReAdd = null;
321 for (RelationMember rm : r.getMembers()) {
322 if (rm.isNode()) {
323 if (rm.getMember() == originalNode) {
324 if (newRel == null) {
325 newRel = new Relation(r);
326 rolesToReAdd = new HashSet<String>();
327 }
328 rolesToReAdd.add(rm.getRole());
329 }
330 }
331 }
332 if (newRel != null) {
333 for (Node n : newNodes) {
334 for (String role : rolesToReAdd) {
335 newRel.addMember(new RelationMember(role, n));
336 }
337 }
338 cmds.add(new ChangeCommand(r, newRel));
339 }
340 }
341 }
342
343 /**
344 * dupe a single node into as many nodes as there are ways using it, OR
345 *
346 * dupe a single node once, and put the copy on the selected way
347 */
348 private void unglueWays() {
349 LinkedList<Command> cmds = new LinkedList<Command>();
350 LinkedList<Node> newNodes = new LinkedList<Node>();
351
352 if (selectedWay == null) {
353 Way wayWithSelectedNode = null;
354 LinkedList<Way> parentWays = new LinkedList<Way>();
355 for (OsmPrimitive osm : selectedNode.getReferrers()) {
356 if (osm.isUsable() && osm instanceof Way) {
357 Way w = (Way) osm;
358 if (wayWithSelectedNode == null && !w.isFirstLastNode(selectedNode)) {
359 wayWithSelectedNode = w;
360 } else {
361 parentWays.add(w);
362 }
363 }
364 }
365 if (wayWithSelectedNode == null) {
366 wayWithSelectedNode = parentWays.removeFirst();
367 }
368 for (Way w : parentWays) {
369 cmds.add(new ChangeCommand(w, modifyWay(selectedNode, w, cmds, newNodes)));
370 }
371 } else {
372 cmds.add(new ChangeCommand(selectedWay, modifyWay(selectedNode, selectedWay, cmds, newNodes)));
373 }
374
375 fixRelations(selectedNode, cmds, newNodes);
376
377 Main.main.undoRedo.add(new SequenceCommand(tr("Dupe into {0} nodes", newNodes.size()+1), cmds));
378 // select one of the new nodes
379 getCurrentDataSet().setSelected(newNodes.getFirst());
380 }
381
382 /**
383 * dupe all nodes that are selected, and put the copies on the selected way
384 *
385 */
386 private void unglueWays2() {
387 LinkedList<Command> cmds = new LinkedList<Command>();
388 List<Node> allNewNodes = new LinkedList<Node>();
389 Way tmpWay = selectedWay;
390
391 for (Node n : selectedNodes) {
392 List<Node> newNodes = new LinkedList<Node>();
393 tmpWay = modifyWay(n, tmpWay, cmds, newNodes);
394 fixRelations(n, cmds, newNodes);
395 allNewNodes.addAll(newNodes);
396 }
397 cmds.add(new ChangeCommand(selectedWay, tmpWay)); // only one changeCommand for a way, else garbage will happen
398
399 Main.main.undoRedo.add(new SequenceCommand(
400 trn("Dupe {0} node into {1} nodes", "Dupe {0} nodes into {1} nodes", selectedNodes.size(), selectedNodes.size(), selectedNodes.size()+allNewNodes.size()), cmds));
401 getCurrentDataSet().setSelected(allNewNodes);
402 }
403
404 @Override
405 protected void updateEnabledState() {
406 if (getCurrentDataSet() == null) {
407 setEnabled(false);
408 } else {
409 updateEnabledState(getCurrentDataSet().getSelected());
410 }
411 }
412
413 @Override
414 protected void updateEnabledState(Collection<? extends OsmPrimitive> selection) {
415 setEnabled(selection != null && !selection.isEmpty());
416 }
417
418 protected boolean checkAndConfirmOutlyingUnglue() {
419 List<OsmPrimitive> primitives = new ArrayList<OsmPrimitive>(2 + (selectedNodes == null ? 0 : selectedNodes.size()));
420 if (selectedNodes != null)
421 primitives.addAll(selectedNodes);
422 if (selectedNode != null)
423 primitives.add(selectedNode);
424 if (selectedWay != null)
425 primitives.add(selectedWay);
426 return Command.checkAndConfirmOutlyingOperation("unglue",
427 tr("Unglue confirmation"),
428 tr("You are about to unglue nodes outside of the area you have downloaded."
429 + "<br>"
430 + "This can cause problems because other objects (that you do not see) might use them."
431 + "<br>"
432 + "Do you really want to unglue?"),
433 tr("You are about to unglue incomplete objects."
434 + "<br>"
435 + "This will cause problems because you don''t see the real object."
436 + "<br>" + "Do you really want to unglue?"),
437 getEditLayer().data.getDataSourceArea(), primitives, null);
438 }
439}
Note: See TracBrowser for help on using the repository browser.