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 private 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}