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