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