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