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}