<?php
/**
 * OAR Drawgantt-SVG
 * @author Pierre Neyron <pierre.neyron@imag.fr>
 *
 */

////////////////////////////////////////////////////////////////////////////////
// DO NOT EDIT CONFIGURATION HERE 
// Customization must go either in:
// - %%OARCONFDIR%%/drawgantt-config.inc.php
// or in:
// - %%OARCONFDIR%%/drawgantt-config-SUFFIX.inc.php with SUFFIX being either:
//   - set in a "config=SUFFIX" query param. SUFFIX must be made of alphanumeric
//     or _ characters only
//   or
//   - guessed from the script path (latest word in the dirpath). The directory
//     name must use alphanumeric or _ characters only, and the resulting
//     configuration file must exist.
//   (mostly useful for a multi-drawgantt setup)
////////////////////////////////////////////////////////////////////////////////

$CONF=array();
// Database access configuration
$CONF['db_type']="pg"; // choices: mysql for Mysql or pg for PostgreSQL
$CONF['db_server']="127.0.0.1";
$CONF['db_port']="5432"; // usually 3306 for Mysql or 5432 for PostgreSQL
$CONF['db_name']="oar"; // OAR read only user account
$CONF['db_user']="oar_ro";
$CONF['db_passwd']="oar_ro";
$CONF['db_max_job_rows']=20000; // max number of job rows retrieved from database, which can be handled.

// Data display configuration
$CONF['timezone'] = "UTC";
$CONF['site'] = "My OAR resources"; // name for your infrastructure or site
$CONF['resource_labels'] = array('network_address','cpuset'); // properties to describe resources (labels on the left). Must also be part of resource_hierarchy below
$CONF['cpuset_label_display_string'] = "%02d";
$CONF['label_display_regex'] = array( // shortening regex for labels (e.g. to shorten node-1.mycluster to node-1). label will be replaced with the 1st selection of the regex, unless an array is used with the replacement string as the second argument
  'network_address' => '/^([^.]+)\..*$/',
  'type' => array('/^default$/','proc'),
);
$CONF['label_display_functions'] = array( // custom functions to format labels for display. The function takes the resource object as argument. This object returns the value for each property of the resource in $obj->data[property]
);
$CONF['label_cmp_regex'] = array( // substring selection regex for comparing and sorting labels (resources)
  'network_address' => '/^([^-]+)-(\d+)\..*$/',
);
$CONF['label_cmp_functions'] = array( // custom functions to format labels for comparison. The function takes the resource object as argument. This object returns the value for each property of the resource in $obj->data[property]
);
$CONF['resource_properties'] = array( // properties to display in the pop-up on top of the resources labels (on the left)
  'deploy', 'cpuset', 'besteffort', 'network_address', 'type', 'drain');
$CONF['resource_hierarchy'] = array( // properties to use to build the resource hierarchy drawing
  'network_address','cpuset',
);
$CONF['resource_base'] = "cpuset"; // base resource of the hierarchy/grid
$CONF['resource_group_level'] = "network_address"; // level of resources to separate with blue lines in the grid
$CONF['resource_drain_property'] = "drain"; // if set, must also be one of the resource_properties above to activate the functionnality
$CONF['state_colors'] = array( // colors for the states of the resources in the gantt
  'Absent' => 'url(#absentPattern)', 'Suspected' => 'url(#suspectedPattern)', 'Dead' => 'url(#deadPattern)', 'Standby' => 'url(#standbyPattern)', 'Drain' => 'url(#drainPattern)');
$CONF['job_colors'] = array( // colors for the types of the jobs in the gantt
  'besteffort' => 'url(#besteffortPattern)',
  'deploy(=\w)?' => 'url(#deployPattern)',
  'container(=\w+)?' => 'url(#containerPattern)',
  'timesharing=(\*|user),(\*|name)' => 'url(#timesharingPattern)',
  'placeholder=\w+' => 'url(#placeholderPattern)',
);
$CONF['job_click_url'] = ''; // set a URL to open when a job is double-clicked, %%JOBID%% is to be replaced by the jobid in the URL
$CONF['resource_click_url'] = ''; // set a URL to open when a resource is double-clicked, %%TYPE%% is to be replaced by the resource type and %%ID%% by the resource id in the URL

// Geometry customization
$CONF['hierarchy_resource_width'] = 10; // default: 10
$CONF['scale'] = 10; // default: 10
$CONF['text_scale'] = 10; // default: 10
$CONF['time_ruler_scale'] = 6; // default: 6
$CONF['time_ruler_steps'] = array(60,120,180,300,600,1200,1800,3600,7200,10800,21600,28800,43200,86400,172800,259200,604800);
$CONF['gantt_top'] = 50; // default: 50
$CONF['bottom_margin'] = 45; // default: 45
$CONF['right_margin'] = 30; // default 30
$CONF['label_right_align'] = 105; // default: 105
$CONF['hierarchy_left_align'] = 110; // default: 110
$CONF['gantt_left_align'] = 160; // default: 160
$CONF['gantt_min_width'] = 900; // default: 900
$CONF['gantt_min_height'] = 100; // default: 100
$CONF['gantt_min_job_width_for_label'] = 40; // default: 40
$CONF['min_state_duration'] = 2; // default: 2

// Colors and fill patterns for jobs and states
$CONF['job_color_saturation_lightness'] = "75%,75%"; // default: "75%,75%"
$CONF['job_color_saturation_lightness_highlight'] = "50%,50%"; // default: "50%,50%"
$CONF['static_patterns'] = <<<EOT
<pattern id="absentPattern" patternUnits="userSpaceOnUse" x="0" y="0" width="10" height="10" viewBox="0 0 10 10" >
<polygon points="0,0 3,0 0,3" fill="#0000ff" stroke="#0000ff" stroke-width="1" />
<polygon points="7,0 10,0 10,3 3,10 0,10 0,7" fill="#0000ff" stroke="#0000ff" stroke-width="1" />
<polygon points="10,7 10,10 7,10" fill="#0000ff" stroke="#0000ff" stroke-width="1" />
</pattern>
<pattern id="suspectedPattern" patternUnits="userSpaceOnUse" x="0" y="0" width="10" height="10" viewBox="0 0 10 10" >
<polygon points="0,0 3,0 0,3" fill="#ff0000" stroke="#ff0000" stroke-width="1" />
<polygon points="7,0 10,0 10,3 3,10 0,10 0,7" fill="#ff0000" stroke="#ff0000" stroke-width="1" />
<polygon points="10,7 10,10 7,10" fill="#ff0000" stroke="#ff0000" stroke-width="1" />
</pattern>
<pattern id="deadPattern" patternUnits="userSpaceOnUse" x="0" y="0" width="10" height="10" viewBox="0 0 10 10" >
<polygon points="0,0 3,0 0,3" fill="#404040" stroke="#404040" stroke-width="1" />
<polygon points="7,0 10,0 10,3 3,10 0,10 0,7" fill="#404040" stroke="#404040" stroke-width="1" />
<polygon points="10,7 10,10 7,10" fill="#404040" stroke="#404040" stroke-width="1" />
</pattern>
<pattern id="standbyPattern" patternUnits="userSpaceOnUse" x="0" y="0" width="10" height="10" viewBox="0 0 10 10" >
<polygon points="0,0 3,0 0,3" fill="#88ffff" stroke="#88ffff" stroke-width="1" />
<polygon points="7,0 10,0 10,3 3,10 0,10 0,7" fill="#88ffff" stroke="#88ffff" stroke-width="1" />
<polygon points="10,7 10,10 7,10" fill="#88ffff" stroke="#88ffff" stroke-width="1" />
</pattern>
<pattern id="drainPattern" patternUnits="userSpaceOnUse" x="0" y="0" width="15" height="10" viewBox="0 0 10 10" >
<circle cx="5" cy="5" r="4" fill="#ff0000" stroke="#ff0000" stroke-width="1" />
<line x1="2" y1="5" x2="9" y2="5" stroke="#ffffff" stroke-width="2" />
</pattern>
<pattern id="containerPattern" patternUnits="userSpaceOnUse" x="0" y="0" width="20" height="20" viewBox="0 0 20 20" >
<text font-size="10" x="0" y="20" fill="#888888">C</text>
</pattern>
<pattern id="besteffortPattern" patternUnits="userSpaceOnUse" x="0" y="0" width="20" height="20" viewBox="0 0 20 20" >
<text font-size="10" x="10" y="20" fill="#888888">B</text>
</pattern>
<pattern id="placeholderPattern" patternUnits="userSpaceOnUse" x="0" y="0" width="20" height="20" viewBox="0 0 20 20" >
<text font-size="10" x="10" y="20" fill="#888888">P</text>
</pattern>
<pattern id="deployPattern" patternUnits="userSpaceOnUse" x="0" y="0" width="20" height="20" viewBox="0 0 20 20" >
<text font-size="10" x="10" y="10" fill="#888888">D</text>
</pattern>
<pattern id="timesharingPattern" patternUnits="userSpaceOnUse" x="0" y="0" width="20" height="20" viewBox="0 0 20 20" >
<text font-size="10" x="10" y="20" fill="#888888">T</text>
</pattern>
EOT;

// Standby state display options for the part shown in the future
$CONF['standby_truncate_state_to_now'] = 1; // default: 1
// Besteffort job display options for the part shown in the future
$CONF['besteffort_truncate_job_to_now'] = 1; // default: 1
$CONF['besteffort_pattern'] = <<<EOT
<pattern id="%%PATTERN_ID%%" patternUnits="userSpaceOnUse" x="0" y="0" width="10" height="10" viewBox="0 0 10 10" >
<polygon points="0,0 7,0 10,5 7,10 0,10 3,5" fill="%%PATTERN_COLOR%%" stroke-width="0"/>
</pattern>'
EOT;

