## XrtUtils.pm is a sub-module of Graph.pm. It has all the subroutines 
## needed for the Xrt3d part of the package.
##
## $Id: XrtUtils.pm,v 1.11 2006/04/18 17:56:48 emile Exp $ $Name:  $
##
## This software product is developed by Michael Young and David Moore,
## and copyrighted(C) 1998 by the University of California, San Diego
## (UCSD), with all rights reserved. UCSD administers the CAIDA grant,
## NCR-9711092, under which part of this code was developed.
##
## There is no charge for this software. You can redistribute it and/or
## modify it under the terms of the GNU General Public License, v. 2 dated
## June 1991 which is incorporated by reference herein. This software is
## distributed WITHOUT ANY WARRANTY, IMPLIED OR EXPRESS, OF MERCHANTABILITY
## OR FITNESS FOR A PARTICULAR PURPOSE or that the use of it will not
## infringe on any third party's intellectual property rights.
##
## You should have received a copy of the GNU GPL along with this program.
##
##
## IN NO EVENT SHALL THE UNIVERSITY OF CALIFORNIA BE LIABLE TO ANY
## PARTY FOR DIRECT, INDIRECT, SPECIAL, INCIDENTAL, OR CONSEQUENTIAL
## DAMAGES, INCLUDING LOST PROFITS, ARISING OUT OF THE USE OF THIS
## SOFTWARE, EVEN IF THE UNIVERSITY OF CALIFORNIA HAS BEEN ADVISED OF
## THE POSSIBILITY OF SUCH DAMAGE.
##
## THE SOFTWARE PROVIDED HEREIN IS ON AN "AS IS" BASIS, AND THE
## UNIVERSITY OF CALIFORNIA HAS NO OBLIGATION TO PROVIDE MAINTENANCE,
## SUPPORT, UPDATES, ENHANCEMENTS, OR MODIFICATIONS. THE UNIVERSITY
## OF CALIFORNIA MAKES NO REPRESENTATIONS AND EXTENDS NO WARRANTIES
## OF ANY KIND, EITHER IMPLIED OR EXPRESS, INCLUDING, BUT NOT LIMITED
## TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY OR FITNESS FOR A
## PARTICULAR PURPOSE, OR THAT THE USE OF THE SOFTWARE WILL NOT INFRINGE
## ANY PATENT, TRADEMARK OR OTHER RIGHTS.
##
##
## Contact: graph-dev@caida.org
##
##
package Chart::Graph::XrtUtils;
use Exporter ();

@ISA = qw(Exporter);
@EXPORT = qw();
%EXPORT_TAGS = (UTILS => [qw(&_set_xrtpaths &_set_ldpath &_print_matrix 
			     &_print_array &_verify_ticks &_exec_xrt3d &_exec_xrt2d
			     &_exec_netpbm &_exec_xvfb &_try_port &_convert_raster
			     &_childpid_dead &_transfer_file)],
	       );

Exporter::export_ok_tags('UTILS');

use Carp;
use POSIX ":sys_wait_h";	# for waitpid()
use Chart::Graph::Utils qw(:UTILS); 

$cvs_Id = '$Id: XrtUtils.pm,v 1.11 2006/04/18 17:56:48 emile Exp $';
$cvs_Author = '$Author: emile $';
$cvs_Name = '$Name:  $';
$cvs_Revision = '$Revision: 1.11 $';

$VERSION = 3.1;

use strict;
use File::Basename;

use vars qw($xrt2d $xrt3d);

#
# Subroutine: set_converterpaths()
#
# Description: set paths for converter programs in particular.  This
#              subroutine can take one or two arguments and tests if
#              the required converter programs are indeed available
#              for the choosen method to convert a file from one
#              graphics format to another.
#
sub _set_converterpaths {
	my @converters = @_;

	# Loop through the list of converter seeing which are available
	foreach my $converter (@converters) {
	  if (not -x $$converter) {
		if (not $$converter = _get_path($$converter)) {
		  return(0);
		} 
	  }
	}
	return(1);
  }

