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