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