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<OsmPrimitive>(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<Way>(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    static public 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<Node>(splitPoints);
266        List<List<Node>> wayChunks = new LinkedList<List<Node>>();
267        List<Node> currentWayChunk = new ArrayList<Node>();
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<Node>();
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<Command>(wayChunks.size());
338        List<OsmPrimitive> newSelection = new ArrayList<OsmPrimitive>(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<Way>();
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        }
364        boolean warnmerole = false;
365        boolean warnme = false;
366        // now copy all relations to new way also
367
368        for (Relation r : OsmPrimitive.getFilteredList(way.getReferrers(), Relation.class)) {
369            if (!r.isUsable()) {
370                continue;
371            }
372            Relation c = null;
373            String type = r.get("type");
374            if (type == null) {
375                type = "";
376            }
377
378            int i_c = 0, i_r = 0;
379            List<RelationMember> relationMembers = r.getMembers();
380            for (RelationMember rm: relationMembers) {
381                if (rm.isWay() && rm.getMember() == way) {
382                    boolean insert = true;
383                    if ("restriction".equals(type))
384                    {
385                        /* this code assumes the restriction is correct. No real error checking done */
386                        String role = rm.getRole();
387                        if("from".equals(role) || "to".equals(role))
388                        {
389                            OsmPrimitive via = null;
390                            for (RelationMember rmv : r.getMembers()) {
391                                if("via".equals(rmv.getRole())){
392                                    via = rmv.getMember();
393                                }
394                            }
395                            List<Node> nodes = new ArrayList<Node>();
396                            if(via != null) {
397                                if(via instanceof Node) {
398                                    nodes.add((Node)via);
399                                } else if(via instanceof Way) {
400                                    nodes.add(((Way)via).lastNode());
401                                    nodes.add(((Way)via).firstNode());
402                                }
403                            }
404                            Way res = null;
405                            for(Node n : nodes) {
406                                if(changedWay.isFirstLastNode(n)) {
407                                    res = way;
408                                }
409                            }
410                            if(res == null)
411                            {
412                                for (Way wayToAdd : newWays) {
413                                    for(Node n : nodes) {
414                                        if(wayToAdd.isFirstLastNode(n)) {
415                                            res = wayToAdd;
416                                        }
417                                    }
418                                }
419                                if(res != null)
420                                {
421                                    if (c == null) {
422                                        c = new Relation(r);
423                                    }
424                                    c.addMember(new RelationMember(role, res));
425                                    c.removeMembersFor(way);
426                                    insert = false;
427                                }
428                            } else {
429                                insert = false;
430                            }
431                        }
432                        else if(!"via".equals(role)) {
433                            warnme = true;
434                        }
435                    }
436                    else if (!("route".equals(type)) && !("multipolygon".equals(type))) {
437                        warnme = true;
438                    }
439                    if (c == null) {
440                        c = new Relation(r);
441                    }
442
443                    if(insert)
444                    {
445                        if (rm.hasRole() && !nowarnroles.contains(rm.getRole())) {
446                            warnmerole = true;
447                        }
448
449                        Boolean backwards = null;
450                        int k = 1;
451                        while (i_r - k >= 0 || i_r + k < relationMembers.size()) {
452                            if ((i_r - k >= 0) && relationMembers.get(i_r - k).isWay()){
453                                Way w = relationMembers.get(i_r - k).getWay();
454                                if ((w.lastNode() == way.firstNode()) || w.firstNode() == way.firstNode()) {
455                                    backwards = false;
456                                } else if ((w.firstNode() == way.lastNode()) || w.lastNode() == way.lastNode()) {
457                                    backwards = true;
458                                }
459                                break;
460                            }
461                            if ((i_r + k < relationMembers.size()) && relationMembers.get(i_r + k).isWay()){
462                                Way w = relationMembers.get(i_r + k).getWay();
463                                if ((w.lastNode() == way.firstNode()) || w.firstNode() == way.firstNode()) {
464                                    backwards = true;
465                                } else if ((w.firstNode() == way.lastNode()) || w.lastNode() == way.lastNode()) {
466                                    backwards = false;
467                                }
468                                break;
469                            }
470                            k++;
471                        }
472
473                        int j = i_c;
474                        for (Way wayToAdd : newWays) {
475                            RelationMember em = new RelationMember(rm.getRole(), wayToAdd);
476                            j++;
477                            if ((backwards != null) && backwards) {
478                                c.addMember(i_c, em);
479                            } else {
480                                c.addMember(j, em);
481                            }
482                        }
483                        i_c = j;
484                    }
485                }
486                i_c++; i_r++;
487            }
488
489            if (c != null) {
490                commandList.add(new ChangeCommand(layer,r, c));
491            }
492        }
493        if (warnmerole) {
494            new Notification(
495                    tr("A role based relation membership was copied to all new ways.<br>You should verify this and correct it when necessary."))
496                    .setIcon(JOptionPane.WARNING_MESSAGE)
497                    .show();
498        } else if (warnme) {
499            new Notification(
500                    tr("A relation membership was copied to all new ways.<br>You should verify this and correct it when necessary."))
501                    .setIcon(JOptionPane.WARNING_MESSAGE)
502                    .show();
503        }
504
505        return new SplitWayResult(
506                new SequenceCommand(
507                        tr("Split way {0} into {1} parts", way.getDisplayName(DefaultNameFormatter.getInstance()),wayChunks.size()),
508                        commandList
509                        ),
510                        newSelection,
511                        way,
512                        newWays
513                );
514    }
515
516    /**
517     * Splits the way {@code way} at the nodes in {@code atNodes} and replies
518     * the result of this process in an instance of {@link SplitWayResult}.
519     *
520     * Note that changes are not applied to the data yet. You have to
521     * submit the command in {@link SplitWayResult#getCommand()} first,
522     * i.e. {@code Main.main.undoredo.add(result.getCommand())}.
523     *
524     * Replies null if the way couldn't be split at the given nodes.
525     *
526     * @param layer the layer which the way belongs to. Must not be null.
527     * @param way the way to split. Must not be null.
528     * @param atNodes the list of nodes where the way is split. Must not be null.
529     * @param selection The list of currently selected primitives
530     * @return the result from the split operation
531     */
532    static public SplitWayResult split(OsmDataLayer layer, Way way, List<Node> atNodes, Collection<? extends OsmPrimitive> selection) {
533        List<List<Node>> chunks = buildSplitChunks(way, atNodes);
534        if (chunks == null) return null;
535        return splitWay(layer,way, chunks, selection);
536    }
537
538    @Override
539    protected void updateEnabledState() {
540        if (getCurrentDataSet() == null) {
541            setEnabled(false);
542        } else {
543            updateEnabledState(getCurrentDataSet().getSelected());
544        }
545    }
546
547    @Override
548    protected void updateEnabledState(Collection<? extends OsmPrimitive> selection) {
549        if (selection == null) {
550            setEnabled(false);
551            return;
552        }
553        for (OsmPrimitive primitive: selection) {
554            if (primitive instanceof Node) {
555                setEnabled(true); // Selection still can be wrong, but let SplitWayAction process and tell user what's wrong
556                return;
557            }
558        }
559        setEnabled(false);
560    }
561}