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