// Advanced customization for the computation of the colors of the jobs
// Uncomment and adapt the following to override the default function
//class MyShuffle extends Shuffle {
//    // Default function: get the color's hue value as a function of the job_id
//    function job2int($job) {
//        // compute a suffled number for job_id, so that colors are not too close
//        $magic_number = (1+sqrt(5))/2;
//        return (int)(360 * fmod($job->job_id * $magic_number, 1));
//    }
//    // Other example: get the color's hue value as a function of the job_user value
//    protected $cache = array();
//    function job2int($job) {
//        // shuffled number based on the job_user:
//        if (! array_key_exists($job->job_user, $this->cache)) {
//            $n = (int) base_convert(substr(md5($job->job_user) ,0, 5), 16, 10);
//            $magic_number = (1+sqrt(5))/2;
//            $this->cache[$job->job_user] = (int)(360 * fmod($n * $magic_number, 1));
//        }
//        return $this->cache[$job->job_user];
//    }
//}
//Shuffle::init(new MyShuffle()); // this line must be uncommented for the overiding to take effect

// Minimum timespan the gantt can handle
$CONF['min_timespan']= 480; // gantt does not show if (stop date - start date) < 8 minutes

// Debugging
$CONF['debug'] = 0; // Set to > 0 to enable debug prints in the web server error logs

////////////////////////////////////////////////////////////////////////////////
// End of the embedded configuration settings
////////////////////////////////////////////////////////////////////////////////

////////////////////////////////////////////////////////////////////////////////
//
// This php script take the following parameter as argument in URL (GET):
// - display: all | no_ruler | no_mobile_ruler | ruler_only, as option the the rulers display
// - config: <suffix>, to add a suffix to config file (drawgantt-config.inc.php.[suffix]) and allow multiple configuration for a same installation
// - timezome: UTC | ..., as the timezone to use
// - start: <ctime>, start date for the gantt
// - stop: <ctime>, stop date for the gantt
// - relative_start: <int>, start date relative to now for the gantt
// - relative_stop: <int>, stop date relative to now for the gantt
// - filter: <SQL>, SQL where clause for the OAR resource selection
// - resource_base: <resource>, base resource to use in hierarchy
// - width: <int>: gantt width in pixel
// - scale: <int>: scale for the gantt in pixel
//
// The generated SVG page can be used as a widget in a parent html document. API for that widget is the following:
// - use an <object type = "image/svg+xml"> HTML element and set both the data and innerHTML attributes to the drawgantt-svg.php URL.
// - the caller page can provide the following for interaction with the SVG document:
//   - inboxbox element and infoboxDisplay(array of lines), infoboxHide(), infoboxMove(x,y) callback function to use an infobox in the parent document
//   - setZoomWindow(now, zoom1, zoom2) callback function to use the mouse selection zone (zoom)
//   - highlightOthers(object_ref, element class, boolean) callback function to propagate highlight to other gantts in the parent document
// - also, the parent document must set a reference to the object element that contains the SVG document:
//   - the parent script: ganttObjectElement.contentDocument.object_ref = ganttObjectElement
//
////////////////////////////////////////////////////////////////////////////////

// Manage the configuration file
define('CONFIG_DIR', '%%OARCONFDIR%%');
$config_file_suffix = '';
// Look for a config file suffix in the query params
if (array_key_exists('config',$_GET) and preg_match('/^\w+$/', $_GET['config'])) {
  $config_file_suffix = $_GET['config'];
  error_log('drawgantt-svg: config suffix found in query params: '.$config_file_suffix);
// Look for a config file suffix in the script path
} elseif (preg_match('/^.*\W(\w+)\/.*$/',$_SERVER['SCRIPT_NAME'],$match) and is_file(CONFIG_DIR . '/drawgantt-config-'.$match[1].'.inc.php')) {
  $config_file_suffix = $match[1];
  error_log('drawgantt-svg: config suffix guessed from URL: '.$config_file_suffix);
}
$config_file = CONFIG_DIR . '/drawgantt-config'.($config_file_suffix?'-'.$config_file_suffix:'').'.inc.php';
if (is_readable($config_file)) {
  error_log('drawgantt-svg: use config file '.$config_file);
  require $config_file;
} else {
  error_log('drawgantt-svg: config file not readable: '.$config_file.', use default values.');
}

////////////////////////////////////////////////////////////////////////////////
// Script parameters (GET)
////////////////////////////////////////////////////////////////////////////////
date_default_timezone_set(array_key_exists('timezone',$_GET)?$_GET['timezone']:$CONF['timezone']);
$display = array_key_exists('display',$_GET)?$_GET['display']:'all';
$gantt_start_date = array_key_exists('start',$_GET)?round($_GET['start']):0;
$gantt_stop_date = array_key_exists('stop',$_GET)?round($_GET['stop']):0;
$gantt_relative_start_date = array_key_exists('relative_start',$_GET)?round($_GET['relative_start']):-86400;
$gantt_relative_stop_date = array_key_exists('relative_stop',$_GET)?round($_GET['relative_stop']):86400;
$resource_filter = array_key_exists('filter', $_GET)?$_GET['filter']:"";
$resource_base = array_key_exists('resource_base', $_GET)?$_GET['resource_base']:$CONF['resource_base'];
if (! in_array($resource_base, $CONF['resource_hierarchy'])) {
  debug(1, "resource_base: $resource_base does not belong to CONF['resource_hierarchy']=".join(",",$CONF['resource_hierarchy']));
  $resource_base = end($CONF['resource_hierarchy']);
  debug(1, "set resource_base to $resource_base");
}
$resource_labels = (empty($CONF['resource_labels']))?array($resource_base):$CONF['resource_labels'];
$gantt_left_align = $CONF['hierarchy_left_align'] + (3 + array_search($resource_base,$CONF['resource_hierarchy'])) * $CONF['hierarchy_resource_width'];
$gantt_width = $CONF['gantt_min_width'];
if (array_key_exists('width', $_GET) and $_GET['width'] > ($gantt_left_align + $CONF['gantt_min_width'] + $CONF['right_margin'])) {
  $gantt_width = $_GET['width'] - $gantt_left_align - $CONF['right_margin'];
}
$scale = array_key_exists('scale', $_GET)?$_GET['scale']:$CONF['scale'];
$patterns = array();

////////////////////////////////////////////////////////////////////////////////
// Utility functions and database wrapper class
////////////////////////////////////////////////////////////////////////////////

// print debug info to the web server error logs
function debug($d, $str) {
  global $CONF, $display;
  if ($display != "mobule_ruler_only" and array_key_exists('debug', $CONF) and $CONF['debug'] >= $d) {
    error_log($str);
  }
}

// conversion function from date to pixel coordinates in the gantt
function date2px($date) {
  global $CONF, $gantt_width, $gantt_left_align, $gantt_start_date, $gantt_stop_date, $gantt_now;
  if ($date < $gantt_start_date) {
    return $gantt_left_align;
  }
  if ($date > $gantt_stop_date) {
    return $gantt_left_align +  $gantt_width;
  }
  return round($gantt_left_align + ($gantt_width * ($date - $gantt_start_date)) / ($gantt_stop_date - $gantt_start_date));
}

// class to compute colors for jobs
class Shuffle {
    protected static $singleton;
    function init($shuffle_instance) {
        self::$singleton = $shuffle_instance;
    }
    function get_int($job) {
        if (self::$singleton === null) {
            self::init(new Shuffle());
        }
        return self::$singleton->job2int($job);
    }
    function job2int($job) {
        // compute a suffled number for job_id, so that colors are not too close
        $magic_number = (1+sqrt(5))/2;
        return (int)(360 * fmod($job->job_id * $magic_number, 1));
    }
}

// database functions
class Db {
  function __construct($backend, $server, $port, $dbname, $user, $password) {
    $this->backend = $backend;
    if ($this->backend == "mysql") {
      $this->handler = mysql_connect("$server:$port", $user, $password) or die('Cound not connect Mysql server: ' . mysql_error());
      mysql_select_db($dbname) or die('Could not select database');
    } elseif ($this->backend == "pg") {
      $this->handler = pg_connect("host=$server port=$port dbname=$dbname user=$user password=$password") or die('Cound not connect PostgreSQL server');
    } else {
      die ("Unknown database backend: $backend (must be 'mysql' or 'pg'");
    }
  }

  function query($query) {
    debug(3, $this->backend . " query: " . $query);
    $t0 = microtime(true);
    if ($this->backend == "mysql") {
      $this->result = mysql_query($query, $this->handler) or die('Query failed: ' . mysql_error());
      $this->num_rows = mysql_num_rows($this->result);
    } elseif ($this->backend == "pg") {
      $this->result = pg_query_params($this->handler, $query, array()) or die('Query failed');
      $this->num_rows = pg_num_rows($this->result);
    } else {
      die ("Unknown database backend: " . $this-> backend . " (must be 'mysql' or 'pg'");
    }
    debug(2, "-> " . $this->num_rows . " rows (" . (microtime(true) - $t0) . "s)");
  }

  function fetch_array() {
    if ($this->backend == "mysql") {
      return mysql_fetch_array($this->result, MYSQL_ASSOC);
    } elseif ($this->backend == "pg") {
      return pg_fetch_array($this->result, NULL, PGSQL_ASSOC);
    } else {
      die ("Unknown database backend: " . $this->backend . " (must be 'mysql' or 'pg'");
    }
  }

