diff --git a/src/main/java/org/neo4j/gis/spatial/AbstractGeometryEncoder.java b/src/main/java/org/neo4j/gis/spatial/AbstractGeometryEncoder.java index 8db100eb..50f858e1 100644 --- a/src/main/java/org/neo4j/gis/spatial/AbstractGeometryEncoder.java +++ b/src/main/java/org/neo4j/gis/spatial/AbstractGeometryEncoder.java @@ -21,6 +21,7 @@ import org.apache.commons.lang3.ArrayUtils; import org.locationtech.jts.geom.Geometry; +import org.locationtech.jts.geom.GeometryFactory; import org.neo4j.gis.spatial.rtree.Envelope; import org.neo4j.graphdb.Entity; import org.neo4j.graphdb.Node; @@ -30,6 +31,15 @@ public abstract class AbstractGeometryEncoder implements GeometryEncoder, Consta protected String bboxProperty = PROP_BBOX; + private GeometryFactory geometryFactory; + + protected GeometryFactory getGeometryFactory() { + if (geometryFactory == null) { + geometryFactory = new GeometryFactory(); + } + return geometryFactory; + } + // Public methods @Override diff --git a/src/main/java/org/neo4j/gis/spatial/SpatialDatabaseService.java b/src/main/java/org/neo4j/gis/spatial/SpatialDatabaseService.java index 61433229..10680fef 100644 --- a/src/main/java/org/neo4j/gis/spatial/SpatialDatabaseService.java +++ b/src/main/java/org/neo4j/gis/spatial/SpatialDatabaseService.java @@ -35,6 +35,7 @@ import org.locationtech.jts.geom.Polygon; import org.neo4j.gis.spatial.encoders.Configurable; import org.neo4j.gis.spatial.encoders.NativePointEncoder; +import org.neo4j.gis.spatial.encoders.NativePointsEncoder; import org.neo4j.gis.spatial.encoders.SimplePointEncoder; import org.neo4j.gis.spatial.index.IndexManager; import org.neo4j.gis.spatial.index.LayerGeohashPointIndex; @@ -526,6 +527,8 @@ String getSignature() { "longitude:latitude")); addRegisteredLayerType(new RegisteredLayerType("NativePoint", NativePointEncoder.class, SimplePointLayer.class, DefaultGeographicCRS.WGS84, LayerRTreeIndex.class, "location")); + addRegisteredLayerType(new RegisteredLayerType("NativePoints", NativePointsEncoder.class, + EditableLayerImpl.class, DefaultGeographicCRS.WGS84, LayerRTreeIndex.class, "geometry")); addRegisteredLayerType(new RegisteredLayerType("NativeGeohash", NativePointEncoder.class, SimplePointLayer.class, DefaultGeographicCRS.WGS84, LayerGeohashPointIndex.class, "location")); addRegisteredLayerType(new RegisteredLayerType("NativeZOrder", NativePointEncoder.class, diff --git a/src/main/java/org/neo4j/gis/spatial/encoders/NativePointEncoder.java b/src/main/java/org/neo4j/gis/spatial/encoders/NativePointEncoder.java index 37d0dc06..54bf1581 100644 --- a/src/main/java/org/neo4j/gis/spatial/encoders/NativePointEncoder.java +++ b/src/main/java/org/neo4j/gis/spatial/encoders/NativePointEncoder.java @@ -21,7 +21,6 @@ import org.locationtech.jts.geom.Coordinate; import org.locationtech.jts.geom.Geometry; -import org.locationtech.jts.geom.GeometryFactory; import org.locationtech.jts.geom.Point; import org.neo4j.gis.spatial.AbstractGeometryEncoder; import org.neo4j.gis.spatial.SpatialDatabaseService; @@ -36,22 +35,14 @@ public class NativePointEncoder extends AbstractGeometryEncoder implements Configurable { private static final String DEFAULT_GEOM = "location"; - private static GeometryFactory geometryFactory; private String locationProperty = DEFAULT_GEOM; private Neo4jCRS crs = Neo4jCRS.findCRS("WGS-84"); - protected static GeometryFactory getGeometryFactory() { - if (geometryFactory == null) { - geometryFactory = new GeometryFactory(); - } - return geometryFactory; - } - @Override protected void encodeGeometryShape(Transaction tx, Geometry geometry, Entity container) { int gtype = SpatialDatabaseService.convertJtsClassToGeometryType(geometry.getClass()); if (gtype == GTYPE_POINT) { - container.setProperty("gtype", gtype); + container.setProperty(PROP_TYPE, gtype); Neo4jPoint neo4jPoint = new Neo4jPoint((Point) geometry, crs); container.setProperty(locationProperty, neo4jPoint); } else { diff --git a/src/main/java/org/neo4j/gis/spatial/encoders/NativePointsEncoder.java b/src/main/java/org/neo4j/gis/spatial/encoders/NativePointsEncoder.java new file mode 100644 index 00000000..47eca035 --- /dev/null +++ b/src/main/java/org/neo4j/gis/spatial/encoders/NativePointsEncoder.java @@ -0,0 +1,106 @@ +/* + * 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 . + */ +package org.neo4j.gis.spatial.encoders; + +import java.util.Arrays; +import org.locationtech.jts.geom.Coordinate; +import org.locationtech.jts.geom.Geometry; +import org.locationtech.jts.geom.LineString; +import org.neo4j.gis.spatial.AbstractGeometryEncoder; +import org.neo4j.gis.spatial.SpatialDatabaseService; +import org.neo4j.gis.spatial.encoders.neo4j.Neo4jCRS; +import org.neo4j.gis.spatial.encoders.neo4j.Neo4jPoint; +import org.neo4j.graphdb.Entity; +import org.neo4j.graphdb.Transaction; + +/** + * Simple encoder that stores line strings as an array of points. + */ +public class NativePointsEncoder extends AbstractGeometryEncoder implements Configurable { + + private String property = "geometry"; + private Neo4jCRS crs = Neo4jCRS.findCRS("WGS-84"); + + @Override + protected void encodeGeometryShape(Transaction tx, Geometry geometry, Entity container) { + var gtype = SpatialDatabaseService.convertJtsClassToGeometryType(geometry.getClass()); + if (geometry instanceof LineString lineString) { + var neo4jPoints = Arrays.stream(lineString.getCoordinates()) + .map(coordinate -> new Neo4jPoint(coordinate, crs)) + .toArray(org.neo4j.graphdb.spatial.Point[]::new); + container.setProperty(PROP_TYPE, gtype); + container.setProperty(property, neo4jPoints); + } else { + throw new IllegalArgumentException( + "Can only store point-arrays as linestring: " + SpatialDatabaseService.convertGeometryTypeToName( + gtype)); + } + + } + + @Override + public Geometry decodeGeometry(Entity container) { + var points = ((org.neo4j.graphdb.spatial.Point[]) container.getProperty(property)); + var factory = getGeometryFactory(); + var coordinates = Arrays.stream(points) + .map(point -> { + if (point.getCRS().getCode() != crs.getCode()) { + throw new IllegalStateException( + "Trying to decode geometry with wrong CRS: layer configured to crs=" + crs + + ", but geometry has crs=" + point.getCRS().getCode()); + } + double[] coordinate = point.getCoordinate().getCoordinate(); + if (crs.dimensions() == 3) { + return new Coordinate(coordinate[0], coordinate[1], coordinate[2]); + } else { + return new Coordinate(coordinate[0], coordinate[1]); + } + }) + .toArray(Coordinate[]::new); + return factory.createLineString(coordinates); + } + + @Override + public String getConfiguration() { + return property + ":" + bboxProperty + ": " + crs.getCode(); + } + + @Override + public void setConfiguration(String configuration) { + if (configuration != null && !configuration.trim().isEmpty()) { + String[] fields = configuration.split(":"); + if (fields.length > 0) { + property = fields[0]; + } + if (fields.length > 1) { + bboxProperty = fields[1]; + } + if (fields.length > 2) { + crs = Neo4jCRS.findCRS(fields[2]); + } + } + } + + @Override + public String getSignature() { + return "NativePointEncoder(geometry='" + property + "', bbox='" + bboxProperty + "', crs=" + crs.getCode() + + ")"; + } +} diff --git a/src/main/java/org/neo4j/gis/spatial/encoders/SimpleGraphEncoder.java b/src/main/java/org/neo4j/gis/spatial/encoders/SimpleGraphEncoder.java index 6d2cc672..7c243493 100644 --- a/src/main/java/org/neo4j/gis/spatial/encoders/SimpleGraphEncoder.java +++ b/src/main/java/org/neo4j/gis/spatial/encoders/SimpleGraphEncoder.java @@ -22,7 +22,6 @@ import org.locationtech.jts.geom.Coordinate; import org.locationtech.jts.geom.CoordinateList; import org.locationtech.jts.geom.Geometry; -import org.locationtech.jts.geom.GeometryFactory; import org.neo4j.gis.spatial.AbstractGeometryEncoder; import org.neo4j.gis.spatial.SpatialDatabaseException; import org.neo4j.graphdb.Direction; @@ -41,19 +40,10 @@ // TODO: Consider generalizing this code and making a general linked list geometry store available in the library public class SimpleGraphEncoder extends AbstractGeometryEncoder { - private GeometryFactory geometryFactory; - protected enum SimpleRelationshipTypes implements RelationshipType { FIRST, NEXT } - private GeometryFactory getGeometryFactory() { - if (geometryFactory == null) { - geometryFactory = new GeometryFactory(); - } - return geometryFactory; - } - private static Node testIsNode(Entity container) { if (!(container instanceof Node)) { throw new SpatialDatabaseException("Cannot decode non-node geometry: " + container); @@ -64,7 +54,7 @@ private static Node testIsNode(Entity container) { @Override protected void encodeGeometryShape(Transaction tx, Geometry geometry, Entity container) { Node node = testIsNode(container); - node.setProperty("gtype", GTYPE_LINESTRING); + node.setProperty(PROP_TYPE, GTYPE_LINESTRING); Node prev = null; for (Coordinate coord : geometry.getCoordinates()) { Node point = tx.createNode(); diff --git a/src/main/java/org/neo4j/gis/spatial/encoders/SimplePointEncoder.java b/src/main/java/org/neo4j/gis/spatial/encoders/SimplePointEncoder.java index 4e87ebe6..a3e7eb52 100644 --- a/src/main/java/org/neo4j/gis/spatial/encoders/SimplePointEncoder.java +++ b/src/main/java/org/neo4j/gis/spatial/encoders/SimplePointEncoder.java @@ -21,7 +21,6 @@ import org.locationtech.jts.geom.Coordinate; import org.locationtech.jts.geom.Geometry; -import org.locationtech.jts.geom.GeometryFactory; import org.neo4j.gis.spatial.AbstractGeometryEncoder; import org.neo4j.gis.spatial.SpatialDatabaseService; import org.neo4j.graphdb.Entity; @@ -34,21 +33,13 @@ public class SimplePointEncoder extends AbstractGeometryEncoder implements Confi public static final String DEFAULT_X = "longitude"; public static final String DEFAULT_Y = "latitude"; - protected GeometryFactory geometryFactory; protected String xProperty = DEFAULT_X; protected String yProperty = DEFAULT_Y; - protected GeometryFactory getGeometryFactory() { - if (geometryFactory == null) { - geometryFactory = new GeometryFactory(); - } - return geometryFactory; - } - @Override protected void encodeGeometryShape(Transaction tx, Geometry geometry, Entity container) { container.setProperty( - "gtype", + PROP_TYPE, SpatialDatabaseService.convertJtsClassToGeometryType(geometry.getClass())); Coordinate[] coords = geometry.getCoordinates(); container.setProperty(xProperty, coords[0].x); diff --git a/src/main/java/org/neo4j/gis/spatial/encoders/SimplePropertyEncoder.java b/src/main/java/org/neo4j/gis/spatial/encoders/SimplePropertyEncoder.java index 54f1ff49..79adc47a 100644 --- a/src/main/java/org/neo4j/gis/spatial/encoders/SimplePropertyEncoder.java +++ b/src/main/java/org/neo4j/gis/spatial/encoders/SimplePropertyEncoder.java @@ -21,7 +21,6 @@ import org.locationtech.jts.geom.Coordinate; import org.locationtech.jts.geom.Geometry; -import org.locationtech.jts.geom.GeometryFactory; import org.neo4j.gis.spatial.AbstractGeometryEncoder; import org.neo4j.gis.spatial.SpatialDatabaseService; import org.neo4j.graphdb.Entity; @@ -35,18 +34,9 @@ // TODO: Consider switching from Float to Double according to Davide Savazzi public class SimplePropertyEncoder extends AbstractGeometryEncoder { - protected GeometryFactory geometryFactory; - - protected GeometryFactory getGeometryFactory() { - if (geometryFactory == null) { - geometryFactory = new GeometryFactory(); - } - return geometryFactory; - } - @Override protected void encodeGeometryShape(Transaction tx, Geometry geometry, Entity container) { - container.setProperty("gtype", SpatialDatabaseService.convertJtsClassToGeometryType(geometry.getClass())); + container.setProperty(PROP_TYPE, SpatialDatabaseService.convertJtsClassToGeometryType(geometry.getClass())); Coordinate[] coords = geometry.getCoordinates(); float[] data = new float[coords.length * 2]; for (int i = 0; i < coords.length; i++) { diff --git a/src/main/java/org/neo4j/gis/spatial/osm/OSMImporter.java b/src/main/java/org/neo4j/gis/spatial/osm/OSMImporter.java index 22557f21..3d86683f 100644 --- a/src/main/java/org/neo4j/gis/spatial/osm/OSMImporter.java +++ b/src/main/java/org/neo4j/gis/spatial/osm/OSMImporter.java @@ -1123,7 +1123,7 @@ protected void addNodeGeometry(WrappedNode node, int gtype, Envelope bbox, int v gtype = vertices > 1 ? GTYPE_MULTIPOINT : GTYPE_POINT; } Node geomNode = tx.createNode(); - geomNode.setProperty("gtype", gtype); + geomNode.setProperty(PROP_TYPE, gtype); geomNode.setProperty("vertices", vertices); geomNode.setProperty(PROP_BBOX, new double[]{bbox.getMinX(), bbox.getMaxX(), bbox.getMinY(), bbox.getMaxY()}); @@ -1207,7 +1207,7 @@ protected void updateGeometryMetaDataFromMember(WrappedNode member, GeometryMeta try (var relationships = member.getRelationships(OSMRelation.GEOM)) { for (Relationship rel : relationships) { nodeProps = getNodeProperties(WrappedNode.fromNode(rel.getEndNode())); - metaGeom.checkSupportedGeometry((Integer) nodeProps.get("gtype")); + metaGeom.checkSupportedGeometry((Integer) nodeProps.get(PROP_TYPE)); metaGeom.expandToIncludeBBox(nodeProps); } } diff --git a/src/test/java/org/neo4j/gis/spatial/procedures/SpatialProceduresTest.java b/src/test/java/org/neo4j/gis/spatial/procedures/SpatialProceduresTest.java index a8562200..e08dcb8b 100644 --- a/src/test/java/org/neo4j/gis/spatial/procedures/SpatialProceduresTest.java +++ b/src/test/java/org/neo4j/gis/spatial/procedures/SpatialProceduresTest.java @@ -1244,6 +1244,16 @@ public void find_no_geometries_using_closest_on_empty_layer() { testCallCount(db, "CALL spatial.closest('geom',{lon:15.2, lat:60.1}, 1.0)", null, 0); } + @Test + public void testNativePoints() { + execute("CREATE (node:Foo { points: [point({latitude: 5.0, longitude: 4.0}), point({latitude: 6.0, longitude: 5.0})]})"); + execute("CALL spatial.addLayer('line','NativePoints','points') YIELD node" + + " MATCH (n:Foo)" + + " WITH collect(n) AS nodes" + + " CALL spatial.addNodes('line', nodes) YIELD count RETURN count"); + testCallCount(db, "CALL spatial.closest('line',{lon:5.1, lat:4.1}, 1.0)", null, 1); + } + /* @Test