#!/usr/bin/perl
# file: chart             G. Moody        7 October 2000
#                         Last revised:   8 February 2008
# _____________________________________________________________________________
# The Chart-O-Matic (CGI program to display PhysioBank data in a web browser)
# Copyright (C) 2000-2008 George B. Moody
#
# This program is free software; you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by the
# Free Software Foundation; either version 2 of the License, or (at your
# option) any later version.
#
# This program 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 General Public License for
# more details.
#
# You should have received a copy of the GNU General Public License along with
# this program; if not, write to the Free Software Foundation, Inc.,
# 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA.
#
# You may contact the author by e-mail (george@mit.edu) or postal mail
# (MIT Room E25-505A, Cambridge, MA 02139 USA).  For updates to this software,
# please visit PhysioNet (http://www.physionet.org/).
# _____________________________________________________________________________

use CGI qw(:standard);
use CGI::Carp 'fatalsToBrowser';

$| = 1;

# _____________________________________________________________________________
# Configuration constants: customize as needed for your installation

$PNH = '/home/physionet/html/';		# root of the http document directory
$PBD = $PNH . 'physiobank/database/';	# directory containing databases
$PTMP = '/ptmp/charts/';		# cache for generated charts
$CONVERT = '/usr/bin/convert';		# path to 'convert' from ImageMagick
$MAIL = '/bin/mail';			# path to command-line 'mail' utility
$PSCHART = '/usr/bin/pschart';		# path to 'pschart' from WFDB package
$TIMETOSEC = '/usr/bin/time2sec';	# path to 'time2sec' from WFDB package

# _____________________________________________________________________________
# Make sure that PTMP exists and is world-readable and -writable!
umask 0;
mkdir "$PTMP", 0777;

# Read the list of databases and their descriptions (once only).
if (!$databases[0]) {
    read_dblist();
}

# Decide what is to be done next.
if (param) {
    $action = param('doit');  # the name of the button that was clicked, if any
}
else {
    $action = "Select database";  # default: choose a database on entry
}

if ($action eq "Select database") {
    choose_db();
}

else {  # database has been chosen; get lists of records and annotators
    $database = param('database');
    if (!param('record')) {
	choose_record(); # pick one of each (also time and scale)
	$action = "Continue";
    }
    $record = param('record');
    if ($record =~ /.+\n/) {
	chop($record);
    }
    $annotator = param('annotator');
    $tstart = param('tstart');
    $width = param('width');
    if ($action ne 'Continue') {
	output_results(); # depending on which button clicked, show or email
    }
}

# _____________________________________________________________________________
# Read the list of available databases.
sub read_dblist {
    $dblistfile = $PBD . 'DBS-with-signals';

    if (open(DBS,$dblistfile)) {
	@dblist = <DBS>;
	$i = 0;
	foreach $d (@dblist) {
	    @fields = split(/\t+/,$d);
	    chop($fields[1]);
	    $dblist[$i++] = $fields[0];
	    $dblabels{$fields[0]} = $fields[1] . " (" . $fields[0] . ")";
	}
    }
    else {
	@dblist = ('');
    }
}

# _____________________________________________________________________________
# Show the database choice form.
sub choose_db {
    print header, start_html(-dtd=>'-//IETF//DTD HTML//EN',
			     -bgcolor=>'white',-title=>'Chart-O-Matic');
    print_header();
    print start_form,
    table({-border=>0,-bgcolor=>'#d0d0ff',-width=>'100%'},
		Tr({-align=>CENTER,-valign=>CENTER},
		   [
		    th('Database:'.
		       popup_menu(-name=>'database',
				  -value=>[@dblist],
				  -labels=>{%dblabels}).
		       submit(-name=>'doit',-value=>'Continue'))
		    ]
		   )),
	  end_form;
    print h2('Chart-O-Matic: View signals and annotations');
    print <<EOF;
<p>

The Chart-O-Matic allows you to obtain a "chart recording" of a portion of a
PhysioBank record.  Please choose a database from the list above, then click
on <em>Continue</em>.  Once you have done so, you will be able to choose a
record, an annotator (if available), the starting time, and the size of the
chart recording.  You will be able to view the chart with your web browser,
or you can have it sent to you via e-mail.

<p>
A key for the annotation codes is available
<a href="/physiobank/annotations.shtml">here</a>.  To view the annotations,
including the times of occurrence and other attributes, as text, try the <a
href="/cgi-bin/rdann">rdann-O-Matic</a>.  To view the digitized waveforms as
text, try the <a href="/cgi-bin/rdsamp">rdsamp-O-Matic</a>.
<hr>
EOF
    print_footer();
}

# _____________________________________________________________________________
# Read the list of records for the chosen database.
sub read_rlist {
    $rlistfile = $PBD . $database . '/RECORDS';

    if (open(RECORDS,$rlistfile)) {
	@rlist = <RECORDS>;
    }
    else {
	@rlist = ('');
    }
}

