Skip to content

Commit a1cc0e5

Browse files
authored
Add support for native point array properties (#404)
1 parent f839966 commit a1cc0e5

File tree

9 files changed

+177
-44
lines changed

9 files changed

+177
-44
lines changed

src/main/java/org/neo4j/gis/spatial/AbstractGeometryEncoder.java

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@
2121

2222
import org.apache.commons.lang3.ArrayUtils;
2323
import org.locationtech.jts.geom.Geometry;
24+
import org.locationtech.jts.geom.GeometryFactory;
2425
import org.neo4j.gis.spatial.rtree.Envelope;
2526
import org.neo4j.graphdb.Entity;
2627
import org.neo4j.graphdb.Node;
@@ -30,6 +31,15 @@ public abstract class AbstractGeometryEncoder implements GeometryEncoder, Consta
3031

3132
protected String bboxProperty = PROP_BBOX;
3233

34+
private GeometryFactory geometryFactory;
35+
36+
protected GeometryFactory getGeometryFactory() {
37+
if (geometryFactory == null) {
38+
geometryFactory = new GeometryFactory();
39+
}
40+
return geometryFactory;
41+
}
42+
3343
// Public methods
3444

3545
@Override

src/main/java/org/neo4j/gis/spatial/SpatialDatabaseService.java

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -35,6 +35,7 @@
3535
import org.locationtech.jts.geom.Polygon;
3636
import org.neo4j.gis.spatial.encoders.Configurable;
3737
import org.neo4j.gis.spatial.encoders.NativePointEncoder;
38+
import org.neo4j.gis.spatial.encoders.NativePointsEncoder;
3839
import org.neo4j.gis.spatial.encoders.SimplePointEncoder;
3940
import org.neo4j.gis.spatial.index.IndexManager;
4041
import org.neo4j.gis.spatial.index.LayerGeohashPointIndex;
@@ -526,6 +527,8 @@ String getSignature() {
526527
"longitude:latitude"));
527528
addRegisteredLayerType(new RegisteredLayerType("NativePoint", NativePointEncoder.class,
528529
SimplePointLayer.class, DefaultGeographicCRS.WGS84, LayerRTreeIndex.class, "location"));
530+
addRegisteredLayerType(new RegisteredLayerType("NativePoints", NativePointsEncoder.class,
531+
EditableLayerImpl.class, DefaultGeographicCRS.WGS84, LayerRTreeIndex.class, "geometry"));
529532
addRegisteredLayerType(new RegisteredLayerType("NativeGeohash", NativePointEncoder.class,
530533
SimplePointLayer.class, DefaultGeographicCRS.WGS84, LayerGeohashPointIndex.class, "location"));
531534
addRegisteredLayerType(new RegisteredLayerType("NativeZOrder", NativePointEncoder.class,

src/main/java/org/neo4j/gis/spatial/encoders/NativePointEncoder.java

Lines changed: 1 addition & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -21,7 +21,6 @@
2121

2222
import org.locationtech.jts.geom.Coordinate;
2323
import org.locationtech.jts.geom.Geometry;
24-
import org.locationtech.jts.geom.GeometryFactory;
2524
import org.locationtech.jts.geom.Point;
2625
import org.neo4j.gis.spatial.AbstractGeometryEncoder;
2726
import org.neo4j.gis.spatial.SpatialDatabaseService;
@@ -36,22 +35,14 @@
3635
public class NativePointEncoder extends AbstractGeometryEncoder implements Configurable {
3736

3837
private static final String DEFAULT_GEOM = "location";
39-
private static GeometryFactory geometryFactory;
4038
private String locationProperty = DEFAULT_GEOM;
4139
private Neo4jCRS crs = Neo4jCRS.findCRS("WGS-84");
4240

43-
protected static GeometryFactory getGeometryFactory() {
44-
if (geometryFactory == null) {
45-
geometryFactory = new GeometryFactory();
46-
}
47-
return geometryFactory;
48-
}
49-
5041
@Override
5142
protected void encodeGeometryShape(Transaction tx, Geometry geometry, Entity container) {
5243
int gtype = SpatialDatabaseService.convertJtsClassToGeometryType(geometry.getClass());
5344
if (gtype == GTYPE_POINT) {
54-
container.setProperty("gtype", gtype);
45+
container.setProperty(PROP_TYPE, gtype);
5546
Neo4jPoint neo4jPoint = new Neo4jPoint((Point) geometry, crs);
5647
container.setProperty(locationProperty, neo4jPoint);
5748
} else {
Lines changed: 106 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,106 @@
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+
package org.neo4j.gis.spatial.encoders;
21+
22+
import java.util.Arrays;
23+
import org.locationtech.jts.geom.Coordinate;
24+
import org.locationtech.jts.geom.Geometry;
25+
import org.locationtech.jts.geom.LineString;
26+
import org.neo4j.gis.spatial.AbstractGeometryEncoder;
27+
import org.neo4j.gis.spatial.SpatialDatabaseService;
28+
import org.neo4j.gis.spatial.encoders.neo4j.Neo4jCRS;
29+
import org.neo4j.gis.spatial.encoders.neo4j.Neo4jPoint;
30+
import org.neo4j.graphdb.Entity;
31+
import org.neo4j.graphdb.Transaction;
32+
33+
/**
34+
* Simple encoder that stores line strings as an array of points.
35+
*/
36+
public class NativePointsEncoder extends AbstractGeometryEncoder implements Configurable {
37+
38+
private String property = "geometry";
39+
private Neo4jCRS crs = Neo4jCRS.findCRS("WGS-84");
40+
41+
@Override
42+
protected void encodeGeometryShape(Transaction tx, Geometry geometry, Entity container) {
43+
var gtype = SpatialDatabaseService.convertJtsClassToGeometryType(geometry.getClass());
44+
if (geometry instanceof LineString lineString) {
45+
var neo4jPoints = Arrays.stream(lineString.getCoordinates())
46+
.map(coordinate -> new Neo4jPoint(coordinate, crs))
47+
.toArray(org.neo4j.graphdb.spatial.Point[]::new);
48+
container.setProperty(PROP_TYPE, gtype);
49+
container.setProperty(property, neo4jPoints);
50+
} else {
51+
throw new IllegalArgumentException(
52+
"Can only store point-arrays as linestring: " + SpatialDatabaseService.convertGeometryTypeToName(
53+
gtype));
54+
}
55+
56+
}
57+
58+
@Override
59+
public Geometry decodeGeometry(Entity container) {
60+
var points = ((org.neo4j.graphdb.spatial.Point[]) container.getProperty(property));
61+
var factory = getGeometryFactory();
62+
var coordinates = Arrays.stream(points)
63+
.map(point -> {
64+
if (point.getCRS().getCode() != crs.getCode()) {
65+
throw new IllegalStateException(
66+
"Trying to decode geometry with wrong CRS: layer configured to crs=" + crs.getCode()
67+
+ ", but geometry has crs=" + point.getCRS().getCode());
68+
}
69+
double[] coordinate = point.getCoordinate().getCoordinate();
70+
if (crs.dimensions() == 3) {
71+
return new Coordinate(coordinate[0], coordinate[1], coordinate[2]);
72+
} else {
73+
return new Coordinate(coordinate[0], coordinate[1]);
74+
}
75+
})
76+
.toArray(Coordinate[]::new);
77+
return factory.createLineString(coordinates);
78+
}
79+
80+
@Override
81+
public String getConfiguration() {
82+
return property + ":" + bboxProperty + ": " + crs.getCode();
83+
}
84+
85+
@Override
86+
public void setConfiguration(String configuration) {
87+
if (configuration != null && !configuration.trim().isEmpty()) {
88+
String[] fields = configuration.split(":");
89+
if (fields.length > 0) {
90+
property = fields[0];
91+
}
92+
if (fields.length > 1) {
93+
bboxProperty = fields[1];
94+
}
95+
if (fields.length > 2) {
96+
crs = Neo4jCRS.findCRS(fields[2]);
97+
}
98+
}
99+
}
100+
101+
@Override
102+
public String getSignature() {
103+
return "NativePointEncoder(geometry='" + property + "', bbox='" + bboxProperty + "', crs=" + crs.getCode()
104+
+ ")";
105+
}
106+
}

src/main/java/org/neo4j/gis/spatial/encoders/SimpleGraphEncoder.java

Lines changed: 1 addition & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -22,7 +22,6 @@
2222
import org.locationtech.jts.geom.Coordinate;
2323
import org.locationtech.jts.geom.CoordinateList;
2424
import org.locationtech.jts.geom.Geometry;
25-
import org.locationtech.jts.geom.GeometryFactory;
2625
import org.neo4j.gis.spatial.AbstractGeometryEncoder;
2726
import org.neo4j.gis.spatial.SpatialDatabaseException;
2827
import org.neo4j.graphdb.Direction;
@@ -41,19 +40,10 @@
4140
// TODO: Consider generalizing this code and making a general linked list geometry store available in the library
4241
public class SimpleGraphEncoder extends AbstractGeometryEncoder {
4342

44-
private GeometryFactory geometryFactory;
45-
4643
protected enum SimpleRelationshipTypes implements RelationshipType {
4744
FIRST, NEXT
4845
}
4946

50-
private GeometryFactory getGeometryFactory() {
51-
if (geometryFactory == null) {
52-
geometryFactory = new GeometryFactory();
53-
}
54-
return geometryFactory;
55-
}
56-
5747
private static Node testIsNode(Entity container) {
5848
if (!(container instanceof Node)) {
5949
throw new SpatialDatabaseException("Cannot decode non-node geometry: " + container);
@@ -64,7 +54,7 @@ private static Node testIsNode(Entity container) {
6454
@Override
6555
protected void encodeGeometryShape(Transaction tx, Geometry geometry, Entity container) {
6656
Node node = testIsNode(container);
67-
node.setProperty("gtype", GTYPE_LINESTRING);
57+
node.setProperty(PROP_TYPE, GTYPE_LINESTRING);
6858
Node prev = null;
6959
for (Coordinate coord : geometry.getCoordinates()) {
7060
Node point = tx.createNode();

src/main/java/org/neo4j/gis/spatial/encoders/SimplePointEncoder.java

Lines changed: 1 addition & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -21,7 +21,6 @@
2121

2222
import org.locationtech.jts.geom.Coordinate;
2323
import org.locationtech.jts.geom.Geometry;
24-
import org.locationtech.jts.geom.GeometryFactory;
2524
import org.neo4j.gis.spatial.AbstractGeometryEncoder;
2625
import org.neo4j.gis.spatial.SpatialDatabaseService;
2726
import org.neo4j.graphdb.Entity;
@@ -34,21 +33,13 @@ public class SimplePointEncoder extends AbstractGeometryEncoder implements Confi
3433

3534
public static final String DEFAULT_X = "longitude";
3635
public static final String DEFAULT_Y = "latitude";
37-
protected GeometryFactory geometryFactory;
3836
protected String xProperty = DEFAULT_X;
3937
protected String yProperty = DEFAULT_Y;
4038

41-
protected GeometryFactory getGeometryFactory() {
42-
if (geometryFactory == null) {
43-
geometryFactory = new GeometryFactory();
44-
}
45-
return geometryFactory;
46-
}
47-
4839
@Override
4940
protected void encodeGeometryShape(Transaction tx, Geometry geometry, Entity container) {
5041
container.setProperty(
51-
"gtype",
42+
PROP_TYPE,
5243
SpatialDatabaseService.convertJtsClassToGeometryType(geometry.getClass()));
5344
Coordinate[] coords = geometry.getCoordinates();
5445
container.setProperty(xProperty, coords[0].x);

src/main/java/org/neo4j/gis/spatial/encoders/SimplePropertyEncoder.java

Lines changed: 1 addition & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -21,7 +21,6 @@
2121

2222
import org.locationtech.jts.geom.Coordinate;
2323
import org.locationtech.jts.geom.Geometry;
24-
import org.locationtech.jts.geom.GeometryFactory;
2524
import org.neo4j.gis.spatial.AbstractGeometryEncoder;
2625
import org.neo4j.gis.spatial.SpatialDatabaseService;
2726
import org.neo4j.graphdb.Entity;
@@ -35,18 +34,9 @@
3534
// TODO: Consider switching from Float to Double according to Davide Savazzi
3635
public class SimplePropertyEncoder extends AbstractGeometryEncoder {
3736

38-
protected GeometryFactory geometryFactory;
39-
40-
protected GeometryFactory getGeometryFactory() {
41-
if (geometryFactory == null) {
42-
geometryFactory = new GeometryFactory();
43-
}
44-
return geometryFactory;
45-
}
46-
4737
@Override
4838
protected void encodeGeometryShape(Transaction tx, Geometry geometry, Entity container) {
49-
container.setProperty("gtype", SpatialDatabaseService.convertJtsClassToGeometryType(geometry.getClass()));
39+
container.setProperty(PROP_TYPE, SpatialDatabaseService.convertJtsClassToGeometryType(geometry.getClass()));
5040
Coordinate[] coords = geometry.getCoordinates();
5141
float[] data = new float[coords.length * 2];
5242
for (int i = 0; i < coords.length; i++) {

src/main/java/org/neo4j/gis/spatial/osm/OSMImporter.java

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1123,7 +1123,7 @@ protected void addNodeGeometry(WrappedNode node, int gtype, Envelope bbox, int v
11231123
gtype = vertices > 1 ? GTYPE_MULTIPOINT : GTYPE_POINT;
11241124
}
11251125
Node geomNode = tx.createNode();
1126-
geomNode.setProperty("gtype", gtype);
1126+
geomNode.setProperty(PROP_TYPE, gtype);
11271127
geomNode.setProperty("vertices", vertices);
11281128
geomNode.setProperty(PROP_BBOX,
11291129
new double[]{bbox.getMinX(), bbox.getMaxX(), bbox.getMinY(), bbox.getMaxY()});
@@ -1207,7 +1207,7 @@ protected void updateGeometryMetaDataFromMember(WrappedNode member, GeometryMeta
12071207
try (var relationships = member.getRelationships(OSMRelation.GEOM)) {
12081208
for (Relationship rel : relationships) {
12091209
nodeProps = getNodeProperties(WrappedNode.fromNode(rel.getEndNode()));
1210-
metaGeom.checkSupportedGeometry((Integer) nodeProps.get("gtype"));
1210+
metaGeom.checkSupportedGeometry((Integer) nodeProps.get(PROP_TYPE));
12111211
metaGeom.expandToIncludeBBox(nodeProps);
12121212
}
12131213
}

src/test/java/org/neo4j/gis/spatial/procedures/SpatialProceduresTest.java

Lines changed: 52 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,7 @@
2929
import static org.junit.jupiter.api.Assertions.assertEquals;
3030
import static org.junit.jupiter.api.Assertions.assertFalse;
3131
import static org.junit.jupiter.api.Assertions.assertInstanceOf;
32+
import static org.junit.jupiter.api.Assertions.assertThrows;
3233
import static org.junit.jupiter.api.Assertions.assertTrue;
3334
import static org.junit.jupiter.api.Assertions.fail;
3435
import static org.neo4j.gis.spatial.Constants.LABEL_LAYER;
@@ -54,6 +55,7 @@
5455
import org.neo4j.gis.spatial.utilities.ReferenceNodes;
5556
import org.neo4j.graphdb.GraphDatabaseService;
5657
import org.neo4j.graphdb.Node;
58+
import org.neo4j.graphdb.QueryExecutionException;
5759
import org.neo4j.graphdb.Result;
5860
import org.neo4j.graphdb.Transaction;
5961
import org.neo4j.graphdb.spatial.Geometry;
@@ -1244,6 +1246,56 @@ public void find_no_geometries_using_closest_on_empty_layer() {
12441246
testCallCount(db, "CALL spatial.closest('geom',{lon:15.2, lat:60.1}, 1.0)", null, 0);
12451247
}
12461248

1249+
@Test
1250+
public void testNativePoints() {
1251+
execute("CREATE (node:Foo { points: [point({latitude: 5.0, longitude: 4.0}), point({latitude: 6.0, longitude: 5.0})]})");
1252+
execute("CALL spatial.addLayer('line','NativePoints','points') YIELD node" +
1253+
" MATCH (n:Foo)" +
1254+
" WITH collect(n) AS nodes" +
1255+
" CALL spatial.addNodes('line', nodes) YIELD count RETURN count");
1256+
testCallCount(db, "CALL spatial.closest('line',{lon:5.1, lat:4.1}, 1.0)", null, 1);
1257+
}
1258+
1259+
@Test
1260+
public void testNativePoints3D() {
1261+
execute("CREATE (node:Foo { points: [point({latitude: 5.0, longitude: 4.0, height: 1.0}), point({latitude: 6.0, longitude: 5.0, height: 2.0})]})");
1262+
Exception exception = assertThrows(QueryExecutionException.class, () -> {
1263+
execute("CALL spatial.addLayer('line','NativePoints','points:bbox:Cartesian') YIELD node" +
1264+
" MATCH (n:Foo)" +
1265+
" WITH collect(n) AS nodes" +
1266+
" CALL spatial.addNodes('line', nodes) YIELD count RETURN count");
1267+
});
1268+
1269+
assertEquals(
1270+
"Failed to invoke procedure `spatial.addNodes`: Caused by: java.lang.IllegalStateException: Trying to decode geometry with wrong CRS: layer configured to crs=7203, but geometry has crs=4979",
1271+
exception.getMessage());
1272+
}
1273+
1274+
@Test
1275+
public void testNativePointsCartesian() {
1276+
execute("CREATE (node:Foo { points: [point({x: 5.0, y: 4.0}), point({x: 6.0, y: 5.0})]})");
1277+
execute("CALL spatial.addLayer('line','NativePoints','points:bbox:Cartesian') YIELD node" +
1278+
" MATCH (n:Foo)" +
1279+
" WITH collect(n) AS nodes" +
1280+
" CALL spatial.addNodes('line', nodes) YIELD count RETURN count");
1281+
testCallCount(db, "CALL spatial.closest('line',point({x:5.1, y:4.1}), 1.0)", null, 1);
1282+
}
1283+
1284+
@Test
1285+
public void testNativePointsCartesian3D() {
1286+
execute("CREATE (node:Foo { points: [point({x: 5.0, y: 4.0, z: 1}), point({x: 6.0, y: 5.0, z: 2})]})");
1287+
Exception exception = assertThrows(QueryExecutionException.class, () -> {
1288+
execute("CALL spatial.addLayer('line','NativePoints','points:bbox:Cartesian') YIELD node" +
1289+
" MATCH (n:Foo)" +
1290+
" WITH collect(n) AS nodes" +
1291+
" CALL spatial.addNodes('line', nodes) YIELD count RETURN count");
1292+
});
1293+
1294+
assertEquals(
1295+
"Failed to invoke procedure `spatial.addNodes`: Caused by: java.lang.IllegalStateException: Trying to decode geometry with wrong CRS: layer configured to crs=7203, but geometry has crs=9157",
1296+
exception.getMessage());
1297+
}
1298+
12471299
/*
12481300
12491301
@Test

0 commit comments

Comments
 (0)