% Copyright (C) 2010-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 facilities for polygon management. % % Coordinates are expected to be often integers, when used for rendering. % % See polygon_test.erl for the corresponding test. % -module(polygon). -include("polygon.hrl"). -type polygon() :: #polygon{}. -export_type([ polygon/0 ]). % For circle record and al: -include("bounding_box.hrl"). % Construction-related section. -export([ get_triangle/3, get_upright_square/2, get_polygon/1 ]). % Operations on polygons. -export([ get_diameter/1, get_smallest_enclosing_rectangle/1, get_area/1, is_in_clockwise_order/1, is_convex/1, render/2, to_string/1 ]). % Color-related section. -export([ set_edge_color/2, get_edge_color/1, set_fill_color/2, get_fill_color/1 ]). % Bounding-box related section. -export([ update_bounding_box/2 ]). % Construction-related section. % Returns a triangle (defined as a polygon) corresponding to the specified three % vertices. % -spec get_triangle( linear_2D:point(), linear_2D:point(), linear_2D:point() ) -> polygon(). get_triangle( V1, V2, V3 ) -> #polygon{ vertices=[ V1, V2, V3 ] }. % Returns an upright square corresponding to the specified center and edge % length. % -spec get_upright_square( linear_2D:point(), linear:distance() ) -> polygon(). get_upright_square( _Center={Xc,Yc}, EdgeLength ) -> Offset = erlang:round( EdgeLength / 2 ), X1 = Xc - Offset, X2 = Xc + Offset, Y1 = Yc - Offset, Y2 = Yc + Offset, #polygon{ vertices=[ {X1,Y1}, {X2,Y1}, {X2,Y2}, {X1,Y2} ] }. % Returns a new polygon whose vertices are the specified ones. % -spec get_polygon( [ linear_2D:point() ] ) -> polygon(). get_polygon( Vertices ) -> #polygon{ vertices=Vertices }. % Operations on polygons. % Returns a polygon diameter, i.e. two points in the polygon which are at the % maximum distance one of the other. % % Returns {V1,V2,D} when V1 and V2 are the endpoints of a diameter and D is its % square length: D = square_distance( V1, V2 ). % -spec get_diameter( polygon() ) -> { linear_2D:point(), linear_2D:point(), linear:square_distance() }. get_diameter( Polygon ) -> case Polygon#polygon.vertices of [] -> throw( no_vertex ); [ _Vertex ] -> throw( single_vertex ); ListWithAtLeastTwoVertices -> % There are at least two vertices: linear_2D:compute_max_overall_distance( ListWithAtLeastTwoVertices ) end. % Returns the smallest upright rectangle which encompasses the specified % polygon. % % More precisely, {TopLeftCorner,BottomRightCorner} is returned, which defines % the rectangle from two opposite points. % -spec get_smallest_enclosing_rectangle( polygon() ) -> { linear_2D:point(), linear_2D:point() }. get_smallest_enclosing_rectangle( Polygon ) -> case Polygon#polygon.vertices of [] -> throw( no_vertex ); [ _Vertex ] -> throw( single_vertex ); ListWithAtLeastTwoVertices -> linear_2D:compute_smallest_enclosing_rectangle( ListWithAtLeastTwoVertices ) end. % Returns the area enclosed of the polygon, supposed to be non-self-intersecting % and having at least two vertices. % % Vertices can be listed clockwise or counter-clockwise. % % Should there be no absolute value computed, and if the polygon was convex, % then the area would be positive iff vertices were listed in counter-clockwise % order. % % See: http://en.wikipedia.org/wiki/Polygon#Area_and_centroid % -spec get_area( polygon() ) -> linear:area(). get_area( Polygon ) -> erlang:abs( get_signed_area( Polygon#polygon.vertices ) ). % Tells whether the specified polygon has its vertices in clockwise order % (otherwise they are in counter-clockwise order). % -spec is_in_clockwise_order( polygon() ) -> boolean(). is_in_clockwise_order( Polygon ) -> case get_signed_area( Polygon#polygon.vertices ) of Area when Area > 0 -> true; _Negative -> false end. % Tells whether the specified polygon is convex (iff true) or concave % (otherwise). % % Polygon must have at least one vertex. % % Sign is either 'undefined' (initially), or 'positive', or 'negative'. % -spec is_convex( polygon() ) -> boolean(). is_convex( Polygon ) -> [ First | T ] = Polygon#polygon.vertices, is_convex( T ++ [ First ], _Previous=First, _Sign=undefined ). % Helper function: is_convex( [], _Previous, _Sign ) -> % Not interrupted, thus convex (includes polygon having only one vertex): true; is_convex( [ P={X,Y} | T ], _Previous={Xp,Yp}, _Sign=undefined ) -> % Setting the first sign: %trace_utils:debug_fmt( "initial: previous= ~w, next= ~w, sum=~w.~n", % [ {Xp,Yp}, P, Xp*Y-X*Y ] ), FirstSign = case Xp*Y-X*Yp of PositiveSum when PositiveSum > 0 -> positive; _NegativeSum -> negative end, is_convex( T, _NewPrevious=P, FirstSign ); is_convex( [ P={X,Y} | T ], _Previous={Xp,Yp}, Sign ) -> %io:format( "iterated: previous= ~w, next= ~w, sum=~w.~n", % [ {Xp,Yp}, P, Xp*Y-X*Yp ] ), % Checking if still obtaining the same sign: NewSign = case Xp*Y-X*Yp of PositiveSum when PositiveSum > 0 -> positive; _NegativeSum -> negative end, %trace_utils:debug_fmt( "Current sign: ~s, new one: ~s.~n", [ Sign, NewSign ] ), case NewSign of Sign -> % Can still be convex: is_convex( T, _NewPrevious=P, Sign ); _OppositeSign -> % Finished, as is concave: false end. % Color-related section. % Sets the edge color of specified polygon. % -spec set_edge_color( gui_color:color(), polygon() ) -> polygon(). set_edge_color( Color, Polygon ) -> Polygon#polygon{ rendering=option_list:set( { edge_color, gui_color:get_color( Color ) }, Polygon#polygon.rendering ) }. % Returns the current edge color of the specified polygon, if specified, % otherwise 'undefined'. % -spec get_edge_color( polygon() ) -> maybe( gui_color:color() ). get_edge_color( Polygon ) -> option_list:lookup( edge_color, Polygon#polygon.rendering ). % Sets the fill color of specified polygon. % % Use 'none' to disable filling. % -spec set_fill_color( gui_color:color(), polygon() ) -> polygon(). set_fill_color( Color, Polygon ) -> Polygon#polygon{ rendering=option_list:set( { fill_color, gui_color:get_color( Color ) }, Polygon#polygon.rendering ) }. % Returns the current fill color of the specified polygon, if specified, % otherwise 'undefined'. % -spec get_fill_color( polygon() ) -> maybe( gui_color:color() ). get_fill_color( Polygon ) -> option_list:lookup( fill, Polygon#polygon.rendering ). % Returns options for the rendering of this polygon that can be directly passed % to the graphical back-end. % -spec get_rendering_options( polygon() ) -> option_list:option_list(). get_rendering_options( Polygon ) -> Polygon#polygon.rendering. % Renders specified polygon in specified canvas. % % Throws an exception if the polygon is not valid. % -spec render( polygon(), gui:canvas() ) -> void(). render( Polygon, Canvas ) -> %trace_utils:debug_fmt( "Rendering polygon:~n~s.", % [ to_string( Polygon ) ] ), case Polygon#polygon.vertices of [] -> throw( null_polygon ); [ _Vertex ] -> throw( one_vertex_polygon ); Vertices -> Opts = get_rendering_options( Polygon ), case option_list:lookup( edge_color, Opts ) of undefined -> ok; DrawColor -> %trace_utils:debug_fmt( "DrawColor = ~p.", [ DrawColor ] ), gui:set_draw_color( Canvas, DrawColor ) end, case option_list:lookup( fill_color, Opts ) of undefined -> ok; FillColor -> gui:set_fill_color( Canvas, FillColor ) end, gui:draw_polygon( Canvas, Vertices ), case Polygon#polygon.bounding_box of { circle, Center, SquareRadius } -> gui:draw_circle( Canvas, Center, round( math:sqrt( SquareRadius ) ) ), gui:draw_cross( Canvas, Center, _EdgeLength=4 ); undefined -> ok end end. % Returns a textual description of the specified polygon. -spec to_string( polygon() ) -> string(). to_string( Polygon ) -> BBText = case Polygon#polygon.bounding_box of undefined -> "none available"; BB -> bounding_box:to_string( BB ) end, io_lib:format( " + vertices: ~w~n", [ Polygon#polygon.vertices ] ) ++ io_lib:format( " + edge color: ~w~n", [ get_edge_color( Polygon ) ] ) ++ io_lib:format( " + fill color: ~w~n", [ get_fill_color( Polygon ) ] ) ++ io_lib:format( " + bounding-box: ~s~n", [ BBText ] ). % Bounding-box related section. % Updates, for the specified polygon, its internal bounding-box, with regard to % the specified bounding-box request. % % Returns a polygon with updated information. % % The lazy circle bounding box is fast to determine, but not optimal: -spec update_bounding_box( 'lazy_circle', polygon() ) -> polygon(). update_bounding_box( lazy_circle, Polygon ) -> { Center, SquareRadius } = bounding_box:get_lazy_circle_box( Polygon#polygon.vertices ), Polygon#polygon{ bounding_box=#circle{ center=Center, square_radius=SquareRadius } }. % Helper functions. % Returns the signed area enclosed of the polygon, supposed to be % non-self-intersecting and having at least two vertices. % % Vertices can be listed clockwise or counter-clockwise. % -spec get_signed_area( [ linear_2D:point() ] ) -> linear:area(). get_signed_area( _Vertices=[ First | T ] ) -> % We will start from the second point, as we always deal with the current % one and its predecessor (avoid to add an element at end of list): get_signed_area( T, _FirstOfAll=First, _Previous=First, _Area=0 ). get_signed_area( _Vertices=[ _Last={X,Y} ], _FirstOfAll={Xf,Yf}, _Previous={Xp,Yp}, Area ) -> % Here we reached the last point of the polygon, so, first, we compute its % product with the previous point, then we do the same with the FirstOfAll % point, as if it was following this Last point: LastTwoSumTerms = Xp*Y-X*Yp + X*Yf-Xf*Y, ( Area + LastTwoSumTerms ) / 2 ; get_signed_area( [ P={X,Y} | T ], FirstOfAll, _Previous={Xp,Yp}, Area ) -> % Here we are not managing the last point: get_signed_area( T, FirstOfAll, _NewPrevious=P, Area + Xp*Y-X*Yp ).