Index: src/org/openstreetmap/josm/actions/JoinAreasAction.java
===================================================================
--- src/org/openstreetmap/josm/actions/JoinAreasAction.java	(Revision 11819)
+++ src/org/openstreetmap/josm/actions/JoinAreasAction.java	(Arbeitskopie)
@@ -1,10 +1,10 @@
 // License: GPL. For details, see LICENSE file.
 package org.openstreetmap.josm.actions;
 
-import static org.openstreetmap.josm.tools.I18n.marktr;
 import static org.openstreetmap.josm.tools.I18n.tr;
 import static org.openstreetmap.josm.tools.I18n.trn;
 
+import java.awt.GridBagLayout;
 import java.awt.event.ActionEvent;
 import java.awt.event.KeyEvent;
 import java.util.ArrayList;
@@ -11,54 +11,43 @@
 import java.util.Collection;
 import java.util.Collections;
 import java.util.Comparator;
-import java.util.EnumSet;
+import java.util.HashMap;
 import java.util.HashSet;
-import java.util.LinkedHashSet;
 import java.util.LinkedList;
 import java.util.List;
 import java.util.Map;
-import java.util.Objects;
 import java.util.Optional;
-import java.util.Set;
-import java.util.TreeMap;
-import java.util.function.BiConsumer;
-import java.util.function.BinaryOperator;
-import java.util.function.Function;
-import java.util.function.Supplier;
 import java.util.function.ToDoubleFunction;
-import java.util.stream.Collector;
 import java.util.stream.Collectors;
 import java.util.stream.Stream;
 
 import javax.swing.JOptionPane;
+import javax.swing.JPanel;
 
 import org.openstreetmap.josm.Main;
-import org.openstreetmap.josm.actions.ReverseWayAction.ReverseWayResult;
-import org.openstreetmap.josm.actions.SplitWayAction.SplitWayResult;
 import org.openstreetmap.josm.command.AddCommand;
 import org.openstreetmap.josm.command.ChangeCommand;
+import org.openstreetmap.josm.command.ChangePropertyCommand;
 import org.openstreetmap.josm.command.Command;
 import org.openstreetmap.josm.command.DeleteCommand;
 import org.openstreetmap.josm.command.SequenceCommand;
-import org.openstreetmap.josm.data.UndoRedoHandler;
 import org.openstreetmap.josm.data.coor.EastNorth;
 import org.openstreetmap.josm.data.osm.DataSet;
 import org.openstreetmap.josm.data.osm.Node;
-import org.openstreetmap.josm.data.osm.NodePositionComparator;
 import org.openstreetmap.josm.data.osm.OsmPrimitive;
+import org.openstreetmap.josm.data.osm.OsmPrimitiveType;
 import org.openstreetmap.josm.data.osm.Relation;
-import org.openstreetmap.josm.data.osm.RelationMember;
 import org.openstreetmap.josm.data.osm.TagCollection;
 import org.openstreetmap.josm.data.osm.Way;
+import org.openstreetmap.josm.gui.ConditionalOptionPaneUtil;
+import org.openstreetmap.josm.gui.DefaultNameFormatter;
 import org.openstreetmap.josm.gui.Notification;
 import org.openstreetmap.josm.gui.conflict.tags.CombinePrimitiveResolverDialog;
-import org.openstreetmap.josm.gui.layer.OsmDataLayer;
+import org.openstreetmap.josm.gui.widgets.JMultilineLabel;
 import org.openstreetmap.josm.tools.Geometry;
-import org.openstreetmap.josm.tools.JosmRuntimeException;
 import org.openstreetmap.josm.tools.Pair;
 import org.openstreetmap.josm.tools.Shortcut;
 import org.openstreetmap.josm.tools.UserCancelException;
