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

Last change on this file since 14693 was 14662, checked in by simon04, 5 years ago

see #16874 fix #17180 - NPE in UnGlueAction

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