Exploring the Neo4j Spatial Plugin: Custom Procedures for Spatial Analysis

Product Manager, Neo4j
4 min read

In the previous three installments, we looked at how the Neo4j Spatial Plugin can be used to import spatial data into Neo4j and perform simple geospatial operations on point and polygon layers. We also showed how the plugin can be used to manipulate and analyze AIS data, demonstrating how some GIS-like functionality can be achieved in Neo4j.
In this final installment, we’ll briefly look at how Neo4j’s plugin capabilities can be further extended to add more advanced GIS functionality. With the Neo4j Spatial Plugin, we have access to a full range of indexed geometry types in the database. Using these as a foundation, it becomes a relatively simple task to add additional geoprocessing capabilities by wrapping open source spatial procedures from open-source libraries, such as Java Topology Suite (JTS), GEOS or GeoTools.
In Exploring Neo4j Spatial: Path Intersections Using AIS Data, I showed how the plugin can calculate if a line crosses a polygon, but not precisely where that intersection happens. I wanted to be able to calculate the precise coordinates where a vessel intersects an exclusion zone. To do this, I wrapped a JTS procedure that does exactly that and created a simple one-trick plugin.
In the past, this would’ve required some Java development skills, which might have put this option beyond the reach of many Neo4j users. My own Java skills are a couple of decades out of date, so I also needed a bit of guidance on how to set up a Maven project in IntelliJ. Fortunately, asking a friendly LLM (I used ChatGPT) provided me with simple step-by-step instructions. And within a few minutes of describing the plugin functionality I wanted to ChatGPT, and just a little back and forth, I had this:
package com.geo4j.spatial;
import org.neo4j.procedure.*;
import org.locationtech.jts.geom.*;
import org.locationtech.jts.io.WKTReader;
import org.locationtech.jts.geom.GeometryFactory;
import org.locationtech.jts.geom.TopologyException;
import java.util.stream.Stream;
public class CrossingPointsProcedure {
public static class Output {
public String intersectionWKT;
public Output(String wkt) {
this.intersectionWKT = wkt;
}
}
@Procedure(name = "geo4j.crossingEntryExitPoints", mode = Mode.READ)
@Description("Returns MULTIPOINT WKT with entry/exit points where a LineString crosses a Polygon")
public Stream<Output> crossingEntryExitPoints(
@Name("lineWKT") String lineWKT,
@Name("polygonWKT") String polygonWKT) throws Exception {
WKTReader reader = new WKTReader();
GeometryFactory geometryFactory = new GeometryFactory();
try {
Geometry line = reader.read(lineWKT);
Geometry polygon = reader.read(polygonWKT);
if (line.crosses(polygon)) {
Geometry intersection = line.intersection(polygon);
if (intersection instanceof LineString) {
LineString segment = (LineString) intersection;
Point start = segment.getStartPoint();
Point end = segment.getEndPoint();
MultiPoint multiPoint = geometryFactory.createMultiPointFromCoords(new Coordinate[] {
start.getCoordinate(),
end.getCoordinate()
});
return Stream.of(new Output(multiPoint.toText()));
}
}
} catch (TopologyException e) {
// Return empty if invalid geometry or error
}
return Stream.empty();
}
}
IntelliJ compiled the plugin, and I dropped it into my DB’s plugins folder and updated neo4j.conf to whitelist geo4j.*
, the namespace I chose for this plugin (see Exploring Neo4j Spatial: Installation, Data Loading, and Simple Querying for further information on how to do this). After a quick database restart, I can now run this query:
//Step 1: Get our AIS transit geometry
MATCH (spatialNode:transit)
WITH spatialNode, spatialNode.geometry AS vesselWKT
// Step 2: Get all exclusion polygons and decode to WKT
MATCH (exclusionNode:exclusion)
WITH spatialNode, vesselWKT, exclusionNode.geometry AS exclusionWKT, exclusionNode
// Step 3: Call custom intersection procedure
CALL geo4j.crossingEntryExitPoints(vesselWKT, exclusionWKT) YIELD intersectionWKT
WHERE intersectionWKT IS NOT NULL AND intersectionWKT <> 'MULTIPOINT EMPTY'
// Step 4: Return results
RETURN
exclusionNode.zone AS exclusionZoneName,
spatialNode.name AS vesselName,
intersectionWKT
And here is the result:
exclusionZoneName
"Exclusion_zone_1"
vesselName
"SILVER LONDON"
intersectionWKT
"MULTIPOINT ((-0.73387 50.35723), (-0.8825171789256092 50.333434799581916))"
Success! Just one vessel crossed the exclusion zone, and my new simple plugin correctly returns the precise location where that vessel entered (and left) the exclusion zone.
Of course, there are further enhancements we can think about, such as using the direction of the transit to identify which point represents the entry, which point the exit, and adding the estimated time of those crossings, but I think this simple demonstration has shown that Neo4j and geospatial analysis is a potent and exciting combination.
Summary
As I discussed in my introduction to Installation, Data Loading, and Simple Querying, Neo4j and geospatial are a great natural fit. The ability to combine analysis of spatial relationships (like proximity or containment) with analysis of non-spatial patterns is an exciting proposition, enabling users to uncover deeper insights. The Neo4j Spatial Plugin provides some valuable tools, enabling users to bring geospatial and non-geospatial data together in the same graph.
This blog series provided a high-level introduction to the Neo4j Spatial Plugin. Please share your own experiences, ideas, and questions with us, and tell us what other spatial enhancements you would like to see in Neo4j.
Exploring the Neo4j Spatial Plugin: Custom Procedures for Spatial Analysis was originally published in Neo4j Developer Blog on Medium, where people are continuing the conversation by highlighting and responding to this story.