src/geokit@centroid.erl

-module(geokit@centroid).
-compile([no_auto_import, nowarn_unused_vars, nowarn_unused_function, nowarn_nomatch, inline]).
-define(FILEPATH, "src/geokit/centroid.gleam").
-export([compute/1]).
-export_type([centroid_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(
    " Centroid (geometric centre) of a [`Geometry`](./geometry.html#Geometry).\n"
    "\n"
    " For a `Point`, the centroid is the point itself. For a\n"
    " `LineString` it is the arithmetic mean of its vertices. For a\n"
    " `Polygon` it is the centroid of the exterior ring weighted by\n"
    " the signed area of each triangle, which is the correct mean for\n"
    " a planar polygon (Turfjs / Shapely use the same formula).\n"
    "\n"
    " All computations treat the Earth as a flat plane in lat/lng — no\n"
    " projection is applied. For polygons spanning more than a few\n"
    " degrees, project to Web Mercator first\n"
    " (see [`geokit/mercator`](./mercator.html)) for an\n"
    " area-accurate centroid.\n"
).

-type centroid_error() :: empty_geometry.

-file("src/geokit/centroid.gleam", 46).
-spec sum_points(list(geokit@latlng:lat_lng()), float(), float(), integer()) -> {float(),
    float(),
    integer()}.
sum_points(Points, Sum_lat, Sum_lng, Count) ->
    case Points of
        [] ->
            {Sum_lat, Sum_lng, Count};

        [Head | Tail] ->
            sum_points(
                Tail,
                Sum_lat + geokit@latlng:lat(Head),
                Sum_lng + geokit@latlng:lng(Head),
                Count + 1
            )
    end.

-file("src/geokit/centroid.gleam", 95).
-spec ring_sums(list(geokit@latlng:lat_lng()), float(), float(), float()) -> {float(),
    float(),
    float()}.
ring_sums(Points, Sum_x, Sum_y, Area_twice) ->
    case Points of
        [] ->
            {Sum_x, Sum_y, Area_twice};

        [_] ->
            {Sum_x, Sum_y, Area_twice};

        [A, B | Rest] ->
            X0 = geokit@latlng:lng(A),
            Y0 = geokit@latlng:lat(A),
            X1 = geokit@latlng:lng(B),
            Y1 = geokit@latlng:lat(B),
            Cross = (X0 * Y1) - (X1 * Y0),
            ring_sums(
                [B | Rest],
                Sum_x + ((X0 + X1) * Cross),
                Sum_y + ((Y0 + Y1) * Cross),
                Area_twice + Cross
            )
    end.

-file("src/geokit/centroid.gleam", 131).
-spec last_of(list(geokit@latlng:lat_lng()), geokit@latlng:lat_lng()) -> geokit@latlng:lat_lng().
last_of(Points, Fallback) ->
    case Points of
        [] ->
            Fallback;

        [Single] ->
            Single;

        [_ | Tail] ->
            last_of(Tail, Fallback)
    end.

-file("src/geokit/centroid.gleam", 139).
-spec append_one(list(geokit@latlng:lat_lng()), geokit@latlng:lat_lng()) -> list(geokit@latlng:lat_lng()).
append_one(Items, Value) ->
    case Items of
        [] ->
            [Value];

        [Head | Tail] ->
            [Head | append_one(Tail, Value)]
    end.

-file("src/geokit/centroid.gleam", 120).
-spec ensure_closed(list(geokit@latlng:lat_lng())) -> list(geokit@latlng:lat_lng()).
ensure_closed(Points) ->
    case Points of
        [] ->
            [];

        [Head | _] ->
            Last = last_of(Points, Head),
            gleam@bool:guard(
                geokit@latlng:equal(Head, Last),
                Points,
                fun() -> append_one(Points, Head) end
            )
    end.

-file("src/geokit/centroid.gleam", 157).
-spec int_to_float(integer()) -> float().
int_to_float(Value) ->
    erlang:float(Value).

-file("src/geokit/centroid.gleam", 38).
-spec mean_of_points(list(geokit@latlng:lat_lng())) -> {ok,
        geokit@latlng:lat_lng()} |
    {error, centroid_error()}.
mean_of_points(Points) ->
    gleam@bool:guard(
        gleam@list:is_empty(Points),
        {error, empty_geometry},
        fun() ->
            {Sum_lat, Sum_lng, Count} = sum_points(Points, +0.0, +0.0, 0),
            Count_f = int_to_float(Count),
            {ok, geokit@latlng:wrap(case Count_f of
                        +0.0 -> +0.0;
                        -0.0 -> -0.0;
                        Gleam@denominator -> Sum_lat / Gleam@denominator
                    end, case Count_f of
                        +0.0 -> +0.0;
                        -0.0 -> -0.0;
                        Gleam@denominator@1 -> Sum_lng / Gleam@denominator@1
                    end)}
        end
    ).

-file("src/geokit/centroid.gleam", 81).
-spec ring_centroid_weighted(list(geokit@latlng:lat_lng())) -> {ok,
        geokit@latlng:lat_lng()} |
    {error, centroid_error()}.
ring_centroid_weighted(Ring) ->
    Closed = ensure_closed(Ring),
    {Sum_x, Sum_y, Signed_area_twice} = ring_sums(Closed, +0.0, +0.0, +0.0),
    gleam@bool:guard(
        Signed_area_twice =:= +0.0,
        mean_of_points(Ring),
        fun() ->
            Factor = case (3.0 * Signed_area_twice) of
                +0.0 -> +0.0;
                -0.0 -> -0.0;
                Gleam@denominator -> 1.0 / Gleam@denominator
            end,
            {ok, geokit@latlng:wrap(Sum_y * Factor, Sum_x * Factor)}
        end
    ).

-file("src/geokit/centroid.gleam", 73).
-spec ring_centroid(list(geokit@latlng:lat_lng())) -> {ok,
        geokit@latlng:lat_lng()} |
    {error, centroid_error()}.
ring_centroid(Ring) ->
    case Ring of
        [] ->
            {error, empty_geometry};

        [Single] ->
            {ok, Single};

        _ ->
            ring_centroid_weighted(Ring)
    end.

-file("src/geokit/centroid.gleam", 64).
-spec polygon_centroid(list(list(geokit@latlng:lat_lng()))) -> {ok,
        geokit@latlng:lat_lng()} |
    {error, centroid_error()}.
polygon_centroid(Rings) ->
    case Rings of
        [] ->
            {error, empty_geometry};

        [Exterior | _] ->
            ring_centroid(Exterior)
    end.

-file("src/geokit/centroid.gleam", 146).
-spec multipolygon_centroid(list(list(list(geokit@latlng:lat_lng())))) -> {ok,
        geokit@latlng:lat_lng()} |
    {error, centroid_error()}.
multipolygon_centroid(Polygons) ->
    Centroids = gleam@list:filter_map(
        Polygons,
        fun(Polygon) -> polygon_centroid(Polygon) end
    ),
    case Centroids of
        [] ->
            {error, empty_geometry};

        _ ->
            mean_of_points(Centroids)
    end.

-file("src/geokit/centroid.gleam", 29).
?DOC(" Compute the centroid of `geometry`.\n").
-spec compute(geokit@geometry:geometry()) -> {ok, geokit@latlng:lat_lng()} |
    {error, centroid_error()}.
compute(Geometry) ->
    case Geometry of
        {point, P} ->
            {ok, P};

        {line_string, Points} ->
            mean_of_points(Points);

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

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