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