The Perl Toolchain Summit needs more sponsors. If your company depends on Perl, please support this very important event.
package GD::Dashboard;

use strict;
#use vars qw($VERSION @ISA @EXPORT @EXPORT_OK);

$GD::Dashboard::VERSION = '0.04';


# Preloaded methods go here.


#
# Constructor options:
#
# FNAME
# QUALITY
#
sub new
{
   my $proto = shift;

   my $self = {
      METERS  => {},
      FNAME => '',
      QUALITY => 100
   };


   # load in options supplied to new()
   for (my $x = 0; $x <= $#_; $x += 2) 
   {
      my $opt = uc($_[$x]);
      
      defined($_[($x + 1)]) or die "Dashboard->new() called with odd number of option parameters - should be of the form option => value";
      $self->{$opt} = $_[($x + 1)]; 
   }

   bless($self);
   return $self;
}

#
# There can be many meters on a graphic.  To specify them, 
# you create a new meter, then pass it to this function, 
# along with its name.  All meters will be referred to by
# name.
#
sub add_meter
{
   my ($self,$name,$meter) = @_;
   $self->{METERS}->{$name} = $meter;
}

#
# Why would you want to use get_meter?  A couple of reasons.
# First, you might have called add_meter(new Dashboard::Gauge()).
# Second, if you have multiple dash layouts, you have probably
# written the code so that you don't have access to the original
# meter variables at the point where you need to set them.
# 
sub get_meter
{
   my ($self,$name) = @_;   
   $self->{METERS}->{$name};
}

sub gdimage
{
   my ($self) = @_;
   my ($aref) = $self->{METERS};
   my $fname = $self->{FNAME};
   
   if (!defined($fname) || $fname eq '')
   {
      warn("GD::Dashboard::gdimage(): You must set FNAME in constructor first!") ;
      return undef;
   }
   
   # Get canvas from specified background graphics   
   my $im;
   
   if ($self->{FNAME} =~ /png$/ )
   {
      $im = GD::Image->newFromPng($self->{FNAME});
   }
   else
   {
      $im = GD::Image->newFromJpeg($self->{FNAME});
   }
   
   # Draw all my meters
   for my $m (keys(%{$aref}))
   {
      my $m2 = $aref->{$m};
      $m2->write_gdimagehandle($im);
   }

   $im;
}

sub png
{
   my ($self) = @_;

   my $im = $self->gdimage;

   return $im->png();
}

sub jpeg
{
   my ($self) = @_;

   my $im = $self->gdimage;

   return $im->jpeg($self->{QUALITY});
}

#
# Is anything wrong with me using this filehandle (HG1) ?
#
sub write_jpeg
{
   my ($self,$fname) = @_;
   
   open (HG1,'>'.$fname);
   binmode HG1;
   print HG1 $self->jpeg();
   close HG1;
}

sub write_png
{
   my ($self,$fname) = @_;
   
   open (HG1,'>'.$fname);
   binmode HG1;
   print HG1 $self->png();
   close HG1;
}

package GD::Dashboard::Base;

# insert base class for meters here.....

# All meters should support:
#      MIN => 0,
#      MAX => 100,
#      VAL => 50,
#      NX => 0,
#      NY => 0,
#      QUALITY => 100,

sub jpeg
{
}

sub write_jpeg
{
}

package GD::Dashboard::Gauge;

use GD;


#
# Constructor Options
#
# MIN
# MAX
# VAL
# NX
# NY
# NLEN
# NWIDTH
# NA1
# NA2
# NCOLOR
# QUALITY
# FNAME
# COUNTERCLOCKWISE
#
sub new
{
   my $proto = shift;

   my $self = {
      FNAME => '',
      MIN => 0,
      MAX => 100,
      VAL => 50,
      NX => 0,
      NY => 0,
      NLEN => 0,
      NWIDTH=>2,
      NA1=>0,
      NA2=>0,
      NCOLOR => [ 0, 0, 255 ],
      QUALITY => 100,
      COUNTERCLOCKWISE => 0
   };


   # load in options supplied to new()
   for (my $x = 0; $x <= $#_; $x += 2) 
   {
      my $opt = uc($_[$x]);
      
      defined($_[($x + 1)]) or die "Dashboard::Gauge->new() called with odd number of option parameters - should be of the form option => value";
      $self->{$opt} = $_[($x + 1)]; 
   }

   bless($self);
   return $self;
}

sub write_gdimagehandle
{
   my ($self,$im) = @_;
   $self->_draw_needle($im);   
}

#sub jpeg
#{
#   my ($self) = @_;
#
#   my $im = GD::Image->newFromJpeg($self->{FNAME});
#   
#   $self->write_gdimagehandle($im);
#   
#   return $im->jpeg(100);
#}
#
#sub write_jpeg
#{
#   my ($self,$fname) = @_;
#   
#   open (HG1,'>'.$fname);
#   binmode HG1;
#   print HG1 $self->jpeg();
#   close HG1;
#}

sub set_reading
{
   my ($self,$val) = @_;

   warn "Warning: set_reading called with value less than minimum." if $val < $self->{MIN};
   warn "Warning: set_reading called with value greater than maximum." if $val > $self->{MAX};
   
   $self->{VAL} = $val;
}


sub _draw_needle
{
   my ($self,$im) = @_;
   my ($x,$y);
   my $pi = 3.141592;
   
   # Must compute x,y coords for tip of needle.
   # Angle system for GD is in degrees, 0 is straight up, 
   # and they increase clockwise.  Sigh.  Angle system
   # for perl is in radians, 0 is as it is defined 
   # traditionally in math, angles increase counterclockwise.
   #
   
   my $norm =  ($self->{VAL}-$self->{MIN}) / ($self->{MAX} - $self->{MIN} );
   my $angle_width;
   
   if ( $self->{NA1} > $self->{NA2} )
   {
      if ($self->{COUNTERCLOCKWISE})
      {
         $angle_width = (2*$pi) - ($self->{NA1}-$self->{NA2}) ;
      }
      else
      {
         $angle_width =($self->{NA1}-$self->{NA2}) ;
      }
   }
   else
   {
      if ($self->{COUNTERCLOCKWISE})
      {
         $angle_width = ($self->{NA2}-$self->{NA1}) ;
      }
      else
      {
         $angle_width = (2*$pi - ($self->{NA2}-$self->{NA1}) );
      }
   }
  
   my $angle;
   if ($self->{COUNTERCLOCKWISE}==1)
   {
      $angle = $self->{NA1} + $norm * $angle_width;
   }
   else
   {
      $angle = $self->{NA1} - $norm * $angle_width;
   }

   $x = $self->{NX} + $self->{NLEN} * cos($angle);
   $y = $self->{NY} - $self->{NLEN} * sin($angle);
   
   # To draw a line with a width other than 1, you actually need
   # to create an image brush.  Sigh.
   #
      my $brush = _prepare_brush($self->{NWIDTH}, $self->{NCOLOR});
      $im->setBrush($brush);

   # draw the needle!
   #
      $im->line($self->{NX},$self->{NY},$x,$y,gdBrushed);

   # how to clean up the brush?
}



#####################
#
# Private functions
#
#####################

##  set the gdBrush object to trick GD into drawing fat lines
sub _prepare_brush 
{
  my ($radius, $ref_color) = @_;
  my (@rgb, $brush, $white, $newcolor);

  # get the rgb values for the desired color
#  @rgb = (0,0,255);
#  @rgb = (255,0,128);
   @rgb = @{$ref_color};
  # create the new image
  $brush = GD::Image->new ($radius*2, $radius*2);

  # get the colors, make the background transparent
#  $white = $brush->colorAllocate (255,255,255);
  $white = $brush->colorAllocate (0,0,0);
  $newcolor = $brush->colorAllocate (@rgb);
  $brush->transparent ($white);

  # draw the circle
  $brush->arc ($radius-1, $radius-1, $radius, $radius, 0, 360, $newcolor);

  # set the new image as the main object's brush
  return $brush;
}



package GD::Dashboard::WarningLight;

#
# TRANSPARENT
# NX
# NY
# FNAME
# VAL
#
sub new
{
   my $proto = shift;

   my $self = {
      VAL => 0,       # 0=off, 1=on
      NX => 0,
      NY => 0,
      FNAME => ''
   };


   # load in options supplied to new()
   for (my $x = 0; $x <= $#_; $x += 2) 
   {
      my $opt = uc($_[$x]);
      
      defined($_[($x + 1)]) or die "Dashboard::WarningLight->new() called with odd number of option parameters - should be of the form option => value";
      $self->{$opt} = $_[($x + 1)]; 
   }

   bless($self);
   return $self;
}

sub write_gdimagehandle
{
   my ($self,$im) = @_;

   if ($self->{VAL} == 1)
   {
      # load the current image
      my $im2 = GD::Image->newFromJpeg($self->{FNAME});
      my ($w,$h) = $im2->getBounds();
      
      if (defined($self->{TRANSPARENT}))
      {
         my $white = $im2->colorClosest(255,255,255);  #TODO this should be a param
         $im2->transparent($white);
      }
      $im->copy($im2,$self->{NX},$self->{NY},0,0,$w,$h);
   }
}


sub set_reading
{
   my ($self,$val) = @_;

   $self->{VAL} = $val;
}

package GD::Dashboard::HorizontalBar;

# Options:
#   TRANSPARENT = [ r,g,b ]
#   SPACING = N
#   MIN
#   MAX
#
sub new
{
   my $proto = shift;

   my $self = {
      MIN => 0,
      MAX => 100,
      VAL => 50,
      NX => 0,
      NY => 0,
      QUALITY => 100,
      DIRECTION=>0,
      BARS=>[],
      SPACING => 0
   };


   # load in options supplied to new()
   for (my $x = 0; $x <= $#_; $x += 2) 
   {
      my $opt = uc($_[$x]);
      
      defined($_[($x + 1)]) or die "Dashboard::HorizontalBar->new() called with odd number of option parameters - should be of the form option => value";
      $self->{$opt} = $_[($x + 1)]; 
   }

   bless($self);
   return $self;
}

sub add_bars
{
   my ($self,$cnt,$fname,$fnameoff) = @_;
   if (!defined($fnameoff)) { $fnameoff = ''; }
   push @{$self->{BARS}}, { CNT=>$cnt,FNAME=>$fname,FNAME_OFF=>$fnameoff} ;
}

sub set_reading
{
   my ($self,$val) = @_;

#   warn "Warning: set_reading called with value less than minimum." if $val < $self->{MIN};
#   warn "Warning: set_reading called with value greater than maximum." if $val > $self->{MAX};
   
   $self->{VAL} = $val;
}

sub write_gdimagehandle
{
   my ($self,$im) = @_;

   # How many bars do we have?
   my $barcnt = 0;
   for my $href (@{$self->{BARS}}) { $barcnt += $href->{CNT}; }
   
   # How many must we display?
   my $norm =  $self->{VAL} / ($self->{MIN} + $self->{MAX} );
   my $disp = int ($barcnt * $norm);   

   # OK copy the graphics as necessary
   my $x = $self->{NX};
   for my $href (@{$self->{BARS}})
   {
      # load the current image
      my $im2 = GD::Image->newFromJpeg($href->{FNAME});
      
      if (defined($self->{TRANSPARENT}))
      {
         my $white = $im2->colorClosest(255,255,255);  #TODO this should be a param
         $im2->transparent($white);
      }
      
      my ($w,$h) = $im2->getBounds();

      
      my $cnt = $href->{CNT};
      while ($disp>0 && $cnt>0)
      {
         $im->copy($im2,$x,$self->{NY},0,0,$w,$h);
         $x += $w + $self->{SPACING};
         $disp--;
         $barcnt--;
         $cnt--;
      }
      
      # Now load up dark image and use it if necessary
      my $fn2 = $href->{FNAME_OFF};
      if (defined($fn2) && $fn2 ne '')
      {
         my $im3 = GD::Image->newFromJpeg($fn2);

         if (defined($self->{TRANSPARENT}))
         {
            my $wt = $im2->colorClosest(255,255,255);  #TODO this should be a param
            $im3->transparent($wt);
         }
         my ($w,$h) = $im2->getBounds();
         
         while ($cnt>0)
         {
            $im->copy($im3,$x,$self->{NY},0,0,$w,$h);
            $x += $w + $self->{SPACING};
            $cnt--;
         }
      }
   }   


}





# Autoload methods go after =cut, and are processed by the autosplit program.

1;
__END__

=head1 NAME

GD::Dashboard - Perl module to create JPEG graphics of meters and dials

=head1 SYNOPSIS

   my $dash = new GD::Dashboard();

   my $g1 = new GD::Dashboard::Gauge(
                      MIN=>0,
                      MAX=>$empcnt,
                      VAL=>$nopwp_cnt,
                      NA1=>3.14/2+0.85,
                      NA2=>3.14/2-0.85,
                      NX=>51,NY=>77,NLEN=>50                      
            );

   $dash->add_meter('RPM', $g1);
   $dash->write_jpeg('dash.jpg');

The Dashboard module aims at providing users with a quick and
easy way to create dashboard or cockpit like JPGs to display
key information.

Dashboard supports the following instruments:

  * Gauges with needles
  * Bar type gauges
  * Warning Lights

Dashboard is built on top of GD.pm, Licoln Stein's interface
to the GD library.

=head1 Classes

The dashboard module contains several classes.  These classes
typically represent either a dashboard or an instrument on 
the dashboard.  The Dashboard object serves as a collection
for the instruments.

=head2 Dashboard

The Dashboard object serves as the collection object that contains
the various instruments in the display.  You can add instruments
to the dashboard, access instruments through it, or tell it to draw
itself.

   my $dash = new Dashboard();
   $dash->add_meter('RPM', $g1);
   $dash->add_meter('Speedo', $g2);
   $dash->write_jpeg('dash.jpg');

=over 4

=item *
FNAME

This is the name of a JPG file to use for the background.  This 
graphic will typically have one or more gauges on it, upon which
this module will draw needles or other indicators.

=item *
QUALITY

The quality of the output JPEG, from 1 (low) to 100 (high).  Defaults to
100.  This value is passed directly to GD.

=back 4

=head3 add_meter(name, meter)

Adds a meter to the dash.  Create the meter using one of the
new() constructors first.  You can add Gauges, HorizontalBars, and
WarningLights.  The name is used by the get_meter() 
function if you need to access the meter later.

=head3 get_meter()

Gets a meter by name.  When adding a meter, you must give it a name.
You can then use get_meter to get the meter object.  This is useful
when you want to change a setting later, such as the meter's value.

=head3 jpeg()

Returns a JPG as a scalar value.

=head3 write_jpeg(fname)

Draws the dashboard to a jpg file given by fname.

=head3 png()

Returns a PNG as a scalar value.

=head3 write_png(fname)

Draws the dashboard to a PNG file given by fname.

=head2 Dashboard::Gauge

This class describes a typical dashboard gauge; that is, an 
instrument that has a needle that rotates.  The needle may
rotate clockwise or counterclockwise.  This gauge is similar
to a car speedometer or and airspeed indicator.

=head3 new()

Most gauge configuration is done in the constructor.  Here is a sample
for the gauge included with this package (m1.jpg):

   my $g1 = new GD::Dashboard::Gauge(FNAME=>base_path().'\icons\m1.jpg',
                      MIN=>0,
                      MAX=>$empcnt,
                      VAL=>$nopwp_cnt,
                      NA1=>3.14/2+0.85,
                      NA2=>3.14/2-0.85,
                      NX=>51,NY=>77,NLEN=>50                      
            );

=over 4

=item *
VAL

This indicates where the needle is pointing.  Generally it should
be somewhere between MIN and MAX.

=item *
MIN

This is the minimum VAL is ever expected to reach.  It corresponds
to a needle position of NA1.  Lower values are not truncated; however,
they will generate warnings.

=item *
MAX

This is the maximum VAL is ever expected to reach.  It corresponds
to a needle position of NA2.  Higher values are not truncated; however,
they will generate warnings.

=item *
NX

This is the X coordinate of the base of the needle.

=item *
NY

This is the Y coordinate of the base of the needle.

=item *
NLEN

This is the length of the needle to draw.

=item *
NWIDTH

This is the width of the needle.

=item *
NA1

NA1 and NA2 are potentially the most confusing parameters.  They
represent the angle of the needle at its MIN and MAX points.  NA1
is the angle that corresponds to VAL=MIN, while NA2 is VAL=MAX.  The
angle is expressed in radians, the same way you would express an angle
to one of perl's trigonometric functions.

=item *
NA2

See NA1.

=item *
NCOLOR

This is the color of the needle.  This value should be passed as a 
reference to an array of RGB values.

=item *
COUNTERCLOCKWISE

Set to 1 if needle moves from MIN to MAX in a counterclockwise direction.
Otherwise you can ignore it.

=back 4

=head2 Dashboard::HorizontalBar

This class describes an LED bargraph display of the type often
found in a graphical equalizer or, on some cars, the oil condition
indicator.  It may be all one color, or it may use different colors
in different ranges.

The graph goes from left to right and consists of a number of bars, meant
to represent LEDs.  Bars can be identical or you can configure different
bars, for example to have the last couple of bars be red instead of green.

   my $m1 = new GD::Dashboard::HorizontalBar(
                  NX => 235,
                  NY => 348,
                  SPACING => 1
                  );
   $m1->add_bars(20,base_path().'\icons\barlight_on.jpg','\icons\barlight_off.jpg');
   $dash->add_meter('m1',$m1);

=head3 new()

=over 4

=item * 
MIN = N
The value representing zero bars illuminated.  Defaults to 0.

=item *
MAX = N
The value representing all bars illuminated.  Defaults to 100.

=item * 
VAL = N
The value to display.  Number of bars illuminated will be 
val / (max-min) percent of total.

=item *
   TRANSPARENT = [ r,g,b ]

This is currently not implemented correctly.  If you pass any array 
reference to this parameter, WHITE will be transparent.  This allows
you to have non-rectangular bars.  Email me if the white bit is a problem.

=item *
   SPACING = N

If you would like bars to be separated by a number of pixles, specify
the number in this parameter.

=back 4

=head3 add_bars(count, fname, fnameoff)

Call this for each different group of bars you would like to add.  Count
is the number of bars.  Fname is the path to a JPG that represents the
bars in their ON state.  Fnameoff is an optional filename to a JPG
that represents the bar in the off state (these are often just built
into the dashboard background, however).

=head3 set_reading(val)

Sets the number of bars that are illuminated.  So if you have 20 bars
defined, 'val' should be between 0 and 20 inclusive.

=head2 Dashboard::WarningLight

This behaves like a warning light on a car dashboard.  It can be turned
on or off.  When VAL is 0, this gauge has basically no effect.  When
VAL is 1, it draws another graphic on the dashboard (this would typically
be the warning light on graphic).  Consequently, the dashboard graphic
should contain the warning light in its "off" state.

=head3 new()

Most configuration of the warning light is done via the constructor.

=over 4

=item * 
FNAME

This is a JPG file that will be drawn at NX,NY when the warning light
is turned on.

=item * 
VAL 

This can be 0 or 1.  A value of 1 turns the warning light on, i.e., it
causes the graphic FNAME to be drawn at NX,NY.

=item *
NX

X position of lower right of graphic FNAME.

=item *
NY

Y position of lower right of graphic FNAME.

=item *
TRANSPARENT

Currently, set this to 1 to make WHITE transparent.  I probably
should make this take an RGB array ref.  Email me if you want it.

=back 4

=head3 set_reading(val)

Sets the VAL parameter.  This can be 0 (warning light off) or 1 (warning
light on).

=head1 NOTES

This is the first release.  There are a few things on my mind for 0.02.  
First, PNG support would be easy to add in.  I don't use it so I haven't
added it (yet).  Email if you want it.  Second, all of the meters are 
probably going to derive from a base class.  Haven't had time to change
it yet.

Eventually I should pay more attention to the needle drawing in the
Gauge class.  If your art is really good, the needles bring it down :(

I'm sure the docs could be better.

=head1 AUTHOR

David Ferrance (dave@ferrance.com)

=head1 LICENSE

Dashboard: A module for creating dashboard graphics.

Copyright (C) 2002 David Ferrance (dave@ferrance.com).  All Rights Reserved. 

This module is free software. It may be used, redistributed and/or modified under the same terms as perl itself. 

Sample graphics provided by rabia@rabia.com.  This module isn't worth much
without a good graphics person to provide you with sweet dashboard layouts.


=cut