-module(geokit@polyline).
-compile([no_auto_import, nowarn_unused_vars, nowarn_unused_function, nowarn_nomatch, inline]).
-define(FILEPATH, "src/geokit/polyline.gleam").
-export([decode_with/2, decode/1, encode/1, encode_with/2]).
-export_type([polyline_error/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(
" Google Encoded Polyline algorithm.\n"
"\n"
" Encodes a sequence of `LatLng` points as a compact ASCII string\n"
" using delta encoding, ZigZag transform, base-32 chunking, and a\n"
" `+ 0x3F` ASCII offset. Used by the Google Maps Directions API,\n"
" OSRM, Mapbox Directions, and Valhalla.\n"
"\n"
" Specification:\n"
" <https://developers.google.com/maps/documentation/utilities/polylinealgorithmformat>.\n"
).
-type polyline_error() :: truncated_input |
{invalid_character, binary(), integer()} |
{precision_out_of_range, integer()}.
-file("src/geokit/polyline.gleam", 129).
-spec bit_not_lower(integer()) -> integer().
bit_not_lower(Value) ->
- Value - 1.
-file("src/geokit/polyline.gleam", 265).
-spec pow10(integer()) -> integer().
pow10(Exponent) ->
gleam@bool:guard(Exponent =< 0, 1, fun() -> 10 * pow10(Exponent - 1) end).
-file("src/geokit/polyline.gleam", 270).
-spec pow_of_two(integer()) -> integer().
pow_of_two(Exponent) ->
gleam@bool:guard(
Exponent =< 0,
1,
fun() -> 2 * pow_of_two(Exponent - 1) end
).
-file("src/geokit/polyline.gleam", 275).
-spec round_to_int(float()) -> integer().
round_to_int(Value) ->
gleam@bool:guard(
Value < +0.0,
- erlang:round(+0.0 - Value),
fun() -> erlang:round(Value) end
).
-file("src/geokit/polyline.gleam", 280).
-spec int_to_float(integer()) -> float().
int_to_float(Value) ->
erlang:float(Value).
-file("src/geokit/polyline.gleam", 288).
-spec grapheme_codepoint(binary(), integer()) -> {ok, integer()} |
{error, polyline_error()}.
grapheme_codepoint(Grapheme, Position) ->
case gleam@string:to_utf_codepoints(Grapheme) of
[Code] ->
Value = gleam_stdlib:identity(Code),
gleam@bool:guard(
Value > 127,
{error, {invalid_character, Grapheme, Position}},
fun() -> {ok, Value} end
);
_ ->
{error, {invalid_character, Grapheme, Position}}
end.
-file("src/geokit/polyline.gleam", 226).
-spec decode_unsigned(list(binary()), integer(), integer(), integer()) -> {ok,
{integer(), list(binary()), integer()}} |
{error, polyline_error()}.
decode_unsigned(Chars, Position, Shift, Acc) ->
case Chars of
[] ->
{error, truncated_input};
[Head | Tail] ->
gleam@result:'try'(
grapheme_codepoint(Head, Position),
fun(Code) ->
gleam@bool:guard(
Code < 63,
{error, {invalid_character, Head, Position}},
fun() ->
Value = Code - 63,
Chunk = case Value < 16#20 of
true ->
Value;
false ->
Value - 16#20
end,
New_acc = Acc + (Chunk * pow_of_two(Shift)),
case Value < 16#20 of
true ->
{ok, {New_acc, Tail, Position + 1}};
false ->
decode_unsigned(
Tail,
Position + 1,
Shift + 5,
New_acc
)
end
end
)
end
)
end.
-file("src/geokit/polyline.gleam", 209).
-spec decode_signed(list(binary()), integer()) -> {ok,
{integer(), list(binary()), integer()}} |
{error, polyline_error()}.
decode_signed(Chars, Position) ->
gleam@result:'try'(
decode_unsigned(Chars, Position, 0, 0),
fun(_use0) ->
{Value, Rest, After_pos} = _use0,
Signed = case Value rem 2 of
0 ->
Value div 2;
_ ->
bit_not_lower(Value div 2)
end,
{ok, {Signed, Rest, After_pos}}
end
).
-file("src/geokit/polyline.gleam", 180).
-spec decode_loop(
list(binary()),
integer(),
integer(),
integer(),
list({integer(), integer()})
) -> {ok, {list({integer(), integer()}), integer()}} | {error, polyline_error()}.
decode_loop(Chars, Position, Last_lat, Last_lng, Acc) ->
case Chars of
[] ->
{ok, {Acc, Position}};
_ ->
gleam@result:'try'(
decode_signed(Chars, Position),
fun(_use0) ->
{Lat_delta, After_lat_chars, After_lat_pos} = _use0,
gleam@result:'try'(
decode_signed(After_lat_chars, After_lat_pos),
fun(_use0@1) ->
{Lng_delta, After_lng_chars, After_lng_pos} = _use0@1,
New_lat = Last_lat + Lat_delta,
New_lng = Last_lng + Lng_delta,
decode_loop(
After_lng_chars,
After_lng_pos,
New_lat,
New_lng,
[{New_lat, New_lng} | Acc]
)
end
)
end
)
end.
-file("src/geokit/polyline.gleam", 150).
?DOC(
" Decode a polyline string with the given precision. Must match the\n"
" precision used at encode time. `precision` must be in `[1, 11]`,\n"
" matching [`encode_with`](#encode_with).\n"
).
-spec decode_with(binary(), integer()) -> {ok, list(geokit@latlng:lat_lng())} |
{error, polyline_error()}.
decode_with(Input, Precision) ->
gleam@bool:guard(
(Precision < 1) orelse (Precision > 11),
{error, {precision_out_of_range, Precision}},
fun() ->
Factor = pow10(Precision),
gleam@result:'try'(
decode_loop(gleam@string:to_graphemes(Input), 0, 0, 0, []),
fun(_use0) ->
{Raw_points, _} = _use0,
{ok,
begin
_pipe = Raw_points,
_pipe@1 = lists:reverse(_pipe),
gleam@list:map(
_pipe@1,
fun(Scaled) ->
{Lat_int, Lng_int} = Scaled,
Lat = case int_to_float(Factor) of
+0.0 -> +0.0;
-0.0 -> -0.0;
Gleam@denominator -> int_to_float(
Lat_int
)
/ Gleam@denominator
end,
Lng = case int_to_float(Factor) of
+0.0 -> +0.0;
-0.0 -> -0.0;
Gleam@denominator@1 -> int_to_float(
Lng_int
)
/ Gleam@denominator@1
end,
geokit@latlng:wrap(Lat, Lng)
end
)
end}
end
)
end
).
-file("src/geokit/polyline.gleam", 143).
?DOC(
" Decode a polyline string with the default precision (5).\n"
"\n"
" ```gleam\n"
" import geokit/polyline\n"
"\n"
" let assert Ok(points) = polyline.decode(\"_p~iF~ps|U_ulLnnqC_mqNvxq`@\")\n"
" // points == [(38.5, -120.2), (40.7, -120.95), (43.252, -126.453)]\n"
" ```\n"
).
-spec decode(binary()) -> {ok, list(geokit@latlng:lat_lng())} |
{error, polyline_error()}.
decode(Input) ->
decode_with(Input, 5).
-file("src/geokit/polyline.gleam", 305).
-spec int_codepoint_to_string(integer()) -> binary().
int_codepoint_to_string(Value) ->
case gleam@string:utf_codepoint(Value) of
{ok, Code} ->
gleam_stdlib:utf_codepoint_list_to_string([Code]);
{error, nil} ->
<<""/utf8>>
end.
-file("src/geokit/polyline.gleam", 116).
-spec encode_unsigned(integer(), binary()) -> binary().
encode_unsigned(Value, Acc) ->
case Value >= 16#20 of
true ->
Chunk = ((Value rem 16#20) + 16#20) + 63,
encode_unsigned(
Value div 16#20,
<<Acc/binary, (int_codepoint_to_string(Chunk))/binary>>
);
false ->
<<Acc/binary, (int_codepoint_to_string(Value + 63))/binary>>
end.
-file("src/geokit/polyline.gleam", 108).
-spec encode_signed(integer()) -> binary().
encode_signed(Value) ->
Shifted = case Value < 0 of
true ->
bit_not_lower(Value * 2);
false ->
Value * 2
end,
encode_unsigned(Shifted, <<""/utf8>>).
-file("src/geokit/polyline.gleam", 83).
-spec encode_loop(
list(geokit@latlng:lat_lng()),
integer(),
integer(),
integer(),
binary()
) -> binary().
encode_loop(Points, Factor, Last_lat, Last_lng, Acc) ->
case Points of
[] ->
Acc;
[Head | Tail] ->
Lat_scaled = round_to_int(
geokit@latlng:lat(Head) * int_to_float(Factor)
),
Lng_scaled = round_to_int(
geokit@latlng:lng(Head) * int_to_float(Factor)
),
Lat_delta = Lat_scaled - Last_lat,
Lng_delta = Lng_scaled - Last_lng,
encode_loop(
Tail,
Factor,
Lat_scaled,
Lng_scaled,
<<<<Acc/binary, (encode_signed(Lat_delta))/binary>>/binary,
(encode_signed(Lng_delta))/binary>>
)
end.
-file("src/geokit/polyline.gleam", 75).
-spec encode_unchecked(list(geokit@latlng:lat_lng()), integer()) -> binary().
encode_unchecked(Points, Precision) ->
Factor = pow10(Precision),
encode_loop(Points, Factor, 0, 0, <<""/utf8>>).
-file("src/geokit/polyline.gleam", 53).
?DOC(
" Encode a list of points using the default precision of 5 (1e-5\n"
" degrees, ~1 m at the equator). This matches Google's original\n"
" algorithm.\n"
"\n"
" ```gleam\n"
" import geokit/polyline\n"
" import geokit/latlng\n"
"\n"
" let assert Ok(p1) = latlng.new(lat: 38.5, lng: -120.2)\n"
" let assert Ok(p2) = latlng.new(lat: 40.7, lng: -120.95)\n"
" let assert Ok(p3) = latlng.new(lat: 43.252, lng: -126.453)\n"
" polyline.encode([p1, p2, p3])\n"
" // == \"_p~iF~ps|U_ulLnnqC_mqNvxq`@\"\n"
" ```\n"
).
-spec encode(list(geokit@latlng:lat_lng())) -> binary().
encode(Points) ->
encode_unchecked(Points, 5).
-file("src/geokit/polyline.gleam", 64).
?DOC(
" Encode a list of points with the given precision (number of\n"
" decimal digits to preserve). `precision` must be in `[1, 11]`;\n"
" precision 6 is used by Valhalla and the Open Source Routing\n"
" Machine for higher accuracy than the Google default of 5.\n"
).
-spec encode_with(list(geokit@latlng:lat_lng()), integer()) -> {ok, binary()} |
{error, polyline_error()}.
encode_with(Points, Precision) ->
gleam@bool:guard(
(Precision < 1) orelse (Precision > 11),
{error, {precision_out_of_range, Precision}},
fun() -> {ok, encode_unchecked(Points, Precision)} end
).