-module(geokit@simplify).
-compile([no_auto_import, nowarn_unused_vars, nowarn_unused_function, nowarn_nomatch, inline]).
-define(FILEPATH, "src/geokit/simplify.gleam").
-export([line_string/2, compute/2]).
-export_type([simplify_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(
" Douglas-Peucker line simplification.\n"
"\n"
" Reduces the number of points in a `LineString` while preserving\n"
" its general shape: any point further than `tolerance` from the\n"
" straight line between its neighbours is kept; closer ones are\n"
" discarded.\n"
"\n"
" `tolerance` is a distance in *degrees* on the lat/lng plane — no\n"
" projection is applied. For polylines spanning more than a few\n"
" degrees, project to Web Mercator first\n"
" (see [`geokit/mercator`](./mercator.html)) and convert your\n"
" tolerance to pixels. For short segments (city-scale and below)\n"
" the planar approximation is well within rendering tolerance.\n"
"\n"
" Reference:\n"
" Douglas, D.; Peucker, T. (1973), \"Algorithms for the reduction\n"
" of the number of points required to represent a digitized line\n"
" or its caricature\".\n"
).
-type simplify_error() :: {negative_tolerance, float()}.
-file("src/geokit/simplify.gleam", 187).
-spec merge_skipping_duplicate_pivot(
list(geokit@latlng:lat_lng()),
list(geokit@latlng:lat_lng())
) -> list(geokit@latlng:lat_lng()).
merge_skipping_duplicate_pivot(Left, Right) ->
case Right of
[] ->
Left;
[_ | Tail] ->
lists:append(Left, Tail)
end.
-file("src/geokit/simplify.gleam", 267).
-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/simplify.gleam", 262).
-spec sqrt_nonneg(float()) -> float().
sqrt_nonneg(Value) ->
gleam@bool:guard(
Value =< +0.0,
+0.0,
fun() -> newton_sqrt(Value, Value, 32) end
).
-file("src/geokit/simplify.gleam", 235).
-spec perpendicular_distance(
geokit@latlng:lat_lng(),
geokit@latlng:lat_lng(),
geokit@latlng:lat_lng()
) -> float().
perpendicular_distance(P, A, B) ->
X = geokit@latlng:lng(P),
Y = geokit@latlng:lat(P),
X1 = geokit@latlng:lng(A),
Y1 = geokit@latlng:lat(A),
X2 = geokit@latlng:lng(B),
Y2 = geokit@latlng:lat(B),
Dx = X2 - X1,
Dy = Y2 - Y1,
Length_squared = (Dx * Dx) + (Dy * Dy),
case Length_squared =:= +0.0 of
true ->
Edx = X - X1,
Edy = Y - Y1,
sqrt_nonneg((Edx * Edx) + (Edy * Edy));
false ->
Cross = ((X - X1) * Dy) - ((Y - Y1) * Dx),
Abs_cross = case Cross < +0.0 of
true ->
+0.0 - Cross;
false ->
Cross
end,
case sqrt_nonneg(Length_squared) of
+0.0 -> +0.0;
-0.0 -> -0.0;
Gleam@denominator -> Abs_cross / Gleam@denominator
end
end.
-file("src/geokit/simplify.gleam", 199).
-spec max_perp_distance(
list(geokit@latlng:lat_lng()),
geokit@latlng:lat_lng(),
geokit@latlng:lat_lng(),
integer(),
float(),
integer()
) -> {float(), integer()}.
max_perp_distance(Middle, A, B, Index, Best_dist, Best_index) ->
case Middle of
[] ->
{Best_dist, Best_index};
[Head | Tail] ->
D = perpendicular_distance(Head, A, B),
case D > Best_dist of
true ->
max_perp_distance(Tail, A, B, Index + 1, D, Index);
false ->
max_perp_distance(
Tail,
A,
B,
Index + 1,
Best_dist,
Best_index
)
end
end.
-file("src/geokit/simplify.gleam", 282).
-spec first_or_default(list(geokit@latlng:lat_lng())) -> geokit@latlng:lat_lng().
first_or_default(Points) ->
case Points of
[] ->
geokit@latlng:wrap(+0.0, +0.0);
[Head | _] ->
Head
end.
-file("src/geokit/simplify.gleam", 289).
-spec last_or_default(list(geokit@latlng:lat_lng()), geokit@latlng:lat_lng()) -> geokit@latlng:lat_lng().
last_or_default(Points, Fallback) ->
case Points of
[] ->
Fallback;
[Single] ->
Single;
[_ | Tail] ->
last_or_default(Tail, Fallback)
end.
-file("src/geokit/simplify.gleam", 305).
-spec drop_last(list(geokit@latlng:lat_lng())) -> list(geokit@latlng:lat_lng()).
drop_last(Points) ->
case Points of
[] ->
[];
[_] ->
[];
[Head | Tail] ->
[Head | drop_last(Tail)]
end.
-file("src/geokit/simplify.gleam", 297).
-spec middle_of(list(geokit@latlng:lat_lng())) -> list(geokit@latlng:lat_lng()).
middle_of(Points) ->
case Points of
[] ->
[];
[_] ->
[];
[_ | Tail] ->
drop_last(Tail)
end.
-file("src/geokit/simplify.gleam", 156).
-spec dp(list(geokit@latlng:lat_lng()), float()) -> list(geokit@latlng:lat_lng()).
dp(Points, Tolerance) ->
Length = erlang:length(Points),
case Length =< 2 of
true ->
Points;
false ->
First = first_or_default(Points),
Last = last_or_default(Points, First),
Middle = middle_of(Points),
{Max_dist, Max_index} = max_perp_distance(
Middle,
First,
Last,
1,
+0.0,
0
),
case Max_dist > Tolerance of
true ->
Left_part = gleam@list:take(Points, Max_index + 1),
Right_part = gleam@list:drop(Points, Max_index),
Left = dp(Left_part, Tolerance),
Right = dp(Right_part, Tolerance),
merge_skipping_duplicate_pivot(Left, Right);
false ->
[First, Last]
end
end.
-file("src/geokit/simplify.gleam", 54).
?DOC(
" Simplify a sequence of points using Douglas-Peucker.\n"
" `tolerance` is in degrees. Larger values keep fewer points.\n"
"\n"
" A tolerance of `0.0` is the **canonical Douglas-Peucker\n"
" behaviour**: it drops every intermediate point that lies\n"
" *exactly* on the straight line between its surviving neighbours,\n"
" because the comparison `perpendicular_distance >. tolerance` is\n"
" strict. Use a tiny positive value (for example `1.0e-12`) if you\n"
" want to preserve every point.\n"
"\n"
" ```gleam\n"
" import geokit/simplify\n"
" import geokit/latlng\n"
"\n"
" let assert Ok(a) = latlng.new(lat: 0.0, lng: 0.0)\n"
" let assert Ok(b) = latlng.new(lat: 0.0, lng: 0.5)\n"
" let assert Ok(c) = latlng.new(lat: 0.0, lng: 1.0)\n"
" let assert Ok(simplified) =\n"
" simplify.line_string(points: [a, b, c], tolerance: 0.001)\n"
" // simplified == [a, c] (b is on the straight line between a and c)\n"
" ```\n"
).
-spec line_string(list(geokit@latlng:lat_lng()), float()) -> {ok,
list(geokit@latlng:lat_lng())} |
{error, simplify_error()}.
line_string(Points, Tolerance) ->
gleam@bool:guard(
Tolerance < +0.0,
{error, {negative_tolerance, Tolerance}},
fun() -> case Points of
[] ->
{ok, []};
[Single] ->
{ok, [Single]};
[A, B] ->
{ok, [A, B]};
_ ->
{ok, dp(Points, Tolerance)}
end end
).
-file("src/geokit/simplify.gleam", 120).
-spec simplify_rings(
list(list(geokit@latlng:lat_lng())),
float(),
list(list(geokit@latlng:lat_lng()))
) -> {ok, list(list(geokit@latlng:lat_lng()))} | {error, simplify_error()}.
simplify_rings(Rings, Tolerance, Acc) ->
case Rings of
[] ->
{ok, lists:reverse(Acc)};
[Head | Tail] ->
gleam@result:'try'(
line_string(Head, Tolerance),
fun(Simplified) ->
simplify_rings(Tail, Tolerance, [Simplified | Acc])
end
)
end.
-file("src/geokit/simplify.gleam", 137).
-spec simplify_polygons(
list(list(list(geokit@latlng:lat_lng()))),
float(),
list(list(list(geokit@latlng:lat_lng())))
) -> {ok, list(list(list(geokit@latlng:lat_lng())))} | {error, simplify_error()}.
simplify_polygons(Polygons, Tolerance, Acc) ->
case Polygons of
[] ->
{ok, lists:reverse(Acc)};
[Head | Tail] ->
gleam@result:'try'(
simplify_rings(Head, Tolerance, []),
fun(Simplified) ->
simplify_polygons(Tail, Tolerance, [Simplified | Acc])
end
)
end.
-file("src/geokit/simplify.gleam", 88).
?DOC(
" Simplify any [`Geometry`](../geometry.html#Geometry), matching the\n"
" [`bbox.compute`](../bbox.html#compute) /\n"
" [`centroid.compute`](../centroid.html#compute) call shape.\n"
"\n"
" - `Point` is returned unchanged.\n"
" - `LineString` is simplified via [`line_string`](#line_string).\n"
" - `Polygon` simplifies each ring; closure is preserved by always\n"
" keeping the first and last vertex of each ring.\n"
" - `MultiPolygon` recurses into each polygon.\n"
"\n"
" ```gleam\n"
" import geokit/geometry\n"
" import geokit/simplify\n"
"\n"
" let result =\n"
" geometry.LineString([a, b, c])\n"
" |> simplify.compute(tolerance: 0.001)\n"
" ```\n"
).
-spec compute(geokit@geometry:geometry(), float()) -> {ok,
geokit@geometry:geometry()} |
{error, simplify_error()}.
compute(Geometry, Tolerance) ->
gleam@bool:guard(
Tolerance < +0.0,
{error, {negative_tolerance, Tolerance}},
fun() -> case Geometry of
{point, _} ->
{ok, Geometry};
{line_string, Points} ->
gleam@result:map(
line_string(Points, Tolerance),
fun(Simplified) -> {line_string, Simplified} end
);
{polygon, Rings} ->
gleam@result:map(
simplify_rings(Rings, Tolerance, []),
fun(New_rings) -> {polygon, New_rings} end
);
{multi_polygon, Polygons} ->
gleam@result:map(
simplify_polygons(Polygons, Tolerance, []),
fun(New_polygons) -> {multi_polygon, New_polygons} end
)
end end
).