Skip to content

Commit

Permalink
Add a spatial function to convert WKT to Geo-JSON (#406)
Browse files Browse the repository at this point in the history
  • Loading branch information
Andy2003 authored Feb 17, 2025
1 parent 0e38119 commit f839966
Show file tree
Hide file tree
Showing 3 changed files with 348 additions and 0 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -20,8 +20,12 @@

package org.neo4j.gis.spatial.functions;

import org.locationtech.jts.geom.Geometry;
import org.locationtech.jts.io.ParseException;
import org.locationtech.jts.io.WKTReader;
import org.neo4j.gis.spatial.Layer;
import org.neo4j.gis.spatial.procedures.SpatialProcedures.GeometryResult;
import org.neo4j.gis.spatial.utilities.GeoJsonUtils;
import org.neo4j.gis.spatial.utilities.SpatialApiBase;
import org.neo4j.graphdb.Node;
import org.neo4j.procedure.Description;
Expand Down Expand Up @@ -55,4 +59,14 @@ public Object asGeometry(@Name("geometry") Object geometry) {
}


@UserFunction("spatial.wktToGeoJson")
@Description("Converts a WKT to GeoJson structure")
public Object wktToGeoJson(@Name("wkt") String wkt) throws ParseException {
if (wkt == null) {
return null;
}
WKTReader wktReader = new WKTReader();
Geometry geometry = wktReader.read(wkt);
return GeoJsonUtils.toGeoJsonStructure(geometry);
}
}
92 changes: 92 additions & 0 deletions src/main/java/org/neo4j/gis/spatial/utilities/GeoJsonUtils.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,92 @@
/*
* Copyright (c) "Neo4j"
* Neo4j Sweden AB [http://neo4j.com]
*
* This file is part of Neo4j Spatial.
*
* Neo4j is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/

package org.neo4j.gis.spatial.utilities;

import java.util.Arrays;
import java.util.List;
import java.util.Map;
import java.util.stream.IntStream;
import java.util.stream.Stream;
import org.locationtech.jts.geom.Coordinate;
import org.locationtech.jts.geom.Geometry;
import org.locationtech.jts.geom.GeometryCollection;
import org.locationtech.jts.geom.LineString;
import org.locationtech.jts.geom.Point;
import org.locationtech.jts.geom.Polygon;


public class GeoJsonUtils {

private GeoJsonUtils() {
}

public static Map<String, Object> toGeoJsonStructure(Geometry geometry) {
if (geometry instanceof GeometryCollection geometryCollection && "GeometryCollection".equals(
geometry.getGeometryType())) {
return Map.of(
"type", geometry.getGeometryType(),
"geometries", IntStream.range(0, geometryCollection.getNumGeometries())
.mapToObj(geometryCollection::getGeometryN)
.map(GeoJsonUtils::toGeoJsonStructure)
.toList()
);
}
return Map.of(
"type", geometry.getGeometryType(),
"coordinates", getCoordinates(geometry)
);
}

private static List<?> getCoordinates(Geometry geometry) {
if (geometry instanceof Point point) {
return getPoint(point.getCoordinate());
}
if (geometry instanceof LineString lineString) {
return Arrays.stream(lineString.getCoordinates()).map(GeoJsonUtils::getPoint).toList();
}
if (geometry instanceof Polygon polygon) {
return Stream.concat(
Stream.of(polygon.getExteriorRing()),
IntStream.range(0, polygon.getNumInteriorRing())
.mapToObj(polygon::getInteriorRingN)
)
.map(GeoJsonUtils::getCoordinates)
.toList();
}
if (geometry instanceof GeometryCollection geometryCollection) {
return IntStream.range(0, geometryCollection.getNumGeometries())
.mapToObj(geometryCollection::getGeometryN)
.map(GeoJsonUtils::getCoordinates)
.toList();
}
throw new IllegalArgumentException("Unsupported geometry type: " + geometry.getGeometryType());
}

private static List<Object> getPoint(Coordinate coordinate) {
if (Double.isNaN(coordinate.getZ())) {
return List.of(coordinate.getX(), coordinate.getY());
}
return List.of(
coordinate.getX(),
coordinate.getY(),
coordinate.getZ());
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -20,11 +20,15 @@

package org.neo4j.gis.spatial.functions;

import static org.hamcrest.MatcherAssert.assertThat;
import static org.hamcrest.Matchers.closeTo;
import static org.hamcrest.Matchers.equalTo;
import static org.junit.jupiter.api.Assertions.assertInstanceOf;

import java.util.List;
import java.util.Map;
import org.hamcrest.MatcherAssert;
import org.junit.jupiter.api.Nested;
import org.junit.jupiter.api.Test;
import org.neo4j.exceptions.KernelException;
import org.neo4j.gis.spatial.AbstractApiTest;
Expand Down Expand Up @@ -69,4 +73,242 @@ public void literal_geometry_return() {
"WITH spatial.asGeometry({latitude: 5.0, longitude: 4.0}) AS geometry RETURN geometry", "geometry");
assertInstanceOf(Geometry.class, geometry, "Should be Geometry type");
}

/**
* Test for all WKT types
*
* @see <a href="https://en.wikipedia.org/wiki/Well-known_text_representation_of_geometry">Wikipedia WKT</a>
*/
@Nested
class WktToGeoJson {

@Test
public void testPoint() {
String wkt = "POINT (30 10)";
Object json = executeObject("return spatial.wktToGeoJson($wkt) as json", Map.of("wkt", wkt),
"json");
assertThat(json, equalTo(Map.of(
"type", "Point",
"coordinates", List.of(30., 10.)
)));
}

@Test
public void testLineString() {
String wkt = "LINESTRING (30 10, 10 30, 40 40)";
Object json = executeObject("return spatial.wktToGeoJson($wkt) as json", Map.of("wkt", wkt),
"json");
assertThat(json, equalTo(Map.of(
"type", "LineString",
"coordinates", List.of(List.of(30., 10.), List.of(10., 30.), List.of(40., 40.))
)));
}

@Test
public void testPolygon() {
String wkt = "POLYGON ((30 10, 40 40, 20 40, 10 20, 30 10))";
Object json = executeObject("return spatial.wktToGeoJson($wkt) as json", Map.of("wkt", wkt),
"json");
assertThat(json, equalTo(Map.of(
"type", "Polygon",
"coordinates",
List.of( // Polygon
List.of( // LineString
List.of(30., 10.),
List.of(40., 40.),
List.of(20., 40.),
List.of(10., 20.),
List.of(30., 10.)
)
)
)));
}

@Test
public void testPolygonWithHole() {
String wkt = "POLYGON ((35 10, 45 45, 15 40, 10 20, 35 10),\n"
+ "(20 30, 35 35, 30 20, 20 30))";
Object json = executeObject("return spatial.wktToGeoJson($wkt) as json", Map.of("wkt", wkt),
"json");
assertThat(json, equalTo(Map.of(
"type", "Polygon",
"coordinates",
List.of( // Polygon
List.of( // LineString
List.of(35., 10.),
List.of(45., 45.),
List.of(15., 40.),
List.of(10., 20.),
List.of(35., 10.)
),
List.of( // hole
List.of(20., 30.),
List.of(35., 35.),
List.of(30., 20.),
List.of(20., 30.)
)
)
)));
}

@Test
public void testMultiPoint() {
String wkt = "MULTIPOINT ((10 40), (40 30), (20 20), (30 10))";
Object json = executeObject("return spatial.wktToGeoJson($wkt) as json", Map.of("wkt", wkt),
"json");
assertThat(json, equalTo(Map.of(
"type", "MultiPoint",
"coordinates", List.of(
List.of(10., 40.),
List.of(40., 30.),
List.of(20., 20.),
List.of(30., 10.)
))));
}

@Test
public void testMultiPoint2() {
String wkt = "MULTIPOINT (10 40, 40 30, 20 20, 30 10)";
Object json = executeObject("return spatial.wktToGeoJson($wkt) as json", Map.of("wkt", wkt),
"json");
assertThat(json, equalTo(Map.of(
"type", "MultiPoint",
"coordinates", List.of(
List.of(10., 40.),
List.of(40., 30.),
List.of(20., 20.),
List.of(30., 10.)
))));
}

@Test
public void testMultiLineString() {
String wkt = "MULTILINESTRING ((10 10, 20 20, 10 40),\n"
+ "(40 40, 30 30, 40 20, 30 10))";
Object json = executeObject("return spatial.wktToGeoJson($wkt) as json", Map.of("wkt", wkt),
"json");
assertThat(json, equalTo(Map.of(
"type", "MultiLineString",
"coordinates", List.of(
List.of( // LineString
List.of(10., 10.),
List.of(20., 20.),
List.of(10., 40.)
),
List.of( // LineString
List.of(40., 40.),
List.of(30., 30.),
List.of(40., 20.),
List.of(30., 10.)
)
))));
}

@Test
public void testMultiPolygon() {
String wkt = "MULTIPOLYGON (((30 20, 45 40, 10 40, 30 20)),\n"
+ "((15 5, 40 10, 10 20, 5 10, 15 5)))";
Object json = executeObject("return spatial.wktToGeoJson($wkt) as json", Map.of("wkt", wkt),
"json");
assertThat(json, equalTo(Map.of(
"type", "MultiPolygon",
"coordinates", List.of( // MultiPolygon
List.of( // Polygon
List.of( // LineString
List.of(30., 20.),
List.of(45., 40.),
List.of(10., 40.),
List.of(30., 20.)
)
),
List.of( // Polygon
List.of( // LineString
List.of(15., 5.),
List.of(40., 10.),
List.of(10., 20.),
List.of(5., 10.),
List.of(15., 5.)
)
)
)
)));
}

@Test
public void testMultiPolygon2() {
String wkt = """
MULTIPOLYGON (((40 40, 20 45, 45 30, 40 40)),
((20 35, 10 30, 10 10, 30 5, 45 20, 20 35),
(30 20, 20 15, 20 25, 30 20)))""";
Object json = executeObject("return spatial.wktToGeoJson($wkt) as json", Map.of("wkt", wkt),
"json");
assertThat(json, equalTo(Map.of(
"type", "MultiPolygon",
"coordinates", List.of( // MultiPolygon
List.of( // Polygon
List.of( // LineString
List.of(40., 40.),
List.of(20., 45.),
List.of(45., 30.),
List.of(40., 40.)
)
),
List.of( // Polygon
List.of( // LineString
List.of(20., 35.),
List.of(10., 30.),
List.of(10., 10.),
List.of(30., 5.),
List.of(45., 20.),
List.of(20., 35.)
),
List.of( // hole
List.of(30., 20.),
List.of(20., 15.),
List.of(20., 25.),
List.of(30., 20.)
)
)
)
)));
}

@Test
public void testGeometryCollection() {
String wkt = """
GEOMETRYCOLLECTION (POINT (40 10),
LINESTRING (10 10, 20 20, 10 40),
POLYGON ((40 40, 20 45, 45 30, 40 40)))""";
Object json = executeObject("return spatial.wktToGeoJson($wkt) as json", Map.of("wkt", wkt),
"json");
assertThat(json, equalTo(Map.of(
"type", "GeometryCollection",
"geometries", List.of(
Map.of( // Point
"type", "Point",
"coordinates", List.of(40., 10.)
),
Map.of( // LineString
"type", "LineString",
"coordinates", List.of(
List.of(10., 10.),
List.of(20., 20.),
List.of(10., 40.)
)
),
Map.of( // Polygon
"type", "Polygon",
"coordinates", List.of(
List.of( // LineString
List.of(40., 40.),
List.of(20., 45.),
List.of(45., 30.),
List.of(40., 40.)
)
)
)
)
)));
}
}
}

0 comments on commit f839966

Please sign in to comment.