commit 5e634d0815394d84b96a2a9e5c26a691f1b726a5
Author: Simon Legner <Simon.Legner@gmail.com>
Date:   Fri Aug 12 16:19:28 2016 +0200

    see #7307 - Enhance GeoJSON export
    
    - Save ways as `LineString` or `Polygon` depending on the area style.
    - Save multipolygons as `MultiPolygon`.

diff --git a/src/org/openstreetmap/josm/data/osm/MultipolygonBuilder.java b/src/org/openstreetmap/josm/data/osm/MultipolygonBuilder.java
index 1951a84..3b34abb 100644
--- a/src/org/openstreetmap/josm/data/osm/MultipolygonBuilder.java
+++ b/src/org/openstreetmap/josm/data/osm/MultipolygonBuilder.java
@@ -13,12 +13,15 @@
 import java.util.Collections;
 import java.util.HashSet;
 import java.util.List;
+import java.util.Map;
 import java.util.Set;
 import java.util.concurrent.ForkJoinPool;
 import java.util.concurrent.ForkJoinTask;
 import java.util.concurrent.RecursiveTask;
+import java.util.stream.Collectors;
 
 import org.openstreetmap.josm.Main;
+import org.openstreetmap.josm.tools.CheckParameterUtil;
 import org.openstreetmap.josm.tools.Geometry;
 import org.openstreetmap.josm.tools.Geometry.PolygonIntersection;
 import org.openstreetmap.josm.tools.MultiMap;
