diff options
Diffstat (limited to 'lib/cepis/geo.ex')
| -rw-r--r-- | lib/cepis/geo.ex | 85 |
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 |