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