001    // License: GPL. Copyright 2007 by Immanuel Scholz and others
002    package org.openstreetmap.josm.actions;
003    
004    import static org.openstreetmap.josm.tools.I18n.marktr;
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.awt.geom.Area;
011    import java.util.ArrayList;
012    import java.util.Collection;
013    import java.util.Collections;
014    import java.util.HashMap;
015    import java.util.HashSet;
016    import java.util.LinkedHashSet;
017    import java.util.LinkedList;
018    import java.util.List;
019    import java.util.Map;
020    import java.util.Set;
021    import java.util.TreeMap;
022    
023    import javax.swing.JOptionPane;
024    
025    import org.openstreetmap.josm.Main;
026    import org.openstreetmap.josm.actions.ReverseWayAction.ReverseWayResult;
027    import org.openstreetmap.josm.actions.SplitWayAction.SplitWayResult;
028    import org.openstreetmap.josm.command.AddCommand;
029    import org.openstreetmap.josm.command.ChangeCommand;
030    import org.openstreetmap.josm.command.Command;
031    import org.openstreetmap.josm.command.DeleteCommand;
032    import org.openstreetmap.josm.command.SequenceCommand;
033    import org.openstreetmap.josm.corrector.UserCancelException;
034    import org.openstreetmap.josm.data.UndoRedoHandler;
035    import org.openstreetmap.josm.data.coor.EastNorth;
036    import org.openstreetmap.josm.data.osm.DataSet;
037    import org.openstreetmap.josm.data.osm.Node;
038    import org.openstreetmap.josm.data.osm.NodePositionComparator;
039    import org.openstreetmap.josm.data.osm.OsmPrimitive;
040    import org.openstreetmap.josm.data.osm.Relation;
041    import org.openstreetmap.josm.data.osm.RelationMember;
042    import org.openstreetmap.josm.data.osm.TagCollection;
043    import org.openstreetmap.josm.data.osm.Way;
044    import org.openstreetmap.josm.gui.conflict.tags.CombinePrimitiveResolverDialog;
045    import org.openstreetmap.josm.tools.Geometry;
046    import org.openstreetmap.josm.tools.Pair;
047    import org.openstreetmap.josm.tools.Shortcut;
048    
049    
050    /**
051     * Join Areas (i.e. closed ways and multipolygons)
052     */
053    public class JoinAreasAction extends JosmAction {
054        // This will be used to commit commands and unite them into one large command sequence at the end
055        private LinkedList<Command> cmds = new LinkedList<Command>();
056        private int cmdsCount = 0;
057    
058    
059        /**
060         * This helper class describes join ares action result.
061         * @author viesturs
062         *
063         */
064        public static class JoinAreasResult {
065    
066            public boolean mergeSuccessful;
067            public boolean hasChanges;
068            public boolean hasRelationProblems;
069    
070            public List<Multipolygon> polygons;
071        }
072    
073        public static class Multipolygon {
074            public Way outerWay;
075            public List<Way> innerWays;
076    
077            public Relation relation;
078    
079            public Multipolygon(Way way) {
080                outerWay = way;
081                innerWays = new ArrayList<Way>();
082            }
083        }
084    
085        // HelperClass
086        // Saves a relation and a role an OsmPrimitve was part of until it was stripped from all relations
087        private static class RelationRole {
088            public final Relation rel;
089            public final String role;
090            public RelationRole(Relation rel, String role) {
091                this.rel = rel;
092                this.role = role;
093            }
094    
095            @Override
096            public int hashCode() {
097                return rel.hashCode();
098            }
099    
100            @Override
101            public boolean equals(Object other) {
102                if (!(other instanceof RelationRole)) return false;
103                RelationRole otherMember = (RelationRole) other;
104                return otherMember.role.equals(role) && otherMember.rel.equals(rel);
105            }
106        }
107    
108    
109        //HelperClass
110        //saves a way and the "inside" side
111        // insideToTheLeft: if true left side is "in", false -right side is "in". Left and right are determined along the orientation of way.
112        public static class WayInPolygon {
113            public final Way way;
114            public boolean insideToTheRight;
115    
116            public WayInPolygon(Way _way, boolean _insideRight) {
117                this.way = _way;
118                this.insideToTheRight = _insideRight;
119            }
120    
121            @Override
122            public int hashCode() {
123                return way.hashCode();
124            }
125    
126            @Override
127            public boolean equals(Object other) {
128                if (!(other instanceof WayInPolygon)) return false;
129                WayInPolygon otherMember = (WayInPolygon) other;
130                return otherMember.way.equals(this.way) && otherMember.insideToTheRight == this.insideToTheRight;
131            }
132        }
133    
134        /**
135         * This helper class describes a polygon, assembled from several ways.
136         * @author viesturs
137         *
138         */
139        public static class AssembledPolygon {
140            public List<WayInPolygon> ways;
141    
142            public AssembledPolygon(List<WayInPolygon> boundary) {
143                this.ways = boundary;
144            }
145    
146            public List<Node> getNodes() {
147                List<Node> nodes = new ArrayList<Node>();
148                for (WayInPolygon way : this.ways) {
149                    //do not add the last node as it will be repeated in the next way
150                    if (way.insideToTheRight) {
151                        for (int pos = 0; pos < way.way.getNodesCount() - 1; pos++) {
152                            nodes.add(way.way.getNode(pos));
153                        }
154                    }
155                    else {
156                        for (int pos = way.way.getNodesCount() - 1; pos > 0; pos--) {
157                            nodes.add(way.way.getNode(pos));
158                        }
159                    }
160                }
161    
162                return nodes;
163            }
164        }
165    
166        public static class AssembledMultipolygon {
167            public AssembledPolygon outerWay;
168            public List<AssembledPolygon> innerWays;
169    
170            public AssembledMultipolygon(AssembledPolygon way) {
171                outerWay = way;
172                innerWays = new ArrayList<AssembledPolygon>();
173            }
174        }
175    
176        /**
177         * This hepler class implements algorithm traversing trough connected ways.
178         * Assumes you are going in clockwise orientation.
179         * @author viesturs
180         *
181         */
182        private static class WayTraverser {
183    
184            private Set<WayInPolygon> availableWays;
185            private WayInPolygon lastWay;
186            private boolean lastWayReverse;
187    
188            public WayTraverser(Collection<WayInPolygon> ways) {
189    
190                availableWays = new HashSet<WayInPolygon>(ways);
191                lastWay = null;
192            }
193    
194            public void removeWays(Collection<WayInPolygon> ways) {
195                availableWays.removeAll(ways);
196            }
197    
198            public boolean hasWays() {
199                return availableWays.size() > 0;
200            }
201    
202            public WayInPolygon startNewWay(WayInPolygon way) {
203                lastWay = way;
204                lastWayReverse = !lastWay.insideToTheRight;
205    
206                return lastWay;
207            }
208    
209            public WayInPolygon startNewWay() {
210                if (availableWays.isEmpty()) {
211                    lastWay = null;
212                } else {
213                    lastWay = availableWays.iterator().next();
214                    lastWayReverse = !lastWay.insideToTheRight;
215                }
216    
217                return lastWay;
218            }
219    
220    
221            public  WayInPolygon advanceNextLeftmostWay() {
222                return advanceNextWay(false);
223            }
224    
225            public  WayInPolygon advanceNextRightmostWay() {
226                return advanceNextWay(true);
227            }
228    
229            private WayInPolygon advanceNextWay(boolean rightmost) {
230    
231                Node headNode = !lastWayReverse ? lastWay.way.lastNode() : lastWay.way.firstNode();
232                Node prevNode = !lastWayReverse ? lastWay.way.getNode(lastWay.way.getNodesCount() - 2) : lastWay.way.getNode(1);
233    
234                //find best next way
235                WayInPolygon bestWay = null;
236                Node bestWayNextNode = null;
237                boolean bestWayReverse = false;
238    
239                for (WayInPolygon way : availableWays) {
240                    if (way.way.firstNode().equals(headNode)) {
241                        //start adjacent to headNode
242                        Node nextNode = way.way.getNode(1);
243    
244                        if (nextNode.equals(prevNode))
245                        {
246                            //this is the path we came from - ignore it.
247                        }
248                        else if (bestWay == null || (Geometry.isToTheRightSideOfLine(prevNode, headNode, bestWayNextNode, nextNode) == rightmost)) {
249                            //the new way is better
250                            bestWay = way;
251                            bestWayReverse = false;
252                            bestWayNextNode = nextNode;
253                        }
254                    }
255    
256                    if (way.way.lastNode().equals(headNode)) {
257                        //end adjacent to headNode
258                        Node nextNode = way.way.getNode(way.way.getNodesCount() - 2);
259    
260                        if (nextNode.equals(prevNode)) {
261                            //this is the path we came from - ignore it.
262                        }
263                        else if (bestWay == null || (Geometry.isToTheRightSideOfLine(prevNode, headNode, bestWayNextNode, nextNode) == rightmost)) {
264                            //the new way is better
265                            bestWay = way;
266                            bestWayReverse = true;
267                            bestWayNextNode = nextNode;
268                        }
269                    }
270                }
271    
272                lastWay = bestWay;
273                lastWayReverse = bestWayReverse;
274    
275                return lastWay;
276            }
277    
278            public boolean isLastWayInsideToTheRight() {
279                return lastWayReverse != lastWay.insideToTheRight;
280            }
281    
282            public Node getLastWayStartNode() {
283                return lastWayReverse ? lastWay.way.lastNode() : lastWay.way.firstNode();
284            }
285    
286            public Node getLastWayEndNode() {
287                return lastWayReverse ? lastWay.way.firstNode() : lastWay.way.lastNode();
288            }
289        }
290    
291        /**
292         * Helper storage class for finding findOuterWays
293         * @author viesturs
294         */
295        static class PolygonLevel {
296            public final int level;
297            public final AssembledMultipolygon pol;
298    
299            public PolygonLevel(AssembledMultipolygon _pol, int _level) {
300                pol = _pol;
301                level = _level;
302            }
303        }
304    
305        // Adds the menu entry, Shortcuts, etc.
306        public JoinAreasAction() {
307            super(tr("Join overlapping Areas"), "joinareas", tr("Joins areas that overlap each other"),
308            Shortcut.registerShortcut("tools:joinareas", tr("Tool: {0}", tr("Join overlapping Areas")),
309                KeyEvent.VK_J, Shortcut.SHIFT), true);
310        }
311    
312        /**
313         * Gets called whenever the shortcut is pressed or the menu entry is selected
314         * Checks whether the selected objects are suitable to join and joins them if so
315         */
316        public void actionPerformed(ActionEvent e) {
317            LinkedList<Way> ways = new LinkedList<Way>(Main.main.getCurrentDataSet().getSelectedWays());
318    
319            if (ways.isEmpty()) {
320                JOptionPane.showMessageDialog(Main.parent, tr("Please select at least one closed way that should be joined."));
321                return;
322            }
323    
324            List<Node> allNodes = new ArrayList<Node>();
325            for (Way way : ways) {
326                if (!way.isClosed()) {
327                    JOptionPane.showMessageDialog(Main.parent, tr("One of the selected ways is not closed and therefore cannot be joined."));
328                    return;
329                }
330    
331                allNodes.addAll(way.getNodes());
332            }
333    
334            // TODO: Only display this warning when nodes outside dataSourceArea are deleted
335            Area dataSourceArea = Main.main.getCurrentDataSet().getDataSourceArea();
336            boolean ok = Command.checkAndConfirmOutlyingOperation("joinarea", tr("Join area confirmation"),
337                    trn("The selected way has nodes outside of the downloaded data region.",
338                        "The selected ways have nodes outside of the downloaded data region.",
339                        ways.size()) + "<br/>"
340                        + tr("This can lead to nodes being deleted accidentally.") + "<br/>"
341                        + tr("Are you really sure to continue?")
342                        + tr("Please abort if you are not sure"),
343                    tr("The selected area is incomplete. Continue?"),
344                    dataSourceArea, allNodes, null);
345            if(!ok) return;
346    
347            //analyze multipolygon relations and collect all areas
348            List<Multipolygon> areas = collectMultipolygons(ways);
349    
350            if (areas == null)
351                //too complex multipolygon relations found
352                return;
353    
354            if (!testJoin(areas)) {
355                JOptionPane.showMessageDialog(Main.parent, tr("No intersection found. Nothing was changed."));
356                return;
357            }
358    
359            if (!resolveTagConflicts(areas))
360                return;
361            //user canceled, do nothing.
362    
363            try {
364                JoinAreasResult result = joinAreas(areas);
365    
366                if (result.hasChanges) {
367    
368                    List<Way> allWays = new ArrayList<Way>();
369                    for (Multipolygon pol : result.polygons) {
370                        allWays.add(pol.outerWay);
371                        allWays.addAll(pol.innerWays);
372                    }
373                    DataSet ds = Main.main.getCurrentDataSet();
374                    ds.setSelected(allWays);
375                    Main.map.mapView.repaint();
376                } else {
377                    JOptionPane.showMessageDialog(Main.parent, tr("No intersection found. Nothing was changed."));
378                }
379            }
380            catch (UserCancelException exception) {
381                //revert changes
382                //FIXME: this is dirty hack
383                makeCommitsOneAction(tr("Reverting changes"));
384                Main.main.undoRedo.undo();
385                Main.main.undoRedo.redoCommands.clear();
386            }
387        }
388    
389        /**
390         * Tests if the areas have some intersections to join.
391         * @param areas
392         * @return
393         */
394        private boolean testJoin(List<Multipolygon> areas) {
395            List<Way> allStartingWays = new ArrayList<Way>();
396    
397            for (Multipolygon area : areas) {
398                allStartingWays.add(area.outerWay);
399                allStartingWays.addAll(area.innerWays);
400            }
401    
402            //find intersection points
403            Set<Node> nodes = Geometry.addIntersections(allStartingWays, true, cmds);
404            return nodes.size() > 0;
405        }
406    
407        /**
408         * Will join two or more overlapping areas
409         * @param areas - list of areas to join
410         * @return new area formed.
411         */
412        private JoinAreasResult joinAreas(List<Multipolygon> areas) throws UserCancelException {
413    
414            JoinAreasResult result = new JoinAreasResult();
415            result.hasChanges = false;
416    
417            List<Way> allStartingWays = new ArrayList<Way>();
418            List<Way> innerStartingWays = new ArrayList<Way>();
419            List<Way> outerStartingWays = new ArrayList<Way>();
420    
421            for (Multipolygon area : areas) {
422                outerStartingWays.add(area.outerWay);
423                innerStartingWays.addAll(area.innerWays);
424            }
425    
426            allStartingWays.addAll(innerStartingWays);
427            allStartingWays.addAll(outerStartingWays);
428    
429            //first remove nodes in the same coordinate
430            boolean removedDuplicates = false;
431            removedDuplicates |= removeDuplicateNodes(allStartingWays);
432    
433            if (removedDuplicates) {
434                result.hasChanges = true;
435                commitCommands(marktr("Removed duplicate nodes"));
436            }
437    
438            //find intersection points
439            Set<Node> nodes = Geometry.addIntersections(allStartingWays, false, cmds);
440    
441            //no intersections, return.
442            if (nodes.isEmpty())
443                return result;
444            commitCommands(marktr("Added node on all intersections"));
445    
446            ArrayList<RelationRole> relations = new ArrayList<RelationRole>();
447    
448            // Remove ways from all relations so ways can be combined/split quietly
449            for (Way way : allStartingWays) {
450                relations.addAll(removeFromAllRelations(way));
451            }
452    
453            // Don't warn now, because it will really look corrupted
454            boolean warnAboutRelations = relations.size() > 0 && allStartingWays.size() > 1;
455    
456            ArrayList<WayInPolygon> preparedWays = new ArrayList<WayInPolygon>();
457    
458            for (Way way : outerStartingWays) {
459                ArrayList<Way> splitWays = splitWayOnNodes(way, nodes);
460                preparedWays.addAll(markWayInsideSide(splitWays, false));
461            }
462    
463            for (Way way : innerStartingWays) {
464                ArrayList<Way> splitWays = splitWayOnNodes(way, nodes);
465                preparedWays.addAll(markWayInsideSide(splitWays, true));
466            }
467    
468            // Find boundary ways
469            ArrayList<Way> discardedWays = new ArrayList<Way>();
470            List<AssembledPolygon> bounadries = findBoundaryPolygons(preparedWays, discardedWays);
471    
472            //find polygons
473            List<AssembledMultipolygon> preparedPolygons = findPolygons(bounadries);
474    
475    
476            //assemble final polygons
477            List<Multipolygon> polygons = new ArrayList<Multipolygon>();
478            Set<Relation> relationsToDelete = new LinkedHashSet<Relation>();
479    
480            for (AssembledMultipolygon pol : preparedPolygons) {
481    
482                //create the new ways
483                Multipolygon resultPol = joinPolygon(pol);
484    
485                //create multipolygon relation, if necessary.
486                RelationRole ownMultipolygonRelation = addOwnMultigonRelation(resultPol.innerWays, resultPol.outerWay);
487    
488                //add back the original relations, merged with our new multipolygon relation
489                fixRelations(relations, resultPol.outerWay, ownMultipolygonRelation, relationsToDelete);
490    
491                //strip tags from inner ways
492                //TODO: preserve tags on existing inner ways
493                stripTags(resultPol.innerWays);
494    
495                polygons.add(resultPol);
496            }
497    
498            commitCommands(marktr("Assemble new polygons"));
499    
500            for(Relation rel: relationsToDelete) {
501                cmds.add(new DeleteCommand(rel));
502            }
503    
504            commitCommands(marktr("Delete relations"));
505    
506            // Delete the discarded inner ways
507            if (discardedWays.size() > 0) {
508                cmds.add(DeleteCommand.delete(Main.map.mapView.getEditLayer(), discardedWays, true));
509                commitCommands(marktr("Delete Ways that are not part of an inner multipolygon"));
510            }
511    
512            makeCommitsOneAction(marktr("Joined overlapping areas"));
513    
514            if (warnAboutRelations) {
515                JOptionPane.showMessageDialog(Main.parent, tr("Some of the ways were part of relations that have been modified. Please verify no errors have been introduced."));
516            }
517    
518            result.hasChanges = true;
519            result.mergeSuccessful = true;
520            result.polygons = polygons;
521            return result;
522        }
523    
524        /**
525         * Checks if tags of two given ways differ, and presents the user a dialog to solve conflicts
526         * @param Way First way to check
527         * @param Way Second Way to check
528         * @return boolean True if all conflicts are resolved, False if conflicts remain.
529         */
530        private boolean resolveTagConflicts(List<Multipolygon> polygons) {
531    
532            List<Way> ways = new ArrayList<Way>();
533    
534            for (Multipolygon pol : polygons) {
535                ways.add(pol.outerWay);
536                ways.addAll(pol.innerWays);
537            }
538    
539            if (ways.size() < 2) {
540                return true;
541            }
542    
543            TagCollection wayTags = TagCollection.unionOfAllPrimitives(ways);
544            try {
545                cmds.addAll(CombinePrimitiveResolverDialog.launchIfNecessary(wayTags, ways, ways));
546                commitCommands(marktr("Fix tag conflicts"));
547                return true;
548            } catch (UserCancelException ex) {
549                return false;
550            }
551        }
552    
553        /**
554         * This method removes duplicate points (if any) from the input way.
555         * @param way the way to process
556         * @return true if any changes where made
557         */
558        private boolean removeDuplicateNodes(List<Way> ways) {
559            //TODO: maybe join nodes with JoinNodesAction, rather than reconnect the ways.
560    
561            Map<Node, Node> nodeMap = new TreeMap<Node, Node>(new NodePositionComparator());
562            int totalNodesRemoved = 0;
563    
564            for (Way way : ways) {
565                if (way.getNodes().size() < 2) {
566                    continue;
567                }
568    
569                int nodesRemoved = 0;
570                List<Node> newNodes = new ArrayList<Node>();
571                Node prevNode = null;
572    
573                for (Node node : way.getNodes()) {
574                    if (!nodeMap.containsKey(node)) {
575                        //new node
576                        nodeMap.put(node, node);
577    
578                        //avoid duplicate nodes
579                        if (prevNode != node) {
580                            newNodes.add(node);
581                        } else {
582                            nodesRemoved ++;
583                        }
584                    } else {
585                        //node with same coordinates already exists, substitute with existing node
586                        Node representator = nodeMap.get(node);
587    
588                        if (representator != node) {
589                            nodesRemoved ++;
590                        }
591    
592                        //avoid duplicate node
593                        if (prevNode != representator) {
594                            newNodes.add(representator);
595                        }
596                    }
597                    prevNode = node;
598                }
599    
600                if (nodesRemoved > 0) {
601    
602                    if (newNodes.size() == 1) { //all nodes in the same coordinate - add one more node, to have closed way.
603                        newNodes.add(newNodes.get(0));
604                    }
605    
606                    Way newWay=new Way(way);
607                    newWay.setNodes(newNodes);
608                    cmds.add(new ChangeCommand(way, newWay));
609                    totalNodesRemoved += nodesRemoved;
610                }
611            }
612    
613            return totalNodesRemoved > 0;
614        }
615    
616        /**
617         * Commits the command list with a description
618         * @param String The description of what the commands do
619         */
620        private void commitCommands(String description) {
621            switch(cmds.size()) {
622            case 0:
623                return;
624            case 1:
625                Main.main.undoRedo.add(cmds.getFirst());
626                break;
627            default:
628                Command c = new SequenceCommand(tr(description), cmds);
629                Main.main.undoRedo.add(c);
630                break;
631            }
632    
633            cmds.clear();
634            cmdsCount++;
635        }
636    
637        /**
638         * This method analyzes the way and assigns each part what direction polygon "inside" is.
639         * @param parts the split parts of the way
640         * @param isInner - if true, reverts the direction (for multipolygon islands)
641         * @return list of parts, marked with the inside orientation.
642         */
643        private ArrayList<WayInPolygon> markWayInsideSide(List<Way> parts, boolean isInner) {
644    
645            ArrayList<WayInPolygon> result = new ArrayList<WayInPolygon>();
646    
647            //prepare prev and next maps
648            Map<Way, Way> nextWayMap = new HashMap<Way, Way>();
649            Map<Way, Way> prevWayMap = new HashMap<Way, Way>();
650    
651            for (int pos = 0; pos < parts.size(); pos ++) {
652    
653                if (!parts.get(pos).lastNode().equals(parts.get((pos + 1) % parts.size()).firstNode()))
654                    throw new RuntimeException("Way not circular");
655    
656                nextWayMap.put(parts.get(pos), parts.get((pos + 1) % parts.size()));
657                prevWayMap.put(parts.get(pos), parts.get((pos + parts.size() - 1) % parts.size()));
658            }
659    
660            //find the node with minimum y - it's guaranteed to be outer. (What about the south pole?)
661            Way topWay = null;
662            Node topNode = null;
663            int topIndex = 0;
664            double minY = Double.POSITIVE_INFINITY;
665    
666            for (Way way : parts) {
667                for (int pos = 0; pos < way.getNodesCount(); pos ++) {
668                    Node node = way.getNode(pos);
669    
670                    if (node.getEastNorth().getY() < minY) {
671                        minY = node.getEastNorth().getY();
672                        topWay = way;
673                        topNode = node;
674                        topIndex = pos;
675                    }
676                }
677            }
678    
679            //get the upper way and it's orientation.
680    
681            boolean wayClockwise; // orientation of the top way.
682    
683            if (topNode.equals(topWay.firstNode()) || topNode.equals(topWay.lastNode())) {
684                Node headNode = null; // the node at junction
685                Node prevNode = null; // last node from previous path
686                wayClockwise = false;
687    
688                //node is in split point - find the outermost way from this point
689    
690                headNode = topNode;
691                //make a fake node that is downwards from head node (smaller Y). It will be a division point between paths.
692                prevNode = new Node(new EastNorth(headNode.getEastNorth().getX(), headNode.getEastNorth().getY() - 1e5));
693    
694                topWay = null;
695                wayClockwise = false;
696                Node bestWayNextNode = null;
697    
698                for (Way way : parts) {
699                    if (way.firstNode().equals(headNode)) {
700                        Node nextNode = way.getNode(1);
701    
702                        if (topWay == null || !Geometry.isToTheRightSideOfLine(prevNode, headNode, bestWayNextNode, nextNode)) {
703                            //the new way is better
704                            topWay = way;
705                            wayClockwise = true;
706                            bestWayNextNode = nextNode;
707                        }
708                    }
709    
710                    if (way.lastNode().equals(headNode)) {
711                        //end adjacent to headNode
712                        Node nextNode = way.getNode(way.getNodesCount() - 2);
713    
714                        if (topWay == null || !Geometry.isToTheRightSideOfLine(prevNode, headNode, bestWayNextNode, nextNode)) {
715                            //the new way is better
716                            topWay = way;
717                            wayClockwise = false;
718                            bestWayNextNode = nextNode;
719                        }
720                    }
721                }
722            } else {
723                //node is inside way - pick the clockwise going end.
724                Node prev = topWay.getNode(topIndex - 1);
725                Node next = topWay.getNode(topIndex + 1);
726    
727                //there will be no parallel segments in the middle of way, so all fine.
728                wayClockwise = Geometry.angleIsClockwise(prev, topNode, next);
729            }
730    
731            Way curWay = topWay;
732            boolean curWayInsideToTheRight = wayClockwise ^ isInner;
733    
734            //iterate till full circle is reached
735            while (true) {
736    
737                //add cur way
738                WayInPolygon resultWay = new WayInPolygon(curWay, curWayInsideToTheRight);
739                result.add(resultWay);
740    
741                //process next way
742                Way nextWay = nextWayMap.get(curWay);
743                Node prevNode = curWay.getNode(curWay.getNodesCount() - 2);
744                Node headNode = curWay.lastNode();
745                Node nextNode = nextWay.getNode(1);
746    
747                if (nextWay == topWay) {
748                    //full loop traversed - all done.
749                    break;
750                }
751    
752                //find intersecting segments
753                // the intersections will look like this:
754                //
755                //                       ^
756                //                       |
757                //                       X wayBNode
758                //                       |
759                //                  wayB |
760                //                       |
761                //             curWay    |       nextWay
762                //----X----------------->X----------------------X---->
763                //    prevNode           ^headNode              nextNode
764                //                       |
765                //                       |
766                //                  wayA |
767                //                       |
768                //                       X wayANode
769                //                       |
770    
771                int intersectionCount = 0;
772    
773                for (Way wayA : parts) {
774    
775                    if (wayA == curWay) {
776                        continue;
777                    }
778    
779                    if (wayA.lastNode().equals(headNode)) {
780    
781                        Way wayB = nextWayMap.get(wayA);
782    
783                        //test if wayA is opposite wayB relative to curWay and nextWay
784    
785                        Node wayANode = wayA.getNode(wayA.getNodesCount() - 2);
786                        Node wayBNode = wayB.getNode(1);
787    
788                        boolean wayAToTheRight = Geometry.isToTheRightSideOfLine(prevNode, headNode, nextNode, wayANode);
789                        boolean wayBToTheRight = Geometry.isToTheRightSideOfLine(prevNode, headNode, nextNode, wayBNode);
790    
791                        if (wayAToTheRight != wayBToTheRight) {
792                            intersectionCount ++;
793                        }
794                    }
795                }
796    
797                //if odd number of crossings, invert orientation
798                if (intersectionCount % 2 == 1) {
799                    curWayInsideToTheRight = !curWayInsideToTheRight;
800                }
801    
802                curWay = nextWay;
803            }
804    
805            return result;
806        }
807    
808        /**
809         * This is a method splits way into smaller parts, using the prepared nodes list as split points.
810         * Uses  SplitWayAction.splitWay for the heavy lifting.
811         * @return list of split ways (or original ways if no splitting is done).
812         */
813        private ArrayList<Way> splitWayOnNodes(Way way, Set<Node> nodes) {
814    
815            ArrayList<Way> result = new ArrayList<Way>();
816            List<List<Node>> chunks = buildNodeChunks(way, nodes);
817    
818            if (chunks.size() > 1) {
819                SplitWayResult split = SplitWayAction.splitWay(Main.map.mapView.getEditLayer(), way, chunks, Collections.<OsmPrimitive>emptyList());
820    
821                //execute the command, we need the results
822                cmds.add(split.getCommand());
823                commitCommands(marktr("Split ways into fragments"));
824    
825                result.add(split.getOriginalWay());
826                result.addAll(split.getNewWays());
827            } else {
828                //nothing to split
829                result.add(way);
830            }
831    
832            return result;
833        }
834    
835        /**
836         * Simple chunking version. Does not care about circular ways and result being
837         * proper, we will glue it all back together later on.
838         * @param way the way to chunk
839         * @param splitNodes the places where to cut.
840         * @return list of node paths to produce.
841         */
842        private List<List<Node>> buildNodeChunks(Way way, Collection<Node> splitNodes) {
843            List<List<Node>> result = new ArrayList<List<Node>>();
844            List<Node> curList = new ArrayList<Node>();
845    
846            for (Node node : way.getNodes()) {
847                curList.add(node);
848                if (curList.size() > 1 && splitNodes.contains(node)) {
849                    result.add(curList);
850                    curList = new ArrayList<Node>();
851                    curList.add(node);
852                }
853            }
854    
855            if (curList.size() > 1) {
856                result.add(curList);
857            }
858    
859            return result;
860        }
861    
862    
863        /**
864         * This method finds witch ways are outer and witch are inner.
865         * @param boundaryWays
866         * @return
867         */
868        private List<AssembledMultipolygon> findPolygons(Collection<AssembledPolygon> boundaries) {
869    
870            List<PolygonLevel> list = findOuterWaysImpl(0, boundaries);
871            List<AssembledMultipolygon> result = new ArrayList<AssembledMultipolygon>();
872    
873            //take every other level
874            for (PolygonLevel pol : list) {
875                if (pol.level % 2 == 0) {
876                    result.add(pol.pol);
877                }
878            }
879    
880            return result;
881        }
882    
883        /**
884         * Collects outer way and corresponding inner ways from all boundaries.
885         * @param boundaryWays
886         * @return the outermostWay.
887         */
888        private List<PolygonLevel> findOuterWaysImpl(int level, Collection<AssembledPolygon> boundaryWays) {
889    
890            //TODO: bad performance for deep nestings...
891            List<PolygonLevel> result = new ArrayList<PolygonLevel>();
892    
893            for (AssembledPolygon outerWay : boundaryWays) {
894    
895                boolean outerGood = true;
896                List<AssembledPolygon> innerCandidates = new ArrayList<AssembledPolygon>();
897    
898                for (AssembledPolygon innerWay : boundaryWays) {
899                    if (innerWay == outerWay) {
900                        continue;
901                    }
902    
903                    if (wayInsideWay(outerWay, innerWay)) {
904                        outerGood = false;
905                        break;
906                    } else if (wayInsideWay(innerWay, outerWay)) {
907                        innerCandidates.add(innerWay);
908                    }
909                }
910    
911                if (!outerGood) {
912                    continue;
913                }
914    
915                //add new outer polygon
916                AssembledMultipolygon pol = new AssembledMultipolygon(outerWay);
917                PolygonLevel polLev = new PolygonLevel(pol, level);
918    
919                //process inner ways
920                if (innerCandidates.size() > 0) {
921                    List<PolygonLevel> innerList = findOuterWaysImpl(level + 1, innerCandidates);
922                    result.addAll(innerList);
923    
924                    for (PolygonLevel pl : innerList) {
925                        if (pl.level == level + 1) {
926                            pol.innerWays.add(pl.pol.outerWay);
927                        }
928                    }
929                }
930    
931                result.add(polLev);
932            }
933    
934            return result;
935        }
936    
937        /**
938         * Finds all ways that form inner or outer boundaries.
939         * @param Collection<Way> A list of (splitted) ways that form a multigon and share common end nodes on intersections.
940         * @param Collection<Way> this list is filled with ways that are to be discarded
941         * @return Collection<Collection<Way>> A list of ways that form the outer and inner boundaries of the multigon.
942         */
943        public static List<AssembledPolygon> findBoundaryPolygons(Collection<WayInPolygon> multigonWays, List<Way> discardedResult) {
944            //first find all discardable ways, by getting outer shells.
945            //this will produce incorrect boundaries in some cases, but second pass will fix it.
946    
947            List<WayInPolygon> discardedWays = new ArrayList<WayInPolygon>();
948            Set<WayInPolygon> processedWays = new HashSet<WayInPolygon>();
949            WayTraverser traverser = new WayTraverser(multigonWays);
950    
951            for (WayInPolygon startWay : multigonWays) {
952                if (processedWays.contains(startWay)) {
953                    continue;
954                }
955    
956                traverser.startNewWay(startWay);
957    
958                List<WayInPolygon> boundary = new ArrayList<WayInPolygon>();
959                WayInPolygon lastWay = startWay;
960    
961                while (true) {
962                    boundary.add(lastWay);
963    
964                    WayInPolygon bestWay = traverser.advanceNextLeftmostWay();
965                    boolean wayInsideToTheRight = bestWay == null ? false : traverser.isLastWayInsideToTheRight();
966    
967                    if (bestWay == null || processedWays.contains(bestWay) || !wayInsideToTheRight) {
968                        //bad segment chain - proceed to discard it
969                        lastWay = null;
970                        break;
971                    } else if (boundary.contains(bestWay)) {
972                        //traversed way found - close the way
973                        lastWay = bestWay;
974                        break;
975                    } else {
976                        //proceed to next segment
977                        lastWay = bestWay;
978                    }
979                }
980    
981                if (lastWay != null) {
982                    //way good
983                    processedWays.addAll(boundary);
984    
985                    //remove junk segments at the start
986                    while (boundary.get(0) != lastWay) {
987                        discardedWays.add(boundary.get(0));
988                        boundary.remove(0);
989                    }
990                } else {
991                    //way bad
992                    discardedWays.addAll(boundary);
993                    processedWays.addAll(boundary);
994                }
995            }
996    
997            //now we have removed junk segments, collect the real result ways
998    
999            traverser.removeWays(discardedWays);
1000    
1001            List<AssembledPolygon> result = new ArrayList<AssembledPolygon>();
1002    
1003            while (traverser.hasWays()) {
1004    
1005                WayInPolygon startWay = traverser.startNewWay();
1006                List<WayInPolygon> boundary = new ArrayList<WayInPolygon>();
1007                WayInPolygon curWay = startWay;
1008    
1009                do {
1010                    boundary.add(curWay);
1011                    curWay = traverser.advanceNextRightmostWay();
1012    
1013                    //should not happen
1014                    if (curWay == null || !traverser.isLastWayInsideToTheRight())
1015                        throw new RuntimeException("Join areas internal error.");
1016    
1017                } while (curWay != startWay);
1018    
1019                //build result
1020                traverser.removeWays(boundary);
1021                result.add(new AssembledPolygon(boundary));
1022            }
1023    
1024            for (WayInPolygon way : discardedWays) {
1025                discardedResult.add(way.way);
1026            }
1027    
1028            //split inner polygons that have several touching parts.
1029            result = fixTouchingPolygons(result);
1030    
1031            return result;
1032        }
1033    
1034        /**
1035         * This method checks if polygons have several touching parts and splits them in several polygons.
1036         * @param polygon the polygon to process.
1037         */
1038        public static List<AssembledPolygon> fixTouchingPolygons(List<AssembledPolygon> polygons)
1039        {
1040            List<AssembledPolygon> newPolygons = new ArrayList<AssembledPolygon>();
1041    
1042            for (AssembledPolygon innerPart : polygons) {
1043                WayTraverser traverser = new WayTraverser(innerPart.ways);
1044    
1045                while (traverser.hasWays()) {
1046    
1047                    WayInPolygon startWay = traverser.startNewWay();
1048                    List<WayInPolygon> boundary = new ArrayList<WayInPolygon>();
1049                    WayInPolygon curWay = startWay;
1050    
1051                    Node startNode = traverser.getLastWayStartNode();
1052                    boundary.add(curWay);
1053    
1054                    while (startNode != traverser.getLastWayEndNode()) {
1055                        curWay = traverser.advanceNextLeftmostWay();
1056                        boundary.add(curWay);
1057    
1058                        //should not happen
1059                        if (curWay == null || !traverser.isLastWayInsideToTheRight())
1060                            throw new RuntimeException("Join areas internal error.");
1061                    }
1062    
1063                    //build result
1064                    traverser.removeWays(boundary);
1065                    newPolygons.add(new AssembledPolygon(boundary));
1066                }
1067            }
1068    
1069            return newPolygons;
1070        }
1071    
1072        /**
1073         * Tests if way is inside other way
1074         * @param outside
1075         * @param inside
1076         * @return
1077         */
1078        public static boolean wayInsideWay(AssembledPolygon inside, AssembledPolygon outside) {
1079            Set<Node> outsideNodes = new HashSet<Node>(outside.getNodes());
1080            List<Node> insideNodes = inside.getNodes();
1081    
1082            for (Node insideNode : insideNodes) {
1083    
1084                if (!outsideNodes.contains(insideNode))
1085                    //simply test the one node
1086                    return Geometry.nodeInsidePolygon(insideNode, outside.getNodes());
1087            }
1088    
1089            //all nodes shared.
1090            return false;
1091        }
1092    
1093        /**
1094         * Joins the lists of ways.
1095         * @param Collection<Way> The list of outer ways that belong to that multigon.
1096         * @return Way The newly created outer way
1097         */
1098        private Multipolygon  joinPolygon(AssembledMultipolygon polygon) throws UserCancelException {
1099            Multipolygon result = new Multipolygon(joinWays(polygon.outerWay.ways));
1100    
1101            for (AssembledPolygon pol : polygon.innerWays) {
1102                result.innerWays.add(joinWays(pol.ways));
1103            }
1104    
1105            return result;
1106        }
1107    
1108        /**
1109         * Joins the outer ways and deletes all short ways that can't be part of a multipolygon anyway.
1110         * @param Collection<Way> The list of outer ways that belong to that multigon.
1111         * @return Way The newly created outer way
1112         */
1113        private Way joinWays(List<WayInPolygon> ways) throws UserCancelException {
1114    
1115            //leave original orientation, if all paths are reverse.
1116            boolean allReverse = true;
1117            for (WayInPolygon way : ways) {
1118                allReverse &= !way.insideToTheRight;
1119            }
1120    
1121            if (allReverse) {
1122                for (WayInPolygon way : ways) {
1123                    way.insideToTheRight = !way.insideToTheRight;
1124                }
1125            }
1126    
1127            Way joinedWay = joinOrientedWays(ways);
1128    
1129            //should not happen
1130            if (joinedWay == null || !joinedWay.isClosed())
1131                throw new RuntimeException("Join areas internal error.");
1132    
1133            return joinedWay;
1134        }
1135    
1136        /**
1137         * Joins a list of ways (using CombineWayAction and ReverseWayAction as specified in WayInPath)
1138         * @param ArrayList<Way> The list of ways to join and reverse
1139         * @return Way The newly created way
1140         */
1141        private Way joinOrientedWays(List<WayInPolygon> ways) throws UserCancelException{
1142            if (ways.size() < 2)
1143                return ways.get(0).way;
1144    
1145            // This will turn ways so all of them point in the same direction and CombineAction won't bug
1146            // the user about this.
1147    
1148            //TODO: ReverseWay and Combine way are really slow and we use them a lot here. This slows down large joins.
1149            List<Way> actionWays = new ArrayList<Way>(ways.size());
1150    
1151            for (WayInPolygon way : ways) {
1152                actionWays.add(way.way);
1153    
1154                if (!way.insideToTheRight) {
1155                    ReverseWayResult res = ReverseWayAction.reverseWay(way.way);
1156                    Main.main.undoRedo.add(res.getReverseCommand());
1157                    cmdsCount++;
1158                }
1159            }
1160    
1161            Pair<Way, Command> result = CombineWayAction.combineWaysWorker(actionWays);
1162    
1163            Main.main.undoRedo.add(result.b);
1164            cmdsCount ++;
1165    
1166            return result.a;
1167        }
1168    
1169        /**
1170         * This method analyzes multipolygon relationships of given ways and collects addition inner ways to consider.
1171         * @param selectedWays the selected ways
1172         * @return list of polygons, or null if too complex relation encountered.
1173         */
1174        private List<Multipolygon> collectMultipolygons(List<Way> selectedWays) {
1175    
1176            List<Multipolygon> result = new ArrayList<Multipolygon>();
1177    
1178            //prepare the lists, to minimize memory allocation.
1179            List<Way> outerWays = new ArrayList<Way>();
1180            List<Way> innerWays = new ArrayList<Way>();
1181    
1182            Set<Way> processedOuterWays = new LinkedHashSet<Way>();
1183            Set<Way> processedInnerWays = new LinkedHashSet<Way>();
1184    
1185            for (Relation r : OsmPrimitive.getParentRelations(selectedWays)) {
1186                if (r.isDeleted() || !r.isMultipolygon()) {
1187                    continue;
1188                }
1189    
1190                boolean hasKnownOuter = false;
1191                outerWays.clear();
1192                innerWays.clear();
1193    
1194                for (RelationMember rm : r.getMembers()) {
1195                    if (rm.getRole().equalsIgnoreCase("outer")) {
1196                        outerWays.add(rm.getWay());
1197                        hasKnownOuter |= selectedWays.contains(rm.getWay());
1198                    }
1199                    else if (rm.getRole().equalsIgnoreCase("inner")) {
1200                        innerWays.add(rm.getWay());
1201                    }
1202                }
1203    
1204                if (!hasKnownOuter) {
1205                    continue;
1206                }
1207    
1208                if (outerWays.size() > 1) {
1209                    JOptionPane.showMessageDialog(Main.parent, tr("Sorry. Cannot handle multipolygon relations with multiple outer ways."));
1210                    return null;
1211                }
1212    
1213                Way outerWay = outerWays.get(0);
1214    
1215                //retain only selected inner ways
1216                innerWays.retainAll(selectedWays);
1217    
1218                if (processedOuterWays.contains(outerWay)) {
1219                    JOptionPane.showMessageDialog(Main.parent, tr("Sorry. Cannot handle way that is outer in multiple multipolygon relations."));
1220                    return null;
1221                }
1222    
1223                if (processedInnerWays.contains(outerWay)) {
1224                    JOptionPane.showMessageDialog(Main.parent, tr("Sorry. Cannot handle way that is both inner and outer in multipolygon relations."));
1225                    return null;
1226                }
1227    
1228                for (Way way :innerWays)
1229                {
1230                    if (processedOuterWays.contains(way)) {
1231                        JOptionPane.showMessageDialog(Main.parent, tr("Sorry. Cannot handle way that is both inner and outer in multipolygon relations."));
1232                        return null;
1233                    }
1234    
1235                    if (processedInnerWays.contains(way)) {
1236                        JOptionPane.showMessageDialog(Main.parent, tr("Sorry. Cannot handle way that is inner in multiple multipolygon relations."));
1237                        return null;
1238                    }
1239                }
1240    
1241                processedOuterWays.add(outerWay);
1242                processedInnerWays.addAll(innerWays);
1243    
1244                Multipolygon pol = new Multipolygon(outerWay);
1245                pol.innerWays.addAll(innerWays);
1246                pol.relation = r;
1247    
1248                result.add(pol);
1249            }
1250    
1251            //add remaining ways, not in relations
1252            for (Way way : selectedWays) {
1253                if (processedOuterWays.contains(way) || processedInnerWays.contains(way)) {
1254                    continue;
1255                }
1256    
1257                result.add(new Multipolygon(way));
1258            }
1259    
1260            return result;
1261        }
1262    
1263        /**
1264         * This method filters the list of relations that form the multipolygons.
1265         * @param relations
1266         * @param polygons
1267         * @return
1268         */
1269        private List<Relation> filterOwnMultipolygonRelations(Collection<Relation> relations, List<Multipolygon> polygons) {
1270    
1271            List<Relation> relationsToRemove = new ArrayList<Relation>();
1272    
1273            for (Multipolygon m : polygons) {
1274                if (m.relation != null) {
1275                    relationsToRemove.add(m.relation);
1276                }
1277            }
1278    
1279            List<Relation> result = new ArrayList<Relation>();
1280    
1281            result.addAll(relations);
1282            result.removeAll(relationsToRemove);
1283            return result;
1284        }
1285    
1286        /**
1287         * Will add own multipolygon relation to the "previously existing" relations. Fixup is done by fixRelations
1288         * @param Collection<Way> List of already closed inner ways
1289         * @param Way The outer way
1290         * @param ArrayList<RelationRole> The list of relation with roles to add own relation to
1291         */
1292        private RelationRole addOwnMultigonRelation(Collection<Way> inner, Way outer) {
1293            if (inner.size() == 0) return null;
1294            // Create new multipolygon relation and add all inner ways to it
1295            Relation newRel = new Relation();
1296            newRel.put("type", "multipolygon");
1297            for (Way w : inner) {
1298                newRel.addMember(new RelationMember("inner", w));
1299            }
1300            cmds.add(new AddCommand(newRel));
1301    
1302            // We don't add outer to the relation because it will be handed to fixRelations()
1303            // which will then do the remaining work.
1304            return new RelationRole(newRel, "outer");
1305        }
1306    
1307        /**
1308         * Removes a given OsmPrimitive from all relations
1309         * @param OsmPrimitive Element to remove from all relations
1310         * @return ArrayList<RelationRole> List of relations with roles the primitives was part of
1311         */
1312        private ArrayList<RelationRole> removeFromAllRelations(OsmPrimitive osm) {
1313            ArrayList<RelationRole> result = new ArrayList<RelationRole>();
1314    
1315            for (Relation r : Main.main.getCurrentDataSet().getRelations()) {
1316                if (r.isDeleted()) {
1317                    continue;
1318                }
1319                for (RelationMember rm : r.getMembers()) {
1320                    if (rm.getMember() != osm) {
1321                        continue;
1322                    }
1323    
1324                    Relation newRel = new Relation(r);
1325                    List<RelationMember> members = newRel.getMembers();
1326                    members.remove(rm);
1327                    newRel.setMembers(members);
1328    
1329                    cmds.add(new ChangeCommand(r, newRel));
1330                    RelationRole saverel =  new RelationRole(r, rm.getRole());
1331                    if (!result.contains(saverel)) {
1332                        result.add(saverel);
1333                    }
1334                    break;
1335                }
1336            }
1337    
1338            commitCommands(marktr("Removed Element from Relations"));
1339            return result;
1340        }
1341    
1342        /**
1343         * Adds the previously removed relations again to the outer way. If there are multiple multipolygon
1344         * relations where the joined areas were in "outer" role a new relation is created instead with all
1345         * members of both. This function depends on multigon relations to be valid already, it won't fix them.
1346         * @param ArrayList<RelationRole> List of relations with roles the (original) ways were part of
1347         * @param Way The newly created outer area/way
1348         * @param relationsToDelete - set of relations to delete.
1349         */
1350        private void fixRelations(ArrayList<RelationRole> rels, Way outer, RelationRole ownMultipol, Set<Relation> relationsToDelete) {
1351            ArrayList<RelationRole> multiouters = new ArrayList<RelationRole>();
1352    
1353            if (ownMultipol != null){
1354                multiouters.add(ownMultipol);
1355            }
1356    
1357            for (RelationRole r : rels) {
1358                if (r.rel.isMultipolygon() && r.role.equalsIgnoreCase("outer")) {
1359                    multiouters.add(r);
1360                    continue;
1361                }
1362                // Add it back!
1363                Relation newRel = new Relation(r.rel);
1364                newRel.addMember(new RelationMember(r.role, outer));
1365                cmds.add(new ChangeCommand(r.rel, newRel));
1366            }
1367    
1368            Relation newRel = null;
1369            switch (multiouters.size()) {
1370            case 0:
1371                return;
1372            case 1:
1373                // Found only one to be part of a multipolygon relation, so just add it back as well
1374                newRel = new Relation(multiouters.get(0).rel);
1375                newRel.addMember(new RelationMember(multiouters.get(0).role, outer));
1376                cmds.add(new ChangeCommand(multiouters.get(0).rel, newRel));
1377                return;
1378            default:
1379                // Create a new relation with all previous members and (Way)outer as outer.
1380                newRel = new Relation();
1381                for (RelationRole r : multiouters) {
1382                    // Add members
1383                    for (RelationMember rm : r.rel.getMembers())
1384                        if (!newRel.getMembers().contains(rm)) {
1385                            newRel.addMember(rm);
1386                        }
1387                    // Add tags
1388                    for (String key : r.rel.keySet()) {
1389                        newRel.put(key, r.rel.get(key));
1390                    }
1391                    // Delete old relation
1392                    relationsToDelete.add(r.rel);
1393                }
1394                newRel.addMember(new RelationMember("outer", outer));
1395                cmds.add(new AddCommand(newRel));
1396            }
1397        }
1398    
1399        /**
1400         * @param Collection<Way> The List of Ways to remove all tags from
1401         */
1402        private void stripTags(Collection<Way> ways) {
1403            for (Way w : ways) {
1404                stripTags(w);
1405            }
1406            /* I18N: current action printed in status display */
1407            commitCommands(marktr("Remove tags from inner ways"));
1408        }
1409    
1410        /**
1411         * @param Way The Way to remove all tags from
1412         */
1413        private void stripTags(Way x) {
1414            if (x.getKeys() == null)
1415                return;
1416            Way y = new Way(x);
1417            for (String key : x.keySet()) {
1418                y.remove(key);
1419            }
1420            cmds.add(new ChangeCommand(x, y));
1421        }
1422    
1423        /**
1424         * Takes the last cmdsCount actions back and combines them into a single action
1425         * (for when the user wants to undo the join action)
1426         * @param String The commit message to display
1427         */
1428        private void makeCommitsOneAction(String message) {
1429            UndoRedoHandler ur = Main.main.undoRedo;
1430            cmds.clear();
1431            int i = Math.max(ur.commands.size() - cmdsCount, 0);
1432            for (; i < ur.commands.size(); i++) {
1433                cmds.add(ur.commands.get(i));
1434            }
1435    
1436            for (i = 0; i < cmds.size(); i++) {
1437                ur.undo();
1438            }
1439    
1440            commitCommands(message == null ? marktr("Join Areas Function") : message);
1441            cmdsCount = 0;
1442        }
1443    
1444        @Override
1445        protected void updateEnabledState() {
1446            if (getCurrentDataSet() == null) {
1447                setEnabled(false);
1448            } else {
1449                updateEnabledState(getCurrentDataSet().getSelected());
1450            }
1451        }
1452    
1453        @Override
1454        protected void updateEnabledState(Collection<? extends OsmPrimitive> selection) {
1455            setEnabled(selection != null && !selection.isEmpty());
1456        }
1457    }