Subversion-Projekte lars-tiefland.php_share

Revision

Details | Letzte Änderung | Log anzeigen | RSS feed

Revision Autor Zeilennr. Zeile
1 lars 1
<?php
2
 
3
/* vim: set expandtab tabstop=4 shiftwidth=4 softtabstop=4: */
4
 
5
/**
6
 * Image_GraphViz
7
 *
8
 * PHP version 4 and 5
9
 *
10
 * Copyright (c) 2001-2007, Dr. Volker Göbbels <vmg@arachnion.de> and
11
 * Sebastian Bergmann <sb@sebastian-bergmann.de>. All rights reserved.
12
 *
13
 * LICENSE: This source file is subject to version 3.0 of the PHP license
14
 * that is available through the world-wide-web at the following URI:
15
 * http://www.php.net/license/3_0.txt.  If you did not receive a copy of
16
 * the PHP License and are unable to obtain it through the web, please
17
 * send a note to license@php.net so we can mail you a copy immediately.
18
 *
19
 * @category  Image
20
 * @package   Image_GraphViz
21
 * @author    Dr. Volker Göbbels <vmg@arachnion.de>
22
 * @author    Sebastian Bergmann <sb@sebastian-bergmann.de>
23
 * @author    Karsten Dambekalns <k.dambekalns@fishfarm.de>
24
 * @author    Michael Lively Jr. <mlively@ft11.net>
25
 * @author    Philippe Jausions <Philippe.Jausions@11abacus.com>
26
 * @copyright 2001-2007 Dr. Volker Göbbels <vmg@arachnion.de> and Sebastian Bergmann <sb@sebastian-bergmann.de>
27
 * @license   http://www.php.net/license/3_0.txt  PHP License 3.0
28
 * @version   CVS: $Id: GraphViz.php 304688 2010-10-24 05:21:17Z clockwerx $
29
 * @link      http://pear.php.net/package/Image_GraphViz
30
 * @link      http://www.graphviz.org/
31
 * @since     File available since Release 0.1.0
32
 */
33
 
34
/**
35
 * Required PEAR classes
36
 */
37
require_once 'System.php';
38
 
39
/**
40
 * Interface to AT&T's GraphViz tools.
41
 *
42
 * The GraphViz class allows for the creation of and to work with directed
43
 * and undirected graphs and their visualization with AT&T's GraphViz tools.
44
 *
45
 * <code>
46
 * <?php
47
 * require_once 'Image/GraphViz.php';
48
 *
49
 * $graph = new Image_GraphViz();
50
 *
51
 * $graph->addNode(
52
 *   'Node1',
53
 *   array(
54
 *     'URL'   => 'http://link1',
55
 *     'label' => 'This is a label',
56
 *     'shape' => 'box'
57
 *   )
58
 * );
59
 *
60
 * $graph->addNode(
61
 *   'Node2',
62
 *   array(
63
 *     'URL'      => 'http://link2',
64
 *     'fontsize' => '14'
65
 *   )
66
 * );
67
 *
68
 * $graph->addNode(
69
 *   'Node3',
70
 *   array(
71
 *     'URL'      => 'http://link3',
72
 *     'fontsize' => '20'
73
 *   )
74
 * );
75
 *
76
 * $graph->addEdge(
77
 *   array(
78
 *     'Node1' => 'Node2'
79
 *   ),
80
 *   array(
81
 *     'label' => 'Edge Label'
82
 *   )
83
 * );
84
 *
85
 * $graph->addEdge(
86
 *   array(
87
 *     'Node1' => 'Node2'
88
 *   ),
89
 *   array(
90
 *     'color' => 'red'
91
 *   )
92
 * );
93
 *
94
 * $graph->image();
95
 * ?>
96
 * </code>
97
 *
98
 * @category  Image
99
 * @package   Image_GraphViz
100
 * @author    Sebastian Bergmann <sb@sebastian-bergmann.de>
101
 * @author    Dr. Volker Göbbels <vmg@arachnion.de>
102
 * @author    Karsten Dambekalns <k.dambekalns@fishfarm.de>
103
 * @author    Michael Lively Jr. <mlively@ft11.net>
104
 * @author    Philippe Jausions <Philippe.Jausions@11abacus.com>
105
 * @copyright 2001-2007 Dr. Volker Göbbels <vmg@arachnion.de> and Sebastian Bergmann <sb@sebastian-bergmann.de>
106
 * @license   http://www.php.net/license/3_0.txt The PHP License, Version 3.0
107
 * @version   Release: @package_version@
108
 * @link      http://pear.php.net/package/Image_GraphViz
109
 * @link      http://www.graphviz.org/
110
 * @since     Class available since Release 0.1
111
 */