  function free() {
    if ($this->backend == "mysql") {
      return mysql_free_result($this->result);
    } elseif ($this->backend == "pg") {
      return pg_free_result($this->result);
    } else {
      die ("Unknown database backend: " . $this->backend . " (must be 'mysql' or 'pg'");
    }
  }

  function close() {
    if ($this->backend == "mysql") {
      return mysql_close($this->handler);
    } elseif ($this->backend == "pg") {
      return pg_close($this->handler);
    } else {
      die ("Unknown database backend: " . $this->backend . " (must be 'mysql' or 'pg'");
    }
  }

  function get_timestamp() {
    if ($this->backend == "mysql") {
      $this->query('SELECT UNIX_TIMESTAMP()');
      $array = mysql_fetch_array($this->result, MYSQL_NUM);
    } elseif ($this->backend == "pg") {
      $this->query('SELECT EXTRACT(EPOCH FROM current_timestamp)');
      $array = pg_fetch_array($this->result, NULL, PGSQL_NUM);
    } else {
      die ("Unknown database backend: " . $this->backend . " (must be 'mysql' or 'pg'");
    }
    $this->free();
    return (int)$array[0];
  }
  function cast($var,$type) {
    if ($this->backend == "mysql") {
      return $var;
    } elseif ($this->backend == "pg") {
      return $var."::".$type;
    } else {
      die ("Unknown database backend: " . $this->backend . " (must be 'mysql' or 'pg'");
    }
  }
}

////////////////////////////////////////////////////////////////////////////////
// Some classes to handle data
////////////////////////////////////////////////////////////////////////////////

// Storage class for State
class State {
  public $value, $start, $stop, $real_start, $real_stop;
  function __construct($value, $start, $stop) {
    global $gantt_start_date, $gantt_stop_date;
    $this->value = $value;
    $this->set_start($start);
    $this->set_stop($stop);
  }
  function set_start($start) {
    global $gantt_start_date;
    $this->real_start = $start;
    $this->start = ($start < $gantt_start_date)?$gantt_start_date:$start;
  }
  function set_stop($stop) {
    global $gantt_stop_date;
    $this->real_stop = $stop;
    $this->stop = (($stop == 0) or ($stop > $gantt_stop_date))?$gantt_stop_date:$stop;
  }
}

// Storage class for jobs
class Job {
  public $job_id,$job_name,$project,$job_type,$state,$job_user,$command,$queue_name,$moldable_walltime,$properties,$launching_directory,$submission_time,$start_time,$stop_time,$resource_ids,$network_addresses,$types;
  protected $color, $colorHL;
  function __construct($job_id,$job_name,$project,$job_type,$state,$job_user,$command,$queue_name,$moldable_walltime,$properties,$launching_directory,$submission_time,$start_time,$stop_time) {
    $this->job_id = $job_id;
    $this->job_name = $job_name;
    $this->project = $project;
    $this->job_type = $job_type;
    $this->state = $state;
    $this->job_user = $job_user;
    $this->command = $command;
    $this->queue_name = $queue_name;
    $this->moldable_walltime = $moldable_walltime;
    $this->properties = $properties;
    $this->launching_directory = $launching_directory;
    $this->submission_time = $submission_time;
    $this->start_time = $start_time;
    $this->stop_time = $stop_time;
    $this->resource_ids = array();
    $this->network_addresses = array();
    $this->types = array();
    $this->color = NULL;
    $this->colorHL = NULL;
  }

  function add_resource_id($resource_id) {
    $this->resource_ids[$resource_id->id] = $resource_id;
  }

  function add_type($type) {
    if (! in_array($type, $this->types)) {
      array_push($this->types, $type);
    }
  }
  function add_network_address($network_address) {
    if ($network_address != '' and ! in_array($network_address, $this->network_addresses)) {
      array_push($this->network_addresses, $network_address);
    }
  }
  // build groups of continuous resource ids
  function group_resources($resources) {
    $grp = NULL;
    foreach ($resources as $r) {
      $weight=0;
      foreach($r->resource_ids as $rid) {
        if (array_key_exists($rid->id, $this->resource_ids)) {
          $weight++;
        }
      }
      if ($weight > 0) {
        $r->add_job_occupation($this, $weight < count($r->resource_ids));
        if ($grp == NULL) {
          debug(3, "new job2resource group for ".$r->id);
          $grp = new Job2ResourceGroup($this, $r);
          $r->add_job2resource_group($grp);
        } else {
          debug(4, "add to group ".$r->id);
          $grp->add_resource($r);
        }
      } else {
        $grp = NULL;
      }
    }
  }
  function svg_text() {
    $output = "Jobid: {$this->job_id}";
    $output .= "|User: ".htmlspecialchars($this->job_user);
    $output .= "|Kind: {$this->job_type}";
    $output .= "|Queue: ".htmlspecialchars($this->queue_name);
    $output .= "|Types: ".join(", ", array_map("htmlspecialchars", $this->types));
    $output .= "|Name: ".htmlspecialchars($this->job_name);
    $output .= "|Project: ".htmlspecialchars($this->project);
    $output .= "|Walltime: " . $this->moldable_walltime/3600 . "hr";
    $output .= "|Resources: ".count($this->resource_ids);
    $output .= "|Machines: ".count($this->network_addresses);
    $output .= "|Submission: ".date("r", $this->submission_time);
    $output .= "|Start: ".date("r", $this->start_time);
    $output .= "|Stop: ".(($this->stop_time > 0)?(date("r", $this->stop_time)):(date("r", $this->start_time + $this->moldable_walltime)));
    $output .= "|State: {$this->state}";
    //$output .= "|Properties: ".htmlspecialchars($this->properties);
    return $output;
  }
  function color($hl) {
    global $CONF;
    if ($this->color == NULL or $this->colorHL == NULL) {
      $hue = Shuffle::get_int($this);
      $this->color = "hsl(".$hue.",{$CONF['job_color_saturation_lightness']})";
      $this->colorHL = "hsl(".$hue.",{$CONF['job_color_saturation_lightness_highlight']})";
    }
    return (($hl)?$this->colorHL:$this->color);
  }
}

// Container class for job to resource groupping: since a job may not have continuous resources, it display can be split in serveral groups (rectangles)
class Job2ResourceGroup {
  public $job, $resources;
  function __construct($job, $r) {
    $this->job = $job;
    $this->resources = array($r);
  }
  function add_resource($r) {
    array_push($this->resources, $r);
  }
  function size() {
    return count($this->resources);
  }
}

// Storage class for the resource_ids
class ResourceId {
  public $id, $cpuset, $states, $resources, $properties, $events;
  function __construct($id, $cpuset) {
    $this->id = $id;
    $this->cpuset = $cpuset;
    $this->states = array();
    $this->resources = array();
    $this->properties = array();
    $this->events = array();
  }
  function add_state($value, $start, $stop) {
    array_push($this->states, new State($value, $start, $stop));
  }
  function add_resource($resource) {
    $this->resources[$resource->type] = $resource;
  }
  function add_property($key, $value) {
    $this->properties[$key] = $value;
    ksort($this->properties);
  }
  function add_event($date, $key, $value) {
    if (array_key_exists($date, $this->events)) {
      array_push($this->events[$date], "$key=$value");
    } else {
      $this->events[$date] = array("$key=$value");
    }
  }
}

