diff --git a/lib/openlocationcode.ex b/lib/openlocationcode.ex index cf7cc20..ae99ca0 100644 --- a/lib/openlocationcode.ex +++ b/lib/openlocationcode.ex @@ -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. @@ -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 @@ -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) @@ -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 diff --git a/mix.lock b/mix.lock index c0a12cb..15991e5 100644 --- a/mix.lock +++ b/mix.lock @@ -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"}, }