-
Notifications
You must be signed in to change notification settings - Fork 1
Expand file tree
/
Copy pathopenlocationcode.ex
More file actions
211 lines (169 loc) · 6.36 KB
/
openlocationcode.ex
File metadata and controls
211 lines (169 loc) · 6.36 KB
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
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
defmodule OpenLocationCode do
# Kernel's function causes a clash with autodidact's function defination
import Kernel, except: [floor: 1]
@pair_code_length 10
@separator "+"
@separator_position 8
@padding "0"
@latitude_max 90
@longitude_max 180
@code_alphabet "23456789CFGHJMPQRVWX"
# The resolution values in degrees for each position in the lat/lng pair
# encoding. These give the place value of each position, and therefore the
# dimensions of the resulting area.
@pair_resolutions [20.0, 1.0, 0.05, 0.0025, 0.000125]
@moduledoc """
Open Location Code (OLC) is a geocoding system for identifying an area anywhere on planet Earth. Originally developed in
2014, OLCs are also called "plus codes". Nearby locations have similar codes, and they can be encoded and decoded offline.
As blocks are refined to a smaller and smaller area, the number of trailing zeros in a plus code will shrink.
For more information on the OLC specification, check the [OLC Wikipedia entry](https://en.wikipedia.org/wiki/Open_Location_Code)
There are two main functions in this module--encoding and decoding.
"""
@doc """
Encodes a location into an Open Location Code string.
Produces a code of the specified length, or the default length if no length
is provided. The length determines the accuracy of the code. The default length is
10 characters, returning a code of approximately 13.5x13.5 meters. Longer
codes represent smaller areas, but lengths > 14 refer to areas smaller than the accuracy of
most devices.
Latitude is in signed decimal degrees and will be clipped to the range -90 to 90. Longitude
is in signed decimal degrees and will be clipped to the range -180 to 180.
## Examples
iex> OpenLocationCode.encode(20.375,2.775, 6)
"7FG49Q00+"
iex> OpenLocationCode.encode(20.3700625,2.7821875)
"7FG49QCJ+2V"
"""
def encode(latitude, longitude, code_length \\ @pair_code_length) do
latitude = clip_latitude(latitude)
longitude = normalize_longitude(longitude)
latitude =
if latitude == 90 do
latitude - precision_by_length(code_length)
else
latitude
end
encode_pairs(latitude + @latitude_max, longitude + @longitude_max, code_length, "", 0)
end
@doc """
Decodes a code string into an `OpenLocationCode.CodeArea` struct
## Examples
iex> OpenLocationCode.decode("6PH57VP3+PR")
%OpenLocationCode.CodeArea{lat_resolution: 1.25e-4,
long_resolution: 1.25e-4,
south_latitude: 1.2867499999999998,
west_longitude: 103.85449999999999}
"""
def decode(olcstring) do
code = clean_code(olcstring)
{south_lat, west_long, lat_res, long_res} = decode_location(code)
%OpenLocationCode.CodeArea{
south_latitude: south_lat,
west_longitude: west_long,
lat_resolution: lat_res,
long_resolution: long_res
}
end
# Codec functions
defp encode_pairs(adj_latitude, adj_longitude, code_length, code, digit_count)
when digit_count < code_length do
place_value =
(digit_count / 2)
|> floor
|> resolution_for_pos
{ncode, adj_latitude} = append_code(code, adj_latitude, place_value)
digit_count = digit_count + 1
{ncode, adj_longitude} = append_code(ncode, adj_longitude, place_value)
digit_count = digit_count + 1
# Should we add a separator here?
ncode =
if digit_count == @separator_position and digit_count < code_length do
ncode <> @separator
else
ncode
end
encode_pairs(adj_latitude, adj_longitude, code_length, ncode, digit_count)
end
defp encode_pairs(_, _, code_length, code, digit_count) when digit_count == code_length do
code
|> pad_trailing
|> ensure_separator
end
defp append_code(code, adj_coord, place_value) do
digit_value = floor(adj_coord / place_value)
adj_coord = adj_coord - digit_value * place_value
code = code <> String.at(@code_alphabet, digit_value)
{code, adj_coord}
end
defp pad_trailing(code) do
if String.length(code) < @separator_position do
String.pad_trailing(code, @separator_position, @padding)
else
code
end
end
defp ensure_separator(code) do
if String.length(code) == @separator_position do
code <> @separator
else
code
end
end
defp floor(num) when is_number(num) do
Kernel.trunc(:math.floor(num))
end
defp resolution_for_pos(position) do
Enum.at(@pair_resolutions, position)
end
defp clip_latitude(latitude) do
Kernel.min(90, Kernel.max(-90, latitude))
end
defp normalize_longitude(longitude) do
case longitude do
l when l < -180 -> normalize_longitude(l + 360)
l when l > 180 -> normalize_longitude(l - 360)
l -> l
end
end
defp precision_by_length(code_length) do
if code_length <= @pair_code_length do
:math.pow(20, div(code_length, -2) + 2)
else
:math.pow(20, -3) / :math.pow(5, code_length - @pair_code_length)
end
end
defp clean_code(code) do
code |> String.replace(@separator, "") |> String.replace_trailing(@padding, "")
end
defp decode_location(code) do
_decode_location(0, code, String.length(code), -90.0, -180.0, 400.0, 400.0)
end
defp _decode_location(digit, code, code_length, south_lat, west_long, lat_res, long_res)
when digit < code_length do
code_at_digit = String.at(code, digit)
if digit < @pair_code_length do
code_at_digit1 = String.at(code, digit + 1)
lat_res = lat_res / 20
long_res = long_res / 20
south_lat = south_lat + lat_res * index_of_codechar(code_at_digit)
west_long = west_long + long_res * index_of_codechar(code_at_digit1)
_decode_location(digit + 2, code, code_length, south_lat, west_long, lat_res, long_res)
else
lat_res = lat_res / 5
long_res = long_res / 4
row = index_of_codechar(code_at_digit) / 4
col = rem(index_of_codechar(code_at_digit), 4)
south_lat = south_lat + lat_res * row
west_long = west_long + long_res * col
_decode_location(digit + 1, code, code_length, south_lat, west_long, lat_res, long_res)
end
end
defp _decode_location(digit, _, code_length, south_lat, west_long, lat_res, long_res)
when digit == code_length do
{south_lat, west_long, lat_res, long_res}
end
defp index_of_codechar(codechar) do
{index, _} = :binary.match(@code_alphabet, codechar)
index
end
end