// Storage for the abstract resources, e.g. host, cpu, core, a.s.o, i.e. not resource_id
class Resource {
  public $id, $type, $parent, $children, $resource_ids, $data;
  function __construct($id, $type, $parent, $data=array()) {
    $this->id = $id;
    $this->type = $type;
    $this->parent = $parent;
    $this->data = $data;
    $this->children = array();
    $this->resource_ids = array();
    $this->jobs = array();
    $this->job2resource_groups = array();
  }
  function add_child($id, $type, $data=array()) {
    if (! array_key_exists($id,$this->children)) {
      $this->children[$id] = new Resource($id, $type, $this, $data);
    }
    return $this->children[$id];
  }
  function add_resource_id($rid) {
    $this->resource_ids[$rid->id] = $rid;
    $rid->add_resource($this);
  }
  function add_job_occupation($job, $partial) {
    $this->jobs[$job->job_id] = array($job, $partial);
  }
  function add_job2resource_group($g) {
    array_push($this->job2resource_groups, $g);
  }
  function get_hierarchy_info() {
    if ($this->parent == NULL) {
      return $this->type.": ".$this->id;
    } else {
      return $this->parent->get_hierarchy_info()."|".$this->type.": ".$this->id;
    }
  }
  function cpusets() {
    $cpusets = array();
    foreach ($this->resource_ids as $rid) {
      array_push($cpusets, $rid->cpuset);
    }
    return $cpusets;
  }
  function properties() {
    $properties = array();
    foreach ($this->resource_ids as $rid) {
      foreach ($rid->properties as $key => $value) {
        if (array_key_exists($key, $properties)) {
          array_push($properties[$key], $value);
        } else {
          $properties[$key] = array($value);
        }
      }
    }
    return $properties;
  }
  function svg_hierarchy($x, $y, $labels=NULL)  {
    global $CONF, $output, $scale, $gantt_width, $gantt_left_align, $resource_base, $first_resource_base, $sorted_resources, $has_aggregated_resources, $resource_labels;
    $h = 0;
    debug(3,"hierarchy: ". $this->id . "(" . $this->type . ")");
    if (in_array($this->type, $resource_labels)) {
      if ($labels == NULL) {
        $labels = array($this->label());
      } else {
        array_push($labels, $this->label());
      }
    }
    if (($this->type != $resource_base) and (count($this->resource_ids) >= 1)) {
      if ($this->type == $CONF['resource_group_level']) {
        $first_resource_base = true;
      }
      usort($this->children, create_function('$a,$b','return $b->cmp($a);'));
      foreach ($this->children as $child) {
        $h += $child->svg_hierarchy($x+$CONF['hierarchy_resource_width'], $y+$h, $labels);
      }
      $output .= '<rect x="'.$x.'" y="'.$y.'" width="'.$CONF['hierarchy_resource_width'].'" height="'.$h.'" fill="#ffff80" stroke="#000000" stroke-width="1" style="opacity: 1" onmouseover="mouseOver(evt, \''.$this->get_hierarchy_info().'\',\'\')" onmouseout="mouseOut(evt,\'\')" onmousemove="mouseMove(evt)" onclick="resourceclick(evt,\''.$this->type.'\',\''.$this->id.'\')"/>';
    } else {
      if (count($this->resource_ids) > 1) {
        $has_aggregated_resources = true;
      }
      if ($this->type == $CONF['resource_group_level']) {
        $first_resource_base = true;
      }
      $popup = array();
      foreach ($this->properties() as $key => $value) {
        sort($value);
        array_push($popup, $key.": ".join(", ", array_unique($value)));
      }
      if(in_array("cpuset", $resource_labels) and ! in_array("cpuset", $CONF['resource_hierarchy']) and ! $has_aggregated_resources) {
        array_push($labels, sprintf($CONF['cpuset_label_display_string'],reset($this->cpusets())));
      }
      $output .= '<text font-size="10" x="'.$CONF['label_right_align'].'" y="'.($y + $scale/2).'" text-anchor="end" dominant-baseline="central" onmouseover="mouseOver(evt, \''.htmlspecialchars(join("|", $popup)).'\',\'\')" onmouseout="mouseOut(evt,\'\')" onmousemove="mouseMove(evt)" onclick="resourceclick(evt,\''.$this->type.'\',\''.$this->id.'\')">';
      $output .= join("/", $labels);
      $output .= '</text>';
      $output .= '<rect x="'.$x.'" y="'.$y.'" width="'.$CONF['hierarchy_resource_width'].'" height="'.$scale.'" fill="#ffff80" stroke="#000000" stroke-width="1" style="opacity: 1" onmouseover="mouseOver(evt, \''.$this->get_hierarchy_info().'\',\'\')" onmouseout="mouseOut(evt,\'\')" onmousemove="mouseMove(evt)" onclick="resourceclick(evt,\''.$this->type.'\',\''.$this->id.'\')"/>';
      if ($first_resource_base == true) {
        $first_resource_base = false;
        $output .= '<line x1="'.$gantt_left_align.'" y1="'.$y.'" x2="'.($gantt_left_align + $gantt_width).'" y2="'.$y.'" stroke="#0000FF" stroke-width="1" />';
      } else {
        $output .= '<line x1="'.$gantt_left_align.'" y1="'.$y.'" x2="'.($gantt_left_align + $gantt_width).'" y2="'.$y.'" stroke="#888888" stroke-width="1" />';
      }
      array_push($sorted_resources, $this);
      $h = $scale;
    }
    return $h;
  }
  function svg_states($y) {
    global $CONF, $output, $scale, $resource_base;
    $states=array();
    foreach (array_values($this->resource_ids) as $rid) {
      foreach ($rid->states as $s) {
        if (!array_key_exists($s->value, $states)) {
          $states[$s->value] = array();
        }
        array_push($states[$s->value], array($s->start, 1,$s->real_start), array($s->stop, -1, $s->real_stop));
      }
    }
    foreach ($states as $value => $date_weight_array) {
      usort($date_weight_array, create_function('$a,$b','list($d1,$w1)=$a; list($d2,$w2)=$b; return ($d1 != $d2)?(($d1 < $d2)?-1:1):(($w1 != $w2)?(($w1 < $w2)?1:-1):0);'));
      $weight = 0;
      foreach ($date_weight_array as $dwr) {
        list ($d,$w,$r) = $dwr;
        debug(4, "$value -> $d:$w");
        if ($w == -1 and $weight == count($this->resource_ids) and ($d - $start) > $CONF['min_state_duration']) {
          debug(3, "new full state $start -> $d");
          # new state $start,$w,full
          $info = ucfirst($resource_base)." $value|Since: ".date("r", $real_start)."|Until: ".(($r)?date("r", $r):"indefinite");
          $height = $scale;
          $output .= '<rect x="'.date2px($start).'" y="'.$y.'" width="'.(date2px($d) - date2px($start)).'" height="'.$height.'" fill="'.$CONF['state_colors'][$value].'" stroke-width="0" style="opacity: 0.75" onmouseover="mouseOver(evt, \''.$info.'\',\'\')" onmouseout="mouseOut(evt,\'\')" onmousemove="mouseMove(evt)" onclick="resourceclick(evt,\''.$this->type.'\',\''.$this->id.'\')"/>';
          $start = $d;
        }
        $weight += $w;
        debug(4, "weight: $weight");
        if ($w == 1 and $weight == 1) {
          $start = $d;
          $real_start = $r;
        } elseif ($weight == 0 and count($this->resource_ids) > 1 and ($d - $start) > $CONF['min_state_duration']) {
          debug(3, "new partial state: $start -> $d");
          # new state $start,$d,partial
          $info = ucfirst($resource_base)." partially $value|Since: ".date("r", $real_start)."|Until: ".(($r)?date("r", $r):"indefinite");
          $height = $scale/2;
          $output .= '<rect x="'.date2px($start).'" y="'.$y.'" width="'.(date2px($d) - date2px($start)).'" height="'.$height.'" fill="'.$CONF['state_colors'][$value].'" stroke-width="0" style="opacity: 0.75" onmouseover="mouseOver(evt, \''.$info.'\',\'\')" onmouseout="mouseOut(evt,\'\')" onmousemove="mouseMove(evt)" onclick="resourceclick(evt,\''.$this->type.'\',\''.$this->id.'\')"/>';
        }
        if ($w == 1 and $weight == count($this->resource_ids) and ($d - $start) > $CONF['min_state_duration']) {
          debug(3, "new partial state before a full state: $start -> $d");
          # new state $start,$d,partial
          $info = ucfirst($resource_base)." partially $value|Since: ".date("r", $real_start)."|Until: ".(($r)?date("r", $r):"indefinite");
          $height = $scale/2;
          $output .= '<rect x="'.date2px($start).'" y="'.$y.'" width="'.(date2px($d) - date2px($start)).'" height="'.$height.'" fill="'.$CONF['state_colors'][$value].'" stroke-width="0" style="opacity: 0.75" onmouseover="mouseOver(evt, \''.$info.'\',\'\')" onmouseout="mouseOut(evt,\'\')" onmousemove="mouseMove(evt)" onclick="resourceclick(evt,\''.$this->type.'\',\''.$this->id.'\')"/>';
          $start = $d;
        }
      }
    }
  }
  function svg_drain($y) {
    global $CONF, $output, $scale, $gantt_now, $gantt_width, $gantt_left_align, $resource_base;
    $drain = 0;
    foreach (array_values($this->resource_ids) as $rid) {
      if (array_key_exists($CONF['resource_drain_property'], $rid->properties) and ($rid->properties[$CONF['resource_drain_property']] == "YES")) {
        $drain++;
      }
    }
    if ($drain > 0) {
      if ($drain < count($this->resource_ids)) {
        $info = ucfirst($resource_base)." partial draining: no new job accepted";
        $height = $scale/2;
        debug(4, $info);
      } else {
        $info = ucfirst($resource_base).((count($this->resource_ids) > 1)?" full":"")." draining: no new job accepted";
        $height = $scale;
        debug(4, $info);
      }
      $output .= '<rect x="'.date2px($gantt_now).'" y="'.$y.'" width="'.($gantt_width - date2px($gantt_now) + $gantt_left_align).'" height="'.$height.'" fill="'.$CONF['state_colors']['Drain'].'" stroke-width="0" style="opacity: 0.5" onmouseover="mouseOver(evt, \''.$info.'\',\'\')" onmouseout="mouseOut(evt,\'\')" onmousemove="mouseMove(evt)" onclick="resourceclick(evt,\''.$this->type.'\',\''.$this->id.'\')"/>';
    }
  }
  function svg_events($y) {
    global $CONF, $output, $scale, $gantt_now, $gantt_width, $gantt_left_align, $resource_base;
    $events = array();
    foreach (array_values($this->resource_ids) as $rid) {
      foreach ($rid->events as $date => $evs) {
        $events[$date][$rid->id]=$evs;
      }
    }
    ksort($events);
    foreach ($events as $date => $rid2evs) {
      $info = "";
      ksort($rid2evs);
      foreach ($rid2evs as $rid => $evs) {
        sort($evs);
        foreach ($evs as $e) {
          $info .= "|rid=$rid: $e";
        }
      }
      $output .= '<circle cx="'.date2px($date).'" cy="'.($y+$scale/2).'" r="'.($scale/2).'" fill="red" stroke-width="0" style="opacity: 0.5" onmouseover="mouseOver(evt, \''.$info.'\',\'\')" onmouseout="mouseOut(evt,\'\')" onmousemove="mouseMove(evt)"/>';
    }
  }
  function svg_jobs($y) {
    global $CONF, $output, $scale, $gantt_now, $output_labels, $patterns, $has_aggregated_resources;
    if ($has_aggregated_resources) { // we need to display the job on partial resources => we cannot use job groups for that.
      foreach ($this->jobs as $job_id => $jp) {
        list($job, $partial) = $jp;
        if($job->stop_time > 0) { // we know the job's stop time because the the job is in the past
          $w = date2px($job->stop_time) - date2px($job->start_time);
        } elseif (in_array('besteffort', $job->types) and ($job->state == "Running")) {
          $w = date2px($gantt_now) - date2px($job->start_time);
        } else {
          $w = date2px($job->start_time + $job->moldable_walltime) - date2px($job->start_time);
        }
        if ($partial) {
          $h = $scale/2;
          $yy = $y + $scale/2;
        } else {
          $h = $scale;
          $yy = $y;
        }
        foreach ($CONF['job_colors'] as $type => $color) {
          if (preg_grep("/^{$type}$/", $job->types)) {
            $output .= '<rect x="'.date2px($job->start_time).'" y="'.$y.'" width="'.$w.'" height="'.$scale.'" fill="'.$color.'" stroke-width="0"  style="opacity: 0.5" />';
          }
        }
        $output .= '<rect class="job'.$job->job_id.'" x="'.date2px($job->start_time).'" y="'.$yy.'" width="'.$w.'" height="'.$h.'" fill="'.$job->color(false).'" stroke-width="0"  style="opacity: 0.3" onmouseover="mouseOver(evt,\''.$job->svg_text().'\', \'job'.$job->job_id.'\')" onmouseout="mouseOut(evt, \'job'.$job->job_id.'\')" onclick="jobclick(evt, \''.$job->job_id.'\')" onmousemove="mouseMove(evt)" />';
        if (in_array('besteffort', $job->types) and ($job->state == "Running") and ! $CONF['besteffort_truncate_job_to_now']) {
          foreach ($CONF['job_colors'] as $type => $color) {
            if (preg_grep("/^{$type}$/", $job->types)) {
              $output .= '<rect x="'.date2px($gantt_now).'" y="'.$y.'" width="'.(date2px($job->start_time + $job->moldable_walltime) - date2px($gantt_now)).'" height="'.$scale.'" fill="'.$color.'" stroke-width="0"  style="opacity: 0.5" />';
            }
          }
          if (! array_key_exists($job->job_id, $patterns)) {
            $p = preg_replace("/%%PATTERN_ID%%/", "besteffort{$job->job_id}Pattern", $CONF['besteffort_pattern']);
            $p = preg_replace("/%%PATTERN_COLOR%%/", $job->color(false), $p);
            $patterns[$job->job_id] = $p;
            $p = preg_replace("/%%PATTERN_ID%%/", "besteffortHL{$job->job_id}Pattern", $CONF['besteffort_pattern']);
            $p = preg_replace("/%%PATTERN_COLOR%%/", $job->color(true), $p);
            $patterns[$job->job_id] .= $p;
          }
          $output .= '<rect class="job'.$job->job_id.'" x="'.date2px($gantt_now).'" y="'.$yy.'" width="'.(date2px($job->start_time + $job->moldable_walltime) - date2px($gantt_now)).'" height="'.$h.'" fill="'."url(#besteffort{$job->job_id}Pattern)".'" stroke-width="0"  style="opacity: 0.3" onmouseover="mouseOver(evt,\''.$job->svg_text().'\', \'job'.$job->job_id.'\')" onmouseout="mouseOut(evt, \'job'.$job->job_id.'\')" onclick="jobclick(evt, \''.$job->job_id.'\')" onmousemove="mouseMove(evt)" />';
        }
      }
    }
    foreach ($this->job2resource_groups as $grp) {
      if($grp->job->stop_time > 0) { // we know the job's stop time because the the job is in the past
        $w = date2px($grp->job->stop_time) - date2px($grp->job->start_time);
      } elseif (in_array('besteffort', $grp->job->types) and ($grp->job->state == "Running") and $CONF['besteffort_truncate_job_to_now']) {
        $w = date2px($gantt_now) - date2px($grp->job->start_time);
      } else {
        $w = date2px($grp->job->start_time + $grp->job->moldable_walltime) - date2px($grp->job->start_time);
      }
      if ($has_aggregated_resources) { // we only have to display the base rectangle using the job group
        $output .= '<rect class="job'.$grp->job->job_id.'" x="'.date2px($grp->job->start_time).'" y="'.$y.'" width="'.$w.'" height="'.($grp->size() * $scale).'" fill="'.$grp->job->color(false).'" stroke="#000088" stroke-width="1"  style="opacity: 0.3" onmouseover="mouseOver(evt,\''.$grp->job->svg_text().'\', \'job'.$grp->job->job_id.'\')" onmouseout="mouseOut(evt, \'job'.$grp->job->job_id.'\')" onclick="jobclick(evt, \''.$grp->job->job_id.'\')" onmousemove="mouseMove(evt)" />';
      } else { // we do everything using the job group
        foreach ($CONF['job_colors'] as $type => $color) {
          if (preg_grep("/^{$type}$/", $grp->job->types)) {
            $output .= '<rect x="'.date2px($grp->job->start_time).'" y="'.$y.'" width="'.$w.'" height="'.($grp->size() * $scale).'" fill="'.$color.'" stroke-width="0"  style="opacity: 0.5" />';
          }
        }
        if (in_array('besteffort', $grp->job->types) and ($grp->job->state == "Running") and ! $CONF['besteffort_truncate_job_to_now']) {
          if (! array_key_exists($grp->job->job_id, $patterns)) {
            $p = preg_replace("/%%PATTERN_ID%%/", "besteffort{$grp->job->job_id}Pattern", $CONF['besteffort_pattern']);
            $p = preg_replace("/%%PATTERN_COLOR%%/", $grp->job->color(false), $p);
            $patterns[$grp->job->job_id] = $p;
            $p = preg_replace("/%%PATTERN_ID%%/", "besteffortHL{$grp->job->job_id}Pattern", $CONF['besteffort_pattern']);
            $p = preg_replace("/%%PATTERN_COLOR%%/", $grp->job->color(true), $p);
            $patterns[$grp->job->job_id] .= $p;
          }
          $output .= '<rect class="job'.$grp->job->job_id.'" x="'.date2px($gantt_now).'" y="'.$y.'" width="'.(date2px($grp->job->start_time + $grp->job->moldable_walltime) - date2px($gantt_now)).'" height="'.($grp->size() * $scale).'" fill="'."url(#besteffort{$grp->job->job_id}Pattern)".'" stroke-width="0"  style="opacity: 0.3" onmouseover="mouseOver(evt,\''.$grp->job->svg_text().'\', \'job'.$grp->job->job_id.'\')" onmouseout="mouseOut(evt, \'job'.$grp->job->job_id.'\')" onclick="jobclick(evt, \''.$grp->job->job_id.'\')" onmousemove="mouseMove(evt)" />';
          $ww = date2px($gantt_now) - date2px($grp->job->start_time);
        } else {
          $ww = $w;
        }
        $output .= '<rect class="job'.$grp->job->job_id.'" x="'.date2px($grp->job->start_time).'" y="'.$y.'" width="'.$ww.'" height="'.($grp->size() * $scale).'" fill="'.$grp->job->color(false).'" stroke-width="0"  style="opacity: 0.3" onmouseover="mouseOver(evt,\''.$grp->job->svg_text().'\', \'job'.$grp->job->job_id.'\')" onmouseout="mouseOut(evt, \'job'.$grp->job->job_id.'\')" onclick="jobclick(evt, \''.$grp->job->job_id.'\')" onmousemove="mouseMove(evt)" />';
        $output .= '<rect class="job'.$grp->job->job_id.'" x="'.date2px($grp->job->start_time).'" y="'.$y.'" width="'.$w.'" height="'.($grp->size() * $scale).'" fill="'.$grp->job->color(false).'" stroke="#000088" stroke-width="1"  style="opacity: 0.3" onmouseover="mouseOver(evt,\''.$grp->job->svg_text().'\', \'job'.$grp->job->job_id.'\')" onmouseout="mouseOut(evt, \'job'.$grp->job->job_id.'\')" onclick="jobclick(evt, \''.$grp->job->job_id.'\')" onmousemove="mouseMove(evt)" />';
      }
      if ($w > $CONF['gantt_min_job_width_for_label']) {
        $output_labels .= '<text font-size="10" x="'.(date2px($grp->job->start_time) + $w / 2).'" y="'.($y + $grp->size() * $scale / 2).'" text-anchor="middle" dominant-baseline="central" onmouseover="mouseOver(evt,\''.$grp->job->svg_text().'\', \'job'.$grp->job->job_id.'\')" onmouseout="mouseOut(evt, \'job'.$grp->job->job_id.'\')" onclick="jobclick(evt, \''.$grp->job->job_id.'\')" onmousemove="mouseMove(evt)">';
        $output_labels .= $grp->job->job_id;
        $output_labels .= '</text>';
      }
    }
  }
  function label() {
    global $CONF;
    $label = $this->id;
    if ($this->type == 'cpuset') {
      $label = sprintf($CONF['cpuset_label_display_string'], $label);
    }
    if (array_key_exists($this->type, $CONF['label_display_functions'])) {
        $label = $CONF['label_display_functions'][$this->type]($this);
    } elseif (array_key_exists($this->type, $CONF['label_display_regex'])) {
      if (is_array($CONF['label_display_regex'][$this->type])) {
        $label = preg_replace($CONF['label_display_regex'][$this->type][0],$CONF['label_display_regex'][$this->type][1],$label);
      } else {
        $label = preg_replace($CONF['label_display_regex'][$this->type],'$1',$label);
      }
    }
    return $label;
  }
  function cmp($r) {
    global $CONF;
    debug(4, "cmp: " . $this->id." ??? ".$r->id . " (" . $this->type . ")");
    if (array_key_exists($this->type, $CONF['label_cmp_functions'])) {
      debug(5, "cmp: use label cmp function");
      $v1 = [$CONF['label_cmp_functions'][$this->type]($this)];
      $v2 = [$CONF['label_cmp_functions'][$this->type]($r)];
    } elseif (array_key_exists($this->type, $CONF['label_cmp_regex'])) {
      $r1 = preg_match($CONF['label_cmp_regex'][$this->type],$this->id, $v1);
      $r2 = preg_match($CONF['label_cmp_regex'][$this->type],$r->id, $v2);
      if (!$r1 or !$r2) {
        debug(5, "cmp: regex match failed");
        $v1 = array($this->id);
        $v2 = array($r->id);
      } else {
        debug(5, "cmp: regex matches");
        array_shift($v1);
        array_shift($v2);
      }
    } else {
      debug(5, "cmp: no regex to match");
      $v1 = array($this->id);
      $v2 = array($r->id);
    }
    debug(4, "cmp: " . join(",",$v1) . " ? " . join(",",$v2));
    while (($e1 = array_shift($v1)) != NULL) {
      $e2 = array_shift($v2);
      if ($e1 < $e2) {
        debug(4, "cmp: $e1 < $e2");
        return 1;
      } elseif ($e2 < $e1) {
        debug(4, "cmp: $e1 > $e2");
        return -1;
      }
      debug(4, "cmp: $e1 = $e2");
    }
    return 0;
  }
}

