AshNeo4j.Geo (AshNeo4j v0.9.0)

Copy Markdown View Source

Geodesic geometry primitives for the in-memory side of AshNeo4j's spatial predicates.

The single source of truth for distance math. The in-memory st_* functions (st_distance, st_dwithin, st_closest_point) use these so they agree with the values Neo4j returns on the pushdown path.

Matching Neo4j's distance model

Neo4j's point.distance/2 for WGS-84 geographic points is a spherical haversine on the WGS-84 equatorial radius (the semi-major axis, 6 378 137 m) — not the mean Earth radius (6 371 000 m) a naive haversine would reach for. Using the mean radius disagrees with Neo4j by ~0.11 % (≈800 m over a 700 km span), which means the same st_distance query would return different answers depending on whether it pushed down to Cypher or evaluated in Elixir.

We deliberately use the same radius Neo4j does, so the two execution paths agree to sub-metre over continental distances. Neo4j's model is spherical, not ellipsoidal — true ellipsoidal distance (Vincenty / Karney) would differ by a further ~0.1–0.5 %, but since we push down to Neo4j, Neo4j's model is the reference we match.

Summary

Functions

The point on segment ab closest to p, as a {lng, lat} pair. Clamps to the segment's endpoints. See point_segment_meters/3 for the projection model.

Projects a 3D (Z) geometry down to its 2D footprint by dropping the height ordinate — the OGC ST_Force2D operation (#270). This is the only sanctioned bridge between the 3D and 2D worlds: e.g. asking whether a 3D antenna is inside a 2D coverage area is st_contains(area, ^force_2d(antenna)).

Geodesic (great-circle haversine) distance in metres between two WGS-84 {lng, lat} coordinate pairs. Matches Neo4j's point.distance/2 to sub-metre over continental distances.

Geodesic distance in metres between two WGS-84-3D {lng, lat, height} coordinate triples (#270). Matches Neo4j's 3D point.distance/2 to within ~0.1 m.

Minimum point_segment_meters/3 from p to any segment formed by consecutive vertices of coords (a list of {lng, lat} pairs). Used for point-to-LineString and point-to-polygon-ring-edge distance. A single-vertex list degenerates to the distance to that vertex; an empty list returns :infinity (a sentinel that orders above any real distance under min/2, so callers can fold it away).

Geodesic distance in metres from point p to the nearest point on the segment ab (each a {lng, lat} pair) — the true closest-point-on-segment distance, not closest-vertex.

Functions

closest_point_on_segment(arg, a, b)

@spec closest_point_on_segment(
  {number(), number()},
  {number(), number()},
  {number(), number()}
) ::
  {number(), number()}

The point on segment ab closest to p, as a {lng, lat} pair. Clamps to the segment's endpoints. See point_segment_meters/3 for the projection model.

force_2d(g)

@spec force_2d(Geo.geometry()) :: Geo.geometry()

Projects a 3D (Z) geometry down to its 2D footprint by dropping the height ordinate — the OGC ST_Force2D operation (#270). This is the only sanctioned bridge between the 3D and 2D worlds: e.g. asking whether a 3D antenna is inside a 2D coverage area is st_contains(area, ^force_2d(antenna)).

Returns the 2D sibling struct with srid: 4326. A geometry that is already 2D is returned unchanged. There is no to_3d — a height cannot be fabricated.

iex> AshNeo4j.Geo.force_2d(%Geo.PointZ{coordinates: {151.0, -33.0, 50.0}, srid: 4979})
%Geo.Point{coordinates: {151.0, -33.0}, srid: 4326}

haversine_meters(arg1, arg2)

@spec haversine_meters({number(), number()}, {number(), number()}) :: float()

Geodesic (great-circle haversine) distance in metres between two WGS-84 {lng, lat} coordinate pairs. Matches Neo4j's point.distance/2 to sub-metre over continental distances.

haversine_meters_3d(arg1, arg2)

@spec haversine_meters_3d(
  {number(), number(), number()},
  {number(), number(), number()}
) :: float()

Geodesic distance in metres between two WGS-84-3D {lng, lat, height} coordinate triples (#270). Matches Neo4j's 3D point.distance/2 to within ~0.1 m.

Neo4j's 3D geographic model is not a naive √(ground² + Δh²) — the great-circle arc is taken at the mean height (h₁+h₂)/2, then combined with the height delta by Pythagoras:

d = ( (arc × (R + h_mean) / R)²  +  Δh² )

where arc is the surface haversine (at the equatorial radius R). Matching this exactly keeps in-memory order_by / calculate consistent with the point.distance pushdown, the same invariant the 2D path holds.

min_segment_meters(p, coords)

@spec min_segment_meters(
  {number(), number()},
  [{number(), number()}]
) :: float() | :infinity

Minimum point_segment_meters/3 from p to any segment formed by consecutive vertices of coords (a list of {lng, lat} pairs). Used for point-to-LineString and point-to-polygon-ring-edge distance. A single-vertex list degenerates to the distance to that vertex; an empty list returns :infinity (a sentinel that orders above any real distance under min/2, so callers can fold it away).

point_segment_meters(p, a, b)

@spec point_segment_meters(
  {number(), number()},
  {number(), number()},
  {number(), number()}
) :: float()

Geodesic distance in metres from point p to the nearest point on the segment ab (each a {lng, lat} pair) — the true closest-point-on-segment distance, not closest-vertex.

The closest point is found by projecting p onto the segment in a local equirectangular frame (longitude scaled by cos(lat) so the projection isn't distorted by meridian convergence), then the distance to that point is measured with haversine_meters/2. Accurate for the short segments of fibre paths and admin-boundary edges; a degenerate segment (a == b) falls back to the distance to a.