Packages

Library for encoding and decoding Open Location Codes (Plus Codes)

Current section

Files

Jump to
openlocationcode lib openlocationcode.ex
Raw

lib/openlocationcode.ex

defmodule OpenLocationCode do
@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