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