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