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

Last change on this file since 7130 was 7005, checked in by Don-vip, 10 years ago

see #8465 - use diamond operator where applicable

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