112
class Image_GraphViz
113
{
114
    /**
115
     * Base path to GraphViz commands
116
     *
117
     * @var string
118
     */
119
    var $binPath = '';
120
 
121
    /**
122
     * Path to GraphViz/dot command
123
     *
124
     * @var string
125
     */
126
    var $dotCommand = 'dot';
127
 
128
    /**
129
     * Path to GraphViz/neato command
130
     *
131
     * @var string
132
     */
133
    var $neatoCommand = 'neato';
134
 
135
    /**
136
     * Representation of the graph
137
     *
138
     * @var array
139
     */
140
    var $graph = array('edgesFrom'  => array(),
141
                       'nodes'      => array(),
142
                       'attributes' => array(),
143
                       'directed'   => true,
144
                       'clusters'   => array(),
145
                       'subgraphs'  => array(),
146
                       'name'       => 'G',
147
                       'strict'     => true,
148
                      );
149
 
150
    /**
151
     * Whether to return PEAR_Error instance on failures instead of FALSE
152
     *
153
     * @var boolean
154
     * @access protected
155
     */
156
    var $_returnFalseOnError = true;
157
 
158
    /**
159
     * Constructor.
160
     *
161
     * Setting the name of the Graph is useful for including multiple image
162
     * maps on one page. If not set, the graph will be named 'G'.
163
     *
164
     * @param boolean $directed    Directed (TRUE) or undirected (FALSE) graph.
165
     *                             Note: You MUST pass a boolean, and not just
166
     *                             an  expression that evaluates to TRUE or
167
     *                             FALSE (i.e. NULL, empty string, 0 will NOT
168
     *                             work)
169
     * @param array   $attributes  Attributes of the graph
170
     * @param string  $name        Name of the Graph
171
     * @param boolean $strict      Whether to collapse multiple edges between
172
     *                             same nodes
173
     * @param boolean $returnError Set to TRUE to return PEAR_Error instances
174
     *                             on failures instead of FALSE
175
     *
176
     * @access public
177
     */
178
    function Image_GraphViz($directed = true, $attributes = array(),
179
                            $name = 'G', $strict = true, $returnError = false)
180
    {
181
        $this->setDirected($directed);
182
        $this->setAttributes($attributes);
183
        $this->graph['name']   = $name;
184
        $this->graph['strict'] = (boolean)$strict;
185
 
186
        $this->_returnFalseOnError = !$returnError;
187
    }
188
 
189
    /**
190
     * Outputs image of the graph in a given format
191
     *
192
     * This methods send HTTP headers
193
     *
194
     * @param string $format  Format of the output image. This may be one
195
     *                        of the formats supported by GraphViz.
196
     * @param string $command "dot" or "neato"
197
     *
198
     * @return boolean TRUE on success, FALSE or PEAR_Error otherwise
199
     * @access public
200
     */
201
    function image($format = 'svg', $command = null)
202
    {
203
        $file = $this->saveParsedGraph();
204
        if (!$file || PEAR::isError($file)) {
205
            return $file;
206
        }
207
 
208
        $outputfile = $file . '.' . $format;
209
 
210
        $rendered = $this->renderDotFile($file, $outputfile, $format,
211
                                         $command);
212
        if ($rendered !== true) {
213
            return $rendered;
214
        }
215
 
216
        $sendContentLengthHeader = true;
217
 
218
        switch (strtolower($format)) {
219
        case 'gif':
220
        case 'png':
221
        case 'bmp':
222
        case 'jpeg':
223
        case 'tiff':
224
            header('Content-Type: image/' . $format);
225
            break;
226
 
227
        case 'tif':
228
            header('Content-Type: image/tiff');
229
            break;
230
 
231
        case 'jpg':
232
            header('Content-Type: image/jpeg');
233
            break;
234
 
235
        case 'ico':
236
            header('Content-Type: image/x-icon');
237
            break;
238
 
239
        case 'wbmp':
240
            header('Content-Type: image/vnd.wap.wbmp');
241
            break;
242
 
243
        case 'pdf':
244
            header('Content-Type: application/pdf');
245
            break;
246
 
247
        case 'mif':
248
            header('Content-Type: application/vnd.mif');
249
            break;
250
 
251
        case 'vrml':
252
            header('Content-Type: application/x-vrml');
253
            break;
254
 
255
        case 'svg':
256
            header('Content-Type: image/svg+xml');
257
            break;
258
 
259
        case 'plain':
260
        case 'plain-ext':
261
            header('Content-Type: text/plain');
262
            break;
263
 
264
        default:
265
            header('Content-Type: application/octet-stream');
266
            $sendContentLengthHeader = false;
267
        }
268
 
269
        if ($sendContentLengthHeader) {
270
            header('Content-Length: ' . filesize($outputfile));
271
        }
272
 
273
        $return = true;
274
        if (readfile($outputfile) === false) {
275
            $return = false;
276
        }
277
        @unlink($outputfile);
278
 
279
        return $return;
280
    }
281
 
282
    /**
283
     * Returns image (data) of the graph in a given format.
284
     *
285
     * @param string $format  Format of the output image. This may be one
286
     *                        of the formats supported by GraphViz.
287
     * @param string $command "dot" or "neato"
288
     *
289
     * @return string The image (data) created by GraphViz, FALSE or PEAR_Error
290
     *                on error
291
     * @access public
292
     * @since  Method available since Release 1.1.0
293
     */
294
    function fetch($format = 'svg', $command = null)
295
    {
296
        $file = $this->saveParsedGraph();
297
        if (!$file || PEAR::isError($file)) {
298
            return $file;
299
        }
300
 
301
        $outputfile = $file . '.' . $format;
302
 
303
        $rendered = $this->renderDotFile($file, $outputfile, $format,
304
                                         $command);
305
        if ($rendered !== true) {
306
            return $rendered;
307
        }
308
 
309
        @unlink($file);
310
 
311
        $fp = fopen($outputfile, 'rb');
312
 
313
        if (!$fp) {
314
            if ($this->_returnFalseOnError) {
315
                return false;
316
            }
317
            $error = PEAR::raiseError('Could not read rendered file');
318
            return $error;
319
        }
320
 
321
        $data = fread($fp, filesize($outputfile));
322
        fclose($fp);
323
        @unlink($outputfile);
324
 
325
        return $data;
326
    }
327
 
328
    /**
329
     * Renders a given dot file into a given format.
330
     *
331
     * @param string $dotfile    The absolute path of the dot file to use.
332
     * @param string $outputfile The absolute path of the file to save to.
333
     * @param string $format     Format of the output image. This may be one
334
     *                           of the formats supported by GraphViz.
335
     * @param string $command    "dot" or "neato"
336
     *
337
     * @return boolean TRUE if the file was saved, FALSE or PEAR_Error
338
     *                 otherwise.
339
     * @access public
340
     */
341
    function renderDotFile($dotfile, $outputfile, $format = 'svg',
342
                           $command = null)
343
    {
344
        if (!file_exists($dotfile)) {
345
            if ($this->_returnFalseOnError) {
346
                return false;
347
            }
348
            $error = PEAR::raiseError('Could not find dot file');
349
            return $error;
350
        }
351
 
352
        $oldmtime = file_exists($outputfile) ? filemtime($outputfile) : 0;
353
 
354
        switch ($command) {
355
        case 'dot':
356
        case 'neato':
357
            break;
358
        default:
359
            $command = $this->graph['directed'] ? 'dot' : 'neato';
360
        }
361
        $command_orig = $command;
362
 
363
        $command = $this->binPath.(($command == 'dot') ? $this->dotCommand
364
                                                       : $this->neatoCommand);
365
 
366
        $command .= ' -T'.escapeshellarg($format)
367
                    .' -o'.escapeshellarg($outputfile)
368
                    .' '.escapeshellarg($dotfile)
369
                    .' 2>&1';
370
        exec($command, $msg, $return_val);
371
 
372
        clearstatcache();
373
        if (file_exists($outputfile) && filemtime($outputfile) > $oldmtime
374
            && $return_val == 0) {
375
            return true;
376
        } elseif ($this->_returnFalseOnError) {
377
            return false;
378
        }
379
        $error = PEAR::raiseError($command_orig.' command failed: '
380
                                  .implode("\n", $msg));
381
        return $error;
382
    }
383
 
384
    /**
385
     * Adds a cluster to the graph.
386
     *
387
     * A cluster is a subgraph with a rectangle around it.
388
     *
389
     * @param string $id         ID.
390
     * @param array  $title      Title.
391
     * @param array  $attributes Attributes of the cluster.
392
     * @param string $group      ID of group to nest cluster into
393
     *
394
     * @return void
395
     * @access public
396
     * @see    addSubgraph()
397
     */
398
    function addCluster($id, $title, $attributes = array(), $group = 'default')
399
    {
400
        $this->graph['clusters'][$id]['title']      = $title;
401
        $this->graph['clusters'][$id]['attributes'] = $attributes;
402
        $this->graph['clusters'][$id]['embedIn']    = $group;
403
    }
404
 
405
    /**
406
     * Adds a subgraph to the graph.
407
     *
408
     * @param string $id         ID.
409
     * @param array  $title      Title.
410
     * @param array  $attributes Attributes of the cluster.
411
     * @param string $group      ID of group to nest subgraph into
412
     *
413
     * @return void
414
     * @access public
415
     */
416
    function addSubgraph($id, $title, $attributes = array(), $group = 'default')
417
    {
418
        $this->graph['subgraphs'][$id]['title']      = $title;
419
        $this->graph['subgraphs'][$id]['attributes'] = $attributes;
420
        $this->graph['subgraphs'][$id]['embedIn']    = $group;
421
    }
422
 
423
    /**
424
     * Adds a note to the graph.
425
     *
426
     * @param string $name       Name of the node.
427
     * @param array  $attributes Attributes of the node.
428
     * @param string $group      Group of the node.
429
     *
430
     * @return void
431
     * @access public
432
     */
433
    function addNode($name, $attributes = array(), $group = 'default')
434
    {
435
        $this->graph['nodes'][$group][$name] = $attributes;
436
    }
437
 
438
    /**
439
     * Removes a node from the graph.
440
     *
441
     * This method doesn't remove edges associated with the node.
442
     *
443
     * @param string $name  Name of the node to be removed.
444
     * @param string $group Group of the node.
445
     *
446
     * @return void
447
     * @access public
448
     */
449
    function removeNode($name, $group = 'default')
450
    {
451
        if (isset($this->graph['nodes'][$group][$name])) {
452
            unset($this->graph['nodes'][$group][$name]);
453
        }
454
    }
455
 
456
    /**
457
     * Adds an edge to the graph.
458
     *
459
     * Examples:
460
     * <code>
461
     * $g->addEdge(array('node1' => 'node2'));
462
     * $attr = array(
463
     *     'label' => '+1',
464
     *     'style' => 'dashed',
465
     * );
466
     * $g->addEdge(array('node3' => 'node4'), $attr);
467
     *
468
     * // With port specification
469
     * $g->addEdge(array('node5' => 'node6'), $attr, array('node6' => 'portA'));
470
     * $g->addEdge(array('node7' => 'node8'), null, array('node7' => 'portC',
471
     *                                                    'node8' => 'portD'));
472
     * </code>
473
     *
474
     * @param array $edge       Start => End node of the edge.
475
     * @param array $attributes Attributes of the edge.
476
     * @param array $ports      Start node => port, End node => port
477
     *
478
     * @return integer an edge ID that can be used with {@link removeEdge()}
479
     * @access public
480
     */
481
    function addEdge($edge, $attributes = array(), $ports = array())
482
    {
483
        if (!is_array($edge)) {
484
            return;
485
        }
486
 
487
        $from = key($edge);
488
        $to   = $edge[$from];
489
        $info = array();
490
 
491
        if (is_array($ports)) {
492
            if (array_key_exists($from, $ports)) {
493
                $info['portFrom'] = $ports[$from];
494
            }
495
 
496
            if (array_key_exists($to, $ports)) {
497
                $info['portTo'] = $ports[$to];
498
            }
499
        }
500
 
501
        if (is_array($attributes)) {
502
            $info['attributes'] = $attributes;
503
        }
504
 
505
        if (!empty($this->graph['strict'])) {
506
            if (!isset($this->graph['edgesFrom'][$from][$to][0])) {
507
                $this->graph['edgesFrom'][$from][$to][0] = $info;
508
            } else {
509
                $this->graph['edgesFrom'][$from][$to][0] = array_merge($this->graph['edgesFrom'][$from][$to][0], $info);
510
            }
511
        } else {
512
            $this->graph['edgesFrom'][$from][$to][] = $info;
513
        }
514
 
515
        return count($this->graph['edgesFrom'][$from][$to]) - 1;
516
    }
517
 
518
    /**
519
     * Removes an edge from the graph.
520
     *
521
     * @param array   $edge Start and End node of the edge to be removed.
522
     * @param integer $id   specific edge ID (only usefull when multiple edges
523
     *                      exist between the same 2 nodes)
524
     *
525
     * @return void
526
     * @access public
527
     */
528
    function removeEdge($edge, $id = null)
529
    {
530
        if (!is_array($edge)) {
531
            return;
532
        }
533
 
534
        $from = key($edge);
535
        $to   = $edge[$from];
536
 
537
        if (!is_null($id)) {
538
            if (isset($this->graph['edgesFrom'][$from][$to][$id])) {
539
                unset($this->graph['edgesFrom'][$from][$to][$id]);
540
 
541
                if (count($this->graph['edgesFrom'][$from][$to]) == 0) {
542
                    unset($this->graph['edgesFrom'][$from][$to]);
543
                }
544
            }
545
        } elseif (isset($this->graph['edgesFrom'][$from][$to])) {
546
            unset($this->graph['edgesFrom'][$from][$to]);
547
        }
548
    }
549
 
550
    /**
551
     * Adds attributes to the graph.
552
     *
553
     * @param array $attributes Attributes to be added to the graph.
554
     *
555
     * @return void
556
     * @access public
557
     */
558
    function addAttributes($attributes)
559
    {
560
        if (is_array($attributes)) {
561
            $this->graph['attributes'] = array_merge($this->graph['attributes'], $attributes);
562
        }
563
    }
564
 
565
    /**
566
     * Sets attributes of the graph.
567
     *
568
     * @param array $attributes Attributes to be set for the graph.
569
     *
570
     * @return void
571
     * @access public
572
     */
573
    function setAttributes($attributes)
574
    {
575
        if (is_array($attributes)) {
576
            $this->graph['attributes'] = $attributes;
577
        }
578
    }
579
 
580
    /**
581
     * Escapes an (attribute) array
582
     *
583
     * Detects if an attribute is <html>, contains double-quotes, etc...
584
     *
585
     * @param array $input input to escape
586
     *
587
     * @return array input escaped
588
     * @access protected
589
     */
590
    function _escapeArray($input)
591
    {
592
        $output = array();
593
 
594
        foreach ((array)$input as $k => $v) {
595
            switch ($k) {
596
            case 'label':
597
            case 'headlabel':
598
            case 'taillabel':
599
                $v = $this->_escape($v, true);
600
                break;
601
            default:
602
                $v = $this->_escape($v);
603
                $k = $this->_escape($k);
604
            }
605
 
606
            $output[$k] = $v;
607
        }
608
 
609
        return $output;
610
    }
611
 
612
    /**
613
     * Returns a safe "ID" in DOT syntax
614
     *
615
     * @param string  $input string to use as "ID"
616
     * @param boolean $html  whether to attempt detecting HTML-like content
617
     *
618
     * @return string
619
     * @access protected
620
     */
621
    function _escape($input, $html = false)
622
    {
623
        switch (strtolower($input)) {
624
        case 'node':
625
        case 'edge':
626
        case 'graph':
627
        case 'digraph':
628
        case 'subgraph':
629
        case 'strict':
630
            return '"'.$input.'"';
631
        }
632
 
633
        if (is_bool($input)) {
634
            return ($input) ? 'true' : 'false';
635
        }
636
 
637
        if ($html && (strpos($input, '</') !== false
638
                      || strpos($input, '/>') !== false)) {
639
            return '<'.$input.'>';
640
        }
641
 
642
        if (preg_match('/^([a-z_][a-z_0-9]*|-?(\.[0-9]+|[0-9]+(\.[0-9]*)?))$/i',
643
                       $input)) {
644
            return $input;
645
        }
646
 
647
        return '"'.str_replace(array("\r\n", "\n", "\r", '"'),
648
                               array('\n',   '\n', '\n', '\"'), $input).'"';
649
    }
650
 
651
    /**
652
     * Sets directed/undirected flag for the graph.
653
     *
654
     * Note: You MUST pass a boolean, and not just an expression that evaluates
655
     *       to TRUE or FALSE (i.e. NULL, empty string, 0 will not work)
656
     *
657
     * @param boolean $directed Directed (TRUE) or undirected (FALSE) graph.
658
     *
659
     * @return void
660
     * @access public
661
     */
662
    function setDirected($directed)
663
    {
664
        if (is_bool($directed)) {
665
            $this->graph['directed'] = $directed;
666
        }
667
    }
668
 
669
    /**
670
     * Loads a graph from a file in Image_GraphViz format
671
     *
672
     * @param string $file File to load graph from.
673
     *
674
     * @return void
675
     * @access public
676
     */
677
    function load($file)
678
    {
679
        if ($serializedGraph = implode('', @file($file))) {
680
            $g = unserialize($serializedGraph);
681
 
682
            if (!is_array($g)) {
683
                return;
684
            }
685
 
686
            // Convert old storage format to new one
687
            $defaults = array('edgesFrom'  => array(),
688
                              'nodes'      => array(),
689
                              'attributes' => array(),
690
                              'directed'   => true,
691
                              'clusters'   => array(),
692
                              'subgraphs'  => array(),
693
                              'name'       => 'G',
694
                              'strict'     => true,
695
                        );
696
 
697
            $this->graph = array_merge($defaults, $g);
698
 
699
            if (isset($this->graph['edges'])) {
700
                foreach ($this->graph['edges'] as $id => $nodes) {
701
                    $attr = (isset($this->graph['edgeAttributes'][$id]))
702
                            ? $this->graph['edgeAttributes'][$id]
703
                            : array();
704
 
705
                    $this->addEdge($nodes, $attr);
706
                }
707
 
708
                unset($this->graph['edges']);
709
                unset($this->graph['edgeAttributes']);
710
            }
711
        }
712
    }
713
 
714
    /**
715
     * Save graph to file in Image_GraphViz format
716
     *
717
     * This saves the serialized version of the instance, not the
718
     * rendered graph.
719
     *
720
     * @param string $file File to save the graph to.
721
     *
722
     * @return string File the graph was saved to, FALSE or PEAR_Error on
723
     *                failure.
724
     * @access public
725
     */
726
    function save($file = '')
727
    {
728
        $serializedGraph = serialize($this->graph);
729
 
730
        if (empty($file)) {
731
            $file = System::mktemp('graph_');
732
        }
733
 
734
        if ($fp = @fopen($file, 'wb')) {
735
            @fputs($fp, $serializedGraph);
736
            @fclose($fp);
737
 
738
            return $file;
739
        }
740
 
741
        if ($this->_returnFalseOnError) {
742
            return false;
743
        }
744
        $error = PEAR::raiseError('Could not save serialized graph instance');
745
        return $error;
746
    }
747
 
748
    /**
749
     * Returns a list of sub-groups for a given parent group
750
     *
751
     * @param string $parent Group ID
752
     *
753
     * @return array list of group IDs
754
     * @access protected
755
     */
756
    function _getSubgraphs($parent)
757
    {
758
        $subgraphs = array();
759
        foreach ($this->graph['clusters'] as $id => $info) {
760
            if ($info['embedIn'] === $parent) {
761
                $subgraphs[] = $id;
762
            }
763
        }
764
        foreach ($this->graph['subgraphs'] as $id => $info) {
765
            if ($info['embedIn'] === $parent) {
766
                $subgraphs[] = $id;
767
            }
768
        }
769
        return $subgraphs;
770
    }
771
 
772
    /**
773
     * Returns a list of cluster/subgraph IDs
774
     *
775
     * @return array
776
     * @access protected
777
     */
778
    function _getGroups()
779
    {
780
        $groups = array_merge(array_keys($this->graph['clusters']),
781
                              array_keys($this->graph['subgraphs']));
782
        return array_unique($groups);
783
    }
784
 
785
    /**
786
     * Returns a list of top groups
787
     *
788
     * @return array
789
     * @access protected
790
     */
791
    function _getTopGraphs()
792
    {
793
        $top = array();
794
        $groups = $this->_getGroups();
795
 
796
        foreach ($groups as $id) {
797
            $isTop = ($id === 'default');
798
            if (isset($this->graph['clusters'][$id])
799
                && $this->graph['clusters'][$id]['embedIn'] === 'default') {
800
                $isTop = true;
801
            }
802
            if (isset($this->graph['subgraphs'][$id])
803
                && $this->graph['subgraphs'][$id]['embedIn'] === 'default') {
804
                $isTop = true;
805
            }
806
            if ($isTop) {
807
                $top[] = $id;
808
            }
809
        }
810
 
811
        return array_unique($top);
812
    }
813
 
814
    /**
815
     * Parses the graph into GraphViz markup.
816
     *
817
     * @return string GraphViz markup
818
     * @access public
819
     */
820
    function parse()
821
    {
822
        $parsedGraph  = (empty($this->graph['strict'])) ? '' : 'strict ';
823
        $parsedGraph .= (empty($this->graph['directed'])) ? 'graph ' : 'digraph ';
824
        $parsedGraph .= $this->_escape($this->graph['name'])." {\n";
825
 
826
        $indent = '    ';
827
 
828
        $attr = $this->_escapeArray($this->graph['attributes']);
829
 
830
        foreach ($attr as $key => $value) {
831
            $parsedGraph .= $indent.$key.'='.$value.";\n";
832
        }
833
 
834
        $groups = $this->_getGroups();
835
        foreach ($this->graph['nodes'] as $group => $nodes) {
836
            if (!in_array($group, $groups)) {
837
                $parsedGraph .= $this->_nodes($nodes, $indent);
838
            }
839
        }
840
        $tops = $this->_getTopGraphs();
841
        foreach ($tops as $group) {
842
            $parsedGraph .= $this->_subgraph($group, $indent);
843
        }
844
 
845
        if (!empty($this->graph['directed'])) {
846
            $separator = ' -> ';
847
        } else {
848
            $separator = ' -- ';
849
        }
850
 
851
        foreach ($this->graph['edgesFrom'] as $from => $toNodes) {
852
            $from = $this->_escape($from);
853
 
854
            foreach ($toNodes as $to => $edges) {
855
                $to = $this->_escape($to);
856
 
857
                foreach ($edges as $info) {
858
                    $f = $from;
859
                    $t = $to;
860
 
861
                    if (array_key_exists('portFrom', $info)) {
862
                        $f .= ':'.$this->_escape($info['portFrom']);
863
                    }
864
 
865
                    if (array_key_exists('portTo', $info)) {
866
                        $t .= ':'.$this->_escape($info['portTo']);
867
                    }
868
 
869
                    $parsedGraph .= $indent.$f.$separator.$t;
870
 
871
                    if (!empty($info['attributes'])) {
872
                        $attributeList = array();
873
 
874
                        foreach ($this->_escapeArray($info['attributes']) as $key => $value) {
875
                            switch ($key) {
876
                            case 'lhead':
877
                            case 'ltail':
878
                                if (strncasecmp($value, 'cluster', 7)) {
879
                                    $value = 'cluster_'.$value;
880
                                }
881
                                break;
882
                            }
883
                            $attributeList[] = $key.'='.$value;
884
                        }
885
 
886
                        $parsedGraph .= ' [ '.implode(',', $attributeList).' ]';
887
                    }
888
 
889
                    $parsedGraph .= ";\n";
890
                }
891
            }
892
        }
893
 
894
        return $parsedGraph . "}\n";
895
    }
896
 
897
    /**
898
     * Output nodes
899
     *
900
     * @param array  $nodes  nodes list
901
     * @param string $indent space indentation
902
     *
903
     * @return string output
904
     * @access protected
905
     */
906
    function _nodes($nodes, $indent)
907
    {
908
        $parsedGraph = '';
909
        foreach ($nodes as $node => $attributes) {
910
            $parsedGraph .= $indent.$this->_escape($node);
911
 
912
            $attributeList = array();
913
 
914
            foreach ($this->_escapeArray($attributes) as $key => $value) {
915
                $attributeList[] = $key.'='.$value;
916
            }
917
 
918
            if (!empty($attributeList)) {
919
                $parsedGraph .= ' [ '.implode(',', $attributeList).' ]';
920
            }
921
 
922
            $parsedGraph .= ";\n";
923
        }
924
        return $parsedGraph;
925
    }
926
 
927
    /**
928
     * Generates output for a group
929
     *
930
     * @return string output
931
     * @access protected
932
     */
933
    function _subgraph($group, &$indent)
934
    {
935
        $parsedGraph = '';
936
        $nodes = $this->graph['nodes'][$group];
937
 
938
        if ($group !== 'default') {
939
            $type = null;
940
            $_group = $this->_escape($group);
941
 
942
            if (isset($this->graph['clusters'][$group])) {
943
                $type = 'clusters';
944
                if (strncasecmp($group, 'cluster', 7)) {
945
                    $_group = $this->_escape('cluster_'.$group);
946
                }
947
            } elseif (isset($this->graph['subgraphs'][$group])) {
948
                $type = 'subgraphs';
949
            }
950
            $parsedGraph .= $indent.'subgraph '.$_group." {\n";
951
 
952
            $indent .= '    ';
953
 
954
            if ($type !== null && isset($this->graph[$type][$group])) {
955
                $cluster = $this->graph[$type][$group];
956
                $_attr    = $this->_escapeArray($cluster['attributes']);
957
 
958
                $attr = array();
959
                foreach ($_attr as $key => $value) {
960
                    $attr[] = $key.'='.$value;
961
                }
962
 
963
                if (strlen($cluster['title'])) {
964
                    $attr[] = 'label='
965
                              .$this->_escape($cluster['title'], true);
966
                }
967
 
968
                if ($attr) {
969
                    $parsedGraph .= $indent.'graph [ '.implode(',', $attr)
970
                                    ." ];\n";
971
                }
972
            }
973
        }
974
 
975
        $parsedGraph .= $this->_nodes($nodes, $indent);
976
 
977
        foreach ($this->_getSubgraphs($group) as $_group) {
978
            $parsedGraph .= $this->_subgraph($_group, $indent);
979
        }
980
 
981
        if ($group !== 'default') {
982
            $indent = substr($indent, 0, -4);
983
 
984
            $parsedGraph .= $indent."}\n";
985
        }
986
 
987
        return $parsedGraph;
988
    }
989
 
990
    /**
991
     * Saves GraphViz markup to file (in DOT language)
992
     *
993
     * @param string $file File to write the GraphViz markup to.
994
     *
995
     * @return string File to which the GraphViz markup was written, FALSE or
996
     *                or PEAR_Error on failure.
997
     * @access public
998
     */
999
    function saveParsedGraph($file = '')
1000
    {
1001
        $parsedGraph = $this->parse();
1002
 
1003
        if (!empty($parsedGraph)) {
1004
            if (empty($file)) {
1005
                $file = System::mktemp('graph_');
1006
            }
1007
 
1008
            if ($fp = @fopen($file, 'wb')) {
1009
                @fputs($fp, $parsedGraph);
1010
                @fclose($fp);
1011
 
1012
                return $file;
1013
            }
1014
        }
1015
 
1016
        if ($this->_returnFalseOnError) {
1017
            return false;
1018
        }
1019
        $error = PEAR::raiseError('Could not save graph');
1020
        return $error;
1021
    }
1022
}
1023
 
1024
/*
1025
 * Local variables:
1026
 * tab-width: 4
1027
 * c-basic-offset: 4
1028
 * c-hanging-comment-ender-p: nil
1029
 * End:
1030
 */
1031
?>