#!/usr/bin/env perl

# The contents of this file are subject to the Mozilla Public License
# Version 1.0 (the "License"); you may not use this file except in
# compliance with the License. You may obtain a copy of the License at
# http://www.mozilla.org/MPL/
#
# Software distributed under the License is distributed on an "AS IS"
# basis, WITHOUT WARRANTY OF ANY KIND, either express or implied. See the
# License for the specific language governing rights and limitations
# under the License.
#
# The Original Code is the Berkeley Open Infrastructure for Network Computing.
#
# The Initial Developer of the Original Code is the SETI@home project.
# Portions created by the SETI@home project are Copyright (C) 2002
# University of California at Berkeley. All Rights Reserved.
#
# Contributor(s):
#

# Stripchart - Version 2.1 by Matt Lebofsky  ( started: November 11, 2002 )
#
# Requires: gnuplot with .gif support (which may require gd)
#           perl 5.0 or later
#           stripchart.cnf (make sure you set the path the .cnf file below)
#
# Stripchart is a wrapper around gnuplot which takes in time-based
# data from a file and creates a web-friendly image. It is quite
# useful for system diagnostic checking, especially when used with
# stripchart.cgi
#
# You should only need to edit the variables in the section
# "GLOBAL/DEFAULT VARS" below
#
# Type "stripchart -h" for usage
#
# See doc/stripchart.txt for details
#

use Getopt::Std;
use File::Basename;
$|++;

################
# READ IN .cnf
################

# Where is the .cnf file?
$cnfpath = "./stripchart.cnf";

# Read it in:
open (CNFFILE,$cnfpath) or die "cannot open configuration file: $cnfpath\nmake sure this variable is set properly";
while (<CNFFILE>) { eval }
close (CNFFILE);

#############
# SUBS
#############

sub to_unix_time {
  # times should generally be in epoch seconds, but just in case they're not:

  # no colons and no decimal point? must already be in unix time
  if ($_[0] !~ /:/ && $_[0] !~ /\./ ) { return $_[0] }

  # colons = civil time
  if ($_[0] =~ /:/) {
    (my $year, my $month, my $day, my $hour, my $minute) = split /:/, $_[0];
    $month--;
    return timelocal(0,$minute,$hour,$day,$month,$year)
    }

  # must be julian date
  return (($_[0] - 2440587.5) * $daysecs);

  }

sub unix_to_civillabel {
  # convert epoch seconds to a pretty date string

  (my $minute, my $hour, my $day, my $month, my $year) =
     (localtime($_[0]))[1,2,3,4,5];
  $month++;
  $year+=1900;
  $year = substr($year,2,2);
  my $label = sprintf("%02d/%02d/%02d %02d:%02d",$month,$day,$year,$hour,$minute);
  return $label
  }

sub time_to_monthlabel {
  # convert epoch seconds to a simple month label (for x axis)

  (my $day, my $month) = (localtime($_[0]))[3,4];
  my $label = $monthabbs[$month] . " $day";
  return $label;

  }

sub time_to_hourlabel {
  # convert epoch seconds to a simple hour label (for x axis)

  (my $minute, my $hour) = (localtime($_[0]))[1,2];
  my $label = sprintf("%02d:%02d",$hour,$minute);
  return $label;

  }

#############
# MAIN
#############

# get command line options

