lib/geo/json/decoder.ex

defmodule Geo.JSON.Decoder do
  @moduledoc false

  alias Geo.{
    Point,
    PointZ,
    LineString,
    LineStringZ,
    LineStringZM,
    Polygon,
    MultiPoint,
    MultiLineString,
    MultiLineStringZ,
    MultiPolygon,
    MultiPolygonZ,
    GeometryCollection
  }

  defmodule DecodeError do
    @type t :: %__MODULE__{message: String.t(), value: any}

    defexception [:message, :value]

    def message(%{message: nil, value: value}) do
      "unable to decode value: #{inspect(value)}"
    end

    def message(%{message: message}) do
      message
    end
  end

  @doc """
  Takes a map representing GeoJSON and returns a Geometry.

  Feature objects with null geometries will be stripped from a FeatureCollection,
  and a standalone Feature object with null geometry will be decoded to nil.
  """
  @spec decode!(map()) :: Geo.geometry() | nil
  def decode!(geo_json) do
    cond do
      Map.has_key?(geo_json, "geometries") ->
        crs = Map.get(geo_json, "crs")

        geometries =
          Enum.map(Map.get(geo_json, "geometries"), fn x ->
            do_decode(
              Map.get(x, "type"),
              Map.get(x, "coordinates"),
              Map.get(x, "properties", %{}),
              crs
            )
          end)

        %GeometryCollection{
          geometries: geometries,
          properties: Map.get(geo_json, "properties", %{})
        }

      Map.has_key?(geo_json, "coordinates") ->
        crs = Map.get(geo_json, "crs")

        do_decode(
          Map.get(geo_json, "type"),
          Map.get(geo_json, "coordinates"),
          Map.get(geo_json, "properties", %{}),
          crs
        )

      Map.get(geo_json, "type") == "Feature" ->
        do_decode(
          "Feature",
          Map.get(geo_json, "geometry"),
          Map.get(geo_json, "properties", %{}),
          Map.get(geo_json, "id", "")
        )

      Map.get(geo_json, "type") == "FeatureCollection" ->
        geometries =
          Map.get(geo_json, "features")
          |> Enum.map(fn x ->
            do_decode(
              Map.get(x, "type"),
              Map.get(x, "geometry"),
              Map.get(x, "properties", %{}),
              Map.get(x, "id", "")
            )
          end)
          |> Enum.reject(&is_nil/1)

        %GeometryCollection{
          geometries: geometries,
          properties: %{}
        }

      true ->
        raise DecodeError, value: geo_json
    end
  end

  @doc """
  Takes a map representing GeoJSON and returns a Geometry.
  """
  @spec decode(map()) :: {:ok, Geo.geometry() | nil} | {:error, DecodeError.t()}
  def decode(geo_json) do
    {:ok, decode!(geo_json)}
  rescue
    exception in [DecodeError] ->
      {:error, exception}
  end

  defp do_decode("Point", [x, y, z], properties, crs) do
    %PointZ{coordinates: {x, y, z}, srid: get_srid(crs), properties: properties}
  end

  defp do_decode("Point", [x, y], properties, crs) do
    %Point{coordinates: {x, y}, srid: get_srid(crs), properties: properties}
  end

  defp do_decode("Point", [], properties, crs) do
    %Point{coordinates: nil, srid: get_srid(crs), properties: properties}
  end

  defp do_decode("LineString", coordinates, properties, crs) do
    coordinates = Enum.map(coordinates, &list_to_tuple(&1))

    %LineString{coordinates: coordinates, srid: get_srid(crs), properties: properties}
  end

  defp do_decode("LineStringZ", coordinates, properties, crs) do
    coordinates = Enum.map(coordinates, &List.to_tuple(&1))

    %LineStringZ{coordinates: coordinates, srid: get_srid(crs), properties: properties}
  end

  defp do_decode("LineStringZM", coordinates, properties, crs) do
    coordinates = Enum.map(coordinates, &List.to_tuple(&1))

    %LineStringZM{coordinates: coordinates, srid: get_srid(crs), properties: properties}
  end

  defp do_decode("Polygon", coordinates, properties, crs) do
    coordinates =
      Enum.map(coordinates, fn sub_coordinates ->
        Enum.map(sub_coordinates, &list_to_tuple(&1))
      end)

    %Polygon{coordinates: coordinates, srid: get_srid(crs), properties: properties}
  end

  defp do_decode("MultiPoint", coordinates, properties, crs) do
    coordinates = Enum.map(coordinates, &list_to_tuple(&1))

    %MultiPoint{coordinates: coordinates, srid: get_srid(crs), properties: properties}
  end

  defp do_decode("MultiLineString", coordinates, properties, crs) do
    coordinates =
      Enum.map(coordinates, fn sub_coordinates ->
        Enum.map(sub_coordinates, &list_to_tuple(&1))
      end)

    %MultiLineString{coordinates: coordinates, srid: get_srid(crs), properties: properties}
  end

  defp do_decode("MultiLineStringZ", coordinates, properties, crs) do
    coordinates =
      Enum.map(coordinates, fn sub_coordinates ->
        Enum.map(sub_coordinates, &List.to_tuple(&1))
      end)

    %MultiLineStringZ{coordinates: coordinates, srid: get_srid(crs), properties: properties}
  end

  defp do_decode("MultiPolygon", coordinates, properties, crs) do
    coordinates =
      Enum.map(coordinates, fn sub_coordinates ->
        Enum.map(sub_coordinates, fn third_sub_coordinates ->
          Enum.map(third_sub_coordinates, &list_to_tuple(&1))
        end)
      end)

    %MultiPolygon{coordinates: coordinates, srid: get_srid(crs), properties: properties}
  end

  defp do_decode("MultiPolygonZ", coordinates, properties, crs) do
    coordinates =
      Enum.map(coordinates, fn sub_coordinates ->
        Enum.map(sub_coordinates, fn third_sub_coordinates ->
          Enum.map(third_sub_coordinates, &list_to_tuple(&1))
        end)
      end)

    %MultiPolygonZ{coordinates: coordinates, srid: get_srid(crs), properties: properties}
  end

  defp do_decode("Feature", nil, _properties, _id), do: nil

  defp do_decode("Feature", geometry, properties, _id) do
    do_decode(Map.get(geometry, "type"), Map.get(geometry, "coordinates"), properties, nil)
  end

  defp do_decode(type, [x, y, _z], properties, crs) do
    do_decode(type, [x, y], properties, crs)
  end

  defp do_decode(type, _, _, _) do
    raise DecodeError, message: "#{type} is not a valid type"
  end

  defp list_to_tuple([x, y | _]), do: {x, y}

  defp get_srid(%{"type" => "name", "properties" => %{"name" => "EPSG:" <> srid}}) do
    {srid, _} = Integer.parse(srid)
    srid
  end

  defp get_srid(%{"type" => "name", "properties" => %{"name" => srid}}) do
    srid
  end

  defp get_srid(nil) do
    nil
  end
end