Skip to content

Commit f839966

Browse files
authored
Add a spatial function to convert WKT to Geo-JSON (#406)
1 parent 0e38119 commit f839966

File tree

3 files changed

+348
-0
lines changed

3 files changed

+348
-0
lines changed

src/main/java/org/neo4j/gis/spatial/functions/SpatialFunctions.java

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,8 +20,12 @@
2020

2121
package org.neo4j.gis.spatial.functions;
2222

23+
import org.locationtech.jts.geom.Geometry;
24+
import org.locationtech.jts.io.ParseException;
25+
import org.locationtech.jts.io.WKTReader;
2326
import org.neo4j.gis.spatial.Layer;
2427
import org.neo4j.gis.spatial.procedures.SpatialProcedures.GeometryResult;
28+
import org.neo4j.gis.spatial.utilities.GeoJsonUtils;
2529
import org.neo4j.gis.spatial.utilities.SpatialApiBase;
2630
import org.neo4j.graphdb.Node;
2731
import org.neo4j.procedure.Description;
@@ -55,4 +59,14 @@ public Object asGeometry(@Name("geometry") Object geometry) {
5559
}
5660

5761

62+
@UserFunction("spatial.wktToGeoJson")
63+
@Description("Converts a WKT to GeoJson structure")
64+
public Object wktToGeoJson(@Name("wkt") String wkt) throws ParseException {
65+
if (wkt == null) {
66+
return null;
67+
}
68+
WKTReader wktReader = new WKTReader();
69+
Geometry geometry = wktReader.read(wkt);
70+
return GeoJsonUtils.toGeoJsonStructure(geometry);
71+
}
5872
}
Lines changed: 92 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,92 @@
1+
/*
2+
* Copyright (c) "Neo4j"
3+
* Neo4j Sweden AB [http://neo4j.com]
4+
*
5+
* This file is part of Neo4j Spatial.
6+
*
7+
* Neo4j is free software: you can redistribute it and/or modify
8+
* it under the terms of the GNU General Public License as published by
9+
* the Free Software Foundation, either version 3 of the License, or
10+
* (at your option) any later version.
11+
*
12+
* This program is distributed in the hope that it will be useful,
13+
* but WITHOUT ANY WARRANTY; without even the implied warranty of
14+
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
15+
* GNU General Public License for more details.
16+
*
17+
* You should have received a copy of the GNU General Public License
18+
* along with this program. If not, see <http://www.gnu.org/licenses/>.
19+
*/
20+
21+
package org.neo4j.gis.spatial.utilities;
22+
23+
import java.util.Arrays;
24+
import java.util.List;
25+
import java.util.Map;
26+
import java.util.stream.IntStream;
27+
import java.util.stream.Stream;
28+
import org.locationtech.jts.geom.Coordinate;
29+
import org.locationtech.jts.geom.Geometry;
30+
import org.locationtech.jts.geom.GeometryCollection;
31+
import org.locationtech.jts.geom.LineString;
32+
import org.locationtech.jts.geom.Point;
33+
import org.locationtech.jts.geom.Polygon;
34+
35+
36+
public class GeoJsonUtils {
37+
38+
private GeoJsonUtils() {
39+
}
40+
41+
public static Map<String, Object> toGeoJsonStructure(Geometry geometry) {
42+
if (geometry instanceof GeometryCollection geometryCollection && "GeometryCollection".equals(
43+
geometry.getGeometryType())) {
44+
return Map.of(
45+
"type", geometry.getGeometryType(),
46+
"geometries", IntStream.range(0, geometryCollection.getNumGeometries())
47+
.mapToObj(geometryCollection::getGeometryN)
48+
.map(GeoJsonUtils::toGeoJsonStructure)
49+
.toList()
50+
);
51+
}
52+
return Map.of(
53+
"type", geometry.getGeometryType(),
54+
"coordinates", getCoordinates(geometry)
55+
);
56+
}
57+
58+
private static List<?> getCoordinates(Geometry geometry) {
59+
if (geometry instanceof Point point) {
60+
return getPoint(point.getCoordinate());
61+
}
62+
if (geometry instanceof LineString lineString) {
63+
return Arrays.stream(lineString.getCoordinates()).map(GeoJsonUtils::getPoint).toList();
64+
}
65+
if (geometry instanceof Polygon polygon) {
66+
return Stream.concat(
67+
Stream.of(polygon.getExteriorRing()),
68+
IntStream.range(0, polygon.getNumInteriorRing())
69+
.mapToObj(polygon::getInteriorRingN)
70+
)
71+
.map(GeoJsonUtils::getCoordinates)
72+
.toList();
73+
}
74+
if (geometry instanceof GeometryCollection geometryCollection) {
75+
return IntStream.range(0, geometryCollection.getNumGeometries())
76+
.mapToObj(geometryCollection::getGeometryN)
77+
.map(GeoJsonUtils::getCoordinates)
78+
.toList();
79+
}
80+
throw new IllegalArgumentException("Unsupported geometry type: " + geometry.getGeometryType());
81+
}
82+
83+
private static List<Object> getPoint(Coordinate coordinate) {
84+
if (Double.isNaN(coordinate.getZ())) {
85+
return List.of(coordinate.getX(), coordinate.getY());
86+
}
87+
return List.of(
88+
coordinate.getX(),
89+
coordinate.getY(),
90+
coordinate.getZ());
91+
}
92+
}

