| 1 | // License: GPL. For details, see LICENSE file.
|
|---|
| 2 | package org.openstreetmap.josm.actions;
|
|---|
| 3 |
|
|---|
| 4 | import static org.openstreetmap.josm.gui.help.HelpUtil.ht;
|
|---|
| 5 | import static org.openstreetmap.josm.tools.I18n.tr;
|
|---|
| 6 | import static org.openstreetmap.josm.tools.I18n.trn;
|
|---|
| 7 |
|
|---|
| 8 | import java.awt.Point;
|
|---|
| 9 | import java.awt.event.ActionEvent;
|
|---|
| 10 | import java.awt.event.KeyEvent;
|
|---|
| 11 | import java.util.ArrayList;
|
|---|
| 12 | import java.util.Collection;
|
|---|
| 13 | import java.util.Collections;
|
|---|
| 14 | import java.util.HashMap;
|
|---|
| 15 | import java.util.HashSet;
|
|---|
| 16 | import java.util.List;
|
|---|
| 17 | import java.util.Map;
|
|---|
| 18 | import java.util.Set;
|
|---|
| 19 | import java.util.stream.Collectors;
|
|---|
| 20 |
|
|---|
| 21 | import javax.swing.JOptionPane;
|
|---|
| 22 |
|
|---|
| 23 | import org.openstreetmap.josm.command.AddCommand;
|
|---|
| 24 | import org.openstreetmap.josm.command.ChangeCommand;
|
|---|
| 25 | import org.openstreetmap.josm.command.ChangeMembersCommand;
|
|---|
| 26 | import org.openstreetmap.josm.command.ChangeNodesCommand;
|
|---|
| 27 | import org.openstreetmap.josm.command.Command;
|
|---|
| 28 | import org.openstreetmap.josm.command.MoveCommand;
|
|---|
| 29 | import org.openstreetmap.josm.command.SequenceCommand;
|
|---|
| 30 | import org.openstreetmap.josm.data.UndoRedoHandler;
|
|---|
| 31 | import org.openstreetmap.josm.data.osm.DefaultNameFormatter;
|
|---|
| 32 | import org.openstreetmap.josm.data.osm.Node;
|
|---|
| 33 | import org.openstreetmap.josm.data.osm.OsmPrimitive;
|
|---|
| 34 | import org.openstreetmap.josm.data.osm.Relation;
|
|---|
| 35 | import org.openstreetmap.josm.data.osm.RelationMember;
|
|---|
| 36 | import org.openstreetmap.josm.data.osm.Way;
|
|---|
| 37 | import org.openstreetmap.josm.gui.MainApplication;
|
|---|
| 38 | import org.openstreetmap.josm.gui.MapView;
|
|---|
| 39 | import org.openstreetmap.josm.gui.Notification;
|
|---|
| 40 | import org.openstreetmap.josm.gui.dialogs.PropertiesMembershipChoiceDialog;
|
|---|
| 41 | import org.openstreetmap.josm.gui.dialogs.PropertiesMembershipChoiceDialog.ExistingBothNew;
|
|---|
| 42 | import org.openstreetmap.josm.tools.Logging;
|
|---|
| 43 | import org.openstreetmap.josm.tools.Shortcut;
|
|---|
| 44 | import org.openstreetmap.josm.tools.UserCancelException;
|
|---|
| 45 |
|
|---|
| 46 | /**
|
|---|
| 47 | * Duplicate nodes that are used by multiple ways or tagged nodes used by a single way
|
|---|
| 48 | * or nodes which referenced more than once by a single way.
|
|---|
| 49 | *
|
|---|
| 50 | * This is the opposite of the MergeNodesAction.
|
|---|
| 51 | *
|
|---|
| 52 | */
|
|---|
| 53 | public class UnGlueAction extends JosmAction {
|
|---|
| 54 |
|
|---|
| 55 | private transient Node selectedNode;
|
|---|
| 56 | private transient Way selectedWay;
|
|---|
| 57 | private transient Set<Node> selectedNodes;
|
|---|
| 58 |
|
|---|
| 59 | /**
|
|---|
| 60 | * Create a new UnGlueAction.
|
|---|
| 61 | */
|
|---|
| 62 | public UnGlueAction() {
|
|---|
| 63 | super(tr("UnGlue Ways"), "unglueways", tr("Duplicate nodes that are used by multiple ways."),
|
|---|
| 64 | Shortcut.registerShortcut("tools:unglue", tr("Tools: {0}", tr("UnGlue Ways")), KeyEvent.VK_G, Shortcut.DIRECT), true);
|
|---|
| 65 | setHelpId(ht("/Action/UnGlue"));
|
|---|
| 66 | }
|
|---|
| 67 |
|
|---|
| 68 | /**
|
|---|
| 69 | * Called when the action is executed.
|
|---|
| 70 | *
|
|---|
| 71 | * This method does some checking on the selection and calls the matching unGlueWay method.
|
|---|
| 72 | */
|
|---|
| 73 | @Override
|
|---|
| 74 | public void actionPerformed(ActionEvent e) {
|
|---|
| 75 | try {
|
|---|
| 76 | unglue();
|
|---|
| 77 | } catch (UserCancelException ignore) {
|
|---|
| 78 | Logging.trace(ignore);
|
|---|
| 79 | } finally {
|
|---|
| 80 | cleanup();
|
|---|
| 81 | }
|
|---|
| 82 | }
|
|---|
| 83 |
|
|---|
| 84 | protected void unglue() throws UserCancelException {
|
|---|
| 85 |
|
|---|
| 86 | Collection<OsmPrimitive> selection = getLayerManager().getEditDataSet().getSelected();
|
|---|
| 87 |
|
|---|
| 88 | String errMsg = null;
|
|---|
| 89 | int errorTime = Notification.TIME_DEFAULT;
|
|---|
| 90 |
|
|---|
| 91 | if (checkSelectionOneNodeAtMostOneWay(selection)) {
|
|---|
| 92 | checkAndConfirmOutlyingUnglue();
|
|---|
| 93 | List<Way> parentWays = selectedNode.getParentWays().stream().filter(Way::isUsable).collect(Collectors.toList());
|
|---|
| 94 |
|
|---|
| 95 | if (parentWays.size() < 2) {
|
|---|
| 96 | if (!parentWays.isEmpty()) {
|
|---|
| 97 | // single way
|
|---|
| 98 | Way way = selectedWay == null ? parentWays.get(0) : selectedWay;
|
|---|
| 99 | boolean closedOrSelfCrossing = way.getNodes().stream().filter(n -> n == selectedNode).count() > 1;
|
|---|
| 100 |
|
|---|
| 101 | final PropertiesMembershipChoiceDialog dialog = PropertiesMembershipChoiceDialog.showIfNecessary(
|
|---|
| 102 | Collections.singleton(selectedNode), !selectedNode.isTagged());
|
|---|
| 103 | if (dialog != null && dialog.getTags().isPresent()) {
|
|---|
| 104 | unglueOneNodeAtMostOneWay(way, dialog);
|
|---|
| 105 | return;
|
|---|
| 106 | } else if (closedOrSelfCrossing) {
|
|---|
| 107 | unglueClosedOrSelfCrossingWay(way, dialog);
|
|---|
| 108 | return;
|
|---|
| 109 | }
|
|---|
| 110 | }
|
|---|
| 111 | errorTime = Notification.TIME_SHORT;
|
|---|
| 112 | errMsg = tr("This node is not glued to anything else.");
|
|---|
| 113 | } else {
|
|---|
| 114 | // and then do the work.
|
|---|
| 115 | unglueWays();
|
|---|
| 116 | }
|
|---|
| 117 | } else if (checkSelectionOneWayAnyNodes(selection)) {
|
|---|
| 118 | checkAndConfirmOutlyingUnglue();
|
|---|
| 119 | selectedNodes.removeIf(n -> n.getParentWays().stream().filter(Way::isUsable).count() < 2);
|
|---|
| 120 | if (selectedNodes.isEmpty()) {
|
|---|
| 121 | if (selection.size() > 1) {
|
|---|
| 122 | errMsg = tr("None of these nodes are glued to anything else.");
|
|---|
| 123 | } else {
|
|---|
| 124 | errMsg = tr("None of this way''s nodes are glued to anything else.");
|
|---|
| 125 | }
|
|---|
| 126 | } else if (selectedNodes.size() == 1) {
|
|---|
| 127 | selectedNode = selectedNodes.iterator().next();
|
|---|
| 128 | unglueWays();
|
|---|
| 129 | } else {
|
|---|
| 130 | // and then do the work.
|
|---|
| 131 | unglueOneWayAnyNodes();
|
|---|
| 132 | }
|
|---|
| 133 | } else {
|
|---|
| 134 | errorTime = Notification.TIME_VERY_LONG;
|
|---|
| 135 | errMsg =
|
|---|
| 136 | tr("The current selection cannot be used for unglueing.")+'\n'+
|
|---|
| 137 | '\n'+
|
|---|
| 138 | tr("Select either:")+'\n'+
|
|---|
| 139 | tr("* One tagged node, or")+'\n'+
|
|---|
| 140 | tr("* One node that is used by more than one way, or")+'\n'+
|
|---|
| 141 | tr("* One node that is used by more than one way and one of those ways, or")+'\n'+
|
|---|
| 142 | tr("* One way that has one or more nodes that are used by more than one way, or")+'\n'+
|
|---|
| 143 | tr("* One way and one or more of its nodes that are used by more than one way.")+'\n'+
|
|---|
| 144 | '\n'+
|
|---|
| 145 | tr("Note: If a way is selected, this way will get fresh copies of the unglued\n"+
|
|---|
| 146 | "nodes and the new nodes will be selected. Otherwise, all ways will get their\n"+
|
|---|
| 147 | "own copy and all nodes will be selected.");
|
|---|
| 148 | }
|
|---|
| 149 |
|
|---|
| 150 | if (errMsg != null) {
|
|---|
| 151 | new Notification(
|
|---|
| 152 | errMsg)
|
|---|
| 153 | .setIcon(JOptionPane.ERROR_MESSAGE)
|
|---|
| 154 | .setDuration(errorTime)
|
|---|
| 155 | .show();
|
|---|
| 156 | }
|
|---|
| 157 | }
|
|---|
| 158 |
|
|---|
| 159 | private void cleanup() {
|
|---|
| 160 | selectedNode = null;
|
|---|
| 161 | selectedWay = null;
|
|---|
| 162 | selectedNodes = null;
|
|---|
| 163 | }
|
|---|
| 164 |
|
|---|
| 165 | static void update(PropertiesMembershipChoiceDialog dialog, Node existingNode, List<Node> newNodes, List<Command> cmds) {
|
|---|
| 166 | updateMemberships(dialog.getMemberships().orElse(null), existingNode, newNodes, cmds);
|
|---|
| 167 | updateProperties(dialog.getTags().orElse(null), existingNode, newNodes, cmds);
|
|---|
| 168 | }
|
|---|
| 169 |
|
|---|
| 170 | private static void updateProperties(ExistingBothNew tags, Node existingNode, Iterable<Node> newNodes, List<Command> cmds) {
|
|---|
| 171 | if (ExistingBothNew.NEW == tags) {
|
|---|
| 172 | final Node newSelectedNode = new Node(existingNode);
|
|---|
| 173 | newSelectedNode.removeAll();
|
|---|
| 174 | cmds.add(new ChangeCommand(existingNode, newSelectedNode));
|
|---|
| 175 | } else if (ExistingBothNew.OLD == tags) {
|
|---|
| 176 | for (Node newNode : newNodes) {
|
|---|
| 177 | newNode.removeAll();
|
|---|
| 178 | }
|
|---|
| 179 | }
|
|---|
| 180 | }
|
|---|
| 181 |
|
|---|
| 182 | /**
|
|---|
| 183 | * Assumes there is one tagged Node stored in selectedNode that it will try to unglue.
|
|---|
| 184 | * (i.e. copy node and remove all tags from the old one.)
|
|---|
| 185 | * @param way way to modify
|
|---|
| 186 | * @param dialog the user dialog
|
|---|
| 187 | */
|
|---|
| 188 | private void unglueOneNodeAtMostOneWay(Way way, PropertiesMembershipChoiceDialog dialog) {
|
|---|
| 189 | List<Command> cmds = new ArrayList<>();
|
|---|
| 190 | List<Node> newNodes = new ArrayList<>();
|
|---|
| 191 | cmds.add(new ChangeNodesCommand(way, modifyWay(selectedNode, way, cmds, newNodes)));
|
|---|
| 192 | if (dialog != null) {
|
|---|
| 193 | update(dialog, selectedNode, newNodes, cmds);
|
|---|
| 194 | }
|
|---|
| 195 |
|
|---|
| 196 | // Place the selected node where the cursor is or some pixels above
|
|---|
| 197 | MapView mv = MainApplication.getMap().mapView;
|
|---|
| 198 | Point currMousePos = mv.getMousePosition();
|
|---|
| 199 | if (currMousePos != null) {
|
|---|
| 200 | cmds.add(new MoveCommand(selectedNode, mv.getLatLon(currMousePos.getX(), currMousePos.getY())));
|
|---|
| 201 | } else {
|
|---|
| 202 | cmds.add(new MoveCommand(selectedNode, 0, 5));
|
|---|
| 203 | }
|
|---|
| 204 | UndoRedoHandler.getInstance().add(new SequenceCommand(tr("Unglued Node"), cmds));
|
|---|
| 205 | getLayerManager().getEditDataSet().setSelected(selectedNode);
|
|---|
| 206 | }
|
|---|
| 207 |
|
|---|
| 208 | /**
|
|---|
| 209 | * Checks if the selection consists of something we can work with.
|
|---|
| 210 | * Checks only if the number and type of items selected looks good.
|
|---|
| 211 | *
|
|---|
| 212 | * If this method returns "true", selectedNode will be set, selectedWay might be set
|
|---|
| 213 | *
|
|---|
| 214 | * Returns true if either one node is selected or one node and one
|
|---|
| 215 | * way are selected and the node is part of the way.
|
|---|
| 216 | *
|
|---|
| 217 | * The way will be put into the object variable "selectedWay", the node into "selectedNode".
|
|---|
| 218 | * @param selection selected primitives
|
|---|
| 219 | * @return true if either one node is selected or one node and one way are selected and the node is part of the way
|
|---|
| 220 | */
|
|---|
| 221 | private boolean checkSelectionOneNodeAtMostOneWay(Collection<? extends OsmPrimitive> selection) {
|
|---|
| 222 |
|
|---|
| 223 | int size = selection.size();
|
|---|
| 224 | if (size < 1 || size > 2)
|
|---|
| 225 | return false;
|
|---|
| 226 |
|
|---|
| 227 | selectedNode = null;
|
|---|
| 228 | selectedWay = null;
|
|---|
| 229 |
|
|---|
| 230 | for (OsmPrimitive p : selection) {
|
|---|
| 231 | if (p instanceof Node) {
|
|---|
| 232 | selectedNode = (Node) p;
|
|---|
| 233 | if (size == 1 || (selectedWay != null && selectedWay.containsNode(selectedNode)))
|
|---|
| 234 | return true;
|
|---|
| 235 | } else if (p instanceof Way) {
|
|---|
| 236 | selectedWay = (Way) p;
|
|---|
| 237 | if (size == 2 && selectedNode != null)
|
|---|
| 238 | return selectedWay.containsNode(selectedNode);
|
|---|
| 239 | }
|
|---|
| 240 | }
|
|---|
| 241 |
|
|---|
| 242 | return false;
|
|---|
| 243 | }
|
|---|
| 244 |
|
|---|
| 245 | /**
|
|---|
| 246 | * Checks if the selection consists of something we can work with.
|
|---|
| 247 | * Checks only if the number and type of items selected looks good.
|
|---|
| 248 | *
|
|---|
| 249 | * Returns true if one way and any number of nodes that are part of that way are selected.
|
|---|
| 250 | * Note: "any" can be none, then all nodes of the way are used.
|
|---|
| 251 | *
|
|---|
| 252 | * The way will be put into the object variable "selectedWay", the nodes into "selectedNodes".
|
|---|
| 253 | * @param selection selected primitives
|
|---|
| 254 | * @return true if one way and any number of nodes that are part of that way are selected
|
|---|
| 255 | */
|
|---|
| 256 | private boolean checkSelectionOneWayAnyNodes(Collection<? extends OsmPrimitive> selection) {
|
|---|
| 257 | if (selection.isEmpty())
|
|---|
| 258 | return false;
|
|---|
| 259 |
|
|---|
| 260 | selectedWay = null;
|
|---|
| 261 | for (OsmPrimitive p : selection) {
|
|---|
| 262 | if (p instanceof Way) {
|
|---|
| 263 | if (selectedWay != null)
|
|---|
| 264 | return false;
|
|---|
| 265 | selectedWay = (Way) p;
|
|---|
| 266 | }
|
|---|
| 267 | }
|
|---|
| 268 | if (selectedWay == null)
|
|---|
| 269 | return false;
|
|---|
| 270 |
|
|---|
| 271 | selectedNodes = new HashSet<>();
|
|---|
| 272 | for (OsmPrimitive p : selection) {
|
|---|
| 273 | if (p instanceof Node) {
|
|---|
| 274 | Node n = (Node) p;
|
|---|
| 275 | if (!selectedWay.containsNode(n))
|
|---|
| 276 | return false;
|
|---|
| 277 | selectedNodes.add(n);
|
|---|
| 278 | }
|
|---|
| 279 | }
|
|---|
| 280 |
|
|---|
| 281 | if (selectedNodes.isEmpty()) {
|
|---|
| 282 | selectedNodes.addAll(selectedWay.getNodes());
|
|---|
| 283 | }
|
|---|
| 284 |
|
|---|
| 285 | return true;
|
|---|
| 286 | }
|
|---|
| 287 |
|
|---|
| 288 | /**
|
|---|
| 289 | * dupe the given node of the given way
|
|---|
| 290 | *
|
|---|
| 291 | * assume that originalNode is in the way
|
|---|
| 292 | * <ul>
|
|---|
| 293 | * <li>the new node will be put into the parameter newNodes.</li>
|
|---|
| 294 | * <li>the add-node command will be put into the parameter cmds.</li>
|
|---|
| 295 | * <li>the changed way will be returned and must be put into cmds by the caller!</li>
|
|---|
| 296 | * </ul>
|
|---|
| 297 | * @param originalNode original node to duplicate
|
|---|
| 298 | * @param w parent way
|
|---|
| 299 | * @param cmds List of commands that will contain the new "add node" command
|
|---|
| 300 | * @param newNodes List of nodes that will contain the new node
|
|---|
| 301 | * @return The modified list of way nodes. Change command must be handled by the caller
|
|---|
| 302 | */
|
|---|
| 303 | private static List<Node> modifyWay(Node originalNode, Way w, List<Command> cmds, List<Node> newNodes) {
|
|---|
| 304 | // clone the node for the way
|
|---|
| 305 | Node newNode = cloneNode(originalNode, cmds);
|
|---|
| 306 | newNodes.add(newNode);
|
|---|
| 307 |
|
|---|
| 308 | List<Node> nn = new ArrayList<>(w.getNodes());
|
|---|
| 309 | nn.replaceAll(n -> n == originalNode ? newNode : n);
|
|---|
| 310 | return nn;
|
|---|
| 311 | }
|
|---|
| 312 |
|
|---|
| 313 | private static Node cloneNode(Node originalNode, List<Command> cmds) {
|
|---|
| 314 | Node newNode = new Node(originalNode, true /* clear OSM ID */);
|
|---|
| 315 | cmds.add(new AddCommand(originalNode.getDataSet(), newNode));
|
|---|
| 316 | return newNode;
|
|---|
| 317 | }
|
|---|
| 318 |
|
|---|
| 319 | /**
|
|---|
| 320 | * put all newNodes into the same relation(s) that originalNode is in
|
|---|
| 321 | * @param memberships where the memberships should be places
|
|---|
| 322 | * @param originalNode original node to duplicate
|
|---|
| 323 | * @param cmds List of commands that will contain the new "change relation" commands
|
|---|
| 324 | * @param newNodes List of nodes that contain the new node
|
|---|
| 325 | */
|
|---|
| 326 | private static void updateMemberships(ExistingBothNew memberships, Node originalNode, List<Node> newNodes, List<Command> cmds) {
|
|---|
| 327 | if (memberships == null || ExistingBothNew.OLD == memberships) {
|
|---|
| 328 | return;
|
|---|
| 329 | }
|
|---|
| 330 | // modify all relations containing the node
|
|---|
| 331 | for (Relation r : OsmPrimitive.getParentRelations(Collections.singleton(originalNode))) {
|
|---|
| 332 | if (r.isDeleted()) {
|
|---|
| 333 | continue;
|
|---|
| 334 | }
|
|---|
| 335 | List<RelationMember> newMembers = new ArrayList<>(r.getMembers());
|
|---|
| 336 | // loop backwards because we add or remove members, works also when nodes appear
|
|---|
| 337 | // multiple times in the same relation
|
|---|
| 338 | boolean changed = false;
|
|---|
| 339 | for (int i = r.getMembersCount() - 1; i >= 0; i--) {
|
|---|
| 340 | RelationMember rm = r.getMember(i);
|
|---|
| 341 | if (rm.getMember() != originalNode)
|
|---|
| 342 | continue;
|
|---|
| 343 | for (Node n : newNodes) {
|
|---|
| 344 | newMembers.add(i + 1, new RelationMember(rm.getRole(), n));
|
|---|
| 345 | }
|
|---|
| 346 | if (ExistingBothNew.NEW == memberships) {
|
|---|
| 347 | // remove old member
|
|---|
| 348 | newMembers.remove(i);
|
|---|
| 349 | }
|
|---|
| 350 | changed = true;
|
|---|
| 351 | }
|
|---|
| 352 | if (changed) {
|
|---|
| 353 | cmds.add(new ChangeMembersCommand(r, newMembers));
|
|---|
| 354 | }
|
|---|
| 355 | }
|
|---|
| 356 | }
|
|---|
| 357 |
|
|---|
| 358 | /**
|
|---|
| 359 | * dupe a single node into as many nodes as there are ways using it, OR
|
|---|
| 360 | *
|
|---|
| 361 | * dupe a single node once, and put the copy on the selected way
|
|---|
| 362 | * @throws UserCancelException if user cancels choice
|
|---|
| 363 | */
|
|---|
| 364 | private void unglueWays() throws UserCancelException {
|
|---|
| 365 | final PropertiesMembershipChoiceDialog dialog = PropertiesMembershipChoiceDialog
|
|---|
| 366 | .showIfNecessary(Collections.singleton(selectedNode), false);
|
|---|
| 367 | List<Command> cmds = new ArrayList<>();
|
|---|
| 368 | List<Node> newNodes = new ArrayList<>();
|
|---|
| 369 | List<Way> parentWays;
|
|---|
| 370 | if (selectedWay == null) {
|
|---|
| 371 | parentWays = selectedNode.referrers(Way.class).filter(Way::isUsable).collect(Collectors.toList());
|
|---|
| 372 | // see #5452 and #18670
|
|---|
| 373 | parentWays.sort((o1, o2) -> {
|
|---|
| 374 | int d = Boolean.compare(!o1.isNew() && !o1.isModified(), !o2.isNew() && !o2.isModified());
|
|---|
| 375 | if (d == 0) {
|
|---|
| 376 | d = Integer.compare(o2.getReferrers().size(), o1.getReferrers().size()); // reversed
|
|---|
| 377 | }
|
|---|
| 378 | if (d == 0) {
|
|---|
| 379 | d = Boolean.compare(o1.isFirstLastNode(selectedNode), o2.isFirstLastNode(selectedNode));
|
|---|
| 380 | }
|
|---|
| 381 | return d;
|
|---|
| 382 | });
|
|---|
| 383 | // first way should not be changed, preferring older ways and those with fewer parents
|
|---|
| 384 | parentWays.remove(0);
|
|---|
| 385 | } else {
|
|---|
| 386 | parentWays = Collections.singletonList(selectedWay);
|
|---|
| 387 | }
|
|---|
| 388 | Set<Way> warnParents = new HashSet<>();
|
|---|
| 389 | for (Way w : parentWays) {
|
|---|
| 390 | if (w.isFirstLastNode(selectedNode))
|
|---|
| 391 | warnParents.add(w);
|
|---|
| 392 | cmds.add(new ChangeNodesCommand(w, modifyWay(selectedNode, w, cmds, newNodes)));
|
|---|
| 393 | }
|
|---|
| 394 |
|
|---|
| 395 | if (dialog != null) {
|
|---|
| 396 | update(dialog, selectedNode, newNodes, cmds);
|
|---|
| 397 | }
|
|---|
| 398 | notifyWayPartOfRelation(warnParents);
|
|---|
| 399 |
|
|---|
| 400 | execCommands(cmds, newNodes);
|
|---|
| 401 | }
|
|---|
| 402 |
|
|---|
| 403 | /**
|
|---|
| 404 | * Add commands to undo-redo system.
|
|---|
| 405 | * @param cmds Commands to execute
|
|---|
| 406 | * @param newNodes New created nodes by this set of command
|
|---|
| 407 | */
|
|---|
| 408 | private void execCommands(List<Command> cmds, List<Node> newNodes) {
|
|---|
| 409 | UndoRedoHandler.getInstance().add(new SequenceCommand(/* for correct i18n of plural forms - see #9110 */
|
|---|
| 410 | trn("Dupe into {0} node", "Dupe into {0} nodes", newNodes.size() + 1L, newNodes.size() + 1L), cmds));
|
|---|
| 411 | // select one of the new nodes
|
|---|
| 412 | getLayerManager().getEditDataSet().setSelected(newNodes.get(0));
|
|---|
| 413 | }
|
|---|
| 414 |
|
|---|
| 415 | /**
|
|---|
| 416 | * Duplicates a node used several times by the same way. See #9896.
|
|---|
| 417 | * First occurrence is kept. A closed way will be "opened" when the closing node is unglued.
|
|---|
| 418 | * @param way way to modify
|
|---|
| 419 | * @param dialog user dialog, might be null
|
|---|
| 420 | * @return true if action is OK false if there is nothing to do
|
|---|
| 421 | */
|
|---|
| 422 | private boolean unglueClosedOrSelfCrossingWay(Way way, PropertiesMembershipChoiceDialog dialog) {
|
|---|
| 423 | // According to previous check, only one valid way through that node
|
|---|
| 424 | List<Command> cmds = new ArrayList<>();
|
|---|
| 425 | List<Node> oldNodes = way.getNodes();
|
|---|
| 426 | List<Node> newNodes = new ArrayList<>(oldNodes.size());
|
|---|
| 427 | List<Node> addNodes = new ArrayList<>();
|
|---|
| 428 | int count = 0;
|
|---|
| 429 | for (Node n: oldNodes) {
|
|---|
| 430 | if (n == selectedNode && count++ > 0) {
|
|---|
| 431 | n = cloneNode(selectedNode, cmds);
|
|---|
| 432 | addNodes.add(n);
|
|---|
| 433 | }
|
|---|
| 434 | newNodes.add(n);
|
|---|
| 435 | }
|
|---|
| 436 | if (addNodes.isEmpty()) {
|
|---|
| 437 | // selectedNode doesn't need unglue
|
|---|
| 438 | return false;
|
|---|
| 439 | }
|
|---|
| 440 | if (dialog != null) {
|
|---|
| 441 | update(dialog, selectedNode, addNodes, cmds);
|
|---|
| 442 | }
|
|---|
| 443 | addCheckedChangeNodesCmd(cmds, way, newNodes);
|
|---|
| 444 | execCommands(cmds, addNodes);
|
|---|
| 445 | return true;
|
|---|
| 446 | }
|
|---|
| 447 |
|
|---|
| 448 | /**
|
|---|
| 449 | * dupe all nodes that are selected, and put the copies on the selected way
|
|---|
| 450 | * @throws UserCancelException if user cancels choice
|
|---|
| 451 | */
|
|---|
| 452 | private void unglueOneWayAnyNodes() throws UserCancelException {
|
|---|
| 453 | final PropertiesMembershipChoiceDialog dialog =
|
|---|
| 454 | PropertiesMembershipChoiceDialog.showIfNecessary(selectedNodes, false);
|
|---|
| 455 |
|
|---|
| 456 | Map<Node, Node> replaced = new HashMap<>();
|
|---|
| 457 | List<Command> cmds = new ArrayList<>();
|
|---|
| 458 |
|
|---|
| 459 | selectedNodes.forEach(n -> replaced.put(n, cloneNode(n, cmds)));
|
|---|
| 460 | List<Node> modNodes = new ArrayList<>(selectedWay.getNodes());
|
|---|
| 461 | modNodes.replaceAll(n -> replaced.getOrDefault(n, n));
|
|---|
| 462 |
|
|---|
| 463 | if (dialog != null) {
|
|---|
| 464 | replaced.forEach((k, v) -> update(dialog, k, Collections.singletonList(v), cmds));
|
|---|
| 465 | }
|
|---|
| 466 |
|
|---|
| 467 | // only one changeCommand for a way, else garbage will happen
|
|---|
| 468 | addCheckedChangeNodesCmd(cmds, selectedWay, modNodes);
|
|---|
| 469 | UndoRedoHandler.getInstance().add(new SequenceCommand(
|
|---|
| 470 | trn("Dupe {0} node into {1} nodes", "Dupe {0} nodes into {1} nodes",
|
|---|
| 471 | selectedNodes.size(), selectedNodes.size(), 2 * selectedNodes.size()), cmds));
|
|---|
| 472 | getLayerManager().getEditDataSet().setSelected(replaced.values());
|
|---|
| 473 | }
|
|---|
| 474 |
|
|---|
| 475 | private boolean addCheckedChangeNodesCmd(List<Command> cmds, Way w, List<Node> nodes) {
|
|---|
| 476 | boolean relationCheck = !calcAffectedRelations(Collections.singleton(w)).isEmpty();
|
|---|
| 477 | cmds.add(new ChangeNodesCommand(w, nodes));
|
|---|
| 478 | if (relationCheck) {
|
|---|
| 479 | notifyWayPartOfRelation(Collections.singleton(w));
|
|---|
| 480 | }
|
|---|
| 481 | return relationCheck;
|
|---|
| 482 | }
|
|---|
| 483 |
|
|---|
| 484 | @Override
|
|---|
| 485 | protected void updateEnabledState() {
|
|---|
| 486 | updateEnabledStateOnCurrentSelection();
|
|---|
| 487 | }
|
|---|
| 488 |
|
|---|
| 489 | @Override
|
|---|
| 490 | protected void updateEnabledState(Collection<? extends OsmPrimitive> selection) {
|
|---|
| 491 | updateEnabledStateOnModifiableSelection(selection);
|
|---|
| 492 | }
|
|---|
| 493 |
|
|---|
| 494 | protected void checkAndConfirmOutlyingUnglue() throws UserCancelException {
|
|---|
| 495 | List<OsmPrimitive> primitives = new ArrayList<>(2 + (selectedNodes == null ? 0 : selectedNodes.size()));
|
|---|
| 496 | if (selectedNodes != null)
|
|---|
| 497 | primitives.addAll(selectedNodes);
|
|---|
| 498 | if (selectedNode != null)
|
|---|
| 499 | primitives.add(selectedNode);
|
|---|
| 500 | final boolean ok = checkAndConfirmOutlyingOperation("unglue",
|
|---|
| 501 | tr("Unglue confirmation"),
|
|---|
| 502 | tr("You are about to unglue nodes which can have other referrers not yet downloaded."
|
|---|
| 503 | + "<br>"
|
|---|
| 504 | + "This can cause problems because other objects (that you do not see) might use them."
|
|---|
| 505 | + "<br>"
|
|---|
| 506 | + "Do you really want to unglue?"),
|
|---|
| 507 | tr("You are about to unglue incomplete objects."
|
|---|
| 508 | + "<br>"
|
|---|
| 509 | + "This will cause problems because you don''t see the real object."
|
|---|
| 510 | + "<br>" + "Do you really want to unglue?"),
|
|---|
| 511 | primitives, null);
|
|---|
| 512 | if (!ok) {
|
|---|
| 513 | throw new UserCancelException();
|
|---|
| 514 | }
|
|---|
| 515 | }
|
|---|
| 516 |
|
|---|
| 517 | protected void notifyWayPartOfRelation(final Collection<Way> ways) {
|
|---|
| 518 | Set<Relation> affectedRelations = calcAffectedRelations(ways);
|
|---|
| 519 | if (affectedRelations.isEmpty()) {
|
|---|
| 520 | return;
|
|---|
| 521 | }
|
|---|
| 522 | final int size = affectedRelations.size();
|
|---|
| 523 | final String msg1 = trn("Unglueing possibly affected {0} relation: {1}", "Unglueing possibly affected {0} relations: {1}",
|
|---|
| 524 | size, size, DefaultNameFormatter.getInstance().formatAsHtmlUnorderedList(affectedRelations, 20));
|
|---|
| 525 | final String msg2 = trn("Ensure that the relation has not been broken!", "Ensure that the relations have not been broken!",
|
|---|
| 526 | size);
|
|---|
| 527 | new Notification(msg1 + msg2).setIcon(JOptionPane.WARNING_MESSAGE).show();
|
|---|
| 528 | }
|
|---|
| 529 |
|
|---|
| 530 | protected Set<Relation> calcAffectedRelations(final Collection<Way> ways) {
|
|---|
| 531 | final Set<Node> affectedNodes = (selectedNodes != null) ? selectedNodes : Collections.singleton(selectedNode);
|
|---|
| 532 | return OsmPrimitive.getParentRelations(ways)
|
|---|
| 533 | .stream().filter(r -> isRelationAffected(r, affectedNodes, ways))
|
|---|
| 534 | .collect(Collectors.toSet());
|
|---|
| 535 | }
|
|---|
| 536 |
|
|---|
| 537 | private static boolean isRelationAffected(Relation r, Set<Node> affectedNodes, Collection<Way> ways) {
|
|---|
| 538 | if (!r.isUsable())
|
|---|
| 539 | return false;
|
|---|
| 540 | // see #18670: suppress notification when well known restriction types are not affected
|
|---|
| 541 | if (!r.hasTag("type", "restriction", "connectivity", "destination_sign") || r.hasIncompleteMembers())
|
|---|
| 542 | return true;
|
|---|
| 543 | int count = 0;
|
|---|
| 544 | for (RelationMember rm : r.getMembers()) {
|
|---|
| 545 | if (rm.isNode() && affectedNodes.contains(rm.getNode()))
|
|---|
| 546 | count++;
|
|---|
| 547 | if (rm.isWay() && ways.contains(rm.getWay())) {
|
|---|
| 548 | count++;
|
|---|
| 549 | if ("via".equals(rm.getRole())) {
|
|---|
| 550 | count++;
|
|---|
| 551 | }
|
|---|
| 552 | }
|
|---|
| 553 | }
|
|---|
| 554 | return count >= 2;
|
|---|
| 555 | }
|
|---|
| 556 | }
|
|---|