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