if (getopts("f:t:r:i:o:O:l:T:x:y:Y:b:Bd:D:vLc:CsjVh") == FALSE || $opt_h) {
  print STDERR << "EOF";
stripchart: creates stripchart .gif graphic based on data in flat files
options:
  -i: input FILE      - name of input data file (mandatory)
  -o: output FILE     - name of output .gif file (default: STDOUT)
  -O: output FILE     - name of output .gif file and dump to STDOUT as well
  -f: from TIME       - stripchart with data starting at TIME
                        (default: 24 hours ago)
  -t: to TIME         - stripchart with data ending at TIME (default: now)
  -r: range RANGE     - stripchart data centered around "from" time the size
                        of RANGE (overrides -t)
  -l: last LINES      - stripchart last number of LINES in data file
                        (overrides -f and -t and -r)
  -T: title TITLE     - title to put on graphic (default: FILE RANGE)
  -x: column X        - time or "x" column (default: 2)
  -y: column Y        - value or "y" column (default: 3)
  -Y: column Y'       - overplot second "y" column (default: none)
  -b: baseline VALUE  - overplot baseline of arbitrary value VALUE
  -B: baseline-avg    - overrides -b, it plots baseline of computed average
  -d: dump low VALUE  - ignore data less than VALUE
  -D: dump high VALUE - ignore data higher than VALUE
  -v: verbose         - puts verbose runtime output to STDERR
  -L: log             - makes y axis log scale
  -c: colors "COLORS" - set gnuplot colors for graph/axis/fonts/data (default:
                        "$colorvals"
                        in order: bground, axis/fonts, grids, pointcolor1,2,3)
  -C: cgi             - output CGI header to STDOUT if being called as CGI
  -s: stats           - turn extra plot stats on (current, avg, min, max)
  -j: julian times    - time columns is in local julian date (legacy stuff)
  -V: version         - print version number and exit
  -h: help            - display this help

notes:
  * TIME either unix date, julian date, or civil date in the form:
      YYYY:MM:DD:HH:MM (year, month, day, hour, minute)
    If you enter something with colons, it assumes it is civil date
    If you have a decimal point, it assumes it is julian date
    If it is an integer, it assumes it is unix date (epoch seconds)
    If it is a negative number, it is in decimal days from current time
      (i.e. -2.5 = two and a half days ago)
    * All times on command line are assumed to be "local" times
    * All times in the data file must be in unix date (epoch seconds)
  * RANGE is given in decimal days (i.e. 1.25 = 1 day, 6 hours)
  * if LINES == 0, (i.e. -l 0) then the whole data file is read in
  * columns (given with -x, -y, -Y flags) start at 1
  * titles given with -T can contain the following key words which will
    be converted:
      FILE - basename of input file
      RANGE - pretty civil date range (in local time zone)
    the default title is: FILE RANGE
EOF
  exit 1
  }

if ($opt_V) {
  print STDERR "Stripchart version $majorversion.$minorversion by Matt Lebofsky\n";
  exit 1
  }

if ($opt_C && $opt_v) {
  print STDERR "you cannot have -v and -C set at the same time..\n"; exit 1 }

if ($opt_i) { $infile = $opt_i }
else { print STDERR "need to set input file via -i. exiting..\n"; exit 1 }

if ($opt_f) {
  if ($opt_f < 0) { $fromtime = to_unix_time($rightnow) + ($daysecs * $opt_f) }
  else { $fromtime = to_unix_time($opt_f) }
  }
else { $fromtime = to_unix_time($rightnow) - $daysecs }

if ($opt_t) { $totime = to_unix_time($opt_t) }
else { $totime = to_unix_time($rightnow) }

if ($opt_v) { print STDERR "selected time range: $fromtime - $totime\n" }

if ($opt_r) {
  $totime = $fromtime + ($opt_r * $daysecs);
  $fromtime -= ($opt_r * $daysecs)
  }

# if -l is set, override from/to with close-to-impossible all-inclusive range
if (defined($opt_l)) { $fromtime = 0; $totime = 2147483647 }

if ($opt_x) { $timecol = $opt_x } else { $timecol = 2 }
if ($opt_y) { $datacol = $opt_y } else { $datacol = 3 }
if ($opt_Y) { $extracol = $opt_Y }

if ($opt_c) { $colorvals = $opt_c }

# read in file

if ($opt_v) { print STDERR "reading in data from $infile..\n" }

if ($opt_l > 0) {
  $numlines = $opt_l;
  @stripdata = `$tailexe -$numlines $infile` or die "cannot open file: $infile";
  }
else {
  @stripdata = `$catexe $infile` or die "cannot open file: $infile";
  $numlines = @stripdata;
  }

if ($opt_v) { print STDERR "read in $numlines lines..\n" }

# make gnuplot data file

if ($opt_v) { print STDERR "making temp data file for gnuplot..\n" }

open (GNUDATA,">$gnudata") or die "cannot write temp data file: $gnudata";

$statcurrent = 0;
$statmax = "x";
$statmin = "x";
$stattotal = 0;

$checkfromtime = $fromtime;
if ($opt_j) { $checkfromtime = (($fromtime + $tzdiff) / $daysecs ) + 2440587.5 }

