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 | } |