summaryrefslogtreecommitdiffstats
path: root/lib/cepis/geo.ex
blob: 047d87cc0258222835b207b2cd8d6e5559c38a01 (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
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