Geo.Turf

CIhex.pm

Spatial analysis for Elixir, ported from TurfJS. Operates on Geo structs using WGS84 coordinates.

Requirements

GeoTurf supports Elixir 1.15 and later. CI verifies the minimum supported combination of Elixir 1.15 / OTP 26 and the current Elixir 1.18 / OTP 28 combination.

Installation

Add geo_turf to the dependencies in mix.exs:

def deps do
[
{:geo_turf, "~> 0.5.0"}
]
end

WGS84 and SRID metadata

GeoTurf's geodesic calculations require WGS84 longitude/latitude coordinates (EPSG:4326). They accept Geo structs with srid: 4326 or srid: nil.

An SRID of nil does not mean GeoTurf detected WGS84; it means the caller is asserting that the coordinates are WGS84. This supports Geo's default structs, while explicitly declared incompatible SRIDs are rejected. Collections are checked recursively.

Normally, geometries in another coordinate reference system must be reprojected before use. If the coordinates are already WGS84 and only the metadata is stale, correct that geometry explicitly:

geometry = %{geometry | srid: 4326}

There is deliberately no global validation bypass. Disabling the guard could allow projected coordinates to produce plausible but incorrect distances, areas, or predicates.

Usage

All functions accept and return standard Geo structs.

Geo.Turf.Measure

Measurements and geometry queries.

point = %Geo.Point{coordinates: {-75.343, 39.984}}
other = %Geo.Point{coordinates: {-75.534, 39.123}}
Geo.Turf.Measure.distance(point, other, :kilometers) # => 97.12922118967835
Geo.Turf.Measure.bearing(point, other) # => -170.23
Geo.Turf.Measure.close_to(point, other, 100, :kilometers) # => true
Geo.Turf.Measure.destination(point, 50, 90, units: :kilometers) # => %Geo.Point{...}
route = %Geo.LineString{coordinates: [{-23.621, 64.769}, {-23.629, 64.766}, {-23.638, 64.766}]}
Geo.Turf.Measure.length_of(route, :kilometers) # => 0.93
Geo.Turf.Measure.along(route, 0.5, :kilometers) # => %Geo.Point{...}
polygon = %Geo.Polygon{coordinates: [[{125, -15}, {113, -22}, {154, -27}, {144, -15}, {125, -15}]]}
Geo.Turf.Measure.area(polygon) # => 3332484969239.27 (m²)
Geo.Turf.Measure.center(polygon) # => %Geo.Point{...} (bbox centre)
Geo.Turf.Measure.centroid(polygon) # => %Geo.Point{...} (mean of vertices)

length_of/2 is GeoTurf's canonical length interface; its name deliberately avoids ambiguity with Elixir's Kernel.length/1.

Geo.Turf.Classification

Spatial predicates and search.

poly = %Geo.Polygon{coordinates: [[{0, 0}, {0, 10}, {10, 10}, {10, 0}, {0, 0}]]}
Geo.Turf.Classification.point_in_polygon?(%Geo.Point{coordinates: {5, 5}}, poly) # => true
Geo.Turf.Classification.point_in_polygon?(%Geo.Point{coordinates: {15, 5}}, poly) # => false
points = [%Geo.Point{coordinates: {5, 5}}, %Geo.Point{coordinates: {15, 5}}]
Geo.Turf.Classification.points_within_polygon(points, poly) # => [%Geo.Point{coordinates: {5, 5}}]
target = %Geo.Point{coordinates: {0, 0}}
Geo.Turf.Classification.nearest_point(target, points) # => %Geo.Point{coordinates: {5, 5}}

Geo.Turf.Transformation

Geometry construction and transformation.

point = %Geo.Point{coordinates: {-75.343, 39.984}}
Geo.Turf.Transformation.circle(point, 10, units: :kilometers) # => %Geo.Polygon{...}

Geo.Turf.Helpers

Bounding box utilities.

polygon = %Geo.Polygon{coordinates: [[{0, 0}, {0, 10}, {10, 10}, {10, 0}, {0, 0}]]}
Geo.Turf.Helpers.bbox(polygon) # => {0, 0, 10, 10}
Geo.Turf.Helpers.bbox_polygon({0, 0, 10, 10}) # => %Geo.Polygon{...}
# Compose them:
polygon |> Geo.Turf.Helpers.bbox() |> Geo.Turf.Helpers.bbox_polygon() # => %Geo.Polygon{...}

See the full API docs for all available functions.

Works with geo_postgis

If you query a PostGIS database via geo_postgis, the structs it returns work directly with GeoTurf — no conversion needed:

# Ecto query returns %Geo.Point{} and %Geo.Polygon{} fields automatically.
# Pass them straight into GeoTurf:
locations
|> Enum.filter(&Geo.Turf.Measure.close_to(&1.geom, origin, 50, :kilometers))
|> Enum.sort_by(&Geo.Turf.Measure.distance(&1.geom, origin))

Suggestions

Missing a function from TurfJS? Open an issue — a test case alongside it is always welcome.