///////////////////////////////////////////////////////////////////////////////
// Main program
///////////////////////////////////////////////////////////////////////////////

debug(1, "starting drawgantt-svg");
$script_start_time = microtime(true);
// Connecting database
$db = new Db($CONF['db_type'], $CONF['db_server'], $CONF['db_port'], $CONF['db_name'], $CONF['db_user'], $CONF['db_passwd']);

// Retrieve the "now" date
debug(2,"retrieve time from DB");
$gantt_now = $db->get_timestamp();

// Compute gantt start and stop dates
if ($gantt_start_date == 0) {
  $gantt_start_date = $gantt_now + $gantt_relative_start_date;
}
if ($gantt_stop_date == 0) {
  $gantt_stop_date = $gantt_now + $gantt_relative_stop_date;
}
$timespan = $gantt_stop_date - $gantt_start_date;
if ($timespan < $CONF['min_timespan']) {
  debug(2, "Timespan $timespan too small, using ".$CONF['min_timespan']);
  $timespan = $CONF['min_timespan'];
  $gantt_start_date = ($gantt_stop_date + $gantt_start_date)/2 - $timespan / 2;
  $gantt_stop_date = $gantt_start_date + $timespan;
}

// Retrieve the resource hierarchy
$resource_root = new Resource($CONF['site'], 'site', NULL, array());
$resource_ids = array();

