src/geokit@polyline.erl

-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
    ).