1 /*
  2     Copyright 2008-2023
  3         Matthias Ehmann,
  4         Michael Gerhaeuser,
  5         Carsten Miller,
  6         Bianca Valentin,
  7         Alfred Wassermann,
  8         Peter Wilfahrt
  9 
 10     This file is part of JSXGraph.
 11 
 12     JSXGraph is free software dual licensed under the GNU LGPL or MIT License.
 13 
 14     You can redistribute it and/or modify it under the terms of the
 15 
 16       * GNU Lesser General Public License as published by
 17         the Free Software Foundation, either version 3 of the License, or
 18         (at your option) any later version
 19       OR
 20       * MIT License: https://github.com/jsxgraph/jsxgraph/blob/master/LICENSE.MIT
 21 
 22     JSXGraph is distributed in the hope that it will be useful,
 23     but WITHOUT ANY WARRANTY; without even the implied warranty of
 24     MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 25     GNU Lesser General Public License for more details.
 26 
 27     You should have received a copy of the GNU Lesser General Public License and
 28     the MIT License along with JSXGraph. If not, see <https://www.gnu.org/licenses/>
 29     and <https://opensource.org/licenses/MIT/>.
 30  */
 31 
 32 /*global JXG: true, define: true, AMprocessNode: true, MathJax: true, document: true */
 33 /*jslint nomen: true, plusplus: true, newcap:true*/
 34 
 35 import JXG from "../jxg";
 36 import Options from "../options";
 37 import AbstractRenderer from "./abstract";
 38 import Const from "../base/constants";
 39 import Type from "../utils/type";
 40 import Color from "../utils/color";
 41 import Base64 from "../utils/base64";
 42 import Numerics from "../math/numerics";
 43 
 44 /**
 45  * Uses SVG to implement the rendering methods defined in {@link JXG.AbstractRenderer}.
 46  * @class JXG.SVGRenderer
 47  * @augments JXG.AbstractRenderer
 48  * @param {Node} container Reference to a DOM node containing the board.
 49  * @param {Object} dim The dimensions of the board
 50  * @param {Number} dim.width
 51  * @param {Number} dim.height
 52  * @see JXG.AbstractRenderer
 53  */
 54 JXG.SVGRenderer = function (container, dim) {
 55     var i;
 56 
 57     // docstring in AbstractRenderer
 58     this.type = "svg";
 59 
 60     this.isIE =
 61         navigator.appVersion.indexOf("MSIE") !== -1 || navigator.userAgent.match(/Trident\//);
 62 
 63     /**
 64      * SVG root node
 65      * @type Node
 66      */
 67     this.svgRoot = null;
 68 
 69     /**
 70      * The SVG Namespace used in JSXGraph.
 71      * @see http://www.w3.org/TR/SVG2/
 72      * @type String
 73      * @default http://www.w3.org/2000/svg
 74      */
 75     this.svgNamespace = "http://www.w3.org/2000/svg";
 76 
 77     /**
 78      * The xlink namespace. This is used for images.
 79      * @see http://www.w3.org/TR/xlink/
 80      * @type String
 81      * @default http://www.w3.org/1999/xlink
 82      */
 83     this.xlinkNamespace = "http://www.w3.org/1999/xlink";
 84 
 85     // container is documented in AbstractRenderer
 86     this.container = container;
 87 
 88     // prepare the div container and the svg root node for use with JSXGraph
 89     this.container.style.MozUserSelect = "none";
 90     this.container.style.userSelect = "none";
 91 
 92     this.container.style.overflow = "hidden";
 93     if (this.container.style.position === "") {
 94         this.container.style.position = "relative";
 95     }
 96 
 97     this.svgRoot = this.container.ownerDocument.createElementNS(this.svgNamespace, "svg");
 98     this.svgRoot.style.overflow = "hidden";
 99     this.svgRoot.style.display = "block";
100 
101     this.resize(dim.width, dim.height);
102 
103     //this.svgRoot.setAttributeNS(null, 'shape-rendering', 'crispEdge'); //'optimizeQuality'); //geometricPrecision');
104 
105     this.container.appendChild(this.svgRoot);
106 
107     /**
108      * The <tt>defs</tt> element is a container element to reference reusable SVG elements.
109      * @type Node
110      * @see https://www.w3.org/TR/SVG2/struct.html#DefsElement
111      */
112     this.defs = this.container.ownerDocument.createElementNS(this.svgNamespace, "defs");
113     this.svgRoot.appendChild(this.defs);
114 
115     /**
116      * Filters are used to apply shadows.
117      * @type Node
118      * @see https://www.w3.org/TR/SVG2/struct.html#DefsElement
119      */
120     /**
121      * Create an SVG shadow filter. If the object's RGB color is [r,g,b], it's opacity is op, and
122      * the parameter color is given as [r', g', b'] with opacity op'
123      * the shadow will have RGB color [blend*r + r', blend*g + g', blend*b + b'] and the opacity will be equal to op * op'.
124      * Further, blur and offset can be adjusted.
125      *
126      * The shadow color is [r*ble
127      * @param {String} id Node is of the filter.
128      * @param {Array|String} rgb RGB value for the blend color or the string 'none' for default values. Default 'black'.
129      * @param {Number} opacity Value between 0 and 1, default is 1.
130      * @param {Number} blend  Value between 0 and 1, default is 0.1.
131      * @param {Number} blur  Default: 3
132      * @param {Array} offset [dx, dy]. Default is [5,5].
133      * @returns DOM node to be added to this.defs.
134      * @private
135      */
136     this.createShadowFilter = function (id, rgb, opacity, blend, blur, offset) {
137         var filter = this.container.ownerDocument.createElementNS(this.svgNamespace, 'filter'),
138             feOffset, feColor, feGaussianBlur, feBlend,
139             mat;
140 
141         filter.setAttributeNS(null, 'id', id);
142         filter.setAttributeNS(null, 'width', '300%');
143         filter.setAttributeNS(null, 'height', '300%');
144         filter.setAttributeNS(null, 'filterUnits', 'userSpaceOnUse');
145 
146         feOffset = this.container.ownerDocument.createElementNS(this.svgNamespace, 'feOffset');
147         feOffset.setAttributeNS(null, 'in', 'SourceGraphic'); // b/w: SourceAlpha, Color: SourceGraphic
148         feOffset.setAttributeNS(null, 'result', 'offOut');
149         feOffset.setAttributeNS(null, 'dx', offset[0]);
150         feOffset.setAttributeNS(null, 'dy', offset[1]);
151         filter.appendChild(feOffset);
152 
153         feColor = this.container.ownerDocument.createElementNS(this.svgNamespace, 'feColorMatrix');
154         feColor.setAttributeNS(null, 'in', 'offOut');
155         feColor.setAttributeNS(null, 'result', 'colorOut');
156         feColor.setAttributeNS(null, 'type', 'matrix');
157         // See https://developer.mozilla.org/en-US/docs/Web/SVG/Element/feColorMatrix
158         if (rgb === 'none' || !Type.isArray(rgb) || rgb.length < 3) {
159             feColor.setAttributeNS(null, 'values', '0.1 0 0 0 0  0 0.1 0 0 0  0 0 0.1 0 0  0 0 0 ' + opacity + ' 0');
160         } else {
161             rgb[0] /= 255;
162             rgb[1] /= 255;
163             rgb[2] /= 255;
164             mat = blend + ' 0 0 0 ' + rgb[0] +
165                 '  0 ' + blend + ' 0 0 ' + rgb[1] +
166                 '  0 0 ' + blend + ' 0 ' + rgb[2] +
167                 '  0 0 0 ' + opacity + ' 0';
168             feColor.setAttributeNS(null, 'values', mat);
169         }
170         filter.appendChild(feColor);
171 
172         feGaussianBlur = this.container.ownerDocument.createElementNS(this.svgNamespace, 'feGaussianBlur');
173         feGaussianBlur.setAttributeNS(null, 'in', 'colorOut');
174         feGaussianBlur.setAttributeNS(null, 'result', 'blurOut');
175         feGaussianBlur.setAttributeNS(null, 'stdDeviation', blur);
176         filter.appendChild(feGaussianBlur);
177 
178         feBlend = this.container.ownerDocument.createElementNS(this.svgNamespace, 'feBlend');
179         feBlend.setAttributeNS(null, 'in', 'SourceGraphic');
180         feBlend.setAttributeNS(null, 'in2', 'blurOut');
181         feBlend.setAttributeNS(null, 'mode', 'normal');
182         filter.appendChild(feBlend);
183 
184         return filter;
185     };
186 
187     /**
188      * Create a "unique" string id from the arguments of the function.
189      * Concatenate all arguments by "_".
190      * "Unique" is achieved by simply prepending the container id.
191      * Do not escape the string.
192      *
193      * If the id is used in an "url()" call it must be eascaped.
194      *
195      * @params {String} one or strings which will be concatenated.
196      * @return {String}
197      * @private
198      */
199     this.uniqName = function () {
200         return this.container.id + '_' +
201             Array.prototype.slice.call(arguments).join('_');
202     };
203 
204     /**
205      * Combine arguments to an URL string of the form
206      * url(#...)
207      * Masks the container id.
208      *
209      * @params {Objects} parts of the string
210      * @returns URL string
211      * @private
212      * @example
213      * this.toURL('aaa', '_', 'bbb', 'TriangleEnd')
214      * // Output:
215      * // url(#xxx_bbbTriangleEnd)
216      *
217      */
218     this.toURL = function () {
219         // ES6 would be [...arguments].join()
220         var str = Array.prototype.slice.call(arguments).join('');
221         // Mask special symbols like '/' and '\' in id
222         if (Type.exists(CSS) && Type.exists(CSS.escape)) {
223             str = CSS.escape(str);
224         }
225         return 'url(#' + str + ')';
226     };
227 
228     /* Default shadow filter */
229     this.defs.appendChild(this.createShadowFilter(this.uniqName('f1'), 'none', 1, 0.1, 3, [5, 5]));
230 
231     /**
232      * JSXGraph uses a layer system to sort the elements on the board. This puts certain types of elements in front
233      * of other types of elements. For the order used see {@link JXG.Options.layer}. The number of layers is documented
234      * there, too. The higher the number, the "more on top" are the elements on this layer.
235      * @type Array
236      */
237     this.layer = [];
238     for (i = 0; i < Options.layer.numlayers; i++) {
239         this.layer[i] = this.container.ownerDocument.createElementNS(this.svgNamespace, 'g');
240         this.svgRoot.appendChild(this.layer[i]);
241     }
242 
243     try {
244         this.foreignObjLayer = this.container.ownerDocument.createElementNS(
245             this.svgNamespace,
246             "foreignObject"
247         );
248         this.foreignObjLayer.setAttribute("display", "none");
249         this.foreignObjLayer.setAttribute("x", 0);
250         this.foreignObjLayer.setAttribute("y", 0);
251         this.foreignObjLayer.setAttribute("width", "100%");
252         this.foreignObjLayer.setAttribute("height", "100%");
253         this.foreignObjLayer.setAttribute("id", this.uniqName('foreignObj'));
254         this.svgRoot.appendChild(this.foreignObjLayer);
255         this.supportsForeignObject = true;
256     } catch (e) {
257         this.supportsForeignObject = false;
258     }
259 };
260 
261 JXG.SVGRenderer.prototype = new AbstractRenderer();
262 
263 JXG.extend(
264     JXG.SVGRenderer.prototype,
265     /** @lends JXG.SVGRenderer.prototype */ {
266         /**
267          * Creates an arrow DOM node. Arrows are displayed in SVG with a <em>marker</em> tag.
268          * @private
269          * @param {JXG.GeometryElement} el A JSXGraph element, preferably one that can have an arrow attached.
270          * @param {String} [idAppendix=''] A string that is added to the node's id.
271          * @returns {Node} Reference to the node added to the DOM.
272          */
273         _createArrowHead: function (el, idAppendix, type) {
274             var node2,
275                 node3,
276                 id = el.id + "Triangle",
277                 //type = null,
278                 v,
279                 h;
280 
281             if (Type.exists(idAppendix)) {
282                 id += idAppendix;
283             }
284             node2 = this.createPrim("marker", id);
285 
286             node2.setAttributeNS(null, "stroke", Type.evaluate(el.visProp.strokecolor));
287             node2.setAttributeNS(
288                 null,
289                 "stroke-opacity",
290                 Type.evaluate(el.visProp.strokeopacity)
291             );
292             node2.setAttributeNS(null, "fill", Type.evaluate(el.visProp.strokecolor));
293             node2.setAttributeNS(null, "fill-opacity", Type.evaluate(el.visProp.strokeopacity));
294             node2.setAttributeNS(null, "stroke-width", 0); // this is the stroke-width of the arrow head.
295             // Should be zero to simplify the calculations
296 
297             node2.setAttributeNS(null, "orient", "auto");
298             node2.setAttributeNS(null, "markerUnits", "strokeWidth"); // 'strokeWidth' 'userSpaceOnUse');
299 
300             /*
301                Types 1, 2:
302                The arrow head is an isosceles triangle with base length 10 and height 10.
303 
304                Type 3:
305                A rectangle
306 
307                Types 4, 5, 6:
308                Defined by Bezier curves from mp_arrowheads.html
309 
310                In any case but type 3 the arrow head is 10 units long,
311                type 3 is 10 units high.
312                These 10 units are scaled to strokeWidth * arrowSize pixels, see
313                this._setArrowWidth().
314 
315                See also abstractRenderer.updateLine() where the line path is shortened accordingly.
316 
317                Changes here are also necessary in setArrowWidth().
318 
319                So far, lines with arrow heads are shortenend to avoid overlapping of
320                arrow head and line. This is not the case for curves, yet.
321                Therefore, the offset refX has to be adapted to the path type.
322             */
323             node3 = this.container.ownerDocument.createElementNS(this.svgNamespace, "path");
324             h = 5;
325             if (idAppendix === "End") {
326                 // First arrow
327                 //type = a.typeFirst;
328                 // if (JXG.exists(ev_fa.type)) {
329                 //     type = Type.evaluate(ev_fa.type);
330                 // }
331 
332                 v = 0;
333                 if (type === 2) {
334                     node3.setAttributeNS(null, "d", "M 10,0 L 0,5 L 10,10 L 5,5 z");
335                 } else if (type === 3) {
336                     node3.setAttributeNS(null, "d", "M 0,0 L 3.33,0 L 3.33,10 L 0,10 z");
337                 } else if (type === 4) {
338                     // insetRatio:0.8 tipAngle:45 wingCurve:15 tailCurve:0
339                     h = 3.31;
340                     node3.setAttributeNS(
341                         null,
342                         "d",
343                         "M 0.00,3.31 C 3.53,3.84 7.13,4.50 10.00,6.63 C 9.33,5.52 8.67,4.42 8.00,3.31 C 8.67,2.21 9.33,1.10 10.00,0.00 C 7.13,2.13 3.53,2.79 0.00,3.31"
344                     );
345                 } else if (type === 5) {
346                     // insetRatio:0.9 tipAngle:40 wingCurve:5 tailCurve:15
347                     h = 3.28;
348                     node3.setAttributeNS(
349                         null,
350                         "d",
351                         "M 0.00,3.28 C 3.39,4.19 6.81,5.07 10.00,6.55 C 9.38,5.56 9.00,4.44 9.00,3.28 C 9.00,2.11 9.38,0.99 10.00,0.00 C 6.81,1.49 3.39,2.37 0.00,3.28"
352                     );
353                 } else if (type === 6) {
354                     // insetRatio:0.9 tipAngle:35 wingCurve:5 tailCurve:0
355                     h = 2.84;
356                     node3.setAttributeNS(
357                         null,
358                         "d",
359                         "M 0.00,2.84 C 3.39,3.59 6.79,4.35 10.00,5.68 C 9.67,4.73 9.33,3.78 9.00,2.84 C 9.33,1.89 9.67,0.95 10.00,0.00 C 6.79,1.33 3.39,2.09 0.00,2.84"
360                     );
361                 } else if (type === 7) {
362                     // insetRatio:0.9 tipAngle:60 wingCurve:30 tailCurve:0
363                     h = 5.2;
364                     node3.setAttributeNS(
365                         null,
366                         "d",
367                         "M 0.00,5.20 C 4.04,5.20 7.99,6.92 10.00,10.39 M 10.00,0.00 C 7.99,3.47 4.04,5.20 0.00,5.20"
368                     );
369                 } else {
370                     // type == 1 or > 6
371                     node3.setAttributeNS(null, "d", "M 10,0 L 0,5 L 10,10 z");
372                 }
373                 if (
374                     // !Type.exists(el.rendNode.getTotalLength) &&
375                     el.elementClass === Const.OBJECT_CLASS_LINE
376                 ) {
377                     if (type === 2) {
378                         v = 4.9;
379                     } else if (type === 3) {
380                         v = 3.3;
381                     } else if (type === 4 || type === 5 || type === 6) {
382                         v = 6.66;
383                     } else if (type === 7) {
384                         v = 0.0;
385                     } else {
386                         v = 10.0;
387                     }
388                 }
389             } else {
390                 // Last arrow
391                 // if (JXG.exists(ev_la.type)) {
392                 //     type = Type.evaluate(ev_la.type);
393                 // }
394                 //type = a.typeLast;
395 
396                 v = 10.0;
397                 if (type === 2) {
398                     node3.setAttributeNS(null, "d", "M 0,0 L 10,5 L 0,10 L 5,5 z");
399                 } else if (type === 3) {
400                     v = 3.3;
401                     node3.setAttributeNS(null, "d", "M 0,0 L 3.33,0 L 3.33,10 L 0,10 z");
402                 } else if (type === 4) {
403                     // insetRatio:0.8 tipAngle:45 wingCurve:15 tailCurve:0
404                     h = 3.31;
405                     node3.setAttributeNS(
406                         null,
407                         "d",
408                         "M 10.00,3.31 C 6.47,3.84 2.87,4.50 0.00,6.63 C 0.67,5.52 1.33,4.42 2.00,3.31 C 1.33,2.21 0.67,1.10 0.00,0.00 C 2.87,2.13 6.47,2.79 10.00,3.31"
409                     );
410                 } else if (type === 5) {
411                     // insetRatio:0.9 tipAngle:40 wingCurve:5 tailCurve:15
412                     h = 3.28;
413                     node3.setAttributeNS(
414                         null,
415                         "d",
416                         "M 10.00,3.28 C 6.61,4.19 3.19,5.07 0.00,6.55 C 0.62,5.56 1.00,4.44 1.00,3.28 C 1.00,2.11 0.62,0.99 0.00,0.00 C 3.19,1.49 6.61,2.37 10.00,3.28"
417                     );
418                 } else if (type === 6) {
419                     // insetRatio:0.9 tipAngle:35 wingCurve:5 tailCurve:0
420                     h = 2.84;
421                     node3.setAttributeNS(
422                         null,
423                         "d",
424                         "M 10.00,2.84 C 6.61,3.59 3.21,4.35 0.00,5.68 C 0.33,4.73 0.67,3.78 1.00,2.84 C 0.67,1.89 0.33,0.95 0.00,0.00 C 3.21,1.33 6.61,2.09 10.00,2.84"
425                     );
426                 } else if (type === 7) {
427                     // insetRatio:0.9 tipAngle:60 wingCurve:30 tailCurve:0
428                     h = 5.2;
429                     node3.setAttributeNS(
430                         null,
431                         "d",
432                         "M 10.00,5.20 C 5.96,5.20 2.01,6.92 0.00,10.39 M 0.00,0.00 C 2.01,3.47 5.96,5.20 10.00,5.20"
433                     );
434                 } else {
435                     // type == 1 or > 6
436                     node3.setAttributeNS(null, "d", "M 0,0 L 10,5 L 0,10 z");
437                 }
438                 if (
439                     // !Type.exists(el.rendNode.getTotalLength) &&
440                     el.elementClass === Const.OBJECT_CLASS_LINE
441                 ) {
442                     if (type === 2) {
443                         v = 5.1;
444                     } else if (type === 3) {
445                         v = 0.02;
446                     } else if (type === 4 || type === 5 || type === 6) {
447                         v = 3.33;
448                     } else if (type === 7) {
449                         v = 10.0;
450                     } else {
451                         v = 0.05;
452                     }
453                 }
454             }
455             if (type === 7) {
456                 node2.setAttributeNS(null, "fill", "none");
457                 node2.setAttributeNS(null, "stroke-width", 1); // this is the stroke-width of the arrow head.
458             }
459             node2.setAttributeNS(null, "refY", h);
460             node2.setAttributeNS(null, "refX", v);
461 
462             node2.appendChild(node3);
463             return node2;
464         },
465 
466         /**
467          * Updates color of an arrow DOM node.
468          * @param {Node} node The arrow node.
469          * @param {String} color Color value in a HTML compatible format, e.g. <tt>#00ff00</tt> or <tt>green</tt> for green.
470          * @param {Number} opacity
471          * @param {JXG.GeometryElement} el The element the arrows are to be attached to
472          */
473         _setArrowColor: function (node, color, opacity, el, type) {
474             if (node) {
475                 if (Type.isString(color)) {
476                     if (type !== 7) {
477                         this._setAttribute(function () {
478                             node.setAttributeNS(null, "stroke", color);
479                             node.setAttributeNS(null, "fill", color);
480                             node.setAttributeNS(null, "stroke-opacity", opacity);
481                             node.setAttributeNS(null, "fill-opacity", opacity);
482                         }, el.visPropOld.fillcolor);
483                     } else {
484                         this._setAttribute(function () {
485                             node.setAttributeNS(null, "fill", "none");
486                             node.setAttributeNS(null, "stroke", color);
487                             node.setAttributeNS(null, "stroke-opacity", opacity);
488                         }, el.visPropOld.fillcolor);
489                     }
490                 }
491 
492                 if (this.isIE) {
493                     el.rendNode.parentNode.insertBefore(el.rendNode, el.rendNode);
494                 }
495             }
496         },
497 
498         // Already documented in JXG.AbstractRenderer
499         _setArrowWidth: function (node, width, parentNode, size) {
500             var s, d;
501 
502             if (node) {
503                 // if (width === 0) {
504                 //     // display:none does not work well in webkit
505                 //     node.setAttributeNS(null, 'display', 'none');
506                 // } else {
507                 s = width;
508                 d = s * size;
509                 node.setAttributeNS(null, "viewBox", 0 + " " + 0 + " " + s * 10 + " " + s * 10);
510                 node.setAttributeNS(null, "markerHeight", d);
511                 node.setAttributeNS(null, "markerWidth", d);
512                 node.setAttributeNS(null, "display", "inherit");
513                 // }
514 
515                 if (this.isIE) {
516                     parentNode.parentNode.insertBefore(parentNode, parentNode);
517                 }
518             }
519         },
520 
521         /* ******************************** *
522          *  This renderer does not need to
523          *  override draw/update* methods
524          *  since it provides draw/update*Prim
525          *  methods except for some cases like
526          *  internal texts or images.
527          * ******************************** */
528 
529         /* **************************
530          *    Lines
531          * **************************/
532 
533         // documented in AbstractRenderer
534         updateTicks: function (ticks) {
535             var i,
536                 j,
537                 c,
538                 node,
539                 x,
540                 y,
541                 tickStr = "",
542                 len = ticks.ticks.length,
543                 len2,
544                 str,
545                 isReal = true;
546 
547             for (i = 0; i < len; i++) {
548                 c = ticks.ticks[i];
549                 x = c[0];
550                 y = c[1];
551 
552                 len2 = x.length;
553                 str = " M " + x[0] + " " + y[0];
554                 if (!Type.isNumber(x[0])) {
555                     isReal = false;
556                 }
557                 for (j = 1; isReal && j < len2; ++j) {
558                     if (Type.isNumber(x[j])) {
559                         str += " L " + x[j] + " " + y[j];
560                     } else {
561                         isReal = false;
562                     }
563                 }
564                 if (isReal) {
565                     tickStr += str;
566                 }
567             }
568 
569             node = ticks.rendNode;
570 
571             if (!Type.exists(node)) {
572                 node = this.createPrim("path", ticks.id);
573                 this.appendChildPrim(node, Type.evaluate(ticks.visProp.layer));
574                 ticks.rendNode = node;
575             }
576 
577             node.setAttributeNS(null, "stroke", Type.evaluate(ticks.visProp.strokecolor));
578             node.setAttributeNS(null, "fill", "none");
579             // node.setAttributeNS(null, 'fill', Type.evaluate(ticks.visProp.fillcolor));
580             // node.setAttributeNS(null, 'fill-opacity', Type.evaluate(ticks.visProp.fillopacity));
581             node.setAttributeNS(
582                 null,
583                 "stroke-opacity",
584                 Type.evaluate(ticks.visProp.strokeopacity)
585             );
586             node.setAttributeNS(null, "stroke-width", Type.evaluate(ticks.visProp.strokewidth));
587             this.updatePathPrim(node, tickStr, ticks.board);
588         },
589 
590         /* **************************
591          *    Text related stuff
592          * **************************/
593 
594         // Already documented in JXG.AbstractRenderer
595         displayCopyright: function (str, fontsize) {
596             var node = this.createPrim("text", 'licenseText'),
597                 t;
598             node.setAttributeNS(null, 'x', '20px');
599             node.setAttributeNS(null, 'y', 2 + fontsize + 'px');
600             node.setAttributeNS(null, 'style', 'font-family:Arial,Helvetica,sans-serif; font-size:' +
601                 fontsize + 'px; fill:#356AA0;  opacity:0.3;');
602             t = this.container.ownerDocument.createTextNode(str);
603             node.appendChild(t);
604             this.appendChildPrim(node, 0);
605         },
606 
607         // Already documented in JXG.AbstractRenderer
608         drawInternalText: function (el) {
609             var node = this.createPrim("text", el.id);
610 
611             //node.setAttributeNS(null, "style", "alignment-baseline:middle"); // Not yet supported by Firefox
612             // Preserve spaces
613             //node.setAttributeNS("http://www.w3.org/XML/1998/namespace", "space", "preserve");
614             node.style.whiteSpace = "nowrap";
615 
616             el.rendNodeText = this.container.ownerDocument.createTextNode("");
617             node.appendChild(el.rendNodeText);
618             this.appendChildPrim(node, Type.evaluate(el.visProp.layer));
619 
620             return node;
621         },
622 
623         // Already documented in JXG.AbstractRenderer
624         updateInternalText: function (el) {
625             var content = el.plaintext,
626                 v,
627                 ev_ax = el.getAnchorX(),
628                 ev_ay = el.getAnchorY();
629 
630             if (el.rendNode.getAttributeNS(null, "class") !== el.visProp.cssclass) {
631                 el.rendNode.setAttributeNS(null, "class", Type.evaluate(el.visProp.cssclass));
632                 el.needsSizeUpdate = true;
633             }
634 
635             if (!isNaN(el.coords.scrCoords[1] + el.coords.scrCoords[2])) {
636                 // Horizontal
637                 v = el.coords.scrCoords[1];
638                 if (el.visPropOld.left !== ev_ax + v) {
639                     el.rendNode.setAttributeNS(null, "x", v + "px");
640 
641                     if (ev_ax === "left") {
642                         el.rendNode.setAttributeNS(null, "text-anchor", "start");
643                     } else if (ev_ax === "right") {
644                         el.rendNode.setAttributeNS(null, "text-anchor", "end");
645                     } else if (ev_ax === "middle") {
646                         el.rendNode.setAttributeNS(null, "text-anchor", "middle");
647                     }
648                     el.visPropOld.left = ev_ax + v;
649                 }
650 
651                 // Vertical
652                 v = el.coords.scrCoords[2];
653                 if (el.visPropOld.top !== ev_ay + v) {
654                     el.rendNode.setAttributeNS(null, "y", v + this.vOffsetText * 0.5 + "px");
655 
656                     if (ev_ay === "bottom") {
657                         el.rendNode.setAttributeNS(
658                             null,
659                             "dominant-baseline",
660                             "text-after-edge"
661                         );
662                     } else if (ev_ay === "top") {
663                         el.rendNode.setAttributeNS(null, "dy", "1.6ex");
664                         //el.rendNode.setAttributeNS(null, 'dominant-baseline', 'text-before-edge'); // Not supported by IE, edge
665                     } else if (ev_ay === "middle") {
666                         //el.rendNode.setAttributeNS(null, 'dominant-baseline', 'middle');
667                         el.rendNode.setAttributeNS(null, "dy", "0.6ex");
668                     }
669                     el.visPropOld.top = ev_ay + v;
670                 }
671             }
672             if (el.htmlStr !== content) {
673                 el.rendNodeText.data = content;
674                 el.htmlStr = content;
675             }
676             this.transformImage(el, el.transformations);
677         },
678 
679         /**
680          * Set color and opacity of internal texts.
681          * SVG needs its own version.
682          * @private
683          * @see JXG.AbstractRenderer#updateTextStyle
684          * @see JXG.AbstractRenderer#updateInternalTextStyle
685          */
686         updateInternalTextStyle: function (el, strokeColor, strokeOpacity, duration) {
687             this.setObjectFillColor(el, strokeColor, strokeOpacity);
688         },
689 
690         /* **************************
691          *    Image related stuff
692          * **************************/
693 
694         // Already documented in JXG.AbstractRenderer
695         drawImage: function (el) {
696             var node = this.createPrim("image", el.id);
697 
698             node.setAttributeNS(null, "preserveAspectRatio", "none");
699             this.appendChildPrim(node, Type.evaluate(el.visProp.layer));
700             el.rendNode = node;
701 
702             this.updateImage(el);
703         },
704 
705         // Already documented in JXG.AbstractRenderer
706         transformImage: function (el, t) {
707             var s, m,
708                 node = el.rendNode,
709                 str = "",
710                 cx, cy,
711                 len = t.length;
712 
713             if (len > 0) {
714                 m = this.joinTransforms(el, t);
715                 s = [m[1][1], m[2][1], m[1][2], m[2][2], m[1][0], m[2][0]].join(",");
716                 if (s.indexOf('NaN') === -1) {
717                     str += " matrix(" + s + ") ";
718                     if (el.elementClass === Const.OBJECT_CLASS_TEXT && el.visProp.display === 'html') {
719                         node.style.transform = str;
720                         cx = -el.coords.scrCoords[1];
721                         cy = -el.coords.scrCoords[2];
722                         switch (Type.evaluate(el.visProp.anchorx)) {
723                             case 'right': cx += el.size[0]; break;
724                             case 'middle': cx += el.size[0] * 0.5; break;
725                         }
726                         switch (Type.evaluate(el.visProp.anchory)) {
727                             case 'bottom': cy += el.size[1]; break;
728                             case 'middle': cy += el.size[1] * 0.5; break;
729                         }
730                         node.style['transform-origin'] = (cx) + 'px ' + (cy) + 'px';
731                     } else {
732                         // Images and texts with display:'internal'
733                         node.setAttributeNS(null, "transform", str);
734                     }
735                 }
736             }
737         },
738 
739         // Already documented in JXG.AbstractRenderer
740         updateImageURL: function (el) {
741             var url = Type.evaluate(el.url);
742 
743             if (el._src !== url) {
744                 el.imgIsLoaded = false;
745                 el.rendNode.setAttributeNS(this.xlinkNamespace, "xlink:href", url);
746                 el._src = url;
747 
748                 return true;
749             }
750 
751             return false;
752         },
753 
754         // Already documented in JXG.AbstractRenderer
755         updateImageStyle: function (el, doHighlight) {
756             var css = Type.evaluate(
757                 doHighlight ? el.visProp.highlightcssclass : el.visProp.cssclass
758             );
759 
760             el.rendNode.setAttributeNS(null, "class", css);
761         },
762 
763         // Already documented in JXG.AbstractRenderer
764         drawForeignObject: function (el) {
765             el.rendNode = this.appendChildPrim(
766                 this.createPrim("foreignObject", el.id),
767                 Type.evaluate(el.visProp.layer)
768             );
769 
770             this.appendNodesToElement(el, "foreignObject");
771             this.updateForeignObject(el);
772         },
773 
774         // Already documented in JXG.AbstractRenderer
775         updateForeignObject: function (el) {
776             if (el._useUserSize) {
777                 el.rendNode.style.overflow = "hidden";
778             } else {
779                 el.rendNode.style.overflow = "visible";
780             }
781 
782             this.updateRectPrim(
783                 el.rendNode,
784                 el.coords.scrCoords[1],
785                 el.coords.scrCoords[2] - el.size[1],
786                 el.size[0],
787                 el.size[1]
788             );
789 
790             el.rendNode.innerHTML = el.content;
791             this._updateVisual(el, { stroke: true, dash: true }, true);
792         },
793 
794         /* **************************
795          * Render primitive objects
796          * **************************/
797 
798         // Already documented in JXG.AbstractRenderer
799         appendChildPrim: function (node, level) {
800             if (!Type.exists(level)) {
801                 // trace nodes have level not set
802                 level = 0;
803             } else if (level >= Options.layer.numlayers) {
804                 level = Options.layer.numlayers - 1;
805             }
806 
807             this.layer[level].appendChild(node);
808 
809             return node;
810         },
811 
812         // Already documented in JXG.AbstractRenderer
813         createPrim: function (type, id) {
814             var node = this.container.ownerDocument.createElementNS(this.svgNamespace, type);
815             node.setAttributeNS(null, "id", this.uniqName(id));
816             node.style.position = "absolute";
817             if (type === "path") {
818                 node.setAttributeNS(null, "stroke-linecap", "round");
819                 node.setAttributeNS(null, "stroke-linejoin", "round");
820                 node.setAttributeNS(null, "fill-rule", "evenodd");
821             }
822             return node;
823         },
824 
825         // Already documented in JXG.AbstractRenderer
826         remove: function (shape) {
827             if (Type.exists(shape) && Type.exists(shape.parentNode)) {
828                 shape.parentNode.removeChild(shape);
829             }
830         },
831 
832         // Already documented in JXG.AbstractRenderer
833         setLayer: function (el, level) {
834             if (!Type.exists(level)) {
835                 level = 0;
836             } else if (level >= Options.layer.numlayers) {
837                 level = Options.layer.numlayers - 1;
838             }
839 
840             this.layer[level].appendChild(el.rendNode);
841         },
842 
843         // Already documented in JXG.AbstractRenderer
844         makeArrows: function (el, a) {
845             var node2,
846                 ev_fa = a.evFirst,
847                 ev_la = a.evLast;
848 
849             // Test if the arrow heads already exist
850             if (el.visPropOld.firstarrow === ev_fa && el.visPropOld.lastarrow === ev_la) {
851                 if (this.isIE && el.visPropCalc.visible && (ev_fa || ev_la)) {
852                     el.rendNode.parentNode.insertBefore(el.rendNode, el.rendNode);
853                 }
854                 return;
855             }
856 
857             if (ev_fa) {
858                 node2 = el.rendNodeTriangleStart;
859                 if (!Type.exists(node2)) {
860                     node2 = this._createArrowHead(el, "End", a.typeFirst);
861                     this.defs.appendChild(node2);
862                     el.rendNodeTriangleStart = node2;
863                     el.rendNode.setAttributeNS(
864                         null,
865                         "marker-start",
866                         // "url(#" + this.container.id + "_" + el.id + "TriangleEnd)"
867                         this.toURL(this.container.id, '_', el.id, 'TriangleEnd')
868                     );
869                 } else {
870                     this.defs.appendChild(node2);
871                 }
872             } else {
873                 node2 = el.rendNodeTriangleStart;
874                 if (Type.exists(node2)) {
875                     this.remove(node2);
876                 }
877             }
878             if (ev_la) {
879                 node2 = el.rendNodeTriangleEnd;
880                 if (!Type.exists(node2)) {
881                     node2 = this._createArrowHead(el, "Start", a.typeLast);
882                     this.defs.appendChild(node2);
883                     el.rendNodeTriangleEnd = node2;
884                     el.rendNode.setAttributeNS(
885                         null,
886                         "marker-end",
887                         // "url(#" + this.container.id + "_" + el.id + "TriangleStart)"
888                         this.toURL(this.container.id, '_', el.id, 'TriangleStart')
889                     );
890                 } else {
891                     this.defs.appendChild(node2);
892                 }
893             } else {
894                 node2 = el.rendNodeTriangleEnd;
895                 if (Type.exists(node2)) {
896                     this.remove(node2);
897                 }
898             }
899             el.visPropOld.firstarrow = ev_fa;
900             el.visPropOld.lastarrow = ev_la;
901         },
902 
903         // Already documented in JXG.AbstractRenderer
904         updateEllipsePrim: function (node, x, y, rx, ry) {
905             var huge = 1000000;
906 
907             huge = 200000; // IE
908             // webkit does not like huge values if the object is dashed
909             // iE doesn't like huge values above 216000
910             x = Math.abs(x) < huge ? x : (huge * x) / Math.abs(x);
911             y = Math.abs(y) < huge ? y : (huge * y) / Math.abs(y);
912             rx = Math.abs(rx) < huge ? rx : (huge * rx) / Math.abs(rx);
913             ry = Math.abs(ry) < huge ? ry : (huge * ry) / Math.abs(ry);
914 
915             node.setAttributeNS(null, "cx", x);
916             node.setAttributeNS(null, "cy", y);
917             node.setAttributeNS(null, "rx", Math.abs(rx));
918             node.setAttributeNS(null, "ry", Math.abs(ry));
919         },
920 
921         // Already documented in JXG.AbstractRenderer
922         updateLinePrim: function (node, p1x, p1y, p2x, p2y) {
923             var huge = 1000000;
924 
925             huge = 200000; //IE
926             if (!isNaN(p1x + p1y + p2x + p2y)) {
927                 // webkit does not like huge values if the object is dashed
928                 // IE doesn't like huge values above 216000
929                 p1x = Math.abs(p1x) < huge ? p1x : (huge * p1x) / Math.abs(p1x);
930                 p1y = Math.abs(p1y) < huge ? p1y : (huge * p1y) / Math.abs(p1y);
931                 p2x = Math.abs(p2x) < huge ? p2x : (huge * p2x) / Math.abs(p2x);
932                 p2y = Math.abs(p2y) < huge ? p2y : (huge * p2y) / Math.abs(p2y);
933 
934                 node.setAttributeNS(null, "x1", p1x);
935                 node.setAttributeNS(null, "y1", p1y);
936                 node.setAttributeNS(null, "x2", p2x);
937                 node.setAttributeNS(null, "y2", p2y);
938             }
939         },
940 
941         // Already documented in JXG.AbstractRenderer
942         updatePathPrim: function (node, pointString) {
943             if (pointString === "") {
944                 pointString = "M 0 0";
945             }
946             node.setAttributeNS(null, "d", pointString);
947         },
948 
949         // Already documented in JXG.AbstractRenderer
950         updatePathStringPoint: function (el, size, type) {
951             var s = "",
952                 scr = el.coords.scrCoords,
953                 sqrt32 = size * Math.sqrt(3) * 0.5,
954                 s05 = size * 0.5;
955 
956             if (type === "x") {
957                 s =
958                     " M " +
959                     (scr[1] - size) +
960                     " " +
961                     (scr[2] - size) +
962                     " L " +
963                     (scr[1] + size) +
964                     " " +
965                     (scr[2] + size) +
966                     " M " +
967                     (scr[1] + size) +
968                     " " +
969                     (scr[2] - size) +
970                     " L " +
971                     (scr[1] - size) +
972                     " " +
973                     (scr[2] + size);
974             } else if (type === "+") {
975                 s =
976                     " M " +
977                     (scr[1] - size) +
978                     " " +
979                     scr[2] +
980                     " L " +
981                     (scr[1] + size) +
982                     " " +
983                     scr[2] +
984                     " M " +
985                     scr[1] +
986                     " " +
987                     (scr[2] - size) +
988                     " L " +
989                     scr[1] +
990                     " " +
991                     (scr[2] + size);
992             } else if (type === "|") {
993                 s =
994                     " M " +
995                     scr[1] +
996                     " " +
997                     (scr[2] - size) +
998                     " L " +
999                     scr[1] +
1000                     " " +
1001                     (scr[2] + size);
1002             } else if (type === "-") {
1003                 s =
1004                     " M " +
1005                     (scr[1] - size) +
1006                     " " +
1007                     scr[2] +
1008                     " L " +
1009                     (scr[1] + size) +
1010                     " " +
1011                     scr[2];
1012             } else if (type === "<>") {
1013                 s =
1014                     " M " +
1015                     (scr[1] - size) +
1016                     " " +
1017                     scr[2] +
1018                     " L " +
1019                     scr[1] +
1020                     " " +
1021                     (scr[2] + size) +
1022                     " L " +
1023                     (scr[1] + size) +
1024                     " " +
1025                     scr[2] +
1026                     " L " +
1027                     scr[1] +
1028                     " " +
1029                     (scr[2] - size) +
1030                     " Z ";
1031             } else if (type === "^") {
1032                 s =
1033                     " M " +
1034                     scr[1] +
1035                     " " +
1036                     (scr[2] - size) +
1037                     " L " +
1038                     (scr[1] - sqrt32) +
1039                     " " +
1040                     (scr[2] + s05) +
1041                     " L " +
1042                     (scr[1] + sqrt32) +
1043                     " " +
1044                     (scr[2] + s05) +
1045                     " Z "; // close path
1046             } else if (type === "v") {
1047                 s =
1048                     " M " +
1049                     scr[1] +
1050                     " " +
1051                     (scr[2] + size) +
1052                     " L " +
1053                     (scr[1] - sqrt32) +
1054                     " " +
1055                     (scr[2] - s05) +
1056                     " L " +
1057                     (scr[1] + sqrt32) +
1058                     " " +
1059                     (scr[2] - s05) +
1060                     " Z ";
1061             } else if (type === ">") {
1062                 s =
1063                     " M " +
1064                     (scr[1] + size) +
1065                     " " +
1066                     scr[2] +
1067                     " L " +
1068                     (scr[1] - s05) +
1069                     " " +
1070                     (scr[2] - sqrt32) +
1071                     " L " +
1072                     (scr[1] - s05) +
1073                     " " +
1074                     (scr[2] + sqrt32) +
1075                     " Z ";
1076             } else if (type === "<") {
1077                 s =
1078                     " M " +
1079                     (scr[1] - size) +
1080                     " " +
1081                     scr[2] +
1082                     " L " +
1083                     (scr[1] + s05) +
1084                     " " +
1085                     (scr[2] - sqrt32) +
1086                     " L " +
1087                     (scr[1] + s05) +
1088                     " " +
1089                     (scr[2] + sqrt32) +
1090                     " Z ";
1091             }
1092             return s;
1093         },
1094 
1095         // Already documented in JXG.AbstractRenderer
1096         updatePathStringPrim: function (el) {
1097             var i,
1098                 scr,
1099                 len,
1100                 symbm = " M ",
1101                 symbl = " L ",
1102                 symbc = " C ",
1103                 nextSymb = symbm,
1104                 maxSize = 5000.0,
1105                 pStr = "";
1106 
1107             if (el.numberPoints <= 0) {
1108                 return "";
1109             }
1110 
1111             len = Math.min(el.points.length, el.numberPoints);
1112 
1113             if (el.bezierDegree === 1) {
1114                 for (i = 0; i < len; i++) {
1115                     scr = el.points[i].scrCoords;
1116                     if (isNaN(scr[1]) || isNaN(scr[2])) {
1117                         // PenUp
1118                         nextSymb = symbm;
1119                     } else {
1120                         // Chrome has problems with values being too far away.
1121                         scr[1] = Math.max(Math.min(scr[1], maxSize), -maxSize);
1122                         scr[2] = Math.max(Math.min(scr[2], maxSize), -maxSize);
1123 
1124                         // Attention: first coordinate may be inaccurate if far way
1125                         //pStr += [nextSymb, scr[1], ' ', scr[2]].join('');
1126                         pStr += nextSymb + scr[1] + " " + scr[2]; // Seems to be faster now (webkit and firefox)
1127                         nextSymb = symbl;
1128                     }
1129                 }
1130             } else if (el.bezierDegree === 3) {
1131                 i = 0;
1132                 while (i < len) {
1133                     scr = el.points[i].scrCoords;
1134                     if (isNaN(scr[1]) || isNaN(scr[2])) {
1135                         // PenUp
1136                         nextSymb = symbm;
1137                     } else {
1138                         pStr += nextSymb + scr[1] + " " + scr[2];
1139                         if (nextSymb === symbc) {
1140                             i += 1;
1141                             scr = el.points[i].scrCoords;
1142                             pStr += " " + scr[1] + " " + scr[2];
1143                             i += 1;
1144                             scr = el.points[i].scrCoords;
1145                             pStr += " " + scr[1] + " " + scr[2];
1146                         }
1147                         nextSymb = symbc;
1148                     }
1149                     i += 1;
1150                 }
1151             }
1152             return pStr;
1153         },
1154 
1155         // Already documented in JXG.AbstractRenderer
1156         updatePathStringBezierPrim: function (el) {
1157             var i,
1158                 j,
1159                 k,
1160                 scr,
1161                 lx,
1162                 ly,
1163                 len,
1164                 symbm = " M ",
1165                 symbl = " C ",
1166                 nextSymb = symbm,
1167                 maxSize = 5000.0,
1168                 pStr = "",
1169                 f = Type.evaluate(el.visProp.strokewidth),
1170                 isNoPlot = Type.evaluate(el.visProp.curvetype) !== "plot";
1171 
1172             if (el.numberPoints <= 0) {
1173                 return "";
1174             }
1175 
1176             if (isNoPlot && el.board.options.curve.RDPsmoothing) {
1177                 el.points = Numerics.RamerDouglasPeucker(el.points, 0.5);
1178             }
1179 
1180             len = Math.min(el.points.length, el.numberPoints);
1181             for (j = 1; j < 3; j++) {
1182                 nextSymb = symbm;
1183                 for (i = 0; i < len; i++) {
1184                     scr = el.points[i].scrCoords;
1185 
1186                     if (isNaN(scr[1]) || isNaN(scr[2])) {
1187                         // PenUp
1188                         nextSymb = symbm;
1189                     } else {
1190                         // Chrome has problems with values being too far away.
1191                         scr[1] = Math.max(Math.min(scr[1], maxSize), -maxSize);
1192                         scr[2] = Math.max(Math.min(scr[2], maxSize), -maxSize);
1193 
1194                         // Attention: first coordinate may be inaccurate if far way
1195                         if (nextSymb === symbm) {
1196                             //pStr += [nextSymb, scr[1], ' ', scr[2]].join('');
1197                             pStr += nextSymb + scr[1] + " " + scr[2]; // Seems to be faster now (webkit and firefox)
1198                         } else {
1199                             k = 2 * j;
1200                             pStr += [
1201                                 nextSymb,
1202                                 lx + (scr[1] - lx) * 0.333 + f * (k * Math.random() - j),
1203                                 " ",
1204                                 ly + (scr[2] - ly) * 0.333 + f * (k * Math.random() - j),
1205                                 " ",
1206                                 lx + (scr[1] - lx) * 0.666 + f * (k * Math.random() - j),
1207                                 " ",
1208                                 ly + (scr[2] - ly) * 0.666 + f * (k * Math.random() - j),
1209                                 " ",
1210                                 scr[1],
1211                                 " ",
1212                                 scr[2]
1213                             ].join("");
1214                         }
1215 
1216                         nextSymb = symbl;
1217                         lx = scr[1];
1218                         ly = scr[2];
1219                     }
1220                 }
1221             }
1222             return pStr;
1223         },
1224 
1225         // Already documented in JXG.AbstractRenderer
1226         updatePolygonPrim: function (node, el) {
1227             var i,
1228                 pStr = "",
1229                 scrCoords,
1230                 len = el.vertices.length;
1231 
1232             node.setAttributeNS(null, "stroke", "none");
1233             node.setAttributeNS(null, "fill-rule", "evenodd");
1234             if (el.elType === "polygonalchain") {
1235                 len++;
1236             }
1237 
1238             for (i = 0; i < len - 1; i++) {
1239                 if (el.vertices[i].isReal) {
1240                     scrCoords = el.vertices[i].coords.scrCoords;
1241                     pStr = pStr + scrCoords[1] + "," + scrCoords[2];
1242                 } else {
1243                     node.setAttributeNS(null, "points", "");
1244                     return;
1245                 }
1246 
1247                 if (i < len - 2) {
1248                     pStr += " ";
1249                 }
1250             }
1251             if (pStr.indexOf("NaN") === -1) {
1252                 node.setAttributeNS(null, "points", pStr);
1253             }
1254         },
1255 
1256         // Already documented in JXG.AbstractRenderer
1257         updateRectPrim: function (node, x, y, w, h) {
1258             node.setAttributeNS(null, "x", x);
1259             node.setAttributeNS(null, "y", y);
1260             node.setAttributeNS(null, "width", w);
1261             node.setAttributeNS(null, "height", h);
1262         },
1263 
1264         /* **************************
1265          *  Set Attributes
1266          * **************************/
1267 
1268         // documented in JXG.AbstractRenderer
1269         setPropertyPrim: function (node, key, val) {
1270             if (key === "stroked") {
1271                 return;
1272             }
1273             node.setAttributeNS(null, key, val);
1274         },
1275 
1276         display: function (el, val) {
1277             var node;
1278 
1279             if (el && el.rendNode) {
1280                 el.visPropOld.visible = val;
1281                 node = el.rendNode;
1282                 if (val) {
1283                     node.setAttributeNS(null, "display", "inline");
1284                     node.style.visibility = "inherit";
1285                 } else {
1286                     node.setAttributeNS(null, "display", "none");
1287                     node.style.visibility = "hidden";
1288                 }
1289             }
1290         },
1291 
1292         // documented in JXG.AbstractRenderer
1293         show: function (el) {
1294             JXG.deprecated("Board.renderer.show()", "Board.renderer.display()");
1295             this.display(el, true);
1296             // var node;
1297             //
1298             // if (el && el.rendNode) {
1299             //     node = el.rendNode;
1300             //     node.setAttributeNS(null, 'display', 'inline');
1301             //     node.style.visibility = "inherit";
1302             // }
1303         },
1304 
1305         // documented in JXG.AbstractRenderer
1306         hide: function (el) {
1307             JXG.deprecated("Board.renderer.hide()", "Board.renderer.display()");
1308             this.display(el, false);
1309             // var node;
1310             //
1311             // if (el && el.rendNode) {
1312             //     node = el.rendNode;
1313             //     node.setAttributeNS(null, 'display', 'none');
1314             //     node.style.visibility = "hidden";
1315             // }
1316         },
1317 
1318         // documented in JXG.AbstractRenderer
1319         setBuffering: function (el, type) {
1320             el.rendNode.setAttribute("buffered-rendering", type);
1321         },
1322 
1323         // documented in JXG.AbstractRenderer
1324         setDashStyle: function (el) {
1325             var dashStyle = Type.evaluate(el.visProp.dash),
1326                 ds = Type.evaluate(el.visProp.dashscale),
1327                 sw = ds ? 0.5 * Type.evaluate(el.visProp.strokewidth) : 1,
1328                 node = el.rendNode;
1329 
1330             if (dashStyle > 0) {
1331                 node.setAttributeNS(null, "stroke-dasharray",
1332                     // sw could distinguish highlighting or not.
1333                     // But it seems to preferable to ignore this.
1334                     this.dashArray[dashStyle - 1].map(function (x) { return x * sw; }).join(',')
1335                 );
1336             } else {
1337                 if (node.hasAttributeNS(null, "stroke-dasharray")) {
1338                     node.removeAttributeNS(null, "stroke-dasharray");
1339                 }
1340             }
1341         },
1342 
1343         // documented in JXG.AbstractRenderer
1344         setGradient: function (el) {
1345             var fillNode = el.rendNode,
1346                 node, node2, node3,
1347                 ev_g = Type.evaluate(el.visProp.gradient);
1348 
1349             if (ev_g === "linear" || ev_g === "radial") {
1350                 node = this.createPrim(ev_g + "Gradient", el.id + "_gradient");
1351                 node2 = this.createPrim("stop", el.id + "_gradient1");
1352                 node3 = this.createPrim("stop", el.id + "_gradient2");
1353                 node.appendChild(node2);
1354                 node.appendChild(node3);
1355                 this.defs.appendChild(node);
1356                 fillNode.setAttributeNS(
1357                     null,
1358                     'style',
1359                     // "fill:url(#" + this.container.id + "_" + el.id + "_gradient)"
1360                     'fill:' + this.toURL(this.container.id + '_' + el.id + '_gradient')
1361                 );
1362                 el.gradNode1 = node2;
1363                 el.gradNode2 = node3;
1364                 el.gradNode = node;
1365             } else {
1366                 fillNode.removeAttributeNS(null, "style");
1367             }
1368         },
1369 
1370         /**
1371          * Set the gradient angle for linear color gradients.
1372          *
1373          * @private
1374          * @param {SVGnode} node SVG gradient node of an arbitrary JSXGraph element.
1375          * @param {Number} radians angle value in radians. 0 is horizontal from left to right, Pi/4 is vertical from top to bottom.
1376          */
1377         updateGradientAngle: function (node, radians) {
1378             // Angles:
1379             // 0: ->
1380             // 90: down
1381             // 180: <-
1382             // 90: up
1383             var f = 1.0,
1384                 co = Math.cos(radians),
1385                 si = Math.sin(radians);
1386 
1387             if (Math.abs(co) > Math.abs(si)) {
1388                 f /= Math.abs(co);
1389             } else {
1390                 f /= Math.abs(si);
1391             }
1392 
1393             if (co >= 0) {
1394                 node.setAttributeNS(null, "x1", 0);
1395                 node.setAttributeNS(null, "x2", co * f);
1396             } else {
1397                 node.setAttributeNS(null, "x1", -co * f);
1398                 node.setAttributeNS(null, "x2", 0);
1399             }
1400             if (si >= 0) {
1401                 node.setAttributeNS(null, "y1", 0);
1402                 node.setAttributeNS(null, "y2", si * f);
1403             } else {
1404                 node.setAttributeNS(null, "y1", -si * f);
1405                 node.setAttributeNS(null, "y2", 0);
1406             }
1407         },
1408 
1409         /**
1410          * Set circles for radial color gradients.
1411          *
1412          * @private
1413          * @param {SVGnode} node SVG gradient node
1414          * @param {Number} cx SVG value cx (value between 0 and 1)
1415          * @param {Number} cy  SVG value cy (value between 0 and 1)
1416          * @param {Number} r  SVG value r (value between 0 and 1)
1417          * @param {Number} fx  SVG value fx (value between 0 and 1)
1418          * @param {Number} fy  SVG value fy (value between 0 and 1)
1419          * @param {Number} fr  SVG value fr (value between 0 and 1)
1420          */
1421         updateGradientCircle: function (node, cx, cy, r, fx, fy, fr) {
1422             node.setAttributeNS(null, "cx", cx * 100 + "%"); // Center first color
1423             node.setAttributeNS(null, "cy", cy * 100 + "%");
1424             node.setAttributeNS(null, "r", r * 100 + "%");
1425             node.setAttributeNS(null, "fx", fx * 100 + "%"); // Center second color / focal point
1426             node.setAttributeNS(null, "fy", fy * 100 + "%");
1427             node.setAttributeNS(null, "fr", fr * 100 + "%");
1428         },
1429 
1430         // documented in JXG.AbstractRenderer
1431         updateGradient: function (el) {
1432             var col,
1433                 op,
1434                 node2 = el.gradNode1,
1435                 node3 = el.gradNode2,
1436                 ev_g = Type.evaluate(el.visProp.gradient);
1437 
1438             if (!Type.exists(node2) || !Type.exists(node3)) {
1439                 return;
1440             }
1441 
1442             op = Type.evaluate(el.visProp.fillopacity);
1443             op = op > 0 ? op : 0;
1444             col = Type.evaluate(el.visProp.fillcolor);
1445 
1446             node2.setAttributeNS(null, "style", "stop-color:" + col + ";stop-opacity:" + op);
1447             node3.setAttributeNS(
1448                 null,
1449                 "style",
1450                 "stop-color:" +
1451                 Type.evaluate(el.visProp.gradientsecondcolor) +
1452                 ";stop-opacity:" +
1453                 Type.evaluate(el.visProp.gradientsecondopacity)
1454             );
1455             node2.setAttributeNS(
1456                 null,
1457                 "offset",
1458                 Type.evaluate(el.visProp.gradientstartoffset) * 100 + "%"
1459             );
1460             node3.setAttributeNS(
1461                 null,
1462                 "offset",
1463                 Type.evaluate(el.visProp.gradientendoffset) * 100 + "%"
1464             );
1465             if (ev_g === "linear") {
1466                 this.updateGradientAngle(el.gradNode, Type.evaluate(el.visProp.gradientangle));
1467             } else if (ev_g === "radial") {
1468                 this.updateGradientCircle(
1469                     el.gradNode,
1470                     Type.evaluate(el.visProp.gradientcx),
1471                     Type.evaluate(el.visProp.gradientcy),
1472                     Type.evaluate(el.visProp.gradientr),
1473                     Type.evaluate(el.visProp.gradientfx),
1474                     Type.evaluate(el.visProp.gradientfy),
1475                     Type.evaluate(el.visProp.gradientfr)
1476                 );
1477             }
1478         },
1479 
1480         // documented in JXG.AbstractRenderer
1481         setObjectTransition: function (el, duration) {
1482             var node, props,
1483                 transitionArr = [],
1484                 transitionStr,
1485                 i,
1486                 len = 0,
1487                 nodes = ["rendNode", "rendNodeTriangleStart", "rendNodeTriangleEnd"];
1488 
1489             if (duration === undefined) {
1490                 duration = Type.evaluate(el.visProp.transitionduration);
1491             }
1492 
1493             props = Type.evaluate(el.visProp.transitionproperties);
1494             if (duration === el.visPropOld.transitionduration &&
1495                 props === el.visPropOld.transitionproperties) {
1496                 return;
1497             }
1498 
1499             // if (
1500             //     el.elementClass === Const.OBJECT_CLASS_TEXT &&
1501             //     Type.evaluate(el.visProp.display) === "html"
1502             // ) {
1503             //     // transitionStr = " color " + duration + "ms," +
1504             //     //     " opacity " + duration + "ms";
1505             //     transitionStr = " all " + duration + "ms ease";
1506             // } else {
1507             //     transitionStr =
1508             //         " fill " + duration + "ms," +
1509             //         " fill-opacity " + duration + "ms," +
1510             //         " stroke " + duration + "ms," +
1511             //         " stroke-opacity " + duration + "ms," +
1512             //         " stroke-width " + duration + "ms," +
1513             //         " width " + duration + "ms," +
1514             //         " height " + duration + "ms," +
1515             //         " rx " + duration + "ms," +
1516             //         " ry " + duration + "ms";
1517             // }
1518 
1519             if (Type.exists(props)) {
1520                 len = props.length;
1521             }
1522             for (i = 0; i < len; i++) {
1523                 transitionArr.push(props[i] + ' ' + duration + 'ms');
1524             }
1525             transitionStr = transitionArr.join(', ');
1526 
1527             len = nodes.length;
1528             for (i = 0; i < len; ++i) {
1529                 if (el[nodes[i]]) {
1530                     node = el[nodes[i]];
1531                     node.style.transition = transitionStr;
1532                 }
1533             }
1534 
1535             el.visPropOld.transitionduration = duration;
1536             el.visPropOld.transitionproperties = props;
1537         },
1538 
1539         /**
1540          * Call user-defined function to set visual attributes.
1541          * If "testAttribute" is the empty string, the function
1542          * is called immediately, otherwise it is called in a timeOut.
1543          *
1544          * This is necessary to realize smooth transitions but avoid transitions
1545          * when first creating the objects.
1546          *
1547          * Usually, the string in testAttribute is the visPropOld attribute
1548          * of the values which are set.
1549          *
1550          * @param {Function} setFunc       Some function which usually sets some attributes
1551          * @param {String} testAttribute If this string is the empty string  the function is called immediately,
1552          *                               otherwise it is called in a setImeout.
1553          * @see JXG.SVGRenderer#setObjectFillColor
1554          * @see JXG.SVGRenderer#setObjectStrokeColor
1555          * @see JXG.SVGRenderer#_setArrowColor
1556          * @private
1557          */
1558         _setAttribute: function (setFunc, testAttribute) {
1559             if (testAttribute === "") {
1560                 setFunc();
1561             } else {
1562                 window.setTimeout(setFunc, 1);
1563             }
1564         },
1565 
1566         // documented in JXG.AbstractRenderer
1567         setObjectFillColor: function (el, color, opacity, rendNode) {
1568             var node, c, rgbo, oo,
1569                 rgba = Type.evaluate(color),
1570                 o = Type.evaluate(opacity),
1571                 grad = Type.evaluate(el.visProp.gradient);
1572 
1573             o = o > 0 ? o : 0;
1574 
1575             // TODO  save gradient and gradientangle
1576             if (
1577                 el.visPropOld.fillcolor === rgba &&
1578                 el.visPropOld.fillopacity === o &&
1579                 grad === null
1580             ) {
1581                 return;
1582             }
1583 
1584             if (Type.exists(rgba) && rgba !== false) {
1585                 if (rgba.length !== 9) {
1586                     // RGB, not RGBA
1587                     c = rgba;
1588                     oo = o;
1589                 } else {
1590                     // True RGBA, not RGB
1591                     rgbo = Color.rgba2rgbo(rgba);
1592                     c = rgbo[0];
1593                     oo = o * rgbo[1];
1594                 }
1595 
1596                 if (rendNode === undefined) {
1597                     node = el.rendNode;
1598                 } else {
1599                     node = rendNode;
1600                 }
1601 
1602                 if (c !== "none") {
1603                     this._setAttribute(function () {
1604                         node.setAttributeNS(null, "fill", c);
1605                     }, el.visPropOld.fillcolor);
1606                 }
1607 
1608                 if (el.type === JXG.OBJECT_TYPE_IMAGE) {
1609                     this._setAttribute(function () {
1610                         node.setAttributeNS(null, "opacity", oo);
1611                     }, el.visPropOld.fillopacity);
1612                     //node.style['opacity'] = oo;  // This would overwrite values set by CSS class.
1613                 } else {
1614                     if (c === "none") {
1615                         // This is done only for non-images
1616                         // because images have no fill color.
1617                         oo = 0;
1618                         // This is necessary if there is a foreignObject below.
1619                         node.setAttributeNS(null, "pointer-events", "visibleStroke");
1620                     } else {
1621                         // This is the default
1622                         node.setAttributeNS(null, "pointer-events", "visiblePainted");
1623                     }
1624                     this._setAttribute(function () {
1625                         node.setAttributeNS(null, "fill-opacity", oo);
1626                     }, el.visPropOld.fillopacity);
1627                 }
1628 
1629                 if (grad === "linear" || grad === "radial") {
1630                     this.updateGradient(el);
1631                 }
1632             }
1633             el.visPropOld.fillcolor = rgba;
1634             el.visPropOld.fillopacity = o;
1635         },
1636 
1637         // documented in JXG.AbstractRenderer
1638         setObjectStrokeColor: function (el, color, opacity) {
1639             var rgba = Type.evaluate(color),
1640                 c, rgbo,
1641                 o = Type.evaluate(opacity),
1642                 oo, node;
1643 
1644             o = o > 0 ? o : 0;
1645 
1646             if (el.visPropOld.strokecolor === rgba && el.visPropOld.strokeopacity === o) {
1647                 return;
1648             }
1649 
1650             if (Type.exists(rgba) && rgba !== false) {
1651                 if (rgba.length !== 9) {
1652                     // RGB, not RGBA
1653                     c = rgba;
1654                     oo = o;
1655                 } else {
1656                     // True RGBA, not RGB
1657                     rgbo = Color.rgba2rgbo(rgba);
1658                     c = rgbo[0];
1659                     oo = o * rgbo[1];
1660                 }
1661 
1662                 node = el.rendNode;
1663 
1664                 if (el.elementClass === Const.OBJECT_CLASS_TEXT) {
1665                     if (Type.evaluate(el.visProp.display) === "html") {
1666                         this._setAttribute(function () {
1667                             node.style.color = c;
1668                             node.style.opacity = oo;
1669                         }, el.visPropOld.strokecolor);
1670                     } else {
1671                         this._setAttribute(function () {
1672                             node.setAttributeNS(null, "style", "fill:" + c);
1673                             node.setAttributeNS(null, "style", "fill-opacity:" + oo);
1674                         }, el.visPropOld.strokecolor);
1675                     }
1676                 } else {
1677                     this._setAttribute(function () {
1678                         node.setAttributeNS(null, "stroke", c);
1679                         node.setAttributeNS(null, "stroke-opacity", oo);
1680                     }, el.visPropOld.strokecolor);
1681                 }
1682 
1683                 if (
1684                     el.elementClass === Const.OBJECT_CLASS_CURVE ||
1685                     el.elementClass === Const.OBJECT_CLASS_LINE
1686                 ) {
1687                     if (Type.evaluate(el.visProp.firstarrow)) {
1688                         this._setArrowColor(
1689                             el.rendNodeTriangleStart,
1690                             c, oo, el,
1691                             el.visPropCalc.typeFirst
1692                         );
1693                     }
1694 
1695                     if (Type.evaluate(el.visProp.lastarrow)) {
1696                         this._setArrowColor(
1697                             el.rendNodeTriangleEnd,
1698                             c, oo, el,
1699                             el.visPropCalc.typeLast
1700                         );
1701                     }
1702                 }
1703             }
1704 
1705             el.visPropOld.strokecolor = rgba;
1706             el.visPropOld.strokeopacity = o;
1707         },
1708 
1709         // documented in JXG.AbstractRenderer
1710         setObjectStrokeWidth: function (el, width) {
1711             var node,
1712                 w = Type.evaluate(width);
1713 
1714             if (isNaN(w) || el.visPropOld.strokewidth === w) {
1715                 return;
1716             }
1717 
1718             node = el.rendNode;
1719             this.setPropertyPrim(node, "stroked", "true");
1720             if (Type.exists(w)) {
1721                 this.setPropertyPrim(node, "stroke-width", w + "px");
1722 
1723                 // if (el.elementClass === Const.OBJECT_CLASS_CURVE ||
1724                 // el.elementClass === Const.OBJECT_CLASS_LINE) {
1725                 //     if (Type.evaluate(el.visProp.firstarrow)) {
1726                 //         this._setArrowWidth(el.rendNodeTriangleStart, w, el.rendNode);
1727                 //     }
1728                 //
1729                 //     if (Type.evaluate(el.visProp.lastarrow)) {
1730                 //         this._setArrowWidth(el.rendNodeTriangleEnd, w, el.rendNode);
1731                 //     }
1732                 // }
1733             }
1734             el.visPropOld.strokewidth = w;
1735         },
1736 
1737         // documented in JXG.AbstractRenderer
1738         setLineCap: function (el) {
1739             var capStyle = Type.evaluate(el.visProp.linecap);
1740 
1741             if (
1742                 capStyle === undefined ||
1743                 capStyle === "" ||
1744                 el.visPropOld.linecap === capStyle ||
1745                 !Type.exists(el.rendNode)
1746             ) {
1747                 return;
1748             }
1749 
1750             this.setPropertyPrim(el.rendNode, "stroke-linecap", capStyle);
1751             el.visPropOld.linecap = capStyle;
1752         },
1753 
1754         // documented in JXG.AbstractRenderer
1755         setShadow: function (el) {
1756             var ev_s = Type.evaluate(el.visProp.shadow),
1757                 ev_s_json, c, b, bl, o, op, id, node,
1758                 use_board_filter = true,
1759                 show = false;
1760 
1761             ev_s_json = JSON.stringify(ev_s);
1762             if (ev_s_json === el.visPropOld.shadow) {
1763                 return;
1764             }
1765 
1766             if (typeof ev_s === 'boolean') {
1767                 use_board_filter = true;
1768                 show = ev_s;
1769                 c = 'none';
1770                 b = 3;
1771                 bl = 0.1;
1772                 o = [5, 5];
1773                 op = 1;
1774             } else {
1775                 if (Type.evaluate(ev_s.enabled)) {
1776                     use_board_filter = false;
1777                     show = true;
1778                     c = JXG.rgbParser(Type.evaluate(ev_s.color));
1779                     b = Type.evaluate(ev_s.blur);
1780                     bl = Type.evaluate(ev_s.blend);
1781                     o = Type.evaluate(ev_s.offset);
1782                     op = Type.evaluate(ev_s.opacity);
1783                 } else {
1784                     show = false;
1785                 }
1786             }
1787 
1788             if (Type.exists(el.rendNode)) {
1789                 if (show) {
1790                     if (use_board_filter) {
1791                         el.rendNode.setAttributeNS(null, 'filter', this.toURL(this.container.id + '_' + 'f1'))
1792                         // 'url(#' + this.container.id + '_' + 'f1)');
1793                     } else {
1794                         node = this.container.ownerDocument.getElementById(id);
1795                         if (node) {
1796                             this.defs.removeChild(node);
1797                         }
1798                         id = el.rendNode.id + '_' + 'f1';
1799                         this.defs.appendChild(this.createShadowFilter(id, c, op, bl, b, o));
1800                         el.rendNode.setAttributeNS(null, 'filter', this.toURL(id));
1801                         // 'url(#' + id + ')');
1802                     }
1803                 } else {
1804                     el.rendNode.removeAttributeNS(null, 'filter');
1805                 }
1806             }
1807 
1808             el.visPropOld.shadow = ev_s_json;
1809         },
1810 
1811         /* **************************
1812          * renderer control
1813          * **************************/
1814 
1815         // documented in JXG.AbstractRenderer
1816         suspendRedraw: function () {
1817             // It seems to be important for the Linux version of firefox
1818             //this.suspendHandle = this.svgRoot.suspendRedraw(10000);
1819         },
1820 
1821         // documented in JXG.AbstractRenderer
1822         unsuspendRedraw: function () {
1823             //this.svgRoot.unsuspendRedraw(this.suspendHandle);
1824             //this.svgRoot.unsuspendRedrawAll();
1825             //this.svgRoot.forceRedraw();
1826         },
1827 
1828         // documented in AbstractRenderer
1829         resize: function (w, h) {
1830             // this.svgRoot.style.width  = parseFloat(w) + 'px';
1831             // this.svgRoot.style.height = parseFloat(h) + 'px';
1832 
1833             this.svgRoot.setAttribute("width", parseFloat(w));
1834             this.svgRoot.setAttribute("height", parseFloat(h));
1835             // this.svgRoot.setAttribute('width',  '100%');
1836             // this.svgRoot.setAttribute('height', '100%');
1837         },
1838 
1839         // documented in JXG.AbstractRenderer
1840         createTouchpoints: function (n) {
1841             var i, na1, na2, node;
1842             this.touchpoints = [];
1843             for (i = 0; i < n; i++) {
1844                 na1 = "touchpoint1_" + i;
1845                 node = this.createPrim("path", na1);
1846                 this.appendChildPrim(node, 19);
1847                 node.setAttributeNS(null, "d", "M 0 0");
1848                 this.touchpoints.push(node);
1849 
1850                 this.setPropertyPrim(node, "stroked", "true");
1851                 this.setPropertyPrim(node, "stroke-width", "1px");
1852                 node.setAttributeNS(null, "stroke", "#000000");
1853                 node.setAttributeNS(null, "stroke-opacity", 1.0);
1854                 node.setAttributeNS(null, "display", "none");
1855 
1856                 na2 = "touchpoint2_" + i;
1857                 node = this.createPrim("ellipse", na2);
1858                 this.appendChildPrim(node, 19);
1859                 this.updateEllipsePrim(node, 0, 0, 0, 0);
1860                 this.touchpoints.push(node);
1861 
1862                 this.setPropertyPrim(node, "stroked", "true");
1863                 this.setPropertyPrim(node, "stroke-width", "1px");
1864                 node.setAttributeNS(null, "stroke", "#000000");
1865                 node.setAttributeNS(null, "stroke-opacity", 1.0);
1866                 node.setAttributeNS(null, "fill", "#ffffff");
1867                 node.setAttributeNS(null, "fill-opacity", 0.0);
1868 
1869                 node.setAttributeNS(null, "display", "none");
1870             }
1871         },
1872 
1873         // documented in JXG.AbstractRenderer
1874         showTouchpoint: function (i) {
1875             if (this.touchpoints && i >= 0 && 2 * i < this.touchpoints.length) {
1876                 this.touchpoints[2 * i].setAttributeNS(null, "display", "inline");
1877                 this.touchpoints[2 * i + 1].setAttributeNS(null, "display", "inline");
1878             }
1879         },
1880 
1881         // documented in JXG.AbstractRenderer
1882         hideTouchpoint: function (i) {
1883             if (this.touchpoints && i >= 0 && 2 * i < this.touchpoints.length) {
1884                 this.touchpoints[2 * i].setAttributeNS(null, "display", "none");
1885                 this.touchpoints[2 * i + 1].setAttributeNS(null, "display", "none");
1886             }
1887         },
1888 
1889         // documented in JXG.AbstractRenderer
1890         updateTouchpoint: function (i, pos) {
1891             var x,
1892                 y,
1893                 d = 37;
1894 
1895             if (this.touchpoints && i >= 0 && 2 * i < this.touchpoints.length) {
1896                 x = pos[0];
1897                 y = pos[1];
1898 
1899                 this.touchpoints[2 * i].setAttributeNS(
1900                     null,
1901                     "d",
1902                     "M " +
1903                     (x - d) +
1904                     " " +
1905                     y +
1906                     " " +
1907                     "L " +
1908                     (x + d) +
1909                     " " +
1910                     y +
1911                     " " +
1912                     "M " +
1913                     x +
1914                     " " +
1915                     (y - d) +
1916                     " " +
1917                     "L " +
1918                     x +
1919                     " " +
1920                     (y + d)
1921                 );
1922                 this.updateEllipsePrim(this.touchpoints[2 * i + 1], pos[0], pos[1], 25, 25);
1923             }
1924         },
1925 
1926         /**
1927          * Walk recursively through the DOM subtree of a node and collect all
1928          * value attributes together with the id of that node.
1929          * <b>Attention:</b> Only values of nodes having a valid id are taken.
1930          * @param  {Node} node   root node of DOM subtree that will be searched recursively.
1931          * @return {Array}      Array with entries of the form [id, value]
1932          * @private
1933          */
1934         _getValuesOfDOMElements: function (node) {
1935             var values = [];
1936             if (node.nodeType === 1) {
1937                 node = node.firstChild;
1938                 while (node) {
1939                     if (node.id !== undefined && node.value !== undefined) {
1940                         values.push([node.id, node.value]);
1941                     }
1942                     values = values.concat(this._getValuesOfDOMElements(node));
1943                     node = node.nextSibling;
1944                 }
1945             }
1946             return values;
1947         },
1948 
1949         _getDataUri: function (url, callback) {
1950             var image = new Image();
1951 
1952             image.onload = function () {
1953                 var canvas = document.createElement("canvas");
1954                 canvas.width = this.naturalWidth; // or 'width' if you want a special/scaled size
1955                 canvas.height = this.naturalHeight; // or 'height' if you want a special/scaled size
1956 
1957                 canvas.getContext("2d").drawImage(this, 0, 0);
1958 
1959                 callback(canvas.toDataURL("image/png"));
1960                 canvas.remove();
1961             };
1962 
1963             image.src = url;
1964         },
1965 
1966         _getImgDataURL: function (svgRoot) {
1967             var images, len, canvas, ctx, ur, i;
1968 
1969             images = svgRoot.getElementsByTagName("image");
1970             len = images.length;
1971             if (len > 0) {
1972                 canvas = document.createElement("canvas");
1973                 //img = new Image();
1974                 for (i = 0; i < len; i++) {
1975                     images[i].setAttribute("crossorigin", "anonymous");
1976                     //img.src = images[i].href;
1977                     //img.onload = function() {
1978                     // img.crossOrigin = "anonymous";
1979                     ctx = canvas.getContext("2d");
1980                     canvas.width = images[i].getAttribute("width");
1981                     canvas.height = images[i].getAttribute("height");
1982                     try {
1983                         ctx.drawImage(images[i], 0, 0, canvas.width, canvas.height);
1984 
1985                         // If the image is not png, the format must be specified here
1986                         ur = canvas.toDataURL();
1987                         images[i].setAttribute("xlink:href", ur);
1988                     } catch (err) {
1989                         console.log("CORS problem! Image can not be used", err);
1990                     }
1991                 }
1992                 //canvas.remove();
1993             }
1994             return true;
1995         },
1996 
1997         /**
1998          * Return a data URI of the SVG code representing the construction.
1999          * The SVG code of the construction is base64 encoded. The return string starts
2000          * with "data:image/svg+xml;base64,...".
2001          *
2002          * @param {Boolean} ignoreTexts If true, the foreignObject tag is set to display=none.
2003          * This is necessary for older versions of Safari. Default: false
2004          * @returns {String}  data URI string
2005          *
2006          * @example
2007          * var A = board.create('point', [2, 2]);
2008          *
2009          * var txt = board.renderer.dumpToDataURI(false);
2010          * // txt consists of a string of the form
2011          * // data:image/svg+xml;base64,PHN2Zy. base64 encoded SVG..+PC9zdmc+
2012          * // Behind the comma, there is the base64 encoded SVG code
2013          * // which is decoded with atob().
2014          * // The call of decodeURIComponent(escape(...)) is necessary
2015          * // to handle unicode strings correctly.
2016          * var ar = txt.split(',');
2017          * document.getElementById('output').value = decodeURIComponent(escape(atob(ar[1])));
2018          *
2019          * </pre><div id="JXG1bad4bec-6d08-4ce0-9b7f-d817e8dd762d" class="jxgbox" style="width: 300px; height: 300px;"></div>
2020          * <textarea id="output2023" rows="5" cols="50"></textarea>
2021          * <script type="text/javascript">
2022          *     (function() {
2023          *         var board = JXG.JSXGraph.initBoard('JXG1bad4bec-6d08-4ce0-9b7f-d817e8dd762d',
2024          *             {boundingbox: [-8, 8, 8,-8], axis: true, showcopyright: false, shownavigation: false});
2025          *     var A = board.create('point', [2, 2]);
2026          *
2027          *     var txt = board.renderer.dumpToDataURI(false);
2028          *     // txt consists of a string of the form
2029          *     // data:image/svg+xml;base64,PHN2Zy. base64 encoded SVG..+PC9zdmc+
2030          *     // Behind the comma, there is the base64 encoded SVG code
2031          *     // which is decoded with atob().
2032          *     // The call of decodeURIComponent(escape(...)) is necessary
2033          *     // to handle unicode strings correctly.
2034          *     var ar = txt.split(',');
2035          *     document.getElementById('output2023').value = decodeURIComponent(escape(atob(ar[1])));
2036          *
2037          *     })();
2038          *
2039          * </script><pre>
2040          *
2041          */
2042         dumpToDataURI: function (ignoreTexts) {
2043             var svgRoot = this.svgRoot,
2044                 btoa = window.btoa || Base64.encode,
2045                 svg, i, len,
2046                 values = [];
2047 
2048             // Move all HTML tags (beside the SVG root) of the container
2049             // to the foreignObject element inside of the svgRoot node
2050             // Problem:
2051             // input values are not copied. This can be verified by looking at an innerHTML output
2052             // of an input element. Therefore, we do it "by hand".
2053             if (this.container.hasChildNodes() && Type.exists(this.foreignObjLayer)) {
2054                 if (!ignoreTexts) {
2055                     this.foreignObjLayer.setAttribute("display", "inline");
2056                     while (svgRoot.nextSibling) {
2057                         // Copy all value attributes
2058                         values = values.concat(this._getValuesOfDOMElements(svgRoot.nextSibling));
2059                         this.foreignObjLayer.appendChild(svgRoot.nextSibling);
2060                     }
2061                 }
2062             }
2063 
2064             this._getImgDataURL(svgRoot);
2065 
2066             // Convert the SVG graphic into a string containing SVG code
2067             svgRoot.setAttribute("xmlns", "http://www.w3.org/2000/svg");
2068             svg = new XMLSerializer().serializeToString(svgRoot);
2069 
2070             if (ignoreTexts !== true) {
2071                 // Handle SVG texts
2072                 // Insert all value attributes back into the svg string
2073                 len = values.length;
2074                 for (i = 0; i < len; i++) {
2075                     svg = svg.replace(
2076                         'id="' + values[i][0] + '"',
2077                         'id="' + values[i][0] + '" value="' + values[i][1] + '"'
2078                     );
2079                 }
2080             }
2081 
2082             // if (false) {
2083             //     // Debug: use example svg image
2084             //     svg = '<svg xmlns="http://www.w3.org/2000/svg" version="1.0" width="220" height="220"><rect width="66" height="30" x="21" y="32" stroke="#204a87" stroke-width="2" fill="none" /></svg>';
2085             // }
2086 
2087             // In IE we have to remove the namespace again.
2088             if ((svg.match(/xmlns="http:\/\/www.w3.org\/2000\/svg"/g) || []).length > 1) {
2089                 svg = svg.replace(/xmlns="http:\/\/www.w3.org\/2000\/svg"/g, "");
2090             }
2091 
2092             // Safari fails if the svg string contains a " "
2093             // Obsolete with Safari 12+
2094             svg = svg.replace(/ /g, " ");
2095             svg = svg.replace(/url\("(.*)"\)/g, "url($1)");
2096 
2097             // Move all HTML tags back from
2098             // the foreignObject element to the container
2099             if (Type.exists(this.foreignObjLayer) && this.foreignObjLayer.hasChildNodes()) {
2100                 // Restore all HTML elements
2101                 while (this.foreignObjLayer.firstChild) {
2102                     this.container.appendChild(this.foreignObjLayer.firstChild);
2103                 }
2104                 this.foreignObjLayer.setAttribute("display", "none");
2105             }
2106 
2107             return "data:image/svg+xml;base64," + btoa(unescape(encodeURIComponent(svg)));
2108         },
2109 
2110         /**
2111          * Convert the SVG construction into an HTML canvas image.
2112          * This works for all SVG supporting browsers. Implemented as Promise.
2113          * <p>
2114          * Might fail if any text element or foreign object element contains SVG. This
2115          * is the case e.g. for the default fullscreen symbol.
2116          * <p>
2117          * For IE, it is realized as function.
2118          * It works from version 9, with the exception that HTML texts
2119          * are ignored on IE. The drawing is done with a delay of
2120          * 200 ms. Otherwise there would be problems with IE.
2121          *
2122          * @param {String} canvasId Id of an HTML canvas element
2123          * @param {Number} w Width in pixel of the dumped image, i.e. of the canvas tag.
2124          * @param {Number} h Height in pixel of the dumped image, i.e. of the canvas tag.
2125          * @param {Boolean} ignoreTexts If true, the foreignObject tag is taken out from the SVG root.
2126          * This is necessary for older versions of Safari. Default: false
2127          * @returns {Promise}  Promise object
2128          *
2129          * @example
2130          * 	board.renderer.dumpToCanvas('canvas').then(function() { console.log('done'); });
2131          *
2132          * @example
2133          *  // IE 11 example:
2134          * 	board.renderer.dumpToCanvas('canvas');
2135          * 	setTimeout(function() { console.log('done'); }, 400);
2136          */
2137         dumpToCanvas: function (canvasId, w, h, ignoreTexts) {
2138             var svg, tmpImg,
2139                 cv, ctx,
2140                 doc = this.container.ownerDocument;
2141 
2142             // Prepare the canvas element
2143             cv = doc.getElementById(canvasId);
2144 
2145             // Clear the canvas
2146             /* eslint-disable no-self-assign */
2147             cv.width = cv.width;
2148             /* eslint-enable no-self-assign */
2149 
2150             ctx = cv.getContext("2d");
2151             if (w !== undefined && h !== undefined) {
2152                 cv.style.width = parseFloat(w) + "px";
2153                 cv.style.height = parseFloat(h) + "px";
2154                 // Scale twice the CSS size to make the image crisp
2155                 // cv.setAttribute('width', 2 * parseFloat(wOrg));
2156                 // cv.setAttribute('height', 2 * parseFloat(hOrg));
2157                 // ctx.scale(2 * wOrg / w, 2 * hOrg / h);
2158                 cv.setAttribute("width", parseFloat(w));
2159                 cv.setAttribute("height", parseFloat(h));
2160             }
2161 
2162             // Display the SVG string as data-uri in an HTML img.
2163             tmpImg = new Image();
2164             svg = this.dumpToDataURI(ignoreTexts);
2165             tmpImg.src = svg;
2166 
2167             // Finally, draw the HTML img in the canvas.
2168             if (!("Promise" in window)) {
2169                 tmpImg.onload = function () {
2170                     // IE needs a pause...
2171                     // Seems to be broken
2172                     window.setTimeout(function () {
2173                         try {
2174                             ctx.drawImage(tmpImg, 0, 0, w, h);
2175                         } catch (err) {
2176                             console.log("screenshots not longer supported on IE");
2177                         }
2178                     }, 200);
2179                 };
2180                 return this;
2181             }
2182 
2183             return new Promise(function (resolve, reject) {
2184                 try {
2185                     tmpImg.onload = function () {
2186                         ctx.drawImage(tmpImg, 0, 0, w, h);
2187                         resolve();
2188                     };
2189                 } catch (e) {
2190                     reject(e);
2191                 }
2192             });
2193         },
2194 
2195         /**
2196          * Display SVG image in html img-tag which enables
2197          * easy download for the user.
2198          *
2199          * Support:
2200          * <ul>
2201          * <li> IE: No
2202          * <li> Edge: full
2203          * <li> Firefox: full
2204          * <li> Chrome: full
2205          * <li> Safari: full (No text support in versions prior to 12).
2206          * </ul>
2207          *
2208          * @param {JXG.Board} board Link to the board.
2209          * @param {String} imgId Optional id of an img object. If given and different from the empty string,
2210          * the screenshot is copied to this img object. The width and height will be set to the values of the
2211          * JSXGraph container.
2212          * @param {Boolean} ignoreTexts If set to true, the foreignObject is taken out of the
2213          *  SVGRoot and texts are not displayed. This is mandatory for Safari. Default: false
2214          * @return {Object}       the svg renderer object
2215          */
2216         screenshot: function (board, imgId, ignoreTexts) {
2217             var node,
2218                 doc = this.container.ownerDocument,
2219                 parent = this.container.parentNode,
2220                 // cPos,
2221                 // cssTxt,
2222                 canvas, id, img,
2223                 button, buttonText,
2224                 w, h,
2225                 bas = board.attr.screenshot,
2226                 navbar, navbarDisplay, insert,
2227                 newImg = false,
2228                 _copyCanvasToImg,
2229                 isDebug = false;
2230 
2231             if (this.type === "no") {
2232                 return this;
2233             }
2234 
2235             w = bas.scale * this.container.getBoundingClientRect().width;
2236             h = bas.scale * this.container.getBoundingClientRect().height;
2237 
2238             if (imgId === undefined || imgId === "") {
2239                 newImg = true;
2240                 img = new Image(); //doc.createElement('img');
2241                 img.style.width = w + "px";
2242                 img.style.height = h + "px";
2243             } else {
2244                 newImg = false;
2245                 img = doc.getElementById(imgId);
2246             }
2247             // img.crossOrigin = 'anonymous';
2248 
2249             // Create div which contains canvas element and close button
2250             if (newImg) {
2251                 node = doc.createElement("div");
2252                 node.style.cssText = bas.css;
2253                 node.style.width = w + "px";
2254                 node.style.height = h + "px";
2255                 node.style.zIndex = this.container.style.zIndex + 120;
2256 
2257                 // Try to position the div exactly over the JSXGraph board
2258                 node.style.position = "absolute";
2259                 node.style.top = this.container.offsetTop + "px";
2260                 node.style.left = this.container.offsetLeft + "px";
2261             }
2262 
2263             if (!isDebug) {
2264                 // Create canvas element and add it to the DOM
2265                 // It will be removed after the image has been stored.
2266                 canvas = doc.createElement("canvas");
2267                 id = Math.random().toString(36).substr(2, 5);
2268                 canvas.setAttribute("id", id);
2269                 canvas.setAttribute("width", w);
2270                 canvas.setAttribute("height", h);
2271                 canvas.style.width = w + "px";
2272                 canvas.style.height = w + "px";
2273                 canvas.style.display = "none";
2274                 parent.appendChild(canvas);
2275             } else {
2276                 // Debug: use canvas element 'jxgbox_canvas' from jsxdev/dump.html
2277                 id = "jxgbox_canvas";
2278                 // canvas = document.getElementById(id);
2279                 canvas = doc.getElementById(id);
2280             }
2281 
2282             if (newImg) {
2283                 // Create close button
2284                 button = doc.createElement("span");
2285                 buttonText = doc.createTextNode("\u2716");
2286                 button.style.cssText = bas.cssButton;
2287                 button.appendChild(buttonText);
2288                 button.onclick = function () {
2289                     node.parentNode.removeChild(node);
2290                 };
2291 
2292                 // Add all nodes
2293                 node.appendChild(img);
2294                 node.appendChild(button);
2295                 parent.insertBefore(node, this.container.nextSibling);
2296             }
2297 
2298             // Hide navigation bar in board
2299             navbar = doc.getElementById(this.uniqName('navigationbar'));
2300             if (Type.exists(navbar)) {
2301                 navbarDisplay = navbar.style.display;
2302                 navbar.style.display = "none";
2303                 insert = this.removeToInsertLater(navbar);
2304             }
2305 
2306             _copyCanvasToImg = function () {
2307                 // Show image in img tag
2308                 img.src = canvas.toDataURL("image/png");
2309 
2310                 // Remove canvas node
2311                 if (!isDebug) {
2312                     parent.removeChild(canvas);
2313                 }
2314             };
2315 
2316             // Create screenshot in image element
2317             if ("Promise" in window) {
2318                 this.dumpToCanvas(id, w, h, ignoreTexts).then(_copyCanvasToImg);
2319             } else {
2320                 // IE
2321                 this.dumpToCanvas(id, w, h, ignoreTexts);
2322                 window.setTimeout(_copyCanvasToImg, 200);
2323             }
2324 
2325             // Reinsert navigation bar in board
2326             if (Type.exists(navbar)) {
2327                 navbar.style.display = navbarDisplay;
2328                 insert();
2329             }
2330 
2331             return this;
2332         }
2333     }
2334 );
2335 
2336 export default JXG.SVGRenderer;
2337