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