defmodule Geometry.MultiPointZM do @moduledoc """ A set of points from type `Geometry.PointZM`. `MultiPointZM` implements the protocols `Enumerable` and `Collectable`. ## Examples iex> Enum.map( ...> MultiPointZM.new([ ...> PointZM.new(1, 2, 3, 4), ...> PointZM.new(3, 4, 5, 6) ...> ]), ...> fn [x, _y, _z, _m] -> x end ...> ) [1, 3] iex> Enum.into([PointZM.new(1, 2, 3, 4)], MultiPointZM.new()) %MultiPointZM{ points: MapSet.new([ [1, 2, 3, 4] ]) } """ alias Geometry.GeoJson alias Geometry.MultiPointZM alias Geometry.PointZM alias Geometry.WKB alias Geometry.WKT defstruct points: MapSet.new() @type t :: %MultiPointZM{points: MapSet.t(Geometry.coordinate())} @doc """ Creates an empty `MultiPointZM`. ## Examples iex> MultiPointZM.new() %MultiPointZM{points: MapSet.new()} """ @spec new :: t() def new, do: %MultiPointZM{} @doc """ Creates a `MultiPointZM` from the given `Geometry.PointZM`s. ## Examples iex> MultiPointZM.new([ ...> PointZM.new(1, 2, 3, 4), ...> PointZM.new(1, 2, 3, 4), ...> PointZM.new(3, 4, 5, 6) ...> ]) %MultiPointZM{points: MapSet.new([ [1, 2, 3, 4], [3, 4, 5, 6] ])} iex> MultiPointZM.new([]) %MultiPointZM{points: MapSet.new()} """ @spec new([PointZM.t()]) :: t() def new([]), do: %MultiPointZM{} def new(points) do %MultiPointZM{points: Enum.into(points, MapSet.new(), fn point -> point.coordinate end)} end @doc """ Returns `true` if the given `MultiPointZM` is empty. ## Examples iex> MultiPointZM.empty?(MultiPointZM.new()) true iex> MultiPointZM.empty?( ...> MultiPointZM.new( ...> [PointZM.new(1, 2, 3, 4), PointZM.new(3, 4, 5, 6)] ...> ) ...> ) false """ @spec empty?(t()) :: boolean def empty?(%MultiPointZM{} = multi_point), do: Enum.empty?(multi_point.points) @doc """ Creates a `MultiPointZM` from the given coordinates. ## Examples iex> MultiPointZM.from_coordinates( ...> [[-1, 1, 1, 1], [-2, 2, 2, 2], [-3, 3, 3, 3]] ...> ) %MultiPointZM{ points: MapSet.new([ [-1, 1, 1, 1], [-2, 2, 2, 2], [-3, 3, 3, 3] ]) } iex> MultiPointZM.from_coordinates( ...> [[-1, 1, 1, 1], [-2, 2, 2, 2], [-3, 3, 3, 3]] ...> ) %MultiPointZM{ points: MapSet.new([ [-1, 1, 1, 1], [-2, 2, 2, 2], [-3, 3, 3, 3] ]) } """ @spec from_coordinates([Geometry.coordinate()]) :: t() def from_coordinates(coordinates), do: %MultiPointZM{points: MapSet.new(coordinates)} @doc """ Returns an `:ok` tuple with the `MultiPointZM` from the given GeoJSON term. Otherwise returns an `:error` tuple. ## Examples iex> ~s( ...> { ...> "type": "MultiPoint", ...> "coordinates": [ ...> [1.1, 1.2, 1.3, 1.4], ...> [20.1, 20.2, 20.3, 20.4] ...> ] ...> } ...> ) iex> |> Jason.decode!() iex> |> MultiPointZM.from_geo_json() {:ok, %MultiPointZM{points: MapSet.new([ [1.1, 1.2, 1.3, 1.4], [20.1, 20.2, 20.3, 20.4] ])}} """ @spec from_geo_json(Geometry.geo_json_term()) :: {:ok, t()} | Geometry.geo_json_error() def from_geo_json(json), do: GeoJson.to_multi_point(json, MultiPointZM) @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_point(json, MultiPointZM) do {:ok, geometry} -> geometry error -> raise Geometry.Error, error end end @doc """ Returns the GeoJSON term of a `MultiPointZM`. There are no guarantees about the order of points in the returned `coordinates`. ## Examples ```elixir MultiPointZM.to_geo_json( MultiPointZM.new([ PointZM.new(-1.1, -2.2, -3.3, -4.4), PointZM.new(1.1, 2.2, 3.3, 4.4) ]) ) # => # %{ # "type" => "MultiPoint", # "coordinates" => [ # [-1.1, -2.2, -3.3, -4.4], # [1.1, 2.2, 3.3, 4.4] # ] # } ``` """ @spec to_geo_json(t()) :: Geometry.geo_json_term() def to_geo_json(%MultiPointZM{points: points}) do %{ "type" => "MultiPoint", "coordinates" => MapSet.to_list(points) } end @doc """ Returns an `:ok` tuple with the `MultiPointZM` 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> MultiPointZM.from_wkt( ...> "MultiPoint ZM (-5.1 7.8 1.1 1, 0.1 0.2 2.2 2)" ...> ) {:ok, %MultiPointZM{ points: MapSet.new([ [-5.1, 7.8, 1.1, 1], [0.1, 0.2, 2.2, 2] ]) }} iex> MultiPointZM.from_wkt( ...> "SRID=7219;MultiPoint ZM (-5.1 7.8 1.1 1, 0.1 0.2 2.2 2)" ...> ) {:ok, { %MultiPointZM{ points: MapSet.new([ [-5.1, 7.8, 1.1, 1], [0.1, 0.2, 2.2, 2] ]) }, 7219 }} iex> MultiPointZM.from_wkt("MultiPoint ZM EMPTY") ...> {:ok, %MultiPointZM{}} """ @spec from_wkt(Geometry.wkt()) :: {:ok, t() | {t(), Geometry.srid()}} | Geometry.wkt_error() def from_wkt(wkt), do: WKT.to_geometry(wkt, MultiPointZM) @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, MultiPointZM) do {:ok, geometry} -> geometry error -> raise Geometry.Error, error end end @doc """ Returns the WKT representation for a `MultiPointZM`. With option `:srid` an EWKT representation with the SRID is returned. There are no guarantees about the order of points in the returned WKT-string. ## Examples ```elixir MultiPointZM.to_wkt(MultiPointZM.new()) # => "MultiPoint ZM EMPTY" MultiPointZM.to_wkt( MultiPointZM.new([ PointZM.new(7.1, 8.1, 1.1, 1), PointZM.new(9.2, 5.2, 2.2, 2) ] ) # => "MultiPoint ZM (7.1 8.1 1.1 1, 9.2 5.2 2.2 2)" MultiPointZM.to_wkt( MultiPointZM.new([ PointZM.new(7.1, 8.1, 1.1, 1), PointZM.new(9.2, 5.2, 2.2, 2) ]), srid: 123 ) # => "SRID=123;MultiPoint ZM (7.1 8.1 1.1 1, 9.2 5.2 2.2 2)" ``` """ @spec to_wkt(t(), opts) :: Geometry.wkt() when opts: [srid: Geometry.srid()] def to_wkt(%MultiPointZM{points: points}, opts \\ []) do WKT.to_ewkt( << "MultiPoint ZM ", points |> MapSet.to_list() |> to_wkt_points()::binary >>, opts ) end @doc """ Returns the WKB representation for a `MultiPointZM`. 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.PointZM.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(%MultiPointZM{} = multi_point, 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_point, srid, endian, mode) end @doc """ Returns an `:ok` tuple with the `MultiPointZM` 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.PointZM.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, MultiPointZM) @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, MultiPointZM) do {:ok, geometry} -> geometry error -> raise Geometry.Error, error end end @doc """ Returns the number of elements in `MultiPointZM`. ## Examples iex> MultiPointZM.size( ...> MultiPointZM.new([ ...> PointZM.new(11, 12, 13, 14), ...> PointZM.new(21, 22, 23, 24) ...> ]) ...> ) 2 """ @spec size(t()) :: non_neg_integer() def size(%MultiPointZM{points: points}), do: MapSet.size(points) @doc """ Checks if `MulitPointZM` contains `point`. ## Examples iex> MultiPointZM.member?( ...> MultiPointZM.new([ ...> PointZM.new(11, 12, 13, 14), ...> PointZM.new(21, 22, 23, 24) ...> ]), ...> PointZM.new(11, 12, 13, 14) ...> ) true iex> MultiPointZM.member?( ...> MultiPointZM.new([ ...> PointZM.new(11, 12, 13, 14), ...> PointZM.new(21, 22, 23, 24) ...> ]), ...> PointZM.new(1, 2, 3, 4) ...> ) false """ @spec member?(t(), PointZM.t()) :: boolean() def member?(%MultiPointZM{points: points}, %PointZM{coordinate: coordinate}), do: MapSet.member?(points, coordinate) @doc """ Converts `MultiPointZM` to a list. ## Examples iex> MultiPointZM.to_list( ...> MultiPointZM.new([ ...> PointZM.new(11, 12, 13, 14), ...> PointZM.new(21, 22, 23, 24) ...> ]) ...> ) [ [11, 12, 13, 14], [21, 22, 23, 24] ] """ @spec to_list(t()) :: [PointZM.t()] def to_list(%MultiPointZM{points: points}), do: MapSet.to_list(points) @compile {:inline, to_wkt_points: 1} defp to_wkt_points([]), do: "EMPTY" defp to_wkt_points([coordinate | points]) do <<"(", Enum.reduce(points, PointZM.to_wkt_coordinate(coordinate), fn coordinate, acc -> <> end)::binary, ")">> end @doc false @compile {:inline, to_wkb: 4} @spec to_wkb(t(), Geometry.srid(), Geometry.endian(), Geometry.mode()) :: Geometry.wkb() def to_wkb(%MultiPointZM{points: points}, 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_points(MapSet.to_list(points), endian, mode)::binary >> end @compile {:inline, to_wkb_points: 3} defp to_wkb_points(points, endian, mode) do Enum.reduce(points, WKB.length(points, endian, mode), fn point, acc -> <> end) end @compile {:inline, wkb_code: 3} defp wkb_code(endian, srid?, :hex) do case {endian, srid?} do {:xdr, false} -> "C0000004" {:ndr, false} -> "040000C0" {:xdr, true} -> "E0000004" {:ndr, true} -> "040000E0" end end defp wkb_code(endian, srid?, :binary) do case {endian, srid?} do {:xdr, false} -> <<0xC0000004::big-integer-size(32)>> {:ndr, false} -> <<0xC0000004::little-integer-size(32)>> {:xdr, true} -> <<0xE0000004::big-integer-size(32)>> {:ndr, true} -> <<0xE0000004::little-integer-size(32)>> end end defimpl Enumerable do def count(multi_point) do {:ok, MultiPointZM.size(multi_point)} end def member?(multi_point, val) do {:ok, MultiPointZM.member?(multi_point, val)} end if function_exported?(Enumerable.List, :slice, 4) do def slice(multi_point) do size = MultiPointZM.size(multi_point) {:ok, size, &Enumerable.List.slice(MultiPointZM.to_list(multi_point), &1, &2, size)} end else def slice(multi_point) do size = MultiPointZM.size(multi_point) {:ok, size, &MultiPointZM.to_list/1} end end def reduce(multi_point, acc, fun) do Enumerable.List.reduce(MultiPointZM.to_list(multi_point), acc, fun) end end defimpl Collectable do def into(%MultiPointZM{points: points}) do fun = fn list, {:cont, x} -> [{x, []} | list] list, :done -> new = Enum.into(list, %{}, fn {point, []} -> {point.coordinate, []} end) %MultiPointZM{points: %{points | map: Map.merge(points.map, Map.new(new))}} _list, :halt -> :ok end {[], fun} end end end