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