src/geokit@distance.erl

-module(geokit@distance).
-compile([no_auto_import, nowarn_unused_vars, nowarn_unused_function, nowarn_nomatch, inline]).
-define(FILEPATH, "src/geokit/distance.gleam").
-export([haversine/2, haversine_km/2]).

-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(
    " Great-circle distance between two `LatLng` points.\n"
    "\n"
    " The haversine formula is used; see\n"
    " <https://en.wikipedia.org/wiki/Haversine_formula>. Distances are\n"
    " reported in metres using the WGS84 mean Earth radius\n"
    " (6_371_008.8 m). The error against a Vincenty ellipsoidal\n"
    " distance is bounded by 0.5 % for any two points on Earth, which\n"
    " matches every consumer-grade mapping API.\n"
).

-file("src/geokit/distance.gleam", 67).
-spec newton_sqrt(float(), float(), integer()) -> float().
newton_sqrt(Value, Guess, Iterations) ->
    gleam@bool:guard(
        (Iterations =< 0) orelse (Guess =:= +0.0),
        Guess,
        fun() -> newton_sqrt(Value, (Guess + (case Guess of
                    +0.0 -> +0.0;
                    -0.0 -> -0.0;
                    Gleam@denominator -> Value / Gleam@denominator
                end)) / 2.0, Iterations - 1) end
    ).

-file("src/geokit/distance.gleam", 62).
?DOC(
    " Square root of a non-negative input via Newton iteration. Caller\n"
    " must ensure the argument is `>= 0`; on a negative input the\n"
    " function returns `0.0`.\n"
).
-spec sqrt_nonneg(float()) -> float().
sqrt_nonneg(X) ->
    gleam@bool:guard(X =< +0.0, +0.0, fun() -> newton_sqrt(X, X, 32) end).

-file("src/geokit/distance.gleam", 30).
?DOC(
    " Great-circle distance between `a` and `b`, in metres.\n"
    "\n"
    " ```gleam\n"
    " import geokit/distance\n"
    " import geokit/latlng\n"
    "\n"
    " let assert Ok(tokyo) = latlng.new(lat: 35.6812, lng: 139.7671)\n"
    " let assert Ok(osaka) = latlng.new(lat: 34.6937, lng: 135.5023)\n"
    " distance.haversine(a: tokyo, b: osaka)\n"
    " // ~= 396_900.0 (≈ 397 km)\n"
    " ```\n"
).
-spec haversine(geokit@latlng:lat_lng(), geokit@latlng:lat_lng()) -> float().
haversine(A, B) ->
    Lat_a = gleam_community@maths:degrees_to_radians(geokit@latlng:lat(A)),
    Lat_b = gleam_community@maths:degrees_to_radians(geokit@latlng:lat(B)),
    Delta_lat = gleam_community@maths:degrees_to_radians(
        geokit@latlng:lat(B) - geokit@latlng:lat(A)
    ),
    Delta_lng = gleam_community@maths:degrees_to_radians(
        geokit@latlng:lng(B) - geokit@latlng:lng(A)
    ),
    Half_lat = Delta_lat / 2.0,
    Half_lng = Delta_lng / 2.0,
    Sin_half_lat = gleam_community@maths:sin(Half_lat),
    Sin_half_lng = gleam_community@maths:sin(Half_lng),
    H = (Sin_half_lat * Sin_half_lat) + (((gleam_community@maths:cos(Lat_a) * gleam_community@maths:cos(
        Lat_b
    ))
    * Sin_half_lng)
    * Sin_half_lng),
    H_clamped = gleam@float:clamp(H, +0.0, 1.0),
    Root_h = sqrt_nonneg(H_clamped),
    Root_one_minus_h = sqrt_nonneg(1.0 - H_clamped),
    C = 2.0 * gleam_community@maths:atan2(Root_h, Root_one_minus_h),
    6371008.8 * C.

-file("src/geokit/distance.gleam", 55).
?DOC(
    " Great-circle distance between `a` and `b`, in kilometres.\n"
    " Convenience wrapper around [`haversine`](#haversine).\n"
).
-spec haversine_km(geokit@latlng:lat_lng(), geokit@latlng:lat_lng()) -> float().
haversine_km(A, B) ->
    haversine(A, B) / 1000.0.