% Copyright (C) 2003-2019 Olivier Boudeville
%
% This file is part of the Ceylan-Myriad library.
%
% This library is free software: you can redistribute it and/or modify
% it under the terms of the GNU Lesser General Public License or
% the GNU General Public License, as they are published by the Free Software
% Foundation, either version 3 of these Licenses, or (at your option)
% any later version.
% You can also redistribute it and/or modify it under the terms of the
% Mozilla Public License, version 1.1 or later.
%
% This library is distributed in the hope that it will be useful,
% but WITHOUT ANY WARRANTY; without even the implied warranty of
% MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
% GNU Lesser General Public License and the GNU General Public License
% for more details.
%
% You should have received a copy of the GNU Lesser General Public
% License, of the GNU General Public License and of the Mozilla Public License
% along with this library.
% If not, see and
% .
%
% Author: Olivier Boudeville [olivier (dot) boudeville (at) esperide (dot) com]
% Creation date: Monday, February 15, 2010.
% Gathering of various two dimensional linear facilities.
%
% See linear_2D_test.erl for the corresponding test.
%
-module(linear_2D).
% Relatively aggressive inlining for basic operations:
-compile( inline ).
-compile( { inline_size, 48 } ).
% Operations on points:
%
-export([ get_origin/0, are_close/2, is_within/3, is_within_square/3,
square_distance/2, distance/2, cross_product/2, roundify/1,
get_integer_center/2, get_center/2, translate/2 ]).
% Operations on vectors:
%
-export([ vectorize/2, square_magnitude/1, magnitude/1, scale/2, make_unit/1,
normal_left/1, normal_right/1, dot_product/2 ]).
% Operations on lines:
%
-export([ get_line/2, intersect/2, get_abscissa_for_ordinate/2 ]).
% Operations related to angles:
%
-export([ is_strictly_on_the_right/3, is_obtuse/1, abs_angle_rad/3, angle_rad/3,
abs_angle_deg/3, angle_deg/3 ]).
% Operations on set of points:
%
-export([ compute_smallest_enclosing_rectangle/1,
compute_max_overall_distance/1, compute_convex_hull/1 ]).
% Textual conversions:
%
-export([ to_string/1, to_string/2 ]).
% Only useful for tests:
%
-export([ find_pivot/1, sort_by_angle/2 ]).
% For epsilon:
-include("math_utils.hrl").
% Shorthands:
-type coordinate() :: linear:coordinate().
-type factor() :: linear:factor().
% Implementation notes:
%
% A typical use for integer operations is GUI-related processing.
% 2D point, with floating-point coordinates:
%
-type point() :: { coordinate(), coordinate() }.
% 2D point, with integer coordinates:
%
-type integer_point() :: { linear:integer_coordinate(),
linear:integer_coordinate() }.
% { Width, Height }:
-type dimensions() :: { linear:coordinate(),
linear:coordinate() }.
-type integer_dimensions() :: { linear:integer_coordinate(),
linear:integer_coordinate() }.
% A 2D vector, with floating-point coordinates.
%
% They are typically referenced as [ X, Y ].
%
-type vector() :: { coordinate(), coordinate() }.
-type integer_vector() :: { linear:integer_coordinate(),
linear:integer_coordinate() }.
% A line, whose equation A.x+B.y+C=0, can be defined by its three factors
% {A,B,C}.
%
-type line() :: { factor(), factor(), factor() }.
-type shape() :: 'circle' | 'rectangle' | 'square' | 'triangle' | 'polygon'.
-export_type([ point/0, integer_point/0, dimensions/0, integer_dimensions/0,
vector/0, integer_vector/0, line/0, shape/0 ]).
% Point section.
% Returns the origin of this referential.
%
-spec get_origin() -> point().
get_origin() ->
{ 0, 0 }.
% Returns whether the two specified points are close, i.e. if they could be
% considered as representing the same point (equality operator on points).
%
-spec are_close( point(), point() ) -> boolean().
are_close( _P1={X1,Y1}, _P2={X2,Y2} ) ->
math_utils:are_close( X1, X2 ) andalso math_utils:are_close( Y1, Y2 ).
% Tells whether point P is within a distance D from point C, using some margin
% to overcome numerical errors.
%
-spec is_within( point(), point(), number() ) -> boolean().
is_within( P, C, D ) ->
% "Taylor series", square(epsilon) is negligible here:
square_distance( P, C ) < D * ( D + ?epsilon ).
% Tells whether point P is within a square distance SquareD from point C.
%
-spec is_within_square( point(), point(), number() ) -> boolean().
is_within_square( P, C, SquareD ) ->
square_distance( P, C ) < SquareD.
% Returns the square of the distance between the two specified points.
%
% For comparison purposes, computing the square root is useless.
%
% Could rely on vectorize and square_magnitude as well.
%
-spec square_distance( point(), point() ) -> linear:square_distance().
square_distance( {X1,Y1}, {X2,Y2} ) ->
XDiff = X2-X1,
YDiff = Y2-Y1,
XDiff*XDiff + YDiff*YDiff.
% Returns the distance between the two specified points.
%
% For comparison purposes, computing the square root is useless.
%
% Could rely on vectorize and magnitude as well.
%
-spec distance( point(), point() ) -> linear:distance().
distance( P1, P2 ) ->
math:sqrt( square_distance( P1, P2 ) ).
% Returns the cross-product of the two specified 2D points, i.e. the magnitude
% of the vector that would result from a regular 3D cross product of the input
% vectors, taking their Z values implicitly as 0.
-spec cross_product( point(), point() ) -> number().
cross_product( {X1,Y1}, {X2,Y2} ) ->
X1*Y2 - Y1*X2.
% Returns a point (or vector) whose coordinates have been rounded to nearest
% integer.
%
-spec roundify( point() ) -> integer_point().
roundify( {X,Y} ) ->
{ erlang:round(X), erlang:round(Y) }.
% Returns a vertex corresponding the middle of the two specified vertices,
% returned with integer coordinates.
%
-spec get_integer_center( point(), point() ) -> integer_point().
get_integer_center( P1, P2 ) ->
roundify( get_center( P1, P2 ) ).
% Returns a vertex corresponding the middle of the two specified vertices,
% returned with possibly floating-point coordinates.
%
-spec get_center( point(), point() ) -> point().
get_center( {X1,Y1}, {X2,Y2} ) ->
{ (X1+X2)/2, (Y1+Y2)/2 }.
% Returns a point corresponding to the specified point P translated by the
% specified vector V.
%
-spec translate( point(), vector() ) -> point().
translate( _P={X,Y}, _V={Vx,Vy} ) ->
{ X+Vx, Y+Vy }.
% Section for sets of points.
% Computes the smallest rectangle that encloses the specified list of points.
%
% Returns { TopLeft, BottomRight }.
%
-spec compute_smallest_enclosing_rectangle( [ point() ] ) ->
{ point(), point() }.
compute_smallest_enclosing_rectangle( Points ) ->
compute_smallest_enclosing_rectangle( Points, undefined, undefined ).
% Helper:
compute_smallest_enclosing_rectangle( _Points=[], TopLeft, BottomRight ) ->
{ TopLeft, BottomRight };
compute_smallest_enclosing_rectangle( [ _Points=P | Others ], undefined,
undefined ) ->
% First found initializes best, knowing at least two points are expected:
compute_smallest_enclosing_rectangle( Others, _TopLeft=P, _BottomRight=P );
compute_smallest_enclosing_rectangle( [ _Points={ X, Y } | Others ], { Xt, Yt },
{ Xb, Yb } ) ->
Xmin = erlang:min( X, Xt ),
Ymin = erlang:min( Y, Yt ),
Xmax = erlang:max( X, Xb ),
Ymax = erlang:max( Y, Yb ),
compute_smallest_enclosing_rectangle( Others, { Xmin, Ymin },
{ Xmax, Ymax } ).
% Computes the maximum distance between two points in the specified list of
% points.
%
% Returns {P1,P2,square_distance(P1,P2)} so that (square) distance is maximal.
% We ensure that each internal edge is examined only once: when the distance
% between a given vertex V and all other vertices have been computed, V is
% removed from the list and a new mAximum is searched within this subset.
%
% Here there is only one vertex left:
%
-spec compute_max_overall_distance( [ point() ] ) ->
{ point(), point(), linear:square_distance() }.
compute_max_overall_distance( Points ) when length( Points ) < 2 ->
throw( { no_computable_overall_distance, Points } );
compute_max_overall_distance( Points ) ->
compute_max_overall_distance( Points, undefined ).
% Helper.
compute_max_overall_distance( _Points=[ _H ], Longest ) ->
Longest;
% Here we have not compute a distance yet:
compute_max_overall_distance( _Points=[ H | Others ], undefined ) ->
FirstEntry = compute_max_distance_between( H, Others ),
compute_max_overall_distance( Others, _FirstBest=FirstEntry );
% At least one other vertex remains, and at least one distance was computed:
compute_max_overall_distance( _Points=[ H | Others ],
Best={ _V1, _V2, LongestSquareDistance } ) ->
case compute_max_distance_between( H, Others ) of
NewBest={ _H, _PmaxForH, LongestSquareDistanceFromH }
when LongestSquareDistanceFromH > LongestSquareDistance ->
% We have a new winner:
compute_max_overall_distance( Others, NewBest );
_Other ->
% Here LongestSquareDistance is not beaten:
compute_max_overall_distance( Others, Best )
end.
% Computes the maximum distance between a point (P) and a list of other points.
%
% Returns {P,Pmax,LongestSquareDistance} with LongestSquareDistance being the
% distance between P and Pmax, Pmax being chosen so that LongestSquareDistance
% is maximal.
%
% As there must have been at least one point in the list, Pmax exists here
% (never undefined):
-spec compute_max_distance_between( point(), [ point() ] )->
{ point(), point(), linear:square_distance() }.
compute_max_distance_between( _P, [] ) ->
throw( no_computable_max_distance );
compute_max_distance_between( P, Points ) ->
compute_max_distance_between( P, Points, undefined ).
compute_max_distance_between( P, _Points=[], {Pmax,LongestSquareDistance} ) ->
{ P, Pmax, LongestSquareDistance };
compute_max_distance_between( P, _Points=[ Pnew | OtherPoints ], undefined ) ->
% First point examined is at first by construction the first best:
compute_max_distance_between( P, OtherPoints,
{ Pnew, linear_2D:square_distance( P, Pnew ) } );
compute_max_distance_between( P, _Points=[ Pnew | OtherPoints ],
Best={ _Pmax, LongestSquareDistance } ) ->
case linear_2D:square_distance( P, Pnew ) of
SquareDistance when SquareDistance > LongestSquareDistance ->
% We have a new winner:
compute_max_distance_between( P, OtherPoints,
{ Pnew, SquareDistance } );
_LesserSquareDistance ->
% Previous best not beaten, let's keep it:
compute_max_distance_between( P, OtherPoints, Best )
end.
% Sorting by angle section.
% Finds the pivot, i.e. the leftmost point with the highest ordinate.
%
% The point list is supposed not having duplicates.
%
% Returns {Pivot,PivotLessList} where PivotLessList is the (unordered) input
% list, without the Pivot.
%
-spec find_pivot( [ point() ] ) -> { point(), [ point() ] }.
find_pivot( _PointList = [ FirstPivot | Others ] ) ->
% First found is the first pivot:
find_pivot( Others, FirstPivot, _NewList=[] ).
% Helper:
find_pivot( _Points=[], Pivot, NewList ) ->
{ Pivot, NewList };
% Higher than the pivot, thus not wanted as pivot:
find_pivot( [ Point={_X,Y} | Others ], Pivot={ _Xp, Yp }, NewList ) when Y
find_pivot( Others, Pivot, [ Point | NewList ] );
% Lower than the pivot, thus wanted:
find_pivot( [ Point={_X,Y} | Others ], PreviousPivot={ _Xp, Yp }, NewList )
when Y>Yp ->
find_pivot( Others, Point, [ PreviousPivot | NewList ] );
% Same level as the pivot, but at its right, thus not wanted:
find_pivot( [ Point={X,_Y} | Others ], Pivot={ Xp, _Y }, NewList ) when X>Xp ->
find_pivot( Others, Pivot, [ Point | NewList ] );
% Same level as the pivot, but at its left, thus wanted:
find_pivot( [ Point={X,_Yp} | Others ], PreviousPivot={ Xp, _Yp }, NewList )
when X
find_pivot( Others, Point, [ PreviousPivot | NewList ] );
% Duplicated pivot, abnormal:
find_pivot( [ Pivot | _Others ], Pivot, _NewList ) ->
throw( { duplicate_pivot, Pivot } ).
% Returns a list containing the points sorted according to an increasing angle
% between the abscissa axis and the vector from the pivot that each point.
%
% Note: all points having the same abscissa as the pivot, except the highest
% one, will be removed from the returned list.
%
-spec sort_by_angle( point(), [ point() ] ) -> [ point() ].
sort_by_angle( Pivot, Points ) ->
sort_by_angle( Pivot, Points, _LeftPoints=[], _MiddlePoint=undefined,
_RightPoints=[] ).
% Helper.
%
% LeftPoints and RightPoints are lists of {Angle,Point} pairs.
%
-spec sort_by_angle( point(), [ point() ], [ { number(), point() } ],
'undefined', [ { number(), point() } ] ) -> [ point() ];
( point(), [ point() ], [ { number(), point() } ], point(),
[ { number(), point() } ] ) -> [ point() ].
sort_by_angle( _Pivot, _Points=[], LeftPoints, undefined, RightPoints ) ->
%trace_utils:debug_fmt( "sort_by_angle: no middle point found." ),
% Not having a middle point to integrate here:
L = lists:keysort( _Index=1, LeftPoints )
++ lists:keysort( _Index=1, RightPoints ),
%io:format( "Full list: ~w.~n", [ L ] ),
reverse_and_drop_angle( L, [] );
sort_by_angle( _Pivot, _Points=[], LeftPoints, MiddlePoint, RightPoints ) ->
%io:format( "sort_by_angle: at least one middle point found.~n" ),
L = lists:keysort( _Index=1, LeftPoints )
++ [ {dummy,MiddlePoint} | lists:keysort( _Index=1, RightPoints ) ],
reverse_and_drop_angle( L, [] );
% Note that Y<=Yp by definition of the pivot, hence Y-Yp<=0:
sort_by_angle( Pivot={Xp,Yp}, [ Point={X,Y} | T ], LeftPoints, MiddlePoint,
RightPoints ) ->
case X-Xp of
0 ->
% Here we are just above the pivot, tan(Pi/2) is infinite.
case MiddlePoint of
undefined ->
% First found is first best:
sort_by_angle( Pivot, T, LeftPoints, Point, RightPoints );
{ _Xm, Ym } ->
case Y < Ym of
true ->
% This point is above the previous highest middle
% point, previous middle point can be dropped on the
% floor:
sort_by_angle( Pivot, T, LeftPoints, Point,
RightPoints );
false ->
% The current point can be dropped on the floor, as
% it is below the highest middle point:
sort_by_angle( Pivot, T, LeftPoints, MiddlePoint,
RightPoints )
end
end;
DeltaX when DeltaX > 0 ->
% This is a point on the right of the pivot, stores the tangent of
% the angle the vector defined by the pivot and that point makes
% with the abscissa axis:
sort_by_angle( Pivot, T, LeftPoints, MiddlePoint,
[ { (Y-Yp) / DeltaX, Point } | RightPoints ] );
NegativeDeltaX ->
% This is a point on the left of the pivot:
sort_by_angle( Pivot, T,
[ { (Y-Yp) / NegativeDeltaX, Point } | LeftPoints ],
MiddlePoint, RightPoints )
end.
% Helper:
reverse_and_drop_angle( [], Acc ) ->
Acc;
reverse_and_drop_angle( [ {_Tangent,Point} | T ], Acc ) ->
reverse_and_drop_angle( T, [ Point | Acc ] ).
% Vector section.
% Returns a vector V made from the specified two points: V=P2-P1.
%
-spec vectorize( point(), point() ) -> vector().
vectorize( _P1={X1,Y1}, _P2={X2,Y2} ) ->
{ X2-X1, Y2-Y1 }.
% Returns the square of the magnitude of the specified vector.
%
-spec square_magnitude( vector() ) -> linear:square_distance().
square_magnitude( _V={X,Y} ) ->
X*X + Y*Y.
% Returns the magnitude of the specified vector.
%
-spec magnitude( vector() ) -> linear:distance().
magnitude( V ) ->
math:sqrt( square_magnitude(V) ).
% Scales specified vector of specified factor.
%
-spec scale( vector(), number() ) -> vector().
scale( _V={X,Y}, Factor ) ->
{ Factor*X, Factor*Y }.
% Returns the specified vector with an unit length (magnitude of 1):
%
% (epsilon-based test for null vectors with floating-point coordinates could
% be done here).
%
-spec make_unit( vector() ) -> vector().
make_unit( {0,0} ) ->
throw( cannot_make_null_vector_unit );
make_unit( V ) ->
scale( V, 1 / magnitude( V ) ).
% Returns a (non-unit) vector which is normal to specified vector V, and is on
% the left of V in the standard basis.
%
-spec normal_left( vector() ) -> vector().
normal_left( _V={X,Y} ) ->
{ -Y, X }.
% Returns a (non-unit) vector which is normal to specified vector V, and is on
% the right of V in the standard basis.
%
-spec normal_right( vector() ) -> vector().
normal_right( _V={X,Y} ) ->
{ Y, -X }.
% Returns the dot product of the two specified vectors.
-spec dot_product( vector(), vector() ) -> number().
dot_product( _V1={X1,Y1}, _V2={X2,Y2} ) ->
X1*X2 + Y1*Y2.
% Line section.
% Returns the three coefficients {A,B,C} for the line passing by point P and
% being perpendicular to vector V, whose equation A.x+B.y+C=0.
%
-spec get_line( point(), point() ) -> line().
get_line( _P={Xp,Yp}, _V={Vx,Vy} ) ->
% Here we know that:
% P is on the line: A.Xp+B.Yp+C=0 (I)
% Let M be (X,Y). If M is on the line, the PM.V=0 (null dot product), thus:
% (X-Xp)*Vx+(Y-Yp)*Vy=0 (II)
% We want to determine A, B and C:
% (II) is: Vx.X + Vy*Y - (Xp.Vx+Yp.Vy) = 0 thus:
A=Vx,
B=Vy,
C = - ( Xp*Vx + Yp*Vy ),
{ A, B, C }.
% Returns the intersection of the two specified lines, if it is a point,
% otherwise the atom no_point (the intersection can be void, if the lines are
% parallel but different, or a full line, if they are the same line).
%
% First line has for equation a.x+b.y+c=0, second has for equation u.x+v.y+w=0.
%
-spec intersect( line(), line() ) -> 'no_point' | point().
intersect( _D1={A,B,C}, _D2={U,V,W} ) ->
% We will try to substitute y, as determined from first equation, into the
% second one:
case B of
0 ->
% Thus A.X = -C
case A of
0 ->
% Either empty or the same:
no_point;
_ ->
X = -C/A,
case V of
0 ->
no_point;
_ ->
Y= -(W+U*X) / V,
{X,Y}
end
end;
_ ->
% General case: Y = - (C+A.X)/B (I), will be replaced in second
% equation:
case U of
0 ->
% Thus Y:
Y= -W/V,
% Now X from first:
case A of
0 ->
no_point;
_ ->
X= - (B*Y+C)/A,
{X,Y}
end;
_ ->
% General case, substituing (I) in second equation we have:
% (B.U-V.A).X = V.C-B.D
case B*U-V*A of
0 ->
no_point;
Denom ->
X = (V*C-B*W) / Denom,
Y = - (C+A*X) / B,
{X,Y}
end
end
end.
% Returns the abscissa of a point on line L having Y for ordinate.
%
% Line L must not have for equation Y=constant (i.e. its A parameter must not be
% null).
%
-spec get_abscissa_for_ordinate( line(), coordinate() ) -> coordinate().
get_abscissa_for_ordinate( _L={A,B,C}, Y ) ->
% For y=K, x=-(C+BK)/A
-(C+B*Y) / A.
% Angle section.
% Returns true iff P is strictly on the right of the oriented segment going from
% P1 to P2.
%
-spec is_strictly_on_the_right( point(), point(), point() ) -> boolean().
is_strictly_on_the_right( P, P1, P2 ) ->
Vec_P1P2 = vectorize( P1, P2 ),
RightNormal = normal_right( Vec_P1P2 ),
Vec_P1P = vectorize( P1, P ),
DotProduct = dot_product( Vec_P1P, RightNormal ),
DotProduct > 0.
% Returns whether specified angle (in degrees, canonical form) is obtuse.
%
-spec is_obtuse( unit_utils:int_degrees() ) -> boolean().
is_obtuse( AngleInDegrees ) ->
AngleInDegrees > 90 andalso AngleInDegrees < 180.
% Returns the angle, in radians, between the vector AB and AC.
%
% Note: with this function we cannot tell whether one vector is ahead of the
% other, i.e. if we should use the returned angle or its opposite to go from AB
% to AC.
%
-spec abs_angle_rad( point(), point(), point() ) -> unit_utils:radians().
abs_angle_rad( A, B, C ) ->
AB = vectorize( A, B ),
M1 = magnitude( AB ),
case math_utils:is_null( M1 ) of
true ->
throw( { degenerate_angle, { A, B } } );
_ ->
ok
end,
AC = vectorize( A, C ),
M2 = magnitude( AC ),
case math_utils:is_null( M2 ) of
true ->
throw( { degenerate_angle,{ A, C } } );
_ ->
ok
end,
%io:format( "AB=~w, AC=~w, M1=~f, M2=~f.~n", [AB,AC,M1,M2] ),
math:acos( dot_product( AB, AC ) / ( magnitude( AB ) * magnitude( AC ) ) ).
% Returns the signed (oriented) angle, in radians, between the vector AB and AC.
%
% Note: with this function we can tell that we must rotate counter-clockwise of
% the returned angle to go from AB to AC.
%
-spec angle_rad( point(), point(), point() ) -> unit_utils:radians().
angle_rad( A, B, C ) ->
{ X1, Y1 } = vectorize( A, B ),
{ X2, Y2 } = vectorize( A, C ),
math:atan2( Y2, X2 ) - math:atan2( Y1, X1 ).
% Returns the angle, in canonical degrees, between the vector AB and AC.
%
% Note: with this function we cannot tell whether one vector is ahead of the
% other, i.e. if we should use the returned angle or its opposite to go from AB
% to AC.
%
-spec abs_angle_deg( point(), point(), point() ) -> unit_utils:int_degrees().
abs_angle_deg( A, B, C ) ->
math_utils:canonify( math_utils:radian_to_degree(
abs_angle_rad( A, B, C ) ) ).
% Returns the signed (oriented) angle, in canonical degrees, between the vector
% AB and AC.
%
% Note: with this function we can tell that we must rotate counter-clockwise of
% the returned angle to go from AB to AC.
%
-spec angle_deg( point(), point(), point() ) -> unit_utils:int_degrees().
angle_deg( A, B, C ) ->
math_utils:canonify( math_utils:radian_to_degree(
angle_rad( A, B, C ) ) ).
% Convex hull section.
% Computes the convex hull corresponding to the specified list of points.
%
% Returns the list of points that defines the hull.
%
-spec compute_convex_hull( [point()] ) -> [point()].
compute_convex_hull( Points ) ->
{ Pivot, RemainingPoints } = find_pivot( Points ),
case length( RemainingPoints ) of
Len when Len < 2 ->
throw( not_enough_points_for_convex_hull );
_Other ->
% We have at least 2 points in addition to the pivot.
%io:format( "Pivot is ~w, remaining points: ~w.~n",
% [Pivot,RemainingPoints] ),
[ P1, P2 | T ] = sort_by_angle( Pivot, RemainingPoints ),
% Initially only the pivot is known to belong to the convex hull.
% We had P1, next to be validated against P2.
%
% We also add the pivot to the end of the NextPoints list, so that
% the hull can be closed.
compute_graham_scan_hull( _ToValidate=[ P1, Pivot ],
_NewPoint=P2, _NextPoints=( T++ [ Pivot ] ) )
end.
% Computes the Graham scan for the specified list of points, expected to be
% already sorted by increasing angle between the abscissa axis and the vector
% from the pivot to each of these points (i.e. in increasing order of the angle
% they and the point P make with the x-axis, in counter-clockwise order).
%
% See: http://en.wikipedia.org/wiki/Graham_scan
%
% Returns the corresponding convex hull, in clock-wise order.
%
-spec compute_graham_scan_hull( [ point() ], point(), [ point() ] ) ->
[ point() ].
compute_graham_scan_hull( ToValidate, _Pivot, _NextPoints=[] ) ->
% Last new point is by construction always to pivot.
%io:format( "compute_graham_scan_hull: "
% "exhausted input points, returning: ~w.~n", [ ToValidate ] ),
ToValidate;
compute_graham_scan_hull( ToValidate=[ P2, P1 | T ], NewPoint,
NextPoints=[ Next | OtherNext ] ) ->
% Should P2 be on the line defined by P1 and NewPoint, then P2 will be
% considered as not being on the left: in the convex hull, only necessary
% points will be kept, i.e. no point on the boundary of the hull will be
% kept.
%
% Note: The test seems to be wrongly negated; however it is correct as
% actually we describe the algorithm as seen if represented in a basis whose
% ordinates are increasing when going from the top to the bottom (i.e. like
% when rendered on a classical GUI, the Y axis going then downward).
%
case is_strictly_on_the_right( P2, P1, NewPoint ) of
false ->
%io:format( "compute_graham_scan_hull: point ~w is on the right of "
% "segment from ~w to ~w, keeping ~w.~n",
% [ P2, P1, NewPoint, P2 ] ),
% Here, the point P2 is on the right of the segment going from P1 to
% the Next point, thus P2 can be kept and we can continue with the
% next points:
compute_graham_scan_hull( [ NewPoint | ToValidate ], Next,
OtherNext );
true ->
%io:format( "compute_graham_scan_hull: point ~w is on the left of "
% "segment from ~w to ~w, eliminating ~w.~n",
% [ P2, P1, NewPoint, P2 ] ),
% Here, the point P2 is on the left of (or in) the segment going
% from P1 to the Next point, thus P2 is to be discarded, and will
% have to check predecessor(s) of P2 against the Next point.
%
compute_graham_scan_hull( [ P1 | T ], NewPoint, NextPoints )
end;
% Note however that the first point examined after the pivot (FP1) may have to
% be discarded because of the second. If we just removed FP1, then the
% ToValidate list would just contain the pivot, thus triggering a function
% clause. In that case we just have to replace FP1 by the next(FP1)=P2 here, and
% thus ToValidate will always contain at least two elements:
% This clause matches whenever we just removed the first point in the list
% examined after the pivot. So the first parameter (ToValidate) is just a list
% with one element, the pivot, that was added to close the hull. As we have no
% intermediate point, we accept directly the next point (Next), knowing it will
% be checked at the next recursion:
%compute_graham_scan_hull( [ Pivot ], NewPoint, [ Next | OtherNext ] ) ->
% compute_graham_scan_hull( [ NewPoint, Pivot ], Next, OtherNext ).
% A bit faster as we know L is actually [ Pivot ]:
%
compute_graham_scan_hull( L, NewPoint, [ Next | OtherNext ] ) ->
compute_graham_scan_hull( [ NewPoint | L ], Next, OtherNext ).
% Textual conversion section.
% Returns a precise textual representation of specified point.
%
-spec to_string( point() ) -> string().
to_string( { X, Y } ) ->
text_utils:format( "[ ~w, ~w ]", [ X, Y ] ).
% Returns a human-friendly, approximated textual representation of specified
% point, based on specified print-out precision (number of digits after the
% comma).
%
-spec to_string( point(), basic_utils:count() ) -> string().
to_string( { X, Y }, DigitCountAfterComma ) ->
% We want to avoid displaying larger texts for coordinates, like
% 0.10000000000000009:
%XRounded = math_utils:round_after( X, DigitCountAfterComma ),
%YRounded = math_utils:round_after( Y, DigitCountAfterComma ),
%text_utils:format( "{ ~.*w, ~.*w }", [ Precision, X, Precision, Y ] ).
XString = case is_float( X ) of
true ->
text_utils:format( "~.*f", [ DigitCountAfterComma, X ] );
false ->
% Integer:
text_utils:format( "~B", [ X ] )
end,
YString = case is_float( Y ) of
true ->
text_utils:format( "~.*f", [ DigitCountAfterComma, Y ] );
false ->
% Integer:
text_utils:format( "~B", [ Y ] )
end,
text_utils:format( "[ ~s, ~s ]", [ XString, YString ] ).