///////////////////////////////////////////////////////////////////////////////
// Retrieve OAR data from database
///////////////////////////////////////////////////////////////////////////////

$aborted = false;

// common function used both for past and current jobs and predicted jobs
function parse_jobs_from_db($query) {
  global $CONF, $db, $jobs, $resource_ids, $aborted;
  $t0 = microtime(true);
  $l = 0;
  $j = 0;
  if ($aborted) {
    debug (1, "already aborted");
    return;
  }
  debug(2,"start parsing jobs from " . $db->num_rows . " rows of data");
  $db->query($query);
  if ($db->num_rows > $CONF['db_max_job_rows']) {
    debug (1, "too many data ( > " . $CONF['db_max_job_rows'] . " rows), aborting");
    $aborted = true;
    return;
  }
  while ($line = $db->fetch_array()) {
    if (! array_key_exists($line['job_id'], $jobs)) {
      debug(3, "got new job: ".$line['job_id']);
      $jobs[$line['job_id']] = new Job($line['job_id'], $line['job_name'], $line['project'], $line['job_type'], $line['state'], $line['job_user'], $line['command'], $line['queue_name'], $line['moldable_walltime'], $line['properties'], $line['launching_directory'], $line['submission_time'], $line['start_time'], $line['stop_time']);
      $j++;
    } else {
      debug(4, "got an additional line for job: ".$line['job_id']);
    }
    if (array_key_exists($line['resource_id'], $resource_ids)) {
      $jobs[$line['job_id']]->add_resource_id($resource_ids[$line['resource_id']]);
    } else {
      // create new resource_id so than the job gets the right resource_id count, even if that resource_id is not displayed in the grid... (filter)
      debug(2, "create a phantom resource: " . $line['resource_id'] . " ,for job:" . $line['job_id']);
      $jobs[$line['job_id']]->add_resource_id(new ResourceId($line['resource_id'], -1));
    }
    $jobs[$line['job_id']]->add_network_address($line['network_address']);
    $jobs[$line['job_id']]->add_type($line['type']);
    $l++;
    if (($l % 1000) == 0) {
      debug(2, "$l rows parsed, got $j jobs, in " . (microtime(true) - $t0) . "s");
    }
  }
  $db->free();
  debug(2,"parsing done, got $j jobs, in "  . (microtime(true) - $t0) . "s");
}

if ($display != "mobile_ruler_only") {
  $query = 'SELECT ' . join(',',array_unique(array_merge($CONF['resource_properties'], $CONF['resource_hierarchy'], array('cpuset', 'resource_id')))) . ' FROM resources' . ($resource_filter?' WHERE ('.stripslashes($resource_filter).')':'');
  debug(2,"retrieve resources from DB");
  $db->query($query);

  while ($line = $db->fetch_array()) {
    $rid = new ResourceId($line['resource_id'], $line['cpuset']);
    foreach ($CONF['resource_properties'] as $rp) {
      $rid->add_property($rp, $line[$rp]);
    }
    $resource_ids[$line['resource_id']] = $rid;
    $resource_root->add_resource_id($rid);
    $r = $resource_root;
    foreach ($CONF['resource_hierarchy'] as $rh) {
      debug(4, "add resource $rh: " . $line[$rh] . " to " . $r->type . ": " . $r->id);
      // TODO: add resource description in the constructor
      $r = $r->add_child($line[$rh],$rh,$line);
      $r->add_resource_id($rid);
    }
  }
  $db->free();

  // Retrieve the resources states from resource_log
  $show_scheduler_priority = (array_key_exists('scheduler_priority', $_GET))?"OR (attribute = 'scheduler_priority' AND date_start > $gantt_start_date)":"";
  $value_int = $db->cast("value","int");
  $query = <<<EOT
SELECT resource_id, date_start, date_stop, value, attribute
FROM resource_logs
WHERE date_start <= {$gantt_stop_date}
AND ( date_stop = 0
  OR date_stop >= {$gantt_start_date}
)
AND (
  ( attribute = 'state'
    AND (
      value = 'Absent'
      OR value = 'Dead'
      OR value = 'Suspected'
    )
  )
  OR ( attribute = 'available_upto'
    AND {$value_int} > {$gantt_now}
    AND date_stop = 0
  )
  {$show_scheduler_priority}
)
EOT;
  $available_upto_lines = array();
  debug(2,"retrieve states from DB");
  $db->query($query);
  while ($line = $db->fetch_array()) {
    if ($line['attribute'] == 'state') {
      if (array_key_exists($line['resource_id'], $resource_ids)) {
        $resource_ids[$line['resource_id']]->add_state($line['value'], $line['date_start'], $line['date_stop']);
      }
    } elseif ($line['attribute'] == 'available_upto') {
      array_push($available_upto_lines, $line);
    } elseif ($line['attribute'] == 'scheduler_priority') {
      if (array_key_exists($line['resource_id'], $resource_ids)) {
        $resource_ids[$line['resource_id']]->add_event($line['date_start'],'scheduler_priority',$line['value']);
      }
    }
  }
  $db->free();
  if ($gantt_now < $gantt_stop_date) {
    foreach ($available_upto_lines as $line) {
      if (array_key_exists($line['resource_id'], $resource_ids)) {
        $resource_id = $resource_ids[$line['resource_id']];
        for ($i=0; $i < count($resource_id->states); $i++) {
          $state = $resource_id->states[$i];
          if ($state->value == 'Absent') {
            if ($gantt_now < $gantt_start_date) {
              unset($resource_id->states[$i]);
            } elseif ($state->real_stop == 0) {
              if ($CONF['standby_truncate_state_to_now']) {
                $state->set_stop($gantt_now);
              }
              $state->value = 'Standby';
            }
          }
        }
      }
    }
  }

  // Array to store jobs
  $jobs = array();

  // Retrieve past and current jobs
  $aborted = false;
  $query = <<<EOT
SELECT
  jobs.job_id,
  jobs.job_name,
  jobs.project,
  jobs.job_type,
  jobs.state,
  jobs.job_user,
  jobs.command,
  jobs.queue_name,
  moldable_job_descriptions.moldable_walltime,
  jobs.properties,
  jobs.launching_directory,
  jobs.submission_time,
  jobs.start_time,
  jobs.stop_time,
  assigned_resources.resource_id,
  resources.network_address,
  job_types.type
FROM
  (jobs LEFT JOIN job_types ON (job_types.job_id = jobs.job_id)), assigned_resources, moldable_job_descriptions, resources
WHERE
  ( jobs.stop_time >= {$gantt_start_date} OR
    ( jobs.stop_time = '0' AND
      ( jobs.state = 'Running' OR
      jobs.state = 'Suspended' OR
      jobs.state = 'Resuming'
      )
    )
  ) AND
  jobs.start_time < {$gantt_stop_date} AND
  jobs.assigned_moldable_job = moldable_job_descriptions.moldable_id AND
  moldable_job_descriptions.moldable_id = assigned_resources.moldable_job_id AND
  assigned_resources.resource_id = resources.resource_id
ORDER BY
  jobs.job_id
EOT;
  debug(2,"retrieve past and current jobs from DB");
  parse_jobs_from_db($query);

  // Retrieve predicted jobs (future)
  $query = <<<EOT
SELECT
  jobs.job_id,
  jobs.job_name,
  jobs.project,
  jobs.job_type,
  jobs.state,
  jobs.job_user,
  jobs.command,
  jobs.queue_name,
  moldable_job_descriptions.moldable_walltime,
  jobs.properties,
  jobs.launching_directory,
  jobs.submission_time,
  gantt_jobs_predictions_visu.start_time,
  jobs.stop_time,
  gantt_jobs_resources_visu.resource_id,
  resources.network_address,
  job_types.type
FROM
  (jobs LEFT JOIN job_types ON (job_types.job_id = jobs.job_id)), moldable_job_descriptions, gantt_jobs_resources_visu, gantt_jobs_predictions_visu, resources
WHERE
  gantt_jobs_predictions_visu.moldable_job_id = gantt_jobs_resources_visu.moldable_job_id AND
  gantt_jobs_predictions_visu.moldable_job_id = moldable_job_descriptions.moldable_id AND
  jobs.job_id = moldable_job_descriptions.moldable_job_id AND
  gantt_jobs_predictions_visu.start_time < {$gantt_stop_date} AND
  resources.resource_id = gantt_jobs_resources_visu.resource_id AND
  gantt_jobs_predictions_visu.start_time + moldable_job_descriptions.moldable_walltime >= {$gantt_start_date} AND
  jobs.job_id NOT IN ( SELECT job_id FROM job_types WHERE type = 'besteffort' AND types_index = 'CURRENT' )
ORDER BY
  jobs.job_id
EOT;
  debug(2,"retrieve predicted jobs from DB");
  parse_jobs_from_db($query);
}

