The Perl Toolchain Summit needs more sponsors. If your company depends on Perl, please support this very important event.
package Tk::Scope;
use strict;
use Tk;
use Tk::widgets qw(Canvas Scrollbar DialogBox);
use Audio::Data;
use base qw(Tk::Derived Tk::Canvas);
use File::Temp qw(tempfile);

Construct Tk::Widget 'Scope';

sub ClassInit
{
 my ($class,$mw) = @_;
 
 $mw->bind($class,'<4>',[Wheel => -1, Ev('x')]); 
 $mw->bind($class,'<5>',[Wheel => 1, Ev('x')]); 
 
 $mw->bind($class,'<1>',[Cursor => 1,Ev('x')]); 
 $mw->bind($class,'<B1-Motion>',[Cursor => 2,Ev('x')]); 
 $mw->bind($class,'<ButtonRelease-1>',[Range => Ev('x'),-rangecmd => 1,2,Ev('s')]); 

 $mw->bind($class,'<3>',[Cursor => 'z1',Ev('x')]); 
 $mw->bind($class,'<B3-Motion>',[Cursor => 'z2',Ev('x')]); 
 $mw->bind($class,'<ButtonRelease-3>',[Range => Ev('x'),-zoomcmd => 'z1','z2',Ev('s')]); 
 
 $mw->bind($class,'<Configure>','scheduleRedisplay');
 $mw->bind($class,'<Print>','Print');
 return $class;
}
 
sub Wheel
{
 my ($c,$n,$x) = @_;
 $c->xview(scroll => $n*0.1, 'pages');
}

sub doZoom
{
 my ($c,$t1,$t2,$s) = @_; 
 # warn "Zoom '$s' $t1 -> $t2\n";
 if ($s =~ /Shift/)
  {
   my $s = $c->start;
   my $e = $c->end;
   my $w = $e-$s;               # width now
   my $dt = $t2-$t1;
   my $l1 = $w*$w/$dt;
   my $s1 = $s - ($t1-$s)*$w/$dt;
   $s1 = 0 if ($s1 < 0);
   my $e1 = $s1+$l1;
   $e1 = $c->xmax if ($e1 > $c->xmax);
   $c->start($s1); 
   $c->end($e1); 
  }
 else
  { 
   $c->start($t1); 
   $c->end($t2); 
  } 
}
 
sub Range
{
 my ($c,$x,$callback,$n,$m,@args) = @_;
 $m = 1 if ($m eq '2' && !$c->cget('-range1'));
 $c->Cursor($m,$x);
 unless($n =~ /^\d+$/)
  { 
   $c->itemconfigure($c->{"c$n"},-state => 'hidden');
   $c->itemconfigure($c->{"c$m"},-state => 'hidden');
  }  
 if (($n eq '1' && !$c->cget('-range1')) || ($c->{"cursor$n"} != $c->{"cursor$m"}))
  {
   my ($t1,$t2) = ($c->{"cursor$n"},$c->{"cursor$m"});
   ($t2,$t1) = ($t1,$t2) if $t1 > $t2;
   $c->Callback($callback => $t1,$t2,@args);
  } 
}

sub Cursor
{
 my ($c,$n,$x) = @_;
 $c->Tk::focus;
 $n = 1 if ($n eq '2' && !$c->cget('-range1'));
 if (@_ > 2)
  {
   $c->{"cursor$n"} = $c->x2val($x);
   $c->Callback(-command => "cursor$n");
  } 
 unless (exists $c->{"c$n"})
  {
   my @args;
   push(@args,-dash => '.') unless $n =~ /^\d+$/;
   $c->{"c$n"} = $c->create(line => [0,0,0,0],@args);
  }
 if ($c->{xmax} && defined $c->{"cursor$n"})
  { 
   $x   = $c->val2x($c->{"cursor$n"});  
   my $w = $c->Width;
   my $h = $c->Height;
   if ($x >= 0 && $x <= $w) 
    {
     $c->coords($c->{"c$n"},[$x,0,$x,$h]);
     $c->itemconfigure($c->{"c$n"},-state => 'normal');
    } 
   else
    {
     $c->itemconfigure($c->{"c$n"},-state => 'hidden');
    } 
  }
 else
  {
   $c->itemconfigure($c->{"c$n"},-state => 'hidden');
  }   
}

