1<?php 2/** 3 * RRDGraph Plugin: SVG binding with RRD tool variables 4 * 5 * @author Daniel Goß <developer@flashsystems.de> 6 * @license MIT 7 */ 8 9if (! defined('DOKU_INC')) die(); 10 11/** 12 * Represents a currently active attribute binding within the attribute binding stack. 13 * @author dgoss 14 * 15 */ 16class AttrBinding 17{ 18 /** 19 * The value of this attribute will be replaced by the value stored within the $value variable. 20 * @var String 21 */ 22 public $attribute; 23 24 /** 25 * Value to set for the attribute defined by $attribute. 26 * @var String 27 */ 28 public $value; 29 30 /** 31 * C'tor 32 * @param String $attribute Name of the attribute to bind to the given value. 33 * @param String $value Value to set for the given attribute. 34 */ 35 public function __construct($attribute, $value) { 36 $this->attribute = $attribute; 37 $this->value = $value; 38 } 39} 40 41/** 42 * Implements the SAX XML handlers used by the SvgBinding class for parsing SVG files and applying 43 * the requested bindings. 44 * @author dgoss 45 * 46 */ 47class XmlHandler { 48 /** 49 * File to write the output to. 50 * @var Ressource 51 */ 52 private $output; 53 54 /** 55 * Associative array containing all binding names and the associated, values. 56 * @var Array 57 */ 58 private $bindingValues; 59 60 /** 61 * This stack contains all binding currently active. 62 * The entries can be an instance of AttrBinding or NULL if the current binding is not bound to an attribute but 63 * replaces the bind-tag directly by the value. 64 * @var Array 65 */ 66 private $attrBindingStack = array(); 67 68 /* 69 * @see http://php.net/manual/en/function.xml-set-default-handler.php 70 * This handler is used to pass everything encountered within the XML-file directly into the output file. 71 */ 72 public function xmlPassthroughHandler($parser, $data) { 73 fwrite($this->output, $data); 74 } 75 76 /* 77 * @see http://php.net/manual/en/function.xml-set-element-handler.php 78 */ 79 public function xmlStartElementHandler($parser, $name, $attributes) { 80 if (strtolower($name) == "bind") { 81 if (!array_key_exists("var", $attributes)) throw new Exception("bind-tag is missing the var attribute."); 82 if (!array_key_exists("format", $attributes)) throw new Exception("bind-tag is missing the format attribute."); 83 84 $value = sprintf($attributes["format"], $this->bindingValues[$attributes["var"]]); 85 86 //-- If no attribute is set this is a direcout output binding. 87 if (array_key_exists("attr", $attributes)) { 88 array_push($this->attrBindingStack, new AttrBinding($attributes["attr"], $value)); 89 } else { 90 //-- Direct output bindings are pushed as a null value to allow poping them in the EndElementHandler. 91 array_push($this->attrBindingStack, null); 92 fwrite($this->output, $value); 93 } 94 } else { 95 //-- Add all bindings that are currently on the stack to the list of attributes. 96 foreach ($this->attrBindingStack as $binding) { 97 if ($binding != null) $attributes[$binding->attribute] = $binding->value; 98 } 99 100 fwrite($this->output, '<' . $name); 101 foreach ($attributes as $attr => $value) { 102 fwrite($this->output, ' ' . $attr . '="' . $value. '"'); 103 } 104 fwrite($this->output, '>'); 105 } 106 } 107 108 /* 109 * @see http://php.net/manual/en/function.xml-set-element-handler.php 110 */ 111 public function xmlEndElementHandler($parser, $name) { 112 if (strtolower($name) == "bind") { 113 if (count($this->attrBindingStack) == 0) { 114 throw new Exception("Closing bind tag without opening tag"); 115 } 116 array_pop($this->attrBindingStack); 117 } 118 else 119 { 120 fwrite($this->output, '</' . $name . '>'); 121 } 122 } 123 124 /** 125 * C'tor 126 * @param Resource $xmlParser The XML parser used for parsing this file. The constructor automatically adds its methods to the given parser. 127 * @param String $outputFileName Name of the file to write the generated XML file to. 128 * @param Array $bindingValues Associative array containing the binding names as keys and the values as values. 129 * @throws Exception 130 */ 131 public function __construct(&$xmlParser, $outputFileName, &$bindingValues) { 132 xml_set_object($xmlParser, $this); 133 xml_set_default_handler($xmlParser, 'xmlPassthroughHandler'); 134 xml_set_character_data_handler($xmlParser, 'xmlPassthroughHandler'); 135 xml_set_element_handler($xmlParser, 'xmlStartElementHandler', 'xmlEndElementHandler'); 136 137 xml_parser_set_option($xmlParser, XML_OPTION_CASE_FOLDING, 0); 138 xml_parser_set_option($xmlParser, XML_OPTION_SKIP_WHITE, 1); 139 140 $this->bindingValues = &$bindingValues; 141 142 $this->output = fopen($outputFileName, 'w'); 143 if ($this->output === false) throw new Exception('Could not open file "' . $outputFileName . '" for writing.'); 144 } 145 146 /** 147 * D'tor 148 */ 149 public function __destruct() { 150 fclose($this->output); 151 } 152} 153 154/** 155 * Class to create an image containing an error message. 156 */ 157class SvgBinding { 158 /** 159 * Associative array containing the binding names as keys and the name of the aggregate function 160 * used to create the binding value as a string value. 161 * @var Array 162 */ 163 private $aggregates = array(); 164 165 /** 166 * Adds a new binding to the list of bindings. The given aggregate function will be applied to 167 * all values and the result will be saved as a binding with the given name. 168 * @param String $bindingName Name of the binding to store the result value in. 169 * @param String $aggregateFunction Name of the aggregate function to use for creating the value. 170 */ 171 public function setAggregate($bindingName, $aggregateFunction) { 172 $this->aggregates[$bindingName] = $aggregateFunction; 173 } 174 175 /** 176 * Uses the given SVG file and processes all bindings within it. For creating values for the bindings 177 * rrd_xport is used with the options given within $rrdOptions. 178 * @param String $outputFile Name of the file to write the created SVG to. 179 * @param String $rrdOptions String containing the options to pass to rrd_xport. 180 * @param String $inputFile Name of the file to read the SVG structure from for processing. 181 * @throws Exception 182 */ 183 public function createSVG($outputFile, $rrdOptions, $inputFile) { 184 //-- Export the RRD data for the given options into memory. 185 // RRDExport is writing to the output stream if an error occures. This seems to be a bug, 186 // and currupts our error image. Output buffering to the rescue. 187 ob_start(); 188 $rrdData = @rrd_xport($rrdOptions); 189 ob_end_clean(); 190 if ($rrdData === false) throw new Exception(rrd_error()); 191 192 //-- Construct the binding values by applying the aggregate function. 193 $bindingValues = array(); 194 foreach ($rrdData["data"] as $data) 195 { 196 //-- Only process data if we know the aggregate function 197 $bindingName = $data[legend]; 198 199 $data["data"] = array_filter($data["data"], function ($value) { return !is_nan($value); }); 200 201 if (array_key_exists($bindingName, $this->aggregates)) 202 { 203 switch (strtoupper($this->aggregates[$bindingName])) { 204 //-- Takes the minimum value of all of the values within this RRD dataset. 205 case "MINIMUM": 206 case "MIN": 207 $bindingValues[$bindingName] = min($data["data"]); 208 break; 209 210 //-- Takes the maximum value of all of the values within this RRD dataset. 211 case "MAXIMUM": 212 case "MAX": 213 $bindingValues[$bindingName] = max($data["data"]); 214 break; 215 216 //-- Takes the average of all of the values within this RRD dataset. 217 case "AVERAGE": 218 case "AVG": 219 $bindingValues[$bindingName] = array_sum($data["data"])/count($data["data"]); 220 break; 221 222 //-- Just sums the values from the RRD. This may not be what you expect. See 223 // TOTAL. 224 case "SUM": 225 $bindingValues[$bindingName] = array_sum($data["data"]); 226 break; 227 228 //-- Total converts the realtive values stored within the RRD back to absolute values 229 // and sums them up. For each value within the RRD total += delta_t * value is 230 // calculated. 231 case "TOTAL": 232 $lastTs = NULL; 233 $total = 0; 234 foreach ($data["data"] as $ts => $value) { 235 if ($lastTs != NULL) { 236 if (!is_nan($value)) { 237 $total += ($ts - $lastTs) * $value; 238 } 239 } 240 241 $lastTs = $ts; 242 } 243 $bindingValues[$bindingName] = $total; 244 break; 245 246 //-- Takes the first (lowest timestamp) non NAN value of all of the values within this RRD dataset. 247 case "FIRST": 248 $bindingValues[$bindingName] = reset($data["data"]); 249 break; 250 251 //-- Takes the last (highest timestamp) non NAN value of all of the values within this RRD dataset. 252 case "LAST": 253 $bindingValues[$bindingName] = end($data["data"]); 254 break; 255 256 default: 257 throw new Exception('Unknown aggregation function ' . $this->aggregates[$bindingName]); 258 } 259 } 260 } 261 262 //-- Initialize the SAX parser 263 $xmlParser = xml_parser_create(); 264 $handler = new XmlHandler($xmlParser, $outputFile, $bindingValues); 265 266 //-- Now parse the SVG file. 267 $data = file_get_contents($inputFile); 268 if ($data === false) throw new Exception('Could not load file "' . $inputFile. '".'); 269 if (xml_parse($xmlParser, $data, true) == 0) throw new Exception('Parsing SVG failed: ' . xml_error_string(xml_get_error_code($xmlParser))); 270 271 unset($xmlParser); 272 } 273} 274