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