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