<?php

/***********************************************************
* pmgraphviz.php, requires PmWiki 2.1
* Allows insertion of graphviz dot language to display
* graphs.  Will cache resulting graphs to prevent
* re-rendering.                      
*
* Copyright (c) 2006, Chris Cox ccox@airmail.net
* All Rights, Reserved.
*
* This program is free software; you can redistribute it
* and/or modify it under the terms of the GNU General
* Public License as published by the Free Software
* Foundation; either version 2 of the License, or (at your
* option) any later version.
*
Installation:

Place pmgraphviz.php (this file) into your cookbook 
directory (e.g. /srv/www/htdocs/pmwiki/cookbook)

Include the cookbook in your local/config.php 
include_once('cookbook/pmgraphviz.php');

Create a cache directory at pub/pmgraphviz under your
your PmWiki base directory.  Needs to be writable by
the web http server username.

On a page include the markup (the -- is important).
(:pmgraphviz -- [=
   graph {
      "hello" -> "world";
   }
=] :)

You set these variables to override defaults (usually
set inside your config.php before including the recipe):

$PmGraphVizCacheDir = Path to cache dir ($FarmD/pub/pmgraphviz)
$PmGraphVizDotPgm = Path to graphviz dot (/usr/bin/dot)
$PmGraphVizCacheDirUrl = Url to cache dir ($FarmPubUrl/pmgraphviz)
$PmGraphVizConvertPgm = Path to ImageMagick convert (<empty>)

-<0.01: Initial release.

-<0.02: Bug fixes, use Keep, added client side image map handling.

-<0.03: Fixed typo bug. Attempt to standardize cache vars, prefixes added to file names.\
PmGraphVizDotPgm can now be a URL to a remote webdot server \
(e.g. [=http://www.research.att.com/~north/cgi-bin/webdot/webdot.cgi=]). \
PmGraphVizConvertPgm if set to the ImageMagick convert program, it will try to render \
postscript into convert to produce a nice anti-aliased version of the image.  If it fails \
it tries to rerender without using convert.

->Variables:
     [=
     forceregen   If set to true, cache is ignored and files are forced to regenerate. \
 Defaults to false.
     mapname      Set this to the dot graph name if you include URL's for image mapping. \
Must set this to generate client side maps.
     typeext      Used to determine the type of output from dot and the extension (e.g. \
gif, png, jpg). (recipe assumes it is a displayable inline image type)
     =]
***********************************************************/
/* pmgraphviz */
Markup('pmgraphviz','directives',"/^\(:pmgraphviz[ 	]*(.*?) -- (.*?):\)\s*$/e",
	"pmgraphviz('$1','$2')");