$thisextra = 0;
$thisbaseline = 0;
if ($opt_b) { $thisbaseline = $opt_b }
$numlines = 0;
$firstunixdate = -1;
foreach $dataline (@stripdata) {
  chomp $dataline;
  @dataarray = split /\s/, $dataline;
  $thistime = $dataarray[$timecol-1];
  if ($thistime < $checkfromtime) { next }
  if ($opt_j) { $thistime = int(($dataarray[$timecol-1] - 2440587.5) * $daysecs) - $tzdiff }
  $thisdata = $dataarray[$datacol-1];
  if ($thistime <= $totime) {
    if ($thistime !~ /[a-zA-Z]/ && $thisdata !~ /[a-zA-Z]/ &&  # ignore junky numbers
        $thisdata ne "" ) {
      if ((defined($opt_d) && ($thisdata >= $opt_d)) || !defined($opt_d)) {
        if ((defined($opt_D) && ($thisdata <= $opt_D)) || !defined($opt_D)) {
          $numlines++;
          if ($firstunixdate == -1) { $firstunixdate = $thistime }
          $lastunixdate = $thistime;
          if ($opt_Y) { $thisextra = $dataarray[$extracol-1]; }
          if ($opt_s) {
            $statcurrent = $thisdata;
            $stattotal += $thisdata;
            if ($statmax eq "x" || $thisdata > $statmax) { $statmax = $thisdata }
            if ($statmin eq "x" || $thisdata < $statmin) { $statmin = $thisdata }
            }
          print GNUDATA "$thistime $thisdata $thisextra $thisbaseline\n"
          }
        }
      }
    }
  }
close (GNUDATA);

$statavg = ($stattotal / $numlines);

if ($opt_B) {
  @tmpdata = `$catexe $gnudata`;
  open (GNUDATA,">$gnudata") or die "cannot write temp data file: $gnudata";
  foreach $tmpline (@tmpdata) {
    chomp $tmpline;
    ($tempone,$temptwo,$tempthree,$tempfour) = (split /\s/, $tmpline);
    $tempfour = $statavg;
    print GNUDATA "$tempone $temptwo $tempthree $tempfour\n";
    }
  close (GNUDATA);
  }

if ($numlines == 0) {
  print STDERR "no data selected due to user constraints.\n";
  exit 1
  }

if ($opt_v) { print STDERR "$numlines data points within time range..\n" }

$begin = $firstunixdate; $end = $lastunixdate;
if (!defined($opt_l)) { $begin = $fromtime; $end = $totime }

if ($opt_v) { print STDERR "actual time range being plotted: $begin - $end\n" }

# make gnuplot xtics file

$daydiff = $end - $begin;

if ($opt_v) { print STDERR "making xtics file..\n" }

# depending on the range, pick appropriate tic steps and label steps

$labelstep = 7200;
$ticstep = 3600;

# less than 10 minutes: label every minute, extra tics every 30 seconds
if ($daydiff <= 600) { $labelstep = 60; $ticstep = 30 }
# less than 50 minutes: label every 5 minutes, extra tics every minute
elsif ($daydiff <= 3000) { $labelstep = 300; $ticstep = 60 }
# less than 100 minutes: label every 10 minutes, extra tics every 5 minutes
elsif ($daydiff <= 6000) { $labelstep = 600; $ticstep = 300 }
# less than 200 minutes: label every 20 minutes, extra tics every 10 minutes
elsif ($daydiff <= 12000) { $labelstep = 1200; $ticstep = 600 }

# less than 5 hours: label every 30 minutes, extra tics every 15 minutes
elsif ($daydiff <= 18000) { $labelstep = 1800; $ticstep = 900 }
# less than 10 hours: label every hour, extra tics every 30 minutes
elsif ($daydiff <= 36000) { $labelstep = 3600; $ticstep = 1800 }
# less than 20 hours: label every 2 hours, extra tics every hour
elsif ($daydiff <= 72000) { $labelstep = 7200; $ticstep = 3600 }
# less than 40 hours: label every 4 hours, extra tics every hour
elsif ($daydiff <= 144000) { $labelstep = 14400 ; $ticstep = 3600 }
# less than 60 hours: label every 6 hours, extra tics every hour
elsif ($daydiff <= 216000) { $labelstep = 21600; $ticstep = 3600 }
# less than 120 hours: label every 12 hours, extra tics every 6 hours
elsif ($daydiff <= 432000) { $labelstep = 43200; $ticstep = 21600 }

# less than 10 days: label every day, extra tics every half day
elsif ($daydiff <= 864000) { $labelstep = 86400; $ticstep = 43200 }
# less than 20 days: label every 2 days, extra tics every day
elsif ($daydiff <= 1728000) { $labelstep = 172800; $ticstep = 86400 }
# less than 40 days: label every 4 days, extra tics every 2 days
elsif ($daydiff <= 3456000) { $labelstep = 345600; $ticstep = 172800 }
# less than 70 days: label every 7 days, extra tics every 7 days
elsif ($daydiff <= 6048000) { $labelstep = 604800; $ticstep = 604800 }
# less than 140 days: label every 14 days, extra tics every 7 days
elsif ($daydiff <= 12096000) { $labelstep = 1209600; $ticstep = 604800 }
# less than 280 days: label every 28 days, extra tics every 14 days
elsif ($daydiff <= 24192000) { $labelstep = 2419200; $ticstep = 1209600 }
# less than 600 days: label every 60 days, extra tics every 30 days
elsif ($daydiff <= 51840000) { $labelstep = 5184000; $ticstep = 2592000 }
# less than 1000 days: label every 100 days, extra tics every 50 days
elsif ($daydiff <= 86400000) { $labelstep = 8640000; $ticstep = 4320000 }
# okay, you're on your own now: every 200 days, extra tics every 100 days
else { $labelstep = 17280000; $ticstep = 8640000 }