sub Populate
{
 my ($sc,$args) = @_;
 $sc->{trace} = {};  
 
 $sc->{xa} = $sc->create(line => [0,0,0,0]);
 $sc->{ya} = $sc->create(line => [0,0,0,0]);
	    
 $sc->ConfigSpecs(
		  -yscale => ['METHOD','yscale','Yscale',-1.0],  
		  -start  => ['METHOD','start','Start',0.0],  
		  -end    => ['METHOD','end','End',undef],  
		  -xmax    => ['METHOD','xmax','Xmax',undef],  
		  -access  => ['PASSIVE','access','Access','FETCH'],  
		  -command => ['CALLBACK','callback','Callback',undef],
		  -zoomcmd => ['CALLBACK','zoom','Zoom','doZoom'],
		  -rangecmd => ['CALLBACK','range','Range',undef],
                  -range1  => ['PASSIVE','range1','Range1','1'],
		  DEFAULT => [$sc], 
                 );  
}

sub xscale
{
 my $sc = shift;
 my $t0 = $sc->{start} || 0;
 my $t1 = $sc->{end}   || 0;
 my $w  = $sc->Width;
 return ($t1-$t0)/$w;
}

sub x2val
{
 my ($sc,$x) = @_;
 return $sc->start + $x*$sc->xscale; 
}

sub val2x
{
 my ($sc,$t) = @_;
 return ($t-$sc->start)/$sc->xscale; 
}

sub scheduleRedisplay
{
 my ($sc,@args) = @_;
 unless ($sc->{redisplay})
  {
   $sc->{redisplay} = $sc->afterIdle([Redisplay => $sc, @args]);
  }
}

sub traces
{
 my $sc = shift;
 return keys %{$sc->{trace}}; 
}

sub trace
{
 my ($sc,%args) = @_;
 my $id;
 if (exists $args{-data})
  {
   my $data = delete $args{-data};
   $id = $sc->create('line',[0,0,0,0],%args);
   $sc->traceconfigure($id,-data => $data);
  }
 else
  {
   $id = $sc->create('line',[0,0,0,0],%args);
  } 
 return $id;
}

sub traceconfigure
{
 my ($sc,$id,%args) = @_;
 if (exists $args{-data})
  {
   my $data = delete $args{-data};
   $sc->{trace}{$id} = $data;
   $sc->scheduleRedisplay('data');
  } 
 $sc->itemconfigure($id,%args) if keys %args; 
}

sub tracecget
{
 my ($sc,$id,$key) = @_;
 return $sc->{trace}{$id} if $key eq '-data';
 return $sc->itemcget($id,$key); 
}

sub attrib
{
 my ($sc,$key,$val) = @_;
 if (@_ > 2)
  {
   # warn "$key = $val\n";
   $sc->{$key} = $val; 
   $sc->scheduleRedisplay($key);
  } 
 return $sc->{$key};
}

foreach my $meth (qw(yscale start end xmax cursor1 cursor2))
 {
  no strict 'refs';
  my $key = $meth;
  *$meth = sub { shift->attrib($key => @_) };
 }
 
sub audio
{
 my ($sc,$t1,$t2,@tr) = @_;
 (@tr) = keys %{$sc->{trace}} unless @tr;
 my @result;
 
 foreach my $tr (@tr)
  {
   my $data = $sc->{trace}{$tr}->timerange($t1,$t2);
   return $data unless wantarray;
   push @result,$data;
  }
 return @result; 
}

