Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
207 changes: 107 additions & 100 deletions lib/openlocationcode.ex
Original file line number Diff line number Diff line change
@@ -1,22 +1,23 @@
defmodule OpenLocationCode do
@pair_code_length 10
@separator "+"
@separator_position 8
@padding "0"
@latitude_max 90
@longitude_max 180
# Kernel's function causes a clash with autodidact's function defination
import Kernel, except: [floor: 1]

@code_alphabet "23456789CFGHJMPQRVWX"
@pair_code_length 10
@separator "+"
@separator_position 8
@padding "0"
@latitude_max 90
@longitude_max 180

#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]
@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
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.

Expand All @@ -25,8 +26,8 @@ defmodule OpenLocationCode do
There are two main functions in this module--encoding and decoding.

"""
@doc """

@doc """
Encodes a location into an Open Location Code string.

Produces a code of the specified length, or the default length if no length
Expand All @@ -35,108 +36,114 @@ defmodule OpenLocationCode do
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
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)
iex> OpenLocationCode.encode(20.375,2.775, 6)
"7FG49Q00+"

iex> OpenLocationCode.encode(20.3700625,2.7821875)
"7FG49QCJ+2V"
"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)
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

## Examples

iex> OpenLocationCode.decode("6PH57VP3+PR")
%OpenLocationCode.CodeArea{lat_resolution: 1.25e-4,
long_resolution: 1.25e-4,
south_latitude: 1.2867499999999998,
%OpenLocationCode.CodeArea{lat_resolution: 1.25e-4,
long_resolution: 1.25e-4,
south_latitude: 1.2867499999999998,
west_longitude: 103.85449999999999}

"""
def decode(olcstring) do
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

%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

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)
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 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)
adj_coord = adj_coord - digit_value * place_value
code = code <> String.at(@code_alphabet, digit_value)
{ code, adj_coord }
end
{code, adj_coord}
end

defp pad_trailing(code) do
defp pad_trailing(code) do
if String.length(code) < @separator_position do
String.pad_trailing(code, @separator_position, @padding)
String.pad_trailing(code, @separator_position, @padding)
else
code
end
end
end
end

defp ensure_separator(code) do
defp ensure_separator(code) do
if String.length(code) == @separator_position do
code <> @separator
else
else
code
end
end
end
defp floor(num) when is_number(num) do

defp floor(num) when is_number(num) do
Kernel.trunc(:math.floor(num))
end
end

defp resolution_for_pos(position) do
Enum.at(@pair_resolutions, position)
Expand All @@ -154,51 +161,51 @@ defmodule OpenLocationCode do
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
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
defp clean_code(code) do
code |> String.replace(@separator, "") |> String.replace_trailing(@padding, "")
end
end

defp decode_location(code) do
_decode_location(0, code, String.length(code), -90.0, -180.0, 400.0, 400.0)
_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)
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)
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)
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
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
end

defp index_of_codechar(codechar) do
defp index_of_codechar(codechar) do
{index, _} = :binary.match(@code_alphabet, codechar)
index
end

end
index
end
end
10 changes: 5 additions & 5 deletions mix.lock
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
%{
"earmark": {:hex, :earmark, "1.3.1", "73812f447f7a42358d3ba79283cfa3075a7580a3a2ed457616d6517ac3738cb9", [:mix], [], "hexpm"},
"ex_doc": {:hex, :ex_doc, "0.19.2", "6f4081ccd9ed081b6dc0bd5af97a41e87f5554de469e7d76025fba535180565f", [:mix], [{:earmark, "~> 1.2", [hex: :earmark, repo: "hexpm", optional: false]}, {:makeup_elixir, "~> 0.10", [hex: :makeup_elixir, repo: "hexpm", optional: false]}], "hexpm"},
"makeup": {:hex, :makeup, "0.6.0", "e0fd985525e8d42352782bd76253105fbab0a783ac298708ca9020636c9568af", [:mix], [{:nimble_parsec, "~> 0.5.0", [hex: :nimble_parsec, repo: "hexpm", optional: false]}], "hexpm"},
"makeup_elixir": {:hex, :makeup_elixir, "0.11.0", "aa3446f67356afa5801618867587a8863f176f9c632fb62b20f49bd1ea335e8a", [:mix], [{:makeup, "~> 0.6", [hex: :makeup, repo: "hexpm", optional: false]}], "hexpm"},
"nimble_parsec": {:hex, :nimble_parsec, "0.5.0", "90e2eca3d0266e5c53f8fbe0079694740b9c91b6747f2b7e3c5d21966bba8300", [:mix], [], "hexpm"},
"earmark": {:hex, :earmark, "1.3.1", "73812f447f7a42358d3ba79283cfa3075a7580a3a2ed457616d6517ac3738cb9", [:mix], [], "hexpm", "000aaeff08919e95e7aea13e4af7b2b9734577b3e6a7c50ee31ee88cab6ec4fb"},
"ex_doc": {:hex, :ex_doc, "0.19.2", "6f4081ccd9ed081b6dc0bd5af97a41e87f5554de469e7d76025fba535180565f", [:mix], [{:earmark, "~> 1.2", [hex: :earmark, repo: "hexpm", optional: false]}, {:makeup_elixir, "~> 0.10", [hex: :makeup_elixir, repo: "hexpm", optional: false]}], "hexpm", "4eae888633d2937e0a8839ae6002536d459c22976743c9dc98dd05941a06c016"},
"makeup": {:hex, :makeup, "0.6.0", "e0fd985525e8d42352782bd76253105fbab0a783ac298708ca9020636c9568af", [:mix], [{:nimble_parsec, "~> 0.5.0", [hex: :nimble_parsec, repo: "hexpm", optional: false]}], "hexpm", "ce6462e868dca4c1eb45a42dd12647519e124d9e459b41385fa30f25c8eca304"},
"makeup_elixir": {:hex, :makeup_elixir, "0.11.0", "aa3446f67356afa5801618867587a8863f176f9c632fb62b20f49bd1ea335e8a", [:mix], [{:makeup, "~> 0.6", [hex: :makeup, repo: "hexpm", optional: false]}], "hexpm", "f980183e236d20b6c42af29107f1742ec9501a1bcb6c527a49fe2a2485630b5b"},
"nimble_parsec": {:hex, :nimble_parsec, "0.5.0", "90e2eca3d0266e5c53f8fbe0079694740b9c91b6747f2b7e3c5d21966bba8300", [:mix], [], "hexpm", "5c040b8469c1ff1b10093d3186e2e10dbe483cd73d79ec017993fb3985b8a9b3"},
}