src/test/java/org/neo4j/gis/spatial/functions/SpatialFunctionsTest.java

Lines changed: 242 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,11 +20,15 @@
2020

2121
package org.neo4j.gis.spatial.functions;
2222

23+
import static org.hamcrest.MatcherAssert.assertThat;
2324
import static org.hamcrest.Matchers.closeTo;
25+
import static org.hamcrest.Matchers.equalTo;
2426
import static org.junit.jupiter.api.Assertions.assertInstanceOf;
2527

28+
import java.util.List;
2629
import java.util.Map;
2730
import org.hamcrest.MatcherAssert;
31+
import org.junit.jupiter.api.Nested;
2832
import org.junit.jupiter.api.Test;
2933
import org.neo4j.exceptions.KernelException;
3034
import org.neo4j.gis.spatial.AbstractApiTest;
@@ -69,4 +73,242 @@ public void literal_geometry_return() {
6973
"WITH spatial.asGeometry({latitude: 5.0, longitude: 4.0}) AS geometry RETURN geometry", "geometry");
7074
assertInstanceOf(Geometry.class, geometry, "Should be Geometry type");
7175
}
76+
77+
/**
78+
* Test for all WKT types
79+
*
80+
* @see <a href="https://en.wikipedia.org/wiki/Well-known_text_representation_of_geometry">Wikipedia WKT</a>
81+
*/
82+
@Nested
83+
class WktToGeoJson {
84+
85+
@Test
86+
public void testPoint() {
87+
String wkt = "POINT (30 10)";
88+
Object json = executeObject("return spatial.wktToGeoJson($wkt) as json", Map.of("wkt", wkt),
89+
"json");
90+
assertThat(json, equalTo(Map.of(
91+
"type", "Point",
92+
"coordinates", List.of(30., 10.)
93+
)));
94+
}
95+
96+
@Test
97+
public void testLineString() {
98+
String wkt = "LINESTRING (30 10, 10 30, 40 40)";
99+
Object json = executeObject("return spatial.wktToGeoJson($wkt) as json", Map.of("wkt", wkt),
100+
"json");
101+
assertThat(json, equalTo(Map.of(
102+
"type", "LineString",
103+
"coordinates", List.of(List.of(30., 10.), List.of(10., 30.), List.of(40., 40.))
104+
)));
105+
}
106+
107+
@Test
108+
public void testPolygon() {
109+
String wkt = "POLYGON ((30 10, 40 40, 20 40, 10 20, 30 10))";
110+
Object json = executeObject("return spatial.wktToGeoJson($wkt) as json", Map.of("wkt", wkt),
111+
"json");
112+
assertThat(json, equalTo(Map.of(
113+
"type", "Polygon",
114+
"coordinates",
115+
List.of( // Polygon
116+
List.of( // LineString
117+
List.of(30., 10.),
118+
List.of(40., 40.),
119+
List.of(20., 40.),
120+
List.of(10., 20.),
121+
List.of(30., 10.)
122+
)
123+
)
124+
)));
125+
}
126+
127+
@Test
128+
public void testPolygonWithHole() {
129+
String wkt = "POLYGON ((35 10, 45 45, 15 40, 10 20, 35 10),\n"
130+
+ "(20 30, 35 35, 30 20, 20 30))";
131+
Object json = executeObject("return spatial.wktToGeoJson($wkt) as json", Map.of("wkt", wkt),
132+
"json");
133+
assertThat(json, equalTo(Map.of(
134+
"type", "Polygon",
135+
"coordinates",
136+
List.of( // Polygon
137+
List.of( // LineString
138+
List.of(35., 10.),
139+
List.of(45., 45.),
140+
List.of(15., 40.),
141+
List.of(10., 20.),
142+
List.of(35., 10.)
143+
),
144+
List.of( // hole
145+
List.of(20., 30.),
146+
List.of(35., 35.),
147+
List.of(30., 20.),
148+
List.of(20., 30.)
149+
)
150+
)
151+
)));
152+
}
153+
154+
@Test
155+
public void testMultiPoint() {
156+
String wkt = "MULTIPOINT ((10 40), (40 30), (20 20), (30 10))";
157+
Object json = executeObject("return spatial.wktToGeoJson($wkt) as json", Map.of("wkt", wkt),
158+
"json");
159+
assertThat(json, equalTo(Map.of(
160+
"type", "MultiPoint",
161+
"coordinates", List.of(
162+
List.of(10., 40.),
163+
List.of(40., 30.),
164+
List.of(20., 20.),
165+
List.of(30., 10.)
166+
))));
167+
}
168+
169+
@Test
170+
public void testMultiPoint2() {
171+
String wkt = "MULTIPOINT (10 40, 40 30, 20 20, 30 10)";
172+
Object json = executeObject("return spatial.wktToGeoJson($wkt) as json", Map.of("wkt", wkt),
173+
"json");
174+
assertThat(json, equalTo(Map.of(
175+
"type", "MultiPoint",
176+
"coordinates", List.of(
177+
List.of(10., 40.),
178+
List.of(40., 30.),
179+
List.of(20., 20.),
180+
List.of(30., 10.)
181+
))));
182+
}
183+
184+
@Test
185+
public void testMultiLineString() {
186+
String wkt = "MULTILINESTRING ((10 10, 20 20, 10 40),\n"
187+
+ "(40 40, 30 30, 40 20, 30 10))";
188+
Object json = executeObject("return spatial.wktToGeoJson($wkt) as json", Map.of("wkt", wkt),
189+
"json");
190+
assertThat(json, equalTo(Map.of(
191+
"type", "MultiLineString",
192+
"coordinates", List.of(
193+
List.of( // LineString
194+
List.of(10., 10.),
195+
List.of(20., 20.),
196+
List.of(10., 40.)
197+
),
198+
List.of( // LineString
199+
List.of(40., 40.),
200+
List.of(30., 30.),
201+
List.of(40., 20.),
202+
List.of(30., 10.)
203+
)
204+
))));
205+
}
206+
207+
@Test
208+
public void testMultiPolygon() {
209+
String wkt = "MULTIPOLYGON (((30 20, 45 40, 10 40, 30 20)),\n"
210+
+ "((15 5, 40 10, 10 20, 5 10, 15 5)))";
211+
Object json = executeObject("return spatial.wktToGeoJson($wkt) as json", Map.of("wkt", wkt),
212+
"json");
213+
assertThat(json, equalTo(Map.of(
214+
"type", "MultiPolygon",
215+
"coordinates", List.of( // MultiPolygon
216+
List.of( // Polygon
217+
List.of( // LineString
218+
List.of(30., 20.),
219+
List.of(45., 40.),
220+
List.of(10., 40.),
221+
List.of(30., 20.)
222+
)
223+
),
224+
List.of( // Polygon
225+
List.of( // LineString
226+
List.of(15., 5.),
227+
List.of(40., 10.),
228+
List.of(10., 20.),
229+
List.of(5., 10.),
230+
List.of(15., 5.)
231+
)
232+
)
233+
)
234+
)));
235+
}
236+
237+
@Test
238+
public void testMultiPolygon2() {
239+
String wkt = """
240+
MULTIPOLYGON (((40 40, 20 45, 45 30, 40 40)),
241+
((20 35, 10 30, 10 10, 30 5, 45 20, 20 35),
242+
(30 20, 20 15, 20 25, 30 20)))""";
243+
Object json = executeObject("return spatial.wktToGeoJson($wkt) as json", Map.of("wkt", wkt),
244+
"json");
245+
assertThat(json, equalTo(Map.of(
246+
"type", "MultiPolygon",
247+
"coordinates", List.of( // MultiPolygon
248+
List.of( // Polygon
249+
List.of( // LineString
250+
List.of(40., 40.),
251+
List.of(20., 45.),
252+
List.of(45., 30.),
253+
List.of(40., 40.)
254+
)
255+
),
256+
List.of( // Polygon
257+
List.of( // LineString
258+
List.of(20., 35.),
259+
List.of(10., 30.),
260+
List.of(10., 10.),
261+
List.of(30., 5.),
262+
List.of(45., 20.),
263+
List.of(20., 35.)
264+
),
265+
List.of( // hole
266+
List.of(30., 20.),
267+
List.of(20., 15.),
268+
List.of(20., 25.),
269+
List.of(30., 20.)
270+
)
271+
)
272+
)
273+
)));
274+
}
275+
276+
@Test
277+
public void testGeometryCollection() {
278+
String wkt = """
279+
GEOMETRYCOLLECTION (POINT (40 10),
280+
LINESTRING (10 10, 20 20, 10 40),
281+
POLYGON ((40 40, 20 45, 45 30, 40 40)))""";
282+
Object json = executeObject("return spatial.wktToGeoJson($wkt) as json", Map.of("wkt", wkt),
283+
"json");
284+
assertThat(json, equalTo(Map.of(
285+
"type", "GeometryCollection",
286+
"geometries", List.of(
287+
Map.of( // Point
288+
"type", "Point",
289+
"coordinates", List.of(40., 10.)
290+
),
291+
Map.of( // LineString
292+
"type", "LineString",
293+
"coordinates", List.of(
294+
List.of(10., 10.),
295+
List.of(20., 20.),
296+
List.of(10., 40.)
297+
)
298+
),
299+
Map.of( // Polygon
300+
"type", "Polygon",
301+
"coordinates", List.of(
302+
List.of( // LineString
303+
List.of(40., 40.),
304+
List.of(20., 45.),
305+
List.of(45., 30.),
306+
List.of(40., 40.)
307+
)
308+
)
309+
)
310+
)
311+
)));
312+
}
313+
}
72314
}

0 commit comments

Comments
 (0)