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.Component; 009import java.awt.GridBagLayout; 010import java.awt.event.ActionEvent; 011import java.awt.event.KeyEvent; 012import java.util.ArrayList; 013import java.util.Collection; 014import java.util.Collections; 015import java.util.Iterator; 016import java.util.List; 017import java.util.concurrent.atomic.AtomicInteger; 018 019import javax.swing.DefaultListCellRenderer; 020import javax.swing.JLabel; 021import javax.swing.JList; 022import javax.swing.JOptionPane; 023import javax.swing.JPanel; 024import javax.swing.ListSelectionModel; 025 026import org.openstreetmap.josm.command.SplitWayCommand; 027import org.openstreetmap.josm.data.UndoRedoHandler; 028import org.openstreetmap.josm.data.osm.DataSet; 029import org.openstreetmap.josm.data.osm.DefaultNameFormatter; 030import org.openstreetmap.josm.data.osm.Node; 031import org.openstreetmap.josm.data.osm.OsmPrimitive; 032import org.openstreetmap.josm.data.osm.OsmUtils; 033import org.openstreetmap.josm.data.osm.PrimitiveId; 034import org.openstreetmap.josm.data.osm.Way; 035import org.openstreetmap.josm.data.osm.WaySegment; 036import org.openstreetmap.josm.gui.ExtendedDialog; 037import org.openstreetmap.josm.gui.MainApplication; 038import org.openstreetmap.josm.gui.MapFrame; 039import org.openstreetmap.josm.gui.Notification; 040import org.openstreetmap.josm.tools.GBC; 041import org.openstreetmap.josm.tools.Shortcut; 042 043/** 044 * Splits a way into multiple ways (all identical except for their node list). 045 * 046 * Ways are just split at the selected nodes. The nodes remain in their 047 * original order. Selected nodes at the end of a way are ignored. 048 */ 049public class SplitWayAction extends JosmAction { 050 051 /** 052 * Create a new SplitWayAction. 053 */ 054 public SplitWayAction() { 055 super(tr("Split Way"), "splitway", tr("Split a way at the selected node."), 056 Shortcut.registerShortcut("tools:splitway", tr("Tool: {0}", tr("Split Way")), KeyEvent.VK_P, Shortcut.DIRECT), true); 057 setHelpId(ht("/Action/SplitWay")); 058 } 059 060 /** 061 * Called when the action is executed. 062 * 063 * This method performs an expensive check whether the selection clearly defines one 064 * of the split actions outlined above, and if yes, calls the splitWay method. 065 */ 066 @Override 067 public void actionPerformed(ActionEvent e) { 068 runOn(getLayerManager().getEditDataSet()); 069 } 070 071 /** 072 * Run the action on the given dataset. 073 * @param ds dataset 074 * @since 14542 075 */ 076 public static void runOn(DataSet ds) { 077 078 if (SegmentToKeepSelectionDialog.DISPLAY_COUNT.get() > 0) { 079 new Notification(tr("Cannot split since another split operation is already in progress")) 080 .setIcon(JOptionPane.WARNING_MESSAGE).show(); 081 return; 082 } 083 084 List<Node> selectedNodes = new ArrayList<>(ds.getSelectedNodes()); 085 List<Way> selectedWays = new ArrayList<>(ds.getSelectedWays()); 086 List<Way> applicableWays = getApplicableWays(selectedWays, selectedNodes); 087 088 if (applicableWays == null) { 089 new Notification( 090 tr("The current selection cannot be used for splitting - no node is selected.")) 091 .setIcon(JOptionPane.WARNING_MESSAGE) 092 .show(); 093 return; 094 } else if (applicableWays.isEmpty()) { 095 new Notification( 096 tr("The selected nodes do not share the same way.")) 097 .setIcon(JOptionPane.WARNING_MESSAGE) 098 .show(); 099 return; 100 } 101 102 // If several ways have been found, remove ways that doesn't have selected 103 // node in the middle 104 if (applicableWays.size() > 1) { 105 for (Iterator<Way> it = applicableWays.iterator(); it.hasNext();) { 106 Way w = it.next(); 107 for (Node n : selectedNodes) { 108 if (!w.isInnerNode(n)) { 109 it.remove(); 110 break; 111 } 112 } 113 } 114 } 115 116 if (applicableWays.isEmpty()) { 117 new Notification( 118 trn("The selected node is not in the middle of any way.", 119 "The selected nodes are not in the middle of any way.", 120 selectedNodes.size())) 121 .setIcon(JOptionPane.WARNING_MESSAGE) 122 .show(); 123 return; 124 } else if (applicableWays.size() > 1) { 125 new Notification( 126 trn("There is more than one way using the node you selected. Please select the way also.", 127 "There is more than one way using the nodes you selected. Please select the way also.", 128 selectedNodes.size())) 129 .setIcon(JOptionPane.WARNING_MESSAGE) 130 .show(); 131 return; 132 } 133 134 // Finally, applicableWays contains only one perfect way 135 final Way selectedWay = applicableWays.get(0); 136 final List<List<Node>> wayChunks = SplitWayCommand.buildSplitChunks(selectedWay, selectedNodes); 137 if (wayChunks != null) { 138 final List<OsmPrimitive> sel = new ArrayList<>(ds.getSelectedRelations()); 139 sel.addAll(selectedWays); 140 141 final List<Way> newWays = SplitWayCommand.createNewWaysFromChunks(selectedWay, wayChunks); 142 final Way wayToKeep = SplitWayCommand.Strategy.keepLongestChunk().determineWayToKeep(newWays); 143 144 if (ExpertToggleAction.isExpert() && !selectedWay.isNew()) { 145 final ExtendedDialog dialog = new SegmentToKeepSelectionDialog(selectedWay, newWays, wayToKeep, sel); 146 dialog.toggleEnable("way.split.segment-selection-dialog"); 147 if (!dialog.toggleCheckState()) { 148 dialog.setModal(false); 149 dialog.showDialog(); 150 return; // splitting is performed in SegmentToKeepSelectionDialog.buttonAction() 151 } 152 } 153 if (wayToKeep != null) { 154 doSplitWay(selectedWay, wayToKeep, newWays, sel); 155 } 156 } 157 } 158 159 /** 160 * A dialog to query which way segment should reuse the history of the way to split. 161 */ 162 static class SegmentToKeepSelectionDialog extends ExtendedDialog { 163 static final AtomicInteger DISPLAY_COUNT = new AtomicInteger(); 164 final transient Way selectedWay; 165 final transient List<Way> newWays; 166 final JList<Way> list; 167 final transient List<OsmPrimitive> selection; 168 final transient Way wayToKeep; 169 170 SegmentToKeepSelectionDialog(Way selectedWay, List<Way> newWays, Way wayToKeep, List<OsmPrimitive> selection) { 171 super(MainApplication.getMainFrame(), tr("Which way segment should reuse the history of {0}?", selectedWay.getId()), 172 new String[]{tr("Ok"), tr("Cancel")}, true); 173 174 this.selectedWay = selectedWay; 175 this.newWays = newWays; 176 this.selection = selection; 177 this.wayToKeep = wayToKeep; 178 this.list = new JList<>(newWays.toArray(new Way[0])); 179 configureList(); 180 181 setButtonIcons("ok", "cancel"); 182 final JPanel pane = new JPanel(new GridBagLayout()); 183 pane.add(new JLabel(getTitle()), GBC.eol().fill(GBC.HORIZONTAL)); 184 pane.add(list, GBC.eop().fill(GBC.HORIZONTAL)); 185 setContent(pane); 186 setDefaultCloseOperation(HIDE_ON_CLOSE); 187 } 188 189 private void configureList() { 190 list.setSelectionMode(ListSelectionModel.SINGLE_SELECTION); 191 list.addListSelectionListener(e -> { 192 final Way selected = list.getSelectedValue(); 193 if (selected != null && MainApplication.isDisplayingMapView() && selected.getNodesCount() > 1) { 194 final Collection<WaySegment> segments = new ArrayList<>(selected.getNodesCount() - 1); 195 final Iterator<Node> it = selected.getNodes().iterator(); 196 Node previousNode = it.next(); 197 while (it.hasNext()) { 198 final Node node = it.next(); 199 segments.add(WaySegment.forNodePair(selectedWay, previousNode, node)); 200 previousNode = node; 201 } 202 setHighlightedWaySegments(segments); 203 } 204 }); 205 list.setCellRenderer(new SegmentListCellRenderer()); 206 } 207 208 protected void setHighlightedWaySegments(Collection<WaySegment> segments) { 209 selectedWay.getDataSet().setHighlightedWaySegments(segments); 210 MainApplication.getMap().mapView.repaint(); 211 } 212 213 @Override 214 public void setVisible(boolean visible) { 215 super.setVisible(visible); 216 if (visible) { 217 DISPLAY_COUNT.incrementAndGet(); 218 list.setSelectedValue(wayToKeep, true); 219 } else { 220 setHighlightedWaySegments(Collections.<WaySegment>emptyList()); 221 DISPLAY_COUNT.decrementAndGet(); 222 } 223 } 224 225 @Override 226 protected void buttonAction(int buttonIndex, ActionEvent evt) { 227 super.buttonAction(buttonIndex, evt); 228 toggleSaveState(); // necessary since #showDialog() does not handle it due to the non-modal dialog 229 if (getValue() == 1) { 230 doSplitWay(selectedWay, list.getSelectedValue(), newWays, selection); 231 } 232 } 233 } 234 235 static class SegmentListCellRenderer extends DefaultListCellRenderer { 236 @Override 237 public Component getListCellRendererComponent(JList<?> list, Object value, int index, boolean isSelected, boolean cellHasFocus) { 238 final Component c = super.getListCellRendererComponent(list, value, index, isSelected, cellHasFocus); 239 final String name = DefaultNameFormatter.getInstance().format((Way) value); 240 // get rid of id from DefaultNameFormatter.decorateNameWithId() 241 final String nameWithoutId = name 242 .replace(tr(" [id: {0}]", ((Way) value).getId()), "") 243 .replace(tr(" [id: {0}]", ((Way) value).getUniqueId()), ""); 244 ((JLabel) c).setText(tr("Segment {0}: {1}", index + 1, nameWithoutId)); 245 return c; 246 } 247 } 248 249 /** 250 * Determine which ways to split. 251 * @param selectedWays List of user selected ways. 252 * @param selectedNodes List of user selected nodes. 253 * @return List of ways to split 254 */ 255 static List<Way> getApplicableWays(List<Way> selectedWays, List<Node> selectedNodes) { 256 if (selectedNodes.isEmpty()) 257 return null; 258 259 // Special case - one of the selected ways touches (not cross) way that we want to split 260 if (selectedNodes.size() == 1) { 261 Node n = selectedNodes.get(0); 262 List<Way> referredWays = n.getParentWays(); 263 Way inTheMiddle = null; 264 for (Way w: referredWays) { 265 // Need to look at all nodes see #11184 for a case where node n is 266 // firstNode, lastNode and also in the middle 267 if (selectedWays.contains(w) && w.isInnerNode(n)) { 268 if (inTheMiddle == null) { 269 inTheMiddle = w; 270 } else { 271 inTheMiddle = null; 272 break; 273 } 274 } 275 } 276 if (inTheMiddle != null) 277 return Collections.singletonList(inTheMiddle); 278 } 279 280 // List of ways shared by all nodes 281 return UnJoinNodeWayAction.getApplicableWays(selectedWays, selectedNodes); 282 } 283 284 static void doSplitWay(Way way, Way wayToKeep, List<Way> newWays, List<OsmPrimitive> newSelection) { 285 final MapFrame map = MainApplication.getMap(); 286 final boolean isMapModeDraw = map != null && map.mapMode == map.mapModeDraw; 287 final SplitWayCommand result = SplitWayCommand.doSplitWay(way, wayToKeep, newWays, !isMapModeDraw ? newSelection : null); 288 UndoRedoHandler.getInstance().add(result); 289 List<? extends PrimitiveId> newSel = result.getNewSelection(); 290 if (newSel != null && !newSel.isEmpty()) { 291 way.getDataSet().setSelected(newSel); 292 } 293 } 294 295 @Override 296 protected void updateEnabledState() { 297 updateEnabledStateOnCurrentSelection(); 298 } 299 300 @Override 301 protected void updateEnabledState(Collection<? extends OsmPrimitive> selection) { 302 // Selection still can be wrong, but let SplitWayAction process and tell user what's wrong 303 setEnabled(OsmUtils.isOsmCollectionEditable(selection) 304 && selection.stream().anyMatch(o -> o instanceof Node && !o.isIncomplete())); 305 } 306}