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