001// License: GPL. For details, see LICENSE file.
002package org.openstreetmap.josm.actions;
003
004import static org.openstreetmap.josm.gui.help.HelpUtil.ht;
005import static org.openstreetmap.josm.tools.I18n.tr;
006import static org.openstreetmap.josm.tools.I18n.trn;
007
008import java.awt.event.ActionEvent;
009import java.awt.event.KeyEvent;
010import java.util.ArrayList;
011import java.util.Collection;
012import java.util.LinkedList;
013import java.util.List;
014
015import javax.swing.JOptionPane;
016
017import org.openstreetmap.josm.command.RemoveNodesCommand;
018import org.openstreetmap.josm.data.osm.Node;
019import org.openstreetmap.josm.data.osm.OsmPrimitive;
020import org.openstreetmap.josm.data.osm.Way;
021import org.openstreetmap.josm.gui.MainApplication;
022import org.openstreetmap.josm.gui.Notification;
023import org.openstreetmap.josm.tools.Shortcut;
024
025/**
026 * Disconnect nodes from a way they currently belong to.
027 * @since 6253
028 */
029public class UnJoinNodeWayAction extends JosmAction {
030
031    /**
032     * Constructs a new {@code UnJoinNodeWayAction}.
033     */
034    public UnJoinNodeWayAction() {
035        super(tr("Disconnect Node from Way"), "unjoinnodeway",
036                tr("Disconnect nodes from a way they currently belong to"),
037                Shortcut.registerShortcut("tools:unjoinnodeway",
038                    tr("Tool: {0}", tr("Disconnect Node from Way")), KeyEvent.VK_J, Shortcut.ALT), true);
039        putValue("help", ht("/Action/UnJoinNodeWay"));
040    }
041
042    /**
043     * Called when the action is executed.
044     */
045    @Override
046    public void actionPerformed(ActionEvent e) {
047
048        Collection<OsmPrimitive> selection = getLayerManager().getEditDataSet().getSelected();
049
050        List<Node> selectedNodes = OsmPrimitive.getFilteredList(selection, Node.class);
051        List<Way> selectedWays = OsmPrimitive.getFilteredList(selection, Way.class);
052
053        selectedNodes = cleanSelectedNodes(selectedWays, selectedNodes);
054
055        List<Way> applicableWays = getApplicableWays(selectedWays, selectedNodes);
056
057        if (applicableWays == null) {
058            notify(tr("Select at least one node to be disconnected."),
059                   JOptionPane.WARNING_MESSAGE);
060            return;
061        } else if (applicableWays.isEmpty()) {
062            notify(trn("Selected node cannot be disconnected from anything.",
063                       "Selected nodes cannot be disconnected from anything.",
064                       selectedNodes.size()),
065                   JOptionPane.WARNING_MESSAGE);
066            return;
067        } else if (applicableWays.size() > 1) {
068            notify(trn("There is more than one way using the node you selected. "
069                       + "Please select the way also.",
070                       "There is more than one way using the nodes you selected. "
071                       + "Please select the way also.",
072                       selectedNodes.size()),
073                   JOptionPane.WARNING_MESSAGE);
074            return;
075        } else if (applicableWays.get(0).getRealNodesCount() < selectedNodes.size() + 2) {
076            // there is only one affected way, but removing the selected nodes would only leave it
077            // with less than 2 nodes
078            notify(trn("The affected way would disappear after disconnecting the "
079                       + "selected node.",
080                       "The affected way would disappear after disconnecting the "
081                       + "selected nodes.",
082                       selectedNodes.size()),
083                   JOptionPane.WARNING_MESSAGE);
084            return;
085        }
086
087        // Finally, applicableWays contains only one perfect way
088        Way selectedWay = applicableWays.get(0);
089
090        // I'm sure there's a better way to handle this
091        MainApplication.undoRedo.add(new RemoveNodesCommand(selectedWay, selectedNodes));
092    }
093
094    /**
095     * Send a notification message.
096     * @param msg Message to be sent.
097     * @param messageType Nature of the message.
098     */
099    public void notify(String msg, int messageType) {
100        new Notification(msg).setIcon(messageType).show();
101    }
102
103    /**
104     * Removes irrelevant nodes from user selection.
105     *
106     * The action can be performed reliably even if we remove :
107     *   * Nodes not referenced by any ways
108     *   * When only one way is selected, nodes not part of this way (#10396).
109     *
110     * @param selectedWays  List of user selected way.
111     * @param selectedNodes List of user selected nodes.
112     * @return New list of nodes cleaned of irrelevant nodes.
113     */
114    private List<Node> cleanSelectedNodes(List<Way> selectedWays,
115                                          List<Node> selectedNodes) {
116        List<Node> resultingNodes = new LinkedList<>();
117
118        // List of node referenced by a route
119        for (Node n: selectedNodes) {
120            if (n.isReferredByWays(1)) {
121                resultingNodes.add(n);
122            }
123        }
124        // If exactly one selected way, remove node not referencing par this way.
125        if (selectedWays.size() == 1) {
126            Way w = selectedWays.get(0);
127            for (Node n: new ArrayList<>(resultingNodes)) {
128                if (!w.containsNode(n)) {
129                    resultingNodes.remove(n);
130                }
131            }
132        }
133        // Warn if nodes were removed
134        if (resultingNodes.size() != selectedNodes.size()) {
135            notify(tr("Some irrelevant nodes have been removed from the selection"),
136                   JOptionPane.INFORMATION_MESSAGE);
137        }
138        return resultingNodes;
139    }
140
141    /**
142     * Find ways to which the disconnect can be applied. This is the list of ways
143     * with more than two nodes which pass through all the given nodes, intersected
144     * with the selected ways (if any)
145     * @param selectedWays List of user selected ways.
146     * @param selectedNodes List of user selected nodes.
147     * @return List of relevant ways
148     */
149    static List<Way> getApplicableWays(List<Way> selectedWays, List<Node> selectedNodes) {
150        if (selectedNodes.isEmpty())
151            return null;
152
153        // List of ways shared by all nodes
154        List<Way> result = new ArrayList<>(selectedNodes.get(0).getParentWays());
155        for (int i = 1; i < selectedNodes.size(); i++) {
156            List<Way> ref = selectedNodes.get(i).getParentWays();
157            result.removeIf(way -> !ref.contains(way));
158        }
159
160        // Remove broken ways
161        result.removeIf(way -> way.getNodesCount() <= 2);
162
163        if (selectedWays.isEmpty())
164            return result;
165        else {
166            // Return only selected ways
167            result.removeIf(way -> !selectedWays.contains(way));
168            return result;
169        }
170    }
171
172    @Override
173    protected void updateEnabledState() {
174        updateEnabledStateOnCurrentSelection();
175    }
176
177    @Override
178    protected void updateEnabledState(Collection<? extends OsmPrimitive> selection) {
179        updateEnabledStateOnModifiableSelection(selection);
180    }
181}