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.UndoRedoHandler; 019import org.openstreetmap.josm.data.osm.DataSet; 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 setHelpId(ht("/Action/UnJoinNodeWay")); 041 } 042 043 /** 044 * Called when the action is executed. 045 */ 046 @Override 047 public void actionPerformed(ActionEvent e) { 048 049 final DataSet dataSet = getLayerManager().getEditDataSet(); 050 List<Node> selectedNodes = new ArrayList<>(dataSet.getSelectedNodes()); 051 List<Way> selectedWays = new ArrayList<>(dataSet.getSelectedWays()); 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 UndoRedoHandler.getInstance().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}