// License: GPL. For details, see LICENSE file.
package org.openstreetmap.josm.actions;
import static org.openstreetmap.josm.gui.help.HelpUtil.ht;
import static org.openstreetmap.josm.tools.I18n.tr;
import static org.openstreetmap.josm.tools.I18n.trn;
import java.awt.event.ActionEvent;
import java.awt.event.KeyEvent;
import java.util.ArrayList;
import java.util.Collection;
import java.util.LinkedList;
import java.util.List;
import javax.swing.JOptionPane;
import org.openstreetmap.josm.Main;
import org.openstreetmap.josm.command.RemoveNodesCommand;
import org.openstreetmap.josm.data.osm.Node;
import org.openstreetmap.josm.data.osm.OsmPrimitive;
import org.openstreetmap.josm.data.osm.Way;
import org.openstreetmap.josm.gui.Notification;
import org.openstreetmap.josm.tools.Shortcut;
/**
* Disconnect nodes from a way they currently belong to.
* @since 6253
*/
public class UnJoinNodeWayAction extends JosmAction {
/**
* Constructs a new {@code UnJoinNodeWayAction}.
*/
public UnJoinNodeWayAction() {
super(tr("Disconnect Node from Way"), "unjoinnodeway",
tr("Disconnect nodes from a way they currently belong to"),
Shortcut.registerShortcut("tools:unjoinnodeway",
tr("Tool: {0}", tr("Disconnect Node from Way")), KeyEvent.VK_J, Shortcut.ALT), true);
putValue("help", ht("/Action/UnJoinNodeWay"));
}
/**
* Called when the action is executed.
*/
@Override
public void actionPerformed(ActionEvent e) {
Collection<OsmPrimitive> selection = getLayerManager().getEditDataSet().getSelected();
List<Node> selectedNodes = OsmPrimitive.getFilteredList(selection, Node.class);
List<Way> selectedWays = OsmPrimitive.getFilteredList(selection, Way.class);
selectedNodes = cleanSelectedNodes(selectedWays, selectedNodes);
List<Way> applicableWays = getApplicableWays(selectedWays, selectedNodes);
if (applicableWays == null) {
notify(tr("Select at least one node to be disconnected."),
JOptionPane.WARNING_MESSAGE);
return;
} else if (applicableWays.isEmpty()) {
notify(trn("Selected node cannot be disconnected from anything.",
"Selected nodes cannot be disconnected from anything.",
selectedNodes.size()),
JOptionPane.WARNING_MESSAGE);
return;
} else if (applicableWays.size() > 1) {
notify(trn("There is more than one way using the node you selected. "
+ "Please select the way also.",
"There is more than one way using the nodes you selected. "
+ "Please select the way also.",
selectedNodes.size()),
JOptionPane.WARNING_MESSAGE);
return;
} else if (applicableWays.get(0).getRealNodesCount() < selectedNodes.size() + 2) {
// there is only one affected way, but removing the selected nodes would only leave it
// with less than 2 nodes
notify(trn("The affected way would disappear after disconnecting the "
+ "selected node.",
"The affected way would disappear after disconnecting the "
+ "selected nodes.",
selectedNodes.size()),
JOptionPane.WARNING_MESSAGE);
return;
}
// Finally, applicableWays contains only one perfect way
Way selectedWay = applicableWays.get(0);
// I'm sure there's a better way to handle this
Main.main.undoRedo.add(new RemoveNodesCommand(selectedWay, selectedNodes));
}
/**
* Send a notification message.
* @param msg Message to be sent.
* @param messageType Nature of the message.
*/
public void notify(String msg, int messageType) {
new Notification(msg).setIcon(messageType).show();
}
/**
* Removes irrelevant nodes from user selection.
*
* The action can be performed reliably even if we remove :
* * Nodes not referenced by any ways
* * When only one way is selected, nodes not part of this way (#10396).
*
* @param selectedWays List of user selected way.
* @param selectedNodes List of user selected nodes.
* @return New list of nodes cleaned of irrelevant nodes.
*/
private List<Node> cleanSelectedNodes(List<Way> selectedWays,
List<Node> selectedNodes) {
List<Node> resultingNodes = new LinkedList<>();
// List of node referenced by a route
for (Node n: selectedNodes) {
if (n.isReferredByWays(1)) {
resultingNodes.add(n);
}
}
// If exactly one selected way, remove node not referencing par this way.
if (selectedWays.size() == 1) {
Way w = selectedWays.get(0);
for (Node n: new ArrayList<Node>(resultingNodes)) {
if (!w.containsNode(n)) {
resultingNodes.remove(n);
}
}
}
// Warn if nodes were removed
if (resultingNodes.size() != selectedNodes.size()) {
notify(tr("Some irrelevant nodes have been removed from the selection"),
JOptionPane.INFORMATION_MESSAGE);
}
return resultingNodes;
}
/**
* Find ways to which the disconnect can be applied. This is the list of ways
* with more than two nodes which pass through all the given nodes, intersected
* with the selected ways (if any)
* @param selectedWays List of user selected ways.
* @param selectedNodes List of user selected nodes.
* @return List of relevant ways
*/
static List<Way> getApplicableWays(List<Way> selectedWays, List<Node> selectedNodes) {
if (selectedNodes.isEmpty())
return null;
// List of ways shared by all nodes
List<Way> result = new ArrayList<>(OsmPrimitive.getFilteredList(selectedNodes.get(0).getReferrers(), Way.class));
for (int i = 1; i < selectedNodes.size(); i++) {
List<OsmPrimitive> ref = selectedNodes.get(i).getReferrers();
result.removeIf(way -> !ref.contains(way));
}
// Remove broken ways
result.removeIf(way -> way.getNodesCount() <= 2);
if (selectedWays.isEmpty())
return result;
else {
// Return only selected ways
result.removeIf(way -> !selectedWays.contains(way));
return result;
}
}
@Override
protected void updateEnabledState() {
updateEnabledStateOnCurrentSelection();
}
@Override
protected void updateEnabledState(Collection<? extends OsmPrimitive> selection) {
setEnabled(selection != null && !selection.isEmpty());
}
}