// Closing connection to the database
$db->close();

///////////////////////////////////////////////////////////////////////////////
// SVG document generation
///////////////////////////////////////////////////////////////////////////////

$output = "";
$gantt_height = 0;
if ($display != "no_ruler") {
  $gantt_top = $CONF['gantt_top'];
  $gantt_min_height = $CONF['gantt_min_height'];
  $bottom_margin = $CONF['bottom_margin'];
} else {
  $gantt_top = 1;
  $gantt_min_height = 0;
  $bottom_margin = 0;
}
if ($display != "mobile_ruler_only") {
  $sorted_resources = array();
  $has_aggregated_resources = false;
  $gantt_height = $resource_root->svg_hierarchy($CONF['hierarchy_left_align'], $gantt_top);
  debug(2, "has aggreagated resources: ".($has_aggregated_resources?"yes":"no"));

  // Split resources of jobs in continuous groups
  if (!$aborted) {
    $t0=microtime(true);
    debug(2, "start grouping resources of jobs");
    foreach ($jobs as $job) {
      $job->group_resources($sorted_resources);
    }
    debug(2, "grouping done in " . (microtime(true) - $t0) . "s");
  }

  $y = $gantt_top;
  foreach ($sorted_resources as $r) {
    $r->svg_states($y);
    $y += $scale;
  }
  $y = $gantt_top;
  foreach ($sorted_resources as $r) {
    $r->svg_drain($y);
    $y += $scale;
  }
  $y = $gantt_top;
  foreach ($sorted_resources as $r) {
    $r->svg_jobs($y);
    $r->svg_events($y);
    $y += $scale;
  }
  $output .= $output_labels;

  // compute sizes
  $page_height = max($gantt_min_height, $gantt_top + $gantt_height + $bottom_margin);
} else {
  $page_height = 30;
}
$page_width = $gantt_left_align + $gantt_width + $CONF['right_margin'];

// labers will be printed at the end of the doc to be on top of the job boxes.
$output_labels = "";
// SVG headers + script + defs
$output_headers = <<<EOT
<?xml version="1.0" standalone="no"?>
<svg width="{$page_width}px" height="{$page_height}px" viewBox="0 0 {$page_width} {$page_height}" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" xmlns:ev="http://www.w3.org/2001/xml-events" xml:space="preserve" zoomAndPan="magnify" onload="init(evt)" color-rendering="optimizeSpeed" image-rendering="optimizeSpeed" text-rendering="optimizeSpeed" shape-rendering="optimizeSpeed" onmousedown="rootMouseDown(evt)" onmouseup="rootMouseUp(evt)" onmousemove="rootMouseMove(evt)" onclick="rootClick(evt)" >

<script type="text/ecmascript"><![CDATA[
var svgDocument;
var infobox, infoboxtext, infoboxrect;
var timeruler;
var zoom, zoom_draw, zoom_x1, zoom_x2, zoom_y1, zoom_y2, zoom_width;
var parent_content;
var parent_infobox;
function init(evt) {

EOT;
if ($aborted) {
  $output_headers .= 'alert("['.$CONF['site'].'] Aborted, too many jobs to display, time window is too large. Please mind narrowing your lookup...");';
}
$output_headers .= <<<EOT
  svgDocument = evt.target.ownerDocument;
  resourcemark = svgDocument.getElementById("resourcemark");
  infobox = svgDocument.getElementById("infobox");
  infoboxrect = svgDocument.getElementById("infoboxrect");
  infoboxtext = svgDocument.getElementById("infoboxtext");
  timeruler=svgDocument.getElementById("timeruler");
  zoom = svgDocument.getElementById("zoom");
  zoom_x1 = 0;
  zoom_x2 = 0;
  zoom_y1 = 0;
  zoom_y2 = 0;
  zoom_width = 0;
  zoom_draw = false;
// workaround to make the script work on both firefox and chrome
  parent_content = (parent.content)?(parent.content):(parent);
  parent_content.addEventListener("scroll",drawTimeRuler, false);
  parent_infobox = parent_content.document.getElementById("infobox");
  if (parent_infobox == null || typeof(parent_content.infoboxDisplay) != 'function' || typeof(parent_content.infoboxHide) != 'function' || typeof(parent_content.infoboxMove) != 'function') {
    parent_infobox = null;
  }
  drawTimeRuler();
}

function px2date(y) {
  if (y < {$gantt_left_align}) {
    return {$gantt_start_date};
  }
  if (y > {$gantt_left_align} +  {$gantt_width}) {
    return {$gantt_stop_date};
  }
  return Math.round((y - {$gantt_left_align}) * ({$gantt_stop_date} - {$gantt_start_date}) / {$gantt_width} + {$gantt_start_date});
}

function zoomDraw() {
  if ( zoom_width > 5) {
    zoom.setAttribute("x", Math.min(zoom_x1,zoom_x2));
    zoom.setAttribute("y", {$gantt_top});
    zoom.setAttribute("width", zoom_width);
    zoom.setAttribute("height", {$gantt_height} );
    zoom.setAttribute("display", "inline");
  } else {
    zoom.setAttribute("display", "none");
  }
}
function rootMouseDown(evt) {
  if (parent_content != null && typeof(parent_content.setZoomWindow) == 'function' && evt.pageX > {$gantt_left_align} && evt.pageX < ({$gantt_left_align} + {$gantt_width}) && evt.pageY > {$gantt_top} && evt.pageY < ({$gantt_top} + {$gantt_height})) {
    zoom_x1 = evt.pageX;
    zoom_x2 = zoom_x1;
    zoom_width = 0;
    zoom_draw = true;
  }
}
function rootMouseUp(evt) {
  zoom_draw = false;
  if (zoom_width > 5 && parent_content != null && typeof(parent_content.setZoomWindow) == 'function') {
    parent_content.setZoomWindow({$gantt_now}, px2date(Math.min(zoom_x1,zoom_x2)), px2date(Math.max(zoom_x1,zoom_x2)));
  }
  zoom_x1 = 0;
  zoom_x2 = 0;
  zoom_width = 0;
}
function rootMouseMove(evt) {
  if (zoom_draw &&
    (evt.pageX > {$gantt_left_align}) && (evt.pageX < ({$gantt_left_align} + {$gantt_width})) &&
    (evt.pageY > {$gantt_top}) && (evt.pageY < ({$gantt_top} + {$gantt_height}))) {
    zoom_x2 = evt.pageX;
    zoom_width=Math.abs(zoom_x2 - zoom_x1);
    zoomDraw();
  }
  if (resourcemark == null) {
    resourcemark = svgDocument.getElementById("resourcemark");
  }
  if (evt.pageY > {$gantt_top} && evt.pageY < ({$gantt_top} + {$gantt_height})) {
    y = Math.floor((evt.pageY - {$gantt_top}) / {$scale}) * {$scale} + {$gantt_top};
    resourcemark.setAttribute("transform", "translate(0," + y + ")");
    resourcemark.setAttribute("display", "inline");
  } else {
    resourcemark.setAttribute("display", "none");
  }
}
function rootClick(evt) {
  zoom_draw = false;
  zoom_x1 = 0;
  zoom_x2 = 0;
  zoom_width = 0;
  zoomDraw();
}
function drawTimeRuler(evt) {
  if ("{$display}" != "mobile_ruler_only" && timeruler != null) {
    if (parent_content != null && {$page_height} > parent_content.innerHeight) {
      y = parent_content.scrollY + parent_content.innerHeight - 45;
      timeruler.setAttribute("transform","translate(0," + y + ")");
      timeruler.setAttribute("display", "inline");
    } else {
      timeruler.setAttribute("display", "none");
    }
  }
}
function resourceclick(evt, type, id) {
  var url = '{$CONF['resource_click_url']}';
  if (url != '' && evt.detail > 1) {
    window.open(url.replace('%%TYPE%%', type).replace('%%ID%%', id));
  }
}
function jobclick(evt, jobid) {
  var url = '{$CONF['job_click_url']}';
  if (url != '' && evt.detail > 1) {
    window.open(url.replace('%%JOBID%%', jobid));
  }
}
function highlight(object, hl_element_class, bool) {
  if (object == null || object != svgDocument.object_ref) {
    var elems = document.getElementsByClassName(hl_element_class);
    for(var i = 0; i < elems.length; i++) {
      if (bool) {
        elems[i].setAttribute("fill", elems[i].getAttribute("fill").replace(",{$CONF['job_color_saturation_lightness']})",",{$CONF['job_color_saturation_lightness_highlight']})"));
        elems[i].setAttribute("fill", elems[i].getAttribute("fill").replace("besteffort","besteffortHL"));
        elems[i].setAttribute("stroke-width", 1.5);
      } else {
        elems[i].setAttribute("fill", elems[i].getAttribute("fill").replace(",{$CONF['job_color_saturation_lightness_highlight']})",",{$CONF['job_color_saturation_lightness']})"));
        elems[i].setAttribute("fill", elems[i].getAttribute("fill").replace("besteffortHL","besteffort"));
        elems[i].setAttribute("stroke-width", 1);
      }
    }
  }
  if (object == null && parent_content != null && typeof(parent_content.highlightOthers) == 'function') {
      parent_content.highlightOthers(svgDocument.object_ref, hl_element_class, bool);
  }
}
function mouseOver(evt, message, hl_element_class) {
  var length = 0;
  var array;
  var i = 0;
  var tspan;
  if (hl_element_class != '') {
    highlight(null, hl_element_class, true);
  }
  array = message.split("|");
  if (parent_infobox != null) {
    parent_content.infoboxDisplay(array);
  } else if (infobox != null && infoboxtext != null && infoboxrect != null) {
    while (infoboxtext.hasChildNodes()) {
      infoboxtext.removeChild(infoboxtext.lastChild);
    }
    infobox.setAttribute("display", "inline");
    for (i in array) {
      tspan = svgDocument.createElementNS("http://www.w3.org/2000/svg","tspan");
      tspan.setAttribute("x",10);
      tspan.setAttribute("dy",10);
      tspan.appendChild(svgDocument.createTextNode(array[i]));
      infoboxtext.appendChild(tspan);
      length = Math.max(length, tspan.getComputedTextLength());
    }
    infoboxrect.setAttribute("width", length + 20);
    infoboxrect.setAttribute("height", array.length * {$CONF['text_scale']} + 20);
  }
}
function mouseOut(evt, hl_element_class) {
  if (parent_infobox != null) {
    parent_content.infoboxHide(evt);
  } else if (infobox != null && infoboxtext != null && infoboxrect != null) {
    infobox.setAttribute("display", "none");
  }
  if (hl_element_class != '') {
    highlight(null, hl_element_class, false);
  }
}
function mouseMove(evt) {
  var width, height;
  var x,y;
  if (parent_infobox != null) {
    // get the position of the mouse in the parent window
    x = svgDocument.object_ref.getBoundingClientRect().left + evt.clientX;
    y = svgDocument.object_ref.getBoundingClientRect().top + evt.clientY;
    parent_content.infoboxMove(x,y,"svg");
  } else if (infobox != null && infoboxtext != null && infoboxrect != null) {
    width=parseInt(infoboxrect.getAttribute("width"));
    height=parseInt(infoboxrect.getAttribute("height"));
    if ((evt.pageX + 10 + width) < {$page_width}) {
      x = (evt.pageX + 10);
    } else {
      x = ({$page_width} - width);
    }
    if (parent_content != null && (evt.pageY + 20 + height) < Math.min({$page_height}, parent_content.scrollY + parent_content.innerHeight)) {
      y = (evt.pageY + 20);
    } else {
      y = (evt.pageY - height - 5 );
    }
    infobox.setAttribute("transform", "translate(" + x + "," + y + ")");
  }
}
]]></script>
EOT;