function pmgraphviz($opts, $dotcmds) {
	global $KPV, $KeepToken, $FarmD, $PubDirUrl,
		$PubCacheDir, $PubCacheUrl,
		$PmGraphVizCacheDir, $PmGraphVizDotPgm,
		$PmGraphVizCacheDirUrl, $PmGraphVizConvertPgm;


	SDV($PubCacheDir, "pub/cache");
	SDV($PubCacheUrl, "$PubDirUrl/cache");
	SDV($PmGraphVizCacheDir, $PubCacheDir);
	SDV($PmGraphVizCacheDirUrl, $PubCacheUrl);

	SDV($PmGraphVizDotPgm, '/usr/bin/dot');
	// If PmGraphVizConvertPgm is empty, then don't process with convert
	// e.g. set it to /usr/bin/convert if you have one.
	SDV($PmGraphVizConvertPgm, '');

	// Enabling debug will write the dot file to  /tmp/pmgraphviz.out
	$debug=0;
	$convert=0;
	if ($PmGraphVizConvertPgm != '')
		$convert=1;
	// Check to see if we are wanting to use a remote dot server.
	$remote=0;
	$cachedot=0;
	if (preg_match("/^https?:/", $PmGraphVizDotPgm)) {
		$remote=1;
		$cachedot=1;
		$convert=0;
	}

	// Process markup arguments first
	//
	$defaults = array(
		'typeext'=>'png',
		'unsafe'=>'false',
		'overrides'=>'true',
		'mapname'=>'',
		'forceregen'=>'false'
	);

	$args = array_merge($defaults, ParseArgs($opts));
	$urladd='';


	// Allows overrides=false in the :pmgraphviz: markup to disallow
	// settings on the URL line.
	//
	$overrides = $args['overrides'];
	if ($overrides == 'false') {
		$_GET = NULL;
	}

	$typeext = isset($_GET['typeext']) ? $_GET['typeext'] :
		$args['typeext'];
	if (isset($_GET['typeext']))
		$urladd.="&amp;typeext=".urlencode($_GET['typeext']);
	$forceregen = isset($_GET['forceregen']) ? $_GET['forceregen'] :
		$args['forceregen'];
	if (isset($_GET['forceregen']))
		$urladd.="&amp;forceregen=".urlencode($_GET['forceregen']);

	// For unsafe (?) things
	// Like handling creating of arbitrarily named files from the
	// url line.
	$unsafe=$args['unsafe'];
	if ($unsafe == 'true') {
		$mapname = isset($_GET['mapname']) ? $_GET['mapname'] :
			$args['mapname'];
		if (isset($_GET['mapname']))
			$urladd.="&amp;mapname=".urlencode($_GET['mapname']);

	} else {
		$mapname=$args['mapname'];
	}

	$out='';

	// Compute the md5 of the opts plus dotcmds, if we
	// find a match in the cache, then return back
	// the graphic file there.
	//
	// Files may collect there over time.  You may
	// want to create a skulking routine to clean
	// out old entries in order to remove orphans.

    	$dotcmds = preg_replace( "/$KeepToken(\\d+?)$KeepToken/e", '$KPV[$1]', $dotcmds);
   	$dotcmds = preg_replace( "/&gt;/", ">", $dotcmds);
   	$dotcmds = preg_replace( "/&lt;/", "<", $dotcmds);
 
	$checkmd5 = md5("$opts"."$dotcmds");
	$filename = "pmgv-".$checkmd5.".".$typeext;


	// Write dot file if cachedot enabled. Done automatically for remote.
	//
	if ($cachedot) {
		$dotname = "pmgv-".$checkmd5.".dot";
		if ($f = fopen($PmGraphVizCacheDir."/".$dotname, "w")) {
			fputs($f, "$dotcmds");
			pclose($f);
		}
	}

	// Handle remote webdot server separately (no caching done).
	//
	if ($remote) {
		$out.="<img border=\"0\" src=\"".$PmGraphVizDotPgm."/".$PmGraphVizCacheDirUrl."/".$dotname.".dot.".$typeext."\" alt=\"PmGraphViz\" \>";
		return Keep($out);
	}
	// End remote

	if ($forceregen != 'true' && is_readable($PmGraphVizCacheDir."/".$filename)) {
		$out.="<img border=\"0\" src=\"".$PmGraphVizCacheDirUrl."/".$filename."\" alt=\"PmGraphViz\" ";
		if ($mapname != '') 
			$out.=" usemap=\"#".$mapname."\" ismap=\"ismap\"";
		$out.=" />";
		if ($mapname != '') {
			$html_map=file_get_contents($PmGraphVizCacheDir."/".$filename."-map");
			$out.="\n".$html_map;
		}
		return Keep($out);
	}
	// If we didn't return there, then we regen
	// the graphic

	// Produce client side image map
	//
	if ($pf = popen("$PmGraphVizDotPgm -Tcmapx -o $PmGraphVizCacheDir/$filename-map 2>&1",'wb')) {
		fputs($pf, "$dotcmds");
		pclose($pf);
	}

	// Produce image via local dot
	//
	$html_map=file_get_contents($PmGraphVizCacheDir."/".$filename."-map");
	while (1) {
		if ($convert) {
			$dotpgmline="$PmGraphVizDotPgm -Tps | $PmGraphVizConvertPgm -density 96 - $PmGraphVizCacheDir/$filename 2>/tmp/pmgraphviz.out";
		} else {
			$dotpgmline="$PmGraphVizDotPgm -T $typeext -o $PmGraphVizCacheDir/$filename 2>&1";
		}
		if ($pf = popen($dotpgmline,'wb')) {
			fputs($pf, "$dotcmds");
			pclose($pf);
			if (is_readable($PmGraphVizCacheDir."/".$filename)) {
				$out.="<img border=\"0\" src=\"".$PmGraphVizCacheDirUrl."/".$filename."\" alt=\"PmGraphViz\" ";
				if ($mapname != '') 
					$out.=" usemap=\"#".$mapname."\" ismap=\"ismap\"";
				$out.=" />";
				if ($mapname != '') {
					$html_map=file_get_contents($PmGraphVizCacheDir."/".$filename."-map");
					$out.="\n".$html_map;
				}
				return Keep($out);
			} else {
				if ($convert) {
					$convert=0;
					continue;
				}
				return "!! pmgraphvis: Error\n";
			}
		}
	}
	// don't want to make it here.
	return "!! pmgraphvis: How did I get here?\n";
}

?>