defmodule Geometry.MultiPolygonZ do @moduledoc """ A set of polygons from type `Geometry.PolygonZ` `MultiPointZ` implements the protocols `Enumerable` and `Collectable`. ## Examples iex> Enum.map( ...> MultiPolygonZ.new([ ...> PolygonZ.new([ ...> LineStringZ.new([ ...> PointZ.new(11, 12, 13), ...> PointZ.new(11, 22, 23), ...> PointZ.new(31, 22, 33), ...> PointZ.new(11, 12, 13) ...> ]), ...> ]), ...> PolygonZ.new([ ...> LineStringZ.new([ ...> PointZ.new(35, 10, 13), ...> PointZ.new(45, 45, 23), ...> PointZ.new(10, 20, 33), ...> PointZ.new(35, 10, 13) ...> ]), ...> LineStringZ.new([ ...> PointZ.new(20, 30, 13), ...> PointZ.new(35, 35, 23), ...> PointZ.new(30, 20, 33), ...> PointZ.new(20, 30, 13) ...> ]) ...> ]) ...> ]), ...> fn polygon -> length(polygon) == 1 end ...> ) [true, false] iex> Enum.into( ...> [ ...> PolygonZ.new([ ...> LineStringZ.new([ ...> PointZ.new(11, 12, 13), ...> PointZ.new(11, 22, 23), ...> PointZ.new(31, 22, 33), ...> PointZ.new(11, 12, 13) ...> ]) ...> ]) ...> ], ...> MultiPolygonZ.new()) %MultiPolygonZ{ polygons: MapSet.new([ [ [ [11, 12, 13], [11, 22, 23], [31, 22, 33], [11, 12, 13] ] ] ]) } """ alias Geometry.{GeoJson, MultiPolygonZ, PolygonZ, WKB, WKT} defstruct polygons: MapSet.new() @type t :: %MultiPolygonZ{polygons: MapSet.t([Geometry.coordinates()])} @doc """ Creates an empty `MultiPolygonZ`. ## Examples iex> MultiPolygonZ.new() %MultiPolygonZ{polygons: MapSet.new()} """ @spec new :: t() def new, do: %MultiPolygonZ{} @doc """ Creates a `MultiPolygonZ` from the given `Geometry.MultiPolygonZ`s. ## Examples iex> MultiPolygonZ.new([ ...> PolygonZ.new([ ...> LineStringZ.new([ ...> PointZ.new(6, 2, 3), ...> PointZ.new(8, 2, 4), ...> PointZ.new(8, 4, 5), ...> PointZ.new(6, 2, 3) ...> ]), ...> ]), ...> PolygonZ.new([ ...> LineStringZ.new([ ...> PointZ.new(1, 1, 3), ...> PointZ.new(9, 1, 4), ...> PointZ.new(9, 8, 5), ...> PointZ.new(1, 1, 3) ...> ]), ...> LineStringZ.new([ ...> PointZ.new(6, 2, 3), ...> PointZ.new(7, 2, 4), ...> PointZ.new(7, 3, 5), ...> PointZ.new(6, 2, 3) ...> ]) ...> ]) ...> ]) %MultiPolygonZ{ polygons: MapSet.new([ [ [[1, 1, 3], [9, 1, 4], [9, 8, 5], [1, 1, 3]], [[6, 2, 3], [7, 2, 4], [7, 3, 5], [6, 2, 3]] ], [[[6, 2, 3], [8, 2, 4], [8, 4, 5], [6, 2, 3]]] ]) } iex> MultiPolygonZ.new([]) %MultiPolygonZ{} """ @spec new([PolygonZ.t()]) :: t() def new([]), do: %MultiPolygonZ{} def new(polygons), do: %MultiPolygonZ{ polygons: Enum.into(polygons, MapSet.new(), fn polygon -> polygon.rings end) } @doc """ Returns `true` if the given `MultiPolygonZ` is empty. ## Examples iex> MultiPolygonZ.empty?(MultiPolygonZ.new()) true iex> MultiPolygonZ.empty?( ...> MultiPolygonZ.new([ ...> PolygonZ.new([ ...> LineStringZ.new([ ...> PointZ.new(1, 1, 3), ...> PointZ.new(1, 5, 4), ...> PointZ.new(5, 4, 2), ...> PointZ.new(1, 1, 3) ...> ]) ...> ]) ...> ]) ...> ) false """ @spec empty?(t()) :: boolean def empty?(%MultiPolygonZ{} = multi_polygon), do: Enum.empty?(multi_polygon.polygons) @doc """ Creates a `MultiPolygonZ` from the given coordinates. ## Examples iex> MultiPolygonZ.from_coordinates([ ...> [ ...> [[6, 2, 3], [8, 2, 4], [8, 4, 5], [6, 2, 3]] ...> ], [ ...> [[1, 1, 3], [9, 1, 4], [9, 8, 5], [1, 1, 3]], ...> [[6, 2, 4], [7, 2, 6], [7, 3, 3], [6, 2, 4]] ...> ] ...> ]) %MultiPolygonZ{ polygons: MapSet.new([ [ [[6, 2, 3], [8, 2, 4], [8, 4, 5], [6, 2, 3]], ], [ [[1, 1, 3], [9, 1, 4], [9, 8, 5], [1, 1, 3]], [[6, 2, 4], [7, 2, 6], [7, 3, 3], [6, 2, 4]] ] ]) } """ @spec from_coordinates([[Geometry.coordinates()]]) :: t() def from_coordinates(coordinates) do %MultiPolygonZ{ polygons: MapSet.new(coordinates) } end @doc """ Returns an `:ok` tuple with the `MultiPolygonZ` from the given GeoJSON term. Otherwise returns an `:error` tuple. ## Examples iex> ~s( ...> { ...> "type": "MultiPolygon", ...> "coordinates": [ ...> [ ...> [[6, 2, 3], [8, 2, 4], [8, 4, 5], [6, 2, 3]] ...> ], [ ...> [[1, 1, 3], [9, 1, 4], [9, 8, 5], [1, 1, 3]], ...> [[6, 2, 4], [7, 2, 6], [7, 3, 3], [6, 2, 4]] ...> ] ...> ] ...> } ...> ) ...> |> Jason.decode!() ...> |> MultiPolygonZ.from_geo_json() {:ok, %MultiPolygonZ{ polygons: MapSet.new([ [ [[1, 1, 3], [9, 1, 4], [9, 8, 5], [1, 1, 3]], [[6, 2, 4], [7, 2, 6], [7, 3, 3], [6, 2, 4]] ], [ [[6, 2, 3], [8, 2, 4], [8, 4, 5], [6, 2, 3]] ] ]) }} """ @spec from_geo_json(Geometry.geo_json_term()) :: {:ok, t()} | Geometry.geo_json_error() def from_geo_json(json), do: GeoJson.to_multi_polygon(json, MultiPolygonZ) @doc """ The same as `from_geo_json/1`, but raises a `Geometry.Error` exception if it fails. """ @spec from_geo_json!(Geometry.geo_json_term()) :: t() def from_geo_json!(json) do case GeoJson.to_multi_polygon(json, MultiPolygonZ) do {:ok, geometry} -> geometry error -> raise Geometry.Error, error end end @doc """ Returns the GeoJSON term of a `MultiPolygonZ`. There are no guarantees about the order of polygons in the returned `coordinates`. ## Examples ```elixir MultiPolygonZ.to_list( MultiPolygonZ.new([ PolygonZ.new([ LineStringZ.new([ PointZ.new(111, 112, 113), PointZ.new(111, 122, 123), PointZ.new(131, 122, 133), PointZ.new(111, 112, 113) ]) ]), PolygonZ.new([ LineStringZ.new([ PointZ.new(211, 212, 213), PointZ.new(211, 222, 223), PointZ.new(231, 222, 233), PointZ.new(211, 212, 213) ]) ]) ]) ) # => # %{ # "type" => "MultiPolygon", # "coordinates" => [ # [ # [ # [11, 12, 13], # [11, 22, 23], # [31, 22, 33], # [11, 12, 13] # ] # ], [ # [ # [21, 22, 23], # [21, 22, 23], # [21, 22, 23], # [21, 22, 23] # ] # ] # ] # } ``` """ @spec to_geo_json(t()) :: Geometry.geo_json_term() def to_geo_json(%MultiPolygonZ{polygons: polygons}) do %{ "type" => "MultiPolygon", "coordinates" => MapSet.to_list(polygons) } end @doc """ Returns an `:ok` tuple with the `MultiPolygonZ` 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> MultiPolygonZ.from_wkt(" ...> SRID=1234;MULTIPOLYGON Z ( ...> ( ...> (40 40 10, 20 45 20, 45 30 15, 40 40 10) ...> ), ( ...> (20 35 20, 10 30 10, 10 10 30, 30 5 10, 45 20 10, 20 35 20), ...> (30 20 10, 20 15 20, 20 25 15, 30 20 10) ...> ) ...> ) ...> ") {:ok, %MultiPolygonZ{ polygons: MapSet.new([ [ [ [20, 35, 20], [10, 30, 10], [10, 10, 30], [30, 5, 10], [45, 20, 10], [20, 35, 20] ], [ [30, 20, 10], [20, 15, 20], [20, 25, 15], [30, 20, 10] ] ], [ [ [40, 40, 10], [20, 45, 20], [45, 30, 15], [40, 40, 10] ] ] ]) }, 1234} iex> MultiPolygonZ.from_wkt("MultiPolygon Z EMPTY") {:ok, %MultiPolygonZ{}} """ @spec from_wkt(Geometry.wkt()) :: {:ok, t()} | {:ok, t(), Geometry.srid()} | Geometry.wkt_error() def from_wkt(wkt), do: WKT.to_geometry(wkt, MultiPolygonZ) @doc """ The same as `from_wkt/1`, but raises a `Geometry.Error` exception if it fails. """ @spec from_wkt!(Geometry.wkt()) :: t() | {t(), Geometry.srid()} def from_wkt!(wkt) do case WKT.to_geometry(wkt, MultiPolygonZ) do {:ok, geometry} -> geometry {:ok, geometry, srid} -> {geometry, srid} error -> raise Geometry.Error, error end end @doc """ Returns the WKT representation for a `MultiPolygonZ`. With option `:srid` an EWKT representation with the SRID is returned. There are no guarantees about the order of polygons in the returned WKT-string. ## Examples ```elixir MultiPolygonZ.to_wkt( MultiPolygonZ.new([ PolygonZ.new([ LineStrinZM.new([ PointZ.new(20, 35, 20), PointZ.new(10, 30, 10), PointZ.new(10, 10, 30), PointZ.new(30, 5, 10), PointZ.new(45, 20, 10), PointZ.new(20, 35, 20) ]), LineStringZ.new([ PointZ.new(30, 20, 10), PointZ.new(20, 15, 20), PointZ.new(20, 25, 15), PointZ.new(30, 20, 10) ]) ]), PolygonZ.new([ LineStringZ.new([ PointZ.new(40, 40, 10), PointZ.new(20, 45, 20), PointZ.new(45, 30, 15), PointZ.new(40, 40, 10) ]) ]) ]) ) # Returns a string without any \\n or extra spaces (formatted just for readability): # SRID=478;MultiPolygon Z ( # ( # (20 35 20, 10 30 10, 10 10 30, 30 5 10, 45 20 10, 20 35 20), # (30 20 10, 20 15 20, 20 25 15, 30 20 10) # ), ( # (40 40 10, 20 45 20, 45 30 15, 40 40 10) # ) # ) """ @spec to_wkt(t(), opts) :: Geometry.wkt() when opts: [srid: Geometry.srid()] def to_wkt(%MultiPolygonZ{polygons: polygons}, opts \\ []) do WKT.to_ewkt( << "MultiPolygon Z ", polygons |> MapSet.to_list() |> to_wkt_polygons()::binary() >>, opts ) end @doc """ Returns the WKB representation for a `MultiPolygonZ`. 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`. An example of a simpler geometry can be found in the description for the `Geometry.PointZ.to_wkb/1` function. """ @spec to_wkb(t(), opts) :: Geometry.wkb() when opts: [endian: Geometry.endian(), srid: Geometry.srid()] def to_wkb(%MultiPolygonZ{} = multi_polygon, opts \\ []) do endian = Keyword.get(opts, :endian, Geometry.default_endian()) srid = Keyword.get(opts, :srid) to_wkb(multi_polygon, srid, endian) end @doc """ Returns an `:ok` tuple with the `MultiPolygonZ` from the given WKB string. Otherwise returns an `:error` tuple. If the geometry contains a SRID the id is added to the tuple. An example of a simpler geometry can be found in the description for the `Geometry.PointZ.from_wkb/1` function. """ @spec from_wkb(Geometry.wkb()) :: {:ok, t()} | {:ok, t(), Geometry.srid()} | Geometry.wkb_error() def from_wkb(wkb), do: WKB.to_geometry(wkb, MultiPolygonZ) @doc """ The same as `from_wkb/1`, but raises a `Geometry.Error` exception if it fails. """ @spec from_wkb!(Geometry.wkb()) :: t() | {t(), Geometry.srid()} def from_wkb!(wkb) do case WKB.to_geometry(wkb, MultiPolygonZ) do {:ok, geometry} -> geometry {:ok, geometry, srid} -> {geometry, srid} error -> raise Geometry.Error, error end end @doc """ Returns the number of elements in `MultiPolygonZ`. ## Examples iex> MultiPolygonZ.size( ...> MultiPolygonZ.new([ ...> PolygonZ.new([ ...> LineStringZ.new([ ...> PointZ.new(11, 12, 13), ...> PointZ.new(11, 22, 23), ...> PointZ.new(31, 22, 33), ...> PointZ.new(11, 12, 13) ...> ]) ...> ]) ...> ]) ...> ) 1 """ @spec size(t()) :: non_neg_integer() def size(%MultiPolygonZ{polygons: polygons}), do: MapSet.size(polygons) @doc """ Checks if `MultiPolygonZ` contains `point`. ## Examples iex> MultiPolygonZ.member?( ...> MultiPolygonZ.new([ ...> PolygonZ.new([ ...> LineStringZ.new([ ...> PointZ.new(11, 12, 13), ...> PointZ.new(11, 22, 23), ...> PointZ.new(31, 22, 33), ...> PointZ.new(11, 12, 13) ...> ]) ...> ]) ...> ]), ...> PolygonZ.new([ ...> LineStringZ.new([ ...> PointZ.new(11, 12, 13), ...> PointZ.new(11, 22, 23), ...> PointZ.new(31, 22, 33), ...> PointZ.new(11, 12, 13) ...> ]) ...> ]) ...> ) true iex> MultiPolygonZ.member?( ...> MultiPolygonZ.new([ ...> PolygonZ.new([ ...> LineStringZ.new([ ...> PointZ.new(11, 12, 13), ...> PointZ.new(11, 22, 23), ...> PointZ.new(31, 22, 33), ...> PointZ.new(11, 12, 13) ...> ]) ...> ]) ...> ]), ...> PolygonZ.new([ ...> LineStringZ.new([ ...> PointZ.new(11, 12, 13), ...> PointZ.new(11, 22, 23), ...> PointZ.new(33, 22, 33), ...> PointZ.new(11, 12, 13) ...> ]) ...> ]) ...> ) false """ @spec member?(t(), PolygonZ.t()) :: boolean() def member?(%MultiPolygonZ{polygons: polygons}, %PolygonZ{rings: rings}), do: MapSet.member?(polygons, rings) @doc """ Converts `MultiPolygonZ` to a list. ## Examples iex> MultiPolygonZ.to_list( ...> MultiPolygonZ.new([ ...> PolygonZ.new([ ...> LineStringZ.new([ ...> PointZ.new(11, 12, 13), ...> PointZ.new(11, 22, 23), ...> PointZ.new(31, 22, 33), ...> PointZ.new(11, 12, 13) ...> ]) ...> ]) ...> ]) ...> ) [ [ [ [11, 12, 13], [11, 22, 23], [31, 22, 33], [11, 12, 13] ] ] ] """ @spec to_list(t()) :: [PolygonZ.t()] def to_list(%MultiPolygonZ{polygons: polygons}), do: MapSet.to_list(polygons) @compile {:inline, to_wkt_polygons: 1} defp to_wkt_polygons([]), do: "EMPTY" defp to_wkt_polygons([polygon | polygons]) do <<"(", Enum.reduce(polygons, PolygonZ.to_wkt_rings(polygon), fn polygon, acc -> <> end)::binary(), ")">> end @doc false @compile {:inline, to_wkb: 3} @spec to_wkb(t(), Geometry.srid() | nil, Geometry.endian()) :: Geometry.wkb() def to_wkb(%MultiPolygonZ{polygons: polygons}, srid, endian) do << WKB.byte_order(endian)::binary(), wkb_code(endian, not is_nil(srid))::binary(), WKB.srid(srid, endian)::binary(), to_wkb_polygons(MapSet.to_list(polygons), endian)::binary() >> end @compile {:inline, to_wkb_polygons: 2} defp to_wkb_polygons(polygons, endian) do Enum.reduce(polygons, WKB.length(polygons, endian), fn polygon, acc -> <> end) end @compile {:inline, wkb_code: 2} defp wkb_code(endian, srid?) do case {endian, srid?} do {:xdr, false} -> "80000006" {:ndr, false} -> "06000080" {:xdr, true} -> "A0000006" {:ndr, true} -> "060000A0" end end defimpl Enumerable do # credo:disable-for-next-line Credo.Check.Readability.Specs def count(multi_polygon) do {:ok, MultiPolygonZ.size(multi_polygon)} end # credo:disable-for-next-line Credo.Check.Readability.Specs def member?(multi_polygon, val) do {:ok, MultiPolygonZ.member?(multi_polygon, val)} end # credo:disable-for-next-line Credo.Check.Readability.Specs def slice(multi_polygon) do size = MultiPolygonZ.size(multi_polygon) {:ok, size, &Enumerable.List.slice(MultiPolygonZ.to_list(multi_polygon), &1, &2, size)} end # credo:disable-for-next-line Credo.Check.Readability.Specs def reduce(multi_polygon, acc, fun) do Enumerable.List.reduce(MultiPolygonZ.to_list(multi_polygon), acc, fun) end end defimpl Collectable do # credo:disable-for-next-line Credo.Check.Readability.Specs def into(%MultiPolygonZ{polygons: polygons}) do fun = fn list, {:cont, x} -> [{x, []} | list] list, :done -> map = Map.merge( polygons.map, Enum.into(list, %{}, fn {polygon, []} -> {polygon.rings, []} end) ) %MultiPolygonZ{polygons: %{polygons | map: map}} _list, :halt -> :ok end {[], fun} end end end