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
|
b
|
|
| 13 | 13 | import java.util.Collections; |
| 14 | 14 | import java.util.HashSet; |
| 15 | 15 | import java.util.List; |
| | 16 | import java.util.Map; |
| 16 | 17 | import java.util.Set; |
| 17 | 18 | import java.util.concurrent.ForkJoinPool; |
| 18 | 19 | import java.util.concurrent.ForkJoinTask; |
| 19 | 20 | import java.util.concurrent.RecursiveTask; |
| | 21 | import java.util.stream.Collectors; |
| 20 | 22 | |
| 21 | 23 | import org.openstreetmap.josm.Main; |
| | 24 | import org.openstreetmap.josm.tools.CheckParameterUtil; |
| 22 | 25 | import org.openstreetmap.josm.tools.Geometry; |
| 23 | 26 | import org.openstreetmap.josm.tools.Geometry.PolygonIntersection; |
| 24 | 27 | import org.openstreetmap.josm.tools.MultiMap; |
| … |
… |
public JoinedPolygonCreationException(String message) {
|
| 164 | 167 | } |
| 165 | 168 | |
| 166 | 169 | /** |
| | 170 | * Joins the given {@code multipolygon} to a pair of outer and inner multipolygon rings. |
| | 171 | * |
| | 172 | * @param multipolygon the multipolygon to join. |
| | 173 | * @return a pair of outer and inner multipolygon rings. |
| | 174 | * @throws JoinedPolygonCreationException if the creation fails. |
| | 175 | */ |
| | 176 | public static Pair<List<JoinedPolygon>, List<JoinedPolygon>> joinWays(Relation multipolygon) throws JoinedPolygonCreationException { |
| | 177 | CheckParameterUtil.ensureThat(multipolygon.isMultipolygon(), "multipolygon.isMultipolygon"); |
| | 178 | final Map<String, Set<Way>> members = multipolygon.getMembers().stream() |
| | 179 | .filter(RelationMember::isWay) |
| | 180 | .collect(Collectors.groupingBy(RelationMember::getRole, Collectors.mapping(RelationMember::getWay, Collectors.toSet()))); |
| | 181 | final List<JoinedPolygon> outerRings = joinWays(members.getOrDefault("outer", Collections.emptySet())); |
| | 182 | final List<JoinedPolygon> innerRings = joinWays(members.getOrDefault("inner", Collections.emptySet())); |
| | 183 | return Pair.create(outerRings, innerRings); |
| | 184 | } |
| | 185 | |
| | 186 | /** |
| 167 | 187 | * Joins the given {@code ways} to multipolygon rings. |
| 168 | 188 | * @param ways the ways to join. |
| 169 | 189 | * @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
|
b
|
|
| 6 | 6 | import java.math.RoundingMode; |
| 7 | 7 | import java.util.HashMap; |
| 8 | 8 | import java.util.Iterator; |
| | 9 | import java.util.List; |
| 9 | 10 | import java.util.Map; |
| 10 | 11 | import java.util.Map.Entry; |
| | 12 | import java.util.stream.Stream; |
| 11 | 13 | |
| 12 | 14 | import javax.json.Json; |
| 13 | 15 | import javax.json.JsonArrayBuilder; |
| … |
… |
|
| 19 | 21 | import org.openstreetmap.josm.data.coor.EastNorth; |
| 20 | 22 | import org.openstreetmap.josm.data.coor.LatLon; |
| 21 | 23 | import org.openstreetmap.josm.data.osm.DataSet; |
| 22 | | import org.openstreetmap.josm.data.osm.INode; |
| 23 | | import org.openstreetmap.josm.data.osm.IRelation; |
| 24 | | import org.openstreetmap.josm.data.osm.IWay; |
| | 24 | import org.openstreetmap.josm.data.osm.MultipolygonBuilder; |
| | 25 | import org.openstreetmap.josm.data.osm.MultipolygonBuilder.JoinedPolygon; |
| 25 | 26 | import org.openstreetmap.josm.data.osm.Node; |
| 26 | 27 | import org.openstreetmap.josm.data.osm.OsmPrimitive; |
| | 28 | import org.openstreetmap.josm.data.osm.Relation; |
| 27 | 29 | import org.openstreetmap.josm.data.osm.Way; |
| 28 | | import org.openstreetmap.josm.data.osm.visitor.PrimitiveVisitor; |
| | 30 | import org.openstreetmap.josm.data.osm.visitor.AbstractVisitor; |
| 29 | 31 | import org.openstreetmap.josm.data.projection.Projection; |
| 30 | 32 | import org.openstreetmap.josm.gui.layer.OsmDataLayer; |
| | 33 | import org.openstreetmap.josm.gui.mappaint.ElemStyles; |
| | 34 | import org.openstreetmap.josm.tools.Pair; |
| 31 | 35 | |
| 32 | 36 | /** |
| 33 | 37 | * Writes OSM data as a GeoJSON string, using JSR 353: Java API for JSON Processing (JSON-P). |
| … |
… |
public String write(boolean pretty) {
|
| 79 | 83 | } |
| 80 | 84 | } |
| 81 | 85 | |
| 82 | | private class GeometryPrimitiveVisitor implements PrimitiveVisitor { |
| | 86 | private class GeometryPrimitiveVisitor extends AbstractVisitor { |
| 83 | 87 | |
| 84 | 88 | private final JsonObjectBuilder geomObj; |
| 85 | 89 | |
| … |
… |
public String write(boolean pretty) {
|
| 88 | 92 | } |
| 89 | 93 | |
| 90 | 94 | @Override |
| 91 | | public void visit(INode n) { |
| | 95 | public void visit(Node n) { |
| 92 | 96 | geomObj.add("type", "Point"); |
| 93 | 97 | LatLon ll = n.getCoor(); |
| 94 | 98 | if (ll != null) { |
| 95 | | geomObj.add("coordinates", getCoorArray(Json.createArrayBuilder(), n.getCoor())); |
| | 99 | geomObj.add("coordinates", getCoorArray(null, n.getCoor())); |
| 96 | 100 | } |
| 97 | 101 | } |
| 98 | 102 | |
| 99 | 103 | @Override |
| 100 | | public void visit(IWay w) { |
| 101 | | geomObj.add("type", "LineString"); |
| 102 | | if (w instanceof Way) { |
| 103 | | JsonArrayBuilder array = Json.createArrayBuilder(); |
| 104 | | for (Node n : ((Way) w).getNodes()) { |
| 105 | | LatLon ll = n.getCoor(); |
| 106 | | if (ll != null) { |
| 107 | | array.add(getCoorArray(Json.createArrayBuilder(), ll)); |
| 108 | | } |
| | 104 | public void visit(Way w) { |
| | 105 | if (w != null) { |
| | 106 | final JsonArrayBuilder array = getCoorsArray(w.getNodes()); |
| | 107 | if (ElemStyles.hasAreaElemStyle(w, false)) { |
| | 108 | final JsonArrayBuilder container = Json.createArrayBuilder().add(array); |
| | 109 | geomObj.add("type", "Polygon"); |
| | 110 | geomObj.add("coordinates", container); |
| | 111 | } else { |
| | 112 | geomObj.add("type", "LineString"); |
| | 113 | geomObj.add("coordinates", array); |
| 109 | 114 | } |
| 110 | | geomObj.add("coordinates", array); |
| 111 | 115 | } |
| 112 | 116 | } |
| 113 | 117 | |
| 114 | 118 | @Override |
| 115 | | public void visit(IRelation r) { |
| 116 | | // Do nothing |
| | 119 | public void visit(Relation r) { |
| | 120 | if (r != null && r.isMultipolygon() && !r.hasIncompleteMembers()) { |
| | 121 | final Pair<List<JoinedPolygon>, List<JoinedPolygon>> mp = MultipolygonBuilder.joinWays(r); |
| | 122 | final JsonArrayBuilder polygon = Json.createArrayBuilder(); |
| | 123 | Stream.concat(mp.a.stream(), mp.b.stream()) |
| | 124 | .map(p -> getCoorsArray(p.getNodes()) |
| | 125 | // since first node is not duplicated as last node |
| | 126 | .add(getCoorArray(null, p.getNodes().get(0).getCoor()))) |
| | 127 | .forEach(polygon::add); |
| | 128 | geomObj.add("type", "MultiPolygon"); |
| | 129 | final JsonArrayBuilder multiPolygon = Json.createArrayBuilder().add(polygon); |
| | 130 | geomObj.add("coordinates", multiPolygon); |
| | 131 | } |
| 117 | 132 | } |
| 118 | 133 | } |
| 119 | 134 | |
| … |
… |
private JsonArrayBuilder getCoorArray(JsonArrayBuilder builder, LatLon c) {
|
| 122 | 137 | } |
| 123 | 138 | |
| 124 | 139 | private static JsonArrayBuilder getCoorArray(JsonArrayBuilder builder, EastNorth c) { |
| 125 | | return builder |
| | 140 | return builder != null ? builder : Json.createArrayBuilder() |
| 126 | 141 | .add(BigDecimal.valueOf(c.getX()).setScale(11, RoundingMode.HALF_UP)) |
| 127 | 142 | .add(BigDecimal.valueOf(c.getY()).setScale(11, RoundingMode.HALF_UP)); |
| 128 | 143 | } |
| 129 | 144 | |
| | 145 | private JsonArrayBuilder getCoorsArray(Iterable<Node> nodes) { |
| | 146 | final JsonArrayBuilder builder = Json.createArrayBuilder(); |
| | 147 | for (Node n : nodes) { |
| | 148 | LatLon ll = n.getCoor(); |
| | 149 | if (ll != null) { |
| | 150 | builder.add(getCoorArray(null, ll)); |
| | 151 | } |
| | 152 | } |
| | 153 | return builder; |
| | 154 | } |
| | 155 | |
| 130 | 156 | protected void appendPrimitive(OsmPrimitive p, JsonArrayBuilder array) { |
| 131 | 157 | if (p.isIncomplete()) { |
| 132 | 158 | return; |
| … |
… |
protected void appendBounds(Bounds b, JsonObjectBuilder object) {
|
| 176 | 202 | protected void appendLayerFeatures(DataSet ds, JsonObjectBuilder object) { |
| 177 | 203 | JsonArrayBuilder array = Json.createArrayBuilder(); |
| 178 | 204 | if (ds != null) { |
| 179 | | for (Node n : ds.getNodes()) { |
| 180 | | appendPrimitive(n, array); |
| 181 | | } |
| 182 | | for (Way w : ds.getWays()) { |
| 183 | | appendPrimitive(w, array); |
| 184 | | } |
| | 205 | ds.allPrimitives().forEach(p -> appendPrimitive(p, array)); |
| 185 | 206 | } |
| 186 | 207 | object.add("features", array); |
| 187 | 208 | } |
diff --git a/src/org/openstreetmap/josm/tools/Geometry.java b/src/org/openstreetmap/josm/tools/Geometry.java
index 2b0437b..4be8207 100644
|
a
|
b
|
|
| 11 | 11 | import java.util.Collections; |
| 12 | 12 | import java.util.Comparator; |
| 13 | 13 | import java.util.EnumSet; |
| 14 | | import java.util.HashSet; |
| 15 | 14 | import java.util.LinkedHashSet; |
| 16 | 15 | import java.util.List; |
| 17 | 16 | import java.util.Set; |
| … |
… |
|
| 25 | 24 | import org.openstreetmap.josm.data.coor.LatLon; |
| 26 | 25 | import org.openstreetmap.josm.data.osm.BBox; |
| 27 | 26 | import org.openstreetmap.josm.data.osm.MultipolygonBuilder; |
| | 27 | import org.openstreetmap.josm.data.osm.MultipolygonBuilder.JoinedPolygon; |
| 28 | 28 | import org.openstreetmap.josm.data.osm.Node; |
| 29 | 29 | import org.openstreetmap.josm.data.osm.NodePositionComparator; |
| 30 | 30 | import org.openstreetmap.josm.data.osm.OsmPrimitive; |
| 31 | | import org.openstreetmap.josm.data.osm.OsmPrimitiveType; |
| 32 | 31 | import org.openstreetmap.josm.data.osm.Relation; |
| 33 | | import org.openstreetmap.josm.data.osm.RelationMember; |
| 34 | 32 | import org.openstreetmap.josm.data.osm.Way; |
| 35 | 33 | import org.openstreetmap.josm.data.osm.visitor.paint.relations.Multipolygon; |
| 36 | 34 | import org.openstreetmap.josm.data.osm.visitor.paint.relations.MultipolygonCache; |
| … |
… |
public static EastNorth getCenter(List<Node> nodes) {
|
| 862 | 860 | return new EastNorth(xC, yC); |
| 863 | 861 | } |
| 864 | 862 | |
| 865 | | public static class MultiPolygonMembers { |
| 866 | | public final Set<Way> outers = new HashSet<>(); |
| 867 | | public final Set<Way> inners = new HashSet<>(); |
| 868 | | |
| 869 | | public MultiPolygonMembers(Relation multiPolygon) { |
| 870 | | for (RelationMember m : multiPolygon.getMembers()) { |
| 871 | | if (m.getType().equals(OsmPrimitiveType.WAY)) { |
| 872 | | if ("outer".equals(m.getRole())) { |
| 873 | | outers.add(m.getWay()); |
| 874 | | } else if ("inner".equals(m.getRole())) { |
| 875 | | inners.add(m.getWay()); |
| 876 | | } |
| 877 | | } |
| 878 | | } |
| 879 | | } |
| 880 | | } |
| 881 | | |
| 882 | 863 | /** |
| 883 | 864 | * Tests if the {@code node} is inside the multipolygon {@code multiPolygon}. The nullable argument |
| 884 | 865 | * {@code isOuterWayAMatch} allows to decide if the immediate {@code outer} way of the multipolygon is a match. |
| … |
… |
public static boolean isNodeInsideMultiPolygon(Node node, Relation multiPolygon,
|
| 903 | 884 | */ |
| 904 | 885 | public static boolean isPolygonInsideMultiPolygon(List<Node> nodes, Relation multiPolygon, Predicate<Way> isOuterWayAMatch) { |
| 905 | 886 | // Extract outer/inner members from multipolygon |
| 906 | | final MultiPolygonMembers mpm = new MultiPolygonMembers(multiPolygon); |
| 907 | | // Construct complete rings for the inner/outer members |
| 908 | | final List<MultipolygonBuilder.JoinedPolygon> outerRings; |
| 909 | | final List<MultipolygonBuilder.JoinedPolygon> innerRings; |
| | 887 | final Pair<List<JoinedPolygon>, List<JoinedPolygon>> outerInner; |
| 910 | 888 | try { |
| 911 | | outerRings = MultipolygonBuilder.joinWays(mpm.outers); |
| 912 | | innerRings = MultipolygonBuilder.joinWays(mpm.inners); |
| | 889 | outerInner = MultipolygonBuilder.joinWays(multiPolygon); |
| 913 | 890 | } catch (MultipolygonBuilder.JoinedPolygonCreationException ex) { |
| 914 | 891 | Main.trace(ex); |
| 915 | 892 | Main.debug("Invalid multipolygon " + multiPolygon); |
| 916 | 893 | return false; |
| 917 | 894 | } |
| 918 | 895 | // Test if object is inside an outer member |
| 919 | | for (MultipolygonBuilder.JoinedPolygon out : outerRings) { |
| | 896 | for (JoinedPolygon out : outerInner.a) { |
| 920 | 897 | if (nodes.size() == 1 |
| 921 | 898 | ? nodeInsidePolygon(nodes.get(0), out.getNodes()) |
| 922 | 899 | : EnumSet.of(PolygonIntersection.FIRST_INSIDE_SECOND, PolygonIntersection.CROSSING).contains( |
| 923 | 900 | polygonIntersection(nodes, out.getNodes()))) { |
| 924 | 901 | boolean insideInner = false; |
| 925 | 902 | // If inside an outer, check it is not inside an inner |
| 926 | | for (MultipolygonBuilder.JoinedPolygon in : innerRings) { |
| | 903 | for (JoinedPolygon in : outerInner.b) { |
| 927 | 904 | if (polygonIntersection(in.getNodes(), out.getNodes()) == PolygonIntersection.FIRST_INSIDE_SECOND |
| 928 | 905 | && (nodes.size() == 1 |
| 929 | 906 | ? 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
|
b
|
|
| 2 | 2 | package org.openstreetmap.josm.io; |
| 3 | 3 | |
| 4 | 4 | import static org.junit.Assert.assertEquals; |
| | 5 | import static org.junit.Assert.assertTrue; |
| 5 | 6 | |
| 6 | 7 | import org.junit.BeforeClass; |
| 7 | 8 | import org.junit.Test; |
| 8 | 9 | import org.openstreetmap.josm.JOSMFixture; |
| | 10 | import org.openstreetmap.josm.TestUtils; |
| 9 | 11 | import org.openstreetmap.josm.data.coor.LatLon; |
| 10 | 12 | import org.openstreetmap.josm.data.osm.DataSet; |
| 11 | 13 | import org.openstreetmap.josm.data.osm.Node; |
| 12 | 14 | import org.openstreetmap.josm.gui.layer.OsmDataLayer; |
| 13 | 15 | import org.openstreetmap.josm.gui.preferences.projection.ProjectionPreference; |
| 14 | 16 | |
| | 17 | import java.io.FileInputStream; |
| | 18 | |
| 15 | 19 | /** |
| 16 | 20 | * Unit tests of {@link GeoJSONWriter} class. |
| 17 | 21 | */ |
| … |
… |
public void testPoint() {
|
| 65 | 69 | " ]\n" + |
| 66 | 70 | "}").replace("'", "\""), writer.write().trim()); |
| 67 | 71 | } |
| | 72 | |
| | 73 | /** |
| | 74 | * Unit test for multipolygon |
| | 75 | */ |
| | 76 | @Test |
| | 77 | public void testMultipolygon() throws Exception { |
| | 78 | try (FileInputStream in = new FileInputStream(TestUtils.getTestDataRoot() + "multipolygon.osm")) { |
| | 79 | DataSet ds = OsmReader.parseDataSet(in, null); |
| | 80 | final OsmDataLayer layer = new OsmDataLayer(ds, "foo", null); |
| | 81 | final GeoJSONWriter writer = new GeoJSONWriter(layer, ProjectionPreference.wgs84.getProjection()); |
| | 82 | assertTrue(writer.write().contains("MultiPolygon")); |
| | 83 | } |
| | 84 | } |
| 68 | 85 | } |