source: josm/trunk/src/org/openstreetmap/josm/actions/CombineWayAction.java @ 5240

Revision 5132, 20.8 KB checked in by simon04, 7 weeks ago (diff)

fix #7513 - Warn non-experts when combining ways with conflicting tags or ways being part of relations

  • 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;
6
7import java.awt.event.ActionEvent;
8import java.awt.event.KeyEvent;
9import java.util.ArrayList;
10import java.util.Collection;
11import java.util.Collections;
12import java.util.HashMap;
13import java.util.HashSet;
14import java.util.LinkedHashMap;
15import java.util.LinkedHashSet;
16import java.util.LinkedList;
17import java.util.List;
18import java.util.Set;
19import java.util.Stack;
20
21import javax.swing.JOptionPane;
22import javax.swing.SwingUtilities;
23
24import org.openstreetmap.josm.Main;
25import org.openstreetmap.josm.command.ChangeCommand;
26import org.openstreetmap.josm.command.Command;
27import org.openstreetmap.josm.command.DeleteCommand;
28import org.openstreetmap.josm.command.SequenceCommand;
29import org.openstreetmap.josm.corrector.ReverseWayTagCorrector;
30import org.openstreetmap.josm.corrector.UserCancelException;
31import org.openstreetmap.josm.data.osm.Node;
32import org.openstreetmap.josm.data.osm.OsmPrimitive;
33import org.openstreetmap.josm.data.osm.TagCollection;
34import org.openstreetmap.josm.data.osm.Way;
35import org.openstreetmap.josm.data.preferences.BooleanProperty;
36import org.openstreetmap.josm.gui.ExtendedDialog;
37import org.openstreetmap.josm.gui.conflict.tags.CombinePrimitiveResolverDialog;
38import org.openstreetmap.josm.tools.Pair;
39import org.openstreetmap.josm.tools.Shortcut;
40
41/**
42 * Combines multiple ways into one.
43 *
44 */
45public class CombineWayAction extends JosmAction {
46
47    private static final BooleanProperty PROP_REVERSE_WAY = new BooleanProperty("tag-correction.reverse-way", true);
48
49    public CombineWayAction() {
50        super(tr("Combine Way"), "combineway", tr("Combine several ways into one."),
51                Shortcut.registerShortcut("tools:combineway", tr("Tool: {0}", tr("Combine Way")), KeyEvent.VK_C, Shortcut.DIRECT), true);
52        putValue("help", ht("/Action/CombineWay"));
53    }
54
55    protected static boolean confirmChangeDirectionOfWays() {
56        ExtendedDialog ed = new ExtendedDialog(Main.parent,
57                tr("Change directions?"),
58                new String[] {tr("Reverse and Combine"), tr("Cancel")});
59        ed.setButtonIcons(new String[] {"wayflip.png", "cancel.png"});
60        ed.setContent(tr("The ways can not be combined in their current directions.  "
61                + "Do you want to reverse some of them?"));
62        ed.showDialog();
63        return ed.getValue() == 1;
64    }
65
66    protected static void warnCombiningImpossible() {
67        String msg = tr("Could not combine ways "
68                + "(They could not be merged into a single string of nodes)");
69        JOptionPane.showMessageDialog(
70                Main.parent,
71                msg,
72                tr("Information"),
73                JOptionPane.INFORMATION_MESSAGE
74        );
75        return;
76    }
77
78    protected static Way getTargetWay(Collection<Way> combinedWays) {
79        // init with an arbitrary way
80        Way targetWay = combinedWays.iterator().next();
81
82        // look for the first way already existing on
83        // the server
84        for (Way w : combinedWays) {
85            targetWay = w;
86            if (!w.isNew()) {
87                break;
88            }
89        }
90        return targetWay;
91    }
92
93    /**
94     * @param ways
95     * @return null if ways cannot be combined. Otherwise returns the combined
96     *              ways and the commands to combine
97     * @throws UserCancelException
98     */
99    public static Pair<Way, Command> combineWaysWorker(Collection<Way> ways) throws UserCancelException {
100
101        // prepare and clean the list of ways to combine
102        //
103        if (ways == null || ways.isEmpty())
104            return null;
105        ways.remove(null); // just in case -  remove all null ways from the collection
106
107        // remove duplicates, preserving order
108        ways = new LinkedHashSet<Way>(ways);
109
110        // try to build a new way which includes all the combined
111        // ways
112        //
113        NodeGraph graph = NodeGraph.createUndirectedGraphFromNodeWays(ways);
114        List<Node> path = graph.buildSpanningPath();
115        if (path == null) {
116            warnCombiningImpossible();
117            return null;
118        }
119        // check whether any ways have been reversed in the process
120        // and build the collection of tags used by the ways to combine
121        //
122        TagCollection wayTags = TagCollection.unionOfAllPrimitives(ways);
123
124        List<Way> reversedWays = new LinkedList<Way>();
125        List<Way> unreversedWays = new LinkedList<Way>();
126        for (Way w: ways) {
127            if ((path.indexOf(w.getNode(0)) + 1) == path.lastIndexOf(w.getNode(1))) {
128                unreversedWays.add(w);
129            } else {
130                reversedWays.add(w);
131            }
132        }
133        // reverse path if all ways have been reversed
134        if (unreversedWays.isEmpty()) {
135            Collections.reverse(path);
136            unreversedWays = reversedWays;
137            reversedWays = null;
138        }
139        if ((reversedWays != null) && !reversedWays.isEmpty()) {
140            if (!confirmChangeDirectionOfWays()) return null;
141            // filter out ways that have no direction-dependent tags
142            unreversedWays = ReverseWayTagCorrector.irreversibleWays(unreversedWays);
143            reversedWays = ReverseWayTagCorrector.irreversibleWays(reversedWays);
144            // reverse path if there are more reversed than unreversed ways with direction-dependent tags
145            if (reversedWays.size() > unreversedWays.size()) {
146                Collections.reverse(path);
147                List<Way> tempWays = unreversedWays;
148                unreversedWays = reversedWays;
149                reversedWays = tempWays;
150            }
151            // if there are still reversed ways with direction-dependent tags, reverse their tags
152            if (!reversedWays.isEmpty() && PROP_REVERSE_WAY.get()) {
153                List<Way> unreversedTagWays = new ArrayList<Way>(ways);
154                unreversedTagWays.removeAll(reversedWays);
155                ReverseWayTagCorrector reverseWayTagCorrector = new ReverseWayTagCorrector();
156                List<Way> reversedTagWays = new ArrayList<Way>();
157                Collection<Command> changePropertyCommands =  null;
158                for (Way w : reversedWays) {
159                    Way wnew = new Way(w);
160                    reversedTagWays.add(wnew);
161                    changePropertyCommands = reverseWayTagCorrector.execute(w, wnew);
162                }
163                if ((changePropertyCommands != null) && !changePropertyCommands.isEmpty()) {
164                    for (Command c : changePropertyCommands) {
165                        c.executeCommand();
166                    }
167                }
168                wayTags = TagCollection.unionOfAllPrimitives(reversedTagWays);
169                wayTags.add(TagCollection.unionOfAllPrimitives(unreversedTagWays));
170            }
171        }
172
173        // create the new way and apply the new node list
174        //
175        Way targetWay = getTargetWay(ways);
176        Way modifiedTargetWay = new Way(targetWay);
177        modifiedTargetWay.setNodes(path);
178
179        List<Command> resolution = CombinePrimitiveResolverDialog.launchIfNecessary(wayTags, ways, Collections.singleton(targetWay));
180
181        LinkedList<Command> cmds = new LinkedList<Command>();
182        LinkedList<Way> deletedWays = new LinkedList<Way>(ways);
183        deletedWays.remove(targetWay);
184
185        cmds.add(new ChangeCommand(targetWay, modifiedTargetWay));
186        cmds.addAll(resolution);
187        cmds.add(new DeleteCommand(deletedWays));
188        final SequenceCommand sequenceCommand = new SequenceCommand(tr("Combine {0} ways", ways.size()), cmds);
189
190        return new Pair<Way, Command>(targetWay, sequenceCommand);
191    }
192
193    public void actionPerformed(ActionEvent event) {
194        if (getCurrentDataSet() == null)
195            return;
196        Collection<OsmPrimitive> selection = getCurrentDataSet().getSelected();
197        Set<Way> selectedWays = OsmPrimitive.getFilteredSet(selection, Way.class);
198        if (selectedWays.size() < 2) {
199            JOptionPane.showMessageDialog(
200                    Main.parent,
201                    tr("Please select at least two ways to combine."),
202                    tr("Information"),
203                    JOptionPane.INFORMATION_MESSAGE
204            );
205            return;
206        }
207        // combine and update gui
208        Pair<Way, Command> combineResult;
209        try {
210            combineResult = combineWaysWorker(selectedWays);
211        } catch (UserCancelException ex) {
212            return;
213        }
214
215        if (combineResult == null)
216            return;
217        final Way selectedWay = combineResult.a;
218        Main.main.undoRedo.add(combineResult.b);
219        if(selectedWay != null)
220        {
221            Runnable guiTask = new Runnable() {
222                public void run() {
223                    getCurrentDataSet().setSelected(selectedWay);
224                }
225            };
226            if (SwingUtilities.isEventDispatchThread()) {
227                guiTask.run();
228            } else {
229                SwingUtilities.invokeLater(guiTask);
230            }
231        }
232    }
233
234    @Override
235    protected void updateEnabledState() {
236        if (getCurrentDataSet() == null) {
237            setEnabled(false);
238            return;
239        }
240        Collection<OsmPrimitive> selection = getCurrentDataSet().getSelected();
241        updateEnabledState(selection);
242    }
243
244    @Override
245    protected void updateEnabledState(Collection<? extends OsmPrimitive> selection) {
246        int numWays = 0;
247        for (OsmPrimitive osm : selection)
248            if (osm instanceof Way) {
249                numWays++;
250            }
251        setEnabled(numWays >= 2);
252    }
253
254    static public class NodePair {
255        private Node a;
256        private Node b;
257        public NodePair(Node a, Node b) {
258            this.a =a;
259            this.b = b;
260        }
261
262        public NodePair(Pair<Node,Node> pair) {
263            this.a = pair.a;
264            this.b = pair.b;
265        }
266
267        public NodePair(NodePair other) {
268            this.a = other.a;
269            this.b = other.b;
270        }
271
272        public Node getA() {
273            return a;
274        }
275
276        public Node getB() {
277            return b;
278        }
279
280        public boolean isAdjacentToA(NodePair other) {
281            return other.getA() == a || other.getB() == a;
282        }
283
284        public boolean isAdjacentToB(NodePair other) {
285            return other.getA() == b || other.getB() == b;
286        }
287
288        public boolean isSuccessorOf(NodePair other) {
289            return other.getB() == a;
290        }
291
292        public boolean isPredecessorOf(NodePair other) {
293            return b == other.getA();
294        }
295
296        public NodePair swap() {
297            return new NodePair(b,a);
298        }
299
300        @Override
301        public String toString() {
302            return new StringBuilder()
303            .append("[")
304            .append(a.getId())
305            .append(",")
306            .append(b.getId())
307            .append("]")
308            .toString();
309        }
310
311        public boolean contains(Node n) {
312            return a == n || b == n;
313        }
314
315        @Override
316        public int hashCode() {
317            final int prime = 31;
318            int result = 1;
319            result = prime * result + ((a == null) ? 0 : a.hashCode());
320            result = prime * result + ((b == null) ? 0 : b.hashCode());
321            return result;
322        }
323        @Override
324        public boolean equals(Object obj) {
325            if (this == obj)
326                return true;
327            if (obj == null)
328                return false;
329            if (getClass() != obj.getClass())
330                return false;
331            NodePair other = (NodePair) obj;
332            if (a == null) {
333                if (other.a != null)
334                    return false;
335            } else if (!a.equals(other.a))
336                return false;
337            if (b == null) {
338                if (other.b != null)
339                    return false;
340            } else if (!b.equals(other.b))
341                return false;
342            return true;
343        }
344    }
345
346    static public class NodeGraph {
347        static public List<NodePair> buildNodePairs(Way way, boolean directed) {
348            ArrayList<NodePair> pairs = new ArrayList<NodePair>();
349            for (Pair<Node,Node> pair: way.getNodePairs(false /* don't sort */)) {
350                pairs.add(new NodePair(pair));
351                if (!directed) {
352                    pairs.add(new NodePair(pair).swap());
353                }
354            }
355            return pairs;
356        }
357
358        static public List<NodePair> buildNodePairs(List<Way> ways, boolean directed) {
359            ArrayList<NodePair> pairs = new ArrayList<NodePair>();
360            for (Way w: ways) {
361                pairs.addAll(buildNodePairs(w, directed));
362            }
363            return pairs;
364        }
365
366        static public List<NodePair> eliminateDuplicateNodePairs(List<NodePair> pairs) {
367            ArrayList<NodePair> cleaned = new ArrayList<NodePair>();
368            for(NodePair p: pairs) {
369                if (!cleaned.contains(p) && !cleaned.contains(p.swap())) {
370                    cleaned.add(p);
371                }
372            }
373            return cleaned;
374        }
375
376        static public NodeGraph createDirectedGraphFromNodePairs(List<NodePair> pairs) {
377            NodeGraph graph = new NodeGraph();
378            for (NodePair pair: pairs) {
379                graph.add(pair);
380            }
381            return graph;
382        }
383
384        static public NodeGraph createDirectedGraphFromWays(Collection<Way> ways) {
385            NodeGraph graph = new NodeGraph();
386            for (Way w: ways) {
387                graph.add(buildNodePairs(w, true /* directed */));
388            }
389            return graph;
390        }
391
392        static public NodeGraph createUndirectedGraphFromNodeList(List<NodePair> pairs) {
393            NodeGraph graph = new NodeGraph();
394            for (NodePair pair: pairs) {
395                graph.add(pair);
396                graph.add(pair.swap());
397            }
398            return graph;
399        }
400
401        static public NodeGraph createUndirectedGraphFromNodeWays(Collection<Way> ways) {
402            NodeGraph graph = new NodeGraph();
403            for (Way w: ways) {
404                graph.add(buildNodePairs(w, false /* undirected */));
405            }
406            return graph;
407        }
408
409        private Set<NodePair> edges;
410        private int numUndirectedEges = 0;
411        private HashMap<Node, List<NodePair>> successors;
412        private HashMap<Node, List<NodePair>> predecessors;
413
414        protected void rememberSuccessor(NodePair pair) {
415            if (successors.containsKey(pair.getA())) {
416                if (!successors.get(pair.getA()).contains(pair)) {
417                    successors.get(pair.getA()).add(pair);
418                }
419            } else {
420                ArrayList<NodePair> l = new ArrayList<NodePair>();
421                l.add(pair);
422                successors.put(pair.getA(), l);
423            }
424        }
425
426        protected void rememberPredecessors(NodePair pair) {
427            if (predecessors.containsKey(pair.getB())) {
428                if (!predecessors.get(pair.getB()).contains(pair)) {
429                    predecessors.get(pair.getB()).add(pair);
430                }
431            } else {
432                ArrayList<NodePair> l = new ArrayList<NodePair>();
433                l.add(pair);
434                predecessors.put(pair.getB(), l);
435            }
436        }
437
438        protected boolean isTerminalNode(Node n) {
439            if (successors.get(n) == null) return false;
440            if (successors.get(n).size() != 1) return false;
441            if (predecessors.get(n) == null) return true;
442            if (predecessors.get(n).size() == 1) {
443                NodePair p1 = successors.get(n).iterator().next();
444                NodePair p2 = predecessors.get(n).iterator().next();
445                return p1.equals(p2.swap());
446            }
447            return false;
448        }
449
450        protected void prepare() {
451            Set<NodePair> undirectedEdges = new LinkedHashSet<NodePair>();
452            successors = new LinkedHashMap<Node, List<NodePair>>();
453            predecessors = new LinkedHashMap<Node, List<NodePair>>();
454
455            for (NodePair pair: edges) {
456                if (!undirectedEdges.contains(pair) && ! undirectedEdges.contains(pair.swap())) {
457                    undirectedEdges.add(pair);
458                }
459                rememberSuccessor(pair);
460                rememberPredecessors(pair);
461            }
462            numUndirectedEges = undirectedEdges.size();
463        }
464
465        public NodeGraph() {
466            edges = new LinkedHashSet<NodePair>();
467        }
468
469        public void add(NodePair pair) {
470            if (!edges.contains(pair)) {
471                edges.add(pair);
472            }
473        }
474
475        public void add(List<NodePair> pairs) {
476            for (NodePair pair: pairs) {
477                add(pair);
478            }
479        }
480
481        protected Node getStartNode() {
482            Set<Node> nodes = getNodes();
483            for (Node n: nodes) {
484                if (successors.get(n) != null && successors.get(n).size() ==1)
485                    return n;
486            }
487            return null;
488        }
489
490        protected Set<Node> getTerminalNodes() {
491            Set<Node> ret = new LinkedHashSet<Node>();
492            for (Node n: getNodes()) {
493                if (isTerminalNode(n)) {
494                    ret.add(n);
495                }
496            }
497            return ret;
498        }
499
500        protected Set<Node> getNodes(Stack<NodePair> pairs) {
501            HashSet<Node> nodes = new LinkedHashSet<Node>();
502            for (NodePair pair: pairs) {
503                nodes.add(pair.getA());
504                nodes.add(pair.getB());
505            }
506            return nodes;
507        }
508
509        protected List<NodePair> getOutboundPairs(NodePair pair) {
510            return getOutboundPairs(pair.getB());
511        }
512
513        protected List<NodePair> getOutboundPairs(Node node) {
514            List<NodePair> l = successors.get(node);
515            if (l == null)
516                return Collections.emptyList();
517            return l;
518        }
519
520        protected Set<Node> getNodes() {
521            Set<Node> nodes = new LinkedHashSet<Node>();
522            for (NodePair pair: edges) {
523                nodes.add(pair.getA());
524                nodes.add(pair.getB());
525            }
526            return nodes;
527        }
528
529        protected boolean isSpanningWay(Stack<NodePair> way) {
530            return numUndirectedEges == way.size();
531        }
532
533        protected List<Node> buildPathFromNodePairs(Stack<NodePair> path) {
534            LinkedList<Node> ret = new LinkedList<Node>();
535            for (NodePair pair: path) {
536                ret.add(pair.getA());
537            }
538            ret.add(path.peek().getB());
539            return ret;
540        }
541
542        /**
543         * Tries to find a spanning path starting from node <code>startNode</code>.
544         *
545         * Traverses the path in depth-first order.
546         *
547         * @param startNode the start node
548         * @return the spanning path; null, if no path is found
549         */
550        protected List<Node> buildSpanningPath(Node startNode) {
551            if (startNode == null)
552                return null;
553            Stack<NodePair> path = new Stack<NodePair>();
554            Stack<NodePair> nextPairs  = new Stack<NodePair>();
555            nextPairs.addAll(getOutboundPairs(startNode));
556            while(!nextPairs.isEmpty()) {
557                NodePair cur= nextPairs.pop();
558                if (! path.contains(cur) && ! path.contains(cur.swap())) {
559                    while(!path.isEmpty() && !path.peek().isPredecessorOf(cur)) {
560                        path.pop();
561                    }
562                    path.push(cur);
563                    if (isSpanningWay(path)) return buildPathFromNodePairs(path);
564                    nextPairs.addAll(getOutboundPairs(path.peek()));
565                }
566            }
567            return null;
568        }
569
570        /**
571         * Tries to find a path through the graph which visits each edge (i.e.
572         * the segment of a way) exactly one.
573         *
574         * @return the path; null, if no path was found
575         */
576        public List<Node> buildSpanningPath() {
577            prepare();
578            // try to find a path from each "terminal node", i.e. from a
579            // node which is connected by exactly one undirected edges (or
580            // two directed edges in opposite direction) to the graph. A
581            // graph built up from way segments is likely to include such
582            // nodes, unless all ways are closed.
583            // In the worst case this loops over all nodes which is
584            // very slow for large ways.
585            //
586            Set<Node> nodes = getTerminalNodes();
587            nodes = nodes.isEmpty() ? getNodes() : nodes;
588            for (Node n: nodes) {
589                List<Node> path = buildSpanningPath(n);
590                if (path != null)
591                    return path;
592            }
593            return null;
594        }
595    }
596}
Note: See TracBrowser for help on using the repository browser.