summaryrefslogtreecommitdiffstats
path: root/library/Icinga/Chart/SVGRenderer.php
blob: d3891f22487b71f8a551c7a8774d4e8718203d4a (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
<?php
/* Icinga Web 2 | (c) 2013 Icinga Development Team | GPLv2+ */

namespace Icinga\Chart;

use DOMNode;
use DOMElement;
use DOMDocument;
use DOMImplementation;
use Icinga\Chart\Render\LayoutBox;
use Icinga\Chart\Render\RenderContext;
use Icinga\Chart\Primitive\Canvas;

/**
 * SVG Renderer component.
 *
 * Creates the basic DOM tree of the SVG to use
 */
class SVGRenderer
{
    const X_ASPECT_RATIO_MIN = 'xMin';

    const X_ASPECT_RATIO_MID = 'xMid';

    const X_ASPECT_RATIO_MAX = 'xMax';

    const Y_ASPECT_RATIO_MIN = 'YMin';

    const Y_ASPECT_RATIO_MID = 'YMid';

    const Y_ASPECT_RATIO_MAX = 'YMax';

    const ASPECT_RATIO_PAD = 'meet';

    const ASPECT_RATIO_CUTOFF = 'slice';

    /**
     * The XML-document
     *
     * @var DOMDocument
     */
    private $document;

    /**
     * The SVG-element
     *
     * @var DOMNode
     */
    private $svg;

    /**
     * The description of this SVG, useful for screen readers
     *
     * @var string
     */
    private $ariaDescription;

    /**
     * The title of this SVG, useful for screen readers
     *
     * @var string
     */
    private $ariaTitle;

    /**
     * The aria role used by this svg element
     *
     * @var string
     */
    private $ariaRole = 'img';

    /**
     * The root layer for all elements
     *
     * @var Canvas
     */
    private $rootCanvas;

    /**
     * The width of this renderer
     *
     * @var int
     */
    private $width = 100;

    /**
     * The height of this renderer
     *
     * @var int
     */
    private $height = 100;

    /**
     * Whether the aspect ratio is preversed
     *
     * @var bool
     */
    private $preserveAspectRatio = false;

    /**
     * Horizontal alignment of SVG element
     *
     * @var string
     */
    private $xAspectRatio = self::X_ASPECT_RATIO_MID;

    /**
     * Vertical alignment of SVG element
     *
     * @var string
     */
    private $yAspectRatio = self::Y_ASPECT_RATIO_MID;

    /**
     * Define whether aspect differences should be handled using padding (default) or cutoff
     *
     * @var string
     */
    private $xFillMode = "meet";


    /**
     * Create the root document and the SVG root node
     */
    private function createRootDocument()
    {
        $implementation = new DOMImplementation();
        $docType = $implementation->createDocumentType(
            'svg',
            '-//W3C//DTD SVG 1.1//EN',
            'http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd'
        );

        $this->document = $implementation->createDocument(null, '', $docType);
        $this->svg = $this->createOuterBox();
        $this->document->appendChild($this->svg);
    }

    /**
     * Create the outer SVG box  containing the root svg element and namespace and return it
     *
     * @return DOMElement The SVG root node
     */
    private function createOuterBox()
    {
        $ctx = $this->createRenderContext();
        $svg = $this->document->createElement('svg');
        $svg->setAttribute('xmlns', 'http://www.w3.org/2000/svg');
        $svg->setAttribute('xmlns:xlink', 'http://www.w3.org/1999/xlink');
        $svg->setAttribute('role', $this->ariaRole);
        $svg->setAttribute('width', '100%');
        $svg->setAttribute('height', '100%');
        $svg->setAttribute(
            'viewBox',
            sprintf(
                '0 0 %s %s',
                $ctx->getNrOfUnitsX(),
                $ctx->getNrOfUnitsY()
            )
        );
        if ($this->preserveAspectRatio) {
            $svg->setAttribute(
                'preserveAspectRatio',
                sprintf(
                    '%s%s %s',
                    $this->xAspectRatio,
                    $this->yAspectRatio,
                    $this->xFillMode
                )
            );
        }
        return $svg;
    }

    /**
     * Add aria title and description
     *
     * Adds an aria title and desc element to the given SVG node, which are used to describe this SVG by accessibility
     * tools such as screen readers.
     *
     * @param DOMNode $svg          The SVG DOMNode to which the aria attributes should be attached
     * @param         $title        The title text
     * @param         $description  The description text
     */
    private function addAriaDescription(DOMNode $svg, $titleText, $descriptionText)
    {
        $doc = $svg->ownerDocument;

        $titleId = $descId = '';
        if (isset($this->ariaTitle)) {
            $titleId = 'aria-title-' . $this->stripNonAlphanumeric($titleText);
            $title = $doc->createElement('title');
            $title->setAttribute('id', $titleId);

            $title->appendChild($doc->createTextNode($titleText));
            $svg->appendChild($title);
        }

        if (isset($this->ariaDescription)) {
            $descId = 'aria-desc-' . $this->stripNonAlphanumeric($descriptionText);
            $desc = $doc->createElement('desc');
            $desc->setAttribute('id', $descId);

            $desc->appendChild($doc->createTextNode($descriptionText));
            $svg->appendChild($desc);
        }

        $svg->setAttribute('aria-labelledby', join(' ', array($titleId, $descId)));
    }

    /**
     * Initialises the XML-document, SVG-element and this figure's root canvas
     *
     * @param int $width    The width ratio
     * @param int $height   The height ratio
     */
    public function __construct($width, $height)
    {
        $this->width = $width;
        $this->height = $height;
        $this->rootCanvas = new Canvas('root', new LayoutBox(0, 0));
    }

    /**
     * Render the SVG-document
     *
     * @return string The resulting XML structure
     */
    public function render()
    {
        $this->createRootDocument();
        $ctx = $this->createRenderContext();
        $this->addAriaDescription($this->svg, $this->ariaTitle, $this->ariaDescription);
        $this->svg->appendChild($this->rootCanvas->toSvg($ctx));
        $this->document->formatOutput = true;
        $this->document->encoding = 'UTF-8';
        return $this->document->saveXML();
    }

    /**
     * Create a render context that will be used for rendering elements
     *
     * @return RenderContext The created RenderContext instance
     */
    public function createRenderContext()
    {
        return new RenderContext($this->document, $this->width, $this->height);
    }

    /**
     * Return the root canvas of this rendered
     *
     * @return Canvas The canvas that will be the uppermost element in this figure
     */
    public function getCanvas()
    {
        return $this->rootCanvas;
    }

    /**
     * Preserve the aspect ratio of the rendered object
     *
     * Do not deform the content of the SVG when the aspect ratio of the viewBox
     * differs from the aspect ratio of the SVG element, but add padding or cutoff
     * instead
     *
     * @param bool $preserve    Whether the aspect ratio should be preserved
     */
    public function preserveAspectRatio($preserve = true)
    {
        $this->preserveAspectRatio = $preserve;
    }

    /**
     * Change the horizontal alignment of the SVG element
     *
     * Change the horizontal alignment of the svg, when preserveAspectRatio is used and
     * padding is present. Defaults to
     */
    public function setXAspectRatioAlignment($alignment)
    {
        $this->xAspectRatio = $alignment;
    }

    /**
     * Change the vertical alignment of the SVG element
     *
     * Change the vertical alignment of the svg, when preserveAspectRatio is used and
     * padding is present.
     */
    public function setYAspectRatioAlignment($alignment)
    {
        $this->yAspectRatio = $alignment;
    }

    /**
     * Set the aria description, that is used as a title for this SVG in screen readers
     *
     * @param $text
     */
    public function setAriaTitle($text)
    {
        $this->ariaTitle = $text;
    }

    /**
     * Set the aria description, that is used to describe this SVG in screen readers
     *
     * @param $text
     */
    public function setAriaDescription($text)
    {
        $this->ariaDescription = $text;
    }

    /**
     * Set the aria role, that is used to describe the purpose of this SVG in screen readers
     *
     * @param $text
     */
    public function setAriaRole($text)
    {
        $this->ariaRole = $text;
    }


    private function stripNonAlphanumeric($str)
    {
        return preg_replace('/[^A-Za-z]+/', '', $str);
    }
}