Packages

Elixir bindings to https://gdal.org/en/stable/ via https://github.com/georust/gdal and https://github.com/rusterlium/rustler

Current section

Files

Jump to
ex_gdal lib ex_gdal.ex
Raw

lib/ex_gdal.ex

defmodule ExGdal do
@moduledoc """
Elixir bindings for GDAL via Rustler NIF.
Provides read access to raster datasets (GeoTIFF, etc.) through the GDAL library.
"""
alias ExGdal.{Dataset, GeoTransform, Native}
@doc """
Opens a raster dataset at the given path.
Returns `{:ok, %Dataset{}}` with cached metadata, or `{:error, reason}`.
"""
@spec open(String.t()) :: {:ok, Dataset.t()} | {:error, String.t()}
def open(path) do
path = Path.expand(path)
with {:ok, ref} <- Native.gdal_open(path),
{:ok, count} <- Native.gdal_raster_count(ref),
{:ok, size} <- Native.gdal_raster_size(ref),
{:ok, driver} <- Native.gdal_driver_name(ref) do
{:ok,
%Dataset{
ref: ref,
path: path,
raster_count: count,
raster_size: size,
driver: driver
}}
end
end
@doc "Returns the number of raster bands."
@spec band_count(Dataset.t()) :: {:ok, non_neg_integer()} | {:error, String.t()}
def band_count(%Dataset{raster_count: count}), do: {:ok, count}
@doc "Returns `{width, height}` in pixels."
@spec raster_size(Dataset.t()) :: {:ok, {non_neg_integer(), non_neg_integer()}} | {:error, String.t()}
def raster_size(%Dataset{raster_size: size}), do: {:ok, size}
@doc """
Reads an entire band as a raw binary of native-endian pixels.
Band index is 1-based.
"""
@spec read_band(Dataset.t(), pos_integer()) :: {:ok, binary()} | {:error, String.t()}
def read_band(%Dataset{ref: ref}, band_idx) do
Native.gdal_read_band(ref, band_idx)
end
@doc """
Reads a rectangular window from a band as raw bytes.
Band index is 1-based. (x, y) is the top-left corner, (w, h) is the window size.
"""
@spec read_band_window(Dataset.t(), pos_integer(), integer(), integer(), non_neg_integer(), non_neg_integer()) ::
{:ok, binary()} | {:error, String.t()}
def read_band_window(%Dataset{ref: ref}, band_idx, x, y, w, h) do
Native.gdal_read_band_window(ref, band_idx, x, y, w, h)
end
@doc """
Returns the data type of a band as an atom.
Possible values: `:uint8`, `:int16`, `:uint16`, `:int32`, `:uint32`,
`:float32`, `:float64`, `:unknown`.
"""
@spec band_type(Dataset.t(), pos_integer()) :: {:ok, atom()} | {:error, String.t()}
def band_type(%Dataset{ref: ref}, band_idx) do
Native.gdal_band_type(ref, band_idx)
end
@doc "Returns the no-data value for a band, or `{:ok, nil}` if not set."
@spec no_data_value(Dataset.t(), pos_integer()) :: {:ok, float() | nil} | {:error, String.t()}
def no_data_value(%Dataset{ref: ref}, band_idx) do
Native.gdal_no_data_value(ref, band_idx)
end
@doc "Returns the spatial reference as WKT."
@spec spatial_ref_wkt(Dataset.t()) :: {:ok, String.t()} | {:error, String.t()}
def spatial_ref_wkt(%Dataset{ref: ref}) do
Native.gdal_spatial_ref_wkt(ref)
end
@doc "Returns the spatial reference as PROJ4 string."
@spec spatial_ref_proj4(Dataset.t()) :: {:ok, String.t()} | {:error, String.t()}
def spatial_ref_proj4(%Dataset{ref: ref}) do
Native.gdal_spatial_ref_proj4(ref)
end
@doc "Returns the geo-transform as a `%GeoTransform{}` struct."
@spec geo_transform(Dataset.t()) :: {:ok, GeoTransform.t()} | {:error, String.t()}
def geo_transform(%Dataset{ref: ref}) do
case Native.gdal_geo_transform(ref) do
{:ok, list} when is_list(list) and length(list) == 6 ->
{:ok, GeoTransform.from_list(list)}
{:error, _} = err ->
err
end
end
@doc """
Reads a metadata item. Domain defaults to `""` (the default domain).
"""
@spec metadata_item(Dataset.t(), String.t(), String.t()) :: {:ok, String.t() | nil} | {:error, String.t()}
def metadata_item(%Dataset{ref: ref}, key, domain \\ "") do
Native.gdal_metadata_item(ref, key, domain)
end
@doc """
Lists all metadata domain names present on the dataset.
The default domain is represented by `""`.
"""
@spec metadata_domains(Dataset.t()) :: {:ok, [String.t()]} | {:error, String.t()}
def metadata_domains(%Dataset{ref: ref}) do
Native.gdal_metadata_domains(ref)
end
@doc """
Returns all metadata entries for a domain as `"Key=Value"` strings.
Returns `{:ok, nil}` if the domain does not exist.
"""
@spec metadata_domain(Dataset.t(), String.t()) :: {:ok, [String.t()] | nil} | {:error, String.t()}
def metadata_domain(%Dataset{ref: ref}, domain \\ "") do
Native.gdal_metadata_domain(ref, domain)
end
@doc """
Returns the description string for a band (1-based index).
For concentration rasters produced by PlumeFutures, this is
the ISO 8601 timestamp of the time step.
"""
@spec band_description(Dataset.t(), pos_integer()) :: {:ok, String.t()} | {:error, String.t()}
def band_description(%Dataset{ref: ref}, band_idx) do
Native.gdal_band_description(ref, band_idx)
end
@doc """
Returns all band descriptions as a list of strings.
"""
@spec band_descriptions(Dataset.t()) :: {:ok, [String.t()]} | {:error, String.t()}
def band_descriptions(%Dataset{raster_count: count, ref: ref}) do
results =
Enum.reduce_while(1..count, [], fn i, acc ->
case Native.gdal_band_description(ref, i) do
{:ok, desc} -> {:cont, [desc | acc]}
{:error, _} = err -> {:halt, err}
end
end)
case results do
{:error, _} = err -> err
list when is_list(list) -> {:ok, Enum.reverse(list)}
end
end
@doc "Returns the short driver name (e.g. `\"GTiff\"`)."
@spec driver_name(Dataset.t()) :: {:ok, String.t()} | {:error, String.t()}
def driver_name(%Dataset{driver: driver}), do: {:ok, driver}
@doc """
Extracts contour geometries from a raster band.
Returns a list of contour features, each containing a WKB geometry binary
and elevation metadata. Use either `:interval` for regularly-spaced contours
or `:levels` for specific threshold values.
## Options
* `:interval` - contour interval (mutually exclusive with `:levels`)
* `:base` - base contour level (default `0.0`, used with `:interval`)
* `:levels` - explicit list of contour levels as floats
* `:polygonize` - if `true`, produce filled polygons instead of linestrings
(default `true`)
* `:nodata` - override the band's nodata value
## Examples
# Fixed threshold levels
{:ok, features} = ExGdal.contours(ds, 1,
levels: [0.005, 0.05, 0.5],
polygonize: true
)
# Regular interval contours
{:ok, features} = ExGdal.contours(ds, 1, interval: 10.0, base: 0.0)
Each feature is a map with keys:
* `:id` - integer feature ID
* `:level` - contour elevation
* `:level_min` - lower bound of contour band (polygon mode)
* `:level_max` - upper bound of contour band (polygon mode)
* `:wkb` - geometry as WKB binary
"""
@spec contours(Dataset.t(), pos_integer(), keyword()) ::
{:ok, [map()]} | {:error, String.t()}
def contours(%Dataset{ref: ref}, band_idx, opts \\ []) do
interval = Keyword.get(opts, :interval)
base = Keyword.get(opts, :base, 0.0)
levels = Keyword.get(opts, :levels, [])
polygonize = Keyword.get(opts, :polygonize, true)
nodata = Keyword.get(opts, :nodata)
Native.gdal_contours(ref, band_idx, interval, base, levels, polygonize, nodata)
end
end