#
# Subroutine: _convert_raster($plot_file, $output_file)
#
# Description: A subroutine to over see the conversion process from
#              one raster graphic format to another.  It will try
#              ImageMagick convert first and if that fails try Netpbm
#              utilities if they are available in that format.
#
sub _convert_raster {
    my $FORMAT = shift;
    my $plot_file = shift;
    my $output_file = shift;

  # First try ImageMagick as it is more robust and simpler
  if (_set_converterpaths(\$convert)) {
	if (_exec_convert($convert, $FORMAT, $plot_file, $output_file)) {
	  return(1);
	} else {
	  carp "Attempt to use ImageMagick failed, will try Netpbm."
	} 
  }	else {
	carp "No ImageMagick found, will try Netpbm."
  }

  if ($FORMAT eq 'GIF') {
	_try_netpbm_combo($xwdtopnm, $ppmtogif, $plot_file, $output_file);
  }
  elsif ($FORMAT eq 'JPG') {
	_try_netpbm_combo($xwdtopnm, $ppmtojpg, $plot_file, $output_file);
  }
  elsif ($FORMAT eq 'PNG') {
	_try_netpbm_combo($xwdtopnm, $pnmtopng, $plot_file, $output_file);
  } else {
	carp "Untrapped raster image format - XrtUtils.pm internal error";
	return(0);
  }
}

#
# Subroutine _try_netpbm_combo($xwdtopbm, pbmtotarget, $xwd_file, $target_file)
#
#
# Description: Contains the logic for testing if a combination of
#              netpbm programs can be accessed and executed to perform
#              the desired conversion.  If not, it produces the
#              appropriate error messages.  Basically, it saves a
#              batch of conditional statements that would otherwise be
#              repeated.
#
sub _try_netpbm_combo {
    my ($xwdtopbm, $pbmtotarget, $xwd_file, $target_file) = @_;
	
	if (_set_converterpaths(\$xwdtopbm, \$pbmtotarget)) {
	  if (_exec_netpbm($xwdtopbm, $pbmtotarget, $xwd_file, $target_file)) {
		return(1);
	  } else {
		carp "Failure to execute any suitable image " . 
		  "converters for create file: $target_file";
		return(0);
	  } 
	} else {
	carp "Unable to find any suitable image converters to " . 
	  "create file: $target_file";
	return(0);
  }
}

# 
# Subroutine: set_xrtpaths()
# 
# Description: set paths for external programs required by xrt()
#              if they are not defined already
#
sub _set_xrtpaths {

    my $xrtver = shift;



    if (defined($xrtver)) {
	  if ($xrtver eq "xrt2d") {
	    if (not $Chart::Graph::xrt2d = _get_path("xrt2d")) {
		  return 0;
	    }
	  }

	  if ($xrtver eq "xrt3d") {
	    if (not $Chart::Graph::xrt3d = _get_path("xrt3d")) {
		  return 0;
	    }
	  }
    }

    if (not defined($xwdtopnm)) {
	if (!($xwdtopnm = _get_path("xwdtopnm"))) {
	    return 0;
	}
    }

    if (not defined($xvfb)) {
	if (not $xvfb = _get_path("Xvfb")) {
	    return 0;
	}
    }

    # make sure /usr/dt/lib is in the library path
    _set_ldpath("/usr/dt/lib");

    return 1;
}

#
# Subroutine: set_ldpath()
#
# Description: Xvfb has trouble finding libMrm, so we have to add
#              /usr/dt/lib to LD_LIBRARY_PATH
#

sub _set_ldpath {
    my ($libpath) = @_;
    
    if (not defined($ENV{LD_LIBRARY_PATH})) {
	$ENV{LD_LIBRARY_PATH} = "$libpath";
	return 1;
    }

    my @ldpath = split (/:/, $ENV{LD_LIBRARY_PATH});

    # make sure library path isn't already defined
    foreach my $i(@ldpath){
	if ($i eq $libpath) {
	    return 1;
	}
    }

    # add library path to LD_LIBRARY_PATH
    $ENV{LD_LIBRARY_PATH} = "$libpath:$ENV{LD_LIBRARY_PATH}";
    return 1;
}

# 
# Subroutine: print_matrix() 
# 
# Description: print out all the elements 
#              in a X by Y  matrix, row by row
#

sub _print_matrix {
    my ($handle, @matrix) = @_;
    
    foreach my $row (@matrix){
	foreach my $i (@{$row}){
	    print $handle "$i\t";
	}
	print $handle "\n";
    }
    return 1;
}