if ($display != 'mobile_ruler_only') {
  // diagram drawing
  $output = '<rect x="'.$gantt_left_align.'" y="'.$gantt_top.'" width="'.$gantt_width.'" height="'.$gantt_height.'" stroke="#0000FF" stroke-width="1" fill="'.(($aborted)?'#DDDDDD':'#FFFFFF').'" />'.$output;
} else {
  $output = "";
}
// compute the steps of the time rulers
$ruler_step= ($gantt_stop_date - $gantt_start_date) / ($CONF['time_ruler_scale']);
while (($r = array_pop($CONF['time_ruler_steps'])) >= $ruler_step) {}
$ruler_step = $r;

if ($display != 'mobile_ruler_only' and $display != 'no_ruler') {
  // print top time ruler
  $d = $gantt_start_date - $gantt_start_date % $ruler_step;
  while(($d += $ruler_step) < $gantt_stop_date) {
    $output .= '<text font-size="10" x="'.date2px($d).'" y="'.($gantt_top - 15).'" text-anchor="middle" >'.date("Y-m-d",$d).'</text>';
    $output .= '<text font-size="10" x="'.date2px($d).'" y="'.($gantt_top - 5).'" text-anchor="middle" >'.date("H:i:s",$d).'</text>';
  }

  // print bottom time ruler
  $d = $gantt_start_date - $gantt_start_date % $ruler_step;
  while(($d += $ruler_step) < $gantt_stop_date) {
    $output .= '<text font-size="10" x="'.date2px($d).'" y="'.($gantt_top + $gantt_height + 25).'" text-anchor="middle" >'.date("Y-m-d",$d).'</text>';
    $output .= '<text font-size="10" x="'.date2px($d).'" y="'.($gantt_top + $gantt_height + 15).'" text-anchor="middle" >'.date("H:i:s",$d).'</text>';
  }
}

if ($display != 'mobile_ruler_only') {
  // print time grid lines
  $d = $gantt_start_date - $gantt_start_date % $ruler_step;
  while(($d += $ruler_step) < $gantt_stop_date) {
    $output .= '<line x1="'.date2px($d).'" y1="'.($gantt_top - 5).'" x2="'.date2px($d).'" y2="'.($gantt_top + $gantt_height + 5).'" stroke="#0000FF" stroke-width="1" />';
  }

  // print now line
  if ($gantt_now < $gantt_start_date or $gantt_now > $gantt_stop_date) {
    $output .= '<line x1="'.date2px($gantt_now).'" y1="'.($gantt_top - 5).'" x2="'.date2px($gantt_now).'" y2="'.($gantt_top + $gantt_height + 5).'" stroke="#FF0000" stroke-width="2" stroke-dasharray="10,10"/>';
  } else {
    $output .= '<line x1="'.date2px($gantt_now).'" y1="'.($gantt_top - 5).'" x2="'.date2px($gantt_now).'" y2="'.($gantt_top + $gantt_height + 5).'" stroke="#FF0000" stroke-width="2"/>';
  }
}

if ($display != 'no_mobile_ruler' and $display != "no_ruler") {
  // print mobile time ruler
  $output .= '<g id="timeruler" display="inline">';
  $output .= '<rect x="'.($gantt_left_align - 30).'" y="1" width="'.($gantt_width + 60).'" height="29" stroke="#000000" stroke-width="1" fill="#FFFFFF" style="opacity: 0.7"/>';
  $d = $gantt_start_date - $gantt_start_date % $ruler_step;
  while(($d += $ruler_step) < $gantt_stop_date) {
    $output .= '<text font-size="10" x="'.date2px($d).'" y="25" text-anchor="middle" >'.date("Y-m-d",$d).'</text>';
    $output .= '<text font-size="10" x="'.date2px($d).'" y="15" text-anchor="middle" >'.date("H:i:s",$d).'</text>';
  }
  $output .= '</g>';
}


// print volatile stuff: infobox + zoom
$resourcemark_x = $CONF['label_right_align'] + 1;
$output .= <<<EOT
<rect id="resourcemark" x="{$resourcemark_x}" y="0" rx="0" ry="0" width="2" height="{$scale}" fill="#888888" stroke="#888888" stroke-width="1" style="opacity: 0.9" />
<g id="infobox" display="none">
<rect id="infoboxrect" x="0" y="0" rx="10" ry="10" width="200" height="150" fill="#FFFFFF" stroke="#888888" stroke-width="1" style="opacity: 0.9" />
<text font-size="10" id="infoboxtext" x="10" y="10" fill="#000000" />
</g>
<rect x="0" y="0" width="0" height="0" id="zoom" stroke="#0000FF" stroke-width="1" fill="#8888FF" style="opacity: 0.25" display="none" />
EOT;

$script_exec_time = microtime(true) - $script_start_time;
if ($display != 'mobile_ruler_only' and $display != 'no_ruler') {
  $output .= '<text id="stats" font-size="10" x="'.($gantt_left_align + $gantt_width /2).'" y="'.($page_height - 5).'" text-anchor="middle" fill="#888888">OAR drawgantt-svg for '.$CONF['site'].' - ref date: '.date("Y-m-d H:i:s",($gantt_start_date + $gantt_stop_date) / 2).' timespan: '.(($timespan >= 3600*24)?(floor($timespan/3600/24)."d"):"").gmdate("H\hi\ms\s",$timespan).' - generated in '.sprintf("%.3f",$script_exec_time).'s</text>';
}
// end SVG doc
$output .=  <<<EOT
</svg>
EOT;

$output_defs = "<defs>".$CONF['static_patterns'].join("",array_values($patterns))."</defs>";

// generate doc
header("Content-Type: image/svg+xml");
header('Content-Encoding: gzip');
print gzencode($output_headers . $output_defs . $output);

debug(1, "=> total execution time: " . $script_exec_time . "s");
?>
