source: josm/trunk/src/org/openstreetmap/josm/actions/SplitWayAction.java @ 12718

Last change on this file since 12718 was 12718, checked in by Don-vip, 3 months ago

see #13036 - see #15229 - see #15182 - make Commands depends only on a DataSet, not a Layer. This removes a lot of GUI dependencies

  • Property svn:eol-style set to native
File size: 36.0 KB
Line 
1// License: GPL. For details, see LICENSE file.
2package org.openstreetmap.josm.actions;
3
4import static org.openstreetmap.josm.gui.help.HelpUtil.ht;
5import static org.openstreetmap.josm.tools.I18n.tr;
6import static org.openstreetmap.josm.tools.I18n.trn;
7
8import java.awt.Component;
9import java.awt.GridBagLayout;
10import java.awt.event.ActionEvent;
11import java.awt.event.KeyEvent;
12import java.util.ArrayList;
13import java.util.Arrays;
14import java.util.Collection;
15import java.util.Collections;
16import java.util.HashSet;
17import java.util.Iterator;
18import java.util.LinkedList;
19import java.util.List;
20import java.util.Optional;
21import java.util.Set;
22import java.util.concurrent.atomic.AtomicInteger;
23
24import javax.swing.DefaultListCellRenderer;
25import javax.swing.JLabel;
26import javax.swing.JList;
27import javax.swing.JOptionPane;
28import javax.swing.JPanel;
29import javax.swing.ListSelectionModel;
30
31import org.openstreetmap.josm.Main;
32import org.openstreetmap.josm.command.AddCommand;
33import org.openstreetmap.josm.command.ChangeCommand;
34import org.openstreetmap.josm.command.Command;
35import org.openstreetmap.josm.command.SequenceCommand;
36import org.openstreetmap.josm.data.osm.DefaultNameFormatter;
37import org.openstreetmap.josm.data.osm.Node;
38import org.openstreetmap.josm.data.osm.OsmPrimitive;
39import org.openstreetmap.josm.data.osm.PrimitiveId;
40import org.openstreetmap.josm.data.osm.Relation;
41import org.openstreetmap.josm.data.osm.RelationMember;
42import org.openstreetmap.josm.data.osm.Way;
43import org.openstreetmap.josm.data.osm.WaySegment;
44import org.openstreetmap.josm.gui.ExtendedDialog;
45import org.openstreetmap.josm.gui.MainApplication;
46import org.openstreetmap.josm.gui.MapFrame;
47import org.openstreetmap.josm.gui.Notification;
48import org.openstreetmap.josm.gui.layer.OsmDataLayer;
49import org.openstreetmap.josm.tools.CheckParameterUtil;
50import org.openstreetmap.josm.tools.GBC;
51import org.openstreetmap.josm.tools.Shortcut;
52
53/**
54 * Splits a way into multiple ways (all identical except for their node list).
55 *
56 * Ways are just split at the selected nodes.  The nodes remain in their
57 * original order.  Selected nodes at the end of a way are ignored.
58 */
59public class SplitWayAction extends JosmAction {
60
61    /**
62     * Represents the result of a {@link SplitWayAction}
63     * @see SplitWayAction#splitWay
64     * @see SplitWayAction#split
65     */
66    public static class SplitWayResult {
67        private final Command command;
68        private final List<? extends PrimitiveId> newSelection;
69        private final Way originalWay;
70        private final List<Way> newWays;
71
72        /**
73         * @param command The command to be performed to split the way (which is saved for later retrieval with {@link #getCommand})
74         * @param newSelection The new list of selected primitives ids (which is saved for later retrieval with {@link #getNewSelection})
75         * @param originalWay The original way being split (which is saved for later retrieval with {@link #getOriginalWay})
76         * @param newWays The resulting new ways (which is saved for later retrieval with {@link #getOriginalWay})
77         */
78        public SplitWayResult(Command command, List<? extends PrimitiveId> newSelection, Way originalWay, List<Way> newWays) {
79            this.command = command;
80            this.newSelection = newSelection;
81            this.originalWay = originalWay;
82            this.newWays = newWays;
83        }
84
85        /**
86         * Replies the command to be performed to split the way
87         * @return The command to be performed to split the way
88         */
89        public Command getCommand() {
90            return command;
91        }
92
93        /**
94         * Replies the new list of selected primitives ids
95         * @return The new list of selected primitives ids
96         */
97        public List<? extends PrimitiveId> getNewSelection() {
98            return newSelection;
99        }
100
101        /**
102         * Replies the original way being split
103         * @return The original way being split
104         */
105        public Way getOriginalWay() {
106            return originalWay;
107        }
108
109        /**
110         * Replies the resulting new ways
111         * @return The resulting new ways
112         */
113        public List<Way> getNewWays() {
114            return newWays;
115        }
116    }
117
118    /**
119     * Create a new SplitWayAction.
120     */
121    public SplitWayAction() {
122        super(tr("Split Way"), "splitway", tr("Split a way at the selected node."),
123                Shortcut.registerShortcut("tools:splitway", tr("Tool: {0}", tr("Split Way")), KeyEvent.VK_P, Shortcut.DIRECT), true);
124        putValue("help", ht("/Action/SplitWay"));
125    }
126
127    /**
128     * Called when the action is executed.
129     *
130     * This method performs an expensive check whether the selection clearly defines one
131     * of the split actions outlined above, and if yes, calls the splitWay method.
132     */
133    @Override
134    public void actionPerformed(ActionEvent e) {
135
136        if (SegmentToKeepSelectionDialog.DISPLAY_COUNT.get() > 0) {
137            new Notification(tr("Cannot split since another split operation is already in progress"))
138                    .setIcon(JOptionPane.WARNING_MESSAGE).show();
139            return;
140        }
141
142        Collection<OsmPrimitive> selection = getLayerManager().getEditDataSet().getSelected();
143
144        List<Node> selectedNodes = OsmPrimitive.getFilteredList(selection, Node.class);
145        List<Way> selectedWays = OsmPrimitive.getFilteredList(selection, Way.class);
146        List<Way> applicableWays = getApplicableWays(selectedWays, selectedNodes);
147
148        if (applicableWays == null) {
149            new Notification(
150                    tr("The current selection cannot be used for splitting - no node is selected."))
151                    .setIcon(JOptionPane.WARNING_MESSAGE)
152                    .show();
153            return;
154        } else if (applicableWays.isEmpty()) {
155            new Notification(
156                    tr("The selected nodes do not share the same way."))
157                    .setIcon(JOptionPane.WARNING_MESSAGE)
158                    .show();
159            return;
160        }
161
162        // If several ways have been found, remove ways that doesn't have selected
163        // node in the middle
164        if (applicableWays.size() > 1) {
165            for (Iterator<Way> it = applicableWays.iterator(); it.hasNext();) {
166                Way w = it.next();
167                for (Node n : selectedNodes) {
168                    if (!w.isInnerNode(n)) {
169                        it.remove();
170                        break;
171                    }
172                }
173            }
174        }
175
176        if (applicableWays.isEmpty()) {
177            new Notification(
178                    trn("The selected node is not in the middle of any way.",
179                        "The selected nodes are not in the middle of any way.",
180                        selectedNodes.size()))
181                    .setIcon(JOptionPane.WARNING_MESSAGE)
182                    .show();
183            return;
184        } else if (applicableWays.size() > 1) {
185            new Notification(
186                    trn("There is more than one way using the node you selected. Please select the way also.",
187                        "There is more than one way using the nodes you selected. Please select the way also.",
188                        selectedNodes.size()))
189                    .setIcon(JOptionPane.WARNING_MESSAGE)
190                    .show();
191            return;
192        }
193
194        // Finally, applicableWays contains only one perfect way
195        final Way selectedWay = applicableWays.get(0);
196        final List<List<Node>> wayChunks = buildSplitChunks(selectedWay, selectedNodes);
197        if (wayChunks != null) {
198            List<Relation> selectedRelations = 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(selectedWay, wayToKeep, newWays, sel);
217                MainApplication.undoRedo.add(result.getCommand());
218                if (!result.getNewSelection().isEmpty()) {
219                    getLayerManager().getEditDataSet().setSelected(result.getNewSelection());
220                }
221            }
222        }
223    }
224
225    /**
226     * A dialog to query which way segment should reuse the history of the way to split.
227     */
228    static class SegmentToKeepSelectionDialog extends ExtendedDialog {
229        static final AtomicInteger DISPLAY_COUNT = new AtomicInteger();
230        final transient Way selectedWay;
231        final transient List<Way> newWays;
232        final JList<Way> list;
233        final transient List<OsmPrimitive> selection;
234        final transient Way wayToKeep;
235
236        SegmentToKeepSelectionDialog(Way selectedWay, List<Way> newWays, Way wayToKeep, List<OsmPrimitive> selection) {
237            super(Main.parent, tr("Which way segment should reuse the history of {0}?", selectedWay.getId()),
238                    new String[]{tr("Ok"), tr("Cancel")}, true);
239
240            this.selectedWay = selectedWay;
241            this.newWays = newWays;
242            this.selection = selection;
243            this.wayToKeep = wayToKeep;
244            this.list = new JList<>(newWays.toArray(new Way[newWays.size()]));
245            configureList();
246
247            setButtonIcons("ok", "cancel");
248            final JPanel pane = new JPanel(new GridBagLayout());
249            pane.add(new JLabel(getTitle()), GBC.eol().fill(GBC.HORIZONTAL));
250            pane.add(list, GBC.eop().fill(GBC.HORIZONTAL));
251            setContent(pane);
252            setDefaultCloseOperation(HIDE_ON_CLOSE);
253        }
254
255        private void configureList() {
256            list.setSelectionMode(ListSelectionModel.SINGLE_SELECTION);
257            list.addListSelectionListener(e -> {
258                final Way selected = list.getSelectedValue();
259                if (selected != null && MainApplication.isDisplayingMapView() && 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            list.setCellRenderer(new SegmentListCellRenderer());
272        }
273
274        protected void setHighlightedWaySegments(Collection<WaySegment> segments) {
275            selectedWay.getDataSet().setHighlightedWaySegments(segments);
276            MainApplication.getMap().mapView.repaint();
277        }
278
279        @Override
280        public void setVisible(boolean visible) {
281            super.setVisible(visible);
282            if (visible) {
283                DISPLAY_COUNT.incrementAndGet();
284                list.setSelectedValue(wayToKeep, true);
285            } else {
286                setHighlightedWaySegments(Collections.<WaySegment>emptyList());
287                DISPLAY_COUNT.decrementAndGet();
288            }
289        }
290
291        @Override
292        protected void buttonAction(int buttonIndex, ActionEvent evt) {
293            super.buttonAction(buttonIndex, evt);
294            toggleSaveState(); // necessary since #showDialog() does not handle it due to the non-modal dialog
295            if (getValue() == 1) {
296                SplitWayResult result = doSplitWay(selectedWay, list.getSelectedValue(), newWays, selection);
297                MainApplication.undoRedo.add(result.getCommand());
298                if (!result.getNewSelection().isEmpty()) {
299                    MainApplication.getLayerManager().getEditDataSet().setSelected(result.getNewSelection());
300                }
301            }
302        }
303    }
304
305    static class SegmentListCellRenderer extends DefaultListCellRenderer {
306        @Override
307        public Component getListCellRendererComponent(JList<?> list, Object value, int index, boolean isSelected, boolean cellHasFocus) {
308            final Component c = super.getListCellRendererComponent(list, value, index, isSelected, cellHasFocus);
309            final String name = DefaultNameFormatter.getInstance().format((Way) value);
310            // get rid of id from DefaultNameFormatter.decorateNameWithId()
311            final String nameWithoutId = name
312                    .replace(tr(" [id: {0}]", ((Way) value).getId()), "")
313                    .replace(tr(" [id: {0}]", ((Way) value).getUniqueId()), "");
314            ((JLabel) c).setText(tr("Segment {0}: {1}", index + 1, nameWithoutId));
315            return c;
316        }
317    }
318
319    /**
320     * Determines which way chunk should reuse the old id and its history
321     *
322     * @since 8954
323     * @since 10599 (functional interface)
324     */
325    @FunctionalInterface
326    public interface Strategy {
327
328        /**
329         * Determines which way chunk should reuse the old id and its history.
330         *
331         * @param wayChunks the way chunks
332         * @return the way to keep
333         */
334        Way determineWayToKeep(Iterable<Way> wayChunks);
335
336        /**
337         * Returns a strategy which selects the way chunk with the highest node count to keep.
338         * @return strategy which selects the way chunk with the highest node count to keep
339         */
340        static Strategy keepLongestChunk() {
341            return wayChunks -> {
342                    Way wayToKeep = null;
343                    for (Way i : wayChunks) {
344                        if (wayToKeep == null || i.getNodesCount() > wayToKeep.getNodesCount()) {
345                            wayToKeep = i;
346                        }
347                    }
348                    return wayToKeep;
349                };
350        }
351
352        /**
353         * Returns a strategy which selects the first way chunk.
354         * @return strategy which selects the first way chunk
355         */
356        static Strategy keepFirstChunk() {
357            return wayChunks -> wayChunks.iterator().next();
358        }
359    }
360
361    /**
362     * Determine which ways to split.
363     * @param selectedWays List of user selected ways.
364     * @param selectedNodes List of user selected nodes.
365     * @return List of ways to split
366     */
367    static List<Way> getApplicableWays(List<Way> selectedWays, List<Node> selectedNodes) {
368        if (selectedNodes.isEmpty())
369            return null;
370
371        // Special case - one of the selected ways touches (not cross) way that we want to split
372        if (selectedNodes.size() == 1) {
373            Node n = selectedNodes.get(0);
374            List<Way> referredWays = n.getParentWays();
375            Way inTheMiddle = null;
376            for (Way w: referredWays) {
377                // Need to look at all nodes see #11184 for a case where node n is
378                // firstNode, lastNode and also in the middle
379                if (selectedWays.contains(w) && w.isInnerNode(n)) {
380                    if (inTheMiddle == null) {
381                        inTheMiddle = w;
382                    } else {
383                        inTheMiddle = null;
384                        break;
385                    }
386                }
387            }
388            if (inTheMiddle != null)
389                return Collections.singletonList(inTheMiddle);
390        }
391
392        // List of ways shared by all nodes
393        return UnJoinNodeWayAction.getApplicableWays(selectedWays, selectedNodes);
394    }
395
396    /**
397     * Splits the nodes of {@code wayToSplit} into a list of node sequences
398     * which are separated at the nodes in {@code splitPoints}.
399     *
400     * This method displays warning messages if {@code wayToSplit} and/or
401     * {@code splitPoints} aren't consistent.
402     *
403     * Returns null, if building the split chunks fails.
404     *
405     * @param wayToSplit the way to split. Must not be null.
406     * @param splitPoints the nodes where the way is split. Must not be null.
407     * @return the list of chunks
408     */
409    public static List<List<Node>> buildSplitChunks(Way wayToSplit, List<Node> splitPoints) {
410        CheckParameterUtil.ensureParameterNotNull(wayToSplit, "wayToSplit");
411        CheckParameterUtil.ensureParameterNotNull(splitPoints, "splitPoints");
412
413        Set<Node> nodeSet = new HashSet<>(splitPoints);
414        List<List<Node>> wayChunks = new LinkedList<>();
415        List<Node> currentWayChunk = new ArrayList<>();
416        wayChunks.add(currentWayChunk);
417
418        Iterator<Node> it = wayToSplit.getNodes().iterator();
419        while (it.hasNext()) {
420            Node currentNode = it.next();
421            boolean atEndOfWay = currentWayChunk.isEmpty() || !it.hasNext();
422            currentWayChunk.add(currentNode);
423            if (nodeSet.contains(currentNode) && !atEndOfWay) {
424                currentWayChunk = new ArrayList<>();
425                currentWayChunk.add(currentNode);
426                wayChunks.add(currentWayChunk);
427            }
428        }
429
430        // Handle circular ways specially.
431        // If you split at a circular way at two nodes, you just want to split
432        // it at these points, not also at the former endpoint.
433        // So if the last node is the same first node, join the last and the
434        // first way chunk.
435        List<Node> lastWayChunk = wayChunks.get(wayChunks.size() - 1);
436        if (wayChunks.size() >= 2
437                && wayChunks.get(0).get(0) == lastWayChunk.get(lastWayChunk.size() - 1)
438                && !nodeSet.contains(wayChunks.get(0).get(0))) {
439            if (wayChunks.size() == 2) {
440                new Notification(
441                        tr("You must select two or more nodes to split a circular way."))
442                        .setIcon(JOptionPane.WARNING_MESSAGE)
443                        .show();
444                return null;
445            }
446            lastWayChunk.remove(lastWayChunk.size() - 1);
447            lastWayChunk.addAll(wayChunks.get(0));
448            wayChunks.remove(wayChunks.size() - 1);
449            wayChunks.set(0, lastWayChunk);
450        }
451
452        if (wayChunks.size() < 2) {
453            if (wayChunks.get(0).get(0) == wayChunks.get(0).get(wayChunks.get(0).size() - 1)) {
454                new Notification(
455                        tr("You must select two or more nodes to split a circular way."))
456                        .setIcon(JOptionPane.WARNING_MESSAGE)
457                        .show();
458            } else {
459                new Notification(
460                        tr("The way cannot be split at the selected nodes. (Hint: Select nodes in the middle of the way.)"))
461                        .setIcon(JOptionPane.WARNING_MESSAGE)
462                        .show();
463            }
464            return null;
465        }
466        return wayChunks;
467    }
468
469    /**
470     * Creates new way objects for the way chunks and transfers the keys from the original way.
471     * @param way the original way whose  keys are transferred
472     * @param wayChunks the way chunks
473     * @return the new way objects
474     */
475    protected static List<Way> createNewWaysFromChunks(Way way, Iterable<List<Node>> wayChunks) {
476        final List<Way> newWays = new ArrayList<>();
477        for (List<Node> wayChunk : wayChunks) {
478            Way wayToAdd = new Way();
479            wayToAdd.setKeys(way.getKeys());
480            wayToAdd.setNodes(wayChunk);
481            newWays.add(wayToAdd);
482        }
483        return newWays;
484    }
485
486    /**
487     * Splits the way {@code way} into chunks of {@code wayChunks} and replies
488     * the result of this process in an instance of {@link SplitWayResult}.
489     *
490     * Note that changes are not applied to the data yet. You have to
491     * submit the command in {@link SplitWayResult#getCommand()} first,
492     * i.e. {@code Main.main.undoredo.add(result.getCommand())}.
493     *
494     * @param layer the layer which the way belongs to.
495     * @param way the way to split. Must not be null.
496     * @param wayChunks the list of way chunks into the way is split. Must not be null.
497     * @param selection The list of currently selected primitives
498     * @return the result from the split operation
499     * @deprecated to be removed end of 2017. Use {@link #splitWay(Way, List, Collection)} instead
500     */
501    @Deprecated
502    public static SplitWayResult splitWay(OsmDataLayer layer, Way way, List<List<Node>> wayChunks,
503            Collection<? extends OsmPrimitive> selection) {
504        return splitWay(way, wayChunks, selection);
505    }
506
507    /**
508     * Splits the way {@code way} into chunks of {@code wayChunks} and replies
509     * the result of this process in an instance of {@link SplitWayResult}.
510     *
511     * Note that changes are not applied to the data yet. You have to
512     * submit the command in {@link SplitWayResult#getCommand()} first,
513     * i.e. {@code Main.main.undoredo.add(result.getCommand())}.
514     *
515     * @param way the way to split. Must not be null.
516     * @param wayChunks the list of way chunks into the way is split. Must not be null.
517     * @param selection The list of currently selected primitives
518     * @return the result from the split operation
519     * @since 12718
520     */
521    public static SplitWayResult splitWay(Way way, List<List<Node>> wayChunks,
522            Collection<? extends OsmPrimitive> selection) {
523        return splitWay(way, wayChunks, selection, Strategy.keepLongestChunk());
524    }
525
526    /**
527     * Splits the way {@code way} into chunks of {@code wayChunks} and replies
528     * the result of this process in an instance of {@link SplitWayResult}.
529     * The {@link org.openstreetmap.josm.actions.SplitWayAction.Strategy} is used to determine which
530     * way chunk should reuse the old id and its history.
531     *
532     * Note that changes are not applied to the data yet. You have to
533     * submit the command in {@link SplitWayResult#getCommand()} first,
534     * i.e. {@code Main.main.undoredo.add(result.getCommand())}.
535     *
536     * @param layer the layer which the way belongs to.
537     * @param way the way to split. Must not be null.
538     * @param wayChunks the list of way chunks into the way is split. Must not be null.
539     * @param selection The list of currently selected primitives
540     * @param splitStrategy The strategy used to determine which way chunk should reuse the old id and its history
541     * @return the result from the split operation
542     * @since 8954
543     * @deprecated to be removed end of 2017. Use {@link #splitWay(Way, List, Collection, Strategy)} instead
544     */
545    @Deprecated
546    public static SplitWayResult splitWay(OsmDataLayer layer, Way way, List<List<Node>> wayChunks,
547            Collection<? extends OsmPrimitive> selection, Strategy splitStrategy) {
548        return splitWay(way, wayChunks, selection, splitStrategy);
549    }
550
551    /**
552     * Splits the way {@code way} into chunks of {@code wayChunks} and replies
553     * the result of this process in an instance of {@link SplitWayResult}.
554     * The {@link org.openstreetmap.josm.actions.SplitWayAction.Strategy} is used to determine which
555     * way chunk should reuse the old id and its history.
556     *
557     * Note that changes are not applied to the data yet. You have to
558     * submit the command in {@link SplitWayResult#getCommand()} first,
559     * i.e. {@code Main.main.undoredo.add(result.getCommand())}.
560     *
561     * @param way the way to split. Must not be null.
562     * @param wayChunks the list of way chunks into the way is split. Must not be null.
563     * @param selection The list of currently selected primitives
564     * @param splitStrategy The strategy used to determine which way chunk should reuse the old id and its history
565     * @return the result from the split operation
566     * @since 12718
567     */
568    public static SplitWayResult splitWay(Way way, List<List<Node>> wayChunks,
569            Collection<? extends OsmPrimitive> selection, Strategy splitStrategy) {
570        // build a list of commands, and also a new selection list
571        final List<OsmPrimitive> newSelection = new ArrayList<>(selection.size() + wayChunks.size());
572        newSelection.addAll(selection);
573
574        // Create all potential new ways
575        final List<Way> newWays = createNewWaysFromChunks(way, wayChunks);
576
577        // Determine which part reuses the existing way
578        final Way wayToKeep = splitStrategy.determineWayToKeep(newWays);
579
580        return wayToKeep != null ? doSplitWay(way, wayToKeep, newWays, newSelection) : null;
581    }
582
583    static SplitWayResult doSplitWay(Way way, Way wayToKeep, List<Way> newWays, List<OsmPrimitive> newSelection) {
584
585        Collection<Command> commandList = new ArrayList<>(newWays.size());
586        Collection<String> nowarnroles = Main.pref.getCollection("way.split.roles.nowarn",
587                Arrays.asList("outer", "inner", "forward", "backward", "north", "south", "east", "west"));
588
589        final MapFrame map = MainApplication.getMap();
590        final boolean isMapModeDraw = map != null && map.mapMode == map.mapModeDraw;
591
592        // Change the original way
593        final Way changedWay = new Way(way);
594        changedWay.setNodes(wayToKeep.getNodes());
595        commandList.add(new ChangeCommand(way.getDataSet(), way, changedWay));
596        if (!isMapModeDraw && !newSelection.contains(way)) {
597            newSelection.add(way);
598        }
599        final int indexOfWayToKeep = newWays.indexOf(wayToKeep);
600        newWays.remove(wayToKeep);
601
602        if (!isMapModeDraw) {
603            newSelection.addAll(newWays);
604        }
605        for (Way wayToAdd : newWays) {
606            commandList.add(new AddCommand(way.getDataSet(), wayToAdd));
607        }
608
609        boolean warnmerole = false;
610        boolean warnme = false;
611        // now copy all relations to new way also
612
613        for (Relation r : OsmPrimitive.getFilteredList(way.getReferrers(), Relation.class)) {
614            if (!r.isUsable()) {
615                continue;
616            }
617            Relation c = null;
618            String type = Optional.ofNullable(r.get("type")).orElse("");
619
620            int ic = 0;
621            int ir = 0;
622            List<RelationMember> relationMembers = r.getMembers();
623            for (RelationMember rm: relationMembers) {
624                if (rm.isWay() && rm.getMember() == way) {
625                    boolean insert = true;
626                    if ("restriction".equals(type) || "destination_sign".equals(type)) {
627                        /* this code assumes the restriction is correct. No real error checking done */
628                        String role = rm.getRole();
629                        if ("from".equals(role) || "to".equals(role)) {
630                            OsmPrimitive via = findVia(r, type);
631                            List<Node> nodes = new ArrayList<>();
632                            if (via != null) {
633                                if (via instanceof Node) {
634                                    nodes.add((Node) via);
635                                } else if (via instanceof Way) {
636                                    nodes.add(((Way) via).lastNode());
637                                    nodes.add(((Way) via).firstNode());
638                                }
639                            }
640                            Way res = null;
641                            for (Node n : nodes) {
642                                if (changedWay.isFirstLastNode(n)) {
643                                    res = way;
644                                }
645                            }
646                            if (res == null) {
647                                for (Way wayToAdd : newWays) {
648                                    for (Node n : nodes) {
649                                        if (wayToAdd.isFirstLastNode(n)) {
650                                            res = wayToAdd;
651                                        }
652                                    }
653                                }
654                                if (res != null) {
655                                    if (c == null) {
656                                        c = new Relation(r);
657                                    }
658                                    c.addMember(new RelationMember(role, res));
659                                    c.removeMembersFor(way);
660                                    insert = false;
661                                }
662                            } else {
663                                insert = false;
664                            }
665                        } else if (!"via".equals(role)) {
666                            warnme = true;
667                        }
668                    } else if (!("route".equals(type)) && !("multipolygon".equals(type))) {
669                        warnme = true;
670                    }
671                    if (c == null) {
672                        c = new Relation(r);
673                    }
674
675                    if (insert) {
676                        if (rm.hasRole() && !nowarnroles.contains(rm.getRole())) {
677                            warnmerole = true;
678                        }
679
680                        Boolean backwards = null;
681                        int k = 1;
682                        while (ir - k >= 0 || ir + k < relationMembers.size()) {
683                            if ((ir - k >= 0) && relationMembers.get(ir - k).isWay()) {
684                                Way w = relationMembers.get(ir - k).getWay();
685                                if ((w.lastNode() == way.firstNode()) || w.firstNode() == way.firstNode()) {
686                                    backwards = Boolean.FALSE;
687                                } else if ((w.firstNode() == way.lastNode()) || w.lastNode() == way.lastNode()) {
688                                    backwards = Boolean.TRUE;
689                                }
690                                break;
691                            }
692                            if ((ir + k < relationMembers.size()) && relationMembers.get(ir + k).isWay()) {
693                                Way w = relationMembers.get(ir + k).getWay();
694                                if ((w.lastNode() == way.firstNode()) || w.firstNode() == way.firstNode()) {
695                                    backwards = Boolean.TRUE;
696                                } else if ((w.firstNode() == way.lastNode()) || w.lastNode() == way.lastNode()) {
697                                    backwards = Boolean.FALSE;
698                                }
699                                break;
700                            }
701                            k++;
702                        }
703
704                        int j = ic;
705                        final List<Way> waysToAddBefore = newWays.subList(0, indexOfWayToKeep);
706                        for (Way wayToAdd : waysToAddBefore) {
707                            RelationMember em = new RelationMember(rm.getRole(), wayToAdd);
708                            j++;
709                            if (Boolean.TRUE.equals(backwards)) {
710                                c.addMember(ic + 1, em);
711                            } else {
712                                c.addMember(j - 1, em);
713                            }
714                        }
715                        final List<Way> waysToAddAfter = newWays.subList(indexOfWayToKeep, newWays.size());
716                        for (Way wayToAdd : waysToAddAfter) {
717                            RelationMember em = new RelationMember(rm.getRole(), wayToAdd);
718                            j++;
719                            if (Boolean.TRUE.equals(backwards)) {
720                                c.addMember(ic, em);
721                            } else {
722                                c.addMember(j, em);
723                            }
724                        }
725                        ic = j;
726                    }
727                }
728                ic++;
729                ir++;
730            }
731
732            if (c != null) {
733                commandList.add(new ChangeCommand(r.getDataSet(), r, c));
734            }
735        }
736        if (warnmerole) {
737            new Notification(
738                    tr("A role based relation membership was copied to all new ways.<br>You should verify this and correct it when necessary."))
739                    .setIcon(JOptionPane.WARNING_MESSAGE)
740                    .show();
741        } else if (warnme) {
742            new Notification(
743                    tr("A relation membership was copied to all new ways.<br>You should verify this and correct it when necessary."))
744                    .setIcon(JOptionPane.WARNING_MESSAGE)
745                    .show();
746        }
747
748        return new SplitWayResult(
749                new SequenceCommand(
750                        /* for correct i18n of plural forms - see #9110 */
751                        trn("Split way {0} into {1} part", "Split way {0} into {1} parts", newWays.size() + 1,
752                                way.getDisplayName(DefaultNameFormatter.getInstance()), newWays.size() + 1),
753                        commandList
754                        ),
755                        newSelection,
756                        way,
757                        newWays
758                );
759    }
760
761    static OsmPrimitive findVia(Relation r, String type) {
762        for (RelationMember rmv : r.getMembers()) {
763            if (("restriction".equals(type) && "via".equals(rmv.getRole()))
764             || ("destination_sign".equals(type) && rmv.hasRole("sign", "intersection"))) {
765                return rmv.getMember();
766            }
767        }
768        return null;
769    }
770
771    /**
772     * Splits the way {@code way} at the nodes in {@code atNodes} and replies
773     * the result of this process in an instance of {@link SplitWayResult}.
774     *
775     * Note that changes are not applied to the data yet. You have to
776     * submit the command in {@link SplitWayResult#getCommand()} first,
777     * i.e. {@code Main.main.undoredo.add(result.getCommand())}.
778     *
779     * Replies null if the way couldn't be split at the given nodes.
780     *
781     * @param layer the layer which the way belongs to.
782     * @param way the way to split. Must not be null.
783     * @param atNodes the list of nodes where the way is split. Must not be null.
784     * @param selection The list of currently selected primitives
785     * @return the result from the split operation
786     * @deprecated to be removed end of 2017. Use {@link #split(Way, List, Collection) instead}
787     */
788    @Deprecated
789    public static SplitWayResult split(OsmDataLayer layer, Way way, List<Node> atNodes, Collection<? extends OsmPrimitive> selection) {
790        return split(way, atNodes, selection);
791    }
792
793    /**
794     * Splits the way {@code way} at the nodes in {@code atNodes} and replies
795     * the result of this process in an instance of {@link SplitWayResult}.
796     *
797     * Note that changes are not applied to the data yet. You have to
798     * submit the command in {@link SplitWayResult#getCommand()} first,
799     * i.e. {@code Main.main.undoredo.add(result.getCommand())}.
800     *
801     * Replies null if the way couldn't be split at the given nodes.
802     *
803     * @param way the way to split. Must not be null.
804     * @param atNodes the list of nodes where the way is split. Must not be null.
805     * @param selection The list of currently selected primitives
806     * @return the result from the split operation
807     * @since 12718
808     */
809    public static SplitWayResult split(Way way, List<Node> atNodes, Collection<? extends OsmPrimitive> selection) {
810        List<List<Node>> chunks = buildSplitChunks(way, atNodes);
811        return chunks != null ? splitWay(way, chunks, selection) : null;
812    }
813
814    @Override
815    protected void updateEnabledState() {
816        updateEnabledStateOnCurrentSelection();
817    }
818
819    @Override
820    protected void updateEnabledState(Collection<? extends OsmPrimitive> selection) {
821        if (selection == null) {
822            setEnabled(false);
823            return;
824        }
825        for (OsmPrimitive primitive: selection) {
826            if (primitive instanceof Node) {
827                setEnabled(true); // Selection still can be wrong, but let SplitWayAction process and tell user what's wrong
828                return;
829            }
830        }
831        setEnabled(false);
832    }
833}
Note: See TracBrowser for help on using the repository browser.