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.event.ActionEvent;
009import java.awt.event.KeyEvent;
010import java.util.ArrayList;
011import java.util.Arrays;
012import java.util.Collection;
013import java.util.Collections;
014import java.util.HashSet;
015import java.util.Iterator;
016import java.util.LinkedList;
017import java.util.List;
018import java.util.Set;
019
020import javax.swing.JOptionPane;
021
022import org.openstreetmap.josm.Main;
023import org.openstreetmap.josm.command.AddCommand;
024import org.openstreetmap.josm.command.ChangeCommand;
025import org.openstreetmap.josm.command.Command;
026import org.openstreetmap.josm.command.SequenceCommand;
027import org.openstreetmap.josm.data.osm.Node;
028import org.openstreetmap.josm.data.osm.OsmPrimitive;
029import org.openstreetmap.josm.data.osm.PrimitiveId;
030import org.openstreetmap.josm.data.osm.Relation;
031import org.openstreetmap.josm.data.osm.RelationMember;
032import org.openstreetmap.josm.data.osm.Way;
033import org.openstreetmap.josm.gui.DefaultNameFormatter;
034import org.openstreetmap.josm.gui.Notification;
035import org.openstreetmap.josm.gui.layer.OsmDataLayer;
036import org.openstreetmap.josm.tools.CheckParameterUtil;
037import org.openstreetmap.josm.tools.Shortcut;
038
039/**
040 * Splits a way into multiple ways (all identical except for their node list).
041 *
042 * Ways are just split at the selected nodes.  The nodes remain in their
043 * original order.  Selected nodes at the end of a way are ignored.
044 */
045
046public class SplitWayAction extends JosmAction {
047
048    /**
049     * Represents the result of a {@link SplitWayAction}
050     * @see SplitWayAction#splitWay
051     * @see SplitWayAction#split
052     */
053    public static class SplitWayResult {
054        private final Command command;
055        private final List<? extends PrimitiveId> newSelection;
056        private Way originalWay;
057        private List<Way> newWays;
058
059        /**
060         * @param command The command to be performed to split the way (which is saved for later retrieval by the {@link #getCommand} method)
061         * @param newSelection The new list of selected primitives ids (which is saved for later retrieval by the {@link #getNewSelection} method)
062         * @param originalWay The original way being split (which is saved for later retrieval by the {@link #getOriginalWay} method)
063         * @param newWays The resulting new ways (which is saved for later retrieval by the {@link #getOriginalWay} method)
064         */
065        public SplitWayResult(Command command, List<? extends PrimitiveId> newSelection, Way originalWay, List<Way> newWays) {
066            this.command = command;
067            this.newSelection = newSelection;
068            this.originalWay = originalWay;
069            this.newWays = newWays;
070        }
071
072        /**
073         * Replies the command to be performed to split the way
074         * @return The command to be performed to split the way
075         */
076        public Command getCommand() {
077            return command;
078        }
079
080        /**
081         * Replies the new list of selected primitives ids
082         * @return The new list of selected primitives ids
083         */
084        public List<? extends PrimitiveId> getNewSelection() {
085            return newSelection;
086        }
087
088        /**
089         * Replies the original way being split
090         * @return The original way being split
091         */
092        public Way getOriginalWay() {
093            return originalWay;
094        }
095
096        /**
097         * Replies the resulting new ways
098         * @return The resulting new ways
099         */
100        public List<Way> getNewWays() {
101            return newWays;
102        }
103    }
104
105    /**
106     * Create a new SplitWayAction.
107     */
108    public SplitWayAction() {
109        super(tr("Split Way"), "splitway", tr("Split a way at the selected node."),
110                Shortcut.registerShortcut("tools:splitway", tr("Tool: {0}", tr("Split Way")), KeyEvent.VK_P, Shortcut.DIRECT), true);
111        putValue("help", ht("/Action/SplitWay"));
112    }
113
114    /**
115     * Called when the action is executed.
116     *
117     * This method performs an expensive check whether the selection clearly defines one
118     * of the split actions outlined above, and if yes, calls the splitWay method.
119     */
120    @Override
121    public void actionPerformed(ActionEvent e) {
122
123        Collection<OsmPrimitive> selection = getCurrentDataSet().getSelected();
124
125        List<Node> selectedNodes = OsmPrimitive.getFilteredList(selection, Node.class);
126        List<Way> selectedWays = OsmPrimitive.getFilteredList(selection, Way.class);
127        List<Relation> selectedRelations = OsmPrimitive.getFilteredList(selection, Relation.class);
128        List<Way> applicableWays = getApplicableWays(selectedWays, selectedNodes);
129
130        if (applicableWays == null) {
131            new Notification(
132                    tr("The current selection cannot be used for splitting - no node is selected."))
133                    .setIcon(JOptionPane.WARNING_MESSAGE)
134                    .show();
135            return;
136        } else if (applicableWays.isEmpty()) {
137            new Notification(
138                    tr("The selected nodes do not share the same way."))
139                    .setIcon(JOptionPane.WARNING_MESSAGE)
140                    .show();
141            return;
142        }
143
144        // If several ways have been found, remove ways that doesn't have selected node in the middle
145        if (applicableWays.size() > 1) {
146            WAY_LOOP:
147                for (Iterator<Way> it = applicableWays.iterator(); it.hasNext();) {
148                    Way w = it.next();
149                    for (Node n : selectedNodes) {
150                        if (!w.isInnerNode(n)) {
151                            it.remove();
152                            continue WAY_LOOP;
153                        }
154                    }
155                }
156        }
157
158        if (applicableWays.isEmpty()) {
159            new Notification(
160                    trn("The selected node is not in the middle of any way.",
161                        "The selected nodes are not in the middle of any way.",
162                        selectedNodes.size()))
163                    .setIcon(JOptionPane.WARNING_MESSAGE)
164                    .show();
165            return;
166        } else if (applicableWays.size() > 1) {
167            new Notification(
168                    trn("There is more than one way using the node you selected. Please select the way also.",
169                        "There is more than one way using the nodes you selected. Please select the way also.",
170                        selectedNodes.size()))
171                    .setIcon(JOptionPane.WARNING_MESSAGE)
172                    .show();
173            return;
174        }
175
176        // Finally, applicableWays contains only one perfect way
177        Way selectedWay = applicableWays.get(0);
178
179        List<List<Node>> wayChunks = buildSplitChunks(selectedWay, selectedNodes);
180        if (wayChunks != null) {
181            List<OsmPrimitive> sel = new ArrayList<>(selectedWays.size() + selectedRelations.size());
182            sel.addAll(selectedWays);
183            sel.addAll(selectedRelations);
184            SplitWayResult result = splitWay(getEditLayer(),selectedWay, wayChunks, sel);
185            Main.main.undoRedo.add(result.getCommand());
186            getCurrentDataSet().setSelected(result.getNewSelection());
187        }
188    }
189
190    private List<Way> getApplicableWays(List<Way> selectedWays, List<Node> selectedNodes) {
191        if (selectedNodes.isEmpty())
192            return null;
193
194        // Special case - one of the selected ways touches (not cross) way that we want to split
195        if (selectedNodes.size() == 1) {
196            Node n = selectedNodes.get(0);
197            List<Way> referedWays = OsmPrimitive.getFilteredList(n.getReferrers(), Way.class);
198            Way inTheMiddle = null;
199            boolean foundSelected = false;
200            for (Way w: referedWays) {
201                if (selectedWays.contains(w)) {
202                    foundSelected = true;
203                }
204                if (w.getNode(0) != n && w.getNode(w.getNodesCount() - 1) != n) {
205                    if (inTheMiddle == null) {
206                        inTheMiddle = w;
207                    } else {
208                        inTheMiddle = null;
209                        break;
210                    }
211                }
212            }
213            if (foundSelected && inTheMiddle != null)
214                return Collections.singletonList(inTheMiddle);
215        }
216
217        // List of ways shared by all nodes
218        List<Way> result = new ArrayList<>(OsmPrimitive.getFilteredList(selectedNodes.get(0).getReferrers(), Way.class));
219        for (int i=1; i<selectedNodes.size(); i++) {
220            List<OsmPrimitive> ref = selectedNodes.get(i).getReferrers();
221            for (Iterator<Way> it = result.iterator(); it.hasNext(); ) {
222                if (!ref.contains(it.next())) {
223                    it.remove();
224                }
225            }
226        }
227
228        // Remove broken ways
229        for (Iterator<Way> it = result.iterator(); it.hasNext(); ) {
230            if (it.next().getNodesCount() <= 2) {
231                it.remove();
232            }
233        }
234
235        if (selectedWays.isEmpty())
236            return result;
237        else {
238            // Return only selected ways
239            for (Iterator<Way> it = result.iterator(); it.hasNext(); ) {
240                if (!selectedWays.contains(it.next())) {
241                    it.remove();
242                }
243            }
244            return result;
245        }
246    }
247
248    /**
249     * Splits the nodes of {@code wayToSplit} into a list of node sequences
250     * which are separated at the nodes in {@code splitPoints}.
251     *
252     * This method displays warning messages if {@code wayToSplit} and/or
253     * {@code splitPoints} aren't consistent.
254     *
255     * Returns null, if building the split chunks fails.
256     *
257     * @param wayToSplit the way to split. Must not be null.
258     * @param splitPoints the nodes where the way is split. Must not be null.
259     * @return the list of chunks
260     */
261    public static List<List<Node>> buildSplitChunks(Way wayToSplit, List<Node> splitPoints){
262        CheckParameterUtil.ensureParameterNotNull(wayToSplit, "wayToSplit");
263        CheckParameterUtil.ensureParameterNotNull(splitPoints, "splitPoints");
264
265        Set<Node> nodeSet = new HashSet<>(splitPoints);
266        List<List<Node>> wayChunks = new LinkedList<>();
267        List<Node> currentWayChunk = new ArrayList<>();
268        wayChunks.add(currentWayChunk);
269
270        Iterator<Node> it = wayToSplit.getNodes().iterator();
271        while (it.hasNext()) {
272            Node currentNode = it.next();
273            boolean atEndOfWay = currentWayChunk.isEmpty() || !it.hasNext();
274            currentWayChunk.add(currentNode);
275            if (nodeSet.contains(currentNode) && !atEndOfWay) {
276                currentWayChunk = new ArrayList<>();
277                currentWayChunk.add(currentNode);
278                wayChunks.add(currentWayChunk);
279            }
280        }
281
282        // Handle circular ways specially.
283        // If you split at a circular way at two nodes, you just want to split
284        // it at these points, not also at the former endpoint.
285        // So if the last node is the same first node, join the last and the
286        // first way chunk.
287        List<Node> lastWayChunk = wayChunks.get(wayChunks.size() - 1);
288        if (wayChunks.size() >= 2
289                && wayChunks.get(0).get(0) == lastWayChunk.get(lastWayChunk.size() - 1)
290                && !nodeSet.contains(wayChunks.get(0).get(0))) {
291            if (wayChunks.size() == 2) {
292                new Notification(
293                        tr("You must select two or more nodes to split a circular way."))
294                        .setIcon(JOptionPane.WARNING_MESSAGE)
295                        .show();
296                return null;
297            }
298            lastWayChunk.remove(lastWayChunk.size() - 1);
299            lastWayChunk.addAll(wayChunks.get(0));
300            wayChunks.remove(wayChunks.size() - 1);
301            wayChunks.set(0, lastWayChunk);
302        }
303
304        if (wayChunks.size() < 2) {
305            if (wayChunks.get(0).get(0) == wayChunks.get(0).get(wayChunks.get(0).size() - 1)) {
306                new Notification(
307                        tr("You must select two or more nodes to split a circular way."))
308                        .setIcon(JOptionPane.WARNING_MESSAGE)
309                        .show();
310            } else {
311                new Notification(
312                        tr("The way cannot be split at the selected nodes. (Hint: Select nodes in the middle of the way.)"))
313                        .setIcon(JOptionPane.WARNING_MESSAGE)
314                        .show();
315            }
316            return null;
317        }
318        return wayChunks;
319    }
320
321    /**
322     * Splits the way {@code way} into chunks of {@code wayChunks} and replies
323     * the result of this process in an instance of {@link SplitWayResult}.
324     *
325     * Note that changes are not applied to the data yet. You have to
326     * submit the command in {@link SplitWayResult#getCommand()} first,
327     * i.e. {@code Main.main.undoredo.add(result.getCommand())}.
328     *
329     * @param layer the layer which the way belongs to. Must not be null.
330     * @param way the way to split. Must not be null.
331     * @param wayChunks the list of way chunks into the way is split. Must not be null.
332     * @param selection The list of currently selected primitives
333     * @return the result from the split operation
334     */
335    public static SplitWayResult splitWay(OsmDataLayer layer, Way way, List<List<Node>> wayChunks, Collection<? extends OsmPrimitive> selection) {
336        // build a list of commands, and also a new selection list
337        Collection<Command> commandList = new ArrayList<>(wayChunks.size());
338        List<OsmPrimitive> newSelection = new ArrayList<>(selection.size() + wayChunks.size());
339        newSelection.addAll(selection);
340
341        Iterator<List<Node>> chunkIt = wayChunks.iterator();
342        Collection<String> nowarnroles = Main.pref.getCollection("way.split.roles.nowarn",
343                Arrays.asList("outer", "inner", "forward", "backward", "north", "south", "east", "west"));
344
345        // First, change the original way
346        Way changedWay = new Way(way);
347        changedWay.setNodes(chunkIt.next());
348        commandList.add(new ChangeCommand(way, changedWay));
349        if (!newSelection.contains(way)) {
350            newSelection.add(way);
351        }
352
353        List<Way> newWays = new ArrayList<>();
354        // Second, create new ways
355        while (chunkIt.hasNext()) {
356            Way wayToAdd = new Way();
357            wayToAdd.setKeys(way.getKeys());
358            newWays.add(wayToAdd);
359            wayToAdd.setNodes(chunkIt.next());
360            commandList.add(new AddCommand(layer, wayToAdd));
361            newSelection.add(wayToAdd);
362        }
363        boolean warnmerole = false;
364        boolean warnme = false;
365        // now copy all relations to new way also
366
367        for (Relation r : OsmPrimitive.getFilteredList(way.getReferrers(), Relation.class)) {
368            if (!r.isUsable()) {
369                continue;
370            }
371            Relation c = null;
372            String type = r.get("type");
373            if (type == null) {
374                type = "";
375            }
376
377            int i_c = 0, i_r = 0;
378            List<RelationMember> relationMembers = r.getMembers();
379            for (RelationMember rm: relationMembers) {
380                if (rm.isWay() && rm.getMember() == way) {
381                    boolean insert = true;
382                    if ("restriction".equals(type)) {
383                        /* this code assumes the restriction is correct. No real error checking done */
384                        String role = rm.getRole();
385                        if("from".equals(role) || "to".equals(role)) {
386                            OsmPrimitive via = null;
387                            for (RelationMember rmv : r.getMembers()) {
388                                if ("via".equals(rmv.getRole())){
389                                    via = rmv.getMember();
390                                }
391                            }
392                            List<Node> nodes = new ArrayList<>();
393                            if (via != null) {
394                                if (via instanceof Node) {
395                                    nodes.add((Node)via);
396                                } else if (via instanceof Way) {
397                                    nodes.add(((Way)via).lastNode());
398                                    nodes.add(((Way)via).firstNode());
399                                }
400                            }
401                            Way res = null;
402                            for (Node n : nodes) {
403                                if(changedWay.isFirstLastNode(n)) {
404                                    res = way;
405                                }
406                            }
407                            if (res == null) {
408                                for (Way wayToAdd : newWays) {
409                                    for(Node n : nodes) {
410                                        if(wayToAdd.isFirstLastNode(n)) {
411                                            res = wayToAdd;
412                                        }
413                                    }
414                                }
415                                if (res != null) {
416                                    if (c == null) {
417                                        c = new Relation(r);
418                                    }
419                                    c.addMember(new RelationMember(role, res));
420                                    c.removeMembersFor(way);
421                                    insert = false;
422                                }
423                            } else {
424                                insert = false;
425                            }
426                        } else if(!"via".equals(role)) {
427                            warnme = true;
428                        }
429                    } else if (!("route".equals(type)) && !("multipolygon".equals(type))) {
430                        warnme = true;
431                    }
432                    if (c == null) {
433                        c = new Relation(r);
434                    }
435
436                    if (insert) {
437                        if (rm.hasRole() && !nowarnroles.contains(rm.getRole())) {
438                            warnmerole = true;
439                        }
440
441                        Boolean backwards = null;
442                        int k = 1;
443                        while (i_r - k >= 0 || i_r + k < relationMembers.size()) {
444                            if ((i_r - k >= 0) && relationMembers.get(i_r - k).isWay()){
445                                Way w = relationMembers.get(i_r - k).getWay();
446                                if ((w.lastNode() == way.firstNode()) || w.firstNode() == way.firstNode()) {
447                                    backwards = false;
448                                } else if ((w.firstNode() == way.lastNode()) || w.lastNode() == way.lastNode()) {
449                                    backwards = true;
450                                }
451                                break;
452                            }
453                            if ((i_r + k < relationMembers.size()) && relationMembers.get(i_r + k).isWay()){
454                                Way w = relationMembers.get(i_r + k).getWay();
455                                if ((w.lastNode() == way.firstNode()) || w.firstNode() == way.firstNode()) {
456                                    backwards = true;
457                                } else if ((w.firstNode() == way.lastNode()) || w.lastNode() == way.lastNode()) {
458                                    backwards = false;
459                                }
460                                break;
461                            }
462                            k++;
463                        }
464
465                        int j = i_c;
466                        for (Way wayToAdd : newWays) {
467                            RelationMember em = new RelationMember(rm.getRole(), wayToAdd);
468                            j++;
469                            if ((backwards != null) && backwards) {
470                                c.addMember(i_c, em);
471                            } else {
472                                c.addMember(j, em);
473                            }
474                        }
475                        i_c = j;
476                    }
477                }
478                i_c++;
479                i_r++;
480            }
481
482            if (c != null) {
483                commandList.add(new ChangeCommand(layer, r, c));
484            }
485        }
486        if (warnmerole) {
487            new Notification(
488                    tr("A role based relation membership was copied to all new ways.<br>You should verify this and correct it when necessary."))
489                    .setIcon(JOptionPane.WARNING_MESSAGE)
490                    .show();
491        } else if (warnme) {
492            new Notification(
493                    tr("A relation membership was copied to all new ways.<br>You should verify this and correct it when necessary."))
494                    .setIcon(JOptionPane.WARNING_MESSAGE)
495                    .show();
496        }
497
498        return new SplitWayResult(
499                new SequenceCommand(
500                        /* for correct i18n of plural forms - see #9110 */
501                        trn("Split way {0} into {1} part", "Split way {0} into {1} parts", wayChunks.size(),
502                                way.getDisplayName(DefaultNameFormatter.getInstance()), wayChunks.size()),
503                        commandList
504                        ),
505                        newSelection,
506                        way,
507                        newWays
508                );
509    }
510
511    /**
512     * Splits the way {@code way} at the nodes in {@code atNodes} and replies
513     * the result of this process in an instance of {@link SplitWayResult}.
514     *
515     * Note that changes are not applied to the data yet. You have to
516     * submit the command in {@link SplitWayResult#getCommand()} first,
517     * i.e. {@code Main.main.undoredo.add(result.getCommand())}.
518     *
519     * Replies null if the way couldn't be split at the given nodes.
520     *
521     * @param layer the layer which the way belongs to. Must not be null.
522     * @param way the way to split. Must not be null.
523     * @param atNodes the list of nodes where the way is split. Must not be null.
524     * @param selection The list of currently selected primitives
525     * @return the result from the split operation
526     */
527    public static SplitWayResult split(OsmDataLayer layer, Way way, List<Node> atNodes, Collection<? extends OsmPrimitive> selection) {
528        List<List<Node>> chunks = buildSplitChunks(way, atNodes);
529        if (chunks == null) return null;
530        return splitWay(layer,way, chunks, selection);
531    }
532
533    @Override
534    protected void updateEnabledState() {
535        if (getCurrentDataSet() == null) {
536            setEnabled(false);
537        } else {
538            updateEnabledState(getCurrentDataSet().getSelected());
539        }
540    }
541
542    @Override
543    protected void updateEnabledState(Collection<? extends OsmPrimitive> selection) {
544        if (selection == null) {
545            setEnabled(false);
546            return;
547        }
548        for (OsmPrimitive primitive: selection) {
549            if (primitive instanceof Node) {
550                setEnabled(true); // Selection still can be wrong, but let SplitWayAction process and tell user what's wrong
551                return;
552            }
553        }
554        setEnabled(false);
555    }
556}