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