Extension: Spatial

Maintained by: Simon Harrison <noisyboiler@googlemail.com>

An API to the Neo4j Spatial Extension for creating, destroying and querying Well Known Text (WKT) geometries over GIS map Layers.

Requires Neo4j >= 2.0 <= 2.2.3, the Neo4j Spatial Extension, libgeos and the Python package Shapely.

Neo4j Setup

Clone the Neo4j Spatial project and checkout the branch that corresponds to the Neo4j version you are using, e.g. remotes/origin/0.14-neo4j-2.2 for Neo4j Community 2.2.3 makes sense, then build the Neo4j spatial extension.

mvn clean package install -Dmaven.test.skip=true

Stop your Neo4j server and install the plugin.

cp $NEO4J_SPATIAL_HOME/target/neo4j-spatial-XXXX-server-plugin.zip $NEO4J_HOME/plugins

unzip $NEO4J_HOME/plugins/neo4j-spatial-XXXX-server-plugin.zip -d $NEO4J_HOME/plugins

To test your install, from a shell you can use curl to ask for the configuration of the Neo4j server.

curl http://username:password@localhost:7474/db/data/

In the “extensions” section of response you should see “SpatialPlugin”.

Run Tests

The py2neo Spatial APIs rely on the upstream project, the Neo4j Spatial Extension, to be kept up to date with Neo4j. Because of this it’s advised to run the tests for this extension to check for compatibility problems.

pip install -r ./test/requirements.txt

py.test ./test/ext/spatial/

These tests have been run successfully against Neo4j 2.0 - 2.2.3 using Python 2.7.

API Overview

Simple CRUD APIs for GIS Layers, Points Of Interest (POI) and other geometries plus some endpoints for spatial queries over your data.

Each GIS Layer you create is essentially an “index” and is modelled as an Rtree within your graph which Neo4j will use when executing “spatial” type queries. This should not be confused with the Neo4j schema indexes, nor the “legacy” type of index, and is used internally by the spatial extension to optimise expensive spatial queries.

py2neo Spatial creates layers of type org.neo4j.gis.spatial.EditableLayerImpl and encodes spatial data with the org.neo4j.gis.spatial.WKTGeometryEncoder. See the Neo4j Spatial Extension for alternatives.

Each vector geometry you create must be in the standard, unprojected WGS84 (EPSG:4326) geographic coordinate system. For a vector geometry this will be marked-up in Well Known Text (WKT) and for a POI this will be as a longitude/latitude (x/y) pair. There is nothing stopping you adding your POI as WKT - the long/lat API exists for the convenience of the many clients that deal in point coordinates.

Note

py2neo Spatial works with WGS84 (EPSG 4326) WKT strings. When py2neo Spatial handles Point Of Interest coordinates it use the standard of POINT (x y) which translates to POINT (Lon Lat) - not lat/long.

See wiki Well Known Text for a list of possible geometries that you can create. Internally the data will be stored as Well Known Binary (WKB).

Extended py2neo REST API for the Neo4j Spatial Extension

py2neo Spatial is an extension to the py2neo project and provides REST APIs for the Neo4j Spatial Extension.

The Neo4j Spatial Extension should be considered in two ways: embedded server and REST API, and the former has a richer API than the latter. The REST API was more of a prototype than a complete product and designed primarily to support Point objects and some WKT APIs.

py2neo Spatial implements some of the “prototype” JAVA REST API for WKT geometries. The current status is:

implemented = [
    # add an editable WKT geometry encoded Layer to the graph
    'addEditableLayer',
    # get a (WKT) Layer from the graph
    'getLayer',
    # add a Node with a WKT geometry property to an EditableLayer
    'addGeometryWKTToLayer',
    # add an existing, non-geographically aware Node, to a Layer
    'addNodeToLayer',
    # update the geometry on a Node
    'updateGeometryFromWKT',
    # query APIs
    'findGeometriesWithinDistance',
    'find_within_bounding_box',
]

not_implemented = [
    # for bespoke and optimised layer queries. no case for this yet.
    'addCQLDynamicLayer',
    # for bulk `addNodeToLayer` type requests. likely to follow.
    'addNodesToLayer',
    # not implemented. currently prefer WKT Nodes on EditableLayers.
    'addSimplePointLayer',
    # this appears to be broken upstream, with the `distanceInKm` behaving
    # more like a tolerance. No test cases could be written against this.
    'findClosestGeometries',
]
class py2neo.ext.spatial.plugin.Spatial(graph)[source]

A py2neo extension for WKT type GIS operations

add_node_to_layer_by_id(node_id, layer_name, geometry_name, wkt_string, labels=None)[source]