# _____________________________________________________________________________
# Read the list of annotators for the chosen database.
sub read_alist {
    $alistfile = $PBD . $database . '/ANNOTATORS';

    if (open(ANNOTATORS,$alistfile)) {
	@annotators = <ANNOTATORS>;
	$i = 0;
	foreach $a (@annotators) {
	    @fields = split(/\t+/,$a);
	    chop($fields[1]);
	    $alist[$i++] = $fields[0];
	    $adlist{$fields[0]} = $fields[0] . " (" . $fields[1] . ")"; 
	}
	$alist[$i++] = '';
	$adlist{''} = "(no annotations)";
    }
    else {
	$alist[0] = '';
	$adlist{''} = ('(no annotations)');
    }
}

# _____________________________________________________________________________
# Show the record/annotator/start/chart width choice form.
sub choose_record {
    read_rlist();
    read_alist();

    print header, start_html(-dtd=>'-//IETF//DTD HTML//EN',
			     -bgcolor=>'white',-title=>'Chart-O-Matic ('.
			     $dblabels{$database}.")");
    print_header();
    
    print start_form,
          table({-border=>0,-bgcolor=>'#d0d0ff',-width=>'100%'},
		Tr({-valign=>CENTER},
		   [
		    th({-align=>RIGHT},
		       'Database:') .
		    td({-align=>LEFT}, '<a href=/physiobank/database/' .
		       $database . "/>" . $dblabels{$database} . "</a>",
		       submit(-name=>'doit', -value=>'Select database')),

		    th({-align=>RIGHT},'Record:') .
		    td({-align=>LEFT}, popup_menu(-name=>'record',
						 -values=>[@rlist])) .
		    td({-align=>RIGHT}, '<a href=/cgi-bin/rdsamp?database=' .
		       $database . '&record=' . $record . '&tstart=' . $tstart . '&width=' . $wsec .
		       '>Convert signals to text</a>'),

		    th({-align=>RIGHT},'Annotator:') .
		    td({-align=>LEFT}, popup_menu(-name=>'annotator',
						 -values=>[@alist],
						 -labels=>{%adlist})) .
		    td({-align=>RIGHT}, '<a href=/cgi-bin/rdann?database=' .
		       $database . '&record=' . $record . '&tstart=' . $tstart . '&width=' . $wsec .
		       '&annotator=' . $annotator .
		       '>Convert annotations to text</a>'),


		    th({-align=>RIGHT},'Start time:') .
		    td({-align=>LEFT},textfield(-name=>'tstart',
					       -size=>40, -value=>'0')) .
		    td({-align=>RIGHT},
		   '<a href=/physiobank/annotations.shtml>Annotation key</a>'),

		    th({-align=>RIGHT},'Chart width:') .
		    td({-align=>LEFT},radio_group(-name=>'width',
					   -value=>['small', 'medium','large'],
						 -default=>'medium')),

		    td({-align=>RIGHT},
		       submit(-name=>'doit',-value=>'Show chart')) .
		    td({-align=>LEFT},submit(-name=>'doit',
					    -value=>'E-mail chart to:'),
		       textfield(-name=>'email',-size=>20)) .
		    td({-align=>RIGHT},submit(-name=>'doit',
					     -value=>'Chart-O-Matic Help'))
		    ]
		   )),
	  hidden(-name=>'database',-value=>$database),
          end_form;
}

# _____________________________________________________________________________
# Show instructions for filling in the record/annotator choice form.
sub print_help {
    print h2('Chart-O-Matic: View signals and annotations');
    print <<EOF;
<p>
Please choose a record and an annotator from the lists shown above
(Choose "<em>(no annotations)</em>" if you wish to view signals without
annotations.)

<p>
To view the requested chart with your web browser, click on
<em>Show chart</em>.  Use your browser's <em>Save</em> or
<em>Save As</em> features to copy the chart to a file on your disk if you wish
to save it, or use the link provided above the chart to download a
high-resolution PostScript version of the chart.

<p> To obtain the requested chart in PostScript format via e-mail, enter
your e-mail address in the space provided above, then click on
<em>E-mail chart to:</em>.

<p> In the start time field, enter the elapsed time from the beginning
of the record, in <em>hh:mm:ss</em> format.  For example, to begin two
minutes from the beginning of the record, enter <tt>2:0</tt> in the
start time field (leading zero digits may be omitted).  Depending on
the chart width you choose, the chart will show 5, 10, or 15 seconds
of data beginning at the specified time.  If you choose a medium or
large plot, you may need to use your web browser's horizontal scroll
bar to view the right-hand side of the plot.

<p>
A key for the annotation codes is available
<a href="/physiobank/annotations.shtml">here</a>.  To view the annotations,
including the times of occurrence and other attributes, as text, try the <a
href="/cgi-bin/rdann">rdann-O-Matic</a>.  To view the digitized waveforms as
text, try the <a href="/cgi-bin/rdsamp">rdsamp-O-Matic</a>.

<p>
The chart output is generated by
<a href="/physiotools/wag/pschar-1.htm"><tt>pschart</tt></a>,
which is freely available in portable C source form and in precompiled binary
versions for several popular operating systems.  You may run <tt>pschart</tt>
on your own computer to obtain charts from PhysioNet and other sources in
PostScript form.

<hr>
EOF
}