@@ -164,6 +167,23 @@ public JoinedPolygonCreationException(String message) {
     }
 
     /**
+     * Joins the given {@code multipolygon} to a pair of outer and inner multipolygon rings.
+     *
+     * @param multipolygon the multipolygon to join.
+     * @return a pair of outer and inner multipolygon rings.
+     * @throws JoinedPolygonCreationException if the creation fails.
+     */
+    public static Pair<List<JoinedPolygon>, List<JoinedPolygon>> joinWays(Relation multipolygon) throws JoinedPolygonCreationException {
+        CheckParameterUtil.ensureThat(multipolygon.isMultipolygon(), "multipolygon.isMultipolygon");
+        final Map<String, Set<Way>> members = multipolygon.getMembers().stream()
+                .filter(RelationMember::isWay)
+                .collect(Collectors.groupingBy(RelationMember::getRole, Collectors.mapping(RelationMember::getWay, Collectors.toSet())));
+        final List<JoinedPolygon> outerRings = joinWays(members.getOrDefault("outer", Collections.emptySet()));
+        final List<JoinedPolygon> innerRings = joinWays(members.getOrDefault("inner", Collections.emptySet()));
+        return Pair.create(outerRings, innerRings);
+    }
+
+    /**
      * Joins the given {@code ways} to multipolygon rings.
      * @param ways the ways to join.
      * @return a list of multipolygon rings.
diff --git a/src/org/openstreetmap/josm/io/GeoJSONWriter.java b/src/org/openstreetmap/josm/io/GeoJSONWriter.java
index 2771b1e..77fa21f 100644
--- a/src/org/openstreetmap/josm/io/GeoJSONWriter.java
+++ b/src/org/openstreetmap/josm/io/GeoJSONWriter.java
@@ -6,8 +6,10 @@
 import java.math.RoundingMode;
 import java.util.HashMap;
 import java.util.Iterator;
+import java.util.List;
 import java.util.Map;
 import java.util.Map.Entry;
+import java.util.stream.Stream;
 
 import javax.json.Json;
 import javax.json.JsonArrayBuilder;
@@ -19,15 +21,17 @@
 import org.openstreetmap.josm.data.coor.EastNorth;
 import org.openstreetmap.josm.data.coor.LatLon;
 import org.openstreetmap.josm.data.osm.DataSet;
-import org.openstreetmap.josm.data.osm.INode;
-import org.openstreetmap.josm.data.osm.IRelation;
-import org.openstreetmap.josm.data.osm.IWay;
+import org.openstreetmap.josm.data.osm.MultipolygonBuilder;
+import org.openstreetmap.josm.data.osm.MultipolygonBuilder.JoinedPolygon;
 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.Way;
-import org.openstreetmap.josm.data.osm.visitor.PrimitiveVisitor;
+import org.openstreetmap.josm.data.osm.visitor.AbstractVisitor;
 import org.openstreetmap.josm.data.projection.Projection;
 import org.openstreetmap.josm.gui.layer.OsmDataLayer;
+import org.openstreetmap.josm.gui.mappaint.ElemStyles;
+import org.openstreetmap.josm.tools.Pair;
 
 /**
  * Writes OSM data as a GeoJSON string, using JSR 353: Java API for JSON Processing (JSON-P).
@@ -79,7 +83,7 @@ public String write(boolean pretty) {
         }
     }
 
-    private class GeometryPrimitiveVisitor implements PrimitiveVisitor {
+    private class GeometryPrimitiveVisitor extends AbstractVisitor {
 
         private final JsonObjectBuilder geomObj;
 
@@ -88,32 +92,43 @@ public String write(boolean pretty) {
         }
 
         @Override
-        public void visit(INode n) {
+        public void visit(Node n) {
             geomObj.add("type", "Point");
             LatLon ll = n.getCoor();
             if (ll != null) {
-                geomObj.add("coordinates", getCoorArray(Json.createArrayBuilder(), n.getCoor()));
+                geomObj.add("coordinates", getCoorArray(null, n.getCoor()));
             }
         }
 
         @Override
-        public void visit(IWay w) {
-            geomObj.add("type", "LineString");
-            if (w instanceof Way) {
-                JsonArrayBuilder array = Json.createArrayBuilder();
-                for (Node n : ((Way) w).getNodes()) {
-                    LatLon ll = n.getCoor();
-                    if (ll != null) {
-                        array.add(getCoorArray(Json.createArrayBuilder(), ll));
-                    }
+        public void visit(Way w) {
+            if (w != null) {
+                final JsonArrayBuilder array = getCoorsArray(w.getNodes());
+                if (ElemStyles.hasAreaElemStyle(w, false)) {
+                    final JsonArrayBuilder container = Json.createArrayBuilder().add(array);
+                    geomObj.add("type", "Polygon");
+                    geomObj.add("coordinates", container);
+                } else {
+                    geomObj.add("type", "LineString");
+                    geomObj.add("coordinates", array);
                 }
-                geomObj.add("coordinates", array);
             }
         }
 
         @Override
-        public void visit(IRelation r) {
-            // Do nothing
+        public void visit(Relation r) {
+            if (r != null && r.isMultipolygon() && !r.hasIncompleteMembers()) {
+                final Pair<List<JoinedPolygon>, List<JoinedPolygon>> mp = MultipolygonBuilder.joinWays(r);
+                final JsonArrayBuilder polygon = Json.createArrayBuilder();
+                Stream.concat(mp.a.stream(), mp.b.stream())
+                        .map(p -> getCoorsArray(p.getNodes())
+                                // since first node is not duplicated as last node
+                                .add(getCoorArray(null, p.getNodes().get(0).getCoor())))
+                        .forEach(polygon::add);
+                geomObj.add("type", "MultiPolygon");
+                final JsonArrayBuilder multiPolygon = Json.createArrayBuilder().add(polygon);
+                geomObj.add("coordinates", multiPolygon);
+            }
         }
     }
 
@@ -122,11 +137,22 @@ private JsonArrayBuilder getCoorArray(JsonArrayBuilder builder, LatLon c) {
     }
 
     private static JsonArrayBuilder getCoorArray(JsonArrayBuilder builder, EastNorth c) {
-        return builder
+        return builder != null ? builder : Json.createArrayBuilder()
                 .add(BigDecimal.valueOf(c.getX()).setScale(11, RoundingMode.HALF_UP))
                 .add(BigDecimal.valueOf(c.getY()).setScale(11, RoundingMode.HALF_UP));
     }
 
+    private JsonArrayBuilder getCoorsArray(Iterable<Node> nodes) {
+        final JsonArrayBuilder builder = Json.createArrayBuilder();
+        for (Node n : nodes) {
+            LatLon ll = n.getCoor();
+            if (ll != null) {
+                builder.add(getCoorArray(null, ll));
+            }
+        }
+        return builder;
+    }
+
     protected void appendPrimitive(OsmPrimitive p, JsonArrayBuilder array) {
         if (p.isIncomplete()) {
             return;
@@ -176,12 +202,7 @@ protected void appendBounds(Bounds b, JsonObjectBuilder object) {
     protected void appendLayerFeatures(DataSet ds, JsonObjectBuilder object) {
         JsonArrayBuilder array = Json.createArrayBuilder();
         if (ds != null) {
-            for (Node n : ds.getNodes()) {
-                appendPrimitive(n, array);
-            }
-            for (Way w : ds.getWays()) {
-                appendPrimitive(w, array);
-            }
+            ds.allPrimitives().forEach(p -> appendPrimitive(p, array));
         }
         object.add("features", array);
     }
diff --git a/src/org/openstreetmap/josm/tools/Geometry.java b/src/org/openstreetmap/josm/tools/Geometry.java
index 2b0437b..4be8207 100644
--- a/src/org/openstreetmap/josm/tools/Geometry.java
+++ b/src/org/openstreetmap/josm/tools/Geometry.java
@@ -11,7 +11,6 @@
 import java.util.Collections;
 import java.util.Comparator;
 import java.util.EnumSet;
-import java.util.HashSet;
 import java.util.LinkedHashSet;
 import java.util.List;
 import java.util.Set;
@@ -25,12 +24,11 @@
 import org.openstreetmap.josm.data.coor.LatLon;
 import org.openstreetmap.josm.data.osm.BBox;
 import org.openstreetmap.josm.data.osm.MultipolygonBuilder;
+import org.openstreetmap.josm.data.osm.MultipolygonBuilder.JoinedPolygon;
 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.Way;
 import org.openstreetmap.josm.data.osm.visitor.paint.relations.Multipolygon;
 import org.openstreetmap.josm.data.osm.visitor.paint.relations.MultipolygonCache;
@@ -862,23 +860,6 @@ public static EastNorth getCenter(List<Node> nodes) {
         return new EastNorth(xC, yC);
     }
 
-    public static class MultiPolygonMembers {
-        public final Set<Way> outers = new HashSet<>();
-        public final Set<Way> inners = new HashSet<>();
-
-        public MultiPolygonMembers(Relation multiPolygon) {
-            for (RelationMember m : multiPolygon.getMembers()) {
-                if (m.getType().equals(OsmPrimitiveType.WAY)) {
-                    if ("outer".equals(m.getRole())) {
-                        outers.add(m.getWay());
-                    } else if ("inner".equals(m.getRole())) {
-                        inners.add(m.getWay());
-                    }
-                }
-            }
-        }
-    }
-
     /**
      * Tests if the {@code node} is inside the multipolygon {@code multiPolygon}. The nullable argument
      * {@code isOuterWayAMatch} allows to decide if the immediate {@code outer} way of the multipolygon is a match.
@@ -903,27 +884,23 @@ public static boolean isNodeInsideMultiPolygon(Node node, Relation multiPolygon,
      */
     public static boolean isPolygonInsideMultiPolygon(List<Node> nodes, Relation multiPolygon, Predicate<Way> isOuterWayAMatch) {
         // Extract outer/inner members from multipolygon
-        final MultiPolygonMembers mpm = new MultiPolygonMembers(multiPolygon);
-        // Construct complete rings for the inner/outer members
-        final List<MultipolygonBuilder.JoinedPolygon> outerRings;
-        final List<MultipolygonBuilder.JoinedPolygon> innerRings;
+        final Pair<List<JoinedPolygon>, List<JoinedPolygon>> outerInner;
         try {
-            outerRings = MultipolygonBuilder.joinWays(mpm.outers);
-            innerRings = MultipolygonBuilder.joinWays(mpm.inners);
+            outerInner = MultipolygonBuilder.joinWays(multiPolygon);
         } catch (MultipolygonBuilder.JoinedPolygonCreationException ex) {
             Main.trace(ex);
             Main.debug("Invalid multipolygon " + multiPolygon);
             return false;
         }
         // Test if object is inside an outer member
-        for (MultipolygonBuilder.JoinedPolygon out : outerRings) {
+        for (JoinedPolygon out : outerInner.a) {
             if (nodes.size() == 1
                     ? nodeInsidePolygon(nodes.get(0), out.getNodes())
                     : EnumSet.of(PolygonIntersection.FIRST_INSIDE_SECOND, PolygonIntersection.CROSSING).contains(
                             polygonIntersection(nodes, out.getNodes()))) {
                 boolean insideInner = false;
                 // If inside an outer, check it is not inside an inner
-                for (MultipolygonBuilder.JoinedPolygon in : innerRings) {
+                for (JoinedPolygon in : outerInner.b) {
                     if (polygonIntersection(in.getNodes(), out.getNodes()) == PolygonIntersection.FIRST_INSIDE_SECOND
                             && (nodes.size() == 1
                             ? nodeInsidePolygon(nodes.get(0), in.getNodes())
diff --git a/test/unit/org/openstreetmap/josm/io/GeoJSONWriterTest.java b/test/unit/org/openstreetmap/josm/io/GeoJSONWriterTest.java
index af79c85..34e0477 100644
--- a/test/unit/org/openstreetmap/josm/io/GeoJSONWriterTest.java
+++ b/test/unit/org/openstreetmap/josm/io/GeoJSONWriterTest.java
@@ -2,16 +2,20 @@
 package org.openstreetmap.josm.io;
 
 import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertTrue;
 
 import org.junit.BeforeClass;
 import org.junit.Test;
 import org.openstreetmap.josm.JOSMFixture;
+import org.openstreetmap.josm.TestUtils;
 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.gui.layer.OsmDataLayer;
 import org.openstreetmap.josm.gui.preferences.projection.ProjectionPreference;
 
+import java.io.FileInputStream;
+
 /**
  * Unit tests of {@link GeoJSONWriter} class.
  */
@@ -65,4 +69,17 @@ public void testPoint() {
                 "    ]\n" +
                 "}").replace("'", "\""), writer.write().trim());
     }
+
+    /**
+     * Unit test for multipolygon
+     */
+    @Test
+    public void testMultipolygon() throws Exception {
+        try (FileInputStream in = new FileInputStream(TestUtils.getTestDataRoot() + "multipolygon.osm")) {
+            DataSet ds = OsmReader.parseDataSet(in, null);
+            final OsmDataLayer layer = new OsmDataLayer(ds, "foo", null);
+            final GeoJSONWriter writer = new GeoJSONWriter(layer, ProjectionPreference.wgs84.getProjection());
+            assertTrue(writer.write().contains("MultiPolygon"));
+        }
+    }
 }
