Code Coverage
 
Classes and Traits
Functions and Methods
Lines
Total
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
13 / 13
CRAP
100.00% covered (success)
100.00%
77 / 77
GrafovatkoExporter
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
13 / 13
36
100.00% covered (success)
100.00%
77 / 77
 __construct
100.00% covered (success)
100.00%
1 / 1
1
100.00% covered (success)
100.00%
2 / 2
 addProcessor
100.00% covered (success)
100.00%
1 / 1
1
100.00% covered (success)
100.00%
3 / 3
 export
100.00% covered (success)
100.00%
1 / 1
4
100.00% covered (success)
100.00%
7 / 7
 exportJsonString
100.00% covered (success)
100.00%
1 / 1
2
100.00% covered (success)
100.00%
4 / 4
 exportSvgElement
100.00% covered (success)
100.00%
1 / 1
3
100.00% covered (success)
100.00%
6 / 6
 exportHtmlFile
100.00% covered (success)
100.00%
1 / 1
4
100.00% covered (success)
100.00%
11 / 11
 getPrefix
100.00% covered (success)
100.00%
1 / 1
1
100.00% covered (success)
100.00%
1 / 1
 setPrefix
100.00% covered (success)
100.00%
1 / 1
2
100.00% covered (success)
100.00%
4 / 4
 processGraph
100.00% covered (success)
100.00%
1 / 1
2
100.00% covered (success)
100.00%
3 / 3
 processNodeAttrs
100.00% covered (success)
100.00%
1 / 1
2
100.00% covered (success)
100.00%
3 / 3
 processEdgeAttrs
100.00% covered (success)
100.00%
1 / 1
2
100.00% covered (success)
100.00%
3 / 3
 dumpNodeTree
100.00% covered (success)
100.00%
1 / 1
6
100.00% covered (success)
100.00%
11 / 11
 exportNestedGraph
100.00% covered (success)
100.00%
1 / 1
6
100.00% covered (success)
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,
        ]);
    }
}