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