-module(geokit@geohash).
-compile([no_auto_import, nowarn_unused_vars, nowarn_unused_function, nowarn_nomatch, inline]).
-define(FILEPATH, "src/geokit/geohash.gleam").
-export([encode/2, decode_bounds/1, decode/1, neighbor/2, neighbors/1]).
-export_type([geohash_error/0, direction/0, neighbors/0]).
-if(?OTP_RELEASE >= 27).
-define(MODULEDOC(Str), -moduledoc(Str)).
-define(DOC(Str), -doc(Str)).
-else.
-define(MODULEDOC(Str), -compile([])).
-define(DOC(Str), -compile([])).
-endif.
?MODULEDOC(
" Niemeyer geohash encoding and decoding.\n"
"\n"
" A geohash is a short alphanumeric string identifying a\n"
" rectangular cell on the Earth's surface. Longer hashes pinpoint\n"
" smaller cells. The alphabet is base32 (10 digits + 22 letters,\n"
" omitting `a`, `i`, `l`, `o` to avoid visual ambiguity); each\n"
" character contributes 5 bits, alternating between longitude and\n"
" latitude.\n"
"\n"
" See <http://geohash.org/> for the original specification.\n"
).
-type geohash_error() :: {precision_out_of_range, integer()} |
empty_hash |
{invalid_character, binary(), integer()}.
-type direction() :: north |
south |
east |
west |
north_east |
north_west |
south_east |
south_west.
-type neighbors() :: {neighbors,
binary(),
binary(),
binary(),
binary(),
binary(),
binary(),
binary(),
binary()}.
-file("src/geokit/geohash.gleam", 99).
-spec encode_bits(
float(),
float(),
float(),
float(),
float(),
float(),
integer(),
boolean(),
list(integer())
) -> list(integer()).
encode_bits(
Lat_min,
Lat_max,
Lng_min,
Lng_max,
Lat,
Lng,
Remaining,
Use_lng,
Acc
) ->
gleam@bool:guard(Remaining =< 0, Acc, fun() -> case Use_lng of
true ->
Mid = (Lng_min + Lng_max) / 2.0,
Go_high = Lng > Mid,
New_min = case Go_high of
true ->
Mid;
false ->
Lng_min
end,
New_max = case Go_high of
true ->
Lng_max;
false ->
Mid
end,
Bit = case Go_high of
true ->
1;
false ->
0
end,
encode_bits(
Lat_min,
Lat_max,
New_min,
New_max,
Lat,
Lng,
Remaining - 1,
false,
[Bit | Acc]
);
false ->
Mid@1 = (Lat_min + Lat_max) / 2.0,
Go_high@1 = Lat > Mid@1,
New_min@1 = case Go_high@1 of
true ->
Mid@1;
false ->
Lat_min
end,
New_max@1 = case Go_high@1 of
true ->
Lat_max;
false ->
Mid@1
end,
Bit@1 = case Go_high@1 of
true ->
1;
false ->
0
end,
encode_bits(
New_min@1,
New_max@1,
Lng_min,
Lng_max,
Lat,
Lng,
Remaining - 1,
true,
[Bit@1 | Acc]
)
end end).
-file("src/geokit/geohash.gleam", 253).
-spec decode_bits(
list(integer()),
float(),
float(),
float(),
float(),
boolean()
) -> {float(), float(), float(), float()}.
decode_bits(Bits, Lat_min, Lat_max, Lng_min, Lng_max, Use_lng) ->
case Bits of
[] ->
{Lat_min, Lat_max, Lng_min, Lng_max};
[Bit | Rest] ->
case Use_lng of
true ->
Mid = (Lng_min + Lng_max) / 2.0,
case Bit of
1 ->
decode_bits(
Rest,
Lat_min,
Lat_max,
Mid,
Lng_max,
false
);
_ ->
decode_bits(
Rest,
Lat_min,
Lat_max,
Lng_min,
Mid,
false
)
end;
false ->
Mid@1 = (Lat_min + Lat_max) / 2.0,
case Bit of
1 ->
decode_bits(
Rest,
Mid@1,
Lat_max,
Lng_min,
Lng_max,
true
);
_ ->
decode_bits(
Rest,
Lat_min,
Mid@1,
Lng_min,
Lng_max,
true
)
end
end
end.
-file("src/geokit/geohash.gleam", 319).
-spec index_in_alphabet_loop(binary(), binary(), integer()) -> {ok, integer()} |
{error, nil}.
index_in_alphabet_loop(Needle, Haystack, Position) ->
case gleam_stdlib:string_pop_grapheme(Haystack) of
{error, nil} ->
{error, nil};
{ok, {Head, Tail}} ->
gleam@bool:guard(
Head =:= Needle,
{ok, Position},
fun() -> index_in_alphabet_loop(Needle, Tail, Position + 1) end
)
end.
-file("src/geokit/geohash.gleam", 474).
-spec index_of_char_in(binary(), binary(), integer()) -> {ok, integer()} |
{error, nil}.
index_of_char_in(Haystack, Needle, Position) ->
case gleam_stdlib:string_pop_grapheme(Haystack) of
{error, nil} ->
{error, nil};
{ok, {Head, Tail}} ->
gleam@bool:guard(
Head =:= Needle,
{ok, Position},
fun() -> index_of_char_in(Tail, Needle, Position + 1) end
)
end.
-file("src/geokit/geohash.gleam", 169).
-spec bits_to_base32(list(integer()), binary()) -> binary().
bits_to_base32(Bits, Acc) ->
case Bits of
[B0, B1, B2, B3, B4 | Rest] ->
Value = ((((B0 * 16) + (B1 * 8)) + (B2 * 4)) + (B3 * 2)) + B4,
Char = case gleam@string:slice(
<<"0123456789bcdefghjkmnpqrstuvwxyz"/utf8>>,
Value,
1
) of
<<""/utf8>> ->
<<"0"/utf8>>;
Ch ->
Ch
end,
bits_to_base32(Rest, <<Acc/binary, Char/binary>>);
_ ->
Acc
end.
-file("src/geokit/geohash.gleam", 75).
?DOC(
" Encode a [`LatLng`](../latlng.html#LatLng) as a base32 geohash of\n"
" the requested precision. Precision is the number of output\n"
" characters and must be in `[1, 12]`. Each additional character\n"
" shrinks the cell width by a factor of ~5.6.\n"
"\n"
" ```gleam\n"
" import geokit/geohash\n"
" import geokit/latlng\n"
"\n"
" let assert Ok(tokyo) = latlng.new(lat: 35.6812, lng: 139.7671)\n"
" let assert Ok(hash) = geohash.encode(point: tokyo, precision: 8)\n"
" // hash == \"xn76urx4\"\n"
" ```\n"
).
-spec encode(geokit@latlng:lat_lng(), integer()) -> {ok, binary()} |
{error, geohash_error()}.
encode(Point, Precision) ->
gleam@bool:guard(
(Precision < 1) orelse (Precision > 12),
{error, {precision_out_of_range, Precision}},
fun() ->
Target_bits = Precision * 5,
Bits = encode_bits(
-90.0,
90.0,
-180.0,
180.0,
geokit@latlng:lat(Point),
geokit@latlng:lng(Point),
Target_bits,
true,
[]
),
{ok, bits_to_base32(lists:reverse(Bits), <<""/utf8>>)}
end
).
-file("src/geokit/geohash.gleam", 315).
-spec index_in_alphabet(binary()) -> {ok, integer()} | {error, nil}.
index_in_alphabet(Char) ->
index_in_alphabet_loop(Char, <<"0123456789bcdefghjkmnpqrstuvwxyz"/utf8>>, 0).
-file("src/geokit/geohash.gleam", 230).
-spec hash_to_bits(binary(), integer(), list(integer())) -> {ok,
list(integer())} |
{error, geohash_error()}.
hash_to_bits(Hash, Position, Acc) ->
case gleam_stdlib:string_pop_grapheme(Hash) of
{error, nil} ->
{ok, Acc};
{ok, {Head, Tail}} ->
case index_in_alphabet(Head) of
{error, nil} ->
{error, {invalid_character, Head, Position}};
{ok, Value} ->
hash_to_bits(
Tail,
Position + 1,
[Value rem 2,
(Value div 2) rem 2,
(Value div 4) rem 2,
(Value div 8) rem 2,
(Value div 16) rem 2 |
Acc]
)
end
end.
-file("src/geokit/geohash.gleam", 209).
?DOC(
" Decode a geohash to the south-west and north-east corners of its\n"
" cell, returned as a tuple `#(sw, ne)`.\n"
"\n"
" The input is case-insensitive: upper-case characters are folded to\n"
" lower-case before lookup. This matches `chrisveness/latlon-geohash`\n"
" and `ngeohash`.\n"
).
-spec decode_bounds(binary()) -> {ok,
{geokit@latlng:lat_lng(), geokit@latlng:lat_lng()}} |
{error, geohash_error()}.
decode_bounds(Hash) ->
Normalised = string:lowercase(Hash),
gleam@bool:guard(
Normalised =:= <<""/utf8>>,
{error, empty_hash},
fun() ->
gleam@result:'try'(
hash_to_bits(Normalised, 0, []),
fun(Bits) ->
{Lat_min, Lat_max, Lng_min, Lng_max} = decode_bits(
lists:reverse(Bits),
-90.0,
90.0,
-180.0,
180.0,
true
),
{ok,
{geokit@latlng:wrap(Lat_min, Lng_min),
geokit@latlng:wrap(Lat_max, Lng_max)}}
end
)
end
).
-file("src/geokit/geohash.gleam", 196).
?DOC(
" Decode a geohash to the centre of the cell it identifies.\n"
"\n"
" The input is case-insensitive: upper-case characters are folded\n"
" to lower-case before lookup.\n"
"\n"
" ```gleam\n"
" import geokit/geohash\n"
"\n"
" let assert Ok(centre) = geohash.decode(\"xn76urx4\")\n"
" // centre ≈ (35.6812, 139.7671)\n"
" ```\n"
).
-spec decode(binary()) -> {ok, geokit@latlng:lat_lng()} |
{error, geohash_error()}.
decode(Hash) ->
gleam@result:'try'(
decode_bounds(Hash),
fun(_use0) ->
{Sw, Ne} = _use0,
Lat = (geokit@latlng:lat(Sw) + geokit@latlng:lat(Ne)) / 2.0,
Lng = (geokit@latlng:lng(Sw) + geokit@latlng:lng(Ne)) / 2.0,
{ok, geokit@latlng:wrap(Lat, Lng)}
end
).
-file("src/geokit/geohash.gleam", 488).
-spec neighbor_table(direction(), boolean()) -> binary().
neighbor_table(Direction, Is_even) ->
case {Direction, Is_even} of
{north, true} ->
<<"p0r21436x8zb9dcf5h7kjnmqesgutwvy"/utf8>>;
{north, false} ->
<<"bc01fg45238967deuvhjyznpkmstqrwx"/utf8>>;
{east, true} ->
<<"bc01fg45238967deuvhjyznpkmstqrwx"/utf8>>;
{east, false} ->
<<"p0r21436x8zb9dcf5h7kjnmqesgutwvy"/utf8>>;
{south, true} ->
<<"14365h7k9dcfesgujnmqp0r2twvyx8zb"/utf8>>;
{south, false} ->
<<"238967debc01fg45kmstqrwxuvhjyznp"/utf8>>;
{west, true} ->
<<"238967debc01fg45kmstqrwxuvhjyznp"/utf8>>;
{west, false} ->
<<"14365h7k9dcfesgujnmqp0r2twvyx8zb"/utf8>>;
{_, _} ->
<<""/utf8>>
end.
-file("src/geokit/geohash.gleam", 505).
-spec border_table(direction(), boolean()) -> binary().
border_table(Direction, Is_even) ->
case {Direction, Is_even} of
{north, true} ->
<<"prxz"/utf8>>;
{north, false} ->
<<"bcfguvyz"/utf8>>;
{east, true} ->
<<"bcfguvyz"/utf8>>;
{east, false} ->
<<"prxz"/utf8>>;
{south, true} ->
<<"028b"/utf8>>;
{south, false} ->
<<"0145hjnp"/utf8>>;
{west, true} ->
<<"0145hjnp"/utf8>>;
{west, false} ->
<<"028b"/utf8>>;
{_, _} ->
<<""/utf8>>
end.
-file("src/geokit/geohash.gleam", 438).
-spec step(binary(), direction()) -> {ok, binary()} | {error, geohash_error()}.
step(Hash, Direction) ->
gleam@bool:guard(
Hash =:= <<""/utf8>>,
{error, empty_hash},
fun() ->
Length = string:length(Hash),
Parent_length = Length - 1,
Parent = gleam@string:slice(Hash, 0, Parent_length),
Last = gleam@string:slice(Hash, Parent_length, 1),
Is_even = (Length rem 2) =:= 0,
Border = border_table(Direction, Is_even),
Lookup = neighbor_table(Direction, Is_even),
Parent_result = case gleam_stdlib:contains_string(Border, Last) of
true ->
case Parent of
<<""/utf8>> ->
{error, empty_hash};
_ ->
step(Parent, Direction)
end;
false ->
{ok, Parent}
end,
gleam@result:'try'(
Parent_result,
fun(New_parent) -> case index_of_char_in(Lookup, Last, 0) of
{error, nil} ->
{error, {invalid_character, Last, Parent_length}};
{ok, Value} ->
{ok,
<<New_parent/binary,
(gleam@string:slice(
<<"0123456789bcdefghjkmnpqrstuvwxyz"/utf8>>,
Value,
1
))/binary>>}
end end
)
end
).
-file("src/geokit/geohash.gleam", 406).
-spec two_steps(binary(), direction(), direction()) -> {ok, binary()} |
{error, geohash_error()}.
two_steps(Hash, First, Second) ->
gleam@result:'try'(
neighbor(Hash, First),
fun(Partial) -> neighbor(Partial, Second) end
).
-file("src/geokit/geohash.gleam", 389).
?DOC(
" The neighbour of `hash` in the given [`Direction`](#Direction).\n"
"\n"
" Returns [`EmptyHash`](#GeohashError) when called on the empty\n"
" string. Wraps across the antimeridian for east / west, returns\n"
" [`EmptyHash`](#GeohashError) when a polar neighbour would cross\n"
" the pole (north of the northernmost row or south of the\n"
" southernmost row).\n"
"\n"
" The input is case-insensitive: upper-case characters are folded\n"
" to lower-case before lookup, matching `chrisveness/latlon-geohash`\n"
" and `ngeohash`.\n"
).
-spec neighbor(binary(), direction()) -> {ok, binary()} |
{error, geohash_error()}.
neighbor(Hash, Direction) ->
Normalised = string:lowercase(Hash),
case Direction of
north ->
step(Normalised, north);
south ->
step(Normalised, south);
east ->
step(Normalised, east);
west ->
step(Normalised, west);
north_east ->
two_steps(Normalised, north, east);
north_west ->
two_steps(Normalised, north, west);
south_east ->
two_steps(Normalised, south, east);
south_west ->
two_steps(Normalised, south, west)
end.
-file("src/geokit/geohash.gleam", 417).
?DOC(
" All eight neighbours of `hash`, returned as a\n"
" [`Neighbors`](#Neighbors) record.\n"
).
-spec neighbors(binary()) -> {ok, neighbors()} | {error, geohash_error()}.
neighbors(Hash) ->
gleam@result:'try'(
neighbor(Hash, north),
fun(N) ->
gleam@result:'try'(
neighbor(Hash, south),
fun(S) ->
gleam@result:'try'(
neighbor(Hash, east),
fun(E) ->
gleam@result:'try'(
neighbor(Hash, west),
fun(W) ->
gleam@result:'try'(
neighbor(Hash, north_east),
fun(Ne) ->
gleam@result:'try'(
neighbor(Hash, north_west),
fun(Nw) ->
gleam@result:'try'(
neighbor(
Hash,
south_east
),
fun(Se) ->
gleam@result:'try'(
neighbor(
Hash,
south_west
),
fun(Sw) ->
{ok,
{neighbors,
N,
S,
E,
W,
Ne,
Nw,
Se,
Sw}}
end
)
end
)
end
)
end
)
end
)
end
)
end
)
end
).