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}