summaryrefslogtreecommitdiffstats
path: root/lib/cepis/geo.ex
diff options
context:
space:
mode:
Diffstat (limited to 'lib/cepis/geo.ex')
-rw-r--r--lib/cepis/geo.ex85
1 files changed, 85 insertions, 0 deletions
diff --git a/lib/cepis/geo.ex b/lib/cepis/geo.ex
new file mode 100644
index 0000000..047d87c
--- /dev/null
+++ b/lib/cepis/geo.ex
@@ -0,0 +1,85 @@
+defmodule Cepis.Geo do
+ @moduledoc """
+ The CEP's coordinate, when the database has one.
+
+ The point is the **median** of the coordinates of that CEP's addresses, not
+ the mean: one badly geocoded address 40 km away shifts a mean and does not
+ move a median.
+
+ Always check `precisao` before using `lat`/`lon` for distance:
+
+ - `"logradouro"`: the point is a stretch of street. `raio_m` lands around
+ 100–150 m. Good for maps and for distance.
+ - `"localidade"`: the CEP *is* the whole municipality (a city-wide general
+ CEP), and the point is its centroid. `raio_m` runs from 7 km to 40 km.
+ Computing shipping from this is wrong by tens of kilometres.
+
+ `raio_m` is the radius containing 90% of that CEP's addresses. It exists
+ precisely so that error cannot pass unnoticed.
+ """
+
+ @enforce_keys [:lat, :lon, :raio_m, :precisao]
+ defstruct [:lat, :lon, :raio_m, :precisao]
+
+ @type t :: %__MODULE__{
+ lat: float(),
+ lon: float(),
+ raio_m: integer(),
+ precisao: String.t()
+ }
+
+ @doc """
+ `true` when the coordinate points at a street, not a municipality.
+
+ Shorthand for `precisao == "logradouro"`, which is the check to make before
+ using the point to measure distance.
+
+ if geo && Cepis.Geo.precise?(geo) do
+ shipping_from(geo.lat, geo.lon)
+ else
+ shipping_from_municipality(address.ibge)
+ end
+ """
+ @spec precise?(t()) :: boolean()
+ def precise?(%__MODULE__{precisao: precisao}), do: precisao == "logradouro"
+
+ @doc false
+ @spec from_map(term()) :: t() | nil
+ def from_map(%{"lat" => lat, "lon" => lon, "raio_m" => raio_m, "precisao" => precisao})
+ when is_binary(precisao) do
+ %__MODULE__{
+ lat: to_float(lat),
+ lon: to_float(lon),
+ raio_m: to_integer(raio_m),
+ precisao: precisao
+ }
+ rescue
+ ArgumentError -> nil
+ end
+
+ def from_map(_), do: nil
+
+ defp to_float(value) when is_float(value), do: value
+ defp to_float(value) when is_integer(value), do: value * 1.0
+
+ defp to_float(value) when is_binary(value) do
+ case Float.parse(value) do
+ {number, ""} -> number
+ _ -> raise ArgumentError, "not a number"
+ end
+ end
+
+ defp to_float(_), do: raise(ArgumentError, "not a number")
+
+ defp to_integer(value) when is_integer(value), do: value
+ defp to_integer(value) when is_float(value), do: trunc(value)
+
+ defp to_integer(value) when is_binary(value) do
+ case Integer.parse(value) do
+ {number, ""} -> number
+ _ -> raise ArgumentError, "not an integer"
+ end
+ end
+
+ defp to_integer(_), do: raise(ArgumentError, "not an integer")
+end