1 /*
  2     Copyright 2008-2015
  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 <http://www.gnu.org/licenses/>
 29     and <http://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 /* depends:
 36  jxg
 37  options
 38  renderer/abstract
 39  base/constants
 40  utils/type
 41  utils/env
 42  utils/color
 43  math/numerics
 44 */
 45 
 46 define([
 47     'jxg', 'options', 'renderer/abstract', 'base/constants', 'utils/type', 'utils/env', 'utils/color', 'math/numerics'
 48 ], function (JXG, Options, AbstractRenderer, Const, Type, Env, Color, Numerics) {
 49 
 50     "use strict";
 51 
 52     /**
 53      * Uses SVG to implement the rendering methods defined in {@link JXG.AbstractRenderer}.
 54      * @class JXG.AbstractRenderer
 55      * @augments JXG.AbstractRenderer
 56      * @param {Node} container Reference to a DOM node containing the board.
 57      * @param {Object} dim The dimensions of the board
 58      * @param {Number} dim.width
 59      * @param {Number} dim.height
 60      * @see JXG.AbstractRenderer
 61      */
 62     JXG.SVGRenderer = function (container, dim) {
 63         var i;
 64 
 65         // docstring in AbstractRenderer
 66         this.type = 'svg';
 67 
 68         this.isIE = navigator.appVersion.indexOf("MSIE") !== -1 || navigator.userAgent.match(/Trident\//);
 69 
 70         /**
 71          * SVG root node
 72          * @type Node
 73          */
 74         this.svgRoot = null;
 75 
 76         /**
 77          * The SVG Namespace used in JSXGraph.
 78          * @see http://www.w3.org/TR/SVG/
 79          * @type String
 80          * @default http://www.w3.org/2000/svg
 81          */
 82         this.svgNamespace = 'http://www.w3.org/2000/svg';
 83 
 84         /**
 85          * The xlink namespace. This is used for images.
 86          * @see http://www.w3.org/TR/xlink/
 87          * @type String
 88          * @default http://www.w3.org/1999/xlink
 89          */
 90         this.xlinkNamespace = 'http://www.w3.org/1999/xlink';
 91 
 92         // container is documented in AbstractRenderer
 93         this.container = container;
 94 
 95         // prepare the div container and the svg root node for use with JSXGraph
 96         this.container.style.MozUserSelect = 'none';
 97 
 98         this.container.style.overflow = 'hidden';
 99         if (this.container.style.position === '') {
100             this.container.style.position = 'relative';
101         }
102 
103         this.svgRoot = this.container.ownerDocument.createElementNS(this.svgNamespace, "svg");
104         this.svgRoot.style.overflow = 'hidden';
105 
106         this.svgRoot.style.width = dim.width + 'px';
107         this.svgRoot.style.height = dim.height + 'px';
108 
109         this.container.appendChild(this.svgRoot);
110 
111         /**
112          * The <tt>defs</tt> element is a container element to reference reusable SVG elements.
113          * @type Node
114          * @see http://www.w3.org/TR/SVG/struct.html#DefsElement
115          */
116         this.defs = this.container.ownerDocument.createElementNS(this.svgNamespace, 'defs');
117         this.svgRoot.appendChild(this.defs);
118 
119         /**
120          * Filters are used to apply shadows.
121          * @type Node
122          * @see http://www.w3.org/TR/SVG/filters.html#FilterElement
123          */
124         this.filter = this.container.ownerDocument.createElementNS(this.svgNamespace, 'filter');
125         this.filter.setAttributeNS(null, 'id', this.container.id + '_' + 'f1');
126         /*
127         this.filter.setAttributeNS(null, 'x', '-100%');
128         this.filter.setAttributeNS(null, 'y', '-100%');
129         this.filter.setAttributeNS(null, 'width', '400%');
130         this.filter.setAttributeNS(null, 'height', '400%');
131         //this.filter.setAttributeNS(null, 'filterUnits', 'userSpaceOnUse');
132         */
133         this.filter.setAttributeNS(null, 'width', '300%');
134         this.filter.setAttributeNS(null, 'height', '300%');
135         this.filter.setAttributeNS(null, 'filterUnits', 'userSpaceOnUse');
136 
137         this.feOffset = this.container.ownerDocument.createElementNS(this.svgNamespace, 'feOffset');
138         this.feOffset.setAttributeNS(null, 'result', 'offOut');
139         this.feOffset.setAttributeNS(null, 'in', 'SourceAlpha');
140         this.feOffset.setAttributeNS(null, 'dx', '5');
141         this.feOffset.setAttributeNS(null, 'dy', '5');
142         this.filter.appendChild(this.feOffset);
143 
144         this.feGaussianBlur = this.container.ownerDocument.createElementNS(this.svgNamespace, 'feGaussianBlur');
145         this.feGaussianBlur.setAttributeNS(null, 'result', 'blurOut');
146         this.feGaussianBlur.setAttributeNS(null, 'in', 'offOut');
147         this.feGaussianBlur.setAttributeNS(null, 'stdDeviation', '3');
148         this.filter.appendChild(this.feGaussianBlur);
149 
150         this.feBlend = this.container.ownerDocument.createElementNS(this.svgNamespace, 'feBlend');
151         this.feBlend.setAttributeNS(null, 'in', 'SourceGraphic');
152         this.feBlend.setAttributeNS(null, 'in2', 'blurOut');
153         this.feBlend.setAttributeNS(null, 'mode', 'normal');
154         this.filter.appendChild(this.feBlend);
155 
156         this.defs.appendChild(this.filter);
157 
158         /**
159          * JSXGraph uses a layer system to sort the elements on the board. This puts certain types of elements in front
160          * of other types of elements. For the order used see {@link JXG.Options.layer}. The number of layers is documented
161          * there, too. The higher the number, the "more on top" are the elements on this layer.
162          * @type Array
163          */
164         this.layer = [];
165         for (i = 0; i < Options.layer.numlayers; i++) {
166             this.layer[i] = this.container.ownerDocument.createElementNS(this.svgNamespace, 'g');
167             this.svgRoot.appendChild(this.layer[i]);
168         }
169 
170         /**
171          * Defines dash patterns. Defined styles are: <ol>
172          * <li value="-1"> 2px dash, 2px space</li>
173          * <li> 5px dash, 5px space</li>
174          * <li> 10px dash, 10px space</li>
175          * <li> 20px dash, 20px space</li>
176          * <li> 20px dash, 10px space, 10px dash, 10px dash</li>
177          * <li> 20px dash, 5px space, 10px dash, 5px space</li></ol>
178          * @type Array
179          * @default ['2, 2', '5, 5', '10, 10', '20, 20', '20, 10, 10, 10', '20, 5, 10, 5']
180          * @see http://www.w3.org/TR/SVG/painting.html#StrokeProperties
181          */
182         this.dashArray = ['2, 2', '5, 5', '10, 10', '20, 20', '20, 10, 10, 10', '20, 5, 10, 5'];
183     };
184 
185     JXG.SVGRenderer.prototype = new AbstractRenderer();
186 
187     JXG.extend(JXG.SVGRenderer.prototype, /** @lends JXG.SVGRenderer.prototype */ {
188 
189         /**
190          * Creates an arrow DOM node. Arrows are displayed in SVG with a <em>marker</em> tag.
191          * @private
192          * @param {JXG.GeometryElement} element A JSXGraph element, preferably one that can have an arrow attached.
193          * @param {String} [idAppendix=''] A string that is added to the node's id.
194          * @returns {Node} Reference to the node added to the DOM.
195          */
196         _createArrowHead: function (element, idAppendix) {
197             var node2, node3,
198                 id = element.id + 'Triangle',
199                 s, d;
200 
201             if (Type.exists(idAppendix)) {
202                 id += idAppendix;
203             }
204             node2 = this.createPrim('marker', id);
205 
206             node2.setAttributeNS(null, 'stroke', Type.evaluate(element.visProp.strokecolor));
207             node2.setAttributeNS(null, 'stroke-opacity', Type.evaluate(element.visProp.strokeopacity));
208             node2.setAttributeNS(null, 'fill', Type.evaluate(element.visProp.strokecolor));
209             node2.setAttributeNS(null, 'fill-opacity', Type.evaluate(element.visProp.strokeopacity));
210             node2.setAttributeNS(null, 'stroke-width', 0);  // this is the stroke-width of the arrow head.
211                                                             // Should be zero to make the positioning easy
212 
213             node2.setAttributeNS(null, 'orient', 'auto');
214             node2.setAttributeNS(null, 'markerUnits', 'strokeWidth'); // 'strokeWidth' 'userSpaceOnUse');
215 
216             /*
217             * Changes here are also necessary in _setArrowAtts()
218             */
219             s = parseInt(element.visProp.strokewidth, 10);
220             //node2.setAttributeNS(null, 'viewBox', (-s) + ' ' + (-s) + ' ' + s * 12 + ' ' + s * 12);
221             node2.setAttributeNS(null, 'viewBox', (-s) + ' ' + (-s) + ' ' + s * 10 + ' ' + s * 10);
222 
223             /*
224                The arrow head is an equilateral triangle with base length 10 and height 10.
225                This 10 units are scaled to strokeWidth*3 pixels or minimum 10 pixels.
226                See also abstractRenderer.updateLine() where the line path is shortened accordingly.
227             */
228             d = Math.max(s * 3, 10);
229             node2.setAttributeNS(null, 'markerHeight', d);
230             node2.setAttributeNS(null, 'markerWidth', d);
231 
232             node3 = this.container.ownerDocument.createElementNS(this.svgNamespace, 'path');
233 
234             if (idAppendix === 'End') {     // First arrow
235                 node2.setAttributeNS(null, 'refY', 5);
236                 node2.setAttributeNS(null, 'refX', 10);
237                 node3.setAttributeNS(null, 'd', 'M 10 0 L 0 5 L 10 10 z');
238             } else {                        // Last arrow
239                 node2.setAttributeNS(null, 'refY', 5);
240                 node2.setAttributeNS(null, 'refX', 0);
241                 node3.setAttributeNS(null, 'd', 'M 0 0 L 10 5 L 0 10 z');
242             }
243 
244             node2.appendChild(node3);
245             return node2;
246         },
247 
248         /**
249          * Updates an arrow DOM node.
250          * @param {Node} node The arrow node.
251          * @param {String} color Color value in a HTML compatible format, e.g. <tt>#00ff00</tt> or <tt>green</tt> for green.
252          * @param {Number} opacity
253          * @param {Number} width
254          */
255         _setArrowAtts: function (node, color, opacity, width, parentNode) {
256             var s, d;
257 
258             if (node) {
259                 node.setAttributeNS(null, 'stroke', color);
260                 node.setAttributeNS(null, 'stroke-opacity', opacity);
261                 node.setAttributeNS(null, 'fill', color);
262                 node.setAttributeNS(null, 'fill-opacity', opacity);
263 
264                 // This is the stroke-width of the arrow head.
265                 // Should be zero to make the positioning easy
266                 node.setAttributeNS(null, 'stroke-width', 0);
267 
268                 // The next lines are important if the strokeWidth of the line is changed.
269                 s = width;
270                 node.setAttributeNS(null, 'viewBox', (-s) + ' ' + (-s) + ' ' + s * 10 + ' ' + s * 10);
271                 d = Math.max(s * 3, 10);
272 
273                 node.setAttributeNS(null, 'markerHeight', d);
274                 node.setAttributeNS(null, 'markerWidth', d);
275 
276                 if (this.isIE) {
277                     parentNode.parentNode.insertBefore(parentNode, parentNode);
278                 }
279             }
280 
281         },
282 
283         /* ******************************** *
284          *  This renderer does not need to
285          *  override draw/update* methods
286          *  since it provides draw/update*Prim
287          *  methods except for some cases like
288          *  internal texts or images.
289          * ******************************** */
290 
291         /* **************************
292          *    Lines
293          * **************************/
294 
295         // documented in AbstractRenderer
296         updateTicks: function (ticks) {
297             var i, c, node, x, y,
298                 tickStr = '',
299                 len = ticks.ticks.length;
300 
301             for (i = 0; i < len; i++) {
302                 c = ticks.ticks[i];
303                 x = c[0];
304                 y = c[1];
305 
306                 if (typeof x[0] === 'number' && typeof x[1] === 'number') {
307                     tickStr += "M " + (x[0]) + " " + (y[0]) + " L " + (x[1]) + " " + (y[1]) + " ";
308                 }
309             }
310 
311             node = ticks.rendNode;
312 
313             if (!Type.exists(node)) {
314                 node = this.createPrim('path', ticks.id);
315                 this.appendChildPrim(node, ticks.visProp.layer);
316                 ticks.rendNode = node;
317             }
318 
319             node.setAttributeNS(null, 'stroke', ticks.visProp.strokecolor);
320             node.setAttributeNS(null, 'stroke-opacity', ticks.visProp.strokeopacity);
321             node.setAttributeNS(null, 'stroke-width', ticks.visProp.strokewidth);
322             this.updatePathPrim(node, tickStr, ticks.board);
323         },
324 
325         /* **************************
326          *    Text related stuff
327          * **************************/
328 
329         // already documented in JXG.AbstractRenderer
330         displayCopyright: function (str, fontsize) {
331             var node = this.createPrim('text', 'licenseText'),
332                 t;
333             node.setAttributeNS(null, 'x', '20px');
334             node.setAttributeNS(null, 'y', (2 + fontsize) + 'px');
335             node.setAttributeNS(null, "style", "font-family:Arial,Helvetica,sans-serif; font-size:" + fontsize + "px; fill:#356AA0;  opacity:0.3;");
336             t = this.container.ownerDocument.createTextNode(str);
337             node.appendChild(t);
338             this.appendChildPrim(node, 0);
339         },
340 
341         // already documented in JXG.AbstractRenderer
342         drawInternalText: function (el) {
343             var node = this.createPrim('text', el.id);
344 
345             node.setAttributeNS(null, "class", el.visProp.cssclass);
346             //node.setAttributeNS(null, "style", "alignment-baseline:middle"); // Not yet supported by Firefox
347 
348             // Preserve spaces 
349             node.setAttributeNS("http://www.w3.org/XML/1998/namespace", "space", "preserve");
350 
351             el.rendNodeText = this.container.ownerDocument.createTextNode('');
352             node.appendChild(el.rendNodeText);
353             this.appendChildPrim(node,  el.visProp.layer);
354 
355             return node;
356         },
357 
358         // already documented in JXG.AbstractRenderer
359         updateInternalText: function (el) {
360             var content = el.plaintext, v;
361 
362             // el.rendNode.setAttributeNS(null, "class", el.visProp.cssclass);
363             if (!isNaN(el.coords.scrCoords[1] + el.coords.scrCoords[2])) {
364 
365                 // Horizontal
366                 v = el.coords.scrCoords[1];
367                 if (el.visPropOld.left !== (el.visProp.anchorx + v)) {
368                     el.rendNode.setAttributeNS(null, 'x', v + 'px');
369 
370                     if (el.visProp.anchorx === 'left') {
371                         el.rendNode.setAttributeNS(null, 'text-anchor', 'start');
372                     } else if (el.visProp.anchorx === 'right') {
373                         el.rendNode.setAttributeNS(null, 'text-anchor', 'end');
374                     } else if (el.visProp.anchorx === 'middle') {
375                         el.rendNode.setAttributeNS(null, 'text-anchor', 'middle');
376                     }
377                     el.visPropOld.left = el.visProp.anchorx + v;
378                 }
379 
380                 // Vertical
381                 v = el.coords.scrCoords[2];
382                 if (el.visPropOld.top !== (el.visProp.anchory + v)) {
383                     el.rendNode.setAttributeNS(null, 'y', (v + this.vOffsetText * 0.5) + 'px');
384 
385                     if (el.visProp.anchory === 'bottom') {
386                         el.rendNode.setAttributeNS(null, 'dominant-baseline', 'text-after-edge');
387                     } else if (el.visProp.anchory === 'top') {
388                         el.rendNode.setAttributeNS(null, 'dominant-baseline', 'text-before-edge');
389                     } else if (el.visProp.anchory === 'middle') {
390                         el.rendNode.setAttributeNS(null, 'dominant-baseline', 'middle');
391                     }
392                     el.visPropOld.top = el.visProp.anchory + v;
393                 }
394             }
395             if (el.htmlStr !== content) {
396                 el.rendNodeText.data = content;
397                 el.htmlStr = content;
398             }
399             this.transformImage(el, el.transformations);
400         },
401 
402         /**
403          * Set color and opacity of internal texts.
404          * SVG needs its own version.
405          * @private
406          * @see JXG.AbstractRenderer#updateTextStyle
407          * @see JXG.AbstractRenderer#updateInternalTextStyle
408          */
409         updateInternalTextStyle: function (element, strokeColor, strokeOpacity) {
410             this.setObjectFillColor(element, strokeColor, strokeOpacity);
411         },
412 
413         /* **************************
414          *    Image related stuff
415          * **************************/
416 
417         // already documented in JXG.AbstractRenderer
418         drawImage: function (el) {
419             var node = this.createPrim('image', el.id);
420 
421             node.setAttributeNS(null, 'preserveAspectRatio', 'none');
422             this.appendChildPrim(node, el.visProp.layer);
423             el.rendNode = node;
424 
425             this.updateImage(el);
426         },
427 
428         // already documented in JXG.AbstractRenderer
429         transformImage: function (el, t) {
430             var s, m,
431                 node = el.rendNode,
432                 str = "",
433                 len = t.length;
434 
435             if (len > 0) {
436                 m = this.joinTransforms(el, t);
437                 s = [m[1][1], m[2][1], m[1][2], m[2][2], m[1][0], m[2][0]].join(',');
438                 str += ' matrix(' + s + ') ';
439                 node.setAttributeNS(null, 'transform', str);
440             }
441         },
442 
443         // already documented in JXG.AbstractRenderer
444         updateImageURL: function (el) {
445             var url = Type.evaluate(el.url);
446 
447             el.rendNode.setAttributeNS(this.xlinkNamespace, 'xlink:href', url);
448         },
449 
450         // already documented in JXG.AbstractRenderer
451         updateImageStyle: function (el, doHighlight) {
452             var css = doHighlight ? el.visProp.highlightcssclass : el.visProp.cssclass;
453 
454             el.rendNode.setAttributeNS(null, 'class', css);
455         },
456 
457         /* **************************
458          * Render primitive objects
459          * **************************/
460 
461         // already documented in JXG.AbstractRenderer
462         appendChildPrim: function (node, level) {
463             if (!Type.exists(level)) { // trace nodes have level not set
464                 level = 0;
465             } else if (level >= Options.layer.numlayers) {
466                 level = Options.layer.numlayers - 1;
467             }
468 
469             this.layer[level].appendChild(node);
470 
471             return node;
472         },
473 
474         // already documented in JXG.AbstractRenderer
475         createPrim: function (type, id) {
476             var node = this.container.ownerDocument.createElementNS(this.svgNamespace, type);
477             node.setAttributeNS(null, 'id', this.container.id + '_' + id);
478             node.style.position = 'absolute';
479             if (type === 'path') {
480                 node.setAttributeNS(null, 'stroke-linecap', 'round');
481                 node.setAttributeNS(null, 'stroke-linejoin', 'round');
482             }
483             return node;
484         },
485 
486         // already documented in JXG.AbstractRenderer
487         remove: function (shape) {
488             if (Type.exists(shape) && Type.exists(shape.parentNode)) {
489                 shape.parentNode.removeChild(shape);
490             }
491         },
492 
493         // already documented in JXG.AbstractRenderer
494         makeArrows: function (el) {
495             var node2;
496 
497             if (el.visPropOld.firstarrow === el.visProp.firstarrow && el.visPropOld.lastarrow === el.visProp.lastarrow) {
498                 if (this.isIE && el.visProp.visible && (el.visProp.firstarrow || el.visProp.lastarrow)) {
499                     el.rendNode.parentNode.insertBefore(el.rendNode, el.rendNode);
500                 }
501                 return;
502             }
503 
504             if (el.visProp.firstarrow) {
505                 node2 = el.rendNodeTriangleStart;
506                 if (!Type.exists(node2)) {
507                     node2 = this._createArrowHead(el, 'End');
508                     this.defs.appendChild(node2);
509                     el.rendNodeTriangleStart = node2;
510                     el.rendNode.setAttributeNS(null, 'marker-start', 'url(#' + this.container.id + '_' + el.id + 'TriangleEnd)');
511                 } else {
512                     this.defs.appendChild(node2);
513                 }
514             } else {
515                 node2 = el.rendNodeTriangleStart;
516                 if (Type.exists(node2)) {
517                     this.remove(node2);
518                 }
519             }
520             if (el.visProp.lastarrow) {
521                 node2 = el.rendNodeTriangleEnd;
522                 if (!Type.exists(node2)) {
523                     node2 = this._createArrowHead(el, 'Start');
524                     this.defs.appendChild(node2);
525                     el.rendNodeTriangleEnd = node2;
526                     el.rendNode.setAttributeNS(null, 'marker-end', 'url(#' + this.container.id + '_' + el.id + 'TriangleStart)');
527                 } else {
528                     this.defs.appendChild(node2);
529                 }
530             } else {
531                 node2 = el.rendNodeTriangleEnd;
532                 if (Type.exists(node2)) {
533                     this.remove(node2);
534                 }
535             }
536             el.visPropOld.firstarrow = el.visProp.firstarrow;
537             el.visPropOld.lastarrow = el.visProp.lastarrow;
538         },
539 
540         // already documented in JXG.AbstractRenderer
541         updateEllipsePrim: function (node, x, y, rx, ry) {
542             var huge = 1000000;
543 
544             // webkit does not like huge values if the object is dashed
545             x = Math.abs(x) < huge ? x : huge * x / Math.abs(x);
546             y = Math.abs(y) < huge ? y : huge * y / Math.abs(y);
547             rx = Math.abs(rx) < huge ? rx : huge * rx / Math.abs(rx);
548             ry = Math.abs(ry) < huge ? ry : huge * ry / Math.abs(ry);
549 
550             node.setAttributeNS(null, 'cx', x);
551             node.setAttributeNS(null, 'cy', y);
552             node.setAttributeNS(null, 'rx', Math.abs(rx));
553             node.setAttributeNS(null, 'ry', Math.abs(ry));
554         },
555 
556         // already documented in JXG.AbstractRenderer
557         updateLinePrim: function (node, p1x, p1y, p2x, p2y) {
558             var huge = 1000000;
559 
560             if (!isNaN(p1x + p1y + p2x + p2y)) {
561                 // webkit does not like huge values if the object is dashed
562                 p1x = Math.abs(p1x) < huge ? p1x : huge * p1x / Math.abs(p1x);
563                 p1y = Math.abs(p1y) < huge ? p1y : huge * p1y / Math.abs(p1y);
564                 p2x = Math.abs(p2x) < huge ? p2x : huge * p2x / Math.abs(p2x);
565                 p2y = Math.abs(p2y) < huge ? p2y : huge * p2y / Math.abs(p2y);
566 
567                 node.setAttributeNS(null, 'x1', p1x);
568                 node.setAttributeNS(null, 'y1', p1y);
569                 node.setAttributeNS(null, 'x2', p2x);
570                 node.setAttributeNS(null, 'y2', p2y);
571             }
572         },
573 
574         // already documented in JXG.AbstractRenderer
575         updatePathPrim: function (node, pointString) {
576             if (pointString === '') {
577                 pointString = 'M 0 0';
578             }
579             node.setAttributeNS(null, 'd', pointString);
580         },
581 
582         // already documented in JXG.AbstractRenderer
583         updatePathStringPoint: function (el, size, type) {
584             var s = '',
585                 scr = el.coords.scrCoords,
586                 sqrt32 = size * Math.sqrt(3) * 0.5,
587                 s05 = size * 0.5;
588 
589             if (type === 'x') {
590                 s = ' M ' + (scr[1] - size) + ' ' + (scr[2] - size) +
591                     ' L ' + (scr[1] + size) + ' ' + (scr[2] + size) +
592                     ' M ' + (scr[1] + size) + ' ' + (scr[2] - size) +
593                     ' L ' + (scr[1] - size) + ' ' + (scr[2] + size);
594             } else if (type === '+') {
595                 s = ' M ' + (scr[1] - size) + ' ' + (scr[2]) +
596                     ' L ' + (scr[1] + size) + ' ' + (scr[2]) +
597                     ' M ' + (scr[1])        + ' ' + (scr[2] - size) +
598                     ' L ' + (scr[1])        + ' ' + (scr[2] + size);
599             } else if (type === '<>') {
600                 s = ' M ' + (scr[1] - size) + ' ' + (scr[2]) +
601                     ' L ' + (scr[1])        + ' ' + (scr[2] + size) +
602                     ' L ' + (scr[1] + size) + ' ' + (scr[2]) +
603                     ' L ' + (scr[1])        + ' ' + (scr[2] - size) + ' Z ';
604             } else if (type === '^') {
605                 s = ' M ' + (scr[1])          + ' ' + (scr[2] - size) +
606                     ' L ' + (scr[1] - sqrt32) + ' ' + (scr[2] + s05) +
607                     ' L ' + (scr[1] + sqrt32) + ' ' + (scr[2] + s05) +
608                     ' Z ';  // close path
609             } else if (type === 'v') {
610                 s = ' M ' + (scr[1])          + ' ' + (scr[2] + size) +
611                     ' L ' + (scr[1] - sqrt32) + ' ' + (scr[2] - s05) +
612                     ' L ' + (scr[1] + sqrt32) + ' ' + (scr[2] - s05) +
613                     ' Z ';
614             } else if (type === '>') {
615                 s = ' M ' + (scr[1] + size) + ' ' + (scr[2]) +
616                     ' L ' + (scr[1] - s05)  + ' ' + (scr[2] - sqrt32) +
617                     ' L ' + (scr[1] - s05)  + ' ' + (scr[2] + sqrt32) +
618                     ' Z ';
619             } else if (type === '<') {
620                 s = ' M ' + (scr[1] - size) + ' ' + (scr[2]) +
621                     ' L ' + (scr[1] + s05)  + ' ' + (scr[2] - sqrt32) +
622                     ' L ' + (scr[1] + s05)  + ' ' + (scr[2] + sqrt32) +
623                     ' Z ';
624             }
625             return s;
626         },
627 
628         // already documented in JXG.AbstractRenderer
629         updatePathStringPrim: function (el) {
630             var i, scr, len,
631                 symbm = ' M ',
632                 symbl = ' L ',
633                 symbc = ' C ',
634                 nextSymb = symbm,
635                 maxSize = 5000.0,
636                 pStr = '';
637                 // isNotPlot = (el.visProp.curvetype !== 'plot');
638 
639             if (el.numberPoints <= 0) {
640                 return '';
641             }
642 
643             len = Math.min(el.points.length, el.numberPoints);
644 
645             if (el.bezierDegree === 1) {
646                 /*
647                 if (isNotPlot && el.visProp.rdpsmoothing) {
648                     el.points = Numerics.RamerDouglasPeucker(el.points, 0.5);
649                     el.numberPoints = el.points.length;
650                 }
651                 */
652 
653                 for (i = 0; i < len; i++) {
654                     scr = el.points[i].scrCoords;
655                     if (isNaN(scr[1]) || isNaN(scr[2])) {  // PenUp
656                         nextSymb = symbm;
657                     } else {
658                         // Chrome has problems with values being too far away.
659                         scr[1] = Math.max(Math.min(scr[1], maxSize), -maxSize);
660                         scr[2] = Math.max(Math.min(scr[2], maxSize), -maxSize);
661 
662                         // Attention: first coordinate may be inaccurate if far way
663                         //pStr += [nextSymb, scr[1], ' ', scr[2]].join('');
664                         pStr += nextSymb + scr[1] + ' ' + scr[2];   // Seems to be faster now (webkit and firefox)
665                         nextSymb = symbl;
666                     }
667                 }
668             } else if (el.bezierDegree === 3) {
669                 i = 0;
670                 while (i < len) {
671                     scr = el.points[i].scrCoords;
672                     if (isNaN(scr[1]) || isNaN(scr[2])) {  // PenUp
673                         nextSymb = symbm;
674                     } else {
675                         pStr += nextSymb + scr[1] + ' ' + scr[2];
676                         if (nextSymb === symbc) {
677                             i += 1;
678                             scr = el.points[i].scrCoords;
679                             pStr += ' ' + scr[1] + ' ' + scr[2];
680                             i += 1;
681                             scr = el.points[i].scrCoords;
682                             pStr += ' ' + scr[1] + ' ' + scr[2];
683                         }
684                         nextSymb = symbc;
685                     }
686                     i += 1;
687                 }
688             }
689             return pStr;
690         },
691 
692         // already documented in JXG.AbstractRenderer
693         updatePathStringBezierPrim: function (el) {
694             var i, j, k, scr, lx, ly, len,
695                 symbm = ' M ',
696                 symbl = ' C ',
697                 nextSymb = symbm,
698                 maxSize = 5000.0,
699                 pStr = '',
700                 f = el.visProp.strokewidth,
701                 isNoPlot = (el.visProp.curvetype !== 'plot');
702 
703             if (el.numberPoints <= 0) {
704                 return '';
705             }
706 
707             if (isNoPlot && el.board.options.curve.RDPsmoothing) {
708                 el.points = Numerics.RamerDouglasPeucker(el.points, 0.5);
709             }
710 
711             len = Math.min(el.points.length, el.numberPoints);
712             for (j = 1; j < 3; j++) {
713                 nextSymb = symbm;
714                 for (i = 0; i < len; i++) {
715                     scr = el.points[i].scrCoords;
716 
717                     if (isNaN(scr[1]) || isNaN(scr[2])) {  // PenUp
718                         nextSymb = symbm;
719                     } else {
720                         // Chrome has problems with values being too far away.
721                         scr[1] = Math.max(Math.min(scr[1], maxSize), -maxSize);
722                         scr[2] = Math.max(Math.min(scr[2], maxSize), -maxSize);
723 
724                         // Attention: first coordinate may be inaccurate if far way
725                         if (nextSymb === symbm) {
726                             //pStr += [nextSymb, scr[1], ' ', scr[2]].join('');
727                             pStr += nextSymb + scr[1] + ' ' + scr[2];   // Seems to be faster now (webkit and firefox)
728                         } else {
729                             k = 2 * j;
730                             pStr += [nextSymb,
731                                 (lx + (scr[1] - lx) * 0.333 + f * (k * Math.random() - j)), ' ',
732                                 (ly + (scr[2] - ly) * 0.333 + f * (k * Math.random() - j)), ' ',
733                                 (lx + (scr[1] - lx) * 0.666 + f * (k * Math.random() - j)), ' ',
734                                 (ly + (scr[2] - ly) * 0.666 + f * (k * Math.random() - j)), ' ',
735                                 scr[1], ' ', scr[2]].join('');
736                         }
737 
738                         nextSymb = symbl;
739                         lx = scr[1];
740                         ly = scr[2];
741                     }
742                 }
743             }
744             return pStr;
745         },
746 
747         // already documented in JXG.AbstractRenderer
748         updatePolygonPrim: function (node, el) {
749             var i,
750                 pStr = '',
751                 scrCoords,
752                 len = el.vertices.length;
753 
754             node.setAttributeNS(null, 'stroke', 'none');
755 
756             for (i = 0; i < len - 1; i++) {
757                 if (el.vertices[i].isReal) {
758                     scrCoords = el.vertices[i].coords.scrCoords;
759                     pStr = pStr + scrCoords[1] + "," + scrCoords[2];
760                 } else {
761                     node.setAttributeNS(null, 'points', '');
762                     return;
763                 }
764 
765                 if (i < len - 2) {
766                     pStr += " ";
767                 }
768             }
769             if (pStr.indexOf('NaN') === -1) {
770                 node.setAttributeNS(null, 'points', pStr);
771             }
772         },
773 
774         // already documented in JXG.AbstractRenderer
775         updateRectPrim: function (node, x, y, w, h) {
776             node.setAttributeNS(null, 'x', x);
777             node.setAttributeNS(null, 'y', y);
778             node.setAttributeNS(null, 'width', w);
779             node.setAttributeNS(null, 'height', h);
780         },
781 
782         /* **************************
783          *  Set Attributes
784          * **************************/
785 
786         // documented in JXG.AbstractRenderer
787         setPropertyPrim: function (node, key, val) {
788             if (key === 'stroked') {
789                 return;
790             }
791             node.setAttributeNS(null, key, val);
792         },
793 
794         // documented in JXG.AbstractRenderer
795         show: function (el) {
796             var node;
797 
798             if (el && el.rendNode) {
799                 node = el.rendNode;
800                 node.setAttributeNS(null, 'display', 'inline');
801                 node.style.visibility = "inherit";
802             }
803         },
804 
805         // documented in JXG.AbstractRenderer
806         hide: function (el) {
807             var node;
808 
809             if (el && el.rendNode) {
810                 node = el.rendNode;
811                 node.setAttributeNS(null, 'display', 'none');
812                 node.style.visibility = "hidden";
813             }
814         },
815 
816         // documented in JXG.AbstractRenderer
817         setBuffering: function (el, type) {
818             el.rendNode.setAttribute('buffered-rendering', type);
819         },
820 
821         // documented in JXG.AbstractRenderer
822         setDashStyle: function (el) {
823             var dashStyle = el.visProp.dash, node = el.rendNode;
824 
825             if (el.visProp.dash > 0) {
826                 node.setAttributeNS(null, 'stroke-dasharray', this.dashArray[dashStyle - 1]);
827             } else {
828                 if (node.hasAttributeNS(null, 'stroke-dasharray')) {
829                     node.removeAttributeNS(null, 'stroke-dasharray');
830                 }
831             }
832         },
833 
834         // documented in JXG.AbstractRenderer
835         setGradient: function (el) {
836             var fillNode = el.rendNode, col, op,
837                 node, node2, node3, x1, x2, y1, y2;
838 
839             op = Type.evaluate(el.visProp.fillopacity);
840             op = (op > 0) ? op : 0;
841 
842             col = Type.evaluate(el.visProp.fillcolor);
843 
844             if (el.visProp.gradient === 'linear') {
845                 node = this.createPrim('linearGradient', el.id + '_gradient');
846                 x1 = '0%';
847                 x2 = '100%';
848                 y1 = '0%';
849                 y2 = '0%';
850 
851                 node.setAttributeNS(null, 'x1', x1);
852                 node.setAttributeNS(null, 'x2', x2);
853                 node.setAttributeNS(null, 'y1', y1);
854                 node.setAttributeNS(null, 'y2', y2);
855                 node2 = this.createPrim('stop', el.id + '_gradient1');
856                 node2.setAttributeNS(null, 'offset', '0%');
857                 node2.setAttributeNS(null, 'style', 'stop-color:' + col + ';stop-opacity:' + op);
858                 node3 = this.createPrim('stop', el.id + '_gradient2');
859                 node3.setAttributeNS(null, 'offset', '100%');
860                 node3.setAttributeNS(null, 'style', 'stop-color:' + el.visProp.gradientsecondcolor + ';stop-opacity:' + el.visProp.gradientsecondopacity);
861                 node.appendChild(node2);
862                 node.appendChild(node3);
863                 this.defs.appendChild(node);
864                 fillNode.setAttributeNS(null, 'style', 'fill:url(#' + this.container.id + '_' + el.id + '_gradient)');
865                 el.gradNode1 = node2;
866                 el.gradNode2 = node3;
867             } else if (el.visProp.gradient === 'radial') {
868                 node = this.createPrim('radialGradient', el.id + '_gradient');
869 
870                 node.setAttributeNS(null, 'cx', '50%');
871                 node.setAttributeNS(null, 'cy', '50%');
872                 node.setAttributeNS(null, 'r', '50%');
873                 node.setAttributeNS(null, 'fx', el.visProp.gradientpositionx * 100 + '%');
874                 node.setAttributeNS(null, 'fy', el.visProp.gradientpositiony * 100 + '%');
875 
876                 node2 = this.createPrim('stop', el.id + '_gradient1');
877                 node2.setAttributeNS(null, 'offset', '0%');
878                 node2.setAttributeNS(null, 'style', 'stop-color:' + el.visProp.gradientsecondcolor + ';stop-opacity:' + el.visProp.gradientsecondopacity);
879                 node3 = this.createPrim('stop', el.id + '_gradient2');
880                 node3.setAttributeNS(null, 'offset', '100%');
881                 node3.setAttributeNS(null, 'style', 'stop-color:' + col + ';stop-opacity:' + op);
882 
883                 node.appendChild(node2);
884                 node.appendChild(node3);
885                 this.defs.appendChild(node);
886                 fillNode.setAttributeNS(null, 'style', 'fill:url(#' + this.container.id + '_' + el.id + '_gradient)');
887                 el.gradNode1 = node2;
888                 el.gradNode2 = node3;
889             } else {
890                 fillNode.removeAttributeNS(null, 'style');
891             }
892         },
893 
894         // documented in JXG.AbstractRenderer
895         updateGradient: function (el) {
896             var col, op,
897                 node2 = el.gradNode1,
898                 node3 = el.gradNode2;
899 
900             if (!Type.exists(node2) || !Type.exists(node3)) {
901                 return;
902             }
903 
904             op = Type.evaluate(el.visProp.fillopacity);
905             op = (op > 0) ? op : 0;
906 
907             col = Type.evaluate(el.visProp.fillcolor);
908 
909             if (el.visProp.gradient === 'linear') {
910                 node2.setAttributeNS(null, 'style', 'stop-color:' + col + ';stop-opacity:' + op);
911                 node3.setAttributeNS(null, 'style', 'stop-color:' + el.visProp.gradientsecondcolor + ';stop-opacity:' + el.visProp.gradientsecondopacity);
912             } else if (el.visProp.gradient === 'radial') {
913                 node2.setAttributeNS(null, 'style', 'stop-color:' + el.visProp.gradientsecondcolor + ';stop-opacity:' + el.visProp.gradientsecondopacity);
914                 node3.setAttributeNS(null, 'style', 'stop-color:' + col + ';stop-opacity:' + op);
915             }
916         },
917 
918         // documented in JXG.AbstractRenderer
919         setObjectFillColor: function (el, color, opacity) {
920             var node, c, rgbo, oo,
921                 rgba = Type.evaluate(color),
922                 o = Type.evaluate(opacity);
923 
924             o = (o > 0) ? o : 0;
925 
926             if (el.visPropOld.fillcolor === rgba && el.visPropOld.fillopacity === o) {
927                 return;
928             }
929             if (Type.exists(rgba) && rgba !== false) {
930                 if (rgba.length !== 9) {          // RGB, not RGBA
931                     c = rgba;
932                     oo = o;
933                 } else {                       // True RGBA, not RGB
934                     rgbo = Color.rgba2rgbo(rgba);
935                     c = rgbo[0];
936                     oo = o * rgbo[1];
937                 }
938 
939                 node = el.rendNode;
940 
941                 if (c !== 'none') {               // problem in firefox 17
942                     node.setAttributeNS(null, 'fill', c);
943                 } else {
944                     oo = 0;
945                 }
946 
947                 if (el.type === JXG.OBJECT_TYPE_IMAGE) {
948                     node.setAttributeNS(null, 'opacity', oo);
949                     //node.style['opacity'] = oo;  // This would overwrite values set y CSS class.
950                 } else {
951                     node.setAttributeNS(null, 'fill-opacity', oo);
952                 }
953 
954                 if (Type.exists(el.visProp.gradient)) {
955                     this.updateGradient(el);
956                 }
957             }
958             el.visPropOld.fillcolor = rgba;
959             el.visPropOld.fillopacity = o;
960         },
961 
962         // documented in JXG.AbstractRenderer
963         setObjectStrokeColor: function (el, color, opacity) {
964             var rgba = Type.evaluate(color), c, rgbo,
965                 o = Type.evaluate(opacity), oo,
966                 node;
967 
968             o = (o > 0) ? o : 0;
969 
970             if (el.visPropOld.strokecolor === rgba && el.visPropOld.strokeopacity === o) {
971                 return;
972             }
973 
974             if (Type.exists(rgba) && rgba !== false) {
975                 if (rgba.length !== 9) {          // RGB, not RGBA
976                     c = rgba;
977                     oo = o;
978                 } else {                       // True RGBA, not RGB
979                     rgbo = Color.rgba2rgbo(rgba);
980                     c = rgbo[0];
981                     oo = o * rgbo[1];
982                 }
983 
984                 node = el.rendNode;
985 
986                 if (el.elementClass === Const.OBJECT_CLASS_TEXT) {
987                     if (el.visProp.display === 'html') {
988                         node.style.color = c;
989                         node.style.opacity = oo;
990                     } else {
991                         node.setAttributeNS(null, "style", "fill:" + c);
992                         node.setAttributeNS(null, "style", "fill-opacity:" + oo);
993                     }
994                 } else {
995                     node.setAttributeNS(null, 'stroke', c);
996                     node.setAttributeNS(null, 'stroke-opacity', oo);
997                 }
998 
999                 if (el.type === Const.OBJECT_TYPE_ARROW) {
1000                     this._setArrowAtts(el.rendNodeTriangle, c, oo, el.visProp.strokewidth, el.rendNode);
1001                 } else if (el.elementClass === Const.OBJECT_CLASS_CURVE || el.elementClass === Const.OBJECT_CLASS_LINE) {
1002                     if (el.visProp.firstarrow) {
1003                         this._setArrowAtts(el.rendNodeTriangleStart, c, oo, el.visProp.strokewidth, el.rendNode);
1004                     }
1005 
1006                     if (el.visProp.lastarrow) {
1007                         this._setArrowAtts(el.rendNodeTriangleEnd, c, oo, el.visProp.strokewidth, el.rendNode);
1008                     }
1009                 }
1010             }
1011 
1012             el.visPropOld.strokecolor = rgba;
1013             el.visPropOld.strokeopacity = o;
1014         },
1015 
1016         // documented in JXG.AbstractRenderer
1017         setObjectStrokeWidth: function (el, width) {
1018             var node,
1019                 w = Type.evaluate(width);
1020 
1021             if (isNaN(w) || el.visPropOld.strokewidth === w) {
1022                 return;
1023             }
1024 
1025             node = el.rendNode;
1026             this.setPropertyPrim(node, 'stroked', 'true');
1027             if (Type.exists(w)) {
1028                 this.setPropertyPrim(node, 'stroke-width', w + 'px');
1029 
1030                 if (el.type === Const.OBJECT_TYPE_ARROW) {
1031                     this._setArrowAtts(el.rendNodeTriangle, el.visProp.strokecolor, el.visProp.strokeopacity, w, el.rendNode);
1032                 } else if (el.elementClass === Const.OBJECT_CLASS_CURVE || el.elementClass === Const.OBJECT_CLASS_LINE) {
1033                     if (el.visProp.firstarrow) {
1034                         this._setArrowAtts(el.rendNodeTriangleStart, el.visProp.strokecolor, el.visProp.strokeopacity, w, el.rendNode);
1035                     }
1036 
1037                     if (el.visProp.lastarrow) {
1038                         this._setArrowAtts(el.rendNodeTriangleEnd, el.visProp.strokecolor, el.visProp.strokeopacity, w, el.rendNode);
1039                     }
1040                 }
1041             }
1042             el.visPropOld.strokewidth = w;
1043         },
1044 
1045         // documented in JXG.AbstractRenderer
1046         setShadow: function (el) {
1047             if (el.visPropOld.shadow === el.visProp.shadow) {
1048                 return;
1049             }
1050 
1051             if (Type.exists(el.rendNode)) {
1052                 if (el.visProp.shadow) {
1053                     el.rendNode.setAttributeNS(null, 'filter', 'url(#' + this.container.id + '_' + 'f1)');
1054                 } else {
1055                     el.rendNode.removeAttributeNS(null, 'filter');
1056                 }
1057             }
1058             el.visPropOld.shadow = el.visProp.shadow;
1059         },
1060 
1061         /* **************************
1062          * renderer control
1063          * **************************/
1064 
1065         // documented in JXG.AbstractRenderer
1066         suspendRedraw: function () {
1067             // It seems to be important for the Linux version of firefox
1068             //this.suspendHandle = this.svgRoot.suspendRedraw(10000);
1069         },
1070 
1071         // documented in JXG.AbstractRenderer
1072         unsuspendRedraw: function () {
1073             //this.svgRoot.unsuspendRedraw(this.suspendHandle);
1074             //this.svgRoot.unsuspendRedrawAll();
1075             //this.svgRoot.forceRedraw();
1076         },
1077 
1078         // documented in AbstractRenderer
1079         resize: function (w, h) {
1080             this.svgRoot.style.width = parseFloat(w) + 'px';
1081             this.svgRoot.style.height = parseFloat(h) + 'px';
1082         },
1083 
1084         // documented in JXG.AbstractRenderer
1085         createTouchpoints: function (n) {
1086             var i, na1, na2, node;
1087             this.touchpoints = [];
1088             for (i = 0; i < n; i++) {
1089                 na1 = 'touchpoint1_' + i;
1090                 node = this.createPrim('path', na1);
1091                 this.appendChildPrim(node, 19);
1092                 node.setAttributeNS(null, 'd', 'M 0 0');
1093                 this.touchpoints.push(node);
1094 
1095                 this.setPropertyPrim(node, 'stroked', 'true');
1096                 this.setPropertyPrim(node, 'stroke-width', '1px');
1097                 node.setAttributeNS(null, 'stroke', '#000000');
1098                 node.setAttributeNS(null, 'stroke-opacity', 1.0);
1099                 node.setAttributeNS(null, 'display', 'none');
1100 
1101                 na2 = 'touchpoint2_' + i;
1102                 node = this.createPrim('ellipse', na2);
1103                 this.appendChildPrim(node, 19);
1104                 this.updateEllipsePrim(node, 0, 0, 0, 0);
1105                 this.touchpoints.push(node);
1106 
1107                 this.setPropertyPrim(node, 'stroked', 'true');
1108                 this.setPropertyPrim(node, 'stroke-width', '1px');
1109                 node.setAttributeNS(null, 'stroke', '#000000');
1110                 node.setAttributeNS(null, 'stroke-opacity', 1.0);
1111                 node.setAttributeNS(null, 'fill', '#ffffff');
1112                 node.setAttributeNS(null, 'fill-opacity', 0.0);
1113 
1114                 node.setAttributeNS(null, 'display', 'none');
1115             }
1116         },
1117 
1118         // documented in JXG.AbstractRenderer
1119         showTouchpoint: function (i) {
1120             if (this.touchpoints && i >= 0 && 2 * i < this.touchpoints.length) {
1121                 this.touchpoints[2 * i].setAttributeNS(null, 'display', 'inline');
1122                 this.touchpoints[2 * i + 1].setAttributeNS(null, 'display', 'inline');
1123             }
1124         },
1125 
1126         // documented in JXG.AbstractRenderer
1127         hideTouchpoint: function (i) {
1128             if (this.touchpoints && i >= 0 && 2 * i < this.touchpoints.length) {
1129                 this.touchpoints[2 * i].setAttributeNS(null, 'display', 'none');
1130                 this.touchpoints[2 * i + 1].setAttributeNS(null, 'display', 'none');
1131             }
1132         },
1133 
1134         // documented in JXG.AbstractRenderer
1135         updateTouchpoint: function (i, pos) {
1136             var x, y,
1137                 d = 37;
1138 
1139             if (this.touchpoints && i >= 0 && 2 * i < this.touchpoints.length) {
1140                 x = pos[0];
1141                 y = pos[1];
1142 
1143                 this.touchpoints[2 * i].setAttributeNS(null, 'd', 'M ' + (x - d) + ' ' + y + ' ' +
1144                     'L ' + (x + d) + ' ' + y + ' ' +
1145                     'M ' + x + ' ' + (y - d) + ' ' +
1146                     'L ' + x + ' ' + (y + d));
1147                 this.updateEllipsePrim(this.touchpoints[2 * i + 1], pos[0], pos[1], 25, 25);
1148             }
1149         }
1150     });
1151 
1152     return JXG.SVGRenderer;
1153 });
1154