# This code is copyright 2002 by Eric Wilhelm and A. Zahner Co.
# It may be freely modified and distributed under the same licensing terms as Perl
# provided that this notice remains intact and that all changes are documented 


package Math::Geometry::Planar::Offset;


$VERSION = '1.02';

use vars qw(
            $VERSION
            @ISA
            @EXPORT
            @EXPORT_OK
            $precision
            $debug
           );

use strict;
use Carp;
use Math::Geometry::Planar;

$debug     = 1;
$precision = 7;

require Exporter;
@ISA       = qw(Exporter);
@EXPORT    = qw(OffsetPolygon);
@EXPORT_OK = qw(
                $precision
                $debug
               );

=pod

=head1 NAME

Math::Geometry::Planar::Offset - Calculate offset polygons

=head1 SYNOPSIS

 use Math::Geometry::Planar::Offset;

=head1 METHODS

The available methods are:

=head4 $polygon->offset_polygon($distance);

Returns reference to an array of polygons representing the original polygon
offsetted by $distance

=cut

require 5.005;

my $delta = 10 ** (-$precision);

my $offset_depth;
my $screen_height = 600;
my $flag;
my @bisectors;

################################################################################
#
# Best approximation for pi
#
sub pi {
  atan2(1,1) * 4;
}
#########################################################################
# make offset polygon subroutine:
# call with offset distance and ref to array of points for original polygon
# polygon input must be pre-wrapped so point[n]=point[0]
#
# Will return a list of polygons (as refs)
sub OffsetPolygon {
  my ($loc_points,$offset,$canvas) = @_;
  my %intersects;
  my ($count,$n,$n2);
  my ($time, $time_static);
  # my $key1,$key2;
  my @angles;
  my @directions;
  my @phi;
  my @bis_dir;
  my @bis_end;
  my @bis_scale;
  my @result1;
  my @result2;
  my @outlist;
  my ($cut1,$cut2,$cut);
  $#outlist = 0;
  my @newpoints;
  my @newpoints1;

  # set limit on number of recursions
  # does perl have its own limit to the number of scopes generated?
  if($offset_depth > 100){print "reached recursion depth_limit\n";return();};
  #ew: $offset_depth would have had to be reset by calling script 
  # (now is incremented and decremented on each side of recursive call)
  my $npoints = @{$loc_points};
  $debug && print "points: ", $npoints, "\n";
  $debug && print "number of points $npoints\n";
  # print "points: @{$$loc_points[0]}\n";
  # exit;
  # my $bis_length = 15;
  # All polygons should be counter-clockwise !!!
  find_direction($loc_points,\@angles,\@directions);
  ($debug) && print "starting recurse: ",$offset_depth -1,"\n";
  ($debug) && print "offset:  $offset\n";
  # ew removed junk related to null points
  # bail out if not at least a triangle
  if($npoints < 3) {
    carp("Need at least 3 non-colinear points to offset");
    return;
  }
  # Now to make the bisectors
  for($n = 0;$n < $npoints;$n++) {
    $phi[$n] = ((pi) - $angles[$n]) / 2;
    $bis_scale[$n] = abs(1 / sin($phi[$n]));
    $bis_dir[$n] = $directions[$n] + $phi[$n];
    $bis_end[$n][0] = $$loc_points[$n][0] + $offset * $bis_scale[$n] * cos($bis_dir[$n]);  # X-coordinate
    $bis_end[$n][1] = $$loc_points[$n][1] + $offset * $bis_scale[$n] * sin($bis_dir[$n]);  # Y-coordinate
  };
  
  # Draw bisectors
  if ($canvas) {
    # first delete existing bisectors
    for($n = 0; $n<@bisectors;$n++) {
      $canvas ->delete($bisectors[$n]);
    }
    @bisectors = ();
    # then draw the bisector points from the vertices.
    for($n = 0; $n < $npoints; $n++) {
      my $x1 = $$loc_points[$n][0];
      my $y1 = $screen_height-$$loc_points[$n][1];
      my $x2 = $bis_end[$n][0];
      my $y2 = $screen_height-$bis_end[$n][1];
      $bisectors[$n] = $canvas->create('line', $x1,$y1,$x2,$y2, '-fill' => 'blue','-arrow'=>'last');
    }
  }
  
  # Need to find whether a bisector first intersects a bisector,
  # or a bisector first intersects a "ghost" edge.  Also, must know
  # which one and at what time:
  my ($Ax1,$Ay1,$Ax2,$Ay2,$Bx1,$By1,$Bx2,$By2);
  my ($delAx,$delAy,$delBx,$delBy,$Am,$Ab,$Bm,$Bb);
  my ($x_int,$y_int);
  my ($first_join,$first_split,$split_seg);
  my $first_event = "";
  my $first_time = abs($offset)+1;
  my $time_dir = $offset / abs($offset);  # +1 or -1 to give direction
  my $new_offset;
  my $join_time = $first_time;
  my $split_time = $first_time;
  
  # first find intersections of adjacent bisectors (if any)
  for($n = 0;$n <$npoints;$n++) {
    $Ax1 = $$loc_points[$n][0];
    $Ay1 = $$loc_points[$n][1];
    $Ax2 = $bis_end[$n][0];
    $Ay2 = $bis_end[$n][1];
    $delAx = $Ax2 - $Ax1;
    $delAy = $Ay2 - $Ay1;
    if($delAx)  {$Am = $delAy / $delAx;  $Ab = $Ay1 - $Ax1 * $Am;}else{$Am = "inf";};
    # Check against the next bisector:
    $Bx1 = $$loc_points[$n+1-$npoints][0];
    $By1 = $$loc_points[$n+1-$npoints][1];
    $Bx2 = $bis_end[$n+1-$npoints][0];
    $By2 = $bis_end[$n+1-$npoints][1];
    $delBx = $Bx2 - $Bx1;
    $delBy = $By2 - $By1;
    if($delBx)  {$Bm = $delBy / $delBx;$Bb = $By1 - $Bx1 * $Bm;  }else{$Bm = "inf";};
    if( ($Am == $Bm) || ($Am eq $Bm) ){next;};
    # Calculate determinants to find if intersection is within the bisector segment.
    if  (! do_cross($Ax1,$Ay1,$Ax2,$Ay2,$Bx1,$By1,$Bx2,$By2) ){next;};
    # if we have an intersection of the skeleton and only a triangle,
    # then it has collapsed to a point:
    if($npoints < 4) {
      $debug && print "collapsed\n";return();
    }
    if($Am eq "inf") {  # Slope of first line is infinite, so use Ax1.
      $x_int = $Ax1;    # Will always be on vertical line.
      $y_int = $Bm * $x_int + $Bb;
    } elsif($Bm eq "inf") {  # Slope of second line is infinite, so use Bx1.
      $x_int = $Bx1;$y_int = $Am * $x_int + $Ab;
    } else {
      $x_int = ($Bb - $Ab) / ($Am - $Bm);$y_int = $Am *$x_int + $Ab;
    }
    # Now, if the lines are not parallel, and the intersection
    # happens on the line segments, we are here with
    # the x and y coordinates of the intersection of the two lines.
  #   printf ("intersection: %6.0f,%6.0f\n",$x_int, $y_int);
  
    # Let's find the time of intersect.
    # distance  formula with adjustment for speed
    $time = sqrt( ($x_int - $$loc_points[$n][0])**2 + ($y_int - $$loc_points[$n][1])**2 ) / $bis_scale[$n];
    if( (abs($time)<$first_time) )#&& ( $time / abs($time)== $offset / abs($offset) ) )
      {
      # note that none of the times loaded here have a sign
      $first_time = abs($time);
      $join_time = $first_time;
      $first_join=$n;
      $first_event="join";
      };
  
  
    }; # end for n (for each bisector vs next neighbor)
  # Time is smallest relevant offset distance before first join.
  # first join is address of point to be joined.
  # first event is "join" if controlling (could be over-ridden by split)
  # If this is the controlling case, create the joined polygon at that offset time
  # and make a recursive call.
  #
  #
  # Now to check for intersections of bisectors with edges
  # Have to check against all segments except adjacent ones.
  # Nothing will happen if it is a triangle.
  if($npoints>3)
  {
  for($n = 0;$n <$npoints;$n++)
    {
    $Ax1 = $$loc_points[$n][0];  # Get bisector endpoints
    $Ay1 = $$loc_points[$n][1];
    $Ax2 = $bis_end[$n][0];
    $Ay2 = $bis_end[$n][1];
    $debug && print "starting at $Ax1 $Ay1\n";
    $debug && print "bisector toward $Ax2 $Ay2\n";
    # I need it to be a ray, so I am just making a really long segment here.
    $delAx = 1000* $offset* ($Ax2 - $Ax1);
    $delAy = 1000* $offset* ($Ay2 - $Ay1);
    $Ax2 = $Ax1+ $delAx;
    $Ay2 = $Ay1 + $delAy;
    if($delAx)  {$Am = $delAy / $delAx;  $Ab = $Ay1 - $Ax1 * $Am;}else{$Am = "inf";};
    # this loop has to compare to all of the polygon sides except the adjacent ones
    for($n2 = 0; $n2 < $npoints; $n2++)
      {
      $time = abs($offset) +1;
      # Can't split adjacent segments
      if( ($n2 == $n) || ($n2 == $n-1) || ( ($n==0)&&($n2 == $npoints-1) ) ){next;};
      $debug && print "inner loop at n2 = $n2\n";
      $Bx1 = $$loc_points[$n2][0];  # Get edge endpoints
      $By1 = $$loc_points[$n2][1];
      $Bx2 = $$loc_points[$n2+1-$npoints][0];
      $By2 = $$loc_points[$n2+1-$npoints][1];
      $delBx = $Bx2 - $Bx1;
      $delBy = $By2 - $By1;
      if($delBx)  {$Bm = $delBy / $delBx;$Bb = $By1 - $Bx1 * $Bm;  }else{$Bm = "inf";};
      if( ($Am == $Bm) || ($Am eq $Bm) ){next;};
      # Note that I am not using the dot product here (it shows up below.
      # I need to know where the infinite ray of the bisector
      # crosses the infinite line of the polygon edge (but this may cause problems as well)
  #     if( ! do_cross($Ax1,$Ay1,$Ax2,$Ay2,$Bx1,$By1,$Bx2,$By2) ){next;};
      if($Am eq "inf")  # Slope of first line is infinite, so use Ax1.
        {$x_int = $Ax1;$y_int = $Bm * $x_int + $Bb;}
      elsif($Bm eq "inf")    # Slope of second line is infinite, so use Bx1.
        {$x_int = $Bx1;$y_int = $Am * $x_int + $Ab;}
      else{$x_int = ($Bb - $Ab) / ($Am - $Bm);$y_int = $Am *$x_int + $Ab;};
      # now make sure that the intersection happened on the right side
      # (point the ray in the offset direction)
      my $delxto_int = $x_int - $Ax1;
      my $delyto_int = $y_int - $Ay1;
      my $next = $n2+1;
      if ($next == $npoints){$next = 0;};
      # This method is not handling the split which happens to edges terminating
      # at a concave point.  This event is a third case because the time calculated
      # below will be smaller for the extension of the edge before the concavity than
      # for the edge after the concavity, which is the one to properly split.
      
  
      # I have suspicions about the accuracy here:
      # Original intent was to make sure that the ray was properly treated
      # now handled by using a really long segment (1000*offset) for the ray.
  # #     print "bisector direction:  $bis_dir[$n]\n";
  # #     print "direction to intersect:  ", atan2($delyto_int, $delxto_int), "\n";
       #if( $bis_dir[$n] == (atan2($delyto_int,$delxto_int)))
        {
        # direction is good, calculate distance
        # note that the int_scale should be naturally signed to prevent strangeness on internal offsets.
        # dvdp: 
        # $int_scale = 1 / (sin( $directions[$n2] - $bis_dir[$n])) ;
        # This could lead to a devision by 0 so we calculate the reverse and check first
        my $int_scale = (sin( $directions[$n2] - $bis_dir[$n])) ;
        $debug && print "bisector scale:  $bis_scale[$n]\n";
        $debug && print "intersection scale:  1 / $int_scale\n";
        if ($int_scale) {
          $int_scale  = 1 / $int_scale;
          if($bis_scale[$n]+$int_scale) {
            $time_static = sqrt( ($x_int - $$loc_points[$n][0])**2 + 
                    ($y_int - $$loc_points[$n][1])**2 ) / ($bis_scale[$n]+$int_scale);
          } else { # intersection happens only at infinite offset time
            # die "PGON offset dying at 281 with $bis_scale[$n] and $int_scale\n";
          }
        } else {
            # 1 / $int_scale is arbitrary large so division reduces to 0
            $time_static = 0;
        }
        # once you have calculated the time, you need to see if the
        # intersected segment is still in that place at that time to be struck by the ray
        # $Bx1 = $$loc_points[$n2][0] + $time_static  * $time_dir * $bis_scale[$n2] * cos($bis_dir[$n2]);
        # $By1 = $$loc_points[$n2][1] + $time_static  * $time_dir * $bis_scale[$n2] *  sin($bis_dir[$n2]);
        # $Bx2 = $$loc_points[$n2+1][0] + $time_static  * $time_dir * $bis_scale[$next] * cos($bis_dir[$next]);
        # $By2 = $$loc_points[$n2+1][1] + $time_static  * $time_dir * $bis_scale[$next] * sin($bis_dir[$next]);
        # Does time_dir belong here? (yes for test case A)
        $Bx1 = $$loc_points[$n2][0] + $time_static  * $bis_scale[$n2] * cos($bis_dir[$n2]);
        $By1 = $$loc_points[$n2][1] + $time_static  * $bis_scale[$n2] *  sin($bis_dir[$n2]);
        $Bx2 = $$loc_points[$n2+1-$npoints][0] + $time_static * $bis_scale[$next] * cos($bis_dir[$next]);
        $By2 = $$loc_points[$n2+1-$npoints][1] + $time_static * $bis_scale[$next] * sin($bis_dir[$next]);
        if(! do_cross($Ax1,$Ay1,$Ax2,$Ay2,$Bx1,$By1,$Bx2,$By2) ){next;};
        $time = $time_static;
        # make sure the case controls and is in the right direction.  (required for case A)
        # dvdp: replaced division by abs($time) by a multiplication to overcome
        #       potential divide by 0
        if( (abs($time)<$first_time)  && ( $time == $time_dir * abs($time) ) ) {
          # note that none of the times loaded here have a sign
          $first_time = abs($time);
          $split_time = $first_time;
          $first_split=$n;
          $split_seg = $n2;
          $first_event="split";
          }
        }
      } # end for n2
    } # end for n (each bisector verses each edge)
  } # End if not triangle
  
  # Time is the smallest relavent split time (unless join controls)
  # first_split is address of point to be split.
  # first_event is "split" if controlling (could be controlled by join)(or nothing)
  # If this is the controlling case, create the split polygons at that offset time
  # and make a recursive call.
  
  if($first_time <=abs($offset)) {
    if($first_event eq "join")
      {($debug) && printf ("join vertex %d at time %6.2f\n",$first_join,$first_time);};
    if($first_event eq "split")
      {($debug) && printf ("split segment %d with vertex %d at time %6.2f\n",$split_seg,$first_split,$first_time);};
    ($debug) && print "\ttime:  $first_time\n";
    # get a list of points for the offset polygon at first_time
  
    if($first_event eq "join") {
      # How are coincident events handled?
      # remove the offending point and call yourself with time adjusted.
      @newpoints = ();
      for($n = 0; $n<$npoints; $n++) {
        $newpoints[$n][0] = $$loc_points[$n][0] + $first_time * $time_dir* $bis_scale[$n] * cos($bis_dir[$n]);
        $newpoints[$n][1] = $$loc_points[$n][1] + $first_time * $time_dir* $bis_scale[$n] * sin($bis_dir[$n]);
      };
      # keep the one with the smaller scale.
      if($bis_scale[$first_join]<$bis_scale[$first_join+1]) {
        splice(@newpoints,$first_join+1,1);
      } else {
        splice(@newpoints,$first_join,1);
      }
      $npoints--;
      # @{$newpoints[$npoints]} = @{$newpoints[0]};
      $new_offset = $offset - $first_time * $time_dir;  # put a direction on it.
			$offset_depth++;
      @outlist = OffsetPolygon(\@newpoints,$new_offset) ;
			$offset_depth--;
      return(@outlist);
    } elsif ($first_event eq "split") {
      # make two polygons and call yourself with time adjusted.
      @newpoints = ();
      my @split_check;
      my @split_check1;
      for($n = 0; $n<$npoints; $n++)
      {
      $newpoints[$n][0] = $$loc_points[$n][0] + $first_time * $time_dir * $bis_scale[$n] * cos($bis_dir[$n]);
      $newpoints[$n][1]  = $$loc_points[$n][1] + $first_time * $time_dir * $bis_scale[$n] * sin($bis_dir[$n]);
      $split_check[$n] = $n;
      };
      $new_offset = $offset - $first_time * $time_dir;
      @newpoints1 = ();
      @newpoints1 = @newpoints;
        @split_check1 = ();
        @split_check1 = @split_check;
      # have first_split and split_seg, must find a way to wrap around.
      my $cut_startA;
      my $cut_startB;
      my $cutB;
      if($split_seg<$first_split) {
        $cut1 = $#newpoints - $first_split;
        $cut2 = $split_seg+1;
        $cut_startA=$first_split+1;
        $cut_startB = $split_seg+1;
        $cutB = $first_split - $split_seg -1;
      } else {
        $cut1 = $#newpoints - $split_seg;
        $cut2 = $first_split;
        $cut_startA = $split_seg +1;
        $cut_startB = $first_split + 1;
        $cutB = $split_seg - $first_split;
      }
      splice(@newpoints,$cut_startA,$cut1);
      splice(@newpoints,0,$cut2);
      splice(@split_check,$cut_startA,$cut1);
      splice(@split_check,0,$cut2);
      splice(@newpoints1,$cut_startB,$cutB);
      splice(@split_check1,$cut_startB,$cutB);
      my $num = @newpoints;
      my $num1 = @newpoints1;
  
      ($debug) && print "split was:\n@split_check\n@split_check1\n";
      ($debug) && print "first splitted:\n";
      for($n = 0; $n<$num;$n++) {
        ($debug) && printf ("point: %d  (%6.2f,%6.2f)\n",$n,@{$newpoints[$n]});
      }
      ($debug) && print "second splitted:\n";
      for($n = 0; $n < $num1;$n++) {
        ($debug) && printf ("point: %d  (%6.2f,%6.2f)\n",$n,@{$newpoints1[$n]});
      }
      $#result1 = 0;
      shift(@result1);
      $#result2 = 0;
      shift(@result2);
      if($num>2){
			$offset_depth++;	
			@result1 = OffsetPolygon(\@newpoints,$new_offset);
			$offset_depth--;
			}else{$debug && print "too few on first\n";};
      if($num1>2){
			$offset_depth++;	
			@result2 = OffsetPolygon(\@newpoints1,$new_offset);
			$offset_depth--;
			}else{$debug && print "too few on second\n";};
      #@result1 = offset_polygon($new_offset,\@newpoints);
      #@result2 = offset_polygon($new_offset,\@newpoints1);
      return(@result1, @result2);  # This concatenates the list.
  #     @outlist = \@newpoints;
      };
  } else {
    # No splits or joins needed, so offset the thing.
    # Bisector endpoints should be available.
    @newpoints = ();
    for($n = 0; $n<$npoints; $n++)
      {
      $newpoints[$n][0] = $$loc_points[$n][0] + $offset * $bis_scale[$n] * cos($bis_dir[$n]);
      $newpoints[$n][1] = $$loc_points[$n][1] + $offset * $bis_scale[$n] * sin($bis_dir[$n]);
      };
    return( \@newpoints);
    };
  # return a reference to an array of polygons which are arrays of points
  # return (@outlist);
}  # End offset polygon subroutine definition
#########################################################################
# Find  direction subroutine.
# Called with (<Polygon points>,<changes in angle>, <angles>).
# Returns total change in angle (as radians (everything is as radians)).
# Second two array refs are optional.
# Forcing counter-clockwise currently not done here, but maybe should.
sub find_direction {
  my($coords,$del_theta,$thetas) = @_;
  my $sum_theta=0;
  my $sum_delta_theta = 0;
  @{$del_theta} = ();        # Clear arrays referenced for in-place modification.
  @{$thetas}    = ();
  my $n = 0;
  for(;;) {
    last if ($n == @{$coords});
    # dvdp: take advantage of negative indexing in Perl
    my $n2 = $n - @{$coords} + 1;
    my $n3 = $n - 1;
    my $Ax1 = ${$coords}[$n][0];   # Line leaving point
    my $Ay1 = ${$coords}[$n][1];   # this is the one belonging to the point
    my $Ax2 = ${$coords}[$n2][0];
    my $Ay2 = ${$coords}[$n2][1];
    my $delAx = $Ax2 - $Ax1;
    my $delAy = $Ay2 - $Ay1;
    my $Bx1 = ${$coords}[$n3][0];  # Line coming to point n
    my $By1 = ${$coords}[$n3][1];
    my $Bx2 = ${$coords}[$n][0];
    my $By2 = ${$coords}[$n][1];
    my $delBx = $Bx2 - $Bx1;
    my $delBy = $By2 - $By1;
    # dvdp remove coinciding points
    if ( ((abs($delAx) < $delta) && (abs($delAy) < $delta)) ||
         ((abs($delBx) < $delta) && (abs($delBy) < $delta)) ) {
      splice @{$coords},$n,1;
      next;
    }
    my $theta_A  = atan2($delAy,$delAx);    # Angle of leaving line
    my $theta_B  = atan2($delBy,$delBx);    # Angle of coming line
    my $theta_AB = $theta_A - $theta_B;
    if ($theta_AB < -(pi)) {
      $theta_AB += 2 * (pi);
    } elsif ($theta_AB > (pi)) {
      $theta_AB -= 2 * (pi);
    }
    # dvdp remove colinear points (angle is 0 or pi)
    if ( (abs($theta_AB) < $delta) ||
         (abs($theta_AB - (pi)) < $delta) ) {
      splice @{$coords},$n,1;
      $n-- if ($n); # need to recalc thetas if spike removed !
      next;
    }
    ${$thetas}[$n]    = $theta_A;
    ${$del_theta}[$n] = $theta_AB;
    $n++
    # $sum_theta += $theta_A;
    # $sum_delta_theta += ${$del_theta}[$n];
    # print "start at:\n";
    # printf ("point:  %d\t",$n);
    # printf("\tcoordinates:  %d, %d\n", $Ax1,$Ay1);
    # printf ("direction:  %d\n", $theta_A*180 / (pi));
    # printf ("change in angle:  %d\t\n\n",${$del_theta}[$n]*180 / (pi) );
  }
  # if($sum_delta_theta>0){print"\a";};
  # printf ("sum of changes:  %d\tsum of angles:  %d",$sum_delta_theta*180/ pi,$sum_theta*180/ pi);
  # return($sum_delta_theta);
}  # End find_direction sub
#########################################################################
# Define do_cross subroutine.
# Determines if a pair of line segments have an intersection.
sub do_cross {
  my ($Ax1,$Ay1,$Ax2,$Ay2,$Bx1,$By1,$Bx2,$By2) = @_;
  # Calculate four relevant minor determinants
  my $det_123=($Ax2 - $Ax1)*($By1 - $Ay1) - ($Bx1 - $Ax1)*($Ay2 - $Ay1);
  my $det_124=($Ax2 - $Ax1)*($By2 - $Ay1) - ($Bx2 - $Ax1)*($Ay2 - $Ay1);
  my $det_341=($Bx1 - $Ax1)*($By2 - $Ay1) - ($Bx2 - $Ax1)*($By1 - $Ay1);
  my $det_342 =$det_123-$det_124+$det_341;
  if( ($det_123*$det_124 > 0) || ($det_341*$det_342 > 0) ) {
    return(0);  # segments only intersect if the above two products are both negative
  } else {
    return(1);
  }
}  # End do_cross subroutine definition
########################################################################

1;
