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

Last change on this file since 9276 was 9230, checked in by Don-vip, 8 years ago

fix javadoc errors/warnings

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