lib/geometry/multi_polygon_z.ex

defmodule Geometry.MultiPolygonZ do
  # This file is auto-generated by `mix geometry.gen`.
  # The ZM version of this file is used as a template.

  @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
  alias Geometry.MultiPolygonZ
  alias Geometry.PolygonZ
  alias Geometry.WKB
  alias Geometry.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() | {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
      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`.

  The `:mode` determines whether a hex-string or binary is returned. The default
  is `:binary`.

  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(), mode: Geometry.mode()]
  def to_wkb(%MultiPolygonZ{} = multi_polygon, opts \\ []) do
    endian = Keyword.get(opts, :endian, Geometry.default_endian())
    mode = Keyword.get(opts, :mode, Geometry.default_mode())
    srid = Keyword.get(opts, :srid)

    to_wkb(multi_polygon, srid, endian, mode)
  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/2` function.
  """
  @spec from_wkb(Geometry.wkb(), Geometry.mode()) ::
          {:ok, t() | {t(), Geometry.srid()}} | Geometry.wkb_error()
  def from_wkb(wkb, mode \\ :binary), do: WKB.to_geometry(wkb, mode, MultiPolygonZ)

  @doc """
  The same as `from_wkb/2`, but raises a `Geometry.Error` exception if it fails.
  """
  @spec from_wkb!(Geometry.wkb(), Geometry.mode()) :: t() | {t(), Geometry.srid()}
  def from_wkb!(wkb, mode \\ :binary) do
    case WKB.to_geometry(wkb, mode, MultiPolygonZ) do
      {:ok, geometry} -> geometry
      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 ->
        <<acc::binary, ", ", PolygonZ.to_wkt_rings(polygon)::binary>>
      end)::binary, ")">>
  end

  @doc false
  @compile {:inline, to_wkb: 4}
  @spec to_wkb(t(), srid, endian, mode) :: wkb
        when srid: Geometry.srid() | nil,
             endian: Geometry.endian(),
             mode: Geometry.mode(),
             wkb: Geometry.wkb()
  def to_wkb(%MultiPolygonZ{polygons: polygons}, srid, endian, mode) do
    <<
      WKB.byte_order(endian, mode)::binary,
      wkb_code(endian, not is_nil(srid), mode)::binary,
      WKB.srid(srid, endian, mode)::binary,
      to_wkb_polygons(MapSet.to_list(polygons), endian, mode)::binary
    >>
  end

  @compile {:inline, to_wkb_polygons: 3}
  defp to_wkb_polygons(polygons, endian, mode) do
    Enum.reduce(polygons, WKB.length(polygons, endian, mode), fn polygon, acc ->
      <<acc::binary, PolygonZ.to_wkb(polygon, nil, endian, mode)::binary>>
    end)
  end

  @compile {:inline, wkb_code: 3}
  defp wkb_code(endian, srid?, :hex) do
    case {endian, srid?} do
      {:xdr, false} -> "80000006"
      {:ndr, false} -> "06000080"
      {:xdr, true} -> "A0000006"
      {:ndr, true} -> "060000A0"
    end
  end

  defp wkb_code(endian, srid?, :binary) do
    case {endian, srid?} do
      {:xdr, false} -> <<0x80000006::big-integer-size(32)>>
      {:ndr, false} -> <<0x80000006::little-integer-size(32)>>
      {:xdr, true} -> <<0xA0000006::big-integer-size(32)>>
      {:ndr, true} -> <<0xA0000006::little-integer-size(32)>>
    end
  end

  defimpl Enumerable do
    def count(multi_polygon) do
      {:ok, MultiPolygonZ.size(multi_polygon)}
    end

    def member?(multi_polygon, val) do
      {:ok, MultiPolygonZ.member?(multi_polygon, val)}
    end

    if function_exported?(Enumerable.List, :slice, 4) do
      def slice(multi_polygon) do
        size = MultiPolygonZ.size(multi_polygon)

        {:ok, size, &Enumerable.List.slice(MultiPolygonZ.to_list(multi_polygon), &1, &2, size)}
      end
    else
      def slice(multi_polygon) do
        size = MultiPolygonZ.size(multi_polygon)

        {:ok, size, &MultiPolygonZ.to_list/1}
      end
    end

    def reduce(multi_polygon, acc, fun) do
      Enumerable.List.reduce(MultiPolygonZ.to_list(multi_polygon), acc, fun)
    end
  end

  defimpl Collectable do
    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