Geo.Turf
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.