sub Redisplay
{
 my ($sc,$why) = @_;
 delete $sc->{redisplay};
 # warn "Redisplay $why\n";
 my $w  = $sc->Width;
 my $h   = $sc->Height/2;
 my $dur = $sc->{xmax};
 
 $sc->Callback(-xscrollcommand => $sc->start/$dur,$sc->end/$dur) if $dur;  
 
 foreach my $n (1,2)
  {
   $sc->Cursor($n) if exists $sc->{"cursor$n"};
  }
  
 foreach my $tr (keys %{$sc->{trace}})
  {
   my $ys = $sc->{yscale};
   my $data = $sc->{trace}{$tr};
   my $rate = $data->samples/$sc->{xmax};
   next unless $rate;
   my @coord;
   my $acc = $sc->cget('-access');
   my $ds = ($sc->x2val(1)-$sc->start)*$rate;
   my $yb = $h;
   if ($ds > 1)
    {
     # several samples in one pixel
     # warn "$ds = max/min\n"; 
     unless (defined $ys)
      {
       my ($max,$min) = $data->bounds($sc->x2val(0),$sc->x2val($w-1));
       $ys = ($max == $min) ? -1 : -2/($max-$min);  
       $yb = 2*$h-($min*$h*$ys);
      }
     for my $x (0..$w-1)
      {
       my $t  = $sc->x2val($x);
       my $t1 = $sc->x2val($x+1);
       my $samp = int($t*$rate);
       my ($max,$min);
       if ($acc eq 'FETCH')
        {
	 ($max,$min) = $data->bounds($t,$t1);
         $max = $max*$h*$ys+$yb;
         $min = $min*$h*$ys+$yb;
	}
       else 
        { 
         while ($t < $t1)
          {
           my $v = $data->$acc($samp);
           $v = 0 unless defined $v;
           my $y = $v*$h*$ys+$yb;
           $max = $y if (!defined($max) || $y > $max);
           $min = $y if (!defined($min) || $y < $min);
	   $samp++;
	   $t += 1/$rate;
  	  } 
	}  
       push(@coord,$x,$max,$x,$min);	
      }
     } 
    else
     {
      # several pixels in one sample
      # warn "$ds = lines\n"; 
      unless (defined $ys)
       {
        my ($max,$min);
        for (my $t = $sc->start; $t < $sc->end; $t += 1/$rate)
         {
          my $samp = int($t*$rate);
          my $v = $data->$acc($samp);
          $v = 0 unless defined $v;
          $max = $v if (!defined($max) || $v > $max);
          $min = $v if (!defined($min) || $v < $min);
	 }
	$ys = ($max == $min) ? -1 : -2/($max-$min);  
	$yb = 2*$h-($min*$h*$ys);
       }
      for (my $t = $sc->start; $t < $sc->end; $t += 1/$rate)
       {
        my $samp = int($t*$rate);
        my $v = $data->$acc($samp);
        $v = 0 unless defined $v;
        my $y = $v*$h*$ys+$yb;
	push(@coord,$sc->val2x($t),$y);
       } 
     } 
   $sc->coords($tr,\@coord); 
  }
 $sc->Callback(-command => $why);
}


sub scroll
{
 my ($sc,$am,$what) = @_;
 # warn "scroll $am $what\n";
 my $new = $sc->x2val($am*(($what eq 'pages') ? $sc->Width/2 : 1));
 $sc->moveto($new/$sc->xmax);
}

sub moveto
{
 my ($sc,$frac) = @_;
 # warn "moveto $frac\n";
 my $w = $sc->end - $sc->start;
 my $s = $sc->xmax*$frac;
 my $e = $s+$w;
 if ($e > $sc->xmax)
  {
   $e = $sc->xmax;
   $s = $e-$w;
  }
 if ($s < 0)
  {
   $s = 0; 
   $e = $s+$w; 
  }
 $sc->start($s);
 $sc->end($e);
}

sub xview
{
 my ($sc,$cmd,@args) = @_;
 $sc->$cmd(@args);
}

my %page_sizes;

sub page_sizes
{
 unless (keys %page_sizes)
  {
   my @list;
   my ($w,$h) = (297,420);
   for my $size (3..5)
    {
     $page_sizes{"A$size"} = [$w,$h];
     ($h,$w) = ($w,$h/2);
    }
  }
 return \%page_sizes;
}

sub printers
{
 my @list;
 if (open(my $fh,"/etc/printcap"))
  {
   while (<$fh>)
    {
     s/^\s+//;
     s/#.*$//;
     push(@list,$1) if (/^(\w+)/);
    }
   close($fh);
  }
 else
  {
   warn "Cannot open /etc/printcap:$!";
  }
 return @list;
}