-import org.openstreetmap.josm.tools.Utils;
 import org.openstreetmap.josm.tools.bugreport.BugReport;
 
 /**
@@ -66,1520 +55,716 @@
  * @since 2575
  */
 public class JoinAreasAction extends JosmAction {
-    // This will be used to commit commands and unite them into one large command sequence at the end
-    private final transient LinkedList<Command> cmds = new LinkedList<>();
-    private int cmdsCount;
-    private final transient List<Relation> addedRelations = new LinkedList<>();
-
     /**
-     * This helper class describes join areas action result.
-     * @author viesturs
+     * Defines an exception while joining areas.
+     * @author Michael Zangl
      */
-    public static class JoinAreasResult {
-
-        private final boolean hasChanges;
-        private final List<Multipolygon> polygons;
-
-        /**
-         * Constructs a new {@code JoinAreasResult}.
-         * @param hasChanges whether the result has changes
-         * @param polygons the result polygons, can be null
-         */
-        public JoinAreasResult(boolean hasChanges, List<Multipolygon> polygons) {
-            this.hasChanges = hasChanges;
-            this.polygons = polygons;
+    public static class JoinAreasException extends Exception {
+        protected JoinAreasException(String message) {
+            super(message);
         }
+    }
 
-        /**
-         * Determines if the result has changes.
-         * @return {@code true} if the result has changes
-         */
-        public final boolean hasChanges() {
-            return hasChanges;
-        }
+    static class UnclosedAreaException extends JoinAreasException {
+        private Pair<Node, Node> gap;
 
-        /**
-         * Returns the result polygons, can be null.
-         * @return the result polygons, can be null
-         */
-        public final List<Multipolygon> getPolygons() {
-            return polygons;
+        public UnclosedAreaException(Pair<Node, Node> gap) {
+            super("Gap found between: " + gap.a + " and " + gap.b);
+            this.gap = gap;
         }
     }
 
-    public static class Multipolygon {
-        private final Way outerWay;
-        private final List<Way> innerWays;
+    static class SelfIntersectingAreaException extends JoinAreasException {
+        private Pair<UndirectedWaySegment, UndirectedWaySegment> intersect;
 
-        /**
-         * Constructs a new {@code Multipolygon}.
-         * @param way outer way
-         */
-        public Multipolygon(Way way) {
-            outerWay = way;
-            innerWays = new ArrayList<>();
+        public SelfIntersectingAreaException(Pair<UndirectedWaySegment, UndirectedWaySegment> intersect) {
+            super("Intersection found between: " + intersect.a + " and " + intersect.b);
+            this.intersect = intersect;
         }
+    }
 
-        /**
-         * Returns the outer way.
-         * @return the outer way
-         */
-        public final Way getOuterWay() {
-            return outerWay;
-        }
+    static class UndirectedWaySegment {
+        private Node a;
+        private Node b;
 
-        /**
-         * Returns the inner ways.
-         * @return the inner ways
-         */
-        public final List<Way> getInnerWays() {
-            return innerWays;
+        UndirectedWaySegment(Node a, Node b) {
+            if (a == b) {
+                throw new IllegalArgumentException("Way segment cannot start and end at the same node.");
+            }
+            this.a = a;
+            this.b = b;
         }
-    }
 
-    // HelperClass
-    // Saves a relation and a role an OsmPrimitve was part of until it was stripped from all relations
-    private static class RelationRole {
-        public final Relation rel;
-        public final String role;
-
-        RelationRole(Relation rel, String role) {
-            this.rel = rel;
-            this.role = role;
+        public boolean hasEnd(Node current) {
+            return a == current || b == current;
         }
 
-        @Override
-        public int hashCode() {
-            return Objects.hash(rel, role);
+        public Node getOtherEnd(Node current) {
+            if (current == a) {
+                return b;
+            } else if (current == b) {
+                return a;
+            } else {
+                throw new IllegalArgumentException(current + " is not an endpoint");
+            }
         }
 
-        @Override
-        public boolean equals(Object other) {
-            if (this == other) return true;
-            if (other == null || getClass() != other.getClass()) return false;
-            RelationRole that = (RelationRole) other;
-            return Objects.equals(rel, that.rel) &&
-                    Objects.equals(role, that.role);
+        public boolean intersects(UndirectedWaySegment other) {
+            EastNorth intersection = getIntersectionPoint(other);
+            return intersection != null;
         }
-    }
 
-    /**
-     * HelperClass - saves a way and the "inside" side.
-     *
-     * insideToTheLeft: if true left side is "in", false -right side is "in".
-     * Left and right are determined along the orientation of way.
-     */
-    public static class WayInPolygon {
-        public final Way way;
-        public boolean insideToTheRight;
-
-        public WayInPolygon(Way way, boolean insideRight) {
-            this.way = way;
-            this.insideToTheRight = insideRight;
+        private EastNorth getIntersectionPoint(UndirectedWaySegment other) {
+            EastNorth intersection = null;
+            if (!hasEnd(other.a) && !hasEnd(other.b)) {
+                // ignore just touching.
+                intersection = Geometry.getSegmentSegmentIntersection(
+                        a.getEastNorth(), b.getEastNorth(),
+                        other.a.getEastNorth(), other.b.getEastNorth());
+            }
+            return intersection;
         }
 
         @Override
         public int hashCode() {
-            return Objects.hash(way, insideToTheRight);
+            return a.hashCode() + b.hashCode();
         }
 
         @Override
-        public boolean equals(Object other) {
-            if (this == other) return true;
-            if (other == null || getClass() != other.getClass()) return false;
-            WayInPolygon that = (WayInPolygon) other;
-            return insideToTheRight == that.insideToTheRight &&
-                    Objects.equals(way, that.way);
+        public boolean equals(Object obj) {
+            if (this.getClass() == obj.getClass()) {
+                UndirectedWaySegment other = (UndirectedWaySegment) obj;
+                return (a.equals(other.a) && b.equals(other.b)) || (a.equals(other.b) && b.equals(other.a));
+            } else {
+                return false;
+            }
         }
 
         @Override
         public String toString() {
-            return "WayInPolygon [way=" + way + ", insideToTheRight=" + insideToTheRight + "]";
+            return "UndirectedWaySegment [" + a + ", " + b + "]";
         }
+
     }
 
     /**
-     * This helper class describes a polygon, assembled from several ways.
-     * @author viesturs
-     *
+     * This class defines an area that might be joined.
+     * @author Michael Zangl
      */
-    public static class AssembledPolygon {
-        public List<WayInPolygon> ways;
-
-        public AssembledPolygon(List<WayInPolygon> boundary) {
-            this.ways = boundary;
-        }
-
-        public List<Node> getNodes() {
-            List<Node> nodes = new ArrayList<>();
-            for (WayInPolygon way : this.ways) {
-                //do not add the last node as it will be repeated in the next way
-                if (way.insideToTheRight) {
-                    for (int pos = 0; pos < way.way.getNodesCount() - 1; pos++) {
-                        nodes.add(way.way.getNode(pos));
-                    }
-                } else {
-                    for (int pos = way.way.getNodesCount() - 1; pos > 0; pos--) {
-                        nodes.add(way.way.getNode(pos));
-                    }
-                }
-            }
-
-            return nodes;
-        }
-
+    static class JoinableArea {
         /**
-         * Inverse inside and outside
+         * A list of Node->Node segments that compose this area.
+         * You can reconstruct the interior of this area by XORing those lines.
          */
-        public void reverse() {
-            for (WayInPolygon way: ways) {
-                way.insideToTheRight = !way.insideToTheRight;
-            }
-            Collections.reverse(ways);
-        }
-    }
+        private final HashSet<UndirectedWaySegment> waySegments = new HashSet<>();
+        private final List<Way> ways = new ArrayList<>();
+        private final List<Relation> relations = new ArrayList<>();
+        private final Map<String, String> tags;
+        private final OsmPrimitive basePrimitive;
 
-    public static class AssembledMultipolygon {
-        public AssembledPolygon outerWay;
-        public List<AssembledPolygon> innerWays;
-
-        public AssembledMultipolygon(AssembledPolygon way) {
-            outerWay = way;
-            innerWays = new ArrayList<>();
+        JoinableArea(Way way) throws JoinAreasException {
+            this(way, Collections.singleton(way), Collections.emptyList());
         }
-    }
 
-    /**
-     * This hepler class implements algorithm traversing trough connected ways.
-     * Assumes you are going in clockwise orientation.
-     * @author viesturs
-     */
-    private static class WayTraverser {
-
-        /** Set of {@link WayInPolygon} to be joined by walk algorithm */
-        private final List<WayInPolygon> availableWays;
-        /** Current state of walk algorithm */
-        private WayInPolygon lastWay;
-        /** Direction of current way */
-        private boolean lastWayReverse;
-
-        /** Constructor
-         * @param ways available ways
-         */
-        WayTraverser(Collection<WayInPolygon> ways) {
-            availableWays = new ArrayList<>(ways);
-            lastWay = null;
+        JoinableArea(Relation relation) throws JoinAreasException {
+            this(relation, getMembers(relation, "outer"), getMembers(relation, "inner"));
+            relations.add(relation);
         }
 
         /**
-         *  Remove ways from available ways
-         *  @param ways Collection of WayInPolygon
+         * Creates a new joinable area.
+         * @param base The primitive this area is for.
+         * @param outer The ways that should be outer ways.
+         * @param inner The ways that should be inner ways.
+         * @throws JoinAreasException If the area is invalid
          */
-        public void removeWays(Collection<WayInPolygon> ways) {
-            availableWays.removeAll(ways);
-        }
+        JoinableArea(OsmPrimitive base, Collection<Way> outer, Collection<Way> inner) throws JoinAreasException {
+            basePrimitive = base;
+            tags = new HashMap<>(base.getInterestingTags());
+            tags.remove("type", "multipolygon");
 
-        /**
-         * Remove a single way from available ways
-         * @param way WayInPolygon
-         */
-        public void removeWay(WayInPolygon way) {
-            availableWays.remove(way);
-        }
+            try {
+                for (Way o : outer) {
+                    addWayForceNonintersecting(o);
+                }
+                Pair<Node, Node> outerGap = findGap();
+                if (outerGap != null) {
+                    throw new UnclosedAreaException(outerGap);
+                }
 
-        /**
-         * Reset walk algorithm to a new start point
-         * @param way New start point
-         */
-        public void setStartWay(WayInPolygon way) {
-            lastWay = way;
-            lastWayReverse = !way.insideToTheRight;
+                for (Way i : inner) {
+                    addWayForceNonintersecting(i);
+                }
+                Pair<Node, Node> innerGap = findGap();
+                if (innerGap != null) {
+                    throw new UnclosedAreaException(innerGap);
+                }
+            } catch (RuntimeException e) {
+                throw BugReport.intercept(e).put("outer", outer).put("inner", inner);
+            }
         }
 
         /**
-         * Reset walk algorithm to a new start point.
-         * @return The new start point or null if no available way remains
+         * Check if this area is a valid closed area
+         * @return The gap if there is one, null for closed areas.
          */
-        public WayInPolygon startNewWay() {
-            if (availableWays.isEmpty()) {
-                lastWay = null;
-            } else {
-                lastWay = availableWays.iterator().next();
-                lastWayReverse = !lastWay.insideToTheRight;
+        private Pair<Node, Node> findGap() {
+            HashSet<UndirectedWaySegment> leftOver = new HashSet<>(waySegments);
+            while (!leftOver.isEmpty()) {
+                LinkedList<Node> part = removeOutlinePart(leftOver);
+                if (part.getFirst() != part.getLast()) {
+                    return new Pair<>(part.getFirst(), part.getLast());
+                }
             }
-
-            return lastWay;
+            return null;
         }
 
         /**
-         * Walking through {@link WayInPolygon} segments, head node is the current position
-         * @return Head node
+         * Add a new Way to the outline (outer or inner) of this area.
+         * @param way The way.
+         * @throws SelfIntersectingAreaException If the way self-intersects
          */
-        private Node getHeadNode() {
-            return !lastWayReverse ? lastWay.way.lastNode() : lastWay.way.firstNode();
+        private void addWayForceNonintersecting(Way way) throws SelfIntersectingAreaException {
+            for (Pair<Node, Node> pair : way.getNodePairs(false)) {
+                this.addWayForceNonintersecting(new UndirectedWaySegment(pair.a, pair.b));
+            }
+            ways .add(way);
         }
 
-        /**
-         * Node just before head node.
-         * @return Previous node
-         */
-        private Node getPrevNode() {
-            return !lastWayReverse ? lastWay.way.getNode(lastWay.way.getNodesCount() - 2) : lastWay.way.getNode(1);
-        }
-
-        /**
-         * Returns oriented angle (N1N2, N1N3) in range [0; 2*Math.PI[
-         * @param n1 first node
-         * @param n2 second node
-         * @param n3 third node
-         * @return oriented angle (N1N2, N1N3) in range [0; 2*Math.PI[
-         */
-        private static double getAngle(Node n1, Node n2, Node n3) {
-            EastNorth en1 = n1.getEastNorth();
-            EastNorth en2 = n2.getEastNorth();
-            EastNorth en3 = n3.getEastNorth();
-            double angle = Math.atan2(en3.getY() - en1.getY(), en3.getX() - en1.getX()) -
-                    Math.atan2(en2.getY() - en1.getY(), en2.getX() - en1.getX());
-            while (angle >= 2*Math.PI) {
-                angle -= 2*Math.PI;
+        private void addWayForceNonintersecting(UndirectedWaySegment s) throws SelfIntersectingAreaException {
+            if (waySegments.contains(s)) {
+                // We add a way segment twice. This means that the outline of the area contains this segment twice.
+                // This cancels out, so we remove the segment,
+                waySegments.remove(s);
+            } else {
+                // Now check for intersections
+                Optional<UndirectedWaySegment> intersection = waySegments.stream().filter(s::intersects).findAny();
+                if (intersection.isPresent()) {
+                    throw new SelfIntersectingAreaException(new Pair<>(intersection.get(), s));
+                }
+                waySegments.add(s);
             }
-            while (angle < 0) {
-                angle += 2*Math.PI;
-            }
-            return angle;
         }
 
-        /**
-         * Get the next way creating a clockwise path, ensure it is the most right way. #7959
-         * @return The next way.
-         */
-        public WayInPolygon walk() {
-            Node headNode = getHeadNode();
-            Node prevNode = getPrevNode();
-
-            double headAngle = Math.atan2(headNode.getEastNorth().east() - prevNode.getEastNorth().east(),
-                    headNode.getEastNorth().north() - prevNode.getEastNorth().north());
-
-            // Pairs of (way, nextNode)
-            lastWay = Stream.concat(
-                availableWays.stream()
-                    .filter(way -> way.way.firstNode().equals(headNode) && way.insideToTheRight)
-                    .map(way -> new Pair<>(way, way.way.getNode(1))),
-                availableWays.stream()
-                    .filter(way -> way.way.lastNode().equals(headNode) && !way.insideToTheRight)
-                    .map(way -> new Pair<>(way, way.way.getNode(way.way.getNodesCount() - 2))))
-
-                // now find the way with the best angle
-                .min(Comparator.comparingDouble(wayAndNext -> {
-                    Node nextNode = wayAndNext.b;
-                    if (nextNode == prevNode) {
-                        // we always prefer going back.
-                        return Double.POSITIVE_INFINITY;
-                    }
-                    double angle = Math.atan2(nextNode.getEastNorth().east() - headNode.getEastNorth().east(),
-                            nextNode.getEastNorth().north() - headNode.getEastNorth().north()) - headAngle;
-                    if (angle > Math.PI)
-                        angle -= 2*Math.PI;
-                    if (angle <= -Math.PI)
-                        angle += 2*Math.PI;
-                    return angle;
-                })).map(wayAndNext -> wayAndNext.a).orElse(null);
-            lastWayReverse = lastWay != null && !lastWay.insideToTheRight;
-            return lastWay;
+        private static Collection<Way> getMembers(Relation relation, String role) {
+            return relation.getMembers().stream().filter(m -> role.equals(m.getRole()))
+                    .filter(m -> OsmPrimitiveType.WAY.equals(m.getType())).map(m -> m.getWay())
+                    .collect(Collectors.toList());
         }
 
         /**
-         * Search for an other way coming to the same head node at left side from last way. #9951
-         * @return left way or null if none found
+         * Check if the area contains a segment.
+         * @param segment The segment. Assumed to not intersect any of our borders.
+         * @return true if the segment is inside. False if it is on the outline or outside.
          */
-        public WayInPolygon leftComingWay() {
-            Node headNode = getHeadNode();
-            Node prevNode = getPrevNode();
-
-            WayInPolygon mostLeft = null; // most left way connected to head node
-            boolean comingToHead = false; // true if candidate come to head node
-            double angle = 2*Math.PI;
-
-            for (WayInPolygon candidateWay : availableWays) {
-                boolean candidateComingToHead;
-                Node candidatePrevNode;
-
-                if (candidateWay.way.firstNode().equals(headNode)) {
-                    candidateComingToHead = !candidateWay.insideToTheRight;
-                    candidatePrevNode = candidateWay.way.getNode(1);
-                } else if (candidateWay.way.lastNode().equals(headNode)) {
-                     candidateComingToHead = candidateWay.insideToTheRight;
-                     candidatePrevNode = candidateWay.way.getNode(candidateWay.way.getNodesCount() - 2);
-                } else
-                    continue;
-                if (candidateComingToHead && candidateWay.equals(lastWay))
-                    continue;
-
-                double candidateAngle = getAngle(headNode, candidatePrevNode, prevNode);
-
-                if (mostLeft == null || candidateAngle < angle || (Utils.equalsEpsilon(candidateAngle, angle) && !candidateComingToHead)) {
-                    // Candidate is most left
-                    mostLeft = candidateWay;
-                    comingToHead = candidateComingToHead;
-                    angle = candidateAngle;
-                }
+        public boolean contains(UndirectedWaySegment segment) {
+            if (waySegments.contains(segment)) {
+                return false;
             }
+            // To find out which side of the way the outer side is, we can follow a ray starting anywhere at the way in any direction.
+            // Computation is done in East/North space.
+            // We use a ray at a fixed yRay coordinate that ends at xRay;
+            // we need to make sure this ray does not go into the same direction the way is going.
+            // This is done by rotating by 90° if we need to.
 
-            return comingToHead ? mostLeft : null;
-        }
+            int intersections = 0;
+            // Use some "random" start point on the segment
+            EastNorth rayNode1 = segment.a.getEastNorth();
+            EastNorth rayNode2 = segment.b.getEastNorth();
+            EastNorth rayFrom = rayNode1.getCenter(rayNode2);
 
-        @Override
-        public String toString() {
-            return "WayTraverser [availableWays=" + availableWays + ", lastWay=" + lastWay + ", lastWayReverse="
-                    + lastWayReverse + "]";
-        }
-    }
-
-    /**
-     * Helper storage class for finding findOuterWays
-     * @author viesturs
-     */
-    static class PolygonLevel {
-        public final int level;
-        public final AssembledMultipolygon pol;
-
-        PolygonLevel(AssembledMultipolygon pol, int level) {
-            this.pol = pol;
-            this.level = level;
-        }
-    }
-
-    /**
-     * Constructs a new {@code JoinAreasAction}.
-     */
-    public JoinAreasAction() {
-        this(true);
-    }
-
-    /**
-     * Constructs a new {@code JoinAreasAction} with optional shortcut.
-     * @param addShortcut controls whether the shortcut should be registered or not
-     * @since 11611
-     */
-    public JoinAreasAction(boolean addShortcut) {
-        super(tr("Join overlapping Areas"), "joinareas", tr("Joins areas that overlap each other"), addShortcut ?
-        Shortcut.registerShortcut("tools:joinareas", tr("Tool: {0}", tr("Join overlapping Areas")), KeyEvent.VK_J, Shortcut.SHIFT)
-        : null, true);
-    }
-
-    /**
-     * Gets called whenever the shortcut is pressed or the menu entry is selected.
-     * Checks whether the selected objects are suitable to join and joins them if so.
-     */
-    @Override
-    public void actionPerformed(ActionEvent e) {
-        join(Main.getLayerManager().getEditDataSet().getSelectedWays());
-    }
-
-    /**
-     * Joins the given ways.
-     * @param ways Ways to join
-     * @since 7534
-     */
-    public void join(Collection<Way> ways) {
-        addedRelations.clear();
-
-        if (ways.isEmpty()) {
-            new Notification(
-                    tr("Please select at least one closed way that should be joined."))
-                    .setIcon(JOptionPane.INFORMATION_MESSAGE)
-                    .show();
-            return;
-        }
-
-        List<Node> allNodes = new ArrayList<>();
-        for (Way way : ways) {
-            if (!way.isClosed()) {
-                new Notification(
-                        tr("One of the selected ways is not closed and therefore cannot be joined."))
-                        .setIcon(JOptionPane.INFORMATION_MESSAGE)
-                        .show();
-                return;
+            // Now find the x/y mapping function. We need to ensure that rayNode1->rayNode2 is not parallel to our x axis.
+            ToDoubleFunction<EastNorth> x;
+            ToDoubleFunction<EastNorth> y;
+            if (Math.abs(rayNode1.east() - rayNode2.east()) < Math.abs(rayNode1.north() - rayNode2.north())) {
+                x = en -> en.east();
+                y = en -> en.north();
+            } else {
+                x = en -> -en.north();
+                y = en -> en.east();
             }
 
-            allNodes.addAll(way.getNodes());
-        }
+            double xRay = x.applyAsDouble(rayFrom);
+            double yRay = y.applyAsDouble(rayFrom);
 
-        // TODO: Only display this warning when nodes outside dataSourceArea are deleted
-        boolean ok = Command.checkAndConfirmOutlyingOperation("joinarea", tr("Join area confirmation"),
-                trn("The selected way has nodes outside of the downloaded data region.",
-                    "The selected ways have nodes outside of the downloaded data region.",
-                    ways.size()) + "<br/>"
-                    + tr("This can lead to nodes being deleted accidentally.") + "<br/>"
-                    + tr("Are you really sure to continue?")
-                    + tr("Please abort if you are not sure"),
-                tr("The selected area is incomplete. Continue?"),
-                allNodes, null);
-        if (!ok) return;
+            for (UndirectedWaySegment part : waySegments) {
+                // intersect against all way segments
+                EastNorth n1 = part.a.getEastNorth();
+                EastNorth n2 = part.b.getEastNorth();
+                if ((rayNode1.equals(n1) && rayNode2.equals(n2)) || (rayNode2.equals(n1) && rayNode1.equals(n2))) {
+                    // This is the segment we are starting the ray from.
+                    // We ignore this to avoid rounding errors.
+                    continue;
+                }
 
-        //analyze multipolygon relations and collect all areas
-        List<Multipolygon> areas = collectMultipolygons(ways);
+                double x1 = x.applyAsDouble(n1);
+                double x2 = x.applyAsDouble(n2);
+                double y1 = y.applyAsDouble(n1);
+                double y2 = y.applyAsDouble(n2);
 
-        if (areas == null)
-            //too complex multipolygon relations found
-            return;
-
-        if (!testJoin(areas)) {
-            new Notification(
-                    tr("No intersection found. Nothing was changed."))
-                    .setIcon(JOptionPane.INFORMATION_MESSAGE)
-                    .show();
-            return;
-        }
-
-        if (!resolveTagConflicts(areas))
-            return;
-        //user canceled, do nothing.
-
-        try {
-            // see #11026 - Because <ways> is a dynamic filtered (on ways) of a filtered (on selected objects) collection,
-            // retrieve effective dataset before joining the ways (which affects the selection, thus, the <ways> collection)
-            // Dataset retrieving allows to call this code without relying on Main.getCurrentDataSet(), thus, on a mapview instance
-            DataSet ds = ways.iterator().next().getDataSet();
-
-            // Do the job of joining areas
-            JoinAreasResult result = joinAreas(areas);
-
-            if (result.hasChanges) {
-                // move tags from ways to newly created relations
-                // TODO: do we need to also move tags for the modified relations?
-                for (Relation r: addedRelations) {
-                    cmds.addAll(CreateMultipolygonAction.removeTagsFromWaysIfNeeded(r));
+                if (!((y1 <= yRay && yRay < y2) || (y2 <= yRay && yRay < y1))) {
+                    // No intersection, since segment is above/below ray
+                    continue;
                 }
-                commitCommands(tr("Move tags from ways to relations"));
-
-                List<Way> allWays = new ArrayList<>();
-                for (Multipolygon pol : result.polygons) {
-                    allWays.add(pol.outerWay);
-                    allWays.addAll(pol.innerWays);
+                double xIntersect = x1 + (x2 - x1) * (yRay - y1) / (y2 - y1);
+                double onLine = xIntersect / xRay;
+                if (Math.abs(onLine - 1) < 1e-10) {
+                    // Lines that are directly on each other are considered outside.
+                    return false;
                 }
-                if (ds != null) {
-                    ds.setSelected(allWays);
+                if (xIntersect < xRay) {
+                    intersections++;
                 }
-            } else {
-                new Notification(
-                        tr("No intersection found. Nothing was changed."))
-                        .setIcon(JOptionPane.INFORMATION_MESSAGE)
-                        .show();
             }
-        } catch (UserCancelException exception) {
-            Main.trace(exception);
-            //revert changes
-            //FIXME: this is dirty hack
-            makeCommitsOneAction(tr("Reverting changes"));
-            Main.main.undoRedo.undo();
-            Main.main.undoRedo.redoCommands.clear();
-        }
-    }
 
-    /**
-     * Tests if the areas have some intersections to join.
-     * @param areas Areas to test
-     * @return {@code true} if areas are joinable
-     */
-    private boolean testJoin(List<Multipolygon> areas) {
-        List<Way> allStartingWays = new ArrayList<>();
-
-        for (Multipolygon area : areas) {
-            allStartingWays.add(area.outerWay);
-            allStartingWays.addAll(area.innerWays);
+            return intersections % 2 == 1;
         }
 
-        //find intersection points
-        Set<Node> nodes = Geometry.addIntersections(allStartingWays, true, cmds);
-        return !nodes.isEmpty();
-    }
-
-    private static class DuplicateWayCollectorAccu {
-           private List<Way> currentWays = new ArrayList<>();
-           private List<Way> duplicatesFound = new ArrayList<>();
-
-           private void add(Way way) {
-               List<Node> wayNodes = way.getNodes();
-               List<Node> wayNodesReversed = way.getNodes();
-               Collections.reverse(wayNodesReversed);
-               Optional<Way> duplicate = currentWays.stream()
-                   .filter(current -> current.getNodes().equals(wayNodes) || current.getNodes().equals(wayNodesReversed))
-                   .findFirst();
-               if (duplicate.isPresent()) {
-                   currentWays.remove(duplicate.get());
-                   duplicatesFound.add(duplicate.get());
-                   duplicatesFound.add(way);
-               } else {
-                   currentWays.add(way);
-               }
-           }
-
-           private DuplicateWayCollectorAccu combine(DuplicateWayCollectorAccu a2) {
-               duplicatesFound.addAll(a2.duplicatesFound);
-               a2.currentWays.forEach(this::add);
-               return this;
-           }
-    }
-
-    /**
-     * A collector that collects to a list of duplicated way pairs.
-     *
-     * It does not scale well (O(n²)), but the data base should be small enough to make this efficient.
-     *
-     * @author Michael Zangl
-     */
-    private static class DuplicateWayCollector implements Collector<Way, DuplicateWayCollectorAccu, List<Way>> {
-        @Override
-        public Supplier<DuplicateWayCollectorAccu> supplier() {
-            return DuplicateWayCollectorAccu::new;
+        public Collection<UndirectedWaySegment> getSegments() {
+            return Collections.unmodifiableCollection(waySegments);
         }
-
-        @Override
-        public BiConsumer<DuplicateWayCollectorAccu, Way> accumulator() {
-            return DuplicateWayCollectorAccu::add;
-        }
-
-        @Override
-        public BinaryOperator<DuplicateWayCollectorAccu> combiner() {
-            return DuplicateWayCollectorAccu::combine;
-        }
-
-        @Override
-        public Function<DuplicateWayCollectorAccu, List<Way>> finisher() {
-            return a -> a.duplicatesFound;
-        }
-
-        @Override
-        public Set<Collector.Characteristics> characteristics() {
-            return EnumSet.of(Collector.Characteristics.UNORDERED);
-        }
-
     }
 
     /**
-     * Will join two or more overlapping areas
-     * @param areas list of areas to join
-     * @return new area formed.
-     * @throws UserCancelException if user cancels the operation
+     * A hash set with an xor method.
+     * @param <T> element type
      */
-    public JoinAreasResult joinAreas(List<Multipolygon> areas) throws UserCancelException {
-
-        boolean hasChanges = false;
-
-        List<Way> allStartingWays = new ArrayList<>();
-        List<Way> innerStartingWays = new ArrayList<>();
-        List<Way> outerStartingWays = new ArrayList<>();
-
-        for (Multipolygon area : areas) {
-            outerStartingWays.add(area.outerWay);
-            innerStartingWays.addAll(area.innerWays);
+    private static class XOrHashSet<T> extends HashSet<T> {
+        public XOrHashSet() {
+            super();
         }
 
-        allStartingWays.addAll(innerStartingWays);
-        allStartingWays.addAll(outerStartingWays);
-
-        //first remove nodes in the same coordinate
-        boolean removedDuplicates = false;
-        removedDuplicates |= removeDuplicateNodes(allStartingWays);
-
-        if (removedDuplicates) {
-            hasChanges = true;
-            commitCommands(marktr("Removed duplicate nodes"));
+        public XOrHashSet(Collection<? extends T> c) {
+            super(c);
         }
 
-        //find intersection points
-        Set<Node> nodes = Geometry.addIntersections(allStartingWays, false, cmds);
-
-        //no intersections, return.
-        if (nodes.isEmpty())
-            return new JoinAreasResult(hasChanges, null);
-        commitCommands(marktr("Added node on all intersections"));
-
-        List<RelationRole> relations = new ArrayList<>();
-
-        // Remove ways from all relations so ways can be combined/split quietly
-        for (Way way : allStartingWays) {
-            relations.addAll(removeFromAllRelations(way));
-        }
-
-        // Don't warn now, because it will really look corrupted
-        boolean warnAboutRelations = !relations.isEmpty() && allStartingWays.size() > 1;
-
-        List<WayInPolygon> preparedWays = new ArrayList<>();
-
-        // Split the nodes on the
-        List<Way> splitOuterWays = outerStartingWays.stream()
-                .flatMap(way -> splitWayOnNodes(way, nodes).stream()).collect(Collectors.toList());
-        List<Way> splitInnerWays = innerStartingWays.stream()
-                .flatMap(way -> splitWayOnNodes(way, nodes).stream()).collect(Collectors.toList());
-
-        // remove duplicate ways (A->B->C and C->B->A)
-        List<Way> duplicates = Stream.concat(splitOuterWays.stream(), splitInnerWays.stream()).collect(new DuplicateWayCollector());
-
-        splitOuterWays.removeAll(duplicates);
-        splitInnerWays.removeAll(duplicates);
-
-        preparedWays.addAll(markWayInsideSide(splitOuterWays, false));
-        preparedWays.addAll(markWayInsideSide(splitInnerWays, true));
-
-        // Find boundary ways
-        List<Way> discardedWays = new ArrayList<>(duplicates);
-        List<AssembledPolygon> boundaries = findBoundaryPolygons(preparedWays, discardedWays);
-
-        //find polygons
-        List<AssembledMultipolygon> preparedPolygons = findPolygons(boundaries);
-
-        //assemble final polygons
-        List<Multipolygon> polygons = new ArrayList<>();
-        Set<Relation> relationsToDelete = new LinkedHashSet<>();
-
-        for (AssembledMultipolygon pol : preparedPolygons) {
-
-            //create the new ways
-            Multipolygon resultPol = joinPolygon(pol);
-
-            //create multipolygon relation, if necessary.
-            RelationRole ownMultipolygonRelation = addOwnMultipolygonRelation(resultPol.innerWays);
-
-            //add back the original relations, merged with our new multipolygon relation
-            fixRelations(relations, resultPol.outerWay, ownMultipolygonRelation, relationsToDelete);
-
-            //strip tags from inner ways
-            //TODO: preserve tags on existing inner ways
-            stripTags(resultPol.innerWays);
-
-            polygons.add(resultPol);
-        }
-
-        commitCommands(marktr("Assemble new polygons"));
-
-        for (Relation rel: relationsToDelete) {
-            cmds.add(new DeleteCommand(rel));
-        }
-
-        commitCommands(marktr("Delete relations"));
-
-        // Delete the discarded inner ways
-        if (!discardedWays.isEmpty()) {
-            Command deleteCmd = DeleteCommand.delete(Main.getLayerManager().getEditLayer(), discardedWays, true);
-            if (deleteCmd != null) {
-                cmds.add(deleteCmd);
-                commitCommands(marktr("Delete Ways that are not part of an inner multipolygon"));
+        public void xor(T e) {
+            if (!this.add(e)) {
+                this.remove(e);
             }
         }
-
-        makeCommitsOneAction(marktr("Joined overlapping areas"));
-
-        if (warnAboutRelations) {
-            new Notification(
-                    tr("Some of the ways were part of relations that have been modified.<br>Please verify no errors have been introduced."))
-                    .setIcon(JOptionPane.INFORMATION_MESSAGE)
-                    .setDuration(Notification.TIME_LONG)
-                    .show();
-        }
-
-        return new JoinAreasResult(true, polygons);
     }
 
     /**
-     * Checks if tags of two given ways differ, and presents the user a dialog to solve conflicts
-     * @param polygons ways to check
-     * @return {@code true} if all conflicts are resolved, {@code false} if conflicts remain.
+     * This class collects the areas to be joined.
      */
-    private boolean resolveTagConflicts(List<Multipolygon> polygons) {
+    static class JoinAreasCollector {
+        /**
+         * All nodes that are touched by this the geometry for algorithm.
+         */
+        private final Collection<Node> oldTouchedNodes = new HashSet<>();
+        /**
+         * The nodes that are added.
+         */
+        private final List<Node> possibleNewNodes = new ArrayList<>();
+        private final List<JoinableArea> unionOf = new ArrayList<>();
+        /**
+         * All hash sets that may be
+         */
+        private final XOrHashSet<UndirectedWaySegment> waySegments = new XOrHashSet<>();
+        private final DataSet ds;
 
-        List<Way> ways = new ArrayList<>();
-
-        for (Multipolygon pol : polygons) {
-            ways.add(pol.outerWay);
-            ways.addAll(pol.innerWays);
+        JoinAreasCollector(DataSet ds, Collection<? extends OsmPrimitive> waysAndRelations) throws JoinAreasException {
+            this.ds = ds;
+            Collection<JoinableArea> collectAreas = collectAreas(waysAndRelations);
+            collectAreas.forEach(this::unionWithArea);
         }
 
-        if (ways.size() < 2) {
-            return true;
-        }
-
-        TagCollection wayTags = TagCollection.unionOfAllPrimitives(ways);
-        try {
-            cmds.addAll(CombinePrimitiveResolverDialog.launchIfNecessary(wayTags, ways, ways));
-            commitCommands(marktr("Fix tag conflicts"));
-            return true;
-        } catch (UserCancelException ex) {
-            Main.trace(ex);
-            return false;
-        }
-    }
-
-    /**
-     * This method removes duplicate points (if any) from the input way.
-     * @param ways the ways to process
-     * @return {@code true} if any changes where made
-     */
-    private boolean removeDuplicateNodes(List<Way> ways) {
-        //TODO: maybe join nodes with JoinNodesAction, rather than reconnect the ways.
-
-        Map<Node, Node> nodeMap = new TreeMap<>(new NodePositionComparator());
-        int totalNodesRemoved = 0;
-
-        for (Way way : ways) {
-            if (way.getNodes().size() < 2) {
-                continue;
-            }
-
-            int nodesRemoved = 0;
-            List<Node> newNodes = new ArrayList<>();
-            Node prevNode = null;
-
-            for (Node node : way.getNodes()) {
-                if (!nodeMap.containsKey(node)) {
-                    //new node
-                    nodeMap.put(node, node);
-
-                    //avoid duplicate nodes
-                    if (prevNode != node) {
-                        newNodes.add(node);
-                    } else {
-                        nodesRemoved++;
-                    }
-                } else {
-                    //node with same coordinates already exists, substitute with existing node
-                    Node representator = nodeMap.get(node);
-
-                    if (representator != node) {
-                        nodesRemoved++;
-                    }
-
-                    //avoid duplicate node
-                    if (prevNode != representator) {
-                        newNodes.add(representator);
-                    }
+        private static Collection<JoinableArea> collectAreas(Collection<? extends OsmPrimitive> waysAndRelations) throws JoinAreasException {
+            Collection<JoinableArea> areas = new ArrayList<>();
+            for(OsmPrimitive osm : waysAndRelations) {
+                if (osm instanceof Way) {
+                    areas.add(new JoinableArea((Way) osm));
+                } else if (osm instanceof Relation) {
+                    areas.add(new JoinableArea((Relation) osm));
                 }
-                prevNode = node;
             }
-
-            if (nodesRemoved > 0) {
-
-                if (newNodes.size() == 1) { //all nodes in the same coordinate - add one more node, to have closed way.
-                    newNodes.add(newNodes.get(0));
-                }
-
-                Way newWay = new Way(way);
-                newWay.setNodes(newNodes);
-                cmds.add(new ChangeCommand(way, newWay));
-                totalNodesRemoved += nodesRemoved;
-            }
+            return areas;
         }
 
-        return totalNodesRemoved > 0;
-    }
+        void unionWithArea(JoinableArea area) {
+            Collection<UndirectedWaySegment> segments = area.getSegments();
 
-    /**
-     * Commits the command list with a description
-     * @param description The description of what the commands do
-     */
-    private void commitCommands(String description) {
-        switch(cmds.size()) {
-        case 0:
-            return;
-        case 1:
-            commitCommand(cmds.getFirst());
-            break;
-        default:
-            commitCommand(new SequenceCommand(tr(description), cmds));
-            break;
-        }
+            segments.stream().flatMap(s -> Stream.of(s.a, s.b)).forEach(oldTouchedNodes::add);
 
-        cmds.clear();
-        cmdsCount++;
-    }
+            // Our worker list. Once a way is split, it is re-added to the worker to check for more splits.
+            XOrHashSet<UndirectedWaySegment> toAdd = new XOrHashSet<>(segments);
+            while (!toAdd.isEmpty()) {
+                UndirectedWaySegment s = toAdd.iterator().next();
+                toAdd.remove(s);
+                Optional<UndirectedWaySegment> intersects = waySegments.stream().filter(s::intersects).findAny();
+                if (intersects.isPresent()) {
+                    EastNorth intersection = s.getIntersectionPoint(intersects.get());
+                     // Now generate two segments around the intersection.
+                    waySegments.remove(intersects.get());
+                    Node newNode = findOrCreateNode(intersection);
 
-    private static void commitCommand(Command c) {
-        if (Main.main != null) {
-            Main.main.undoRedo.add(c);
-        } else {
-            c.executeCommand();
-        }
-    }
-
-    /**
-     * This method analyzes the way and assigns each part what direction polygon "inside" is.
-     * @param parts the split parts of the way
-     * @param isInner - if true, reverts the direction (for multipolygon islands)
-     * @return list of parts, marked with the inside orientation.
-     * @throws IllegalArgumentException if parts is empty or not circular
-     */
-    private static List<WayInPolygon> markWayInsideSide(List<Way> parts, boolean isInner) {
-        // the data is prepared so that all ways are split at possible intersection points.
-        // To find out which side of the way the outer side is, we can follow a ray starting anywhere at the way in any direction.
-        // Computation is done in East/North space.
-        // We use a ray at a fixed yRay coordinate that ends at xRay;
-        // we need to make sure this ray does not go into the same direction the way is going.
-        // This is done by rotating by 90° if we need to.
-
-        return parts.stream().map(way -> {
-            int intersections = 0;
-            // Use some random start point on the way
-            EastNorth rayNode1 = way.getNode(0).getEastNorth();
-            EastNorth rayNode2 = way.getNode(1).getEastNorth();
-            EastNorth rayFrom = rayNode1.getCenter(rayNode2);
-
-            // Now find the x/y mapping function. We need to ensure that rayNode1->rayNode2 is not parallel to our x axis.
-            ToDoubleFunction<EastNorth> x;
-            ToDoubleFunction<EastNorth> y;
-            if (Math.abs(rayNode1.east() - rayNode2.east()) < Math.abs(rayNode1.north() - rayNode2.north())) {
-                x = en -> en.east();
-                y = en -> en.north();
-            } else {
-                x = en -> -en.north();
-                y = en -> en.east();
-            }
-
-            double xRay = x.applyAsDouble(rayFrom);
-            double yRay = y.applyAsDouble(rayFrom);
-
-            for (Way part : parts) {
-                // intersect against all way segments
-                for (int i = 0; i < part.getNodesCount() - 1; i++) {
-                    EastNorth n1 = part.getNode(i).getEastNorth();
-                    EastNorth n2 = part.getNode(i + 1).getEastNorth();
-                    if ((rayNode1.equals(n1) && rayNode2.equals(n2)) || (rayNode2.equals(n1) && rayNode1.equals(n2))) {
-                        // This is the segment we are starting the ray from.
-                        // We ignore this to avoid rounding errors.
-                        continue;
+                    // it may be that newNode is one of the end nodes
+                    if (newNode != intersects.get().a) {
+                        // We use xor here to fix ways that e.g. reverse on themselves.
+                        waySegments.xor(new UndirectedWaySegment(intersects.get().a, newNode));
                     }
-
-                    double x1 = x.applyAsDouble(n1);
-                    double x2 = x.applyAsDouble(n2);
-                    double y1 = y.applyAsDouble(n1);
-                    double y2 = y.applyAsDouble(n2);
-
-                    if (!((y1 <= yRay && yRay < y2) || (y2 <= yRay && yRay < y1))) {
-                        // No intersection, since segment is above/below ray
-                        continue;
+                    if (newNode != s.a) {
+                        toAdd.xor(new UndirectedWaySegment(s.a, newNode));
                     }
-                    double xIntersect = x1 + (x2 - x1) * (yRay - y1) / (y2 - y1);
-                    if (xIntersect < xRay) {
-                        intersections++;
+                    if (newNode != intersects.get().b) {
+                        waySegments.xor(new UndirectedWaySegment(newNode, intersects.get().b));
                     }
+                    if (newNode != s.b) {
+                        toAdd.xor(new UndirectedWaySegment(newNode, s.b));
+                    }
+                } else {
+                    // No more intersections - we add that segment to our geometry
+                    waySegments.xor(s);
                 }
             }
 
-            return new WayInPolygon(way, (intersections % 2 == 0) ^ isInner ^ (y.applyAsDouble(rayNode1) > yRay));
-        }).collect(Collectors.toList());
-    }
-
-    /**
-     * This is a method that splits way into smaller parts, using the prepared nodes list as split points.
-     * Uses {@link SplitWayAction#splitWay} for the heavy lifting.
-     * @param way way to split
-     * @param nodes split points
-     * @return list of split ways (or original ways if no splitting is done).
-     */
-    private List<Way> splitWayOnNodes(Way way, Set<Node> nodes) {
-
-        List<Way> result = new ArrayList<>();
-        List<List<Node>> chunks = buildNodeChunks(way, nodes);
-
-        if (chunks.size() > 1) {
-            SplitWayResult split = SplitWayAction.splitWay(getLayerManager().getEditLayer(), way, chunks,
-                    Collections.<OsmPrimitive>emptyList(), SplitWayAction.Strategy.keepFirstChunk());
-
-            if (split != null) {
-                //execute the command, we need the results
-                cmds.add(split.getCommand());
-                commitCommands(marktr("Split ways into fragments"));
-
-                result.add(split.getOriginalWay());
-                result.addAll(split.getNewWays());
-            }
+            unionOf.add(area);
         }
-        if (result.isEmpty()) {
-            //nothing to split
-            result.add(way);
-        }
 
-        return result;
-    }
-
-    /**
-     * Simple chunking version. Does not care about circular ways and result being
-     * proper, we will glue it all back together later on.
-     * @param way the way to chunk
-     * @param splitNodes the places where to cut.
-     * @return list of node paths to produce.
-     */
-    private static List<List<Node>> buildNodeChunks(Way way, Collection<Node> splitNodes) {
-        List<List<Node>> result = new ArrayList<>();
-        List<Node> curList = new ArrayList<>();
-
-        for (Node node : way.getNodes()) {
-            curList.add(node);
-            if (curList.size() > 1 && splitNodes.contains(node)) {
-                result.add(curList);
-                curList = new ArrayList<>();
-                curList.add(node);
-            }
+        /**
+         * Find a node close to newNode to handle intersections of 3 or more lines.
+         * @param intersection The position of the node
+         * @return A node.
+         */
+        Node findOrCreateNode(EastNorth intersection) {
+            return Stream.concat(oldTouchedNodes.stream(), possibleNewNodes.stream())
+                    .filter(node -> node.getEastNorth().distanceSq(intersection) < 1e-40)
+                    .findAny()
+                    .orElseGet(() -> this.createNode(intersection));
         }
 
-        if (curList.size() > 1) {
-            result.add(curList);
+        private Node createNode(EastNorth intersection) {
+            Node newNode = new Node(intersection);
+            possibleNewNodes.add(newNode);
+            return newNode;
         }
 
-        return result;
-    }
+        /**
+         * Gets the outlines of this area
+         * @return The outline polygons as Even/Odd area. Not all nodes need to be contained in the data set.
+         */
+        List<List<Node>> getOutlines() {
+            Collection<UndirectedWaySegment> outline = computeOutline();
 
-    /**
-     * This method finds which ways are outer and which are inner.
-     * @param boundaries list of joined boundaries to search in
-     * @return outer ways
-     */
-    private static List<AssembledMultipolygon> findPolygons(Collection<AssembledPolygon> boundaries) {
-
-        List<PolygonLevel> list = findOuterWaysImpl(0, boundaries);
-        List<AssembledMultipolygon> result = new ArrayList<>();
-
-        //take every other level
-        for (PolygonLevel pol : list) {
-            if (pol.level % 2 == 0) {
-                result.add(pol.pol);
+            ArrayList<List<Node>> res = new ArrayList<>();
+            while (!outline.isEmpty()) {
+                res.add(removeOutlinePart(outline));
             }
+            return res;
         }
 
-        return result;
-    }
+        /**
+         * Gets the commands that are required to join the areas.
+         * @return The join commands.
+         */
+        List<Command> getCommands() {
+            if (unionOf.isEmpty()) {
+                return Collections.emptyList();
+            }
+            Collection<UndirectedWaySegment> outline = computeOutline();
 
-    /**
-     * Collects outer way and corresponding inner ways from all boundaries.
-     * @param level depth level
-     * @param boundaryWays list of joined boundaries to search in
-     * @return the outermost Way.
-     */
-    private static List<PolygonLevel> findOuterWaysImpl(int level, Collection<AssembledPolygon> boundaryWays) {
+            List<Command> commands = new ArrayList<>();
+            // The primitives of which we should remove the tags.
+            List<OsmPrimitive> toRemoveTags = new ArrayList<>();
+            unionOf.stream().map(area -> area.basePrimitive).forEach(toRemoveTags::add);
 
-        //TODO: bad performance for deep nestings...
-        List<PolygonLevel> result = new ArrayList<>();
+            // Add the split nodes
+            // Remove nodes of interior segments.
+            possibleNewNodes.stream()
+                .filter(n -> outline.stream().filter(w -> w.hasEnd(n)).findAny().isPresent())
+                .map(n -> new AddCommand(ds, n))
+                .forEach(commands::add);
 
-        for (AssembledPolygon outerWay : boundaryWays) {
-
-            boolean outerGood = true;
-            List<AssembledPolygon> innerCandidates = new ArrayList<>();
-
-            for (AssembledPolygon innerWay : boundaryWays) {
-                if (innerWay == outerWay) {
+            // Now search all ways which are completely used in our new geometry (e.g. multipolygon inners, ...)
+            // We should not change those ways.
+            List<Way> outlineWays = new ArrayList<>();
+            List<UndirectedWaySegment> segmentsToContain = new ArrayList<>(outline);
+            for (Way preserve : findOutlinesToPreserve(segmentsToContain)) {
+                List<UndirectedWaySegment> preservedSegments = segmentsForWay(preserve);
+                if (preservedSegments.size() != preservedSegments.stream().distinct().count()) {
+                    // This way contains a segment twice. Skip it, we want to fix this.
                     continue;
                 }
-
-                if (wayInsideWay(outerWay, innerWay)) {
-                    outerGood = false;
-                    break;
-                } else if (wayInsideWay(innerWay, outerWay)) {
-                    innerCandidates.add(innerWay);
+                if (!segmentsToContain.containsAll(preservedSegments)) {
+                    // it may happen that two outlines that should be preserved happen to be on the same segment
+                    // We need to ignore the second one then.
+                    continue;
                 }
+                outlineWays.add(preserve);
+                segmentsToContain.removeAll(preservedSegments);
             }
 
-            if (!outerGood) {
-                continue;
-            }
+            // Multipolygons that were selected and can now be removed
+            List<Relation> relationsToDelete = unionOf.stream().flatMap(area -> area.relations.stream())
+                    .distinct().collect(Collectors.toList());
+            toRemoveTags.removeAll(relationsToDelete);
 
-            //add new outer polygon
-            AssembledMultipolygon pol = new AssembledMultipolygon(outerWay);
-            PolygonLevel polLev = new PolygonLevel(pol, level);
+            // Compute the ways that need to be removed.
+            // Those are all ways of the old geometry that are not used in any other place.
+            List<Way> waysToDelete = unionOf.stream().flatMap(area -> area.ways.stream())
+                    .distinct()
+                    .filter(way -> !outlineWays.contains(way))
+                    // Preserve ways that are member in any relation that we did not modify
+                    .filter(way -> way.getReferrers().stream().allMatch(relationsToDelete::contains))
+                    // Preserve ways that have tags
+                    .filter(way -> toRemoveTags.contains(way) || way.getInterestingTags().isEmpty())
+                    .collect(Collectors.toList());
+            toRemoveTags.removeAll(waysToDelete);
 
-            //process inner ways
-            if (!innerCandidates.isEmpty()) {
-                List<PolygonLevel> innerList = findOuterWaysImpl(level + 1, innerCandidates);
-                result.addAll(innerList);
+            // Now we are left with the remaining outline in the segmentsToContain array.
+            // For each chunk in that outline, we create a new way
+            // TODO: We can reuse the ways we would delete otherwise.
+            while (!segmentsToContain.isEmpty()) {
+                List<Node> wayToCreate = removeOutlinePart(segmentsToContain);
+                Way osm = new Way();
+                osm.setNodes(wayToCreate);
+                outlineWays.add(osm);
+                commands.add(new AddCommand(ds, osm));
+            }
 
-                for (PolygonLevel pl : innerList) {
-                    if (pl.level == level + 1) {
-                        pol.innerWays.add(pl.pol.outerWay);
-                    }
+            OsmPrimitive resultPrimitive;
+            // Now it is time to generate the final area.
+            if (outlineWays.isEmpty()) {
+                throw new AssertionError("No outline ways found.");
+            } else if (outlineWays.size() == 1) {
+                // We only have one way. Add the tags to that way.
+                resultPrimitive = outlineWays.get(0);
+            } else {
+                // find a relation. Use the more complex multipolygon when merging two of them.
+                Relation multipolygon = relationsToDelete.stream().sorted(Comparator.comparingInt(r -> -r.getMembersCount()))
+                        .findFirst().orElseGet(Relation::new);
+                Pair<Relation, Relation> update = CreateMultipolygonAction.updateMultipolygonRelation(outlineWays, multipolygon);
+                if (update == null) {
+                    throw new AssertionError("The outline ways should be continuous but no multipolygon could be created.");
                 }
+                if (update.a.getDataSet() == null) {
+                    // used the fake relation.
+                    commands.add(new AddCommand(ds, update.b));
+                } else {
+                    commands.add(new ChangeCommand(ds, update.a, update.b));
+                }
+                resultPrimitive = multipolygon;
+                relationsToDelete.remove(multipolygon);
             }
 
-            result.add(polLev);
-        }
+            // We check the tags now.
+            try {
+                List<OsmPrimitive> baseTagged = unionOf.stream().map(area -> area.basePrimitive).collect(Collectors.toList());
+                TagCollection tags = TagCollection.unionOfAllPrimitives(baseTagged);
+                commands.addAll(CombinePrimitiveResolverDialog.launchIfNecessary(tags, baseTagged, Collections.singleton(resultPrimitive)));
+            } catch (UserCancelException ex) {
+                // User aborted. This is simple, since we did not commit anything.
+                Main.trace(ex);
+                return Collections.emptyList();
+            }
 
-        return result;
-    }
-
-    /**
-     * Finds all ways that form inner or outer boundaries.
-     * @param multigonWays A list of (splitted) ways that form a multigon and share common end nodes on intersections.
-     * @param discardedResult this list is filled with ways that are to be discarded
-     * @return A list of ways that form the outer and inner boundaries of the multigon.
-     */
-    public static List<AssembledPolygon> findBoundaryPolygons(Collection<WayInPolygon> multigonWays,
-            List<Way> discardedResult) {
-        // In multigonWays collection, some way are just a point (i.e. way like nodeA-nodeA)
-        // This seems to appear when is apply over invalid way like #9911 test-case
-        // Remove all of these way to make the next work.
-        List<WayInPolygon> cleanMultigonWays = multigonWays.stream()
-                .filter(way -> way.way.getNodesCount() != 2 || !way.way.isClosed())
-                .collect(Collectors.toList());
-        WayTraverser traverser = new WayTraverser(cleanMultigonWays);
-        List<AssembledPolygon> result = new ArrayList<>();
-
-        try {
-            WayInPolygon startWay;
-            while ((startWay = traverser.startNewWay()) != null) {
-                findBoundaryPolygonsStartingWith(discardedResult, traverser, result, startWay);
+            // Apply deletion of the primitives we don't need any more.
+            if (!relationsToDelete.isEmpty()) {
+                commands.add(new DeleteCommand(ds, relationsToDelete));
             }
-        } catch (JosmRuntimeException | IllegalArgumentException | IllegalStateException t) {
-            throw BugReport.intercept(t).put("traverser", traverser);
-        }
+            if (!waysToDelete.isEmpty()) {
+                commands.add(new DeleteCommand(ds, waysToDelete));
 
-        return fixTouchingPolygons(result);
-    }
 
-    private static void findBoundaryPolygonsStartingWith(List<Way> discardedResult, WayTraverser traverser, List<AssembledPolygon> result,
-            WayInPolygon startWay) {
-        List<WayInPolygon> path = new ArrayList<>();
-        List<WayInPolygon> startWays = new ArrayList<>();
-        try {
-            path.add(startWay);
-            while (true) {
-                WayInPolygon leftComing = traverser.leftComingWay();
-                if (leftComing != null && !startWays.contains(leftComing)) {
-                    // Need restart traverser walk
-                    path.clear();
-                    path.add(leftComing);
-                    traverser.setStartWay(leftComing);
-                    startWays.add(leftComing);
+                Collection<Node> nodesToDelete = oldTouchedNodes.stream()
+                        .filter(n -> !n.isTagged())
+                        // don't delete nodes that are part of the new outline
+                        .filter(n -> outlineWays.stream().allMatch(w -> !w.containsNode(n)))
+                        .filter(n -> n.getReferrers().stream().allMatch(r -> waysToDelete.contains(r) || r.isDeleted()))
+                        .collect(Collectors.toList());
+                if (!nodesToDelete.isEmpty()) {
+                    commands.add(new DeleteCommand(ds, nodesToDelete));
                 }
-                WayInPolygon nextWay = traverser.walk();
-                if (nextWay == null) {
-                    throw new JosmRuntimeException("Join areas internal error: traverser could not find a next way.");
+            }
+            for(OsmPrimitive osm : toRemoveTags) {
+                for (String key : osm.getKeys().keySet()) {
+                    commands.add(new ChangePropertyCommand(osm, key, ""));
                 }
-                if (path.get(0) == nextWay) {
-                    // path is closed -> stop here
-                    AssembledPolygon ring = new AssembledPolygon(path);
-                    if (ring.getNodes().size() <= 2) {
-                        // Invalid ring (2 nodes) -> remove
-                        traverser.removeWays(path);
-                        for (WayInPolygon way: path) {
-                            discardedResult.add(way.way);
-                        }
-                    } else {
-                        // Close ring -> add
-                        result.add(ring);
-                        traverser.removeWays(path);
-                    }
-                    break;
-                }
-                if (path.contains(nextWay)) {
-                    // Inner loop -> remove
-                    int index = path.indexOf(nextWay);
-                    while (path.size() > index) {
-                        WayInPolygon currentWay = path.get(index);
-                        discardedResult.add(currentWay.way);
-                        traverser.removeWay(currentWay);
-                        path.remove(index);
-                    }
-                    traverser.setStartWay(path.get(index-1));
-                } else {
-                    path.add(nextWay);
-                }
             }
-        } catch (JosmRuntimeException | IllegalArgumentException | IllegalStateException t) {
-            throw BugReport.intercept(t).put("path", path);
+
+            return commands;
         }
-    }
 
-    /**
-     * This method checks if polygons have several touching parts and splits them in several polygons.
-     * @param polygons the polygons to process.
-     * @return the resulting list of polygons
-     */
-    public static List<AssembledPolygon> fixTouchingPolygons(List<AssembledPolygon> polygons) {
-        List<AssembledPolygon> newPolygons = new ArrayList<>();
-
-        for (AssembledPolygon ring : polygons) {
-            ring.reverse();
-            WayTraverser traverser = new WayTraverser(ring.ways);
-            WayInPolygon startWay;
-
-            while ((startWay = traverser.startNewWay()) != null) {
-                List<WayInPolygon> simpleRingWays = new ArrayList<>();
-                simpleRingWays.add(startWay);
-                WayInPolygon nextWay;
-                while ((nextWay = traverser.walk()) != startWay) {
-                    if (nextWay == null)
-                        throw new JosmRuntimeException("Join areas internal error.");
-                    simpleRingWays.add(nextWay);
-                }
-                traverser.removeWays(simpleRingWays);
-                AssembledPolygon simpleRing = new AssembledPolygon(simpleRingWays);
-                simpleRing.reverse();
-                newPolygons.add(simpleRing);
-            }
+        private List<UndirectedWaySegment> segmentsForWay(Way way) {
+            return way.getNodePairs(false).stream()
+                    .map(pair -> new UndirectedWaySegment(pair.a, pair.b)).collect(Collectors.toList());
         }
 
-        return newPolygons;
-    }
-
-    /**
-     * Tests if way is inside other way
-     * @param outside outer polygon description
-     * @param inside inner polygon description
-     * @return {@code true} if inner is inside outer
-     */
-    public static boolean wayInsideWay(AssembledPolygon inside, AssembledPolygon outside) {
-        Set<Node> outsideNodes = new HashSet<>(outside.getNodes());
-        List<Node> insideNodes = inside.getNodes();
-
-        for (Node insideNode : insideNodes) {
-
-            if (!outsideNodes.contains(insideNode))
-                //simply test the one node
-                return Geometry.nodeInsidePolygon(insideNode, outside.getNodes());
+        private List<Way> findOutlinesToPreserve(List<UndirectedWaySegment> segmentsToContain) {
+            return unionOf.stream().flatMap(u -> u.ways.stream())
+                .filter(w -> segmentsToContain.containsAll(segmentsForWay(w))).collect(Collectors.toList());
         }
 
-        //all nodes shared.
-        return false;
-    }
-
-    /**
-     * Joins the lists of ways.
-     * @param polygon The list of outer ways that belong to that multipolygon.
-     * @return The newly created outer way
-     * @throws UserCancelException if user cancels the operation
-     */
-    private Multipolygon joinPolygon(AssembledMultipolygon polygon) throws UserCancelException {
-        Multipolygon result = new Multipolygon(joinWays(polygon.outerWay.ways));
-
-        for (AssembledPolygon pol : polygon.innerWays) {
-            result.innerWays.add(joinWays(pol.ways));
+        private Collection<UndirectedWaySegment> computeOutline() {
+            return waySegments.stream().filter(
+                        seg -> unionOf.stream().noneMatch(area -> area.contains(seg))
+                    ).collect(Collectors.toList());
         }
-
-        return result;
     }
 
     /**
-     * Joins the outer ways and deletes all short ways that can't be part of a multipolygon anyway.
-     * @param ways The list of outer ways that belong to that multigon.
-     * @return The newly created outer way
-     * @throws UserCancelException if user cancels the operation
+     * Remove a continuous part of the segments
+     * @param segments The segments that may be removed
+     * @return A list of the end nodes of the segments. It is either a closed loop or a segment as long as possible.
      */
-    private Way joinWays(List<WayInPolygon> ways) throws UserCancelException {
-
-        //leave original orientation, if all paths are reverse.
-        boolean allReverse = true;
-        for (WayInPolygon way : ways) {
-            allReverse &= !way.insideToTheRight;
+    private static LinkedList<Node> removeOutlinePart(Collection<UndirectedWaySegment> segments) {
+        Node start = segments.iterator().next().a;
+        LinkedList<Node> nodes = new LinkedList<>();
+        nodes.add(start);
+        // Move in one direction on that way.
+        while ((nodes.size() < 2 || nodes.getFirst() != nodes.getLast()) && !segments.isEmpty()) {
+            Optional<UndirectedWaySegment> traverse = segments.stream().filter(s -> s.hasEnd(nodes.getLast())).findAny();
+            if (!traverse.isPresent()) {
+                break;
+            }
+            segments.remove(traverse.get());
+            nodes.addLast(traverse.get().getOtherEnd(nodes.getLast()));
         }
 
-        if (allReverse) {
-            for (WayInPolygon way : ways) {
-                way.insideToTheRight = !way.insideToTheRight;
+        // Now move in the other direction - as far as we can go.
+        while ((nodes.size() < 2 || nodes.getFirst() != nodes.getLast()) && !segments.isEmpty()) {
+            Optional<UndirectedWaySegment> traverse = segments.stream().filter(s -> s.hasEnd(nodes.getFirst())).findAny();
+            if (!traverse.isPresent()) {
+                break;
             }
+            segments.remove(traverse.get());
+            nodes.addFirst(traverse.get().getOtherEnd(nodes.getFirst()));
         }
 
-        Way joinedWay = joinOrientedWays(ways);
-
-        //should not happen
-        if (joinedWay == null || !joinedWay.isClosed())
-            throw new JosmRuntimeException("Join areas internal error.");
-
-        return joinedWay;
+        return nodes;
     }
 
     /**
-     * Joins a list of ways (using CombineWayAction and ReverseWayAction as specified in WayInPath)
-     * @param ways The list of ways to join and reverse
-     * @return The newly created way
-     * @throws UserCancelException if user cancels the operation
+     * Constructs a new {@code JoinAreasAction}.
      */
-    private Way joinOrientedWays(List<WayInPolygon> ways) throws UserCancelException {
-        if (ways.size() < 2)
-            return ways.get(0).way;
-
-        // This will turn ways so all of them point in the same direction and CombineAction won't bug
-        // the user about this.
-
-        //TODO: ReverseWay and Combine way are really slow and we use them a lot here. This slows down large joins.
-        List<Way> actionWays = new ArrayList<>(ways.size());
-
-        for (WayInPolygon way : ways) {
-            actionWays.add(way.way);
-
-            if (!way.insideToTheRight) {
-                ReverseWayResult res = ReverseWayAction.reverseWay(way.way);
-                commitCommand(res.getReverseCommand());
-                cmdsCount++;
-            }
-        }
-
-        Pair<Way, Command> result = CombineWayAction.combineWaysWorker(actionWays);
-
-        commitCommand(result.b);
-        cmdsCount++;
-
-        return result.a;
+    public JoinAreasAction() {
+        this(true);
     }
 
     /**
-     * This method analyzes multipolygon relationships of given ways and collects addition inner ways to consider.
-     * @param selectedWays the selected ways
-     * @return list of polygons, or null if too complex relation encountered.
+     * Constructs a new {@code JoinAreasAction} with optional shortcut.
+     * @param addShortcut controls whether the shortcut should be registered or not
+     * @since 11611
      */
-    public static List<Multipolygon> collectMultipolygons(Collection<Way> selectedWays) {
-
-        List<Multipolygon> result = new ArrayList<>();
-
-        //prepare the lists, to minimize memory allocation.
-        List<Way> outerWays = new ArrayList<>();
-        List<Way> innerWays = new ArrayList<>();
-
-        Set<Way> processedOuterWays = new LinkedHashSet<>();
-        Set<Way> processedInnerWays = new LinkedHashSet<>();
-
-        for (Relation r : OsmPrimitive.getParentRelations(selectedWays)) {
-            if (r.isDeleted() || !r.isMultipolygon()) {
-                continue;
-            }
-
-            boolean hasKnownOuter = false;
-            outerWays.clear();
-            innerWays.clear();
-
-            for (RelationMember rm : r.getMembers()) {
-                if ("outer".equalsIgnoreCase(rm.getRole())) {
-                    outerWays.add(rm.getWay());
-                    hasKnownOuter |= selectedWays.contains(rm.getWay());
-                } else if ("inner".equalsIgnoreCase(rm.getRole())) {
-                    innerWays.add(rm.getWay());
-                }
-            }
-
-            if (!hasKnownOuter) {
-                continue;
-            }
-
-            if (outerWays.size() > 1) {
-                new Notification(
-                        tr("Sorry. Cannot handle multipolygon relations with multiple outer ways."))
-                        .setIcon(JOptionPane.INFORMATION_MESSAGE)
-                        .show();
-                return null;
-            }
-
-            Way outerWay = outerWays.get(0);
-
-            //retain only selected inner ways
-            innerWays.retainAll(selectedWays);
-
-            if (processedOuterWays.contains(outerWay)) {
-                new Notification(
-                        tr("Sorry. Cannot handle way that is outer in multiple multipolygon relations."))
-                        .setIcon(JOptionPane.INFORMATION_MESSAGE)
-                        .show();
-                return null;
-            }
-
-            if (processedInnerWays.contains(outerWay)) {
-                new Notification(
-                        tr("Sorry. Cannot handle way that is both inner and outer in multipolygon relations."))
-                        .setIcon(JOptionPane.INFORMATION_MESSAGE)
-                        .show();
-                return null;
-            }
-
-            for (Way way :innerWays) {
-                if (processedOuterWays.contains(way)) {
-                    new Notification(
-                            tr("Sorry. Cannot handle way that is both inner and outer in multipolygon relations."))
-                            .setIcon(JOptionPane.INFORMATION_MESSAGE)
-                            .show();
-                    return null;
-                }
-
-                if (processedInnerWays.contains(way)) {
-                    new Notification(
-                            tr("Sorry. Cannot handle way that is inner in multiple multipolygon relations."))
-                            .setIcon(JOptionPane.INFORMATION_MESSAGE)
-                            .show();
-                    return null;
-                }
-            }
-
-            processedOuterWays.add(outerWay);
-            processedInnerWays.addAll(innerWays);
-
-            Multipolygon pol = new Multipolygon(outerWay);
-            pol.innerWays.addAll(innerWays);
-
-            result.add(pol);
-        }
-
-        //add remaining ways, not in relations
-        for (Way way : selectedWays) {
-            if (processedOuterWays.contains(way) || processedInnerWays.contains(way)) {
-                continue;
-            }
-
-            result.add(new Multipolygon(way));
-        }
-
-        return result;
+    public JoinAreasAction(boolean addShortcut) {
+        super(tr("Join overlapping Areas"), "joinareas", tr("Joins areas that overlap each other"),
+                addShortcut ? Shortcut.registerShortcut("tools:joinareas",
+                        tr("Tool: {0}", tr("Join overlapping Areas")), KeyEvent.VK_J, Shortcut.SHIFT) : null,
+                true);
     }
 
     /**
-     * Will add own multipolygon relation to the "previously existing" relations. Fixup is done by fixRelations
-     * @param inner List of already closed inner ways
-     * @return The list of relation with roles to add own relation to
+     * Gets called whenever the shortcut is pressed or the menu entry is selected.
+     * Checks whether the selected objects are suitable to join and joins them if so.
      */
-    private RelationRole addOwnMultipolygonRelation(Collection<Way> inner) {
-        if (inner.isEmpty()) return null;
-        OsmDataLayer layer = Main.getLayerManager().getEditLayer();
-        // Create new multipolygon relation and add all inner ways to it
-        Relation newRel = new Relation();
-        newRel.put("type", "multipolygon");
-        for (Way w : inner) {
-            newRel.addMember(new RelationMember("inner", w));
-        }
-        cmds.add(layer != null ? new AddCommand(layer, newRel) :
-            new AddCommand(inner.iterator().next().getDataSet(), newRel));
-        addedRelations.add(newRel);
-
-        // We don't add outer to the relation because it will be handed to fixRelations()
-        // which will then do the remaining work.
-        return new RelationRole(newRel, "outer");
+    @Override
+    public void actionPerformed(ActionEvent e) {
+        join(Main.getLayerManager().getEditDataSet().getSelected());
     }
 
     /**
-     * Removes a given OsmPrimitive from all relations.
-     * @param osm Element to remove from all relations
-     * @return List of relations with roles the primitives was part of
+     * Joins the given ways.
+     * @param waysAndRelations Ways / Multipolygons to join
+     * @since 7534
      */
-    private List<RelationRole> removeFromAllRelations(OsmPrimitive osm) {
-        List<RelationRole> result = new ArrayList<>();
+    public void join(Collection<? extends OsmPrimitive> waysAndRelations) {
+        waysAndRelations = waysAndRelations.stream()
+                .filter(osm -> (osm instanceof Way && ((Way) osm).isClosed()) || osm.isMultipolygon())
+                .collect(Collectors.toList());
+        if (waysAndRelations.isEmpty()) {
+            new Notification(tr("Please select at least one closed area that should be joined."))
+                    .setIcon(JOptionPane.INFORMATION_MESSAGE).show();
+            return;
+        }
 
-        for (Relation r : osm.getDataSet().getRelations()) {
-            if (r.isDeleted()) {
-                continue;
-            }
-            for (RelationMember rm : r.getMembers()) {
-                if (rm.getMember() != osm) {
-                    continue;
-                }
+        if (!ofSameDataset(waysAndRelations)) {
+            throw new IllegalArgumentException("Not in same DataSet");
+        }
+        waysAndRelations = selectRelationsInsteadOfMembers(waysAndRelations);
+        DataSet ds = waysAndRelations.iterator().next().getDataSet();
 
-                Relation newRel = new Relation(r);
-                List<RelationMember> members = newRel.getMembers();
-                members.remove(rm);
-                newRel.setMembers(members);
-
-                cmds.add(new ChangeCommand(r, newRel));
-                RelationRole saverel = new RelationRole(r, rm.getRole());
-                if (!result.contains(saverel)) {
-                    result.add(saverel);
-                }
-                break;
-            }
+        if (!Command.checkAndConfirmOutlyingOperation("joinarea", tr("Join area confirmation"),
+                trn("The selected area has nodes outside of the downloaded data region.",
+                        "The selected areas have nodes outside of the downloaded data region.", waysAndRelations.size()) + "<br/>"
+                        + tr("This can lead to nodes being deleted accidentally.") + "<br/>"
+                        + tr("Are you really sure to continue?") + tr("Please abort if you are not sure"),
+                tr("The selected area is incomplete. Continue?"), waysAndRelations, null)) {
+            return;
         }
 
-        commitCommands(marktr("Removed Element from Relations"));
-        return result;
-    }
+        try {
+            JoinAreasCollector collector = new JoinAreasCollector(ds, waysAndRelations);
 
-    /**
-     * Adds the previously removed relations again to the outer way. If there are multiple multipolygon
-     * relations where the joined areas were in "outer" role a new relation is created instead with all
-     * members of both. This function depends on multigon relations to be valid already, it won't fix them.
-     * @param rels List of relations with roles the (original) ways were part of
-     * @param outer The newly created outer area/way
-     * @param ownMultipol elements to directly add as outer
-     * @param relationsToDelete set of relations to delete.
-     */
-    private void fixRelations(List<RelationRole> rels, Way outer, RelationRole ownMultipol, Set<Relation> relationsToDelete) {
-        List<RelationRole> multiouters = new ArrayList<>();
+            List<Command> commands = collector.getCommands();
+            Main.main.undoRedo.add(new SequenceCommand(tr("Join Areas"), commands));
 
-        if (ownMultipol != null) {
-            multiouters.add(ownMultipol);
-        }
-
-        for (RelationRole r : rels) {
-            if (r.rel.isMultipolygon() && "outer".equalsIgnoreCase(r.role)) {
-                multiouters.add(r);
-                continue;
-            }
-            // Add it back!
-            Relation newRel = new Relation(r.rel);
-            newRel.addMember(new RelationMember(r.role, outer));
-            cmds.add(new ChangeCommand(r.rel, newRel));
-        }
-
-        OsmDataLayer layer = Main.getLayerManager().getEditLayer();
-        Relation newRel;
-        switch (multiouters.size()) {
-        case 0:
+        } catch (UnclosedAreaException e) {
+            new Notification(tr("One of the selected areas is not closed and therefore cannot be joined."))
+                .setIcon(JOptionPane.INFORMATION_MESSAGE).show();
             return;
-        case 1:
-            // Found only one to be part of a multipolygon relation, so just add it back as well
-            newRel = new Relation(multiouters.get(0).rel);
-            newRel.addMember(new RelationMember(multiouters.get(0).role, outer));
-            cmds.add(new ChangeCommand(multiouters.get(0).rel, newRel));
+        } catch (JoinAreasException e) {
+            new Notification(tr("One of the selected areas has an invalid geomerty."))
+                .setIcon(JOptionPane.INFORMATION_MESSAGE).show();
             return;
-        default:
-            // Create a new relation with all previous members and (Way)outer as outer.
-            newRel = new Relation();
-            for (RelationRole r : multiouters) {
-                // Add members
-                for (RelationMember rm : r.rel.getMembers()) {
-                    if (!newRel.getMembers().contains(rm)) {
-                        newRel.addMember(rm);
-                    }
-                }
-                // Add tags
-                for (String key : r.rel.keySet()) {
-                    newRel.put(key, r.rel.get(key));
-                }
-                // Delete old relation
-                relationsToDelete.add(r.rel);
-            }
-            newRel.addMember(new RelationMember("outer", outer));
-            cmds.add(layer != null ? new AddCommand(layer, newRel) : new AddCommand(outer.getDataSet(), newRel));
         }
     }
 
     /**
-     * Remove all tags from the all the way
-     * @param ways The List of Ways to remove all tags from
+     * If all members of a multipolygon are selected, ask the user to select the polygon instead of the ways.
+     * @param currentSelection
+     * @return The new list of primitives the user selected
      */
-    private void stripTags(Collection<Way> ways) {
-        for (Way w : ways) {
-            final Way wayWithoutTags = new Way(w);
-            wayWithoutTags.removeAll();
-            cmds.add(new ChangeCommand(w, wayWithoutTags));
-        }
-        /* I18N: current action printed in status display */
-        commitCommands(marktr("Remove tags from inner ways"));
-    }
+    private Collection<? extends OsmPrimitive> selectRelationsInsteadOfMembers(Collection<? extends OsmPrimitive> currentSelection) {
+        List<Relation> selectableMultipolygons = currentSelection.stream()
+            .filter(osm -> osm.getType() == OsmPrimitiveType.WAY)
+            // Get all multipolygons referred by the way
+            .flatMap(osm -> osm.getReferrers().stream())
+            .distinct()
+            .filter(osm -> osm.isMultipolygon())
+            .map(osm -> ((Relation) osm))
+            // Filter for those that are completely selected
+            .filter(r -> r.getMembers().stream().map(m -> m.getMember()).allMatch(currentSelection::contains))
+            .collect(Collectors.toList());
 
-    /**
-     * Takes the last cmdsCount actions back and combines them into a single action
-     * (for when the user wants to undo the join action)
-     * @param message The commit message to display
-     */
-    private void makeCommitsOneAction(String message) {
-        cmds.clear();
-        if (Main.main != null) {
-            UndoRedoHandler ur = Main.main.undoRedo;
-            int i = Math.max(ur.commands.size() - cmdsCount, 0);
-            for (; i < ur.commands.size(); i++) {
-                cmds.add(ur.commands.get(i));
+        if (!selectableMultipolygons.isEmpty()) {
+            JPanel msg = new JPanel(new GridBagLayout());
+            msg.add(new JMultilineLabel("<html>" +
+                    tr("You selected the members of the following multipolygons. "
+                            + "Do you want to join the polygons instead?")
+                    + "<ul>" + selectableMultipolygons.stream()
+                            .map(r -> "<li>" + r.getDisplayName(DefaultNameFormatter.getInstance()) + "</li>")
+                            .collect(Collectors.joining())
+                    + "</ul></html>"));
+            boolean answer = ConditionalOptionPaneUtil.showConfirmationDialog(
+                    "join_areas_on_polygons",
+                    Main.parent,
+                    msg,
+                    tr("Join multipolygons?"),
+                    JOptionPane.YES_NO_OPTION,
+                    JOptionPane.QUESTION_MESSAGE,
+                    JOptionPane.YES_OPTION);
+            if (answer) {
+                // the new polygons
+                HashSet<OsmPrimitive> select = new HashSet<>(selectableMultipolygons);
+                // part of the selection that was untouched
+                currentSelection.stream().filter(
+                        w -> !(w instanceof Way && ((Way) w).getReferrers().stream().allMatch(selectableMultipolygons::contains))
+                        ).forEach(select::add);
+                return select;
             }
-
-            for (i = 0; i < cmds.size(); i++) {
-                ur.undo();
-            }
         }
+        return currentSelection;
+    }
 
-        commitCommands(message == null ? marktr("Join Areas Function") : message);
-        cmdsCount = 0;
+    private boolean ofSameDataset(Collection<? extends OsmPrimitive> waysAndRelations) {
+        return waysAndRelations.stream().map(OsmPrimitive::getDataSet).distinct().count() <= 1;
     }
 
+    public static List<List<Node>> getOutline(DataSet data, Collection<? extends OsmPrimitive> primitives) throws JoinAreasException {
+        return new JoinAreasCollector(data, primitives).getOutlines();
+    }
+
     @Override
     protected void updateEnabledState() {
         updateEnabledStateOnCurrentSelection();
Index: src/org/openstreetmap/josm/actions/search/SearchAction.java
===================================================================
--- src/org/openstreetmap/josm/actions/search/SearchAction.java	(Revision 11819)
+++ src/org/openstreetmap/josm/actions/search/SearchAction.java	(Arbeitskopie)
@@ -525,10 +525,7 @@
      * @param mode the search mode to use
      */
     public static void search(String search, SearchMode mode) {
-        final SearchSetting searchSetting = new SearchSetting();
-        searchSetting.text = search;
-        searchSetting.mode = mode;
-        search(searchSetting);
+        search(new SearchSetting(search, mode));
     }
 
     static void search(SearchSetting s) {
@@ -544,15 +541,27 @@
      * @since 10457
      */
     public static Collection<OsmPrimitive> searchAndReturn(String search, SearchMode mode) {
-        final SearchSetting searchSetting = new SearchSetting();
-        searchSetting.text = search;
-        searchSetting.mode = mode;
         CapturingSearchReceiver receiver = new CapturingSearchReceiver();
-        SearchTask.newSearchTask(searchSetting, receiver).run();
+        SearchTask.newSearchTask(new SearchSetting(search, mode), receiver).run();
         return receiver.result;
     }
 
     /**
+     * Performs the search specified by the search string {@code search} and the search mode {@code mode} and returns the result of the search.
+     *
+     * @param search the search string to use
+     * @param ds The dataset to search in
+     * @param mode the search mode to use
+     * @return The result of the search.
+     * @since xxx
+     */
+    public static Collection<OsmPrimitive> searchAndReturn(String search, DataSet ds, SearchMode mode) {
+        CapturingSearchReceiver receiver = new CapturingSearchReceiver();
+        SearchTask.newSearchTask(new SearchSetting(search, mode), ds, receiver).run();
+        return receiver.result;
+    }
+
+    /**
      * Interfaces implementing this may receive the result of the current search.
      * @author Michael Zangl
      * @since 10457
@@ -735,11 +744,20 @@
          * Constructs a new {@code SearchSetting}.
          */
         public SearchSetting() {
-            text = "";
-            mode = SearchMode.replace;
+            this("", SearchMode.replace);
         }
 
         /**
+         * Constructs a new {@code SearchSetting} using the given text / mode
+         * @param text The search definition
+         * @param mode The search mode.
+         */
+        public SearchSetting(String text, SearchMode mode) {
+            this.text = Objects.requireNonNull(text, "text");
+            this.mode = Objects.requireNonNull(mode, "mode");
+        }
+
+        /**
          * Constructs a new {@code SearchSetting} from an existing one.
          * @param original original search settings
          */
Index: src/org/openstreetmap/josm/tools/RightAndLefthandTraffic.java
===================================================================
--- src/org/openstreetmap/josm/tools/RightAndLefthandTraffic.java	(Revision 11819)
+++ src/org/openstreetmap/josm/tools/RightAndLefthandTraffic.java	(Arbeitskopie)
@@ -10,22 +10,21 @@
 import java.io.PrintWriter;
 import java.io.Writer;
 import java.nio.charset.StandardCharsets;
-import java.util.ArrayList;
 import java.util.Collection;
 import java.util.Collections;
 import java.util.List;
 import java.util.Set;
+import java.util.stream.Collectors;
+import java.util.stream.Stream;
 
 import org.openstreetmap.josm.Main;
 import org.openstreetmap.josm.actions.JoinAreasAction;
-import org.openstreetmap.josm.actions.JoinAreasAction.JoinAreasResult;
-import org.openstreetmap.josm.actions.JoinAreasAction.Multipolygon;
-import org.openstreetmap.josm.actions.PurgeAction;
+import org.openstreetmap.josm.actions.JoinAreasAction.JoinAreasException;
 import org.openstreetmap.josm.data.coor.LatLon;
 import org.openstreetmap.josm.data.osm.DataSet;
+import org.openstreetmap.josm.data.osm.Node;
 import org.openstreetmap.josm.data.osm.OsmPrimitive;
 import org.openstreetmap.josm.data.osm.Relation;
-import org.openstreetmap.josm.data.osm.RelationMember;
 import org.openstreetmap.josm.data.osm.Way;
 import org.openstreetmap.josm.io.IllegalDataException;
 import org.openstreetmap.josm.io.OsmReader;
@@ -62,72 +61,68 @@
      * TODO: Synchronization can be refined inside the {@link GeoPropertyIndex} as most look-ups are read-only.
      */
     public static synchronized void initialize() {
-        Collection<Way> optimizedWays = loadOptimizedBoundaries();
-        if (optimizedWays.isEmpty()) {
+        DataSet optimizedWays = loadOptimizedBoundaries();
+        if (optimizedWays.getWays().isEmpty()) {
             optimizedWays = computeOptimizedBoundaries();
             saveOptimizedBoundaries(optimizedWays);
         }
-        rlCache = new GeoPropertyIndex<>(new DefaultGeoProperty(optimizedWays), 24);
+        rlCache = new GeoPropertyIndex<>(new DefaultGeoProperty(optimizedWays.getWays()), 24);
     }
 
-    private static Collection<Way> computeOptimizedBoundaries() {
-        Collection<Way> ways = new ArrayList<>();
-        Collection<OsmPrimitive> toPurge = new ArrayList<>();
+    private static DataSet computeOptimizedBoundaries() {
         // Find all outer ways of left-driving countries. Many of them are adjacent (African and Asian states)
         DataSet data = Territories.getDataSet();
-        Collection<Relation> allRelations = data.getRelations();
-        Collection<Way> allWays = data.getWays();
-        for (Way w : allWays) {
-            if (LEFT.equals(w.get(DRIVING_SIDE))) {
-                addWayIfNotInner(ways, w);
-            }
+        Collection<OsmPrimitive> leftHandTraffic = data.getPrimitives(osm -> (osm instanceof Way || osm.isMultipolygon()) && LEFT.equals(osm.get(DRIVING_SIDE)));
+
+        List<List<Node>> outline;
+        try {
+            outline = JoinAreasAction.getOutline(data, leftHandTraffic);
+        } catch (JoinAreasException e) {
+            // This should not happen.
+            Main.warn(e);
+
+            outline = leftHandTraffic.stream()
+                    .flatMap(RightAndLefthandTraffic::generateSimpleOutline)
+                    .map(Way::getNodes)
+                    .collect(Collectors.toList());
         }
-        for (Relation r : allRelations) {
-            if (r.isMultipolygon() && LEFT.equals(r.get(DRIVING_SIDE))) {
-                for (RelationMember rm : r.getMembers()) {
-                    if (rm.isWay() && "outer".equals(rm.getRole()) && !RIGHT.equals(rm.getMember().get(DRIVING_SIDE))) {
-                        addWayIfNotInner(ways, (Way) rm.getMember());
-                    }
-                }
+
+        // Remove all ways that are not part of the outline.
+        data.getWays().forEach(data::removePrimitive);
+        data.getRelations().forEach(data::removePrimitive);
+        outline.stream().forEach(nodes -> generateWay(nodes, data));
+        data.getNodes().stream().filter(n -> n.getReferrers().isEmpty()).forEach(data::removePrimitive);
+
+        return data;
+    }
+
+    private static void generateWay(List<Node> nodes, DataSet data) {
+        for (Node node : nodes) {
+            // Don't use stream, we can have dupplicate nodes.
+            if (node.getDataSet() == null) {
+                data.addPrimitive(node);
             }
         }
-        toPurge.addAll(allRelations);
-        toPurge.addAll(allWays);
-        toPurge.removeAll(ways);
-        // Remove ways from parent relations for following optimizations
-        for (Relation r : OsmPrimitive.getParentRelations(ways)) {
-            r.setMembers(null);
+        Way w = new Way();
+        w.setNodes(nodes);
+        data.addPrimitive(w);
+    }
+
+    /**
+     * A dirty way to get all left hand traffic outlines.
+     * @param osm The primitives
+     * @return The collection of outline ways.
+     */
+    private static Stream<Way> generateSimpleOutline(OsmPrimitive osm) {
+        if (osm instanceof Way) {
+            return Stream.of((Way) osm);
+        } else {
+            return ((Relation) osm)
+                    .getMembers()
+                    .stream()
+                    .filter(m -> "outer".equals(m.getRole()))
+                    .map(m -> (Way) m.getMember());
         }
-        // Remove all tags to avoid any conflict
-        for (Way w : ways) {
-            w.removeAll();
-        }
-        // Purge all other ways and relations so dataset only contains lefthand traffic data
-        new PurgeAction(false).getPurgeCommand(toPurge).executeCommand();
-        // Combine adjacent countries into a single polygon
-        Collection<Way> optimizedWays = new ArrayList<>();
-        List<Multipolygon> areas = JoinAreasAction.collectMultipolygons(ways);
-        if (areas != null) {
-            try {
-                JoinAreasResult result = new JoinAreasAction(false).joinAreas(areas);
-                if (result.hasChanges()) {
-                    for (Multipolygon mp : result.getPolygons()) {
-                        optimizedWays.add(mp.getOuterWay());
-                    }
-                }
-            } catch (UserCancelException ex) {
-                Main.warn(ex);
-            } catch (JosmRuntimeException ex) {
-                // Workaround to #10511 / #14185. To remove when #10511 is solved
-                Main.error(ex);
-            }
-        }
-        if (optimizedWays.isEmpty()) {
-            // Problem: don't optimize
-            Main.warn("Unable to join left-driving countries polygons");
-            optimizedWays.addAll(ways);
-        }
-        return optimizedWays;
     }
 
     /**
@@ -150,8 +145,7 @@
         ways.add(w);
     }
 
-    private static void saveOptimizedBoundaries(Collection<Way> optimizedWays) {
-        DataSet ds = optimizedWays.iterator().next().getDataSet();
+    private static void saveOptimizedBoundaries(DataSet ds) {
         File file = new File(Main.pref.getCacheDirectory(), "left-right-hand-traffic.osm");
         try (Writer writer = new OutputStreamWriter(new FileOutputStream(file), StandardCharsets.UTF_8);
              OsmWriter w = OsmWriterFactory.createOsmWriter(new PrintWriter(writer), false, ds.getVersion())
@@ -164,12 +158,12 @@
         }
     }
 
-    private static Collection<Way> loadOptimizedBoundaries() {
+    private static DataSet loadOptimizedBoundaries() {
         try (InputStream is = new FileInputStream(new File(Main.pref.getCacheDirectory(), "left-right-hand-traffic.osm"))) {
-           return OsmReader.parseDataSet(is, null).getWays();
+           return OsmReader.parseDataSet(is, null);
         } catch (IllegalDataException | IOException ex) {
             Main.trace(ex);
-            return Collections.emptyList();
+            return new DataSet();
         }
     }
 }
