1<?php
2/**
3 * dataplot-Plugin: Parses Gnuplot data blocks
4 *
5 * @license    GPL 3 (http://www.gnu.org/licenses/gpl-3.0.html)
6 * @author     Yann Pouillon <yann.pouillon@materialsevolution.es>
7 */
8
9
10if ( !defined('DOKU_INC') ) {
11  define('DOKU_INC', realpath(dirname(__FILE__).'/../../').'/');
12}
13if ( !defined('DOKU_PLUGIN') ) {
14  define('DOKU_PLUGIN', DOKU_INC.'lib/plugins/');
15}
16require_once(DOKU_PLUGIN.'syntax.php');
17
18class syntax_plugin_dataplot extends DokuWiki_Syntax_Plugin {
19
20  /**
21   * What about paragraphs?
22   */
23  function getPType() {
24    return 'normal';
25  }
26
27  /**
28   * What kind of syntax are we?
29   */
30  function getType() {
31    return 'substition';
32  }
33
34  /**
35   * Where to sort in?
36   */
37  function getSort() {
38    return 200;
39  }
40
41  /**
42   * Connect pattern to lexer
43   */
44  function connectTo($mode) {
45    $this->Lexer->addSpecialPattern('<dataplot.*?>\n.*?\n</dataplot>', $mode, 'plugin_dataplot');
46  }
47
48  /**
49   * Handle the match
50   */
51  function handle($match, $state, $pos, Doku_Handler $handler) {
52    $info = $this->getInfo();
53
54    // Set-up default data
55    $return = array(
56      'width'    => 0,
57      'height'   => 0,
58      'align'    => '',
59      'layout'   => '2D',
60      'columns'  => 2,
61      'plottype' => 'linespoints',
62      'smooth'   => false,
63      'xlabel'   => '',
64      'ylabel'   => '',
65      'xrange'   => '',
66      'yrange'   => '',
67      'gnuplot'  => '',
68      'debug'    => false,
69      'version'  => ''
70    );
71    $gnu_colors = array(
72      'white',
73      'red',
74      'medium-blue',
75      'orange-red',
76      'dark-violet',
77      'dark-turquoise',
78      'dark-chartreuse',
79      'grey40',
80      'black'
81    );
82
83    // Prepare input
84    $lines = explode("\n", $match);
85    $conf = array_shift($lines);
86    array_pop($lines);
87    $lines = trim(join("\n", $lines))."\n";
88    $lines = explode("\n", $lines);
89
90    // Get number of data columns
91    $cols = explode(" ", preg_replace("!\s+!", " ", trim($lines[0])));
92    $return['columns'] = count($cols);
93
94    // Match config options
95    // Note: treating xlabel and ylabel first then removing them from the
96    //       config string, in order to avoid misinterpretations of
97    //       further options.
98    if ( preg_match('/xlabel="([^"]*)"/i', $conf, $match) ) {
99      $return['xlabel'] = $match[1];
100      $conf = preg_replace('/xlabel="([^"]*)"/i', '', $conf);
101    }
102    if ( preg_match('/ylabel="([^"]*)"/i', $conf, $match) ) {
103      $return['ylabel'] = $match[1];
104      $conf = preg_replace('/ylabel="([^"]*)"/i', '', $conf);
105    }
106    if ( preg_match('/xrange=(-?\d*\.\d+(e-?\d+)?:-?\d*\.\d+(e-?\d+)?)/i', $conf, $match) ) {
107      $return['xrange'] = $match[1];
108    }
109    if ( preg_match('/yrange=(-?\d*\.\d+(e-?\d+)?:-?\d*\.\d+(e-?\d+)?)/i', $conf, $match) ) {
110      $return['yrange'] = $match[1];
111    }
112    if ( preg_match('/\b(2D|3D)\b/i', $conf, $match) ) {
113      $return['layout'] = strtolower($match[1]);
114    }
115    if ( preg_match('/\b(boxes|lines|linespoints|points)\b/i', $conf, $match) ) {
116      $return['plottype'] = $match[1];
117    }
118    if ( preg_match('/\b(smooth)\b/i', $conf, $match) ) {
119      $return['smooth'] = true;
120    }
121    if ( preg_match('/\b(left|center|right)\b/i', $conf, $match) ) {
122      $return['align'] = $match[1];
123    }
124    if ( preg_match('/\b(\d+)x(\d+)\b/', $conf, $match) ) {
125      $return['width']  = $match[1];
126      $return['height'] = $match[2];
127    }
128    if ( preg_match('/\bwidth=([0-9]+)\b/i', $conf, $match) ) {
129      $return['width'] = $match[1];
130    }
131    if ( preg_match('/\bheight=([0-9]+)\b/i', $conf, $match) ) {
132      $return['height'] = $match[1];
133    }
134    if ( preg_match('/\b(debug)\b/i', $conf, $match) ) {
135      $return['debug'] = true;
136    }
137
138    // Force rebuild of images on update
139    $return['version'] = date('Y-m-d H:i:s');
140    $return['hash'] = (string) uniqid("dataplot_", true);
141
142    // Generate Gnuplot code (must be last)
143    $input = trim(join("\n", $lines))."\n";
144    if ( $return['width'] != 0 && $return['height'] != 0 ) {
145      $gnu_size = ' size '.$return['width'].','.$return['height'];
146    } else {
147      $gnu_size = '';
148    }
149    $gnu_labels = '';
150    if ( strlen($return['xlabel']) > 0 ) {
151      $gnu_labels .= "set xlabel \"".$return['xlabel']."\"\n";
152    }
153    if ( strlen($return['ylabel']) > 0 ) {
154      $gnu_labels .= "set ylabel \"".$return['ylabel']."\"\n";
155    }
156    if ( strlen($return['xrange']) > 0 ) {
157      $gnu_ranges .= "set xrange [".$return['xrange']."]\n";
158    }
159    if ( strlen($return['yrange']) > 0 ) {
160      $gnu_ranges .= "set yrange [".$return['yrange']."]\n";
161    }
162
163    $gnu_code  = "# Input parameters:\n#\n";
164    foreach ($return as $param => $value) {
165      if ( $param != 'gnuplot' ) {
166        $gnu_code .= "#  - $param = $value\n";
167      }
168    }
169    $gnu_code .= "#\n\n";
170    $gnu_code .= 'set terminal pngcairo enhanced dashed font "arial,14" linewidth 2'.$gnu_size."\n";
171    $gnu_code .= $gnu_labels;
172    $gnu_code .= $gnu_ranges;
173    $gnu_code .= "set output \"@gnu_output@\"\n";
174    for ($i=1; $i<sizeof($gnu_colors); $i++) {
175      $gnu_code .= "set style line $i linetype rgb \"".$gnu_colors[$i]."\" linewidth 1.2 pointtype $i\n";
176    }
177    $gnu_code .= 'plot';
178    $sep  = ' ';
179    for ($i=2; $i<=$return['columns']; $i++) {
180      $gnu_style = $i-1;
181      if ( $return['smooth'] && ($return['plottype'] == 'linespoints') ) {
182        $gnu_code .= $sep.'"@gnu_input@" using 1:'.$i.' notitle smooth csplines with lines linestyle '.$gnu_style;
183        $sep = ", \\\n     ";
184        $gnu_code .= $sep.'"@gnu_input@" using 1:'.$i.' notitle with points linestyle '.$gnu_style;
185      } else {
186        $gnu_code .= $sep.'"@gnu_input@" using 1:'.$i.' notitle with '.$return[plottype].' linestyle '.$gnu_style;
187      }
188      $sep = ", \\\n     ";
189    }
190    $gnu_code .= "\n";
191    $return['gnuplot'] = $gnu_code;
192
193    // Store input for later use
194    io_saveFile($this->_cachename($return, 'txt'), $input);
195
196    return $return;
197  }
198
199  /**
200   * Cache file is based on parameters that influence the resulting image
201   */
202  function _cachename($data, $ext) {
203    return getcachename(
204      $data['hash'].'x'.$data['layout'].'x'.$data['plottype'], '.'.$ext);
205  }
206
207  /**
208   * Create output
209   */
210  function render($format, Doku_Renderer $renderer, $data) {
211    if ( $format == 'xhtml' ) {
212      $img = DOKU_BASE.'lib/plugins/dataplot/img.php?'.buildURLparams($data);
213      $renderer->doc .= '<img src="'.$img.'" class="media'.$data['align'].'" alt=""';
214      if ( $data['width'] )  $renderer->doc .= ' width="'.$data['width'].'"';
215      if ( $data['height'] ) $renderer->doc .= ' height="'.$data['height'].'"';
216      if ( $data['align'] == 'right' ) $renderer->doc .= ' align="right"';
217      if ( $data['align'] == 'left' )  $renderer->doc .= ' align="left"';
218      $renderer->doc .= '/>';
219
220      // Debugging
221      if ( $data['debug'] ) {
222        $renderer->doc .= '<pre>'.$data['gnuplot'].'</pre>';
223      }
224
225      return true;
226    } elseif ( $format == 'odt' ) {
227      $src = $this->_imgfile($data);
228      $renderer->_odtAddImage($src, $data['width'], $data['height'], $data['align']);
229
230      return true;
231    }
232
233    return false;
234  }
235
236  /**
237   * Return path to the rendered image on our local system
238   */
239  function _imgfile($data) {
240    $cache  = $this->_cachename($data, 'png');
241
242    // Create the file if needed
243    if ( !file_exists($cache) ) {
244      $in = $this->_cachename($data, 'txt');
245      if ( $this->getConf('path') ) {
246        $ok = $this->_run($data, $in, $cache);
247      } else {
248        $ok = false;
249      }
250      if ( !$ok ) return false;
251      clearstatcache();
252    }
253
254    // Resized version
255    if ( $data['width'] ) {
256      $cache = media_resize_image($cache, 'png', $data['width'], $data['height']);
257    }
258
259    // Something went wrong, we're missing the file
260    if ( !file_exists($cache) ) return false;
261
262    return $cache;
263  }
264
265  /**
266   * Run Gnuplot
267   */
268  function _run($data, $in, $out) {
269    global $conf;
270
271    // Check input data
272    if ( !file_exists($in) ) {
273      if ( $conf['debug'] ) {
274        dbglog($in,'no such dataplot input file');
275      }
276
277      return false;
278    }
279
280    // Create Gnuplot script
281    $gnu_code = $data['gnuplot'];
282    $gnu_code = preg_replace('!@gnu_input@!', $in, $gnu_code);
283    $gnu_code = preg_replace('!@gnu_output@!', $out, $gnu_code);
284    $gnu_script = tempnam('/tmp', 'dataplot');
285    $gnu_handle = fopen($gnu_script, 'w');
286    fwrite($gnu_handle, $gnu_code);
287    fclose($gnu_handle);
288
289    // Run command
290    $cmd  = $this->getConf('path');
291    $cmd .= ' '.$gnu_script;
292    exec($cmd, $output, $error);
293
294    // Remove Gnuplot script
295    //unlink($gnu_script);
296
297    if ( $error != 0 ) {
298      if ( $conf['debug'] ) {
299        dbglog(join("\n", $output), 'dataplot command failed: '.$cmd);
300      }
301
302      return false;
303    }
304
305    return true;
306  }
307
308}
309