defmodule Geometry do @moduledoc """ A set of geometry types for WKT/WKB and GeoJson. """ alias Geometry.{ Feature, FeatureCollection, GeoJson, GeometryCollection, GeometryCollectionM, GeometryCollectionZ, GeometryCollectionZM, LineString, LineStringM, LineStringZ, LineStringZM, MultiLineString, MultiLineStringM, MultiLineStringZ, MultiLineStringZM, MultiPoint, MultiPointM, MultiPointZ, MultiPointZM, MultiPolygon, MultiPolygonM, MultiPolygonZ, MultiPolygonZM, Point, PointM, PointZ, PointZM, Polygon, PolygonM, PolygonZ, PolygonZM } alias Geometry.{WKB, WKT} @geometries [ GeometryCollection, GeometryCollectionM, GeometryCollectionZ, GeometryCollectionZM, LineString, LineStringM, LineStringZ, LineStringZM, MultiLineString, MultiLineStringM, MultiLineStringZ, MultiLineStringZM, MultiPoint, MultiPointM, MultiPointZ, MultiPointZM, MultiPolygon, MultiPolygonM, MultiPolygonZ, MultiPolygonZM, Polygon, PolygonM, PolygonZ, PolygonZM, Point, PointM, PointZ, PointZM ] @geo_json [ Feature, FeatureCollection ] @typedoc """ A geometry is one of the provided geometries or geometry-collections. """ @type t :: GeometryCollection.t() | GeometryCollectionM.t() | GeometryCollectionZ.t() | GeometryCollectionZM.t() | LineString.t() | LineStringM.t() | LineStringZ.t() | LineStringZM.t() | MultiLineString.t() | MultiLineStringM.t() | MultiLineStringZ.t() | MultiLineStringZM.t() | MultiPoint.t() | MultiPointM.t() | MultiPointZ.t() | MultiPointZM.t() | MultiPolygon.t() | MultiPolygonM.t() | MultiPolygonZ.t() | MultiPolygonZM.t() | Polygon.t() | PolygonM.t() | PolygonZ.t() | PolygonZM.t() | Point.t() | PointM.t() | PointZ.t() | PointZM.t() @typedoc """ An n-dimensional coordinate. """ @type coordinate :: [number(), ...] @typedoc """ A list of n-dimensional coordinates. """ @type coordinates :: [coordinate()] @typedoc """ The Spatial Reference System Identifier to identify projected, unprojected, and local spatial coordinate system definitions. """ @type srid :: non_neg_integer() @typedoc """ [Well-known text](https://en.wikipedia.org/wiki/Well-known_text_representation_of_geometry) (WKT) is a text markup language for representing vector geometry objects. """ @type wkt :: String.t() @typedoc """ [Well-known binary](https://en.wikipedia.org/wiki/Well-known_text_representation_of_geometry#Well-known_binary) The binary representation of WKT. """ @type wkb :: binary() @typedoc """ Errors that can occur when a geometry is generating from WKT. """ @type wkt_error :: {:error, %{expected: t(), got: t()}} | { :error, message :: String.t(), rest :: String.t(), {line :: pos_integer(), offset :: non_neg_integer()}, offset :: non_neg_integer() } @typedoc """ Errors that can occur when a geometry is generating from WKT. """ @type wkb_error :: {:error, %{expected: t(), got: t()}} | {:error, message :: String.t(), rest :: binary(), offset :: non_neg_integer()} @typedoc """ A [GeoJson](https://geojson.org) term. """ @type geo_json_term :: map() @typedoc """ Errors that can occur when a geometry is generating from GeoJson. """ @type geo_json_error :: {:error, :coordinates_not_found | :geometries_not_found | :invalid_data | :type_not_found | :unknown_type} @typedoc """ Byte order. - `:ndr`: Little endian byte order encoding - `:xdr`: Big endian byte order encoding """ @type endian :: :ndr | :xdr @type mode :: :binary | :hex @doc """ Returns true if a geometry is empty. ## Examples iex> Geometry.empty?(Point.new(1, 2)) false iex> Geometry.empty?(Point.new()) true iex> Geometry.empty?(LineString.new([])) true """ @spec empty?(t()) :: boolean def empty?(%module{} = geometry) when module in @geometries or module in @geo_json do module.empty?(geometry) end @doc """ Returns the WKB representation of a geometry. With option `:srid` an EWKB representation with the SRID is returned. The option `:endian` indicates whether `:xdr` big endian or `:ndr` little endian is returned. The default is `:xdr`. The `:mode` determines whether a hex-string or binary is returned. The default is `:binary`. ## Examples iex> Geometry.to_wkb(PointZ.new(1, 2, 3), endian: :ndr, mode: :hex) "0101000080000000000000F03F00000000000000400000000000000840" iex> Geometry.to_wkb(Point.new(1, 2), srid: 4711) |> Hex.from_binary() "0020000001000012673FF00000000000004000000000000000" """ @spec to_wkb(t(), opts) :: String.t() when opts: [endian: endian(), srid: srid(), mode: mode()] def to_wkb(%module{} = geometry, opts \\ []) when module in @geometries do module.to_wkb(geometry, opts) end @doc """ Returns an `:ok` tuple with the geometry from the given WKT string. Otherwise returns an `:error` tuple. If WKB contains an SRID the tuple is extended by the id. The optional second argument determines if a `:hex`-string or a `:binary` input is expected. The default is `:binary`. ## Examples iex> Geometry.from_wkb("0101000080000000000000F03F00000000000000400000000000000840", :hex) {:ok, %PointZ{coordinate: [1.0, 2.0, 3.0]}} iex> Geometry.from_wkb("0020000001000012673FF00000000000004000000000000000", :hex) {:ok, {%Point{coordinate: [1.0, 2.0]}, 4711}} """ @spec from_wkb(wkb(), mode()) :: {:ok, t() | {t(), srid()}} | wkb_error def from_wkb(wkb, mode \\ :binary), do: WKB.Parser.parse(wkb, mode) @doc """ The same as `from_wkb/2`, but raises a `Geometry.Error` exception if it fails. """ @spec from_wkb!(wkb(), mode()) :: t() | {t(), srid()} def from_wkb!(wkb, mode \\ :binary) do case WKB.Parser.parse(wkb, mode) do {:ok, geometry} -> geometry error -> raise Geometry.Error, error end end @doc """ Returns the WKT representation of a geometry. An optional `:srid` can be set in the options. ## Examples iex> Geometry.to_wkt(Point.new(1, 2)) "Point (1 2)" iex> Geometry.to_wkt(PointZ.new(1.1, 2.2, 3.3), srid: 4211) "SRID=4211;Point Z (1.1 2.2 3.3)" iex> Geometry.to_wkt(LineString.new([Point.new(1, 2), Point.new(3, 4)])) "LineString (1 2, 3 4)" """ @spec to_wkt(t(), opts) :: String.t() when opts: [srid: srid()] def to_wkt(%module{} = geometry, opts \\ []) when module in @geometries do module.to_wkt(geometry, opts) end @doc """ Returns an `:ok` tuple with the geometry from the given WKT string. Otherwise returns an `:error` tuple. If the geometry contains a SRID the id is added to the tuple. ## Examples iex> Geometry.from_wkt("Point ZM (1 2 3 4)") {:ok, %PointZM{coordinate: [1, 2, 3, 4]}} iex> Geometry.from_wkt("SRID=42;Point (1.1 2.2)") {:ok, {%Point{coordinate: [1.1, 2.2]}, 42}} """ @spec from_wkt(wkt()) :: {:ok, t() | {t(), srid()}} | wkt_error def from_wkt(wkt), do: WKT.Parser.parse(wkt) @doc """ The same as `from_wkt/1`, but raises a `Geometry.Error` exception if it fails. """ @spec from_wkt!(wkt()) :: t() | {t(), srid()} def from_wkt!(wkt) do case WKT.Parser.parse(wkt) do {:ok, geometry} -> geometry error -> raise Geometry.Error, error end end @doc """ Returns the GeoJSON term representation of a geometry. ## Examples iex> Geometry.to_geo_json(PointZ.new(1.2, 3.4, 5.6)) %{"type" => "Point", "coordinates" => [1.2, 3.4, 5.6]} iex> Geometry.to_geo_json(LineString.new([Point.new(1, 2), Point.new(3, 4)])) %{"type" => "LineString", "coordinates" => [[1, 2], [3, 4]]} """ @spec to_geo_json(t() | Feature.t() | FeatureCollection.t()) :: geo_json_term def to_geo_json(%module{} = geometry) when module in @geometries or module in @geo_json do module.to_geo_json(geometry) end @doc """ Returns an `:ok` tuple with the geometry from the given GeoJSON term. Otherwise returns an `:error` tuple. The `:type` option specifies which type is expected. The possible values are `:z`, `:m`, and `:zm`. ## Examples iex> ~s({"type": "Point", "coordinates": [1, 2]}) iex> |> Jason.decode!() iex> |> Geometry.from_geo_json() {:ok, %Point{coordinate: [1, 2]}} iex> ~s({"type": "Point", "coordinates": [1, 2, 3, 4]}) iex> |> Jason.decode!() iex> |> Geometry.from_geo_json(type: :zm) {:ok, %PointZM{coordinate: [1, 2, 3, 4]}} """ @spec from_geo_json(geo_json_term(), opts) :: {:ok, t() | Feature.t() | FeatureCollection.t()} | geo_json_error when opts: [type: :z | :m | :zm] def from_geo_json(json, opts \\ []), do: GeoJson.to_geometry(json, opts) @doc """ The same as `from_geo_josn/1`, but raises a `Geometry.Error` exception if it fails. """ @spec from_geo_json!(geo_json_term(), opts) :: t() | Feature.t() | FeatureCollection.t() when opts: [type: :z | :m | :zm] def from_geo_json!(json, opts \\ []) do case GeoJson.to_geometry(json, opts) do {:ok, geometry} -> geometry error -> raise Geometry.Error, error end end @doc false @spec default_endian :: endian() def default_endian, do: :xdr @doc false @spec default_mode :: mode() def default_mode, do: :binary end