if ($opt_v) {
  print STDERR "x label resolution: $labelstep seconds..\n";
  print STDERR "x tic resolution: $ticstep seconds..\n" }

open (TICDATA,">$ticdata") or die "cannot write temp tic file: $ticdata";
print TICDATA "set xtics( \\\n";

if ($daydiff >= 18000 && $daydiff <= 216000) {
  # round down/up to nearest hour
  $firstunixdate = int($firstunixdate/3600) * 3600;
  $lastunixdate = (int($lastunixdate/3600)+1) * 3600;
  }
elsif ($daydiff >= 216000) {
  # round down/up to nearest day
  $firstunixdate = ((int($firstunixdate/86400)) * 86400) - $tzdiff;
  $lastunixdate = ((int($lastunixdate/86400)+1) * 86400) - $tzdiff;
  }

for ($thisdate = $firstunixdate; $thisdate < $lastunixdate; $thisdate += $ticstep) {
  $label = '""';
  if (($thisdate - $firstunixdate) % $labelstep == 0) {
    if ($daydiff <= 432000) {
      $label = '"' . time_to_hourlabel($thisdate) . '"'
      }
    else {
      $label = '"' . time_to_monthlabel($thisdate) . '"'
      }
    }
  print TICDATA $label . " " . $thisdate;
  if (($thisdate + $ticstep) < $lastunixdate) { print TICDATA "," }
  print TICDATA " \\\n";
  }
print TICDATA ")\n";
close (TICDATA);

# make gnuplot file and run it!

if ($opt_v) { print STDERR "running gnuplot to create $opt_o..\n" }

if (defined($opt_l)) { # fix for title
  $fromtime = $begin;
  $totime = $end;
  }

if ($opt_T) { $title = $opt_T } else { $title = "FILE RANGE" }
$thisfile = basename($infile);
$thisrange = "(" . unix_to_civillabel($fromtime) . " -> " .
             unix_to_civillabel($totime) . ")";
$title =~ s/FILE/$thisfile/g;
$title =~ s/RANGE/$thisrange/g;

if ($opt_s) {
  $plotheight += 12;
  $statlabel = sprintf("(cur: %.2f  max: %.2f  min: %.2f  avg: %.2f)",$statcurrent,$statmax,$statmin,$statavg);
  $title .= "\\n$statlabel";
  }

open (GNUSCRIPT,">$gnuscript") or die "cannot open gnuscript file: $gnuscript";
print GNUSCRIPT "set terminal gif small size $plotwidth, $plotheight \\\n";
print GNUSCRIPT "$colorvals\n";
print GNUSCRIPT "set nokey\nset grid\nset title \"$title\"\n";
if ($opt_L) { print GNUSCRIPT "set logscale y\n" }
print GNUSCRIPT "set pointsize 0.1\n";
print GNUSCRIPT "plot \"$gnudata\" using 1:2 with impulses";
if ($opt_Y) {
  print GNUSCRIPT ", \"$gnudata\" using 1:3 with lines"
  }
if ($opt_b || $opt_B) {
  print GNUSCRIPT ", \"$gnudata\" using 1:4 with lines"
  }
print GNUSCRIPT "\n";
close (GNUSCRIPT);

if ($opt_C) {
  print "Content-type: image/gif\n\n";
  }

if ($opt_o) { `$gnuplot/gnuplot $ticdata $gnuscript > $opt_o` }
else {
  `$gnuplot/gnuplot $ticdata $gnuscript > $outfile`;
  open (OUTFILE,"$outfile");
  while ($bytesread=read(OUTFILE,$buffer,1024)) { print $buffer }
  close (OUTFILE);
  if ($opt_O) { `$gnuplot/gnuplot $ticdata $gnuscript > $opt_O` }
  }

# the end. clean up.

if ($opt_v) { print "done. cleaning up..\n" }

unlink ($outfile);
unlink ($ticdata);
unlink ($gnudata);
unlink ($gnuscript);
exit 0;