Add a non-geographically aware Node to a Layer (spatial index) by it’s internal ID. This is any Node without a WKT property, as defined by the extensions WKT_PROPERTY value.

Parameters:
node_id : int

The internal Neo4j identifier of a Node.

layer_name : string

The name of the Layer (index) to add the Node to.

geometry_name : string

A unique name to give the geometry.

wkt_string : string

A WKT geometry to give the Node.

labels : list

An optional list of labels to give to the Node.

Returns:

An HTTP status code.

Raises:
NodeNotFoundError

When a Node with ID node_id cannot be found.

GeometryExistsError

When a geometry with geometry_name already exists.

create_geometry(geometry_name, layer_name, wkt_string, labels=None, node_properties=None)[source]

Create a geometry Node with any WKT string type.

Parameters:
geometry_name : string

A unique name to give the geometry.

layer_name : string

The name of the Layer to add the Node to.

wkt_string : string

A WKT geometry to give the Node.

labels : list

An optional list of labels to give to the Node.

node_properties : dict

Optional keyword arguments to apply as properties on the Node.

Returns:

An HTTP status code.

Raises:
LayerNotFoundError

When the Layer does not exist.

GeometryExistsError

When a geometry with geometry_name already exists.

create_layer(layer_name)[source]

Create a GIS map Layer to add geometries to. The Layer is encoded by the WKTGeometryEncoder.

Parameters:
layer_name : str

The name to give the Layer created. Must be unique.

Returns:

An HTTP status code.

Raises:
LayerExistsError

If a Layer with layer_name already exists.

create_point_of_interest(poi_name, layer_name, longitude, latitude, labels=None, node_properties=None)[source]

Create a Point of Interest (POI) on a Layer.

Parameters:
layer_name : str

The Layer to add the POI to.

poi_name: str

A unique name for the POI.

longitude : Decimal

Decimal number between -180.0 and 180.0, east or west of the Prime Meridian.

latitude : Decimal

Decimal number between -90.0 and 90.0, north or south of the equator.

labels : list

Optional list of labels to apply to the POI Node.

node_properties : dict

Optional keyword arguments to apply as properties on the Node.

Returns:

An HTTP status_code.

Raises:
LayerNotFoundError

When the Layer does not exist.

GeometryExistsError

When a geometry with geometry_name already exists.

delete_geometry(layer_name, geometry_name)[source]

Remove a geometry Node from a Layer.

Note

This does not delete the Node itself, just removes it from the spatial index. Use the standard py2neo API to delete Nodes.

Parameters:
layer_name : str

The name of the Layer to remove the geometry from.

geometry_name : str

The unique name of the geometry to delete.

Returns:

None

Raises:
LayerNotFoundError

When the Layer does not exist.

Note

The return type here is None and not an HTTP status because there is no Neo4j Spatial Extension REST endpoint for deleting a layer, so we use the Py2neo cypher API, which returns nothing on delete.

delete_layer(layer_name)[source]

Delete a GIS map Layer.

This will remove a representation of a GIS map Layer from the Neo4j data store - it will not remove any nodes you may have added to it, or any labels or properties py2neo Spatial may have added to your own Nodes.

Parameters:
layer_name : str

The name of the Layer to delete.

Returns:

None

Raises:
LayerNotFoundError

When the Layer to delete does not exist.

Note

The return type here is None and not an HTTP status because there is no Neo4j Spatial Extension REST endpoint for deleting a layer, so we use the Py2neo cypher API, which returns nothing on delete.

find_containing_geometries(layer_name, longitude, latitude)[source]

Given the position of a point of interest, find all the geometries that contain it on a given Layer.

Parameters:
layer_name : str

The name of the Layer to find containing geometries from.

longitude : Decimal

Decimal number between -180.0 and 180.0, east or west of the Prime Meridian.

latitude : Decimal

Decimal number between -90.0 and 90.0, north or south of the equator.

Returns:

A list of Nodes of geometry type Polygon or MultiPolygon.

find_points_of_interest(layer_name, longitude, latitude, max_distance, labels)[source]

Given a coordinate, find the Point geometries that are “nearby” it and are “interesting”. Only Nodes that have matching labels are deemed “interesting”.

Parameters:
layer_name : str

The name of the Layer to remove the geometry from.

longitude : Decimal

Decimal number between -180.0 and 180.0, east or west of the Prime Meridian.

latitude : Decimal

Decimal number between -90.0 and 90.0, north or south of the equator.

max_distance : int

The radius of the search area to look for “interesting” Points.

labels : list

List of node labels which determine which points are “interesting”.

Returns:

A list of interesting Nodes.

find_within_bounding_box(layer_name, minx, miny, maxx, maxy)[source]

Find the points of interest from a given layer enclosed by a bounding box.

The bounding box is definded by the lat-longs of the bottom left and the top right, essentially:

bbox = (min Longitude, min Latitude, max Longitude, max Latitude)
Parameters:
layer_name : str

The name of the Layer to remove the geometry from.

minx : Decimal

Longitude of the bottom-left corner.

miny : Decimal

Latitude of the bottom-left corner.

maxx : Decimal

Longitude of the top-right corner.

minx : Decimal

Latitude of the top-right corner.

Returns:

A list of all matched nodes in order of distance.

find_within_distance(layer_name, longitude, latitude, distance)[source]

Find all WKT geometry primitive within a given distance from location coord.

Parameters:
layer_name : str

The name of the Layer to remove the geometry from.

longitude : Decimal

Decimal number between -180.0 and 180.0, east or west of the Prime Meridian.

latitude : Decimal

Decimal number between -90.0 and 90.0, north or south of the equator.

distance : int

The radius of the search area in Kilometres (km).

Returns:

A list of all matched nodes.

Raises:
LayerNotFoundError

When the Layer does not exist.

get_layer(layer_name)[source]

Get the Layer identified by layer_name.

Parameters:
layer_name : str

The name of the Layer to return.

Returns:

The Layer Node.

Raises:
LayerNotFoundError

If a Layer with layer_name does not exist.

update_geometry(layer_name, geometry_name, new_wkt_string)[source]

Update the WKT geometry on a Node.

Parameters:
layer_name : str

The name of the Layer containing the geometry to update.

geometry_name : str

The name of the Node geometry to update.

new_wkt_string : str

The new Well Known Text string that will replace that existing on the Node with geometry_name.

Returns:

An HTTP status code.

Raises:
GeometryNotFoundError

When the geometry to update cannot be found.

InvalidWKTError

When the new_wkt_string is invalid.

LayerNotFoundError

When the Layer does not exist.

Neo4j Spatial & Geoserver

Geoserver is an open source server for sharing the geospatial data that you create using py2neo spatial and other GIS technologies.

In order to support a Neo4j type datastore we must first add the Neo4j and Neo4j Spatial source jars into your Geoservers WEB—INF/lib directory and restart it.

Note

Documentation based on Neo4j 2.2.3, GeoServer 2.7 and Neo4j Spatial 0.14 on Ubunutu 14.04 VM

Clone the Neo4j Spatial project and checkout the branch that corresponds to the Neo4j version you are using, e.g. remotes/origin/0.14-neo4j-2.2. Build the Neo4j spatial extension.

mvn clean package install -Dmaven.test.skip=true

On success, unpack the neo4j—spatial—X.XX—neo4j—X.X.X—server—plugin.zip archive that you’ve created in the projects target directory and copy or move the contained jars into the WEB—INF/lib directory found under $GEOSERVER_HOME. Then copy the jars from $NEO4J_HOME/lib into the same directory. Restart Geoserver.

If you’ve been successful you will now find a new Neo4j “Store” available from the Geoserver web UI: “Neo4j — A datasource backed by a Neo4j Spatial datasource”.

Visit the web interface at http://localhost:8080/geoserver/web to find this out.

Note

The default username and password is “admin” and “geoserver”

Getting spatial data into Neo4j is easy with py2neo’s spatial extension but you may not have any yet and you may just want to try out Geoserver, so py2neo Spatial provides some example data and a script to load them.

For example, to put a MultiPolygon modelling Cornwall on to a GIS layer called “uk”, from the root of your project, run:

python py2neo/ext/spatial/scripts/load_data.py --data cornwall --layer uk --username neo4j --password mysecret

Whatever GIS data you have, stop your Neo4j server and place a copy of the database somewhere that Geoserver can access it. From the GeoServer web admin create a new Neo4j Store, name it, describe it, and add the path to your Neo4j database. Save it.

GeoServer should recognise the layers in the datastore, so whatever you named a Layer with py2neo, expect to see it listed now. Next step: pubish and preview the layers.

Warning

Geoserver integration is flakey, and there are no integration tests between all the moving parts. Configuring Geoserver to not raise java.lang.OutOfMemoryError and patching Neo4j Spatial not to raise org.neo4j.graphdb.NotInTransactionException is only straight forward for JAVA developers. Getting your versions of Neo4j, Neo4j Spatial and Geoserver all to play well together is also a challenge. As exciting as the prospect of visualising Neo4j data sets in Geoserver is, it’s currently incredibly hard work and requires someone to take this on seriously.