sub Print
{
 my ($c) = @_;
 my $d = $c->DialogBox(-buttons => [qw(Ok Cancel)],
                       -title => 'Print Options',
		       -popover => 'cursor', -popanchor => 'nw');
 my $ps = page_sizes();
 my $psize = 'A4';
 my $mode = 'color';
 my $path = 'plot.ps';
 my $what = 'All';
 my $printer = 'File';
 my @lopts = (-anchor => 'e', -justify => 'right');
 Tk::grid(
          $d->add('Label',-text => 'Print:',@lopts),
#         $d->add('Optionmenu', -variable => \$what, -options => ['Window','All']),
          $d->add('Label',-text => 'Paper Size:',@lopts),
          $d->add('Optionmenu', -variable => \$psize, -options => [sort keys %$ps]),
          -sticky => 'nsew'
         );
 Tk::grid(
          $d->add('Label',-text => 'Colour mode:',,@lopts),
          $d->add('Optionmenu', -variable => \$mode, -options => [qw(color gray mono)]),
          $d->add('Label',-text => 'Printer:',@lopts),
          $d->add('Optionmenu', -variable => \$printer, -options => [File => printers()]),
          -sticky => 'nsew'
         );
 Tk::grid($d->add('Label',-text => 'File Name:',@lopts),
          $d->add('Entry', -textvariable => \$path, -width => 30),'-',
          $d->add('Button', -text => 'Browse ...',
           -command => sub { $path = $d->getSaveFile(
	                      -initialfile => $path,
			      -initialdir => getcwd(),
                              -filetypes => [
                                ['PostScript Files' => ['.ps']],
                                ['All Files', '*']
                                ])
		     }
		),
         -sticky => 'nsew'
         );

 my $dst;
 do
  {
   my $opt = $d->Show();
   return if $opt eq 'Cancel';
   if ($printer eq 'File')
    {
     unless (length($path) && open($dst,">$path"))
      {
       $c->messagebox(-text => "Cannot open $path:$!");
      }
    }
   else
    {
     ($dst,$path) = tempfile();
    }
  } until (defined($dst) && fileno($dst));

 my %opt = (-colormode  => $mode);

 if ($what eq 'All')
  {
   @opt{'-x','-y','-width','-height'} = $c->bbox('all');
  }
 else
  {
   @opt{'-x','-y','-width','-height'} = ($c->canvasx(0), $c->canvasy(0),
                                         $c->canvasx($c->width), $c->canvasy($c->height));
  }
 $opt{'-width'}  -= $opt{'-x'};
 $opt{'-height'} -= $opt{'-y'};

 # PS origin is normally SW so avoid deltaX, deltaY
 $opt{'-pageanchor'} = 'sw';

 # Get raw page size - we are working in mm
 my ($pw,$ph) = @{$ps->{$psize}};

 # Allow a margin on each edge
 my $margin = 25.4*0.25;  # 1/4 inch on each edge

 $pw -= 2*$margin;
 $ph -= 2*$margin;
 $opt{-pagey}  = $margin;
 $opt{-pagey}  = $margin;

 # Decide if it fits better rotated
 $opt{'-rotate'} = ($opt{'-width'} > $opt{'-height'}) ? 1 : 0;
 if ($opt{-rotate})
  {
   # x is bigger so rotate, pw/ph are swapped
   ($pw,$ph) = ($ph,$pw);
   # and we have to shift it right to allow for rotate as ->postscript
   # method does not take that into account the scaled image will be at
   # most the "height" of the paper
   $opt{-pagex} += $ph;
  }

 # Now compute scale. pw/ph correspond to x/y size of space
 my $xs = $pw / $opt{-width};
 my $ys = $ph / $opt{-height};
 # Core tk prefers -pagewidth to -pageheight if both specified
 # So only add the one which corresponds to smallest scale so
 # that whole image fits
 if ($xs < $ys)
  {
   $opt{-pagewidth} = $pw;
  }
 else
  {
   $opt{-pageheight} = $ph;
  }
 # Now we have finished doing calculations append unit marker for core tk.
 foreach my $key (qw(-pagewidth -pageheight -pagex -pagey))
  {
   $opt{$key} .= 'm' if exists $opt{$key};
  }
# use Data::Dumper;
# warn Dumper(\%opt);
# pseudo_code($c,%opt);
 my ($fh,$temp) = tempfile();
 $c->postscript(-file => $temp, %opt);

 # Now edit PostScript to get thin lines
 # 1 pixel at 300dpi is 72/300 points
 my $t = 72/300;
 while (<$fh>)
  {
   if (/^(\s*(\d+(\.\d+))){2}\s+scale/)
    {
     warn "scale $2\n";
     $t /= $2;
    }
   else
    {
     s/^(\s*)(\d+)(\s+)setlinewidth/"$1".$t*$2."$3setlinewidth"/e;
    }
   print $dst $_;
  }
 close($fh);
 close($dst);
 unlink($temp);

 if ($printer ne 'File')
  {
   system(lpr => "-P$printer",$path);
   unlink($path);
  }
}


1;
__END__