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