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 = getLayerManager().getEditDataSet().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 } 094 095 /** 096 * Send a notification message. 097 * @param msg Message to be sent. 098 * @param messageType Nature of the message. 099 */ 100 public void notify(String msg, int messageType) { 101 new Notification(msg).setIcon(messageType).show(); 102 } 103 104 /** 105 * Removes irrelevant nodes from user selection. 106 * 107 * The action can be performed reliably even if we remove : 108 * * Nodes not referenced by any ways 109 * * When only one way is selected, nodes not part of this way (#10396). 110 * 111 * @param selectedWays List of user selected way. 112 * @param selectedNodes List of user selected nodes. 113 * @return New list of nodes cleaned of irrelevant nodes. 114 */ 115 private List<Node> cleanSelectedNodes(List<Way> selectedWays, 116 List<Node> selectedNodes) { 117 List<Node> resultingNodes = new LinkedList<>(); 118 119 // List of node referenced by a route 120 for (Node n: selectedNodes) { 121 if (n.isReferredByWays(1)) { 122 resultingNodes.add(n); 123 } 124 } 125 // If exactly one selected way, remove node not referencing par this way. 126 if (selectedWays.size() == 1) { 127 Way w = selectedWays.get(0); 128 for (Node n: new ArrayList<Node>(resultingNodes)) { 129 if (!w.containsNode(n)) { 130 resultingNodes.remove(n); 131 } 132 } 133 } 134 // Warn if nodes were removed 135 if (resultingNodes.size() != selectedNodes.size()) { 136 notify(tr("Some irrelevant nodes have been removed from the selection"), 137 JOptionPane.INFORMATION_MESSAGE); 138 } 139 return resultingNodes; 140 } 141 142 /** 143 * Find ways to which the disconnect can be applied. This is the list of ways 144 * with more than two nodes which pass through all the given nodes, intersected 145 * with the selected ways (if any) 146 * @param selectedWays List of user selected ways. 147 * @param selectedNodes List of user selected nodes. 148 * @return List of relevant ways 149 */ 150 static List<Way> getApplicableWays(List<Way> selectedWays, List<Node> selectedNodes) { 151 if (selectedNodes.isEmpty()) 152 return null; 153 154 // List of ways shared by all nodes 155 List<Way> result = new ArrayList<>(OsmPrimitive.getFilteredList(selectedNodes.get(0).getReferrers(), Way.class)); 156 for (int i = 1; i < selectedNodes.size(); i++) { 157 List<OsmPrimitive> ref = selectedNodes.get(i).getReferrers(); 158 for (Iterator<Way> it = result.iterator(); it.hasNext();) { 159 if (!ref.contains(it.next())) { 160 it.remove(); 161 } 162 } 163 } 164 165 // Remove broken ways 166 for (Iterator<Way> it = result.iterator(); it.hasNext();) { 167 if (it.next().getNodesCount() <= 2) { 168 it.remove(); 169 } 170 } 171 172 if (selectedWays.isEmpty()) 173 return result; 174 else { 175 // Return only selected ways 176 for (Iterator<Way> it = result.iterator(); it.hasNext();) { 177 if (!selectedWays.contains(it.next())) { 178 it.remove(); 179 } 180 } 181 return result; 182 } 183 } 184 185 @Override 186 protected void updateEnabledState() { 187 updateEnabledStateOnCurrentSelection(); 188 } 189 190 @Override 191 protected void updateEnabledState(Collection<? extends OsmPrimitive> selection) { 192 setEnabled(selection != null && !selection.isEmpty()); 193 } 194}