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 DefaultListCellRenderer() {
267                @Override
268                public Component getListCellRendererComponent(JList<?> list, Object value, int index, boolean isSelected, boolean cellHasFocus) {
269                    final Component c = super.getListCellRendererComponent(list, value, index, isSelected, cellHasFocus);
270                    final String name = DefaultNameFormatter.getInstance().format((Way) value);
271                    // get rid of id from DefaultNameFormatter.decorateNameWithId()
272                    final String nameWithoutId = name
273                            .replace(tr(" [id: {0}]", ((Way) value).getId()), "")
274                            .replace(tr(" [id: {0}]", ((Way) value).getUniqueId()), "");
275                    ((JLabel) c).setText(tr("Segment {0}: {1}", index + 1, nameWithoutId));
276                    return c;
277                }
278            });
279        }
280
281        protected void setHighlightedWaySegments(Collection<WaySegment> segments) {
282            selectedWay.getDataSet().setHighlightedWaySegments(segments);
283            Main.map.mapView.repaint();
284        }
285
286        @Override
287        public void setVisible(boolean visible) {
288            super.setVisible(visible);
289            if (visible) {
290                DISPLAY_COUNT.incrementAndGet();
291                list.setSelectedValue(wayToKeep, true);
292            } else {
293                setHighlightedWaySegments(Collections.<WaySegment>emptyList());
294                DISPLAY_COUNT.decrementAndGet();
295            }
296        }
297
298        @Override
299        protected void buttonAction(int buttonIndex, ActionEvent evt) {
300            super.buttonAction(buttonIndex, evt);
301            toggleSaveState(); // necessary since #showDialog() does not handle it due to the non-modal dialog
302            if (getValue() == 1) {
303                SplitWayResult result = doSplitWay(Main.getLayerManager().getEditLayer(),
304                        selectedWay, list.getSelectedValue(), newWays, selection);
305                Main.main.undoRedo.add(result.getCommand());
306                Main.getLayerManager().getEditDataSet().setSelected(result.getNewSelection());
307            }
308        }
309    }
310
311    /**
312     * Determines which way chunk should reuse the old id and its history
313     *
314     * @since 8954
315     * @since 10599 (functional interface)
316     */
317    @FunctionalInterface
318    public interface Strategy {
319
320        /**
321         * Determines which way chunk should reuse the old id and its history.
322         *
323         * @param wayChunks the way chunks
324         * @return the way to keep
325         */
326        Way determineWayToKeep(Iterable<Way> wayChunks);
327
328        /**
329         * Returns a strategy which selects the way chunk with the highest node count to keep.
330         * @return strategy which selects the way chunk with the highest node count to keep
331         */
332        static Strategy keepLongestChunk() {
333            return wayChunks -> {
334                    Way wayToKeep = null;
335                    for (Way i : wayChunks) {
336                        if (wayToKeep == null || i.getNodesCount() > wayToKeep.getNodesCount()) {
337                            wayToKeep = i;
338                        }
339                    }
340                    return wayToKeep;
341                };
342        }
343
344        /**
345         * Returns a strategy which selects the first way chunk.
346         * @return strategy which selects the first way chunk
347         */
348        static Strategy keepFirstChunk() {
349            return wayChunks -> wayChunks.iterator().next();
350        }
351    }
352
353    /**
354     * Determine which ways to split.
355     * @param selectedWays List of user selected ways.
356     * @param selectedNodes List of user selected nodes.
357     * @return List of ways to split
358     */
359    static List<Way> getApplicableWays(List<Way> selectedWays, List<Node> selectedNodes) {
360        if (selectedNodes.isEmpty())
361            return null;
362
363        // Special case - one of the selected ways touches (not cross) way that we want to split
364        if (selectedNodes.size() == 1) {
365            Node n = selectedNodes.get(0);
366            List<Way> referredWays =
367                OsmPrimitive.getFilteredList(n.getReferrers(), Way.class);
368            Way inTheMiddle = null;
369            for (Way w: referredWays) {
370                // Need to look at all nodes see #11184 for a case where node n is
371                // firstNode, lastNode and also in the middle
372                if (selectedWays.contains(w) && w.isInnerNode(n)) {
373                    if (inTheMiddle == null) {
374                        inTheMiddle = w;
375                    } else {
376                        inTheMiddle = null;
377                        break;
378                    }
379                }
380            }
381            if (inTheMiddle != null)
382                return Collections.singletonList(inTheMiddle);
383        }
384
385        // List of ways shared by all nodes
386        return UnJoinNodeWayAction.getApplicableWays(selectedWays, selectedNodes);
387    }
388
389    /**
390     * Splits the nodes of {@code wayToSplit} into a list of node sequences
391     * which are separated at the nodes in {@code splitPoints}.
392     *
393     * This method displays warning messages if {@code wayToSplit} and/or
394     * {@code splitPoints} aren't consistent.
395     *
396     * Returns null, if building the split chunks fails.
397     *
398     * @param wayToSplit the way to split. Must not be null.
399     * @param splitPoints the nodes where the way is split. Must not be null.
400     * @return the list of chunks
401     */
402    public static List<List<Node>> buildSplitChunks(Way wayToSplit, List<Node> splitPoints) {
403        CheckParameterUtil.ensureParameterNotNull(wayToSplit, "wayToSplit");
404        CheckParameterUtil.ensureParameterNotNull(splitPoints, "splitPoints");
405
406        Set<Node> nodeSet = new HashSet<>(splitPoints);
407        List<List<Node>> wayChunks = new LinkedList<>();
408        List<Node> currentWayChunk = new ArrayList<>();
409        wayChunks.add(currentWayChunk);
410
411        Iterator<Node> it = wayToSplit.getNodes().iterator();
412        while (it.hasNext()) {
413            Node currentNode = it.next();
414            boolean atEndOfWay = currentWayChunk.isEmpty() || !it.hasNext();
415            currentWayChunk.add(currentNode);
416            if (nodeSet.contains(currentNode) && !atEndOfWay) {
417                currentWayChunk = new ArrayList<>();
418                currentWayChunk.add(currentNode);
419                wayChunks.add(currentWayChunk);
420            }
421        }
422
423        // Handle circular ways specially.
424        // If you split at a circular way at two nodes, you just want to split
425        // it at these points, not also at the former endpoint.
426        // So if the last node is the same first node, join the last and the
427        // first way chunk.
428        List<Node> lastWayChunk = wayChunks.get(wayChunks.size() - 1);
429        if (wayChunks.size() >= 2
430                && wayChunks.get(0).get(0) == lastWayChunk.get(lastWayChunk.size() - 1)
431                && !nodeSet.contains(wayChunks.get(0).get(0))) {
432            if (wayChunks.size() == 2) {
433                new Notification(
434                        tr("You must select two or more nodes to split a circular way."))
435                        .setIcon(JOptionPane.WARNING_MESSAGE)
436                        .show();
437                return null;
438            }
439            lastWayChunk.remove(lastWayChunk.size() - 1);
440            lastWayChunk.addAll(wayChunks.get(0));
441            wayChunks.remove(wayChunks.size() - 1);
442            wayChunks.set(0, lastWayChunk);
443        }
444
445        if (wayChunks.size() < 2) {
446            if (wayChunks.get(0).get(0) == wayChunks.get(0).get(wayChunks.get(0).size() - 1)) {
447                new Notification(
448                        tr("You must select two or more nodes to split a circular way."))
449                        .setIcon(JOptionPane.WARNING_MESSAGE)
450                        .show();
451            } else {
452                new Notification(
453                        tr("The way cannot be split at the selected nodes. (Hint: Select nodes in the middle of the way.)"))
454                        .setIcon(JOptionPane.WARNING_MESSAGE)
455                        .show();
456            }
457            return null;
458        }
459        return wayChunks;
460    }
461
462    /**
463     * Creates new way objects for the way chunks and transfers the keys from the original way.
464     * @param way the original way whose  keys are transferred
465     * @param wayChunks the way chunks
466     * @return the new way objects
467     */
468    protected static List<Way> createNewWaysFromChunks(Way way, Iterable<List<Node>> wayChunks) {
469        final List<Way> newWays = new ArrayList<>();
470        for (List<Node> wayChunk : wayChunks) {
471            Way wayToAdd = new Way();
472            wayToAdd.setKeys(way.getKeys());
473            wayToAdd.setNodes(wayChunk);
474            newWays.add(wayToAdd);
475        }
476        return newWays;
477    }
478
479    /**
480     * Splits the way {@code way} into chunks of {@code wayChunks} and replies
481     * the result of this process in an instance of {@link SplitWayResult}.
482     *
483     * Note that changes are not applied to the data yet. You have to
484     * submit the command in {@link SplitWayResult#getCommand()} first,
485     * i.e. {@code Main.main.undoredo.add(result.getCommand())}.
486     *
487     * @param layer the layer which the way belongs to. Must not be null.
488     * @param way the way to split. Must not be null.
489     * @param wayChunks the list of way chunks into the way is split. Must not be null.
490     * @param selection The list of currently selected primitives
491     * @return the result from the split operation
492     */
493    public static SplitWayResult splitWay(OsmDataLayer layer, Way way, List<List<Node>> wayChunks,
494            Collection<? extends OsmPrimitive> selection) {
495        return splitWay(layer, way, wayChunks, selection, Strategy.keepLongestChunk());
496    }
497
498    /**
499     * Splits the way {@code way} into chunks of {@code wayChunks} and replies
500     * the result of this process in an instance of {@link SplitWayResult}.
501     * The {@link org.openstreetmap.josm.actions.SplitWayAction.Strategy} is used to determine which
502     * way chunk should reuse the old id and its history.
503     *
504     * Note that changes are not applied to the data yet. You have to
505     * submit the command in {@link SplitWayResult#getCommand()} first,
506     * i.e. {@code Main.main.undoredo.add(result.getCommand())}.
507     *
508     * @param layer the layer which the way belongs to. Must not be null.
509     * @param way the way to split. Must not be null.
510     * @param wayChunks the list of way chunks into the way is split. Must not be null.
511     * @param selection The list of currently selected primitives
512     * @param splitStrategy The strategy used to determine which way chunk should reuse the old id and its history
513     * @return the result from the split operation
514     * @since 8954
515     */
516    public static SplitWayResult splitWay(OsmDataLayer layer, Way way, List<List<Node>> wayChunks,
517            Collection<? extends OsmPrimitive> selection, Strategy splitStrategy) {
518        // build a list of commands, and also a new selection list
519        final List<OsmPrimitive> newSelection = new ArrayList<>(selection.size() + wayChunks.size());
520        newSelection.addAll(selection);
521
522        // Create all potential new ways
523        final List<Way> newWays = createNewWaysFromChunks(way, wayChunks);
524
525        // Determine which part reuses the existing way
526        final Way wayToKeep = splitStrategy.determineWayToKeep(newWays);
527
528        return wayToKeep != null ? doSplitWay(layer, way, wayToKeep, newWays, newSelection) : null;
529    }
530
531    static SplitWayResult doSplitWay(OsmDataLayer layer, Way way, Way wayToKeep, List<Way> newWays,
532                                   List<OsmPrimitive> newSelection) {
533
534        Collection<Command> commandList = new ArrayList<>(newWays.size());
535        Collection<String> nowarnroles = Main.pref.getCollection("way.split.roles.nowarn",
536                Arrays.asList("outer", "inner", "forward", "backward", "north", "south", "east", "west"));
537
538        // Change the original way
539        final Way changedWay = new Way(way);
540        changedWay.setNodes(wayToKeep.getNodes());
541        commandList.add(new ChangeCommand(way, changedWay));
542        if (!newSelection.contains(way)) {
543            newSelection.add(way);
544        }
545        final int indexOfWayToKeep = newWays.indexOf(wayToKeep);
546        newWays.remove(wayToKeep);
547
548        newSelection.addAll(newWays);
549        for (Way wayToAdd : newWays) {
550            commandList.add(new AddCommand(layer, wayToAdd));
551        }
552
553        boolean warnmerole = false;
554        boolean warnme = false;
555        // now copy all relations to new way also
556
557        for (Relation r : OsmPrimitive.getFilteredList(way.getReferrers(), Relation.class)) {
558            if (!r.isUsable()) {
559                continue;
560            }
561            Relation c = null;
562            String type = r.get("type");
563            if (type == null) {
564                type = "";
565            }
566
567            int ic = 0;
568            int ir = 0;
569            List<RelationMember> relationMembers = r.getMembers();
570            for (RelationMember rm: relationMembers) {
571                if (rm.isWay() && rm.getMember() == way) {
572                    boolean insert = true;
573                    if ("restriction".equals(type) || "destination_sign".equals(type)) {
574                        /* this code assumes the restriction is correct. No real error checking done */
575                        String role = rm.getRole();
576                        if ("from".equals(role) || "to".equals(role)) {
577                            OsmPrimitive via = findVia(r, type);
578                            List<Node> nodes = new ArrayList<>();
579                            if (via != null) {
580                                if (via instanceof Node) {
581                                    nodes.add((Node) via);
582                                } else if (via instanceof Way) {
583                                    nodes.add(((Way) via).lastNode());
584                                    nodes.add(((Way) via).firstNode());
585                                }
586                            }
587                            Way res = null;
588                            for (Node n : nodes) {
589                                if (changedWay.isFirstLastNode(n)) {
590                                    res = way;
591                                }
592                            }
593                            if (res == null) {
594                                for (Way wayToAdd : newWays) {
595                                    for (Node n : nodes) {
596                                        if (wayToAdd.isFirstLastNode(n)) {
597                                            res = wayToAdd;
598                                        }
599                                    }
600                                }
601                                if (res != null) {
602                                    if (c == null) {
603                                        c = new Relation(r);
604                                    }
605                                    c.addMember(new RelationMember(role, res));
606                                    c.removeMembersFor(way);
607                                    insert = false;
608                                }
609                            } else {
610                                insert = false;
611                            }
612                        } else if (!"via".equals(role)) {
613                            warnme = true;
614                        }
615                    } else if (!("route".equals(type)) && !("multipolygon".equals(type))) {
616                        warnme = true;
617                    }
618                    if (c == null) {
619                        c = new Relation(r);
620                    }
621
622                    if (insert) {
623                        if (rm.hasRole() && !nowarnroles.contains(rm.getRole())) {
624                            warnmerole = true;
625                        }
626
627                        Boolean backwards = null;
628                        int k = 1;
629                        while (ir - k >= 0 || ir + k < relationMembers.size()) {
630                            if ((ir - k >= 0) && relationMembers.get(ir - k).isWay()) {
631                                Way w = relationMembers.get(ir - k).getWay();
632                                if ((w.lastNode() == way.firstNode()) || w.firstNode() == way.firstNode()) {
633                                    backwards = Boolean.FALSE;
634                                } else if ((w.firstNode() == way.lastNode()) || w.lastNode() == way.lastNode()) {
635                                    backwards = Boolean.TRUE;
636                                }
637                                break;
638                            }
639                            if ((ir + k < relationMembers.size()) && relationMembers.get(ir + k).isWay()) {
640                                Way w = relationMembers.get(ir + k).getWay();
641                                if ((w.lastNode() == way.firstNode()) || w.firstNode() == way.firstNode()) {
642                                    backwards = Boolean.TRUE;
643                                } else if ((w.firstNode() == way.lastNode()) || w.lastNode() == way.lastNode()) {
644                                    backwards = Boolean.FALSE;
645                                }
646                                break;
647                            }
648                            k++;
649                        }
650
651                        int j = ic;
652                        final List<Way> waysToAddBefore = newWays.subList(0, indexOfWayToKeep);
653                        for (Way wayToAdd : waysToAddBefore) {
654                            RelationMember em = new RelationMember(rm.getRole(), wayToAdd);
655                            j++;
656                            if (Boolean.TRUE.equals(backwards)) {
657                                c.addMember(ic + 1, em);
658                            } else {
659                                c.addMember(j - 1, em);
660                            }
661                        }
662                        final List<Way> waysToAddAfter = newWays.subList(indexOfWayToKeep, newWays.size());
663                        for (Way wayToAdd : waysToAddAfter) {
664                            RelationMember em = new RelationMember(rm.getRole(), wayToAdd);
665                            j++;
666                            if (Boolean.TRUE.equals(backwards)) {
667                                c.addMember(ic, em);
668                            } else {
669                                c.addMember(j, em);
670                            }
671                        }
672                        ic = j;
673                    }
674                }
675                ic++;
676                ir++;
677            }
678
679            if (c != null) {
680                commandList.add(new ChangeCommand(layer, r, c));
681            }
682        }
683        if (warnmerole) {
684            new Notification(
685                    tr("A role based relation membership was copied to all new ways.<br>You should verify this and correct it when necessary."))
686                    .setIcon(JOptionPane.WARNING_MESSAGE)
687                    .show();
688        } else if (warnme) {
689            new Notification(
690                    tr("A relation membership was copied to all new ways.<br>You should verify this and correct it when necessary."))
691                    .setIcon(JOptionPane.WARNING_MESSAGE)
692                    .show();
693        }
694
695        return new SplitWayResult(
696                new SequenceCommand(
697                        /* for correct i18n of plural forms - see #9110 */
698                        trn("Split way {0} into {1} part", "Split way {0} into {1} parts", newWays.size() + 1,
699                                way.getDisplayName(DefaultNameFormatter.getInstance()), newWays.size() + 1),
700                        commandList
701                        ),
702                        newSelection,
703                        way,
704                        newWays
705                );
706    }
707
708    static OsmPrimitive findVia(Relation r, String type) {
709        for (RelationMember rmv : r.getMembers()) {
710            if (("restriction".equals(type) && "via".equals(rmv.getRole()))
711             || ("destination_sign".equals(type) && rmv.hasRole("sign", "intersection"))) {
712                return rmv.getMember();
713            }
714        }
715        return null;
716    }
717
718    /**
719     * Splits the way {@code way} at the nodes in {@code atNodes} and replies
720     * the result of this process in an instance of {@link SplitWayResult}.
721     *
722     * Note that changes are not applied to the data yet. You have to
723     * submit the command in {@link SplitWayResult#getCommand()} first,
724     * i.e. {@code Main.main.undoredo.add(result.getCommand())}.
725     *
726     * Replies null if the way couldn't be split at the given nodes.
727     *
728     * @param layer the layer which the way belongs to. Must not be null.
729     * @param way the way to split. Must not be null.
730     * @param atNodes the list of nodes where the way is split. Must not be null.
731     * @param selection The list of currently selected primitives
732     * @return the result from the split operation
733     */
734    public static SplitWayResult split(OsmDataLayer layer, Way way, List<Node> atNodes, Collection<? extends OsmPrimitive> selection) {
735        List<List<Node>> chunks = buildSplitChunks(way, atNodes);
736        return chunks != null ? splitWay(layer, way, chunks, selection) : null;
737    }
738
739    @Override
740    protected void updateEnabledState() {
741        updateEnabledStateOnCurrentSelection();
742    }
743
744    @Override
745    protected void updateEnabledState(Collection<? extends OsmPrimitive> selection) {
746        if (selection == null) {
747            setEnabled(false);
748            return;
749        }
750        for (OsmPrimitive primitive: selection) {
751            if (primitive instanceof Node) {
752                setEnabled(true); // Selection still can be wrong, but let SplitWayAction process and tell user what's wrong
753                return;
754            }
755        }
756        setEnabled(false);
757    }
758}