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.Collection;
014import java.util.Collections;
015import java.util.Iterator;
016import java.util.List;
017import java.util.concurrent.atomic.AtomicInteger;
018
019import javax.swing.DefaultListCellRenderer;
020import javax.swing.JLabel;
021import javax.swing.JList;
022import javax.swing.JOptionPane;
023import javax.swing.JPanel;
024import javax.swing.ListSelectionModel;
025
026import org.openstreetmap.josm.Main;
027import org.openstreetmap.josm.command.SplitWayCommand;
028import org.openstreetmap.josm.data.osm.DataSet;
029import org.openstreetmap.josm.data.osm.DefaultNameFormatter;
030import org.openstreetmap.josm.data.osm.Node;
031import org.openstreetmap.josm.data.osm.OsmPrimitive;
032import org.openstreetmap.josm.data.osm.PrimitiveId;
033import org.openstreetmap.josm.data.osm.Relation;
034import org.openstreetmap.josm.data.osm.Way;
035import org.openstreetmap.josm.data.osm.WaySegment;
036import org.openstreetmap.josm.gui.ExtendedDialog;
037import org.openstreetmap.josm.gui.MainApplication;
038import org.openstreetmap.josm.gui.MapFrame;
039import org.openstreetmap.josm.gui.Notification;
040import org.openstreetmap.josm.tools.GBC;
041import org.openstreetmap.josm.tools.Shortcut;
042
043/**
044 * Splits a way into multiple ways (all identical except for their node list).
045 *
046 * Ways are just split at the selected nodes.  The nodes remain in their
047 * original order.  Selected nodes at the end of a way are ignored.
048 */
049public class SplitWayAction extends JosmAction {
050
051    /**
052     * Create a new SplitWayAction.
053     */
054    public SplitWayAction() {
055        super(tr("Split Way"), "splitway", tr("Split a way at the selected node."),
056                Shortcut.registerShortcut("tools:splitway", tr("Tool: {0}", tr("Split Way")), KeyEvent.VK_P, Shortcut.DIRECT), true);
057        putValue("help", ht("/Action/SplitWay"));
058    }
059
060    /**
061     * Called when the action is executed.
062     *
063     * This method performs an expensive check whether the selection clearly defines one
064     * of the split actions outlined above, and if yes, calls the splitWay method.
065     */
066    @Override
067    public void actionPerformed(ActionEvent e) {
068
069        if (SegmentToKeepSelectionDialog.DISPLAY_COUNT.get() > 0) {
070            new Notification(tr("Cannot split since another split operation is already in progress"))
071                    .setIcon(JOptionPane.WARNING_MESSAGE).show();
072            return;
073        }
074
075        Collection<OsmPrimitive> selection = getLayerManager().getEditDataSet().getSelected();
076
077        List<Node> selectedNodes = OsmPrimitive.getFilteredList(selection, Node.class);
078        List<Way> selectedWays = OsmPrimitive.getFilteredList(selection, Way.class);
079        List<Way> applicableWays = getApplicableWays(selectedWays, selectedNodes);
080
081        if (applicableWays == null) {
082            new Notification(
083                    tr("The current selection cannot be used for splitting - no node is selected."))
084                    .setIcon(JOptionPane.WARNING_MESSAGE)
085                    .show();
086            return;
087        } else if (applicableWays.isEmpty()) {
088            new Notification(
089                    tr("The selected nodes do not share the same way."))
090                    .setIcon(JOptionPane.WARNING_MESSAGE)
091                    .show();
092            return;
093        }
094
095        // If several ways have been found, remove ways that doesn't have selected
096        // node in the middle
097        if (applicableWays.size() > 1) {
098            for (Iterator<Way> it = applicableWays.iterator(); it.hasNext();) {
099                Way w = it.next();
100                for (Node n : selectedNodes) {
101                    if (!w.isInnerNode(n)) {
102                        it.remove();
103                        break;
104                    }
105                }
106            }
107        }
108
109        if (applicableWays.isEmpty()) {
110            new Notification(
111                    trn("The selected node is not in the middle of any way.",
112                        "The selected nodes are not in the middle of any way.",
113                        selectedNodes.size()))
114                    .setIcon(JOptionPane.WARNING_MESSAGE)
115                    .show();
116            return;
117        } else if (applicableWays.size() > 1) {
118            new Notification(
119                    trn("There is more than one way using the node you selected. Please select the way also.",
120                        "There is more than one way using the nodes you selected. Please select the way also.",
121                        selectedNodes.size()))
122                    .setIcon(JOptionPane.WARNING_MESSAGE)
123                    .show();
124            return;
125        }
126
127        // Finally, applicableWays contains only one perfect way
128        final Way selectedWay = applicableWays.get(0);
129        final List<List<Node>> wayChunks = SplitWayCommand.buildSplitChunks(selectedWay, selectedNodes);
130        if (wayChunks != null) {
131            List<Relation> selectedRelations = OsmPrimitive.getFilteredList(selection, Relation.class);
132            final List<OsmPrimitive> sel = new ArrayList<>(selectedWays.size() + selectedRelations.size());
133            sel.addAll(selectedWays);
134            sel.addAll(selectedRelations);
135
136            final List<Way> newWays = SplitWayCommand.createNewWaysFromChunks(selectedWay, wayChunks);
137            final Way wayToKeep = SplitWayCommand.Strategy.keepLongestChunk().determineWayToKeep(newWays);
138
139            if (ExpertToggleAction.isExpert() && !selectedWay.isNew()) {
140                final ExtendedDialog dialog = new SegmentToKeepSelectionDialog(selectedWay, newWays, wayToKeep, sel);
141                dialog.toggleEnable("way.split.segment-selection-dialog");
142                if (!dialog.toggleCheckState()) {
143                    dialog.setModal(false);
144                    dialog.showDialog();
145                    return; // splitting is performed in SegmentToKeepSelectionDialog.buttonAction()
146                }
147            }
148            if (wayToKeep != null) {
149                doSplitWay(selectedWay, wayToKeep, newWays, sel);
150            }
151        }
152    }
153
154    /**
155     * A dialog to query which way segment should reuse the history of the way to split.
156     */
157    static class SegmentToKeepSelectionDialog extends ExtendedDialog {
158        static final AtomicInteger DISPLAY_COUNT = new AtomicInteger();
159        final transient Way selectedWay;
160        final transient List<Way> newWays;
161        final JList<Way> list;
162        final transient List<OsmPrimitive> selection;
163        final transient Way wayToKeep;
164
165        SegmentToKeepSelectionDialog(Way selectedWay, List<Way> newWays, Way wayToKeep, List<OsmPrimitive> selection) {
166            super(Main.parent, tr("Which way segment should reuse the history of {0}?", selectedWay.getId()),
167                    new String[]{tr("Ok"), tr("Cancel")}, true);
168
169            this.selectedWay = selectedWay;
170            this.newWays = newWays;
171            this.selection = selection;
172            this.wayToKeep = wayToKeep;
173            this.list = new JList<>(newWays.toArray(new Way[0]));
174            configureList();
175
176            setButtonIcons("ok", "cancel");
177            final JPanel pane = new JPanel(new GridBagLayout());
178            pane.add(new JLabel(getTitle()), GBC.eol().fill(GBC.HORIZONTAL));
179            pane.add(list, GBC.eop().fill(GBC.HORIZONTAL));
180            setContent(pane);
181            setDefaultCloseOperation(HIDE_ON_CLOSE);
182        }
183
184        private void configureList() {
185            list.setSelectionMode(ListSelectionModel.SINGLE_SELECTION);
186            list.addListSelectionListener(e -> {
187                final Way selected = list.getSelectedValue();
188                if (selected != null && MainApplication.isDisplayingMapView() && selected.getNodesCount() > 1) {
189                    final Collection<WaySegment> segments = new ArrayList<>(selected.getNodesCount() - 1);
190                    final Iterator<Node> it = selected.getNodes().iterator();
191                    Node previousNode = it.next();
192                    while (it.hasNext()) {
193                        final Node node = it.next();
194                        segments.add(WaySegment.forNodePair(selectedWay, previousNode, node));
195                        previousNode = node;
196                    }
197                    setHighlightedWaySegments(segments);
198                }
199            });
200            list.setCellRenderer(new SegmentListCellRenderer());
201        }
202
203        protected void setHighlightedWaySegments(Collection<WaySegment> segments) {
204            selectedWay.getDataSet().setHighlightedWaySegments(segments);
205            MainApplication.getMap().mapView.repaint();
206        }
207
208        @Override
209        public void setVisible(boolean visible) {
210            super.setVisible(visible);
211            if (visible) {
212                DISPLAY_COUNT.incrementAndGet();
213                list.setSelectedValue(wayToKeep, true);
214            } else {
215                setHighlightedWaySegments(Collections.<WaySegment>emptyList());
216                DISPLAY_COUNT.decrementAndGet();
217            }
218        }
219
220        @Override
221        protected void buttonAction(int buttonIndex, ActionEvent evt) {
222            super.buttonAction(buttonIndex, evt);
223            toggleSaveState(); // necessary since #showDialog() does not handle it due to the non-modal dialog
224            if (getValue() == 1) {
225                doSplitWay(selectedWay, list.getSelectedValue(), newWays, selection);
226            }
227        }
228    }
229
230    static class SegmentListCellRenderer extends DefaultListCellRenderer {
231        @Override
232        public Component getListCellRendererComponent(JList<?> list, Object value, int index, boolean isSelected, boolean cellHasFocus) {
233            final Component c = super.getListCellRendererComponent(list, value, index, isSelected, cellHasFocus);
234            final String name = DefaultNameFormatter.getInstance().format((Way) value);
235            // get rid of id from DefaultNameFormatter.decorateNameWithId()
236            final String nameWithoutId = name
237                    .replace(tr(" [id: {0}]", ((Way) value).getId()), "")
238                    .replace(tr(" [id: {0}]", ((Way) value).getUniqueId()), "");
239            ((JLabel) c).setText(tr("Segment {0}: {1}", index + 1, nameWithoutId));
240            return c;
241        }
242    }
243
244    /**
245     * Determine which ways to split.
246     * @param selectedWays List of user selected ways.
247     * @param selectedNodes List of user selected nodes.
248     * @return List of ways to split
249     */
250    static List<Way> getApplicableWays(List<Way> selectedWays, List<Node> selectedNodes) {
251        if (selectedNodes.isEmpty())
252            return null;
253
254        // Special case - one of the selected ways touches (not cross) way that we want to split
255        if (selectedNodes.size() == 1) {
256            Node n = selectedNodes.get(0);
257            List<Way> referredWays = n.getParentWays();
258            Way inTheMiddle = null;
259            for (Way w: referredWays) {
260                // Need to look at all nodes see #11184 for a case where node n is
261                // firstNode, lastNode and also in the middle
262                if (selectedWays.contains(w) && w.isInnerNode(n)) {
263                    if (inTheMiddle == null) {
264                        inTheMiddle = w;
265                    } else {
266                        inTheMiddle = null;
267                        break;
268                    }
269                }
270            }
271            if (inTheMiddle != null)
272                return Collections.singletonList(inTheMiddle);
273        }
274
275        // List of ways shared by all nodes
276        return UnJoinNodeWayAction.getApplicableWays(selectedWays, selectedNodes);
277    }
278
279    static void doSplitWay(Way way, Way wayToKeep, List<Way> newWays, List<OsmPrimitive> newSelection) {
280        final MapFrame map = MainApplication.getMap();
281        final boolean isMapModeDraw = map != null && map.mapMode == map.mapModeDraw;
282        final SplitWayCommand result = SplitWayCommand.doSplitWay(way, wayToKeep, newWays, !isMapModeDraw ? newSelection : null);
283        MainApplication.undoRedo.add(result);
284        List<? extends PrimitiveId> newSel = result.getNewSelection();
285        if (newSel != null && !newSel.isEmpty()) {
286            MainApplication.getLayerManager().getEditDataSet().setSelected(newSel);
287        }
288    }
289
290    @Override
291    protected void updateEnabledState() {
292        updateEnabledStateOnCurrentSelection();
293    }
294
295    @Override
296    protected void updateEnabledState(Collection<? extends OsmPrimitive> selection) {
297        // Selection still can be wrong, but let SplitWayAction process and tell user what's wrong
298        setEnabled(selection != null && !selection.isEmpty()
299                && selection.stream().map(OsmPrimitive::getDataSet).noneMatch(DataSet::isLocked)
300                && selection.stream().anyMatch(o -> o instanceof Node && !o.isIncomplete()));
301    }
302}