src/geokit@bbox.erl

-module(geokit@bbox).
-compile([no_auto_import, nowarn_unused_vars, nowarn_unused_function, nowarn_nomatch, inline]).
-define(FILEPATH, "src/geokit/bbox.gleam").
-export([compute/1]).
-export_type([b_box_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(
    " Bounding box computation for a [`Geometry`](./geometry.html#Geometry).\n"
    "\n"
    " A bounding box is the smallest axis-aligned rectangle that\n"
    " contains every point of the geometry. The box is reported as the\n"
    " south-west and north-east corners. Empty inputs produce\n"
    " [`EmptyGeometry`](#BBoxError).\n"
    "\n"
    " The implementation does **not** account for the antimeridian: a\n"
    " geometry that straddles 180° longitude is treated as if the\n"
    " world were a flat rectangle and produces a box that spans almost\n"
    " the entire globe in the longitude axis. Antimeridian-aware\n"
    " bounding boxes (the \"narrow\" variant chosen by `Turfjs`,\n"
    " `terraformer`, etc.) are an explicit design choice and are out\n"
    " of scope for this module.\n"
).

-type b_box_error() :: empty_geometry.

-file("src/geokit/bbox.gleam", 96).
-spec append_list(list(GPE), list(GPE)) -> list(GPE).
append_list(Left, Right) ->
    case Left of
        [] ->
            Right;

        [Head | Tail] ->
            [Head | append_list(Tail, Right)]
    end.

-file("src/geokit/bbox.gleam", 82).
-spec flatten_rings(list(list(geokit@latlng:lat_lng()))) -> list(geokit@latlng:lat_lng()).
flatten_rings(Rings) ->
    case Rings of
        [] ->
            [];

        [Head | Tail] ->
            append_list(Head, flatten_rings(Tail))
    end.

-file("src/geokit/bbox.gleam", 89).
-spec flatten_polygons(list(list(list(geokit@latlng:lat_lng())))) -> list(geokit@latlng:lat_lng()).
flatten_polygons(Polygons) ->
    case Polygons of
        [] ->
            [];

        [Head | Tail] ->
            append_list(flatten_rings(Head), flatten_polygons(Tail))
    end.

-file("src/geokit/bbox.gleam", 73).
-spec all_points(geokit@geometry:geometry()) -> list(geokit@latlng:lat_lng()).
all_points(Geometry) ->
    case Geometry of
        {point, P} ->
            [P];

        {line_string, Ps} ->
            Ps;

        {polygon, Rings} ->
            flatten_rings(Rings);

        {multi_polygon, Polygons} ->
            flatten_polygons(Polygons)
    end.

-file("src/geokit/bbox.gleam", 103).
-spec min_float(float(), float()) -> float().
min_float(A, B) ->
    gleam@bool:guard(A < B, A, fun() -> B end).

-file("src/geokit/bbox.gleam", 108).
-spec max_float(float(), float()) -> float().
max_float(A, B) ->
    gleam@bool:guard(A > B, A, fun() -> B end).

-file("src/geokit/bbox.gleam", 50).
-spec extend_loop(
    list(geokit@latlng:lat_lng()),
    float(),
    float(),
    float(),
    float()
) -> {float(), float(), float(), float()}.
extend_loop(Points, Min_lat, Max_lat, Min_lng, Max_lng) ->
    case Points of
        [] ->
            {Min_lat, Max_lat, Min_lng, Max_lng};

        [Head | Tail] ->
            Lat = geokit@latlng:lat(Head),
            Lng = geokit@latlng:lng(Head),
            extend_loop(
                Tail,
                min_float(Min_lat, Lat),
                max_float(Max_lat, Lat),
                min_float(Min_lng, Lng),
                max_float(Max_lng, Lng)
            )
    end.

-file("src/geokit/bbox.gleam", 29).
?DOC(" Compute the bounding box of `geometry` as `#(sw, ne)`.\n").
-spec compute(geokit@geometry:geometry()) -> {ok,
        {geokit@latlng:lat_lng(), geokit@latlng:lat_lng()}} |
    {error, b_box_error()}.
compute(Geometry) ->
    case all_points(Geometry) of
        [] ->
            {error, empty_geometry};

        [Head | Tail] ->
            {Min_lat, Max_lat, Min_lng, Max_lng} = extend_loop(
                Tail,
                geokit@latlng:lat(Head),
                geokit@latlng:lat(Head),
                geokit@latlng:lng(Head),
                geokit@latlng:lng(Head)
            ),
            Sw = geokit@latlng:wrap(Min_lat, Min_lng),
            Ne = geokit@latlng:wrap(Max_lat, Max_lng),
            {ok, {Sw, Ne}}
    end.