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.Arrays;
014import java.util.Collection;
015import java.util.Collections;
016import java.util.HashSet;
017import java.util.Iterator;
018import java.util.LinkedList;
019import java.util.List;
020import java.util.Set;
021import java.util.concurrent.atomic.AtomicInteger;
022
023import javax.swing.DefaultListCellRenderer;
024import javax.swing.JLabel;
025import javax.swing.JList;
026import javax.swing.JOptionPane;
027import javax.swing.JPanel;
028import javax.swing.ListSelectionModel;
029
030import org.openstreetmap.josm.Main;
031import org.openstreetmap.josm.command.AddCommand;
032import org.openstreetmap.josm.command.ChangeCommand;
033import org.openstreetmap.josm.command.Command;
034import org.openstreetmap.josm.command.SequenceCommand;
035import org.openstreetmap.josm.data.osm.Node;
036import org.openstreetmap.josm.data.osm.OsmPrimitive;
037import org.openstreetmap.josm.data.osm.PrimitiveId;
038import org.openstreetmap.josm.data.osm.Relation;
039import org.openstreetmap.josm.data.osm.RelationMember;
040import org.openstreetmap.josm.data.osm.Way;
041import org.openstreetmap.josm.data.osm.WaySegment;
042import org.openstreetmap.josm.gui.DefaultNameFormatter;
043import org.openstreetmap.josm.gui.ExtendedDialog;
044import org.openstreetmap.josm.gui.Notification;
045import org.openstreetmap.josm.gui.layer.OsmDataLayer;
046import org.openstreetmap.josm.tools.CheckParameterUtil;
047import org.openstreetmap.josm.tools.GBC;
048import org.openstreetmap.josm.tools.Shortcut;
049
050/**
051 * Splits a way into multiple ways (all identical except for their node list).
052 *
053 * Ways are just split at the selected nodes.  The nodes remain in their
054 * original order.  Selected nodes at the end of a way are ignored.
055 */
056public class SplitWayAction extends JosmAction {
057
058    /**
059     * Represents the result of a {@link SplitWayAction}
060     * @see SplitWayAction#splitWay
061     * @see SplitWayAction#split
062     */
063    public static class SplitWayResult {
064        private final Command command;
065        private final List<? extends PrimitiveId> newSelection;
066        private final Way originalWay;
067        private final List<Way> newWays;
068
069        /**
070         * @param command The command to be performed to split the way (which is saved for later retrieval with {@link #getCommand})
071         * @param newSelection The new list of selected primitives ids (which is saved for later retrieval with {@link #getNewSelection})
072         * @param originalWay The original way being split (which is saved for later retrieval with {@link #getOriginalWay})
073         * @param newWays The resulting new ways (which is saved for later retrieval with {@link #getOriginalWay})
074         */
075        public SplitWayResult(Command command, List<? extends PrimitiveId> newSelection, Way originalWay, List<Way> newWays) {
076            this.command = command;
077            this.newSelection = newSelection;
078            this.originalWay = originalWay;
079            this.newWays = newWays;
080        }
081
082        /**
083         * Replies the command to be performed to split the way
084         * @return The command to be performed to split the way
085         */
086        public Command getCommand() {
087            return command;
088        }
089
090        /**
091         * Replies the new list of selected primitives ids
092         * @return The new list of selected primitives ids
093         */
094        public List<? extends PrimitiveId> getNewSelection() {
095            return newSelection;
096        }
097
098        /**
099         * Replies the original way being split
100         * @return The original way being split
101         */
102        public Way getOriginalWay() {
103            return originalWay;
104        }
105
106        /**
107         * Replies the resulting new ways
108         * @return The resulting new ways
109         */
110        public List<Way> getNewWays() {
111            return newWays;
112        }
113    }
114
115    /**
116     * Create a new SplitWayAction.
117     */
118    public SplitWayAction() {
119        super(tr("Split Way"), "splitway", tr("Split a way at the selected node."),
120                Shortcut.registerShortcut("tools:splitway", tr("Tool: {0}", tr("Split Way")), KeyEvent.VK_P, Shortcut.DIRECT), true);
121        putValue("help", ht("/Action/SplitWay"));
122    }
123
124    /**
125     * Called when the action is executed.
126     *
127     * This method performs an expensive check whether the selection clearly defines one
128     * of the split actions outlined above, and if yes, calls the splitWay method.
129     */
130    @Override
131    public void actionPerformed(ActionEvent e) {
132
133        if (SegmentToKeepSelectionDialog.DISPLAY_COUNT.get() > 0) {
134            new Notification(tr("Cannot split since another split operation is already in progress"))
135                    .setIcon(JOptionPane.WARNING_MESSAGE).show();
136            return;
137        }
138
139        Collection<OsmPrimitive> selection = getLayerManager().getEditDataSet().getSelected();
140
141        List<Node> selectedNodes = OsmPrimitive.getFilteredList(selection, Node.class);
142        List<Way> selectedWays = OsmPrimitive.getFilteredList(selection, Way.class);
143        List<Way> applicableWays = getApplicableWays(selectedWays, selectedNodes);
144
145        if (applicableWays == null) {
146            new Notification(
147                    tr("The current selection cannot be used for splitting - no node is selected."))
148                    .setIcon(JOptionPane.WARNING_MESSAGE)
149                    .show();
150            return;
151        } else if (applicableWays.isEmpty()) {
152            new Notification(
153                    tr("The selected nodes do not share the same way."))
154                    .setIcon(JOptionPane.WARNING_MESSAGE)
155                    .show();
156            return;
157        }
158
159        // If several ways have been found, remove ways that doesn't have selected
160        // node in the middle
161        if (applicableWays.size() > 1) {
162            for (Iterator<Way> it = applicableWays.iterator(); it.hasNext();) {
163                Way w = it.next();
164                for (Node n : selectedNodes) {
165                    if (!w.isInnerNode(n)) {
166                        it.remove();
167                        break;
168                    }
169                }
170            }
171        }
172
173        if (applicableWays.isEmpty()) {
174            new Notification(
175                    trn("The selected node is not in the middle of any way.",
176                        "The selected nodes are not in the middle of any way.",
177                        selectedNodes.size()))
178                    .setIcon(JOptionPane.WARNING_MESSAGE)
179                    .show();
180            return;
181        } else if (applicableWays.size() > 1) {
182            new Notification(
183                    trn("There is more than one way using the node you selected. Please select the way also.",
184                        "There is more than one way using the nodes you selected. Please select the way also.",
185                        selectedNodes.size()))
186                    .setIcon(JOptionPane.WARNING_MESSAGE)
187                    .show();
188            return;
189        }
190
191        // Finally, applicableWays contains only one perfect way
192        final Way selectedWay = applicableWays.get(0);
193        final List<List<Node>> wayChunks = buildSplitChunks(selectedWay, selectedNodes);
194        if (wayChunks != null) {
195            List<Relation> selectedRelations = OsmPrimitive.getFilteredList(selection, Relation.class);
196            final List<OsmPrimitive> sel = new ArrayList<>(selectedWays.size() + selectedRelations.size());
197            sel.addAll(selectedWays);
198            sel.addAll(selectedRelations);
199
200            final List<Way> newWays = createNewWaysFromChunks(selectedWay, wayChunks);
201            final Way wayToKeep = Strategy.keepLongestChunk().determineWayToKeep(newWays);
202
203            if (ExpertToggleAction.isExpert() && !selectedWay.isNew()) {
204                final ExtendedDialog dialog = new SegmentToKeepSelectionDialog(selectedWay, newWays, wayToKeep, sel);
205                dialog.toggleEnable("way.split.segment-selection-dialog");
206                if (!dialog.toggleCheckState()) {
207                    dialog.setModal(false);
208                    dialog.showDialog();
209                    return; // splitting is performed in SegmentToKeepSelectionDialog.buttonAction()
210                }
211            }
212            if (wayToKeep != null) {
213                final SplitWayResult result = doSplitWay(getLayerManager().getEditLayer(), selectedWay, wayToKeep, newWays, sel);
214                Main.main.undoRedo.add(result.getCommand());
215                getLayerManager().getEditDataSet().setSelected(result.getNewSelection());
216            }
217        }
218    }
219
220    /**
221     * A dialog to query which way segment should reuse the history of the way to split.
222     */
223    static class SegmentToKeepSelectionDialog extends ExtendedDialog {
224        static final AtomicInteger DISPLAY_COUNT = new AtomicInteger();
225        final transient Way selectedWay;
226        final transient List<Way> newWays;
227        final JList<Way> list;
228        final transient List<OsmPrimitive> selection;
229        final transient Way wayToKeep;
230
231        SegmentToKeepSelectionDialog(Way selectedWay, List<Way> newWays, Way wayToKeep, List<OsmPrimitive> selection) {
232            super(Main.parent, tr("Which way segment should reuse the history of {0}?", selectedWay.getId()),
233                    new String[]{tr("Ok"), tr("Cancel")}, true);
234
235            this.selectedWay = selectedWay;
236            this.newWays = newWays;
237            this.selection = selection;
238            this.wayToKeep = wayToKeep;
239            this.list = new JList<>(newWays.toArray(new Way[newWays.size()]));
240            configureList();
241
242            setButtonIcons(new String[]{"ok", "cancel"});
243            final JPanel pane = new JPanel(new GridBagLayout());
244            pane.add(new JLabel(getTitle()), GBC.eol().fill(GBC.HORIZONTAL));
245            pane.add(list, GBC.eop().fill(GBC.HORIZONTAL));
246            setContent(pane);
247            setDefaultCloseOperation(HIDE_ON_CLOSE);
248        }
249
250        private void configureList() {
251            list.setSelectionMode(ListSelectionModel.SINGLE_SELECTION);
252            list.addListSelectionListener(e -> {
253                final Way selected = list.getSelectedValue();
254                if (Main.isDisplayingMapView() && selected != null && selected.getNodesCount() > 1) {
255                    final Collection<WaySegment> segments = new ArrayList<>(selected.getNodesCount() - 1);
256                    final Iterator<Node> it = selected.getNodes().iterator();
257                    Node previousNode = it.next();
258                    while (it.hasNext()) {
259                        final Node node = it.next();
260                        segments.add(WaySegment.forNodePair(selectedWay, previousNode, node));
261                        previousNode = node;
262                    }
263                    setHighlightedWaySegments(segments);
264                }
265            });
266            list.setCellRenderer(new SegmentListCellRenderer());
267        }
268
269        protected void setHighlightedWaySegments(Collection<WaySegment> segments) {
270            selectedWay.getDataSet().setHighlightedWaySegments(segments);
271            Main.map.mapView.repaint();
272        }
273
274        @Override
275        public void setVisible(boolean visible) {
276            super.setVisible(visible);
277            if (visible) {
278                DISPLAY_COUNT.incrementAndGet();
279                list.setSelectedValue(wayToKeep, true);
280            } else {
281                setHighlightedWaySegments(Collections.<WaySegment>emptyList());
282                DISPLAY_COUNT.decrementAndGet();
283            }
284        }
285
286        @Override
287        protected void buttonAction(int buttonIndex, ActionEvent evt) {
288            super.buttonAction(buttonIndex, evt);
289            toggleSaveState(); // necessary since #showDialog() does not handle it due to the non-modal dialog
290            if (getValue() == 1) {
291                SplitWayResult result = doSplitWay(Main.getLayerManager().getEditLayer(),
292                        selectedWay, list.getSelectedValue(), newWays, selection);
293                Main.main.undoRedo.add(result.getCommand());
294                Main.getLayerManager().getEditDataSet().setSelected(result.getNewSelection());
295            }
296        }
297    }
298
299    static class SegmentListCellRenderer extends DefaultListCellRenderer {
300        @Override
301        public Component getListCellRendererComponent(JList<?> list, Object value, int index, boolean isSelected, boolean cellHasFocus) {
302            final Component c = super.getListCellRendererComponent(list, value, index, isSelected, cellHasFocus);
303            final String name = DefaultNameFormatter.getInstance().format((Way) value);
304            // get rid of id from DefaultNameFormatter.decorateNameWithId()
305            final String nameWithoutId = name
306                    .replace(tr(" [id: {0}]", ((Way) value).getId()), "")
307                    .replace(tr(" [id: {0}]", ((Way) value).getUniqueId()), "");
308            ((JLabel) c).setText(tr("Segment {0}: {1}", index + 1, nameWithoutId));
309            return c;
310        }
311    }
312
313    /**
314     * Determines which way chunk should reuse the old id and its history
315     *
316     * @since 8954
317     * @since 10599 (functional interface)
318     */
319    @FunctionalInterface
320    public interface Strategy {
321
322        /**
323         * Determines which way chunk should reuse the old id and its history.
324         *
325         * @param wayChunks the way chunks
326         * @return the way to keep
327         */
328        Way determineWayToKeep(Iterable<Way> wayChunks);
329
330        /**
331         * Returns a strategy which selects the way chunk with the highest node count to keep.
332         * @return strategy which selects the way chunk with the highest node count to keep
333         */
334        static Strategy keepLongestChunk() {
335            return wayChunks -> {
336                    Way wayToKeep = null;
337                    for (Way i : wayChunks) {
338                        if (wayToKeep == null || i.getNodesCount() > wayToKeep.getNodesCount()) {
339                            wayToKeep = i;
340                        }
341                    }
342                    return wayToKeep;
343                };
344        }
345
346        /**
347         * Returns a strategy which selects the first way chunk.
348         * @return strategy which selects the first way chunk
349         */
350        static Strategy keepFirstChunk() {
351            return wayChunks -> wayChunks.iterator().next();
352        }
353    }
354
355    /**
356     * Determine which ways to split.
357     * @param selectedWays List of user selected ways.
358     * @param selectedNodes List of user selected nodes.
359     * @return List of ways to split
360     */
361    static List<Way> getApplicableWays(List<Way> selectedWays, List<Node> selectedNodes) {
362        if (selectedNodes.isEmpty())
363            return null;
364
365        // Special case - one of the selected ways touches (not cross) way that we want to split
366        if (selectedNodes.size() == 1) {
367            Node n = selectedNodes.get(0);
368            List<Way> referredWays =
369                OsmPrimitive.getFilteredList(n.getReferrers(), Way.class);
370            Way inTheMiddle = null;
371            for (Way w: referredWays) {
372                // Need to look at all nodes see #11184 for a case where node n is
373                // firstNode, lastNode and also in the middle
374                if (selectedWays.contains(w) && w.isInnerNode(n)) {
375                    if (inTheMiddle == null) {
376                        inTheMiddle = w;
377                    } else {
378                        inTheMiddle = null;
379                        break;
380                    }
381                }
382            }
383            if (inTheMiddle != null)
384                return Collections.singletonList(inTheMiddle);
385        }
386
387        // List of ways shared by all nodes
388        return UnJoinNodeWayAction.getApplicableWays(selectedWays, selectedNodes);
389    }
390
391    /**
392     * Splits the nodes of {@code wayToSplit} into a list of node sequences
393     * which are separated at the nodes in {@code splitPoints}.
394     *
395     * This method displays warning messages if {@code wayToSplit} and/or
396     * {@code splitPoints} aren't consistent.
397     *
398     * Returns null, if building the split chunks fails.
399     *
400     * @param wayToSplit the way to split. Must not be null.
401     * @param splitPoints the nodes where the way is split. Must not be null.
402     * @return the list of chunks
403     */
404    public static List<List<Node>> buildSplitChunks(Way wayToSplit, List<Node> splitPoints) {
405        CheckParameterUtil.ensureParameterNotNull(wayToSplit, "wayToSplit");
406        CheckParameterUtil.ensureParameterNotNull(splitPoints, "splitPoints");
407
408        Set<Node> nodeSet = new HashSet<>(splitPoints);
409        List<List<Node>> wayChunks = new LinkedList<>();
410        List<Node> currentWayChunk = new ArrayList<>();
411        wayChunks.add(currentWayChunk);
412
413        Iterator<Node> it = wayToSplit.getNodes().iterator();
414        while (it.hasNext()) {
415            Node currentNode = it.next();
416            boolean atEndOfWay = currentWayChunk.isEmpty() || !it.hasNext();
417            currentWayChunk.add(currentNode);
418            if (nodeSet.contains(currentNode) && !atEndOfWay) {
419                currentWayChunk = new ArrayList<>();
420                currentWayChunk.add(currentNode);
421                wayChunks.add(currentWayChunk);
422            }
423        }
424
425        // Handle circular ways specially.
426        // If you split at a circular way at two nodes, you just want to split
427        // it at these points, not also at the former endpoint.
428        // So if the last node is the same first node, join the last and the
429        // first way chunk.
430        List<Node> lastWayChunk = wayChunks.get(wayChunks.size() - 1);
431        if (wayChunks.size() >= 2
432                && wayChunks.get(0).get(0) == lastWayChunk.get(lastWayChunk.size() - 1)
433                && !nodeSet.contains(wayChunks.get(0).get(0))) {
434            if (wayChunks.size() == 2) {
435                new Notification(
436                        tr("You must select two or more nodes to split a circular way."))
437                        .setIcon(JOptionPane.WARNING_MESSAGE)
438                        .show();
439                return null;
440            }
441            lastWayChunk.remove(lastWayChunk.size() - 1);
442            lastWayChunk.addAll(wayChunks.get(0));
443            wayChunks.remove(wayChunks.size() - 1);
444            wayChunks.set(0, lastWayChunk);
445        }
446
447        if (wayChunks.size() < 2) {
448            if (wayChunks.get(0).get(0) == wayChunks.get(0).get(wayChunks.get(0).size() - 1)) {
449                new Notification(
450                        tr("You must select two or more nodes to split a circular way."))
451                        .setIcon(JOptionPane.WARNING_MESSAGE)
452                        .show();
453            } else {
454                new Notification(
455                        tr("The way cannot be split at the selected nodes. (Hint: Select nodes in the middle of the way.)"))
456                        .setIcon(JOptionPane.WARNING_MESSAGE)
457                        .show();
458            }
459            return null;
460        }
461        return wayChunks;
462    }
463
464    /**
465     * Creates new way objects for the way chunks and transfers the keys from the original way.
466     * @param way the original way whose  keys are transferred
467     * @param wayChunks the way chunks
468     * @return the new way objects
469     */
470    protected static List<Way> createNewWaysFromChunks(Way way, Iterable<List<Node>> wayChunks) {
471        final List<Way> newWays = new ArrayList<>();
472        for (List<Node> wayChunk : wayChunks) {
473            Way wayToAdd = new Way();
474            wayToAdd.setKeys(way.getKeys());
475            wayToAdd.setNodes(wayChunk);
476            newWays.add(wayToAdd);
477        }
478        return newWays;
479    }
480
481    /**
482     * Splits the way {@code way} into chunks of {@code wayChunks} and replies
483     * the result of this process in an instance of {@link SplitWayResult}.
484     *
485     * Note that changes are not applied to the data yet. You have to
486     * submit the command in {@link SplitWayResult#getCommand()} first,
487     * i.e. {@code Main.main.undoredo.add(result.getCommand())}.
488     *
489     * @param layer the layer which the way belongs to. Must not be null.
490     * @param way the way to split. Must not be null.
491     * @param wayChunks the list of way chunks into the way is split. Must not be null.
492     * @param selection The list of currently selected primitives
493     * @return the result from the split operation
494     */
495    public static SplitWayResult splitWay(OsmDataLayer layer, Way way, List<List<Node>> wayChunks,
496            Collection<? extends OsmPrimitive> selection) {
497        return splitWay(layer, way, wayChunks, selection, Strategy.keepLongestChunk());
498    }
499
500    /**
501     * Splits the way {@code way} into chunks of {@code wayChunks} and replies
502     * the result of this process in an instance of {@link SplitWayResult}.
503     * The {@link org.openstreetmap.josm.actions.SplitWayAction.Strategy} is used to determine which
504     * way chunk should reuse the old id and its history.
505     *
506     * Note that changes are not applied to the data yet. You have to
507     * submit the command in {@link SplitWayResult#getCommand()} first,
508     * i.e. {@code Main.main.undoredo.add(result.getCommand())}.
509     *
510     * @param layer the layer which the way belongs to. Must not be null.
511     * @param way the way to split. Must not be null.
512     * @param wayChunks the list of way chunks into the way is split. Must not be null.
513     * @param selection The list of currently selected primitives
514     * @param splitStrategy The strategy used to determine which way chunk should reuse the old id and its history
515     * @return the result from the split operation
516     * @since 8954
517     */
518    public static SplitWayResult splitWay(OsmDataLayer layer, Way way, List<List<Node>> wayChunks,
519            Collection<? extends OsmPrimitive> selection, Strategy splitStrategy) {
520        // build a list of commands, and also a new selection list
521        final List<OsmPrimitive> newSelection = new ArrayList<>(selection.size() + wayChunks.size());
522        newSelection.addAll(selection);
523
524        // Create all potential new ways
525        final List<Way> newWays = createNewWaysFromChunks(way, wayChunks);
526
527        // Determine which part reuses the existing way
528        final Way wayToKeep = splitStrategy.determineWayToKeep(newWays);
529
530        return wayToKeep != null ? doSplitWay(layer, way, wayToKeep, newWays, newSelection) : null;
531    }
532
533    static SplitWayResult doSplitWay(OsmDataLayer layer, Way way, Way wayToKeep, List<Way> newWays,
534                                   List<OsmPrimitive> newSelection) {
535
536        Collection<Command> commandList = new ArrayList<>(newWays.size());
537        Collection<String> nowarnroles = Main.pref.getCollection("way.split.roles.nowarn",
538                Arrays.asList("outer", "inner", "forward", "backward", "north", "south", "east", "west"));
539
540        // Change the original way
541        final Way changedWay = new Way(way);
542        changedWay.setNodes(wayToKeep.getNodes());
543        commandList.add(layer != null ? new ChangeCommand(layer, way, changedWay) : new ChangeCommand(way.getDataSet(), way, changedWay));
544        if (!newSelection.contains(way)) {
545            newSelection.add(way);
546        }
547        final int indexOfWayToKeep = newWays.indexOf(wayToKeep);
548        newWays.remove(wayToKeep);
549
550        newSelection.addAll(newWays);
551        for (Way wayToAdd : newWays) {
552            commandList.add(layer != null ? new AddCommand(layer, wayToAdd) : new AddCommand(way.getDataSet(), wayToAdd));
553        }
554
555        boolean warnmerole = false;
556        boolean warnme = false;
557        // now copy all relations to new way also
558
559        for (Relation r : OsmPrimitive.getFilteredList(way.getReferrers(), Relation.class)) {
560            if (!r.isUsable()) {
561                continue;
562            }
563            Relation c = null;
564            String type = r.get("type");
565            if (type == null) {
566                type = "";
567            }
568
569            int ic = 0;
570            int ir = 0;
571            List<RelationMember> relationMembers = r.getMembers();
572            for (RelationMember rm: relationMembers) {
573                if (rm.isWay() && rm.getMember() == way) {
574                    boolean insert = true;
575                    if ("restriction".equals(type) || "destination_sign".equals(type)) {
576                        /* this code assumes the restriction is correct. No real error checking done */
577                        String role = rm.getRole();
578                        if ("from".equals(role) || "to".equals(role)) {
579                            OsmPrimitive via = findVia(r, type);
580                            List<Node> nodes = new ArrayList<>();
581                            if (via != null) {
582                                if (via instanceof Node) {
583                                    nodes.add((Node) via);
584                                } else if (via instanceof Way) {
585                                    nodes.add(((Way) via).lastNode());
586                                    nodes.add(((Way) via).firstNode());
587                                }
588                            }
589                            Way res = null;
590                            for (Node n : nodes) {
591                                if (changedWay.isFirstLastNode(n)) {
592                                    res = way;
593                                }
594                            }
595                            if (res == null) {
596                                for (Way wayToAdd : newWays) {
597                                    for (Node n : nodes) {
598                                        if (wayToAdd.isFirstLastNode(n)) {
599                                            res = wayToAdd;
600                                        }
601                                    }
602                                }
603                                if (res != null) {
604                                    if (c == null) {
605                                        c = new Relation(r);
606                                    }
607                                    c.addMember(new RelationMember(role, res));
608                                    c.removeMembersFor(way);
609                                    insert = false;
610                                }
611                            } else {
612                                insert = false;
613                            }
614                        } else if (!"via".equals(role)) {
615                            warnme = true;
616                        }
617                    } else if (!("route".equals(type)) && !("multipolygon".equals(type))) {
618                        warnme = true;
619                    }
620                    if (c == null) {
621                        c = new Relation(r);
622                    }
623
624                    if (insert) {
625                        if (rm.hasRole() && !nowarnroles.contains(rm.getRole())) {
626                            warnmerole = true;
627                        }
628
629                        Boolean backwards = null;
630                        int k = 1;
631                        while (ir - k >= 0 || ir + k < relationMembers.size()) {
632                            if ((ir - k >= 0) && relationMembers.get(ir - k).isWay()) {
633                                Way w = relationMembers.get(ir - k).getWay();
634                                if ((w.lastNode() == way.firstNode()) || w.firstNode() == way.firstNode()) {
635                                    backwards = Boolean.FALSE;
636                                } else if ((w.firstNode() == way.lastNode()) || w.lastNode() == way.lastNode()) {
637                                    backwards = Boolean.TRUE;
638                                }
639                                break;
640                            }
641                            if ((ir + k < relationMembers.size()) && relationMembers.get(ir + k).isWay()) {
642                                Way w = relationMembers.get(ir + k).getWay();
643                                if ((w.lastNode() == way.firstNode()) || w.firstNode() == way.firstNode()) {
644                                    backwards = Boolean.TRUE;
645                                } else if ((w.firstNode() == way.lastNode()) || w.lastNode() == way.lastNode()) {
646                                    backwards = Boolean.FALSE;
647                                }
648                                break;
649                            }
650                            k++;
651                        }
652
653                        int j = ic;
654                        final List<Way> waysToAddBefore = newWays.subList(0, indexOfWayToKeep);
655                        for (Way wayToAdd : waysToAddBefore) {
656                            RelationMember em = new RelationMember(rm.getRole(), wayToAdd);
657                            j++;
658                            if (Boolean.TRUE.equals(backwards)) {
659                                c.addMember(ic + 1, em);
660                            } else {
661                                c.addMember(j - 1, em);
662                            }
663                        }
664                        final List<Way> waysToAddAfter = newWays.subList(indexOfWayToKeep, newWays.size());
665                        for (Way wayToAdd : waysToAddAfter) {
666                            RelationMember em = new RelationMember(rm.getRole(), wayToAdd);
667                            j++;
668                            if (Boolean.TRUE.equals(backwards)) {
669                                c.addMember(ic, em);
670                            } else {
671                                c.addMember(j, em);
672                            }
673                        }
674                        ic = j;
675                    }
676                }
677                ic++;
678                ir++;
679            }
680
681            if (c != null) {
682                commandList.add(layer != null ? new ChangeCommand(layer, r, c) : new ChangeCommand(r.getDataSet(), r, c));
683            }
684        }
685        if (warnmerole) {
686            new Notification(
687                    tr("A role based relation membership was copied to all new ways.<br>You should verify this and correct it when necessary."))
688                    .setIcon(JOptionPane.WARNING_MESSAGE)
689                    .show();
690        } else if (warnme) {
691            new Notification(
692                    tr("A relation membership was copied to all new ways.<br>You should verify this and correct it when necessary."))
693                    .setIcon(JOptionPane.WARNING_MESSAGE)
694                    .show();
695        }
696
697        return new SplitWayResult(
698                new SequenceCommand(
699                        /* for correct i18n of plural forms - see #9110 */
700                        trn("Split way {0} into {1} part", "Split way {0} into {1} parts", newWays.size() + 1,
701                                way.getDisplayName(DefaultNameFormatter.getInstance()), newWays.size() + 1),
702                        commandList
703                        ),
704                        newSelection,
705                        way,
706                        newWays
707                );
708    }
709
710    static OsmPrimitive findVia(Relation r, String type) {
711        for (RelationMember rmv : r.getMembers()) {
712            if (("restriction".equals(type) && "via".equals(rmv.getRole()))
713             || ("destination_sign".equals(type) && rmv.hasRole("sign", "intersection"))) {
714                return rmv.getMember();
715            }
716        }
717        return null;
718    }
719
720    /**
721     * Splits the way {@code way} at the nodes in {@code atNodes} and replies
722     * the result of this process in an instance of {@link SplitWayResult}.
723     *
724     * Note that changes are not applied to the data yet. You have to
725     * submit the command in {@link SplitWayResult#getCommand()} first,
726     * i.e. {@code Main.main.undoredo.add(result.getCommand())}.
727     *
728     * Replies null if the way couldn't be split at the given nodes.
729     *
730     * @param layer the layer which the way belongs to. Must not be null.
731     * @param way the way to split. Must not be null.
732     * @param atNodes the list of nodes where the way is split. Must not be null.
733     * @param selection The list of currently selected primitives
734     * @return the result from the split operation
735     */
736    public static SplitWayResult split(OsmDataLayer layer, Way way, List<Node> atNodes, Collection<? extends OsmPrimitive> selection) {
737        List<List<Node>> chunks = buildSplitChunks(way, atNodes);
738        return chunks != null ? splitWay(layer, way, chunks, selection) : null;
739    }
740
741    @Override
742    protected void updateEnabledState() {
743        updateEnabledStateOnCurrentSelection();
744    }
745
746    @Override
747    protected void updateEnabledState(Collection<? extends OsmPrimitive> selection) {
748        if (selection == null) {
749            setEnabled(false);
750            return;
751        }
752        for (OsmPrimitive primitive: selection) {
753            if (primitive instanceof Node) {
754                setEnabled(true); // Selection still can be wrong, but let SplitWayAction process and tell user what's wrong
755                return;
756            }
757        }
758        setEnabled(false);
759    }
760}