diff --git a/CHANGELOG.md b/CHANGELOG.md index aee4564e..a85ada36 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -8,6 +8,7 @@ you spot any mistakes. * BC break: `Graph::createVertices()` now returns an array of vertices instead of the chainable `Graph` (#19) * Feature: `Graph::createVertices()` now also accepts an array of vertex IDs (#19) +* Fix: Various issues with `Vertex`/`Edge` layout attributes (#32) ## 0.5.0 (2013-05-07) diff --git a/lib/Fhaculty/Graph/GraphViz.php b/lib/Fhaculty/Graph/GraphViz.php index 3f1f5cc4..cedb8a25 100644 --- a/lib/Fhaculty/Graph/GraphViz.php +++ b/lib/Fhaculty/Graph/GraphViz.php @@ -5,6 +5,7 @@ use Fhaculty\Graph\Algorithm\Groups; use Fhaculty\Graph\Exception\UnexpectedValueException; use Fhaculty\Graph\Exception\InvalidArgumentException; +use Fhaculty\Graph\Edge\Base as Edge; use \stdClass; class GraphViz @@ -34,6 +35,14 @@ class GraphViz */ private $executable = 'dot'; + /** + * string to use as indentation for dot output + * + * @var string + * @see GraphViz::createScript() + */ + private $formatIndent = ' '; + const DELAY_OPEN = 2.0; const EOL = PHP_EOL; @@ -273,13 +282,13 @@ public function createScript() // add global attributes $layout = $this->graph->getLayout(); if ($layout) { - $script .= ' graph ' . $this->escapeAttributes($layout) . self::EOL; + $script .= $this->formatIndent . 'graph ' . $this->escapeAttributes($layout) . self::EOL; } if ($this->layoutVertex) { - $script .= ' node ' . $this->escapeAttributes($this->layoutVertex) . self::EOL; + $script .= $this->formatIndent . 'node ' . $this->escapeAttributes($this->layoutVertex) . self::EOL; } if ($this->layoutEdge) { - $script .= ' edge ' . $this->escapeAttributes($this->layoutEdge) . self::EOL; + $script .= $this->formatIndent . 'edge ' . $this->escapeAttributes($this->layoutEdge) . self::EOL; } $alg = new Groups($this->graph); @@ -288,25 +297,15 @@ public function createScript() if ($showGroups) { $gid = 0; + $indent = str_repeat($this->formatIndent, 2); // put each group of vertices in a separate subgraph cluster foreach ($alg->getGroups() as $group) { - $script .= ' subgraph cluster_' . $gid++ . ' {' . self::EOL . - ' label = ' . $this->escape($group) . self::EOL; + $script .= $this->formatIndent . 'subgraph cluster_' . $gid++ . ' {' . self::EOL . + $indent . 'label = ' . $this->escape($group) . self::EOL; foreach($alg->getVerticesGroup($group) as $vid => $vertex) { - $layout = $vertex->getLayout(); - - $balance = $vertex->getBalance(); - if($balance !== NULL){ - if($balance > 0){ - $balance = '+' . $balance; - } - if(!isset($layout['label'])){ - $layout['label'] = $vid; - } - $layout['label'] .= ' (' . $balance . ')'; - } + $layout = $this->getLayoutVertex($vertex); - $script .= ' ' . $this->escapeId($vid); + $script .= $indent . $this->escapeId($vid); if($layout){ $script .= ' ' . $this->escapeAttributes($layout); } @@ -318,21 +317,10 @@ public function createScript() // explicitly add all isolated vertices (vertices with no edges) and vertices with special layout set // other vertices wil be added automatically due to below edge definitions foreach ($this->graph->getVertices() as $vid => $vertex){ - $layout = $vertex->getLayout(); - - $balance = $vertex->getBalance(); - if($balance !== NULL){ - if($balance > 0){ - $balance = '+' . $balance; - } - if(!isset($layout['label'])){ - $layout['label'] = $vid; - } - $layout['label'] .= ' (' . $balance . ')'; - } + $layout = $this->getLayoutVertex($vertex); if($vertex->isIsolated() || $layout){ - $script .= ' ' . $this->escapeId($vid); + $script .= $this->formatIndent . $this->escapeId($vid); if($layout){ $script .= ' ' . $this->escapeAttributes($layout); } @@ -349,43 +337,16 @@ public function createScript() $currentStartVertex = $both[0]; $currentTargetVertex = $both[1]; - $script .= ' ' . $this->escapeId($currentStartVertex->getId()) . $edgeop . $this->escapeId($currentTargetVertex->getId()); - - $attrs = $currentEdge->getLayout(); + $script .= $this->formatIndent . $this->escapeId($currentStartVertex->getId()) . $edgeop . $this->escapeId($currentTargetVertex->getId()); - // use flow/capacity/weight as edge label - $label = NULL; + $layout = $this->getLayoutEdge($currentEdge); - $flow = $currentEdge->getFlow(); - $capacity = $currentEdge->getCapacity(); - // flow is set - if ($flow !== NULL) { - // NULL capacity = infinite capacity - $label = $flow . '/' . ($capacity === NULL ? '∞' : $capacity); - // capacity set, but not flow (assume zero flow) - } elseif ($capacity !== NULL) { - $label = '0/' . $capacity; - } - - $weight = $currentEdge->getWeight(); - // weight is set - if ($weight !== NULL) { - if ($label === NULL) { - $label = $weight; - } else { - $label .= '/' . $weight; - } - } - - if ($label !== NULL) { - $attrs['label'] = $label; - } // this edge also points to the opposite direction => this is actually an undirected edge if ($directed && $currentEdge->isConnection($currentTargetVertex, $currentStartVertex)) { - $attrs['dir'] = 'none'; + $layout['dir'] = 'none'; } - if ($attrs) { - $script .= ' ' . $this->escapeAttributes($attrs); + if ($layout) { + $script .= ' ' . $this->escapeAttributes($layout); } $script .= self::EOL; @@ -457,4 +418,60 @@ public static function raw($string) { return (object) array('string' => $string); } + + protected function getLayoutVertex(Vertex $vertex) + { + $layout = $vertex->getLayout(); + + $balance = $vertex->getBalance(); + if($balance !== NULL){ + if($balance > 0){ + $balance = '+' . $balance; + } + if(!isset($layout['label'])){ + $layout['label'] = $vertex->getId(); + } + $layout['label'] .= ' (' . $balance . ')'; + } + + return $layout; + } + + protected function getLayoutEdge(Edge $edge) + { + $layout = $edge->getLayout(); + + // use flow/capacity/weight as edge label + $label = NULL; + + $flow = $edge->getFlow(); + $capacity = $edge->getCapacity(); + // flow is set + if ($flow !== NULL) { + // NULL capacity = infinite capacity + $label = $flow . '/' . ($capacity === NULL ? '∞' : $capacity); + // capacity set, but not flow (assume zero flow) + } elseif ($capacity !== NULL) { + $label = '0/' . $capacity; + } + + $weight = $edge->getWeight(); + // weight is set + if ($weight !== NULL) { + if ($label === NULL) { + $label = $weight; + } else { + $label .= '/' . $weight; + } + } + + if ($label !== NULL) { + if (isset($layout['label'])) { + $layout['label'] .= ' ' . $label; + } else { + $layout['label'] = $label; + } + } + return $layout; + } } diff --git a/tests/Fhaculty/Graph/GraphVizTest.php b/tests/Fhaculty/Graph/GraphVizTest.php new file mode 100644 index 00000000..181f6241 --- /dev/null +++ b/tests/Fhaculty/Graph/GraphVizTest.php @@ -0,0 +1,200 @@ +assertEquals($expected, $this->getDotScriptForGraph($graph)); + } + + public function testGraphIsolatedVertices() + { + $graph = new Graph(); + $graph->createVertex('a'); + $graph->createVertex('b'); + + $expected = <<assertEquals($expected, $this->getDotScriptForGraph($graph)); + } + + public function testEscaping() + { + $graph = new Graph(); + $graph->createVertex('a'); + $graph->createVertex('b¹²³ is; ok\\ay, "right"?'); + $graph->createVertex(3); + $graph->createVertex(4)->setLayoutAttribute('label', 'normal'); + $graph->createVertex(5)->setLayoutAttribute('label', GraphViz::raw('')); + + + $expected = <<] +} + +VIZ; + + $this->assertEquals($expected, $this->getDotScriptForGraph($graph)); + } + + public function testGraphDirected() + { + $graph = new Graph(); + $graph->createVertex('a')->createEdgeTo($graph->createVertex('b')); + + $expected = << "b" +} + +VIZ; + + $this->assertEquals($expected, $this->getDotScriptForGraph($graph)); + } + + public function testGraphMixed() + { + // a -> b -- c + $graph = new Graph(); + $graph->createVertex('a')->createEdgeTo($graph->createVertex('b')); + $graph->createVertex('c')->createEdge($graph->getVertex('b')); + + $expected = << "b" + "c" -> "b" [dir="none"] +} + +VIZ; + + $this->assertEquals($expected, $this->getDotScriptForGraph($graph)); + } + + + public function testGraphUndirectedWithIsolatedVerticesFirst() + { + // a -- b -- c d + $graph = new Graph(); + $graph->createVertices(array('a', 'b', 'c', 'd')); + $graph->getVertex('a')->createEdge($graph->getVertex('b')); + $graph->getVertex('b')->createEdge($graph->getVertex('c')); + + $expected = <<assertEquals($expected, $this->getDotScriptForGraph($graph)); + } + + public function testVertexLabels() + { + $graph = new Graph(); + $graph->createVertex('a')->setBalance(1); + $graph->createVertex('b')->setBalance(0); + $graph->createVertex('c')->setBalance(-1); + $graph->createVertex('d')->setLayoutAttribute('label', 'test'); + $graph->createVertex('e')->setLayoutAttribute('label', 'unnamed')->setBalance(2); + + $expected = <<assertEquals($expected, $this->getDotScriptForGraph($graph)); + } + + public function testEdgeLayoutAtributes() + { + $graph = new Graph(); + $graph->createVertex('1a')->createEdge($graph->createVertex('1b')); + $graph->createVertex('2a')->createEdge($graph->createVertex('2b'))->setLayoutAttribute('numeric', 20); + $graph->createVertex('3a')->createEdge($graph->createVertex('3b'))->setLayoutAttribute('textual', "forty"); + $graph->createVertex('4a')->createEdge($graph->createVertex('4b'))->setLayoutAttribute(1, 1)->setLayoutAttribute(2, 2); + $graph->createVertex('5a')->createEdge($graph->createVertex('5b'))->setLayout(array('a' => 'b', 'c' => 'd')); + + $expected = <<assertEquals($expected, $this->getDotScriptForGraph($graph)); + } + + public function testEdgeLabels() + { + $graph = new Graph(); + $graph->createVertex('1a')->createEdge($graph->createVertex('1b')); + $graph->createVertex('2a')->createEdge($graph->createVertex('2b'))->setWeight(20); + $graph->createVertex('3a')->createEdge($graph->createVertex('3b'))->setCapacity(30); + $graph->createVertex('4a')->createEdge($graph->createVertex('4b'))->setFlow(40); + $graph->createVertex('5a')->createEdge($graph->createVertex('5b'))->setFlow(50)->setCapacity(60); + $graph->createVertex('6a')->createEdge($graph->createVertex('6b'))->setFlow(60)->setCapacity(70)->setWeight(80); + $graph->createVertex('7a')->createEdge($graph->createVertex('7b'))->setLayoutAttribute('label', 'prefixed')->setFlow(70); + + $expected = <<assertEquals($expected, $this->getDotScriptForGraph($graph)); + } + + + private function getDotScriptForGraph(Graph $graph) + { + $graphviz = new GraphViz($graph); + return $graphviz->createScript(); + } +}