#
# Subroutine:  _transfer_file($handle, $data_filename)
#
# Description: open file $data_filename.  Read the contents
# and write it into the command file tab delimited.  Don't
# assume data was tab delimited to be safe.
# 
sub  _transfer_file {
  my $handle = shift;
  my $data_filename = shift;
  my $data;
  my @elements;

  unless(open(DATAHDL, $data_filename)) {
	carp "Unable to open data file: $data_filename for reading";
	return(0);
  }
  while (defined($data = <DATAHDL>)) {
	chomp($data);
	@elements = split(/\s+/, $data);
	foreach my $element (@elements) {
	  print $handle $element, "\t";
	}
	print $handle  "\n";
  }
  unless(close(DATAHDL)) {
    carp "Unable to close data file: $data_filename after reading";
  }
  return(1);
}

# 
# Subroutine: print_array()
# 
# Description:  print out each element of array, one per line
#

sub _print_array {
    my ($handle, @array) = @_;
    my $i;
    
    foreach $i (@array) {
	print $handle "$i\n";
    }
    return 1;
}

# 
# Subroutine: verify_ticks();
#   
# Description: check that the number of tick labels is the same
#              as the number of xy rows and columns. we can only have
#              as many ticks as the number of rows or columns
#              we make this subroutine so that the calling subroutine
#              is kept cleaner.

sub _verify_ticks {
    my ($cnt, $ticks_ref) = @_;

    # if no ticks are given then just
    # give the xrt binary "1, 2,..."
    if (not defined($ticks_ref)) {
	my @def_ticks;
	for (my $i = 0; $i < $cnt; $i++) {
	    $def_ticks[$i] = $i + 1;
        }
	$ticks_ref = \@def_ticks;
    }

    my $tick_cnt = @{$ticks_ref};

    if ($cnt ne $tick_cnt){
	carp "number of tick labels must equal the number of xy rows and columns";
	return 0;
    }
    return 1;
}

# 
# Subroutine: exec_xrt3d()
#
# Description: execute the xrt3d program on the command file.
#              xrt3d generates a xwd file.
# 
sub _exec_xrt3d {
    my ($command_file) = @_;
    my ($output);
    my ($childpid, $port);
    my $display_env = $ENV{DISPLAY};
    my $status;

    if ($Chart::Graph::use_xvfb) {
	# start the virtual X server
	($childpid, $port) = _exec_xvfb();
	$status = system("$Chart::Graph::xrt3d -display :$port.0 < $command_file");
    } else {
	# use the local X server
	# warning: colors might be messed up 
	# depending on your current setup
	$status = system("$Chart::Graph::xrt3d -display $display_env < $command_file");
    }

    #my $status = system("$xrt -display :$port.0 < $command_file");
    if (not _chk_status($status)) {
	return 0;
    }

    if ($Chart::Graph::use_xvfb) {
	kill('KILL', $childpid);
    }

    return 1;
}

# 
# Subroutine: exec_xrt2d()
#
# Description: execute the xrt2d program on the command file.
#              xrt2d generates a xwd file.
#
sub _exec_xrt2d {
    my ($command_file, $options) = @_;
    my ($output);
    my ($childpid, $port);
    my $display_env = $ENV{DISPLAY};
    my $status;

    if ($Chart::Graph::use_xvfb) {
	# start the virtual X server
	($childpid, $port) = _exec_xvfb();
	printf STDERR "\tXRT is $Chart::Graph::xrt2d\n";
	my $status = system("$Chart::Graph::xrt2d -display ipn:$port.0 < $command_file $options");
    } else {
	# use the local X server
	# warning: colors might be messed up 
	# depending on your current setup
	$status = system("$Chart::Graph::xrt2d -display $display_env < $command_file $options");
    }

    if (not _chk_status($status)) {
	return 0;
    }

    if ($Chart::Graph::use_xvfb) {
	kill('KILL', $childpid);
    }

    return 1;
}

