Published
A pin on a map tells you where something is. It is less helpful when the question is "Can we deliver there?", "Does this route cross the park?" or "Why have we put the meeting point in a pond?"
Galactus DB's spatial shapes let your graph work with areas and routes as well as points. You can keep them alongside the rest of your data and ask those questions in Cypher. Let's take a walk through a small imaginary park.
The pond is not part of the picnic area
Our park is a square with a pond in the middle. For this example, the pond is a hole in the usable park area. There is a cafe on dry land and a marker in the water. The coordinates use a simple local grid, so no map is needed.
Run each code block separately in a fresh database:
CREATE (:Park {
name:'Riverside',
shape:spatial.fromWKT(
'POLYGON((0 0,10 0,10 10,0 10,0 0),(4 4,4 6,6 6,6 4,4 4))'
)
}),
(:Place {name:'Cafe', shape:spatial.fromWKT('POINT(2 2)')}),
(:Place {name:'Pond marker', shape:spatial.fromWKT('POINT(5 5)')});
The polygon's first ring is its outside boundary; the second is the hole. Now we can ask which places sit in the usable area:
MATCH (park:Park), (place:Place)
WHERE spatial.covers(park.shape, place.shape)
RETURN place.name;
The answer is Cafe. The pond marker is inside the outer square, but the
hole leaves it out. Geography has saved us from a rather damp lunch.
covers includes the boundary too. For a point strictly inside, use
contains. That distinction matters for a delivery address right on the
edge of a service area: decide whether the boundary belongs before choosing
the function.
Keep the useful bit of a route
A maintenance route starts west of the park and finishes east of it. We only want the stretch inside:
MATCH (park:Park)
RETURN spatial.asWKT(spatial.clip(
spatial.fromWKT('LINESTRING(-2 2,12 2)'),
park.shape
)) AS routeInside;
The result runs from (0,2) to (10,2). clip cuts the route using the park
as a mask and returns a shape you can store or use in another query.
If you only need a yes or no, intersects answers whether shapes meet.
touches, within and crosses help you ask more specific questions: a
route passing through an area is different from one that only brushes its
boundary.
Sorry, this half is closed
Suppose maintenance closes the western half of the park. We want to keep both the closed area and the part still open:
MATCH (park:Park)
WITH spatial.identity(
park.shape,
spatial.fromWKT('POLYGON((0 0,5 0,5 10,0 10,0 0))')
) AS split
RETURN spatial.asWKT(split.inside) AS closed,
spatial.asWKT(split.outside) AS stillOpen;
identity returns both pieces as inside and outside. Other operations
let you choose just the result you need:
| Function | What you keep |
|---|---|
spatial.union(a,b) | Everything covered by either shape |
spatial.intersection(a,b) | Only their shared coverage |
spatial.difference(a,b) | A with B taken out |
spatial.symDifference(a,b) | Either shape, excluding the shared part |
A shared part is not always an area. Two parks might meet along a fence or at a corner, giving you a line or a point. That is useful when your question is about a shared boundary rather than room for a picnic blanket.
The centre is in the pond. Of course it is.
MATCH (park:Park)
RETURN spatial.asWKT(spatial.centroid(park.shape)) AS centre;
Our symmetrical park returns POINT (5 5), right in the water. A centroid is
a weighted centre, so it makes no promise that you can stand there. It is
useful for analysis; give it a little thought before sending anyone to it.
Finding shapes as the graph grows
Shapes have their own spatial index, separate from the existing point index:
CREATE SPATIAL INDEX park_shapes FOR (park:Park) ON (park.shape);
Use the index procedure to find shapes that meet the route:
CALL db.index.spatial.intersects(
'park_shapes',
spatial.fromWKT('LINESTRING(-2 2,12 2)')
)
YIELD node
RETURN node.name;
This returns Riverside. For Cartesian geometry, the index checks bounding
boxes to narrow the search, then checks the actual shapes. The pond still
counts as a hole.
Call the procedure explicitly when you want the index;
WHERE spatial.intersects(...) does not select it automatically. Geographic
indexes currently refine a scan, so they do not offer the same bounding-box
shortcut. Index declarations persist on a durable instance and stay in step
with changes to the graph.
Taking the park out into the world
Local grids work well for floor plans and examples like this one. For longitude and latitude with great-circle edges, choose the geography domain. Coordinates are always longitude, latitude. Set the domain and SRID consistently: changing an SRID does not transform the coordinates for you.
You can bring shapes in and out using WKT, WKB and GeoJSON. Existing
point() queries still work, and spatial.fromPoint lets you turn a point
into a shape explicitly.
There are a few boundaries to keep in mind. Spatial calculations are 2D, even when a stored shape includes height. Geography uses a sphere and has limits on the extents it can process; unrestricted global overlays are not supported. Geographic GeoJSON export needs explicit approximation settings, and polar export is not available yet.
The spatial guide has the coordinate examples, format rules, operation limits and upgrade notes when you need them.
Your graph can now connect the people, places and routes with the areas they actually occupy. That leaves you free to ask the useful questions, including whether there is a dry place to meet for coffee.