source: josm/trunk/src/org/openstreetmap/josm/actions/SplitWayAction.java @ 5241

Revision 5041, 20.7 KB checked in by simon04, 3 months ago (diff)

fix #6601, fix #6667 - SplitWayAction: improve warning prevention - don't warn for roles north/south/east/west

  • Property svn:eol-style set to native
Line 
1// License: GPL. Copyright 2007 by Immanuel Scholz and others
2package org.openstreetmap.josm.actions;
3
4import static org.openstreetmap.josm.gui.help.HelpUtil.ht;
5import static org.openstreetmap.josm.tools.I18n.tr;
6import static org.openstreetmap.josm.tools.I18n.trn;
7
8import java.awt.event.ActionEvent;
9import java.awt.event.KeyEvent;
10import java.util.ArrayList;
11import java.util.Arrays;
12import java.util.Collection;
13import java.util.HashSet;
14import java.util.Iterator;
15import java.util.LinkedList;
16import java.util.List;
17import java.util.Set;
18
19import javax.swing.JOptionPane;
20
21import org.openstreetmap.josm.Main;
22import org.openstreetmap.josm.command.AddCommand;
23import org.openstreetmap.josm.command.ChangeCommand;
24import org.openstreetmap.josm.command.Command;
25import org.openstreetmap.josm.command.SequenceCommand;
26import org.openstreetmap.josm.data.osm.Node;
27import org.openstreetmap.josm.data.osm.OsmPrimitive;
28import org.openstreetmap.josm.data.osm.PrimitiveId;
29import org.openstreetmap.josm.data.osm.Relation;
30import org.openstreetmap.josm.data.osm.RelationMember;
31import org.openstreetmap.josm.data.osm.Way;
32import org.openstreetmap.josm.gui.DefaultNameFormatter;
33import org.openstreetmap.josm.gui.layer.OsmDataLayer;
34import org.openstreetmap.josm.tools.CheckParameterUtil;
35import org.openstreetmap.josm.tools.Shortcut;
36
37/**
38 * Splits a way into multiple ways (all identical except for their node list).
39 *
40 * Ways are just split at the selected nodes.  The nodes remain in their
41 * original order.  Selected nodes at the end of a way are ignored.
42 */
43
44public class SplitWayAction extends JosmAction {
45
46
47    public static class SplitWayResult {
48        private final Command command;
49        private final List<? extends PrimitiveId> newSelection;
50        private Way originalWay;
51        private List<Way> newWays;
52
53        public SplitWayResult(Command command, List<? extends PrimitiveId> newSelection, Way originalWay, List<Way> newWays) {
54            this.command = command;
55            this.newSelection = newSelection;
56            this.originalWay = originalWay;
57            this.newWays = newWays;
58        }
59
60        public Command getCommand() {
61            return command;
62        }
63
64        public List<? extends PrimitiveId> getNewSelection() {
65            return newSelection;
66        }
67
68        public Way getOriginalWay() {
69            return originalWay;
70        }
71
72        public List<Way> getNewWays() {
73            return newWays;
74        }
75    }
76
77    /**
78     * Create a new SplitWayAction.
79     */
80    public SplitWayAction() {
81        super(tr("Split Way"), "splitway", tr("Split a way at the selected node."),
82                Shortcut.registerShortcut("tools:splitway", tr("Tool: {0}", tr("Split Way")), KeyEvent.VK_P, Shortcut.DIRECT), true);
83        putValue("help", ht("/Action/SplitWay"));
84    }
85
86    /**
87     * Called when the action is executed.
88     *
89     * This method performs an expensive check whether the selection clearly defines one
90     * of the split actions outlined above, and if yes, calls the splitWay method.
91     */
92    public void actionPerformed(ActionEvent e) {
93
94        Collection<OsmPrimitive> selection = getCurrentDataSet().getSelected();
95
96        List<Node> selectedNodes = OsmPrimitive.getFilteredList(selection, Node.class);
97        List<Way> selectedWays = OsmPrimitive.getFilteredList(selection, Way.class);
98        List<Relation> selectedRelations = OsmPrimitive.getFilteredList(selection, Relation.class);
99        List<Way> applicableWays = getApplicableWays(selectedWays, selectedNodes);
100
101        if (applicableWays == null) {
102            JOptionPane.showMessageDialog(
103                    Main.parent,
104                    tr("The current selection cannot be used for splitting - no node is selected."),
105                    tr("Warning"),
106                    JOptionPane.WARNING_MESSAGE);
107            return;
108        } else if (applicableWays.isEmpty()) {
109            JOptionPane.showMessageDialog(Main.parent,
110                    tr("The selected nodes do not share the same way."),
111                    tr("Warning"),
112                    JOptionPane.WARNING_MESSAGE);
113            return;
114        }
115
116        { // Remove ways that doesn't have selected node in the middle
117            Iterator<Way> it = applicableWays.iterator();
118            WAY_LOOP:
119                while (it.hasNext()) {
120                    Way w = it.next();
121                    for (Node n : selectedNodes) {
122                        if(!w.isInnerNode(n)) {
123                            it.remove();
124                            continue WAY_LOOP;
125                        }
126                    }
127                }
128        }
129
130        if (applicableWays.isEmpty()) {
131            JOptionPane.showMessageDialog(Main.parent,
132                    trn("The selected node is not in the middle of any way.",
133                            "The selected nodes are not in the middle of any way.",
134                            selectedNodes.size()),
135                            tr("Warning"),
136                            JOptionPane.WARNING_MESSAGE);
137            return;
138        } else if (applicableWays.size() > 1) {
139            JOptionPane.showMessageDialog(Main.parent,
140                    trn("There is more than one way using the node you selected. Please select the way also.",
141                            "There is more than one way using the nodes you selected. Please select the way also.",
142                            selectedNodes.size()),
143                            tr("Warning"),
144                            JOptionPane.WARNING_MESSAGE);
145            return;
146        }
147
148        // Finally, applicableWays contains only one perfect way
149        Way selectedWay = applicableWays.get(0);
150
151        List<List<Node>> wayChunks = buildSplitChunks(selectedWay, selectedNodes);
152        if (wayChunks != null) {
153            List<OsmPrimitive> sel = new ArrayList<OsmPrimitive>(selectedWays.size() + selectedRelations.size());
154            sel.addAll(selectedWays);
155            sel.addAll(selectedRelations);
156            SplitWayResult result = splitWay(getEditLayer(),selectedWay, wayChunks, sel);
157            Main.main.undoRedo.add(result.getCommand());
158            getCurrentDataSet().setSelected(result.getNewSelection());
159        }
160    }
161
162    private List<Way> getApplicableWays(List<Way> selectedWays, List<Node> selectedNodes) {
163        if (selectedNodes.isEmpty())
164            return null;
165
166        // List of ways shared by all nodes
167        List<Way> result = new ArrayList<Way>(OsmPrimitive.getFilteredList(selectedNodes.get(0).getReferrers(), Way.class));
168        for (int i=1; i<selectedNodes.size(); i++) {
169            Iterator<Way> it = result.iterator();
170            List<OsmPrimitive> ref = selectedNodes.get(i).getReferrers();
171            while (it.hasNext()) {
172                if (!ref.contains(it.next())) {
173                    it.remove();
174                }
175            }
176        }
177
178        { // Remove broken ways
179            Iterator<Way> it = result.iterator();
180            while (it.hasNext()) {
181                if (it.next().getNodesCount() <= 2) {
182                    it.remove();
183                }
184            }
185        }
186
187        if (selectedWays.isEmpty())
188            return result;
189        else {
190            // Return only selected ways
191            Iterator<Way> it = result.iterator();
192            while (it.hasNext()) {
193                if (!selectedWays.contains(it.next())) {
194                    it.remove();
195                }
196            }
197            return result;
198        }
199
200    }
201
202    /**
203     * Splits the nodes of {@code wayToSplit} into a list of node sequences
204     * which are separated at the nodes in {@code splitPoints}.
205     *
206     * This method displays warning messages if {@code wayToSplit} and/or
207     * {@code splitPoints} aren't consistent.
208     *
209     * Returns null, if building the split chunks fails.
210     *
211     * @param wayToSplit the way to split. Must not be null.
212     * @param splitPoints the nodes where the way is split. Must not be null.
213     * @return the list of chunks
214     */
215    static public List<List<Node>> buildSplitChunks(Way wayToSplit, List<Node> splitPoints){
216        CheckParameterUtil.ensureParameterNotNull(wayToSplit, "wayToSplit");
217        CheckParameterUtil.ensureParameterNotNull(splitPoints, "splitPoints");
218
219        Set<Node> nodeSet = new HashSet<Node>(splitPoints);
220        List<List<Node>> wayChunks = new LinkedList<List<Node>>();
221        List<Node> currentWayChunk = new ArrayList<Node>();
222        wayChunks.add(currentWayChunk);
223
224        Iterator<Node> it = wayToSplit.getNodes().iterator();
225        while (it.hasNext()) {
226            Node currentNode = it.next();
227            boolean atEndOfWay = currentWayChunk.isEmpty() || !it.hasNext();
228            currentWayChunk.add(currentNode);
229            if (nodeSet.contains(currentNode) && !atEndOfWay) {
230                currentWayChunk = new ArrayList<Node>();
231                currentWayChunk.add(currentNode);
232                wayChunks.add(currentWayChunk);
233            }
234        }
235
236        // Handle circular ways specially.
237        // If you split at a circular way at two nodes, you just want to split
238        // it at these points, not also at the former endpoint.
239        // So if the last node is the same first node, join the last and the
240        // first way chunk.
241        List<Node> lastWayChunk = wayChunks.get(wayChunks.size() - 1);
242        if (wayChunks.size() >= 2
243                && wayChunks.get(0).get(0) == lastWayChunk.get(lastWayChunk.size() - 1)
244                && !nodeSet.contains(wayChunks.get(0).get(0))) {
245            if (wayChunks.size() == 2) {
246                JOptionPane.showMessageDialog(
247                        Main.parent,
248                        tr("You must select two or more nodes to split a circular way."),
249                        tr("Warning"),
250                        JOptionPane.WARNING_MESSAGE);
251                return null;
252            }
253            lastWayChunk.remove(lastWayChunk.size() - 1);
254            lastWayChunk.addAll(wayChunks.get(0));
255            wayChunks.remove(wayChunks.size() - 1);
256            wayChunks.set(0, lastWayChunk);
257        }
258
259        if (wayChunks.size() < 2) {
260            if (wayChunks.get(0).get(0) == wayChunks.get(0).get(wayChunks.get(0).size() - 1)) {
261                JOptionPane.showMessageDialog(
262                        Main.parent,
263                        tr("You must select two or more nodes to split a circular way."),
264                        tr("Warning"),
265                        JOptionPane.WARNING_MESSAGE);
266            } else {
267                JOptionPane.showMessageDialog(
268                        Main.parent,
269                        tr("The way cannot be split at the selected nodes. (Hint: Select nodes in the middle of the way.)"),
270                        tr("Warning"),
271                        JOptionPane.WARNING_MESSAGE);
272            }
273            return null;
274        }
275        return wayChunks;
276    }
277
278    /**
279     * Splits a way
280     * @param layer
281     * @param way
282     * @param wayChunks
283     * @return
284     */
285    public static SplitWayResult splitWay(OsmDataLayer layer, Way way, List<List<Node>> wayChunks, Collection<? extends OsmPrimitive> selection) {
286        // build a list of commands, and also a new selection list
287        Collection<Command> commandList = new ArrayList<Command>(wayChunks.size());
288        List<OsmPrimitive> newSelection = new ArrayList<OsmPrimitive>(selection.size() + wayChunks.size());
289        newSelection.addAll(selection);
290
291        Iterator<List<Node>> chunkIt = wayChunks.iterator();
292        Collection<String> nowarnroles = Main.pref.getCollection("way.split.roles.nowarn",
293                Arrays.asList("outer", "inner", "forward", "backward", "north", "south", "east", "west"));
294
295        // First, change the original way
296        Way changedWay = new Way(way);
297        changedWay.setNodes(chunkIt.next());
298        commandList.add(new ChangeCommand(way, changedWay));
299        if (!newSelection.contains(way)) {
300            newSelection.add(way);
301        }
302
303        List<Way> newWays = new ArrayList<Way>();
304        // Second, create new ways
305        while (chunkIt.hasNext()) {
306            Way wayToAdd = new Way();
307            wayToAdd.setKeys(way.getKeys());
308            newWays.add(wayToAdd);
309            wayToAdd.setNodes(chunkIt.next());
310            commandList.add(new AddCommand(layer,wayToAdd));
311            newSelection.add(wayToAdd);
312
313        }
314        boolean warnmerole = false;
315        boolean warnme = false;
316        // now copy all relations to new way also
317
318        for (Relation r : OsmPrimitive.getFilteredList(way.getReferrers(), Relation.class)) {
319            if (!r.isUsable()) {
320                continue;
321            }
322            Relation c = null;
323            String type = r.get("type");
324            if (type == null) {
325                type = "";
326            }
327
328            int i_c = 0, i_r = 0;
329            List<RelationMember> relationMembers = r.getMembers();
330            for (RelationMember rm: relationMembers) {
331                if (rm.isWay() && rm.getMember() == way) {
332                    boolean insert = true;
333                    if ("restriction".equals(type))
334                    {
335                        /* this code assumes the restriction is correct. No real error checking done */
336                        String role = rm.getRole();
337                        if("from".equals(role) || "to".equals(role))
338                        {
339                            OsmPrimitive via = null;
340                            for (RelationMember rmv : r.getMembers()) {
341                                if("via".equals(rmv.getRole())){
342                                    via = rmv.getMember();
343                                }
344                            }
345                            List<Node> nodes = new ArrayList<Node>();
346                            if(via != null) {
347                                if(via instanceof Node) {
348                                    nodes.add((Node)via);
349                                } else if(via instanceof Way) {
350                                    nodes.add(((Way)via).lastNode());
351                                    nodes.add(((Way)via).firstNode());
352                                }
353                            }
354                            Way res = null;
355                            for(Node n : nodes) {
356                                if(changedWay.isFirstLastNode(n)) {
357                                    res = way;
358                                }
359                            }
360                            if(res == null)
361                            {
362                                for (Way wayToAdd : newWays) {
363                                    for(Node n : nodes) {
364                                        if(wayToAdd.isFirstLastNode(n)) {
365                                            res = wayToAdd;
366                                        }
367                                    }
368                                }
369                                if(res != null)
370                                {
371                                    if (c == null) {
372                                        c = new Relation(r);
373                                    }
374                                    c.addMember(new RelationMember(role, res));
375                                    c.removeMembersFor(way);
376                                    insert = false;
377                                }
378                            } else {
379                                insert = false;
380                            }
381                        }
382                        else if(!"via".equals(role)) {
383                            warnme = true;
384                        }
385                    }
386                    else if (!("route".equals(type)) && !("multipolygon".equals(type))) {
387                        warnme = true;
388                    }
389                    if (c == null) {
390                        c = new Relation(r);
391                    }
392
393                    if(insert)
394                    {
395                        if (rm.hasRole() && !nowarnroles.contains(rm.getRole())) {
396                            warnmerole = true;
397                        }
398
399                        Boolean backwards = null;
400                        int k = 1;
401                        while (i_r - k >= 0 || i_r + k < relationMembers.size()) {
402                            if ((i_r - k >= 0) && relationMembers.get(i_r - k).isWay()){
403                                Way w = relationMembers.get(i_r - k).getWay();
404                                if ((w.lastNode() == way.firstNode()) || w.firstNode() == way.firstNode()) {
405                                    backwards = false;
406                                } else if ((w.firstNode() == way.lastNode()) || w.lastNode() == way.lastNode()) {
407                                    backwards = true;
408                                }
409                                break;
410                            }
411                            if ((i_r + k < relationMembers.size()) && relationMembers.get(i_r + k).isWay()){
412                                Way w = relationMembers.get(i_r + k).getWay();
413                                if ((w.lastNode() == way.firstNode()) || w.firstNode() == way.firstNode()) {
414                                    backwards = true;
415                                } else if ((w.firstNode() == way.lastNode()) || w.lastNode() == way.lastNode()) {
416                                    backwards = false;
417                                }
418                                break;
419                            }
420                            k++;
421                        }
422
423                        int j = i_c;
424                        for (Way wayToAdd : newWays) {
425                            RelationMember em = new RelationMember(rm.getRole(), wayToAdd);
426                            j++;
427                            if ((backwards != null) && backwards) {
428                                c.addMember(i_c, em);
429                            } else {
430                                c.addMember(j, em);
431                            }
432                        }
433                        i_c = j;
434                    }
435                }
436                i_c++; i_r++;
437            }
438
439            if (c != null) {
440                commandList.add(new ChangeCommand(layer,r, c));
441            }
442        }
443        if (warnmerole) {
444            JOptionPane.showMessageDialog(
445                    Main.parent,
446                    tr("<html>A role based relation membership was copied to all new ways.<br>You should verify this and correct it when necessary.</html>"),
447                    tr("Warning"),
448                    JOptionPane.WARNING_MESSAGE);
449        } else if (warnme) {
450            JOptionPane.showMessageDialog(
451                    Main.parent,
452                    tr("<html>A relation membership was copied to all new ways.<br>You should verify this and correct it when necessary.</html>"),
453                    tr("Warning"),
454                    JOptionPane.WARNING_MESSAGE);
455        }
456
457        return new SplitWayResult(
458                new SequenceCommand(
459                        tr("Split way {0} into {1} parts", way.getDisplayName(DefaultNameFormatter.getInstance()),wayChunks.size()),
460                        commandList
461                ),
462                newSelection,
463                way,
464                newWays
465        );
466    }
467
468    /**
469     * Splits the way {@code way} at the nodes in {@code atNodes} and replies
470     * the result of this process in an instance of {@see SplitWayResult}.
471     *
472     * Note that changes are not applied to the data yet. You have to
473     * submit the command in {@see SplitWayResult#getCommand()} first,
474     * i.e. {@code Main.main.undoredo.add(result.getCommand())}.
475     *
476     * Replies null if the way couldn't be split at the given nodes.
477     *
478     * @param layer the layer which the way belongs to. Must not be null.
479     * @param way the way to split. Must not be null.
480     * @param atNodes the list of nodes where the way is split. Must not be null.
481     * @return the result from the split operation
482     */
483    static public SplitWayResult split(OsmDataLayer layer, Way way, List<Node> atNodes, Collection<? extends OsmPrimitive> selection){
484        List<List<Node>> chunks = buildSplitChunks(way, atNodes);
485        if (chunks == null) return null;
486        return splitWay(layer,way, chunks, selection);
487    }
488
489    @Override
490    protected void updateEnabledState() {
491        if (getCurrentDataSet() == null) {
492            setEnabled(false);
493        } else {
494            updateEnabledState(getCurrentDataSet().getSelected());
495        }
496    }
497
498    @Override
499    protected void updateEnabledState(Collection<? extends OsmPrimitive> selection) {
500        if (selection == null) {
501            setEnabled(false);
502            return;
503        }
504        for (OsmPrimitive primitive: selection) {
505            if (primitive instanceof Node) {
506                setEnabled(true); // Selection still can be wrong, but let SplitWayAction process and tell user what's wrong
507                return;
508            }
509        }
510        setEnabled(false);
511    }
512}
Note: See TracBrowser for help on using the repository browser.