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; 015 016import javax.swing.JOptionPane; 017 018import org.openstreetmap.josm.command.RemoveNodesCommand; 019import org.openstreetmap.josm.data.UndoRedoHandler; 020import org.openstreetmap.josm.data.osm.DataSet; 021import org.openstreetmap.josm.data.osm.Node; 022import org.openstreetmap.josm.data.osm.OsmPrimitive; 023import org.openstreetmap.josm.data.osm.Way; 024import org.openstreetmap.josm.gui.Notification; 025import org.openstreetmap.josm.tools.Shortcut; 026 027/** 028 * Disconnect nodes from a way they currently belong to. 029 * @since 6253 030 */ 031public class UnJoinNodeWayAction extends JosmAction { 032 033 /** 034 * Constructs a new {@code UnJoinNodeWayAction}. 035 */ 036 public UnJoinNodeWayAction() { 037 super(tr("Disconnect Node from Way"), "unjoinnodeway", 038 tr("Disconnect nodes from a way they currently belong to"), 039 Shortcut.registerShortcut("tools:unjoinnodeway", 040 tr("Tool: {0}", tr("Disconnect Node from Way")), KeyEvent.VK_J, Shortcut.ALT), true); 041 setHelpId(ht("/Action/UnJoinNodeWay")); 042 } 043 044 /** 045 * Called when the action is executed. 046 */ 047 @Override 048 public void actionPerformed(ActionEvent e) { 049 050 final DataSet dataSet = getLayerManager().getEditDataSet(); 051 List<Node> selectedNodes = new ArrayList<>(dataSet.getSelectedNodes()); 052 List<Way> selectedWays = new ArrayList<>(dataSet.getSelectedWays()); 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 UndoRedoHandler.getInstance().add( 093 new RemoveNodesCommand(selectedWay, new HashSet<>(selectedNodes))); 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<>(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<>(selectedNodes.get(0).getParentWays()); 157 for (int i = 1; i < selectedNodes.size(); i++) { 158 List<Way> ref = selectedNodes.get(i).getParentWays(); 159 result.removeIf(way -> !ref.contains(way)); 160 } 161 162 // Remove broken ways 163 result.removeIf(way -> way.getNodesCount() <= 2); 164 165 if (selectedWays.isEmpty()) 166 return result; 167 else { 168 // Return only selected ways 169 result.removeIf(way -> !selectedWays.contains(way)); 170 return result; 171 } 172 } 173 174 @Override 175 protected void updateEnabledState() { 176 updateEnabledStateOnCurrentSelection(); 177 } 178 179 @Override 180 protected void updateEnabledState(Collection<? extends OsmPrimitive> selection) { 181 updateEnabledStateOnModifiableSelection(selection); 182 } 183}