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

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

Applied #4797 by Kalle Lampila: unglueing the first/last node of an area produces two nodes

  • Property svn:eol-style set to native
File size: 15.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 public void actionPerformed(ActionEvent e) {
64
65 Collection<OsmPrimitive> selection = getCurrentDataSet().getSelected();
66
67 String errMsg = null;
68 if (checkSelection(selection)) {
69 int count = 0;
70 for (Way w : OsmPrimitive.getFilteredList(selectedNode.getReferrers(), Way.class)) {
71 if (!w.isUsable() || w.getNodesCount() < 1) {
72 continue;
73 }
74 count++;
75 }
76 if (count < 2) {
77 // If there aren't enough ways, maybe the user wanted to unglue the nodes
78 // (= copy tags to a new node)
79 if (checkForUnglueNode(selection)) {
80 unglueNode(e);
81 } else {
82 errMsg = tr("This node is not glued to anything else.");
83 }
84 } else {
85 // and then do the work.
86 unglueWays();
87 }
88 } else if (checkSelection2(selection)) {
89 ArrayList<Node> tmpNodes = new ArrayList<Node>();
90 for (Node n : selectedNodes) {
91 int count = 0;
92 for (Way w : OsmPrimitive.getFilteredList(n.getReferrers(), Way.class)) {
93 if (!w.isUsable()) {
94 continue;
95 }
96 count++;
97 }
98 if (count >= 2) {
99 tmpNodes.add(n);
100 }
101 }
102 if (tmpNodes.size() < 1) {
103 if (selection.size() > 1) {
104 errMsg = tr("None of these nodes are glued to anything else.");
105 } else {
106 errMsg = tr("None of this way''s nodes are glued to anything else.");
107 }
108 } else {
109 // and then do the work.
110 selectedNodes = tmpNodes;
111 unglueWays2();
112 }
113 } else {
114 errMsg =
115 tr("The current selection cannot be used for unglueing.")+"\n"+
116 "\n"+
117 tr("Select either:")+"\n"+
118 tr("* One tagged node, or")+"\n"+
119 tr("* One node that is used by more than one way, or")+"\n"+
120 tr("* One node that is used by more than one way and one of those ways, or")+"\n"+
121 tr("* One way that has one or more nodes that are used by more than one way, or")+"\n"+
122 tr("* One way and one or more of its nodes that are used by more than one way.")+"\n"+
123 "\n"+
124 tr("Note: If a way is selected, this way will get fresh copies of the unglued\n"+
125 "nodes and the new nodes will be selected. Otherwise, all ways will get their\n"+
126 "own copy and all nodes will be selected.");
127 }
128
129 if(errMsg != null) {
130 JOptionPane.showMessageDialog(
131 Main.parent,
132 errMsg,
133 tr("Error"),
134 JOptionPane.ERROR_MESSAGE);
135 }
136
137 selectedNode = null;
138 selectedWay = null;
139 selectedNodes = null;
140 }
141
142 /**
143 * Assumes there is one tagged Node stored in selectedNode that it will try to unglue
144 * (= copy node and remove all tags from the old one. Relations will not be removed)
145 */
146 private void unglueNode(ActionEvent e) {
147 LinkedList<Command> cmds = new LinkedList<Command>();
148
149 Node c = new Node(selectedNode);
150 c.removeAll();
151 getCurrentDataSet().clearSelection(c);
152 cmds.add(new ChangeCommand(selectedNode, c));
153
154 Node n = new Node(selectedNode, true);
155
156 // If this wasn't called from menu, place it where the cursor is/was
157 if(e.getSource() instanceof JPanel) {
158 MapView mv = Main.map.mapView;
159 n.setCoor(mv.getLatLon(mv.lastMEvent.getX(), mv.lastMEvent.getY()));
160 }
161
162 cmds.add(new AddCommand(n));
163
164 fixRelations(selectedNode, cmds, Collections.singletonList(n));
165
166 Main.main.undoRedo.add(new SequenceCommand(tr("Unglued Node"), cmds));
167 getCurrentDataSet().setSelected(n);
168 Main.map.mapView.repaint();
169 }
170
171 /**
172 * Checks if selection is suitable for ungluing. This is the case when there's a single,
173 * tagged node selected that's part of at least one way (ungluing an unconnected node does
174 * not make sense. Due to the call order in actionPerformed, this is only called when the
175 * node is only part of one or less ways.
176 *
177 * @param The selection to check against
178 * @return Selection is suitable
179 */
180 private boolean checkForUnglueNode(Collection<? extends OsmPrimitive> selection) {
181 if (selection.size() != 1)
182 return false;
183 OsmPrimitive n = (OsmPrimitive) selection.toArray()[0];
184 if (!(n instanceof Node))
185 return false;
186 if (OsmPrimitive.getFilteredList(n.getReferrers(), Way.class).isEmpty())
187 return false;
188
189 selectedNode = (Node) n;
190 return selectedNode.isTagged();
191 }
192
193 /**
194 * Checks if the selection consists of something we can work with.
195 * Checks only if the number and type of items selected looks good;
196 * does not check whether the selected items are really a valid
197 * input for splitting (this would be too expensive to be carried
198 * out from the selectionChanged listener).
199 *
200 * If this method returns "true", selectedNode and selectedWay will
201 * be set.
202 *
203 * Returns true if either one node is selected or one node and one
204 * way are selected and the node is part of the way.
205 *
206 * The way will be put into the object variable "selectedWay", the
207 * node into "selectedNode".
208 */
209 private boolean checkSelection(Collection<? extends OsmPrimitive> selection) {
210
211 int size = selection.size();
212 if (size < 1 || size > 2)
213 return false;
214
215 selectedNode = null;
216 selectedWay = null;
217
218 for (OsmPrimitive p : selection) {
219 if (p instanceof Node) {
220 selectedNode = (Node) p;
221 if (size == 1 || selectedWay != null)
222 return size == 1 || selectedWay.containsNode(selectedNode);
223 } else if (p instanceof Way) {
224 selectedWay = (Way) p;
225 if (size == 2 && selectedNode != null)
226 return selectedWay.containsNode(selectedNode);
227 }
228 }
229
230 return false;
231 }
232
233 /**
234 * Checks if the selection consists of something we can work with.
235 * Checks only if the number and type of items selected looks good;
236 * does not check whether the selected items are really a valid
237 * input for splitting (this would be too expensive to be carried
238 * out from the selectionChanged listener).
239 *
240 * Returns true if one way and any number of nodes that are part of
241 * that way are selected. Note: "any" can be none, then all nodes of
242 * the way are used.
243 *
244 * The way will be put into the object variable "selectedWay", the
245 * nodes into "selectedNodes".
246 */
247 private boolean checkSelection2(Collection<? extends OsmPrimitive> selection) {
248 if (selection.size() < 1)
249 return false;
250
251 selectedWay = null;
252 for (OsmPrimitive p : selection) {
253 if (p instanceof Way) {
254 if (selectedWay != null)
255 return false;
256 selectedWay = (Way) p;
257 }
258 }
259 if (selectedWay == null)
260 return false;
261
262 selectedNodes = new ArrayList<Node>();
263 for (OsmPrimitive p : selection) {
264 if (p instanceof Node) {
265 Node n = (Node) p;
266 if (!selectedWay.containsNode(n))
267 return false;
268 selectedNodes.add(n);
269 }
270 }
271
272 if (selectedNodes.size() < 1) {
273 selectedNodes.addAll(selectedWay.getNodes());
274 }
275
276 return true;
277 }
278
279 /**
280 * dupe the given node of the given way
281 *
282 * assume that OrginalNode is in the way
283 *
284 * -> the new node will be put into the parameter newNodes.
285 * -> the add-node command will be put into the parameter cmds.
286 * -> the changed way will be returned and must be put into cmds by the caller!
287 */
288 private Way modifyWay(Node originalNode, Way w, List<Command> cmds, List<Node> newNodes) {
289 // clone the node for the way
290 Node newNode = new Node(originalNode, true /* clear OSM ID */);
291 newNodes.add(newNode);
292 cmds.add(new AddCommand(newNode));
293
294 ArrayList<Node> nn = new ArrayList<Node>();
295 for (Node pushNode : w.getNodes()) {
296 if (originalNode == pushNode) {
297 pushNode = newNode;
298 }
299 nn.add(pushNode);
300 }
301 Way newWay = new Way(w);
302 newWay.setNodes(nn);
303
304 return newWay;
305 }
306
307 /**
308 * put all newNodes into the same relation(s) that originalNode is in
309 */
310 private void fixRelations(Node originalNode, List<Command> cmds, List<Node> newNodes) {
311 // modify all relations containing the node
312 Relation newRel = null;
313 HashSet<String> rolesToReAdd = null;
314 for (Relation r : OsmPrimitive.getFilteredList(originalNode.getReferrers(), Relation.class)) {
315 if (r.isDeleted()) {
316 continue;
317 }
318 newRel = null;
319 rolesToReAdd = null;
320 for (RelationMember rm : r.getMembers()) {
321 if (rm.isNode()) {
322 if (rm.getMember() == originalNode) {
323 if (newRel == null) {
324 newRel = new Relation(r);
325 newRel.setMembers(null);
326 rolesToReAdd = new HashSet<String>();
327 }
328 rolesToReAdd.add(rm.getRole());
329 }
330 }
331 }
332 if (newRel != null) {
333 for (RelationMember rm : r.getMembers()) {
334 newRel.addMember(rm);
335 }
336 for (Node n : newNodes) {
337 for (String role : rolesToReAdd) {
338 newRel.addMember(new RelationMember(role, n));
339 }
340 }
341 cmds.add(new ChangeCommand(r, newRel));
342 }
343 }
344 }
345
346 /**
347 * dupe a single node into as many nodes as there are ways using it, OR
348 *
349 * dupe a single node once, and put the copy on the selected way
350 */
351 private void unglueWays() {
352 LinkedList<Command> cmds = new LinkedList<Command>();
353 List<Node> newNodes = new LinkedList<Node>();
354
355 if (selectedWay == null) {
356 boolean firstway = true;
357 // modify all ways containing the nodes
358 for (Way w : OsmPrimitive.getFilteredList(selectedNode.getReferrers(), Way.class)) {
359 if (w.isDeleted() || w.isIncomplete()) {
360 continue;
361 }
362 if (!firstway) {
363 cmds.add(new ChangeCommand(w, modifyWay(selectedNode, w, cmds, newNodes)));
364 }
365 firstway = false;
366 }
367 } else {
368 cmds.add(new ChangeCommand(selectedWay, modifyWay(selectedNode, selectedWay, cmds, newNodes)));
369 }
370
371 fixRelations(selectedNode, cmds, newNodes);
372
373 Main.main.undoRedo.add(new SequenceCommand(tr("Dupe into {0} nodes", newNodes.size()+1), cmds));
374 if (selectedWay == null) { // if a node has been selected, new selection is ALL nodes
375 newNodes.add(selectedNode);
376 } // if a node and a way has been selected, new selection is only the new node that was added to the selected way
377 getCurrentDataSet().setSelected(newNodes);
378 }
379
380 /**
381 * dupe all nodes that are selected, and put the copies on the selected way
382 *
383 */
384 private void unglueWays2() {
385 LinkedList<Command> cmds = new LinkedList<Command>();
386 List<Node> allNewNodes = new LinkedList<Node>();
387 Way tmpWay = selectedWay;
388
389 for (Node n : selectedNodes) {
390 List<Node> newNodes = new LinkedList<Node>();
391 tmpWay = modifyWay(n, tmpWay, cmds, newNodes);
392 fixRelations(n, cmds, newNodes);
393 allNewNodes.addAll(newNodes);
394 }
395 cmds.add(new ChangeCommand(selectedWay, tmpWay)); // only one changeCommand for a way, else garbage will happen
396
397 Main.main.undoRedo.add(new SequenceCommand(
398 trn("Dupe {0} node into {1} nodes", "Dupe {0} nodes into {1} nodes", selectedNodes.size(), selectedNodes.size(), selectedNodes.size()+allNewNodes.size()), cmds));
399 getCurrentDataSet().setSelected(allNewNodes);
400 }
401
402 @Override
403 protected void updateEnabledState() {
404 if (getCurrentDataSet() == null) {
405 setEnabled(false);
406 } else {
407 updateEnabledState(getCurrentDataSet().getSelected());
408 }
409 }
410
411 @Override
412 protected void updateEnabledState(Collection<? extends OsmPrimitive> selection) {
413 setEnabled(selection != null && !selection.isEmpty());
414 }
415}
Note: See TracBrowser for help on using the repository browser.