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