src/geokit@latlng.erl

-module(geokit@latlng).
-compile([no_auto_import, nowarn_unused_vars, nowarn_unused_function, nowarn_nomatch, inline]).
-define(FILEPATH, "src/geokit/latlng.gleam").
-export([new/2, wrap/2, lat/1, lng/1, equal/2]).
-export_type([lat_lng/0, lat_lng_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(
    " Opaque `LatLng` (geographic coordinate) shared by every module\n"
    " in `geokit`.\n"
    "\n"
    " Latitudes are in degrees, in the range [-90, 90], with positive\n"
    " values north of the equator. Longitudes are in degrees, in the\n"
    " range [-180, 180], with positive values east of the prime\n"
    " meridian. Out-of-range inputs to [`new`](#new) return a typed\n"
    " error; use [`wrap`](#wrap) when your source data may be\n"
    " denormalised (for example, sensor output that crosses the\n"
    " antimeridian).\n"
).

-opaque lat_lng() :: {lat_lng, float(), float()}.

-type lat_lng_error() :: {lat_out_of_range, float()} |
    {lng_out_of_range, float()}.

-file("src/geokit/latlng.gleam", 39).
?DOC(
    " Build a `LatLng` from latitude and longitude in degrees.\n"
    "\n"
    " ```gleam\n"
    " import geokit/latlng\n"
    "\n"
    " let assert Ok(tokyo) = latlng.new(lat: 35.6812, lng: 139.7671)\n"
    " ```\n"
).
-spec new(float(), float()) -> {ok, lat_lng()} | {error, lat_lng_error()}.
new(Lat, Lng) ->
    gleam@bool:guard(
        (Lat < -90.0) orelse (Lat > 90.0),
        {error, {lat_out_of_range, Lat}},
        fun() ->
            gleam@bool:guard(
                (Lng < -180.0) orelse (Lng > 180.0),
                {error, {lng_out_of_range, Lng}},
                fun() -> {ok, {lat_lng, Lat, Lng}} end
            )
        end
    ).

-file("src/geokit/latlng.gleam", 68).
-spec wrap_longitude(float()) -> float().
wrap_longitude(Lng) ->
    gleam@bool:guard(
        (Lng >= -180.0) andalso (Lng =< 180.0),
        Lng,
        fun() ->
            Shifted = Lng + 180.0,
            Wrapped = Shifted - (360.0 * math:floor(Shifted / 360.0)),
            Result = Wrapped - 180.0,
            gleam@bool:guard(
                Result > 180.0,
                180.0,
                fun() ->
                    gleam@bool:guard(
                        Result < -180.0,
                        -180.0,
                        fun() -> Result end
                    )
                end
            )
        end
    ).

-file("src/geokit/latlng.gleam", 62).
?DOC(
    " Build a `LatLng`, normalising longitude into `[-180, 180]` and\n"
    " clamping latitude into `[-90, 90]`. Use this when the source data\n"
    " may be denormalised (sensor output crossing the antimeridian,\n"
    " great-circle calculations producing 181°, ...).\n"
    "\n"
    " ```gleam\n"
    " import geokit/latlng\n"
    "\n"
    " let p = latlng.wrap(lat: 91.0, lng: 181.0)\n"
    " // p has lat = 90.0, lng = -179.0\n"
    " ```\n"
).
-spec wrap(float(), float()) -> lat_lng().
wrap(Lat, Lng) ->
    Lat_clamped = gleam@float:clamp(Lat, -90.0, 90.0),
    Lng_wrapped = wrap_longitude(Lng),
    {lat_lng, Lat_clamped, Lng_wrapped}.

-file("src/geokit/latlng.gleam", 86).
?DOC(" Latitude in degrees.\n").
-spec lat(lat_lng()) -> float().
lat(P) ->
    erlang:element(2, P).

-file("src/geokit/latlng.gleam", 91).
?DOC(" Longitude in degrees.\n").
-spec lng(lat_lng()) -> float().
lng(P) ->
    erlang:element(3, P).

-file("src/geokit/latlng.gleam", 99).
?DOC(
    " Value equality. Two `LatLng` values are equal iff their `lat` and\n"
    " `lng` components compare equal.\n"
).
-spec equal(lat_lng(), lat_lng()) -> boolean().
equal(A, B) ->
    (erlang:element(2, A) =:= erlang:element(2, B)) andalso (erlang:element(
        3,
        A
    )
    =:= erlang:element(3, B)).