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.Main; 027import org.openstreetmap.josm.command.SplitWayCommand; 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.PrimitiveId; 033import org.openstreetmap.josm.data.osm.Relation; 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 putValue("help", 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 069 if (SegmentToKeepSelectionDialog.DISPLAY_COUNT.get() > 0) { 070 new Notification(tr("Cannot split since another split operation is already in progress")) 071 .setIcon(JOptionPane.WARNING_MESSAGE).show(); 072 return; 073 } 074 075 Collection<OsmPrimitive> selection = getLayerManager().getEditDataSet().getSelected(); 076 077 List<Node> selectedNodes = OsmPrimitive.getFilteredList(selection, Node.class); 078 List<Way> selectedWays = OsmPrimitive.getFilteredList(selection, Way.class); 079 List<Way> applicableWays = getApplicableWays(selectedWays, selectedNodes); 080 081 if (applicableWays == null) { 082 new Notification( 083 tr("The current selection cannot be used for splitting - no node is selected.")) 084 .setIcon(JOptionPane.WARNING_MESSAGE) 085 .show(); 086 return; 087 } else if (applicableWays.isEmpty()) { 088 new Notification( 089 tr("The selected nodes do not share the same way.")) 090 .setIcon(JOptionPane.WARNING_MESSAGE) 091 .show(); 092 return; 093 } 094 095 // If several ways have been found, remove ways that doesn't have selected 096 // node in the middle 097 if (applicableWays.size() > 1) { 098 for (Iterator<Way> it = applicableWays.iterator(); it.hasNext();) { 099 Way w = it.next(); 100 for (Node n : selectedNodes) { 101 if (!w.isInnerNode(n)) { 102 it.remove(); 103 break; 104 } 105 } 106 } 107 } 108 109 if (applicableWays.isEmpty()) { 110 new Notification( 111 trn("The selected node is not in the middle of any way.", 112 "The selected nodes are not in the middle of any way.", 113 selectedNodes.size())) 114 .setIcon(JOptionPane.WARNING_MESSAGE) 115 .show(); 116 return; 117 } else if (applicableWays.size() > 1) { 118 new Notification( 119 trn("There is more than one way using the node you selected. Please select the way also.", 120 "There is more than one way using the nodes you selected. Please select the way also.", 121 selectedNodes.size())) 122 .setIcon(JOptionPane.WARNING_MESSAGE) 123 .show(); 124 return; 125 } 126 127 // Finally, applicableWays contains only one perfect way 128 final Way selectedWay = applicableWays.get(0); 129 final List<List<Node>> wayChunks = SplitWayCommand.buildSplitChunks(selectedWay, selectedNodes); 130 if (wayChunks != null) { 131 List<Relation> selectedRelations = OsmPrimitive.getFilteredList(selection, Relation.class); 132 final List<OsmPrimitive> sel = new ArrayList<>(selectedWays.size() + selectedRelations.size()); 133 sel.addAll(selectedWays); 134 sel.addAll(selectedRelations); 135 136 final List<Way> newWays = SplitWayCommand.createNewWaysFromChunks(selectedWay, wayChunks); 137 final Way wayToKeep = SplitWayCommand.Strategy.keepLongestChunk().determineWayToKeep(newWays); 138 139 if (ExpertToggleAction.isExpert() && !selectedWay.isNew()) { 140 final ExtendedDialog dialog = new SegmentToKeepSelectionDialog(selectedWay, newWays, wayToKeep, sel); 141 dialog.toggleEnable("way.split.segment-selection-dialog"); 142 if (!dialog.toggleCheckState()) { 143 dialog.setModal(false); 144 dialog.showDialog(); 145 return; // splitting is performed in SegmentToKeepSelectionDialog.buttonAction() 146 } 147 } 148 if (wayToKeep != null) { 149 doSplitWay(selectedWay, wayToKeep, newWays, sel); 150 } 151 } 152 } 153 154 /** 155 * A dialog to query which way segment should reuse the history of the way to split. 156 */ 157 static class SegmentToKeepSelectionDialog extends ExtendedDialog { 158 static final AtomicInteger DISPLAY_COUNT = new AtomicInteger(); 159 final transient Way selectedWay; 160 final transient List<Way> newWays; 161 final JList<Way> list; 162 final transient List<OsmPrimitive> selection; 163 final transient Way wayToKeep; 164 165 SegmentToKeepSelectionDialog(Way selectedWay, List<Way> newWays, Way wayToKeep, List<OsmPrimitive> selection) { 166 super(Main.parent, tr("Which way segment should reuse the history of {0}?", selectedWay.getId()), 167 new String[]{tr("Ok"), tr("Cancel")}, true); 168 169 this.selectedWay = selectedWay; 170 this.newWays = newWays; 171 this.selection = selection; 172 this.wayToKeep = wayToKeep; 173 this.list = new JList<>(newWays.toArray(new Way[0])); 174 configureList(); 175 176 setButtonIcons("ok", "cancel"); 177 final JPanel pane = new JPanel(new GridBagLayout()); 178 pane.add(new JLabel(getTitle()), GBC.eol().fill(GBC.HORIZONTAL)); 179 pane.add(list, GBC.eop().fill(GBC.HORIZONTAL)); 180 setContent(pane); 181 setDefaultCloseOperation(HIDE_ON_CLOSE); 182 } 183 184 private void configureList() { 185 list.setSelectionMode(ListSelectionModel.SINGLE_SELECTION); 186 list.addListSelectionListener(e -> { 187 final Way selected = list.getSelectedValue(); 188 if (selected != null && MainApplication.isDisplayingMapView() && selected.getNodesCount() > 1) { 189 final Collection<WaySegment> segments = new ArrayList<>(selected.getNodesCount() - 1); 190 final Iterator<Node> it = selected.getNodes().iterator(); 191 Node previousNode = it.next(); 192 while (it.hasNext()) { 193 final Node node = it.next(); 194 segments.add(WaySegment.forNodePair(selectedWay, previousNode, node)); 195 previousNode = node; 196 } 197 setHighlightedWaySegments(segments); 198 } 199 }); 200 list.setCellRenderer(new SegmentListCellRenderer()); 201 } 202 203 protected void setHighlightedWaySegments(Collection<WaySegment> segments) { 204 selectedWay.getDataSet().setHighlightedWaySegments(segments); 205 MainApplication.getMap().mapView.repaint(); 206 } 207 208 @Override 209 public void setVisible(boolean visible) { 210 super.setVisible(visible); 211 if (visible) { 212 DISPLAY_COUNT.incrementAndGet(); 213 list.setSelectedValue(wayToKeep, true); 214 } else { 215 setHighlightedWaySegments(Collections.<WaySegment>emptyList()); 216 DISPLAY_COUNT.decrementAndGet(); 217 } 218 } 219 220 @Override 221 protected void buttonAction(int buttonIndex, ActionEvent evt) { 222 super.buttonAction(buttonIndex, evt); 223 toggleSaveState(); // necessary since #showDialog() does not handle it due to the non-modal dialog 224 if (getValue() == 1) { 225 doSplitWay(selectedWay, list.getSelectedValue(), newWays, selection); 226 } 227 } 228 } 229 230 static class SegmentListCellRenderer extends DefaultListCellRenderer { 231 @Override 232 public Component getListCellRendererComponent(JList<?> list, Object value, int index, boolean isSelected, boolean cellHasFocus) { 233 final Component c = super.getListCellRendererComponent(list, value, index, isSelected, cellHasFocus); 234 final String name = DefaultNameFormatter.getInstance().format((Way) value); 235 // get rid of id from DefaultNameFormatter.decorateNameWithId() 236 final String nameWithoutId = name 237 .replace(tr(" [id: {0}]", ((Way) value).getId()), "") 238 .replace(tr(" [id: {0}]", ((Way) value).getUniqueId()), ""); 239 ((JLabel) c).setText(tr("Segment {0}: {1}", index + 1, nameWithoutId)); 240 return c; 241 } 242 } 243 244 /** 245 * Determine which ways to split. 246 * @param selectedWays List of user selected ways. 247 * @param selectedNodes List of user selected nodes. 248 * @return List of ways to split 249 */ 250 static List<Way> getApplicableWays(List<Way> selectedWays, List<Node> selectedNodes) { 251 if (selectedNodes.isEmpty()) 252 return null; 253 254 // Special case - one of the selected ways touches (not cross) way that we want to split 255 if (selectedNodes.size() == 1) { 256 Node n = selectedNodes.get(0); 257 List<Way> referredWays = n.getParentWays(); 258 Way inTheMiddle = null; 259 for (Way w: referredWays) { 260 // Need to look at all nodes see #11184 for a case where node n is 261 // firstNode, lastNode and also in the middle 262 if (selectedWays.contains(w) && w.isInnerNode(n)) { 263 if (inTheMiddle == null) { 264 inTheMiddle = w; 265 } else { 266 inTheMiddle = null; 267 break; 268 } 269 } 270 } 271 if (inTheMiddle != null) 272 return Collections.singletonList(inTheMiddle); 273 } 274 275 // List of ways shared by all nodes 276 return UnJoinNodeWayAction.getApplicableWays(selectedWays, selectedNodes); 277 } 278 279 static void doSplitWay(Way way, Way wayToKeep, List<Way> newWays, List<OsmPrimitive> newSelection) { 280 final MapFrame map = MainApplication.getMap(); 281 final boolean isMapModeDraw = map != null && map.mapMode == map.mapModeDraw; 282 final SplitWayCommand result = SplitWayCommand.doSplitWay(way, wayToKeep, newWays, !isMapModeDraw ? newSelection : null); 283 MainApplication.undoRedo.add(result); 284 List<? extends PrimitiveId> newSel = result.getNewSelection(); 285 if (newSel != null && !newSel.isEmpty()) { 286 MainApplication.getLayerManager().getEditDataSet().setSelected(newSel); 287 } 288 } 289 290 @Override 291 protected void updateEnabledState() { 292 updateEnabledStateOnCurrentSelection(); 293 } 294 295 @Override 296 protected void updateEnabledState(Collection<? extends OsmPrimitive> selection) { 297 // Selection still can be wrong, but let SplitWayAction process and tell user what's wrong 298 setEnabled(selection != null && !selection.isEmpty() 299 && selection.stream().map(OsmPrimitive::getDataSet).noneMatch(DataSet::isLocked) 300 && selection.stream().anyMatch(o -> o instanceof Node && !o.isIncomplete())); 301 } 302}