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