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 * @since 2575
052 */
053public 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 final LinkedList<Command> cmds = new LinkedList<>();
056    private int cmdsCount = 0;
057    private final List<Relation> addedRelations = new LinkedList<>();
058
059    /**
060     * This helper class describes join areas action result.
061     * @author viesturs
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        join(Main.main.getCurrentDataSet().getSelectedWays());
402    }
403
404    /**
405     * Joins the given ways.
406     * @param ways Ways to join
407     * @since 7534
408     */
409    public void join(Collection<Way> ways) {
410        addedRelations.clear();
411
412        if (ways.isEmpty()) {
413            new Notification(
414                    tr("Please select at least one closed way that should be joined."))
415                    .setIcon(JOptionPane.INFORMATION_MESSAGE)
416                    .show();
417            return;
418        }
419
420        List<Node> allNodes = new ArrayList<>();
421        for (Way way : ways) {
422            if (!way.isClosed()) {
423                new Notification(
424                        tr("One of the selected ways is not closed and therefore cannot be joined."))
425                        .setIcon(JOptionPane.INFORMATION_MESSAGE)
426                        .show();
427                return;
428            }
429
430            allNodes.addAll(way.getNodes());
431        }
432
433        // TODO: Only display this warning when nodes outside dataSourceArea are deleted
434        boolean ok = Command.checkAndConfirmOutlyingOperation("joinarea", tr("Join area confirmation"),
435                trn("The selected way has nodes outside of the downloaded data region.",
436                    "The selected ways have nodes outside of the downloaded data region.",
437                    ways.size()) + "<br/>"
438                    + tr("This can lead to nodes being deleted accidentally.") + "<br/>"
439                    + tr("Are you really sure to continue?")
440                    + tr("Please abort if you are not sure"),
441                tr("The selected area is incomplete. Continue?"),
442                allNodes, null);
443        if(!ok) return;
444
445        //analyze multipolygon relations and collect all areas
446        List<Multipolygon> areas = collectMultipolygons(ways);
447
448        if (areas == null)
449            //too complex multipolygon relations found
450            return;
451
452        if (!testJoin(areas)) {
453            new Notification(
454                    tr("No intersection found. Nothing was changed."))
455                    .setIcon(JOptionPane.INFORMATION_MESSAGE)
456                    .show();
457            return;
458        }
459
460        if (!resolveTagConflicts(areas))
461            return;
462        //user canceled, do nothing.
463
464        try {
465            // see #11026 - Because <ways> is a dynamic filtered (on ways) of a filtered (on selected objects) collection,
466            // retrieve effective dataset before joining the ways (which affects the selection, thus, the <ways> collection)
467            // Dataset retrieving allows to call this code without relying on Main.getCurrentDataSet(), thus, on a mapview instance
468            DataSet ds = ways.iterator().next().getDataSet();
469
470            // Do the job of joining areas
471            JoinAreasResult result = joinAreas(areas);
472
473            if (result.hasChanges) {
474                // move tags from ways to newly created relations
475                // TODO: do we need to also move tags for the modified relations?
476                for (Relation r: addedRelations) {
477                    cmds.addAll(CreateMultipolygonAction.removeTagsFromWaysIfNeeded(r));
478                }
479                commitCommands(tr("Move tags from ways to relations"));
480
481                List<Way> allWays = new ArrayList<>();
482                for (Multipolygon pol : result.polygons) {
483                    allWays.add(pol.outerWay);
484                    allWays.addAll(pol.innerWays);
485                }
486                if (ds != null) {
487                    ds.setSelected(allWays);
488                    Main.map.mapView.repaint();
489                }
490            } else {
491                new Notification(
492                        tr("No intersection found. Nothing was changed."))
493                        .setIcon(JOptionPane.INFORMATION_MESSAGE)
494                        .show();
495            }
496        } catch (UserCancelException exception) {
497            //revert changes
498            //FIXME: this is dirty hack
499            makeCommitsOneAction(tr("Reverting changes"));
500            Main.main.undoRedo.undo();
501            Main.main.undoRedo.redoCommands.clear();
502        }
503    }
504
505    /**
506     * Tests if the areas have some intersections to join.
507     * @param areas Areas to test
508     * @return {@code true} if areas are joinable
509     */
510    private boolean testJoin(List<Multipolygon> areas) {
511        List<Way> allStartingWays = new ArrayList<>();
512
513        for (Multipolygon area : areas) {
514            allStartingWays.add(area.outerWay);
515            allStartingWays.addAll(area.innerWays);
516        }
517
518        //find intersection points
519        Set<Node> nodes = Geometry.addIntersections(allStartingWays, true, cmds);
520        return !nodes.isEmpty();
521    }
522
523    /**
524     * Will join two or more overlapping areas
525     * @param areas list of areas to join
526     * @return new area formed.
527     */
528    private JoinAreasResult joinAreas(List<Multipolygon> areas) throws UserCancelException {
529
530        JoinAreasResult result = new JoinAreasResult();
531        result.hasChanges = false;
532
533        List<Way> allStartingWays = new ArrayList<>();
534        List<Way> innerStartingWays = new ArrayList<>();
535        List<Way> outerStartingWays = new ArrayList<>();
536
537        for (Multipolygon area : areas) {
538            outerStartingWays.add(area.outerWay);
539            innerStartingWays.addAll(area.innerWays);
540        }
541
542        allStartingWays.addAll(innerStartingWays);
543        allStartingWays.addAll(outerStartingWays);
544
545        //first remove nodes in the same coordinate
546        boolean removedDuplicates = false;
547        removedDuplicates |= removeDuplicateNodes(allStartingWays);
548
549        if (removedDuplicates) {
550            result.hasChanges = true;
551            commitCommands(marktr("Removed duplicate nodes"));
552        }
553
554        //find intersection points
555        Set<Node> nodes = Geometry.addIntersections(allStartingWays, false, cmds);
556
557        //no intersections, return.
558        if (nodes.isEmpty())
559            return result;
560        commitCommands(marktr("Added node on all intersections"));
561
562        List<RelationRole> relations = new ArrayList<>();
563
564        // Remove ways from all relations so ways can be combined/split quietly
565        for (Way way : allStartingWays) {
566            relations.addAll(removeFromAllRelations(way));
567        }
568
569        // Don't warn now, because it will really look corrupted
570        boolean warnAboutRelations = !relations.isEmpty() && allStartingWays.size() > 1;
571
572        List<WayInPolygon> preparedWays = new ArrayList<>();
573
574        for (Way way : outerStartingWays) {
575            List<Way> splitWays = splitWayOnNodes(way, nodes);
576            preparedWays.addAll(markWayInsideSide(splitWays, false));
577        }
578
579        for (Way way : innerStartingWays) {
580            List<Way> splitWays = splitWayOnNodes(way, nodes);
581            preparedWays.addAll(markWayInsideSide(splitWays, true));
582        }
583
584        // Find boundary ways
585        List<Way> discardedWays = new ArrayList<>();
586        List<AssembledPolygon> bounadries = findBoundaryPolygons(preparedWays, discardedWays);
587
588        //find polygons
589        List<AssembledMultipolygon> preparedPolygons = findPolygons(bounadries);
590
591
592        //assemble final polygons
593        List<Multipolygon> polygons = new ArrayList<>();
594        Set<Relation> relationsToDelete = new LinkedHashSet<>();
595
596        for (AssembledMultipolygon pol : preparedPolygons) {
597
598            //create the new ways
599            Multipolygon resultPol = joinPolygon(pol);
600
601            //create multipolygon relation, if necessary.
602            RelationRole ownMultipolygonRelation = addOwnMultigonRelation(resultPol.innerWays, resultPol.outerWay);
603
604            //add back the original relations, merged with our new multipolygon relation
605            fixRelations(relations, resultPol.outerWay, ownMultipolygonRelation, relationsToDelete);
606
607            //strip tags from inner ways
608            //TODO: preserve tags on existing inner ways
609            stripTags(resultPol.innerWays);
610
611            polygons.add(resultPol);
612        }
613
614        commitCommands(marktr("Assemble new polygons"));
615
616        for(Relation rel: relationsToDelete) {
617            cmds.add(new DeleteCommand(rel));
618        }
619
620        commitCommands(marktr("Delete relations"));
621
622        // Delete the discarded inner ways
623        if (!discardedWays.isEmpty()) {
624            Command deleteCmd = DeleteCommand.delete(Main.main.getEditLayer(), discardedWays, true);
625            if (deleteCmd != null) {
626                cmds.add(deleteCmd);
627                commitCommands(marktr("Delete Ways that are not part of an inner multipolygon"));
628            }
629        }
630
631        makeCommitsOneAction(marktr("Joined overlapping areas"));
632
633        if (warnAboutRelations) {
634            new Notification(
635                    tr("Some of the ways were part of relations that have been modified.<br>Please verify no errors have been introduced."))
636                    .setIcon(JOptionPane.INFORMATION_MESSAGE)
637                    .setDuration(Notification.TIME_LONG)
638                    .show();
639        }
640
641        result.hasChanges = true;
642        result.polygons = polygons;
643        return result;
644    }
645
646    /**
647     * Checks if tags of two given ways differ, and presents the user a dialog to solve conflicts
648     * @param polygons ways to check
649     * @return {@code true} if all conflicts are resolved, {@code false} if conflicts remain.
650     */
651    private boolean resolveTagConflicts(List<Multipolygon> polygons) {
652
653        List<Way> ways = new ArrayList<>();
654
655        for (Multipolygon pol : polygons) {
656            ways.add(pol.outerWay);
657            ways.addAll(pol.innerWays);
658        }
659
660        if (ways.size() < 2) {
661            return true;
662        }
663
664        TagCollection wayTags = TagCollection.unionOfAllPrimitives(ways);
665        try {
666            cmds.addAll(CombinePrimitiveResolverDialog.launchIfNecessary(wayTags, ways, ways));
667            commitCommands(marktr("Fix tag conflicts"));
668            return true;
669        } catch (UserCancelException ex) {
670            return false;
671        }
672    }
673
674    /**
675     * This method removes duplicate points (if any) from the input way.
676     * @param ways the ways to process
677     * @return {@code true} if any changes where made
678     */
679    private boolean removeDuplicateNodes(List<Way> ways) {
680        //TODO: maybe join nodes with JoinNodesAction, rather than reconnect the ways.
681
682        Map<Node, Node> nodeMap = new TreeMap<>(new NodePositionComparator());
683        int totalNodesRemoved = 0;
684
685        for (Way way : ways) {
686            if (way.getNodes().size() < 2) {
687                continue;
688            }
689
690            int nodesRemoved = 0;
691            List<Node> newNodes = new ArrayList<>();
692            Node prevNode = null;
693
694            for (Node node : way.getNodes()) {
695                if (!nodeMap.containsKey(node)) {
696                    //new node
697                    nodeMap.put(node, node);
698
699                    //avoid duplicate nodes
700                    if (prevNode != node) {
701                        newNodes.add(node);
702                    } else {
703                        nodesRemoved ++;
704                    }
705                } else {
706                    //node with same coordinates already exists, substitute with existing node
707                    Node representator = nodeMap.get(node);
708
709                    if (representator != node) {
710                        nodesRemoved ++;
711                    }
712
713                    //avoid duplicate node
714                    if (prevNode != representator) {
715                        newNodes.add(representator);
716                    }
717                }
718                prevNode = node;
719            }
720
721            if (nodesRemoved > 0) {
722
723                if (newNodes.size() == 1) { //all nodes in the same coordinate - add one more node, to have closed way.
724                    newNodes.add(newNodes.get(0));
725                }
726
727                Way newWay=new Way(way);
728                newWay.setNodes(newNodes);
729                cmds.add(new ChangeCommand(way, newWay));
730                totalNodesRemoved += nodesRemoved;
731            }
732        }
733
734        return totalNodesRemoved > 0;
735    }
736
737    /**
738     * Commits the command list with a description
739     * @param description The description of what the commands do
740     */
741    private void commitCommands(String description) {
742        switch(cmds.size()) {
743        case 0:
744            return;
745        case 1:
746            Main.main.undoRedo.add(cmds.getFirst());
747            break;
748        default:
749            Command c = new SequenceCommand(tr(description), cmds);
750            Main.main.undoRedo.add(c);
751            break;
752        }
753
754        cmds.clear();
755        cmdsCount++;
756    }
757
758    /**
759     * This method analyzes the way and assigns each part what direction polygon "inside" is.
760     * @param parts the split parts of the way
761     * @param isInner - if true, reverts the direction (for multipolygon islands)
762     * @return list of parts, marked with the inside orientation.
763     */
764    private List<WayInPolygon> markWayInsideSide(List<Way> parts, boolean isInner) {
765
766        List<WayInPolygon> result = new ArrayList<>();
767
768        //prepare prev and next maps
769        Map<Way, Way> nextWayMap = new HashMap<>();
770        Map<Way, Way> prevWayMap = new HashMap<>();
771
772        for (int pos = 0; pos < parts.size(); pos ++) {
773
774            if (!parts.get(pos).lastNode().equals(parts.get((pos + 1) % parts.size()).firstNode()))
775                throw new RuntimeException("Way not circular");
776
777            nextWayMap.put(parts.get(pos), parts.get((pos + 1) % parts.size()));
778            prevWayMap.put(parts.get(pos), parts.get((pos + parts.size() - 1) % parts.size()));
779        }
780
781        //find the node with minimum y - it's guaranteed to be outer. (What about the south pole?)
782        Way topWay = null;
783        Node topNode = null;
784        int topIndex = 0;
785        double minY = Double.POSITIVE_INFINITY;
786
787        for (Way way : parts) {
788            for (int pos = 0; pos < way.getNodesCount(); pos ++) {
789                Node node = way.getNode(pos);
790
791                if (node.getEastNorth().getY() < minY) {
792                    minY = node.getEastNorth().getY();
793                    topWay = way;
794                    topNode = node;
795                    topIndex = pos;
796                }
797            }
798        }
799
800        //get the upper way and it's orientation.
801
802        boolean wayClockwise; // orientation of the top way.
803
804        if (topNode.equals(topWay.firstNode()) || topNode.equals(topWay.lastNode())) {
805            Node headNode = null; // the node at junction
806            Node prevNode = null; // last node from previous path
807            wayClockwise = false;
808
809            //node is in split point - find the outermost way from this point
810
811            headNode = topNode;
812            //make a fake node that is downwards from head node (smaller Y). It will be a division point between paths.
813            prevNode = new Node(new EastNorth(headNode.getEastNorth().getX(), headNode.getEastNorth().getY() - 1e5));
814
815            topWay = null;
816            wayClockwise = false;
817            Node bestWayNextNode = null;
818
819            for (Way way : parts) {
820                if (way.firstNode().equals(headNode)) {
821                    Node nextNode = way.getNode(1);
822
823                    if (topWay == null || !Geometry.isToTheRightSideOfLine(prevNode, headNode, bestWayNextNode, nextNode)) {
824                        //the new way is better
825                        topWay = way;
826                        wayClockwise = true;
827                        bestWayNextNode = nextNode;
828                    }
829                }
830
831                if (way.lastNode().equals(headNode)) {
832                    //end adjacent to headNode
833                    Node nextNode = way.getNode(way.getNodesCount() - 2);
834
835                    if (topWay == null || !Geometry.isToTheRightSideOfLine(prevNode, headNode, bestWayNextNode, nextNode)) {
836                        //the new way is better
837                        topWay = way;
838                        wayClockwise = false;
839                        bestWayNextNode = nextNode;
840                    }
841                }
842            }
843        } else {
844            //node is inside way - pick the clockwise going end.
845            Node prev = topWay.getNode(topIndex - 1);
846            Node next = topWay.getNode(topIndex + 1);
847
848            //there will be no parallel segments in the middle of way, so all fine.
849            wayClockwise = Geometry.angleIsClockwise(prev, topNode, next);
850        }
851
852        Way curWay = topWay;
853        boolean curWayInsideToTheRight = wayClockwise ^ isInner;
854
855        //iterate till full circle is reached
856        while (true) {
857
858            //add cur way
859            WayInPolygon resultWay = new WayInPolygon(curWay, curWayInsideToTheRight);
860            result.add(resultWay);
861
862            //process next way
863            Way nextWay = nextWayMap.get(curWay);
864            Node prevNode = curWay.getNode(curWay.getNodesCount() - 2);
865            Node headNode = curWay.lastNode();
866            Node nextNode = nextWay.getNode(1);
867
868            if (nextWay == topWay) {
869                //full loop traversed - all done.
870                break;
871            }
872
873            //find intersecting segments
874            // the intersections will look like this:
875            //
876            //                       ^
877            //                       |
878            //                       X wayBNode
879            //                       |
880            //                  wayB |
881            //                       |
882            //             curWay    |       nextWay
883            //----X----------------->X----------------------X---->
884            //    prevNode           ^headNode              nextNode
885            //                       |
886            //                       |
887            //                  wayA |
888            //                       |
889            //                       X wayANode
890            //                       |
891
892            int intersectionCount = 0;
893
894            for (Way wayA : parts) {
895
896                if (wayA == curWay) {
897                    continue;
898                }
899
900                if (wayA.lastNode().equals(headNode)) {
901
902                    Way wayB = nextWayMap.get(wayA);
903
904                    //test if wayA is opposite wayB relative to curWay and nextWay
905
906                    Node wayANode = wayA.getNode(wayA.getNodesCount() - 2);
907                    Node wayBNode = wayB.getNode(1);
908
909                    boolean wayAToTheRight = Geometry.isToTheRightSideOfLine(prevNode, headNode, nextNode, wayANode);
910                    boolean wayBToTheRight = Geometry.isToTheRightSideOfLine(prevNode, headNode, nextNode, wayBNode);
911
912                    if (wayAToTheRight != wayBToTheRight) {
913                        intersectionCount ++;
914                    }
915                }
916            }
917
918            //if odd number of crossings, invert orientation
919            if (intersectionCount % 2 != 0) {
920                curWayInsideToTheRight = !curWayInsideToTheRight;
921            }
922
923            curWay = nextWay;
924        }
925
926        return result;
927    }
928
929    /**
930     * This is a method splits way into smaller parts, using the prepared nodes list as split points.
931     * Uses {@link SplitWayAction#splitWay} for the heavy lifting.
932     * @return list of split ways (or original ways if no splitting is done).
933     */
934    private List<Way> splitWayOnNodes(Way way, Set<Node> nodes) {
935
936        List<Way> result = new ArrayList<>();
937        List<List<Node>> chunks = buildNodeChunks(way, nodes);
938
939        if (chunks.size() > 1) {
940            SplitWayResult split = SplitWayAction.splitWay(getEditLayer(), way, chunks, Collections.<OsmPrimitive>emptyList());
941
942            //execute the command, we need the results
943            cmds.add(split.getCommand());
944            commitCommands(marktr("Split ways into fragments"));
945
946            result.add(split.getOriginalWay());
947            result.addAll(split.getNewWays());
948        } else {
949            //nothing to split
950            result.add(way);
951        }
952
953        return result;
954    }
955
956    /**
957     * Simple chunking version. Does not care about circular ways and result being
958     * proper, we will glue it all back together later on.
959     * @param way the way to chunk
960     * @param splitNodes the places where to cut.
961     * @return list of node paths to produce.
962     */
963    private List<List<Node>> buildNodeChunks(Way way, Collection<Node> splitNodes) {
964        List<List<Node>> result = new ArrayList<>();
965        List<Node> curList = new ArrayList<>();
966
967        for (Node node : way.getNodes()) {
968            curList.add(node);
969            if (curList.size() > 1 && splitNodes.contains(node)) {
970                result.add(curList);
971                curList = new ArrayList<>();
972                curList.add(node);
973            }
974        }
975
976        if (curList.size() > 1) {
977            result.add(curList);
978        }
979
980        return result;
981    }
982
983    /**
984     * This method finds which ways are outer and which are inner.
985     * @param boundaries list of joined boundaries to search in
986     * @return outer ways
987     */
988    private List<AssembledMultipolygon> findPolygons(Collection<AssembledPolygon> boundaries) {
989
990        List<PolygonLevel> list = findOuterWaysImpl(0, boundaries);
991        List<AssembledMultipolygon> result = new ArrayList<>();
992
993        //take every other level
994        for (PolygonLevel pol : list) {
995            if (pol.level % 2 == 0) {
996                result.add(pol.pol);
997            }
998        }
999
1000        return result;
1001    }
1002
1003    /**
1004     * Collects outer way and corresponding inner ways from all boundaries.
1005     * @param level depth level
1006     * @param boundaryWays
1007     * @return the outermostWay.
1008     */
1009    private List<PolygonLevel> findOuterWaysImpl(int level, Collection<AssembledPolygon> boundaryWays) {
1010
1011        //TODO: bad performance for deep nestings...
1012        List<PolygonLevel> result = new ArrayList<>();
1013
1014        for (AssembledPolygon outerWay : boundaryWays) {
1015
1016            boolean outerGood = true;
1017            List<AssembledPolygon> innerCandidates = new ArrayList<>();
1018
1019            for (AssembledPolygon innerWay : boundaryWays) {
1020                if (innerWay == outerWay) {
1021                    continue;
1022                }
1023
1024                if (wayInsideWay(outerWay, innerWay)) {
1025                    outerGood = false;
1026                    break;
1027                } else if (wayInsideWay(innerWay, outerWay)) {
1028                    innerCandidates.add(innerWay);
1029                }
1030            }
1031
1032            if (!outerGood) {
1033                continue;
1034            }
1035
1036            //add new outer polygon
1037            AssembledMultipolygon pol = new AssembledMultipolygon(outerWay);
1038            PolygonLevel polLev = new PolygonLevel(pol, level);
1039
1040            //process inner ways
1041            if (!innerCandidates.isEmpty()) {
1042                List<PolygonLevel> innerList = findOuterWaysImpl(level + 1, innerCandidates);
1043                result.addAll(innerList);
1044
1045                for (PolygonLevel pl : innerList) {
1046                    if (pl.level == level + 1) {
1047                        pol.innerWays.add(pl.pol.outerWay);
1048                    }
1049                }
1050            }
1051
1052            result.add(polLev);
1053        }
1054
1055        return result;
1056    }
1057
1058    /**
1059     * Finds all ways that form inner or outer boundaries.
1060     * @param multigonWays A list of (splitted) ways that form a multigon and share common end nodes on intersections.
1061     * @param discardedResult this list is filled with ways that are to be discarded
1062     * @return A list of ways that form the outer and inner boundaries of the multigon.
1063     */
1064    public static List<AssembledPolygon> findBoundaryPolygons(Collection<WayInPolygon> multigonWays,
1065            List<Way> discardedResult) {
1066        //first find all discardable ways, by getting outer shells.
1067        //this will produce incorrect boundaries in some cases, but second pass will fix it.
1068        List<WayInPolygon> discardedWays = new ArrayList<>();
1069
1070        // In multigonWays collection, some way are just a point (i.e. way like nodeA-nodeA)
1071        // This seems to appear when is apply over invalid way like #9911 test-case
1072        // Remove all of these way to make the next work.
1073        ArrayList<WayInPolygon> cleanMultigonWays = new ArrayList<>();
1074        for(WayInPolygon way: multigonWays)
1075            if(way.way.getNodesCount() == 2 && way.way.firstNode() == way.way.lastNode())
1076                discardedWays.add(way);
1077            else
1078                cleanMultigonWays.add(way);
1079
1080        WayTraverser traverser = new WayTraverser(cleanMultigonWays);
1081        List<AssembledPolygon> result = new ArrayList<>();
1082
1083        WayInPolygon startWay;
1084        while((startWay = traverser.startNewWay()) != null) {
1085            ArrayList<WayInPolygon> path = new ArrayList<>();
1086            List<WayInPolygon> startWays = new ArrayList<>();
1087            path.add(startWay);
1088            while(true) {
1089                WayInPolygon leftComing;
1090                while((leftComing = traverser.leftComingWay()) != null) {
1091                    if(startWays.contains(leftComing))
1092                        break;
1093                    // Need restart traverser walk
1094                    path.clear();
1095                    path.add(leftComing);
1096                    traverser.setStartWay(leftComing);
1097                    startWays.add(leftComing);
1098                    break;
1099                }
1100                WayInPolygon nextWay = traverser.walk();
1101                if(nextWay == null)
1102                    throw new RuntimeException("Join areas internal error.");
1103                if(path.get(0) == nextWay) {
1104                    // path is closed -> stop here
1105                    AssembledPolygon ring = new AssembledPolygon(path);
1106                    if(ring.getNodes().size() <= 2) {
1107                        // Invalid ring (2 nodes) -> remove
1108                        traverser.removeWays(path);
1109                        for(WayInPolygon way: path)
1110                            discardedResult.add(way.way);
1111                    } else {
1112                        // Close ring -> add
1113                        result.add(ring);
1114                        traverser.removeWays(path);
1115                    }
1116                    break;
1117                }
1118                if(path.contains(nextWay)) {
1119                    // Inner loop -> remove
1120                    int index = path.indexOf(nextWay);
1121                    while(path.size() > index) {
1122                        WayInPolygon currentWay = path.get(index);
1123                        discardedResult.add(currentWay.way);
1124                        traverser.removeWay(currentWay);
1125                        path.remove(index);
1126                    }
1127                    traverser.setStartWay(path.get(index-1));
1128                } else {
1129                    path.add(nextWay);
1130                }
1131            }
1132        }
1133
1134        return fixTouchingPolygons(result);
1135    }
1136
1137    /**
1138     * This method checks if polygons have several touching parts and splits them in several polygons.
1139     * @param polygons the polygons to process.
1140     */
1141    public static List<AssembledPolygon> fixTouchingPolygons(List<AssembledPolygon> polygons) {
1142        List<AssembledPolygon> newPolygons = new ArrayList<>();
1143
1144        for (AssembledPolygon ring : polygons) {
1145            ring.reverse();
1146            WayTraverser traverser = new WayTraverser(ring.ways);
1147            WayInPolygon startWay;
1148
1149            while((startWay = traverser.startNewWay()) != null) {
1150                List<WayInPolygon> simpleRingWays = new ArrayList<>();
1151                simpleRingWays.add(startWay);
1152                WayInPolygon nextWay;
1153                while((nextWay = traverser.walk()) != startWay) {
1154                    if(nextWay == null)
1155                        throw new RuntimeException("Join areas internal error.");
1156                    simpleRingWays.add(nextWay);
1157                }
1158                traverser.removeWays(simpleRingWays);
1159                AssembledPolygon simpleRing = new AssembledPolygon(simpleRingWays);
1160                simpleRing.reverse();
1161                newPolygons.add(simpleRing);
1162            }
1163        }
1164
1165        return newPolygons;
1166    }
1167
1168    /**
1169     * Tests if way is inside other way
1170     * @param outside outer polygon description
1171     * @param inside inner polygon description
1172     * @return {@code true} if inner is inside outer
1173     */
1174    public static boolean wayInsideWay(AssembledPolygon inside, AssembledPolygon outside) {
1175        Set<Node> outsideNodes = new HashSet<>(outside.getNodes());
1176        List<Node> insideNodes = inside.getNodes();
1177
1178        for (Node insideNode : insideNodes) {
1179
1180            if (!outsideNodes.contains(insideNode))
1181                //simply test the one node
1182                return Geometry.nodeInsidePolygon(insideNode, outside.getNodes());
1183        }
1184
1185        //all nodes shared.
1186        return false;
1187    }
1188
1189    /**
1190     * Joins the lists of ways.
1191     * @param polygon The list of outer ways that belong to that multigon.
1192     * @return The newly created outer way
1193     */
1194    private Multipolygon  joinPolygon(AssembledMultipolygon polygon) throws UserCancelException {
1195        Multipolygon result = new Multipolygon(joinWays(polygon.outerWay.ways));
1196
1197        for (AssembledPolygon pol : polygon.innerWays) {
1198            result.innerWays.add(joinWays(pol.ways));
1199        }
1200
1201        return result;
1202    }
1203
1204    /**
1205     * Joins the outer ways and deletes all short ways that can't be part of a multipolygon anyway.
1206     * @param ways The list of outer ways that belong to that multigon.
1207     * @return The newly created outer way
1208     */
1209    private Way joinWays(List<WayInPolygon> ways) throws UserCancelException {
1210
1211        //leave original orientation, if all paths are reverse.
1212        boolean allReverse = true;
1213        for (WayInPolygon way : ways) {
1214            allReverse &= !way.insideToTheRight;
1215        }
1216
1217        if (allReverse) {
1218            for (WayInPolygon way : ways) {
1219                way.insideToTheRight = !way.insideToTheRight;
1220            }
1221        }
1222
1223        Way joinedWay = joinOrientedWays(ways);
1224
1225        //should not happen
1226        if (joinedWay == null || !joinedWay.isClosed())
1227            throw new RuntimeException("Join areas internal error.");
1228
1229        return joinedWay;
1230    }
1231
1232    /**
1233     * Joins a list of ways (using CombineWayAction and ReverseWayAction as specified in WayInPath)
1234     * @param ways The list of ways to join and reverse
1235     * @return The newly created way
1236     */
1237    private Way joinOrientedWays(List<WayInPolygon> ways) throws UserCancelException{
1238        if (ways.size() < 2)
1239            return ways.get(0).way;
1240
1241        // This will turn ways so all of them point in the same direction and CombineAction won't bug
1242        // the user about this.
1243
1244        //TODO: ReverseWay and Combine way are really slow and we use them a lot here. This slows down large joins.
1245        List<Way> actionWays = new ArrayList<>(ways.size());
1246
1247        for (WayInPolygon way : ways) {
1248            actionWays.add(way.way);
1249
1250            if (!way.insideToTheRight) {
1251                ReverseWayResult res = ReverseWayAction.reverseWay(way.way);
1252                Main.main.undoRedo.add(res.getReverseCommand());
1253                cmdsCount++;
1254            }
1255        }
1256
1257        Pair<Way, Command> result = CombineWayAction.combineWaysWorker(actionWays);
1258
1259        Main.main.undoRedo.add(result.b);
1260        cmdsCount ++;
1261
1262        return result.a;
1263    }
1264
1265    /**
1266     * This method analyzes multipolygon relationships of given ways and collects addition inner ways to consider.
1267     * @param selectedWays the selected ways
1268     * @return list of polygons, or null if too complex relation encountered.
1269     */
1270    private List<Multipolygon> collectMultipolygons(Collection<Way> selectedWays) {
1271
1272        List<Multipolygon> result = new ArrayList<>();
1273
1274        //prepare the lists, to minimize memory allocation.
1275        List<Way> outerWays = new ArrayList<>();
1276        List<Way> innerWays = new ArrayList<>();
1277
1278        Set<Way> processedOuterWays = new LinkedHashSet<>();
1279        Set<Way> processedInnerWays = new LinkedHashSet<>();
1280
1281        for (Relation r : OsmPrimitive.getParentRelations(selectedWays)) {
1282            if (r.isDeleted() || !r.isMultipolygon()) {
1283                continue;
1284            }
1285
1286            boolean hasKnownOuter = false;
1287            outerWays.clear();
1288            innerWays.clear();
1289
1290            for (RelationMember rm : r.getMembers()) {
1291                if ("outer".equalsIgnoreCase(rm.getRole())) {
1292                    outerWays.add(rm.getWay());
1293                    hasKnownOuter |= selectedWays.contains(rm.getWay());
1294                }
1295                else if ("inner".equalsIgnoreCase(rm.getRole())) {
1296                    innerWays.add(rm.getWay());
1297                }
1298            }
1299
1300            if (!hasKnownOuter) {
1301                continue;
1302            }
1303
1304            if (outerWays.size() > 1) {
1305                new Notification(
1306                        tr("Sorry. Cannot handle multipolygon relations with multiple outer ways."))
1307                        .setIcon(JOptionPane.INFORMATION_MESSAGE)
1308                        .show();
1309                return null;
1310            }
1311
1312            Way outerWay = outerWays.get(0);
1313
1314            //retain only selected inner ways
1315            innerWays.retainAll(selectedWays);
1316
1317            if (processedOuterWays.contains(outerWay)) {
1318                new Notification(
1319                        tr("Sorry. Cannot handle way that is outer in multiple multipolygon relations."))
1320                        .setIcon(JOptionPane.INFORMATION_MESSAGE)
1321                        .show();
1322                return null;
1323            }
1324
1325            if (processedInnerWays.contains(outerWay)) {
1326                new Notification(
1327                        tr("Sorry. Cannot handle way that is both inner and outer in multipolygon relations."))
1328                        .setIcon(JOptionPane.INFORMATION_MESSAGE)
1329                        .show();
1330                return null;
1331            }
1332
1333            for (Way way :innerWays)
1334            {
1335                if (processedOuterWays.contains(way)) {
1336                    new Notification(
1337                            tr("Sorry. Cannot handle way that is both inner and outer in multipolygon relations."))
1338                            .setIcon(JOptionPane.INFORMATION_MESSAGE)
1339                            .show();
1340                    return null;
1341                }
1342
1343                if (processedInnerWays.contains(way)) {
1344                    new Notification(
1345                            tr("Sorry. Cannot handle way that is inner in multiple multipolygon relations."))
1346                            .setIcon(JOptionPane.INFORMATION_MESSAGE)
1347                            .show();
1348                    return null;
1349                }
1350            }
1351
1352            processedOuterWays.add(outerWay);
1353            processedInnerWays.addAll(innerWays);
1354
1355            Multipolygon pol = new Multipolygon(outerWay);
1356            pol.innerWays.addAll(innerWays);
1357
1358            result.add(pol);
1359        }
1360
1361        //add remaining ways, not in relations
1362        for (Way way : selectedWays) {
1363            if (processedOuterWays.contains(way) || processedInnerWays.contains(way)) {
1364                continue;
1365            }
1366
1367            result.add(new Multipolygon(way));
1368        }
1369
1370        return result;
1371    }
1372
1373    /**
1374     * Will add own multipolygon relation to the "previously existing" relations. Fixup is done by fixRelations
1375     * @param inner List of already closed inner ways
1376     * @param outer The outer way
1377     * @return The list of relation with roles to add own relation to
1378     */
1379    private RelationRole addOwnMultigonRelation(Collection<Way> inner, Way outer) {
1380        if (inner.isEmpty()) return null;
1381        // Create new multipolygon relation and add all inner ways to it
1382        Relation newRel = new Relation();
1383        newRel.put("type", "multipolygon");
1384        for (Way w : inner) {
1385            newRel.addMember(new RelationMember("inner", w));
1386        }
1387        cmds.add(new AddCommand(newRel));
1388        addedRelations.add(newRel);
1389
1390        // We don't add outer to the relation because it will be handed to fixRelations()
1391        // which will then do the remaining work.
1392        return new RelationRole(newRel, "outer");
1393    }
1394
1395    /**
1396     * Removes a given OsmPrimitive from all relations.
1397     * @param osm Element to remove from all relations
1398     * @return List of relations with roles the primitives was part of
1399     */
1400    private List<RelationRole> removeFromAllRelations(OsmPrimitive osm) {
1401        List<RelationRole> result = new ArrayList<>();
1402
1403        for (Relation r : Main.main.getCurrentDataSet().getRelations()) {
1404            if (r.isDeleted()) {
1405                continue;
1406            }
1407            for (RelationMember rm : r.getMembers()) {
1408                if (rm.getMember() != osm) {
1409                    continue;
1410                }
1411
1412                Relation newRel = new Relation(r);
1413                List<RelationMember> members = newRel.getMembers();
1414                members.remove(rm);
1415                newRel.setMembers(members);
1416
1417                cmds.add(new ChangeCommand(r, newRel));
1418                RelationRole saverel =  new RelationRole(r, rm.getRole());
1419                if (!result.contains(saverel)) {
1420                    result.add(saverel);
1421                }
1422                break;
1423            }
1424        }
1425
1426        commitCommands(marktr("Removed Element from Relations"));
1427        return result;
1428    }
1429
1430    /**
1431     * Adds the previously removed relations again to the outer way. If there are multiple multipolygon
1432     * relations where the joined areas were in "outer" role a new relation is created instead with all
1433     * members of both. This function depends on multigon relations to be valid already, it won't fix them.
1434     * @param rels List of relations with roles the (original) ways were part of
1435     * @param outer The newly created outer area/way
1436     * @param ownMultipol elements to directly add as outer
1437     * @param relationsToDelete set of relations to delete.
1438     */
1439    private void fixRelations(List<RelationRole> rels, Way outer, RelationRole ownMultipol, Set<Relation> relationsToDelete) {
1440        List<RelationRole> multiouters = new ArrayList<>();
1441
1442        if (ownMultipol != null) {
1443            multiouters.add(ownMultipol);
1444        }
1445
1446        for (RelationRole r : rels) {
1447            if (r.rel.isMultipolygon() && "outer".equalsIgnoreCase(r.role)) {
1448                multiouters.add(r);
1449                continue;
1450            }
1451            // Add it back!
1452            Relation newRel = new Relation(r.rel);
1453            newRel.addMember(new RelationMember(r.role, outer));
1454            cmds.add(new ChangeCommand(r.rel, newRel));
1455        }
1456
1457        Relation newRel;
1458        switch (multiouters.size()) {
1459        case 0:
1460            return;
1461        case 1:
1462            // Found only one to be part of a multipolygon relation, so just add it back as well
1463            newRel = new Relation(multiouters.get(0).rel);
1464            newRel.addMember(new RelationMember(multiouters.get(0).role, outer));
1465            cmds.add(new ChangeCommand(multiouters.get(0).rel, newRel));
1466            return;
1467        default:
1468            // Create a new relation with all previous members and (Way)outer as outer.
1469            newRel = new Relation();
1470            for (RelationRole r : multiouters) {
1471                // Add members
1472                for (RelationMember rm : r.rel.getMembers())
1473                    if (!newRel.getMembers().contains(rm)) {
1474                        newRel.addMember(rm);
1475                    }
1476                // Add tags
1477                for (String key : r.rel.keySet()) {
1478                    newRel.put(key, r.rel.get(key));
1479                }
1480                // Delete old relation
1481                relationsToDelete.add(r.rel);
1482            }
1483            newRel.addMember(new RelationMember("outer", outer));
1484            cmds.add(new AddCommand(newRel));
1485        }
1486    }
1487
1488    /**
1489     * Remove all tags from the all the way
1490     * @param ways The List of Ways to remove all tags from
1491     */
1492    private void stripTags(Collection<Way> ways) {
1493        for (Way w : ways) {
1494            stripTags(w);
1495        }
1496        /* I18N: current action printed in status display */
1497        commitCommands(marktr("Remove tags from inner ways"));
1498    }
1499
1500    /**
1501     * Remove all tags from the way
1502     * @param x The Way to remove all tags from
1503     */
1504    private void stripTags(Way x) {
1505        Way y = new Way(x);
1506        for (String key : x.keySet()) {
1507            y.remove(key);
1508        }
1509        cmds.add(new ChangeCommand(x, y));
1510    }
1511
1512    /**
1513     * Takes the last cmdsCount actions back and combines them into a single action
1514     * (for when the user wants to undo the join action)
1515     * @param message The commit message to display
1516     */
1517    private void makeCommitsOneAction(String message) {
1518        UndoRedoHandler ur = Main.main.undoRedo;
1519        cmds.clear();
1520        int i = Math.max(ur.commands.size() - cmdsCount, 0);
1521        for (; i < ur.commands.size(); i++) {
1522            cmds.add(ur.commands.get(i));
1523        }
1524
1525        for (i = 0; i < cmds.size(); i++) {
1526            ur.undo();
1527        }
1528
1529        commitCommands(message == null ? marktr("Join Areas Function") : message);
1530        cmdsCount = 0;
1531    }
1532
1533    @Override
1534    protected void updateEnabledState() {
1535        if (getCurrentDataSet() == null) {
1536            setEnabled(false);
1537        } else {
1538            updateEnabledState(getCurrentDataSet().getSelected());
1539        }
1540    }
1541
1542    @Override
1543    protected void updateEnabledState(Collection<? extends OsmPrimitive> selection) {
1544        setEnabled(selection != null && !selection.isEmpty());
1545    }
1546}