# _____________________________________________________________________________
# Convert the $tstart string into elapsed time in seconds.
sub strtim{
    if ($tstart =~ /\[.+/) {	# $tstart is absolute; convert using time2sec
	$t2sfile = $PTMP . "t2s.$$";
	unless (fork) {
	    open(STDOUT, ">$t2sfile");
	    exec($TIMETOSEC, "-r", $database . "/" . $record, $tstart);
	}
	wait;
	open(T2SFILE, $t2sfile);
	$sec = <T2SFILE>;
	chop($sec);
	close(T2SFILE);
	unlink $t2sfile;
    }
    else {			# $tstart is relative;  convert to seconds here
	($t1,$t2,$t3,$t4) = split(/:/,$tstart);
	if ($t4 eq "") {
	    if ($t3 eq "") {
		if ($t2 eq "") {
		    $sec = $t1;
		}
		else {
		    $sec = 60 * $t1 + $t2;
		}
	    }
	    else {
		$sec = 3600 * $t1 + 60 * $t2 + $t3;
	    }
	}
	else {
	    $sec = 86400 * $t1 + 3600 * $t2 + 60 * $t3 + $t4;
	}
    }
    return $sec;
}

# _____________________________________________________________________________
# Show or email the selected chart.
sub output_results {
    set_chart_variables();

    # Since the user can pass any strings into this program, make sure that
    # the variables used to generate the file name have no characters other
    # than letters, digits, and underscores. (Record names can also have a
    # '.' -- this is needed for EDF input.)
    $sdb = $database;
    $sdb =~ tr/A-Za-z0-9_/-/cs;
    $srec = $record;
    $srec =~ tr/A-Za-z0-9_./-/cs;
    $sann = $annotator;
    $sann =~ tr/A-Za-z0-9_/-/cs;
    $stime = strtim();
    $stime =~ tr/A-Za-z0-9_/-/cs;
    $swid = $width;
    $swid =~ tr/A-Za-z0-9_/-/cs;
    if ($action ne 'E-mail chart to:') { # show chart in browser window
	$orientation = "-l"; # this is a hack -- we just want the default
	                     # (portrait) orientation in this case, so we
			     # repeat the -l (show signal names) option
    }
    else {	# email chart to user
	$orientation = "-L"; # use landscape mode in this case
    }
    $chname = $sdb . "-" . $srec . "-" . $sann . "-" . $stime . "-" . $swid . $orientation; 

    # Check if the requested chart is already in the cache.
    $tofile = $PTMP . $chname . ".ps";
    if (!open(TOFILE, $tofile)) {
	# Create a PostScript version of the requested chart using pschart.
	# First, create a script for pschart.
	$tifile = $PTMP . $chname . ".script";
	open(TIFILE, ">$tifile");
	$psrec = $record;
	$psrec =~ tr+/A-Za-z0-9_./+-+cs;    # preserve any '/' in record name
	print TIFILE $sdb . "/" . $psrec . " " . $stime . "\n";
	close(TIFILE);
	unless (fork) {
	    # Run pschart using the script, and put the output into the cache.
	    open(STDOUT, ">$tofile");
	    exec($PSCHART, "-a", $sann, "-E", "-t", "25",
		 "-v", "10", "-c", "", "-C", "-G", "-H", "-l", $orientation, "-P", $size,
		 "-m", "20", "20", "5", "5", "-M", "-n", "0", "-S", "4", "2",
		 "-T", "", "-CG", "1", ".5", ".5", "-Cs", "0", "0", "0",
		 "-w", "0.5", $tifile);
	}

	# Wait until pschart is finished before opening its output file.
	wait;
	open(TOFILE, $tofile);
    }

    # Read the pschart output from the cache.
    $postscript = <TOFILE>;
    if (!$postscript) {
	choose_record();
	print h1('No output!');
	print <<EOF;
<p>
No output was generated.  This might occur if the record or annotator name
is incorrect, or if the start time is improperly formatted.
Please check and correct your entries above.

<p>
If you are using one of the PhysioNet mirrors, it may not be able to generate
chart output;  in this case, try using the <a href="http://www.physionet.org/cgi-bin/chart">Chart-O-Matic on the master PhysioNet server</a>.
<hr>
EOF
        print_footer();
    }

    else {  # pschart was successful!
	if ($action ne 'E-mail chart to:') { # show chart in browser window
	    # Convert the PostScript to PNG.
	    $tpfile = $PTMP . $chname .".png";
	    
	    if (!open(TPFILE, $tpfile)) {
		unless (fork) {
		    exec($CONVERT, "-density", "100x100", $tofile, $tpfile);
		}

		# Wait for convert to finish writing the PNG file.
		wait;
	    }
	    else {
		close(TPFILE);
	    }

	    # Calculate starting times for forward/backward buttons on chart.
	    $tnext = $stime + $wsec;
	    $tprev = $stime - $wsec;
	    if ($tprev < 0) {
		$tprev = 0;
	    }

	    # Output the HTML to create the page containing the chart.
	    output_html();
	}
	else {
	    output_email();

	}

    }
    unlink($tifile);
}

# _____________________________________________________________________________
# Set chart specification variables.
sub set_chart_variables {
    $database = param('database');
    $record = param('record');
    if ($record =~ /.+\n/) {
	chop($record);
    }
    $annotator = param('annotator');
    $tstart = param('tstart');
    $width = param('width');

    if (!$database) {
	$database = "mitdb";
    }
    if (!$record) {
	$record = "100";
    }
    if (!$annotator) {
	$annotator = "";
    }
    if (!$tstart) {
	$tstart = 0;
    }
    if (!$width) {
	$width = "medium";
    }
    if (!$action) {
	$action = "Continue";
    }

    if ($width eq "small") {
	$size = "180x150";
	$wsec = 5;
    }

    if ($width eq "medium") {
	$size = "300x150";
	$wsec = 10;
    }

    if ($width eq "large") {
	$size = "425x150";
	$wsec = 15;
    }
}

# _____________________________________________________________________________
# Generate the HTML to display the chart in the window
sub output_html {
    choose_record();
    if ($action eq 'Chart-O-Matic Help') {
	print_help();
	print_footer();
	return;
    }

    print <<EOF;
<table width="100%">
<tr><td align=left>
<a href="/cgi-bin/chart?database=$database&record=$record&annotator=$annotator&tstart=$tprev&width=$width"><img src="/icons/arrow-left.png" alt="Back $wsec seconds"></a></td>
<td align=center><font size=+2><b>Record $database/$record</b></font></td>
<td align=right>
<a href="/cgi-bin/chart?database=$database&record=$record&annotator=$annotator&tstart=$tnext&width=$width"><img src="/icons/arrow-right.png" alt="Ahead $wsec seconds"></a></td></tr>
<tr><td></td><td align=center>
EOF
    print "Download a  <a href=/charts/" . $chname . ".ps>high-resolution";
    print <<EOF;
 PostScript version</a> of this chart</td><td></td></tr>
</table>
<p>
EOF
    print "<center><img src=/charts/" . $chname . ".png><br></center>";
    print end_html;
}

# _____________________________________________________________________________
# Send the output as email.
sub output_email {
    my $email = param('email');

    # Check that the user entered a plausible email address.
    if ($email =~ /^([-\w.]+)@([-\w]+)\.([-\w.]+)$/) {
	my $subject = 'Chart of record ' . $database . '/' .
	    $record . ', annotator ' . $annotator;
	system($MAIL . " -s '$subject' $email <$tofile");
	choose_record();
	print h1('Data sent');
	print <<EOF;
<p>
The requested data have been sent to $email.
EOF
    }
    else {
	choose_record();
	print h1('E-mail address needed');
	print <<EOF;
<p>
If you wish to transmit the requested data via e-mail, enter your e-mail
address in the space provided on the form, then click again on <em>E-mail
chart to:</em>.

<p>
If you wish to view the requested data with your web browser, click on
<em>Show chart</em>.  <strong>The data will appear in another
window.</strong>
<hr>
EOF
    }
    print_footer();
}

# _____________________________________________________________________________
# Boilerplate header.
sub print_header {
    if (open(HEADER, $PNH . "links-physiobank.html")) {
	while (<HEADER>) {
	    print $_;
	}
    }
}

# _____________________________________________________________________________
# Boilerplate footer.
sub print_footer {
    print <<EOF;

<font size=-1><p>
Please e-mail your comments and suggestions to 
<a
href="mailto:webmaster\@physionet.org?subject=Chart-O-Matic"><tt>webmaster\@physionet.org</tt></a>, 
or post them to:
<p>
<i><address>
PhysioNet<br>
MIT Room E25-505A<br>
77 Massachusetts Avenue<br>
Cambridge, MA 02139 USA<br></address></i>
<p>
Updated Wednesday, 1 November 2006 at 20:31 EST
</font>
EOF
    print end_html;
}