# 
# Subroutine: exec_convert 
# 
#
# Description: Use the Imagemagick 'convert' utility to convert the xwd
#              file into any one of the other common raster image
#              formats used commonly in web page production.
#
sub _exec_convert {
    my ($convert, $FORMAT, $xwd_file, $target_file) = @_;
    my ($status);


    if ($Chart::Graph::debug) {
	$status = system(join('', "$convert -verbose $xwd_file ", 
                              $FORMAT, ":$target_file"));
    } else {
	$status = system(join('', "( $convert $xwd_file ",  $FORMAT, 
                                  ":$target_file; ) 2> /dev/null"));
    }
    
    if (not _chk_status($status)) {
	return 0;
    }
    return 1;
}
# 
# Subroutine: _exec_netpbm
# 
#
# Description: Convert a raster image using the older utilities now
#              collected under the name 'netpbm.'  Note that not all
#              conversions are commonly included wiht all UNIX
#              distributions so that while older conversions such as
#              'xwd' -> 'gif' are likely to work, others such as
#              conversions to 'png' may not without downloading new
#              software.
#
#              The conversion strategy always involves a pipe from the
#              X-windows 'xwd' format to some sort 'pbm' format and
#              then from that universal format into the target format.
#              For this reason, it is more prone to machine
#              architecture issues and other errors.
#
sub _exec_netpbm {
    my ($xwdtopbm, $pbmtotarget, $xwd_file, $target_file) = @_;
    my ($status);
    
    if ($Chart::Graph::debug) {
	$status = system("$xwdtopbm $xwd_file | $pbmtotarget > $target_file");
    } else {
	$status = system(join('', "( $xwdtopbm -quiet $xwd_file | ", 
                              "$pbmtotarget -quiet > $target_file; ) ",
                              "2> /dev/null"));
    }
    
    if (not _chk_status($status)) {
	return 0;
    }
    return 1;
}

# 
# Subroutine: exec_xvfb()
#
# Description:  this starts the vitualX server(X is required by xrt, so 
#               we fake out xrt with Xvfb, for speed and compatability)
#
#
sub _exec_xvfb {
    my $port = 99;
    my $childpid;
    my $sleep_time = 1;
    my $try_count = 0;
    my $trialnumber;
    my $childpid_status;

    # starting with port 100, we try to start
    # the virtual server until we find an open port
    # because of the nature of the virtual x server
    # we use, in order to know if we have found an 
    # open port, we have to sleep.
    # we check the pid of the virtual x process we started
    # and see if it died or not.

    while ($childpid_status = _childpid_dead($childpid)) {
	$port++;
	$try_count++;
	if ($try_count > 10) {
	    die "Error: Failed too many times\n";
	}
	$trialnumber = _number_to_eng($try_count);
	print STDERR "*** $trialnumber try ***" unless (not $Chart::Graph::debug);
	$childpid = _try_port($port);	
	sleep($sleep_time);
    }
    print STDERR " SUCCESS!!!\n" unless (not $Chart::Graph::debug);

    # save the childpid so we can stop the virtual server later
    # save the $port so we can tell xrt where the virtual server is.
    return ($childpid, $port);
}
# 
# Subroutine: try_port();
#
# Description:  will try to start Xvfb on specified port
sub _try_port {

    my ($port) = @_;
    my ($childpid);
   
    #fork a process
    if (not defined($childpid = fork())){
	# the fork failed
	carp "cannot fork: $!";
	return 0;
    } elsif ($childpid == 0) {
	# we are in the child process
	if ($Chart::Graph::debug) {
	    if (not exec "$xvfb :$port") {
		die "can't do $xvfb :$port: $!\n";
	  }	
	}
	else {
	    if (not exec "$xvfb :$port 2> /dev/null") {
		die "can't do $xvfb :$port 2> /dev/null: $!\n";
	    }
	} 

	die "should never reach here\n";

    } else {
	# we are in the parent, return the childpid
	# so we can kill it later.
	return $childpid;
    }
    
}

# 
# Subroutine: childpid_dead
# 
# Description: check to see if a PID has died or not
#
#
sub _childpid_dead {
    my ($childpid) = @_;
    
    if (not defined($childpid)) {
	return 1;
    }

    # WNOHANG: waitpid() will not suspend execution  of
    # the  calling  process  if  status is not
    # immediately available  for  one  of  the
    # child processes specified by pid.
    return waitpid($childpid, &WNOHANG);
}

1;
