Code Coverage |
||||||||||
Classes and Traits |
Functions and Methods |
Lines |
||||||||
| Total | |
100.00% |
1 / 1 |
|
100.00% |
13 / 13 |
CRAP | |
100.00% |
77 / 77 |
| GrafovatkoExporter | |
100.00% |
1 / 1 |
|
100.00% |
13 / 13 |
36 | |
100.00% |
77 / 77 |
| __construct | |
100.00% |
1 / 1 |
1 | |
100.00% |
2 / 2 |
|||
| addProcessor | |
100.00% |
1 / 1 |
1 | |
100.00% |
3 / 3 |
|||
| export | |
100.00% |
1 / 1 |
4 | |
100.00% |
7 / 7 |
|||
| exportJsonString | |
100.00% |
1 / 1 |
2 | |
100.00% |
4 / 4 |
|||
| exportSvgElement | |
100.00% |
1 / 1 |
3 | |
100.00% |
6 / 6 |
|||
| exportHtmlFile | |
100.00% |
1 / 1 |
4 | |
100.00% |
11 / 11 |
|||
| getPrefix | |
100.00% |
1 / 1 |
1 | |
100.00% |
1 / 1 |
|||
| setPrefix | |
100.00% |
1 / 1 |
2 | |
100.00% |
4 / 4 |
|||
| processGraph | |
100.00% |
1 / 1 |
2 | |
100.00% |
3 / 3 |
|||
| processNodeAttrs | |
100.00% |
1 / 1 |
2 | |
100.00% |
3 / 3 |
|||
| processEdgeAttrs | |
100.00% |
1 / 1 |
2 | |
100.00% |
3 / 3 |
|||
| dumpNodeTree | |
100.00% |
1 / 1 |
6 | |
100.00% |
11 / 11 |
|||
| exportNestedGraph | |
100.00% |
1 / 1 |
6 | |
100.00% |
19 / 19 |
|||
| <?php declare(strict_types = 1); | |
| /* | |
| * Copyright (c) 2018, Josef Kufner <josef@kufner.cz> | |
| * | |
| * Licensed under the Apache License, Version 2.0 (the "License"); | |
| * you may not use this file except in compliance with the License. | |
| * You may obtain a copy of the License at | |
| * | |
| * http://www.apache.org/licenses/LICENSE-2.0 | |
| * | |
| * Unless required by applicable law or agreed to in writing, software | |
| * distributed under the License is distributed on an "AS IS" BASIS, | |
| * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. | |
| * See the License for the specific language governing permissions and | |
| * limitations under the License. | |
| * | |
| */ | |
| namespace Smalldb\Graph\Grafovatko; | |
| use RuntimeException; | |
| use Smalldb\Graph\Edge; | |
| use Smalldb\Graph\Graph; | |
| use Smalldb\Graph\NestedGraph; | |
| use Smalldb\Graph\Node; | |
| class GrafovatkoExporter | |
| { | |
| private Graph $graph; | |
| public static string $grafovatkoJsLink = 'https://grafovatko.smalldb.org/dist/grafovatko.min.js'; | |
| /** @var ProcessorInterface[] */ | |
| private array $processors = []; | |
| /** | |
| * Global graph IDs prefix | |
| * | |
| * @var string | |
| */ | |
| private string $prefix = ''; | |
| public function __construct(Graph $graph) | |
| { | |
| $this->graph = $graph; | |
| } | |
| public function addProcessor(ProcessorInterface $processor): self | |
| { | |
| $this->processors[] = $processor; | |
| $processor->setPrefix($this->prefix); | |
| return $this; | |
| } | |
| public function export(): array | |
| { | |
| $jsonObject = $this->exportNestedGraph($this->graph); | |
| foreach ($this->processors as $processor) { | |
| $extraSvg = $processor->getExtraSvgElements($this->graph); | |
| if (!empty($extraSvg)) { | |
| foreach ($extraSvg as $el) { | |
| $jsonObject['extraSvg'][] = $el; | |
| } | |
| } | |
| } | |
| return $jsonObject; | |
| } | |
| public function exportJsonString(int $jsonOptions = 0): string | |
| { | |
| $jsonObject = $this->export(); | |
| $jsonString = json_encode($jsonObject, JSON_NUMERIC_CHECK | $jsonOptions); | |
| if ($jsonString === false) { | |
| throw new RuntimeException('Failed to serialize graph: ' . json_last_error_msg()); // @codeCoverageIgnore | |
| } | |
| return $jsonString; | |
| } | |
| public function exportSvgElement(array $attrs = []): string | |
| { | |
| $jsonString = $this->exportJsonString(JSON_HEX_APOS | JSON_HEX_AMP); | |
| $attrsHtml = ""; | |
| foreach ($attrs as $attr => $value) { | |
| if ($value !== null) { | |
| $attrsHtml .= " $attr=\"" . htmlspecialchars($value) . "\""; | |
| } | |
| } | |
| return "<svg$attrsHtml width=\"1\" height=\"1\" data-graph='$jsonString'></svg>"; | |
| } | |
| public function exportHtmlFile(string $targetFileName, ?string $title = null): void | |
| { | |
| $titleHtml = htmlspecialchars($title ?? basename($targetFileName)); | |
| $grafovatkoJsFile = basename(static::$grafovatkoJsLink); | |
| $grafovatkoJsLink = file_exists(dirname($targetFileName) . '/' . $grafovatkoJsFile) | |
| ? $grafovatkoJsFile : static::$grafovatkoJsLink; | |
| $svgElement = $this->exportSvgElement(['id' => 'graph']); | |
| $html = <<<EOF | |
| <!DOCTYPE HTML> | |
| <html> | |
| <head> | |
| <title>$titleHtml</title> | |
| <meta charset="UTF-8" /> | |
| <style type="text/css"> | |
| html, body { | |
| display: flex; | |
| align-items: center; | |
| min-height: 100%; | |
| margin: auto; | |
| padding: 0; | |
| } | |
| svg#graph { | |
| margin: auto; | |
| padding: 1em; | |
| overflow: visible; | |
| } | |
| </style> | |
| </head> | |
| <body> | |
| $svgElement | |
| <script type="text/javascript" src="$grafovatkoJsLink"></script> | |
| <script type="text/javascript"> | |
| window.graphView = new G.GraphView('#graph'); | |
| </script> | |
| </body> | |
| </html> | |
| EOF; | |
| if (!file_put_contents($targetFileName, $html)) { | |
| throw new RuntimeException('Failed to write graph.'); //@codeCoverageIgnore | |
| } | |
| } | |
| public function getPrefix(): string | |
| { | |
| return $this->prefix; | |
| } | |
| public function setPrefix(string $prefix): self | |
| { | |
| $this->prefix = $prefix; | |
| foreach ($this->processors as $processor) { | |
| $processor->setPrefix($prefix); | |
| } | |
| return $this; | |
| } | |
| protected function processGraph(NestedGraph $graph, array $exportedGraph): array | |
| { | |
| foreach ($this->processors as $processor) { | |
| $exportedGraph = $processor->processGraph($graph, $exportedGraph); | |
| } | |
| return $exportedGraph; | |
| } | |
| protected function processNodeAttrs(Node $node, array $exportedNode): array | |
| { | |
| foreach ($this->processors as $processor) { | |
| $exportedNode = $processor->processNodeAttrs($node, $exportedNode); | |
| } | |
| return $exportedNode; | |
| } | |
| protected function processEdgeAttrs(Edge $edge, array $exportedEdge): array | |
| { | |
| foreach ($this->processors as $processor) { | |
| $exportedEdge = $processor->processEdgeAttrs($edge, $exportedEdge); | |
| } | |
| return $exportedEdge; | |
| } | |
| /** | |
| * Debug: Dump plain text representation of the graph hierarchy | |
| */ | |
| public function dumpNodeTree(NestedGraph $graph = null, $indent = "", $withEdges = true): void | |
| { | |
| if ($graph === null) { | |
| $this->dumpNodeTree($this->graph); | |
| return; | |
| } | |
| if ($withEdges) { | |
| foreach ($graph->getEdges() as $edge) { | |
| echo $indent, "- ", $edge->getId(), ' (', $edge->getStart()->getId(), ' -> ', $edge->getEnd()->getId(), ")\n"; | |
| } | |
| } | |
| foreach ($graph->getNodes() as $node) { | |
| echo $indent, "* ", $node->getId(), "\n"; | |
| if ($node->hasNestedGraph()) { | |
| $this->dumpNodeTree($node->getNestedGraph(), $indent . "\t"); | |
| } | |
| } | |
| } | |
| /** | |
| * Export $graph to JSON array. | |
| */ | |
| private function exportNestedGraph(NestedGraph $graph): array | |
| { | |
| $nodes = []; | |
| foreach ($graph->getNodes() as $node) { | |
| $nodeJson = [ | |
| 'id' => $this->prefix . $node->getId(), | |
| 'graph' => $node->hasNestedGraph() ? $this->exportNestedGraph($node->getNestedGraph()) : null, | |
| 'attrs' => $this->processNodeAttrs($node, $node->getAttributes()), | |
| ]; | |
| if ($nodeJson !== null) { | |
| $nodes[] = $nodeJson; | |
| } | |
| } | |
| $edges = []; | |
| foreach ($graph->getEdges() as $edge) { | |
| $edgeJson = [ | |
| 'id' => $this->prefix . $edge->getId(), | |
| 'start' => $this->prefix . $edge->getStart()->getId(), | |
| 'end' => $this->prefix . $edge->getEnd()->getId(), | |
| 'attrs' => $this->processEdgeAttrs($edge, $edge->getAttributes()), | |
| ]; | |
| if ($edgeJson !== null) { | |
| $edges[] = $edgeJson; | |
| } | |
| } | |
| return $this->processGraph($graph, [ | |
| 'layout' => 'dagre', | |
| 'nodes' => $nodes, | |
| 'edges' => $edges, | |
| ]); | |
| } | |
| } |