Index: src/org/openstreetmap/josm/data/validation/tests/MultipolygonTest.java
===================================================================
--- src/org/openstreetmap/josm/data/validation/tests/MultipolygonTest.java	(revision 15185)
+++ src/org/openstreetmap/josm/data/validation/tests/MultipolygonTest.java	(working copy)
@@ -16,6 +16,10 @@
 import java.util.Map;
 import java.util.Map.Entry;
 import java.util.Set;
+import java.util.concurrent.ForkJoinPool;
+import java.util.concurrent.ForkJoinTask;
+import java.util.concurrent.RecursiveTask;
+import java.util.function.Supplier;
 
 import org.openstreetmap.josm.command.ChangeCommand;
 import org.openstreetmap.josm.command.Command;
@@ -38,6 +42,8 @@
 import org.openstreetmap.josm.tools.Geometry;
 import org.openstreetmap.josm.tools.Geometry.PolygonIntersection;
 import org.openstreetmap.josm.tools.Logging;
+import org.openstreetmap.josm.tools.Pair;
+import org.openstreetmap.josm.tools.Utils;
 
 /**
  * Checks if multipolygons are valid
@@ -44,7 +50,18 @@
  * @since 3669
  */
 public class MultipolygonTest extends Test {
+    private static final ForkJoinPool THREAD_POOL = newForkJoinPool();
 
+    private static ForkJoinPool newForkJoinPool() {
+        try {
+            return Utils.newForkJoinPool(
+                    "multipolygon_creation.numberOfThreads", "multipolygon-test-%d", Thread.NORM_PRIORITY);
+        } catch (SecurityException e) {
+            Logging.log(Logging.LEVEL_ERROR, "Unable to create new ForkJoinPool", e);
+            return null;
+        }
+    }
+
     /** Non-Way in multipolygon */
     public static final int WRONG_MEMBER_TYPE = 1601;
     /** No useful role for multipolygon member */
@@ -463,8 +480,7 @@
      * @param sharedNodes all nodes shared by multiple ways of this multipolygon
      */
     private void checkOrSetRoles(Relation r, List<PolyData> allPolygons, Map<Long, RelationMember> wayMap, Set<Node> sharedNodes) {
-        PolygonLevelFinder levelFinder = new PolygonLevelFinder(sharedNodes);
-        List<PolygonLevel> list = levelFinder.findOuterWays(allPolygons);
+        List<PolygonLevel> list = findOuterWaysMultiThread(allPolygons, sharedNodes);
         if (list == null || list.isEmpty()) {
             return;
         }
@@ -800,19 +816,100 @@
     }
 
     /**
+     * Collects outer way and corresponding inner ways from all rings.
+     * @param rings the polygon rings
+     * @param sharedNodes all nodes shared by multiple ways of this multipolygon
+     * @return list of nesting levels
+     */
+    private static List<PolygonLevel> findOuterWaysMultiThread(List<PolyData> rings, Set<Node> sharedNodes) {
+        final IntersectionMatrix cache = new IntersectionMatrix(rings);
+        PolygonLevelFinder worker = new PolygonLevelFinder(cache, sharedNodes, rings, 0, rings.size(),
+                new ArrayList<PolygonLevel>(), 128);
+        if (THREAD_POOL != null) {
+            return THREAD_POOL.invoke(worker);
+        } else {
+            return worker.computeDirectly();
+        }
+    }
+
+    /**
+     * Helper class to avoid unneeded costly intersection calculations.
+     * If the intersection between polygons a and b was calculated we also know
+     * the result of intersection between b and a. The lookup in the hash tables is
+     * much faster than the intersection calculation.
+     */
+    private static class IntersectionMatrix {
+        private final Map<Pair<PolyData, PolyData>, PolygonIntersection> results;
+
+        IntersectionMatrix(Collection<PolyData> polygons) {
+            results = new HashMap<>(Utils.hashMapInitialCapacity(polygons.size() * polygons.size()));
+        }
+
+        /**
+         * Compute the reverse result of the intersection test done by {@code Geometry.polygonIntersection(Area a1, Area a2)}
+         *
+         * @param intersection the intersection result for polygons a1 and a2 (in that order)
+         * @return the intersection result for a2 and a1
+         */
+        private static PolygonIntersection getReverseIntersectionResult(PolygonIntersection intersection) {
+            switch (intersection) {
+                case FIRST_INSIDE_SECOND:
+                    return PolygonIntersection.SECOND_INSIDE_FIRST;
+                case SECOND_INSIDE_FIRST:
+                    return PolygonIntersection.FIRST_INSIDE_SECOND;
+                default:
+                    return intersection;
+            }
+        }
+
+        /**
+         * Returns the precomputed intersection between two polygons if known. Otherwise perform {@code computation}.
+         *
+         * @param a1          first polygon
+         * @param a2          second polygon
+         * @param computation the computation to perform when intersection is unknown
+         * @return the intersection between two polygons
+         * @see Map#computeIfAbsent
+         */
+        PolygonIntersection computeIfAbsent(PolyData a1, PolyData a2, Supplier<PolygonIntersection> computation) {
+            PolygonIntersection intersection = results.get(Pair.create(a1, a2));
+            if (intersection == null) {
+                intersection = computation.get();
+                synchronized (results) {
+                    results.put(Pair.create(a1, a2), intersection);
+                    results.put(Pair.create(a2, a1), getReverseIntersectionResult(intersection));
+                }
+            }
+            return intersection;
+        }
+
+    }
+
+    /**
      * Find nesting levels of polygons. Logic taken from class MultipolygonBuilder, uses different structures.
      */
-    private static class PolygonLevelFinder {
-        private final Set<Node> sharedNodes;
+    private static class PolygonLevelFinder extends RecursiveTask<List<PolygonLevel>> {
+        private final transient IntersectionMatrix cache;
+        private final transient Set<Node> sharedNodes;
+        private final transient List<PolyData> input;
+        private final int from;
+        private final int to;
+        private final transient List<PolygonLevel> output;
+        private final int directExecutionTaskSize;
 
-        PolygonLevelFinder(Set<Node> sharedNodes) {
+        private static final long serialVersionUID = 0;
+
+        PolygonLevelFinder(IntersectionMatrix cache, Set<Node> sharedNodes, List<PolyData> input, int from, int to, List<PolygonLevel> output,
+                int directExecutionTaskSize) {
+            this.cache = cache;
             this.sharedNodes = sharedNodes;
+            this.input = input;
+            this.from = from;
+            this.to = to;
+            this.output = output;
+            this.directExecutionTaskSize = directExecutionTaskSize;
         }
 
-        List<PolygonLevel> findOuterWays(List<PolyData> allPolygons) {
-            return findOuterWaysRecursive(0, allPolygons);
-        }
-
         private List<PolygonLevel> findOuterWaysRecursive(int level, List<PolyData> polygons) {
             final List<PolygonLevel> result = new ArrayList<>();
 
@@ -824,16 +921,16 @@
         }
 
         private void processOuterWay(int level, List<PolyData> polygons, List<PolygonLevel> result, PolyData pd) {
-            List<PolyData> inners = findInnerWaysCandidates(pd, polygons);
+            Pair<Boolean, List<PolyData>> res = findInnerWaysForOuterCandidate(pd, polygons);
 
-            if (inners != null) {
+            if (res.a) {
                 //add new outer polygon
                 PolygonLevel pol = new PolygonLevel(pd, level);
 
                 //process inner ways
+                List<PolyData> inners = res.b;
                 if (!inners.isEmpty()) {
-                    List<PolygonLevel> innerList = findOuterWaysRecursive(level + 1, inners);
-                    result.addAll(innerList);
+                    result.addAll(findOuterWaysRecursive(level + 1, inners));
                 }
 
                 result.add(pol);
@@ -841,60 +938,65 @@
         }
 
         /**
-         * Check if polygon is an out-most ring, if so, collect the inners
+         * Check if polygon is an out-most ring for all given polygons, if so, collect the inner rings.
          * @param outerCandidate polygon which is checked
          * @param polygons all polygons
-         * @return null if outerCandidate is inside any other polygon, else a list of inner polygons (which might be empty)
+         * @return pair, first value is true if outerCandidate is a real outer ring, 2nd value contains the list of inner rings
+         * which might be empty
          */
-        private List<PolyData> findInnerWaysCandidates(PolyData outerCandidate, List<PolyData> polygons) {
+        private Pair<Boolean, List<PolyData>> findInnerWaysForOuterCandidate(PolyData outerCandidate, List<PolyData> polygons) {
             List<PolyData> innerCandidates = new ArrayList<>();
-
+            Boolean outerGood = Boolean.TRUE;
             for (PolyData inner : polygons) {
-                if (inner == outerCandidate) {
-                    continue;
+                if (inner != outerCandidate && outerCandidate.getBounds().intersects(inner.getBounds())) {
+                    final PolygonIntersection intersection = cache.computeIfAbsent(inner, outerCandidate,
+                            () -> getNesting(inner, outerCandidate));
+                    if (PolygonIntersection.FIRST_INSIDE_SECOND == intersection) {
+                        innerCandidates.add(inner);
+                    } else if (PolygonIntersection.SECOND_INSIDE_FIRST == intersection) {
+                        outerGood = Boolean.FALSE;
+                        break;
+                    }
                 }
-                if (!outerCandidate.getBounds().intersects(inner.getBounds())) {
-                    continue;
-                }
-                boolean useIntersectionTest = false;
-                Node unsharedOuterNode = null;
-                Node unsharedInnerNode = getNonIntersectingNode(outerCandidate, inner);
-                if (unsharedInnerNode != null) {
-                    if (checkIfNodeIsInsidePolygon(unsharedInnerNode, outerCandidate)) {
-                        innerCandidates.add(inner);
+            }
+            return new Pair<>(outerGood, innerCandidates);
+        }
+
+        private PolygonIntersection getNesting(PolyData innerCandidate, PolyData outerCandidate) {
+            boolean useIntersectionTest = false;
+            Node unsharedOuterNode = null;
+            Node unsharedInnerNode = getNonIntersectingNode(outerCandidate, innerCandidate);
+            if (unsharedInnerNode != null) {
+                if (checkIfNodeIsInsidePolygon(unsharedInnerNode, outerCandidate)) {
+                    return PolygonIntersection.FIRST_INSIDE_SECOND;
+                } else {
+                    // inner is not inside outerCandidate, check if it contains outerCandidate
+                    unsharedOuterNode = getNonIntersectingNode(innerCandidate, outerCandidate);
+                    if (unsharedOuterNode != null) {
+                        if (checkIfNodeIsInsidePolygon(unsharedOuterNode, innerCandidate)) {
+                            return PolygonIntersection.SECOND_INSIDE_FIRST; // outer is inside inner
+                        }
                     } else {
-                        // inner is not inside outerCandidate, check if it contains outerCandidate
-                        unsharedOuterNode = getNonIntersectingNode(inner, outerCandidate);
-                        if (unsharedOuterNode != null) {
-                            if (checkIfNodeIsInsidePolygon(unsharedOuterNode, inner)) {
-                                return null; // outer is inside inner
-                            }
-                        } else {
-                            useIntersectionTest = true;
-                        }
+                        useIntersectionTest = true;
                     }
+                }
+            } else {
+                // all nodes of inner are also nodes of outerCandidate
+                unsharedOuterNode = getNonIntersectingNode(innerCandidate, outerCandidate);
+                if (unsharedOuterNode == null) {
+                    return PolygonIntersection.CROSSING; // all nodes shared -> same ways, maybe different direction
                 } else {
-                    // all nodes of inner are also nodes of outerCandidate
-                    unsharedOuterNode = getNonIntersectingNode(inner, outerCandidate);
-                    if (unsharedOuterNode == null) {
-                        return null; // all nodes shared -> same ways, maybe different direction
+                    if (checkIfNodeIsInsidePolygon(unsharedOuterNode, innerCandidate)) {
+                        return null; // outer is inside inner
                     } else {
-                        if (checkIfNodeIsInsidePolygon(unsharedOuterNode, inner)) {
-                            return null; // outer is inside inner
-                        } else {
-                            useIntersectionTest = true;
-                        }
+                        useIntersectionTest = true;
                     }
                 }
-                if (useIntersectionTest) {
-                    PolygonIntersection res = Geometry.polygonIntersection(inner.getNodes(), outerCandidate.getNodes());
-                    if (res == PolygonIntersection.FIRST_INSIDE_SECOND)
-                        innerCandidates.add(inner);
-                    else if (res == PolygonIntersection.SECOND_INSIDE_FIRST)
-                        return null;
-                }
             }
-            return innerCandidates;
+            if (useIntersectionTest) {
+                return Geometry.polygonIntersection(innerCandidate.getNodes(), outerCandidate.getNodes());
+            }
+            return PolygonIntersection.OUTSIDE;
         }
 
         /**
@@ -910,12 +1012,38 @@
             }
             return null;
         }
+
+        @Override
+        protected List<PolygonLevel> compute() {
+            if (to - from <= directExecutionTaskSize) {
+                return computeDirectly();
+            } else {
+                final Collection<ForkJoinTask<List<PolygonLevel>>> tasks = new ArrayList<>();
+                for (int fromIndex = from; fromIndex < to; fromIndex += directExecutionTaskSize) {
+                    tasks.add(new PolygonLevelFinder(cache, sharedNodes, input, fromIndex,
+                            Math.min(fromIndex + directExecutionTaskSize, to), new ArrayList<PolygonLevel>(),
+                            directExecutionTaskSize));
+                }
+                for (ForkJoinTask<List<PolygonLevel>> task : ForkJoinTask.invokeAll(tasks)) {
+                    List<PolygonLevel> res = task.join();
+                    output.addAll(res);
+                }
+                return output;
+            }
+        }
+
+        List<PolygonLevel> computeDirectly() {
+            for (int i = from; i < to; i++) {
+                processOuterWay(0, input, output, input.get(i));
+            }
+            return output;
+        }
     }
 
     /**
      * Create a multipolygon relation from the given ways and test it.
      * @param ways the collection of ways
-     * @return a pair of a {@link Multipolygon} instance and the relation.
+     * @return the relation, caller should call getErrors() to check if relation is a valid multipolygon
      * @since 15160
      */
     public Relation makeFromWays(Collection<Way> ways) {
