1 /*
  2     Copyright 2008-2014
  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 
 33 /*global JXG: true, define: true, AMprocessNode: true, MathJax: true, window: true, document: true, init: true, translateASCIIMath: true, google: true*/
 34 
 35 /*jslint nomen: true, plusplus: true*/
 36 
 37 /* depends:
 38  jxg
 39  base/constants
 40  base/coords
 41  options
 42  math/numerics
 43  math/math
 44  math/geometry
 45  math/complex
 46  parser/jessiecode
 47  parser/geonext
 48  utils/color
 49  utils/type
 50  utils/event
 51  utils/env
 52   elements:
 53    transform
 54    point
 55    line
 56    text
 57    grid
 58  */
 59 
 60 /**
 61  * @fileoverview The JXG.Board class is defined in this file. JXG.Board controls all properties and methods
 62  * used to manage a geonext board like managing geometric elements, managing mouse and touch events, etc.
 63  */
 64 
 65 define([
 66     'jxg', 'base/constants', 'base/coords', 'options', 'math/numerics', 'math/math', 'math/geometry', 'math/complex',
 67     'parser/jessiecode', 'parser/geonext', 'utils/color', 'utils/type', 'utils/event', 'utils/env', 'base/transformation',
 68     'base/point', 'base/line', 'base/text', 'element/composition', 'base/composition'
 69 ], function (JXG, Const, Coords, Options, Numerics, Mat, Geometry, Complex, JessieCode, GeonextParser, Color, Type,
 70                 EventEmitter, Env, Transform, Point, Line, Text, Composition, EComposition) {
 71 
 72     'use strict';
 73 
 74     /**
 75      * Constructs a new Board object.
 76      * @class JXG.Board controls all properties and methods used to manage a geonext board like managing geometric
 77      * elements, managing mouse and touch events, etc. You probably don't want to use this constructor directly.
 78      * Please use {@link JXG.JSXGraph#initBoard} to initialize a board.
 79      * @constructor
 80      * @param {String} container The id or reference of the HTML DOM element the board is drawn in. This is usually a HTML div.
 81      * @param {JXG.AbstractRenderer} renderer The reference of a renderer.
 82      * @param {String} id Unique identifier for the board, may be an empty string or null or even undefined.
 83      * @param {JXG.Coords} origin The coordinates where the origin is placed, in user coordinates.
 84      * @param {Number} zoomX Zoom factor in x-axis direction
 85      * @param {Number} zoomY Zoom factor in y-axis direction
 86      * @param {Number} unitX Units in x-axis direction
 87      * @param {Number} unitY Units in y-axis direction
 88      * @param {Number} canvasWidth  The width of canvas
 89      * @param {Number} canvasHeight The height of canvas
 90      * @param {Object} attributes The attributes object given to {@link JXG.JSXGraph#initBoard}
 91      * @borrows JXG.EventEmitter#on as this.on
 92      * @borrows JXG.EventEmitter#off as this.off
 93      * @borrows JXG.EventEmitter#triggerEventHandlers as this.triggerEventHandlers
 94      * @borrows JXG.EventEmitter#eventHandlers as this.eventHandlers
 95      */
 96     JXG.Board = function (container, renderer, id, origin, zoomX, zoomY, unitX, unitY, canvasWidth, canvasHeight, attributes) {
 97         /**
 98          * Board is in no special mode, objects are highlighted on mouse over and objects may be
 99          * clicked to start drag&drop.
100          * @type Number
101          * @constant
102          */
103         this.BOARD_MODE_NONE = 0x0000;
104 
105         /**
106          * Board is in drag mode, objects aren't highlighted on mouse over and the object referenced in
107          * {JXG.Board#mouse} is updated on mouse movement.
108          * @type Number
109          * @constant
110          * @see JXG.Board#drag_obj
111          */
112         this.BOARD_MODE_DRAG = 0x0001;
113 
114         /**
115          * In this mode a mouse move changes the origin's screen coordinates.
116          * @type Number
117          * @constant
118          */
119         this.BOARD_MODE_MOVE_ORIGIN = 0x0002;
120 
121         /**
122          * Update is made with low quality, e.g. graphs are evaluated at a lesser amount of points.
123          * @type Number
124          * @constant
125          * @see JXG.Board#updateQuality
126          */
127         this.BOARD_QUALITY_LOW = 0x1;
128 
129         /**
130          * Update is made with high quality, e.g. graphs are evaluated at much more points.
131          * @type Number
132          * @constant
133          * @see JXG.Board#updateQuality
134          */
135         this.BOARD_QUALITY_HIGH = 0x2;
136 
137         /**
138          * Update is made with high quality, e.g. graphs are evaluated at much more points.
139          * @type Number
140          * @constant
141          * @see JXG.Board#updateQuality
142          */
143         this.BOARD_MODE_ZOOM = 0x0011;
144 
145         /**
146          * Pointer to the document element containing the board.
147          * @type Object
148          */
149         this.document = attributes.document || document;
150         
151         /**
152          * The html-id of the html element containing the board.
153          * @type String
154          */
155         this.container = container;
156 
157         /**
158          * Pointer to the html element containing the board.
159          * @type Object
160          */
161         this.containerObj = (Env.isBrowser ? this.document.getElementById(this.container) : null);
162 
163         if (Env.isBrowser && this.containerObj === null) {
164             throw new Error("\nJSXGraph: HTML container element '" + container + "' not found.");
165         }
166 
167         /**
168          * A reference to this boards renderer.
169          * @type JXG.AbstractRenderer
170          */
171         this.renderer = renderer;
172 
173         /**
174          * Grids keeps track of all grids attached to this board.
175          */
176         this.grids = [];
177 
178         /**
179          * Some standard options
180          * @type JXG.Options
181          */
182         this.options = Type.deepCopy(Options);
183         this.attr = attributes;
184 
185         /**
186          * Dimension of the board.
187          * @default 2
188          * @type Number
189          */
190         this.dimension = 2;
191 
192         this.jc = new JessieCode();
193         this.jc.use(this);
194 
195         /**
196          * Coordinates of the boards origin. This a object with the two properties
197          * usrCoords and scrCoords. usrCoords always equals [1, 0, 0] and scrCoords
198          * stores the boards origin in homogeneous screen coordinates.
199          * @type Object
200          */
201         this.origin = {};
202         this.origin.usrCoords = [1, 0, 0];
203         this.origin.scrCoords = [1, origin[0], origin[1]];
204 
205         /**
206          * Zoom factor in X direction. It only stores the zoom factor to be able
207          * to get back to 100% in zoom100().
208          * @type Number
209          */
210         this.zoomX = zoomX;
211 
212         /**
213          * Zoom factor in Y direction. It only stores the zoom factor to be able
214          * to get back to 100% in zoom100().
215          * @type Number
216          */
217         this.zoomY = zoomY;
218 
219         /**
220          * The number of pixels which represent one unit in user-coordinates in x direction.
221          * @type Number
222          */
223         this.unitX = unitX * this.zoomX;
224 
225         /**
226          * The number of pixels which represent one unit in user-coordinates in y direction.
227          * @type Number
228          */
229         this.unitY = unitY * this.zoomY;
230 
231         /**
232          * Canvas width.
233          * @type Number
234          */
235         this.canvasWidth = canvasWidth;
236 
237         /**
238          * Canvas Height
239          * @type Number
240          */
241         this.canvasHeight = canvasHeight;
242 
243         // If the given id is not valid, generate an unique id
244         if (Type.exists(id) && id !== '' && Env.isBrowser && !Type.exists(this.document.getElementById(id))) {
245             this.id = id;
246         } else {
247             this.id = this.generateId();
248         }
249 
250         EventEmitter.eventify(this);
251 
252         this.hooks = [];
253 
254         /**
255          * An array containing all other boards that are updated after this board has been updated.
256          * @type Array
257          * @see JXG.Board#addChild
258          * @see JXG.Board#removeChild
259          */
260         this.dependentBoards = [];
261 
262         /**
263          * During the update process this is set to false to prevent an endless loop.
264          * @default false
265          * @type Boolean
266          */
267         this.inUpdate = false;
268 
269         /**
270          * An associative array containing all geometric objects belonging to the board. Key is the id of the object and value is a reference to the object.
271          * @type Object
272          */
273         this.objects = {};
274 
275         /**
276          * An array containing all geometric objects on the board in the order of construction.
277          * @type {Array}
278          */
279         this.objectsList = [];
280 
281         /**
282          * An associative array containing all groups belonging to the board. Key is the id of the group and value is a reference to the object.
283          * @type Object
284          */
285         this.groups = {};
286 
287         /**
288          * Stores all the objects that are currently running an animation.
289          * @type Object
290          */
291         this.animationObjects = {};
292 
293         /**
294          * An associative array containing all highlighted elements belonging to the board.
295          * @type Object
296          */
297         this.highlightedObjects = {};
298 
299         /**
300          * Number of objects ever created on this board. This includes every object, even invisible and deleted ones.
301          * @type Number
302          */
303         this.numObjects = 0;
304 
305         /**
306          * An associative array to store the objects of the board by name. the name of the object is the key and value is a reference to the object.
307          * @type Object
308          */
309         this.elementsByName = {};
310 
311         /**
312          * The board mode the board is currently in. Possible values are
313          * <ul>
314          * <li>JXG.Board.BOARD_MODE_NONE</li>
315          * <li>JXG.Board.BOARD_MODE_DRAG</li>
316          * <li>JXG.Board.BOARD_MODE_MOVE_ORIGIN</li>
317          * </ul>
318          * @type Number
319          */
320         this.mode = this.BOARD_MODE_NONE;
321 
322         /**
323          * The update quality of the board. In most cases this is set to {@link JXG.Board#BOARD_QUALITY_HIGH}.
324          * If {@link JXG.Board#mode} equals {@link JXG.Board#BOARD_MODE_DRAG} this is set to
325          * {@link JXG.Board#BOARD_QUALITY_LOW} to speed up the update process by e.g. reducing the number of
326          * evaluation points when plotting functions. Possible values are
327          * <ul>
328          * <li>BOARD_QUALITY_LOW</li>
329          * <li>BOARD_QUALITY_HIGH</li>
330          * </ul>
331          * @type Number
332          * @see JXG.Board#mode
333          */
334         this.updateQuality = this.BOARD_QUALITY_HIGH;
335 
336         /**
337          * If true updates are skipped.
338          * @type Boolean
339          */
340         this.isSuspendedRedraw = false;
341 
342         this.calculateSnapSizes();
343 
344         /**
345          * The distance from the mouse to the dragged object in x direction when the user clicked the mouse button.
346          * @type Number
347          * @see JXG.Board#drag_dy
348          * @see JXG.Board#drag_obj
349          */
350         this.drag_dx = 0;
351 
352         /**
353          * The distance from the mouse to the dragged object in y direction when the user clicked the mouse button.
354          * @type Number
355          * @see JXG.Board#drag_dx
356          * @see JXG.Board#drag_obj
357          */
358         this.drag_dy = 0;
359 
360         /**
361          * The last position where a drag event has been fired.
362          * @type Array
363          * @see JXG.Board#moveObject
364          */
365         this.drag_position = [0, 0];
366 
367         /**
368          * References to the object that is dragged with the mouse on the board.
369          * @type {@link JXG.GeometryElement}.
370          * @see {JXG.Board#touches}
371          */
372         this.mouse = {};
373 
374         /**
375          * Keeps track on touched elements, like {@link JXG.Board#mouse} does for mouse events.
376          * @type Array
377          * @see {JXG.Board#mouse}
378          */
379         this.touches = [];
380 
381         /**
382          * A string containing the XML text of the construction. This is set in {@link JXG.FileReader#parseString}.
383          * Only useful if a construction is read from a GEONExT-, Intergeo-, Geogebra-, or Cinderella-File.
384          * @type String
385          */
386         this.xmlString = '';
387 
388         /**
389          * Cached result of getCoordsTopLeftCorner for touch/mouseMove-Events to save some DOM operations.
390          * @type Array
391          */
392         this.cPos = [];
393 
394         /**
395          * Contains the last time (epoch, msec) since the last touchMove event which was not thrown away or since
396          * touchStart because Android's Webkit browser fires too much of them.
397          * @type Number
398          */
399         this.touchMoveLast = 0;
400 
401         /**
402          * Contains the last time (epoch, msec) since the last getCoordsTopLeftCorner call which was not thrown away.
403          * @type Number
404          */
405         this.positionAccessLast = 0;
406 
407         /**
408          * Collects all elements that triggered a mouse down event.
409          * @type Array
410          */
411         this.downObjects = [];
412 
413         if (this.attr.showcopyright) {
414             this.renderer.displayCopyright(Const.licenseText, parseInt(this.options.text.fontSize, 10));
415         }
416 
417         /**
418          * Full updates are needed after zoom and axis translates. This saves some time during an update.
419          * @default false
420          * @type Boolean
421          */
422         this.needsFullUpdate = false;
423 
424         /**
425          * If reducedUpdate is set to true then only the dragged element and few (e.g. 2) following
426          * elements are updated during mouse move. On mouse up the whole construction is
427          * updated. This enables us to be fast even on very slow devices.
428          * @type Boolean
429          * @default false
430          */
431         this.reducedUpdate = false;
432 
433         /**
434          * The current color blindness deficiency is stored in this property. If color blindness is not emulated
435          * at the moment, it's value is 'none'.
436          */
437         this.currentCBDef = 'none';
438 
439         /**
440          * If GEONExT constructions are displayed, then this property should be set to true.
441          * At the moment there should be no difference. But this may change.
442          * This is set in {@link JXG.GeonextReader#readGeonext}.
443          * @type Boolean
444          * @default false
445          * @see JXG.GeonextReader#readGeonext
446          */
447         this.geonextCompatibilityMode = false;
448 
449         if (this.options.text.useASCIIMathML && translateASCIIMath) {
450             init();
451         } else {
452             this.options.text.useASCIIMathML = false;
453         }
454 
455         /**
456          * A flag which tells if the board registers mouse events.
457          * @type Boolean
458          * @default false
459          */
460         this.hasMouseHandlers = false;
461 
462         /**
463          * A flag which tells if the board registers touch events.
464          * @type Boolean
465          * @default false
466          */
467         this.hasTouchHandlers = false;
468 
469         /**
470          * A flag which stores if the board registered pointer events.
471          * @type {Boolean}
472          * @default false
473          */
474         this.hasPointerHandlers = false;
475 
476         /**
477          * This bool flag stores the current state of the mobile Safari specific gesture event handlers.
478          * @type {boolean}
479          * @default false
480          */
481         this.hasGestureHandlers = false;
482 
483         /**
484          * A flag which tells if the board the JXG.Board#mouseUpListener is currently registered.
485          * @type Boolean
486          * @default false
487          */
488         this.hasMouseUp = false;
489 
490         /**
491          * A flag which tells if the board the JXG.Board#touchEndListener is currently registered.
492          * @type Boolean
493          * @default false
494          */
495         this.hasTouchEnd = false;
496 
497         /**
498          * A flag which tells us if the board has a pointerUp event registered at the moment.
499          * @type {Boolean}
500          * @default false
501          */
502         this.hasPointerUp = false;
503 
504         if (this.attr.registerevents) {
505             this.addEventHandlers();
506         }
507 
508         this.methodMap = {
509             update: 'update',
510             fullUpdate: 'fullUpdate',
511             on: 'on',
512             off: 'off',
513             trigger: 'trigger',
514             setView: 'setBoundingBox',
515             setBoundingBox: 'setBoundingBox',
516             migratePoint: 'migratePoint',
517             colorblind: 'emulateColorblindness',
518             suspendUpdate: 'suspendUpdate',
519             unsuspendUpdate: 'unsuspendUpdate',
520             clearTraces: 'clearTraces',
521             left: 'clickLeftArrow',
522             right: 'clickRightArrow',
523             up: 'clickUpArrow',
524             down: 'clickDownArrow',
525             zoomIn: 'zoomIn',
526             zoomOut: 'zoomOut',
527             zoom100: 'zoom100',
528             zoomElements: 'zoomElements',
529             remove: 'removeObject',
530             removeObject: 'removeObject'
531         };
532     };
533 
534     JXG.extend(JXG.Board.prototype, /** @lends JXG.Board.prototype */ {
535 
536         /**
537          * Generates an unique name for the given object. The result depends on the objects type, if the
538          * object is a {@link JXG.Point}, capital characters are used, if it is of type {@link JXG.Line}
539          * only lower case characters are used. If object is of type {@link JXG.Polygon}, a bunch of lower
540          * case characters prefixed with P_ are used. If object is of type {@link JXG.Circle} the name is
541          * generated using lower case characters. prefixed with k_ is used. In any other case, lower case
542          * chars prefixed with s_ is used.
543          * @param {Object} object Reference of an JXG.GeometryElement that is to be named.
544          * @returns {String} Unique name for the object.
545          */
546         generateName: function (object) {
547             var possibleNames, i, j,
548                 maxNameLength = 2,
549                 pre = '',
550                 post = '',
551                 indices = [],
552                 name = '';
553 
554             if (object.type === Const.OBJECT_TYPE_TICKS) {
555                 return '';
556             }
557 
558             if (object.elementClass === Const.OBJECT_CLASS_POINT) {
559                 // points have capital letters
560                 possibleNames = ['', 'A', 'B', 'C', 'D', 'E', 'F', 'G', 'H', 'I', 'J', 'K', 'L', 'M', 'N', 'O',
561                     'P', 'Q', 'R', 'S', 'T', 'U', 'V', 'W', 'X', 'Y', 'Z'];
562             } else if (object.type === Const.OBJECT_TYPE_ANGLE) {
563                 possibleNames = ['', 'α', 'β', 'γ', 'δ', 'ε', 'ζ', 'η', 'θ',
564                     'ι', 'κ', 'λ', 'μ', 'ν', 'ξ', 'ο', 'π', 'ρ',
565                     'σ', 'τ', 'υ', 'φ', 'χ', 'ψ', 'ω'];
566             } else {
567                 // all other elements get lowercase labels
568                 possibleNames = ['', 'a', 'b', 'c', 'd', 'e', 'f', 'g', 'h', 'i', 'j', 'k', 'l', 'm', 'n', 'o',
569                     'p', 'q', 'r', 's', 't', 'u', 'v', 'w', 'x', 'y', 'z'];
570             }
571 
572             if (object.elementClass !== Const.OBJECT_CLASS_POINT &&
573                     object.elementClass !== Const.OBJECT_CLASS_LINE &&
574                     object.type !== Const.OBJECT_TYPE_ANGLE) {
575                 if (object.type === Const.OBJECT_TYPE_POLYGON) {
576                     pre = 'P_{';
577                 } else if (object.elementClass === Const.OBJECT_CLASS_CIRCLE) {
578                     pre = 'k_{';
579                 } else if (object.type === Const.OBJECT_TYPE_TEXT) {
580                     pre = 't_{';
581                 } else {
582                     pre = 's_{';
583                 }
584                 post = '}';
585             }
586 
587             for (i = 0; i < maxNameLength; i++) {
588                 indices[i] = 0;
589             }
590 
591             while (indices[maxNameLength - 1] < possibleNames.length) {
592                 for (indices[0] = 1; indices[0] < possibleNames.length; indices[0]++) {
593                     name = pre;
594 
595                     for (i = maxNameLength; i > 0; i--) {
596                         name += possibleNames[indices[i - 1]];
597                     }
598 
599                     if (!Type.exists(this.elementsByName[name + post])) {
600                         return name + post;
601                     }
602 
603                 }
604                 indices[0] = possibleNames.length;
605 
606                 for (i = 1; i < maxNameLength; i++) {
607                     if (indices[i - 1] === possibleNames.length) {
608                         indices[i - 1] = 1;
609                         indices[i] += 1;
610                     }
611                 }
612             }
613 
614             return '';
615         },
616 
617         /**
618          * Generates unique id for a board. The result is randomly generated and prefixed with 'jxgBoard'.
619          * @returns {String} Unique id for a board.
620          */
621         generateId: function () {
622             var r = 1;
623 
624             // as long as we don't have a unique id generate a new one
625             while (Type.exists(JXG.boards['jxgBoard' + r])) {
626                 r = Math.round(Math.random() * 65535);
627             }
628 
629             return ('jxgBoard' + r);
630         },
631 
632         /**
633          * Composes an id for an element. If the ID is empty ('' or null) a new ID is generated, depending on the
634          * object type. Additionally, the id of the label is set. As a side effect {@link JXG.Board#numObjects}
635          * is updated.
636          * @param {Object} obj Reference of an geometry object that needs an id.
637          * @param {Number} type Type of the object.
638          * @returns {String} Unique id for an element.
639          */
640         setId: function (obj, type) {
641             var num = this.numObjects,
642                 elId = obj.id;
643 
644             this.numObjects += 1;
645 
646             // Falls Id nicht vorgegeben, eine Neue generieren:
647             if (elId === '' || !Type.exists(elId)) {
648                 elId = this.id + type + num;
649             }
650 
651             obj.id = elId;
652             this.objects[elId] = obj;
653             obj._pos = this.objectsList.length;
654             this.objectsList[this.objectsList.length] = obj;
655 
656             return elId;
657         },
658 
659         /**
660          * After construction of the object the visibility is set
661          * and the label is constructed if necessary.
662          * @param {Object} obj The object to add.
663          */
664         finalizeAdding: function (obj) {
665             if (!obj.visProp.visible) {
666                 this.renderer.hide(obj);
667             }
668         },
669 
670         finalizeLabel: function (obj) {
671             if (obj.hasLabel && !obj.label.visProp.islabel && !obj.label.visProp.visible) {
672                 this.renderer.hide(obj.label);
673             }
674         },
675 
676         /**********************************************************
677          *
678          * Event Handler helpers
679          *
680          **********************************************************/
681 
682         /**
683          * Calculates mouse coordinates relative to the boards container.
684          * @returns {Array} Array of coordinates relative the boards container top left corner.
685          */
686         getCoordsTopLeftCorner: function () {
687             var cPos, doc, crect, scrollLeft, scrollTop,
688                 docElement = this.document.documentElement || this.document.body.parentNode,
689                 docBody = this.document.body,
690                 container = this.containerObj;
691 
692             /**
693              * During drags and origin moves the container element is usually not changed.
694              * Check the position of the upper left corner at most every 500 msecs
695              */
696             if (this.cPos.length > 0 &&
697                     (this.mode === this.BOARD_MODE_DRAG || this.mode === this.BOARD_MODE_MOVE_ORIGIN ||
698                     (new Date()).getTime() - this.positionAccessLast < 500)) {
699                 return this.cPos;
700             }
701 
702             this.positionAccessLast = (new Date()).getTime();
703 
704             // Check if getBoundingClientRect exists. If so, use this as this covers *everything*
705             // even CSS3D transformations etc.
706             if (container.getBoundingClientRect) {
707                 if (typeof window.pageXOffset === 'number') {
708                     scrollLeft = window.pageXOffset;
709                 } else {
710                     if (docElement.ScrollLeft === 'number') {
711                         scrollLeft = docElement.ScrollLeft;
712                     } else {
713                         scrollLeft = this.document.body.scrollLeft;
714                     }
715                 }
716 
717                 if (typeof window.pageYOffset === 'number') {
718                     scrollTop = window.pageYOffset;
719                 } else {
720                     if (docElement.ScrollTop === 'number') {
721                         scrollTop = docElement.ScrollTop;
722                     } else {
723                         scrollTop = this.document.body.scrollTop;
724                     }
725                 }
726 
727                 crect = container.getBoundingClientRect();
728                 cPos = [crect.left + scrollLeft, crect.top + scrollTop];
729 
730                 // add border width
731                 cPos[0] += Env.getProp(container, 'border-left-width');
732                 cPos[1] += Env.getProp(container, 'border-top-width');
733 
734                 // vml seems to ignore paddings
735                 if (this.renderer.type !== 'vml') {
736                     // add padding
737                     cPos[0] += Env.getProp(container, 'padding-left');
738                     cPos[1] += Env.getProp(container, 'padding-top');
739                 }
740 
741                 this.cpos = cPos;
742 
743                 return this.cpos;
744             }
745 
746             cPos = Env.getOffset(container);
747             doc = this.document.documentElement.ownerDocument;
748 
749             if (!this.containerObj.currentStyle && doc.defaultView) {     // Non IE
750                 // this is for hacks like this one used in wordpress for the admin bar:
751                 // html { margin-top: 28px }
752                 // seems like it doesn't work in IE
753 
754                 cPos[0] += Env.getProp(docElement, 'margin-left');
755                 cPos[1] += Env.getProp(docElement, 'margin-top');
756 
757                 cPos[0] += Env.getProp(docElement, 'border-left-width');
758                 cPos[1] += Env.getProp(docElement, 'border-top-width');
759 
760                 cPos[0] += Env.getProp(docElement, 'padding-left');
761                 cPos[1] += Env.getProp(docElement, 'padding-top');
762             }
763 
764             if (docBody) {
765                 cPos[0] += Env.getProp(docBody, 'left');
766                 cPos[1] += Env.getProp(docBody, 'top');
767             }
768 
769             // Google Translate offers widgets for web authors. These widgets apparently tamper with the clientX
770             // and clientY coordinates of the mouse events. The minified sources seem to be the only publicly
771             // available version so we're doing it the hacky way: Add a fixed offset.
772             // see https://groups.google.com/d/msg/google-translate-general/H2zj0TNjjpY/jw6irtPlCw8J
773             if (typeof google === 'object' && google.translate) {
774                 cPos[0] += 10;
775                 cPos[1] += 25;
776             }
777 
778             // add border width
779             cPos[0] += Env.getProp(container, 'border-left-width');
780             cPos[1] += Env.getProp(container, 'border-top-width');
781 
782             // vml seems to ignore paddings
783             if (this.renderer.type !== 'vml') {
784                 // add padding
785                 cPos[0] += Env.getProp(container, 'padding-left');
786                 cPos[1] += Env.getProp(container, 'padding-top');
787             }
788 
789             cPos[0] += this.attr.offsetx;
790             cPos[1] += this.attr.offsety;
791 
792             this.cPos = cPos;
793 
794             return cPos;
795         },
796 
797         /**
798          * Get the position of the mouse in screen coordinates, relative to the upper left corner
799          * of the host tag.
800          * @param {Event} e Event object given by the browser.
801          * @param {Number} [i] Only use in case of touch events. This determines which finger to use and should not be set
802          * for mouseevents.
803          * @returns {Array} Contains the mouse coordinates in user coordinates, ready  for {@link JXG.Coords}
804          */
805         getMousePosition: function (e, i) {
806             var cPos = this.getCoordsTopLeftCorner(),
807                 absPos,
808                 v;
809 
810             // This fixes the object-drag bug on zoomed webpages on Android powered devices with the default WebKit browser
811             // Seems to be obsolete now
812             //if (Env.isWebkitAndroid()) {
813             //    cPos[0] -= document.body.scrollLeft;
814             //    cPos[1] -= document.body.scrollTop;
815             //}
816 
817             // position of mouse cursor relative to containers position of container
818             absPos = Env.getPosition(e, i, this.document);
819 
820             /**
821              * In case there has been no down event before.
822              */
823             if (!Type.exists(this.cssTransMat)) {
824                 this.updateCSSTransforms();
825             }
826             v = [1, absPos[0] - cPos[0], absPos[1] - cPos[1]];
827             v = Mat.matVecMult(this.cssTransMat, v);
828             v[1] /= v[0];
829             v[2] /= v[0];
830             return [v[1], v[2]];
831 
832             // Method without CSS transformation
833             /*
834              return [absPos[0] - cPos[0], absPos[1] - cPos[1]];
835              */
836         },
837 
838         /**
839          * Initiate moving the origin. This is used in mouseDown and touchStart listeners.
840          * @param {Number} x Current mouse/touch coordinates
841          * @param {Number} y Current mouse/touch coordinates
842          */
843         initMoveOrigin: function (x, y) {
844             this.drag_dx = x - this.origin.scrCoords[1];
845             this.drag_dy = y - this.origin.scrCoords[2];
846 
847             this.mode = this.BOARD_MODE_MOVE_ORIGIN;
848             this.updateQuality = this.BOARD_QUALITY_LOW;
849         },
850 
851         /**
852          * Collects all elements below the current mouse pointer and fulfilling the following constraints:
853          * <ul><li>isDraggable</li><li>visible</li><li>not fixed</li><li>not frozen</li></ul>
854          * @param {Number} x Current mouse/touch coordinates
855          * @param {Number} y current mouse/touch coordinates
856          * @param {Object} evt An event object
857          * @param {String} type What type of event? 'touch' or 'mouse'.
858          * @returns {Array} A list of geometric elements.
859          */
860         initMoveObject: function (x, y, evt, type) {
861             var pEl, el, collect = [], haspoint, len = this.objectsList.length,
862                 dragEl = {visProp: {layer: -10000}};
863 
864             //for (el in this.objects) {
865             for (el = 0; el < len; el++) {
866                 pEl = this.objectsList[el];
867                 haspoint = pEl.hasPoint && pEl.hasPoint(x, y);
868 
869                 if (pEl.visProp.visible && haspoint) {
870                     pEl.triggerEventHandlers([type + 'down', 'down'], [evt]);
871                     this.downObjects.push(pEl);
872                 }
873 
874                 if (((this.geonextCompatibilityMode &&
875                         (pEl.elementClass === Const.OBJECT_CLASS_POINT || 
876                           pEl.type === Const.OBJECT_TYPE_TEXT)) ||
877                         !this.geonextCompatibilityMode) &&
878                         pEl.isDraggable &&
879                         pEl.visProp.visible &&
880                         (!pEl.visProp.fixed) && (!pEl.visProp.frozen) &&
881                         haspoint) {
882                     // Elements in the highest layer get priority.
883                     if (pEl.visProp.layer > dragEl.visProp.layer ||
884                             (pEl.visProp.layer === dragEl.visProp.layer && pEl.lastDragTime.getTime() >= dragEl.lastDragTime.getTime())) {
885                         // If an element and its label have the focus
886                         // simultaneously, the element is taken
887                         // this only works if we assume that every browser runs
888                         // through this.objects in the right order, i.e. an element A
889                         // added before element B turns up here before B does.
890                         if (!Type.exists(dragEl.label) || pEl !== dragEl.label) {
891                             dragEl = pEl;
892                             collect[0] = dragEl;
893 
894                             // we can't drop out of this loop because of the event handling system
895                             //if (this.attr.takefirst) {
896                             //    return collect;
897                             //}
898                         }
899                     }
900                 }
901             }
902 
903             if (collect.length > 0) {
904                 this.mode = this.BOARD_MODE_DRAG;
905             }
906 
907             if (this.attr.takefirst) {
908                 collect.length = 1;
909             }
910 
911             return collect;
912         },
913 
914         /**
915          * Moves an object.
916          * @param {Number} x Coordinate
917          * @param {Number} y Coordinate
918          * @param {Object} o The touch object that is dragged: {JXG.Board#mouse} or {JXG.Board#touches}.
919          * @param {Object} evt The event object.
920          * @param {String} type Mouse or touch event?
921          */
922         moveObject: function (x, y, o, evt, type) {
923             var newPos = new Coords(Const.COORDS_BY_SCREEN, this.getScrCoordsOfMouse(x, y), this),
924                 drag = o.obj,
925                 oldCoords;
926 
927             if (!drag) {
928                 return;
929             }
930 
931             /*
932              * Save the position.
933              */
934             //this.drag_position = newPos.scrCoords.slice(1);
935             this.drag_position = [newPos.scrCoords[1], newPos.scrCoords[2]];
936 
937             if (drag.type !== Const.OBJECT_TYPE_GLIDER) {
938                 if (!isNaN(o.targets[0].Xprev + o.targets[0].Yprev)) {
939                     drag.setPositionDirectly(Const.COORDS_BY_SCREEN, 
940                     //newPos.scrCoords.slice(1), 
941                     [newPos.scrCoords[1], newPos.scrCoords[2]], 
942                     [o.targets[0].Xprev, o.targets[0].Yprev]);
943                 }
944                 // Remember the actual position for the next move event. Then we are able to
945                 // compute the difference vector.
946                 o.targets[0].Xprev = newPos.scrCoords[1];
947                 o.targets[0].Yprev = newPos.scrCoords[2];
948                 //this.update(drag);
949                 drag.prepareUpdate().update(false).updateRenderer();
950             } else if (drag.type === Const.OBJECT_TYPE_GLIDER) {
951                 oldCoords = drag.coords;  // Used in group mode
952 
953                 // First the new position of the glider is set to the new mouse position
954                 drag.setPositionDirectly(Const.COORDS_BY_USER, newPos.usrCoords.slice(1));
955 
956                 // Now, we have to adjust the other group elements again.
957                 if (drag.group.length !== 0) {
958                     // Then, from this position we compute the projection to the object the glider on which the glider lives.
959                     // Do we really need this?
960                     if (drag.slideObject.elementClass === Const.OBJECT_CLASS_CIRCLE) {
961                         drag.coords.setCoordinates(Const.COORDS_BY_USER, Geometry.projectPointToCircle(drag, drag.slideObject, this).usrCoords, false);
962                     } else if (drag.slideObject.elementClass === Const.OBJECT_CLASS_LINE) {
963                         drag.coords.setCoordinates(Const.COORDS_BY_USER, Geometry.projectPointToLine(drag, drag.slideObject, this).usrCoords, false);
964                     }
965 
966                     drag.group[drag.group.length - 1].dX = drag.coords.scrCoords[1] - oldCoords.scrCoords[1];
967                     drag.group[drag.group.length - 1].dY = drag.coords.scrCoords[2] - oldCoords.scrCoords[2];
968                     drag.group[drag.group.length - 1].update(this);
969                 } else {
970                     // This update triggers Point.updateGlider() instead of Point.updateGliderFromParent():
971                     //
972                     //this.update(drag);
973                     drag.prepareUpdate().update(false).updateRenderer();
974                 }
975             }
976 
977             drag.triggerEventHandlers([type + 'drag', 'drag'], [evt]);
978 
979             this.updateInfobox(drag);
980             this.update();
981             drag.highlight(true);
982 
983             drag.lastDragTime = new Date();
984         },
985 
986         /**
987          * Moves elements in multitouch mode.
988          * @param {Array} p1 x,y coordinates of first touch
989          * @param {Array} p2 x,y coordinates of second touch
990          * @param {Object} o The touch object that is dragged: {JXG.Board#touches}.
991          * @param {Object} evt The event object that lead to this movement.
992          */
993         twoFingerMove: function (p1, p2, o, evt) {
994             var np1c, np2c, drag;
995 
996             if (Type.exists(o) && Type.exists(o.obj)) {
997                 drag = o.obj;
998             } else {
999                 return;
1000             }
1001 
1002             // New finger position
1003             np1c = new Coords(Const.COORDS_BY_SCREEN, this.getScrCoordsOfMouse(p1[0], p1[1]), this);
1004             np2c = new Coords(Const.COORDS_BY_SCREEN, this.getScrCoordsOfMouse(p2[0], p2[1]), this);
1005 
1006             if (drag.elementClass === Const.OBJECT_CLASS_LINE ||
1007                     drag.type === Const.OBJECT_TYPE_POLYGON) {
1008                 this.twoFingerTouchObject(np1c, np2c, o, drag);
1009             } else if (drag.elementClass === Const.OBJECT_CLASS_CIRCLE) {
1010                 this.twoFingerTouchCircle(np1c, np2c, o, drag);
1011             }
1012             drag.triggerEventHandlers(['touchdrag', 'drag'], [evt]);
1013 
1014             o.targets[0].Xprev = np1c.scrCoords[1];
1015             o.targets[0].Yprev = np1c.scrCoords[2];
1016             o.targets[1].Xprev = np2c.scrCoords[1];
1017             o.targets[1].Yprev = np2c.scrCoords[2];
1018         },
1019 
1020         /**
1021          * Moves a line or polygon with two fingers
1022          * @param {JXG.Coords} np1c x,y coordinates of first touch
1023          * @param {JXG.Coords} np2c x,y coordinates of second touch
1024          * @param {object} o The touch object that is dragged: {JXG.Board#touches}.
1025          * @param {object} drag The object that is dragged:
1026          */
1027         twoFingerTouchObject: function (np1c, np2c, o, drag) {
1028             var np1, np2, op1, op2,
1029                 nmid, omid, nd, od,
1030                 d,
1031                 S, alpha, t1, t2, t3, t4, t5;
1032 
1033             if (Type.exists(o.targets[0]) &&
1034                     Type.exists(o.targets[1]) &&
1035                     !isNaN(o.targets[0].Xprev + o.targets[0].Yprev + o.targets[1].Xprev + o.targets[1].Yprev)) {
1036                 np1 = np1c.usrCoords;
1037                 np2 = np2c.usrCoords;
1038                 // Previous finger position
1039                 op1 = (new Coords(Const.COORDS_BY_SCREEN, [o.targets[0].Xprev, o.targets[0].Yprev], this)).usrCoords;
1040                 op2 = (new Coords(Const.COORDS_BY_SCREEN, [o.targets[1].Xprev, o.targets[1].Yprev], this)).usrCoords;
1041 
1042                 // Affine mid points of the old and new positions
1043                 omid = [1, (op1[1] + op2[1]) * 0.5, (op1[2] + op2[2]) * 0.5];
1044                 nmid = [1, (np1[1] + np2[1]) * 0.5, (np1[2] + np2[2]) * 0.5];
1045 
1046                 // Old and new directions
1047                 od = Mat.crossProduct(op1, op2);
1048                 nd = Mat.crossProduct(np1, np2);
1049                 S = Mat.crossProduct(od, nd);
1050 
1051                 // If parallel, translate otherwise rotate
1052                 if (Math.abs(S[0]) < Mat.eps) {
1053                     return;
1054                 }
1055 
1056                 S[1] /= S[0];
1057                 S[2] /= S[0];
1058                 alpha = Geometry.rad(omid.slice(1), S.slice(1), nmid.slice(1));
1059                 t1 = this.create('transform', [alpha, S[1], S[2]], {type: 'rotate'});
1060 
1061                 // Old midpoint of fingers after first transformation:
1062                 t1.update();
1063                 omid = Mat.matVecMult(t1.matrix, omid);
1064                 omid[1] /= omid[0];
1065                 omid[2] /= omid[0];
1066 
1067                 // Shift to the new mid point
1068                 t2 = this.create('transform', [nmid[1] - omid[1], nmid[2] - omid[2]], {type: 'translate'});
1069                 t2.update();
1070                 //omid = Mat.matVecMult(t2.matrix, omid);
1071 
1072                 t1.melt(t2);
1073                 if (drag.visProp.scalable) {
1074                     // Scale
1075                     d = Geometry.distance(np1, np2) / Geometry.distance(op1, op2);
1076                     t3 = this.create('transform', [-nmid[1], -nmid[2]], {type: 'translate'});
1077                     t4 = this.create('transform', [d, d], {type: 'scale'});
1078                     t5 = this.create('transform', [nmid[1], nmid[2]], {type: 'translate'});
1079                     t1.melt(t3).melt(t4).melt(t5);
1080                 }
1081 
1082                 if (drag.elementClass === Const.OBJECT_CLASS_LINE) {
1083                     t1.applyOnce([drag.point1, drag.point2]);
1084                 } else if (drag.type === Const.OBJECT_TYPE_POLYGON) {
1085                     t1.applyOnce(drag.vertices.slice(0, -1));
1086                 }
1087 
1088                 this.update();
1089                 drag.highlight(true);
1090             }
1091         },
1092 
1093         /*
1094          * Moves a circle with two fingers
1095          * @param {JXG.Coords} np1c x,y coordinates of first touch
1096          * @param {JXG.Coords} np2c x,y coordinates of second touch
1097          * @param {object} o The touch object that is dragged: {JXG.Board#touches}.
1098          * @param {object} drag The object that is dragged:
1099          */
1100         twoFingerTouchCircle: function (np1c, np2c, o, drag) {
1101             var np1, np2, op1, op2,
1102                 d, alpha, t1, t2, t3, t4, t5;
1103 
1104             if (drag.method === 'pointCircle' ||
1105                     drag.method === 'pointLine') {
1106                 return;
1107             }
1108 
1109             if (Type.exists(o.targets[0]) &&
1110                     Type.exists(o.targets[1]) &&
1111                     !isNaN(o.targets[0].Xprev + o.targets[0].Yprev + o.targets[1].Xprev + o.targets[1].Yprev)) {
1112 
1113                 np1 = np1c.usrCoords;
1114                 np2 = np2c.usrCoords;
1115                 // Previous finger position
1116                 op1 = (new Coords(Const.COORDS_BY_SCREEN, [o.targets[0].Xprev, o.targets[0].Yprev], this)).usrCoords;
1117                 op2 = (new Coords(Const.COORDS_BY_SCREEN, [o.targets[1].Xprev, o.targets[1].Yprev], this)).usrCoords;
1118 
1119                 // Shift by the movement of the first finger
1120                 t1 = this.create('transform', [np1[1] - op1[1], np1[2] - op1[2]], {type: 'translate'});
1121                 alpha = Geometry.rad(op2.slice(1), np1.slice(1), np2.slice(1));
1122 
1123                 // Rotate and scale by the movement of the second finger
1124                 t2 = this.create('transform', [-np1[1], -np1[2]], {type: 'translate'});
1125                 t3 = this.create('transform', [alpha], {type: 'rotate'});
1126                 t1.melt(t2).melt(t3);
1127 
1128                 if (drag.visProp.scalable) {
1129                     d = Geometry.distance(np1, np2) / Geometry.distance(op1, op2);
1130                     t4 = this.create('transform', [d, d], {type: 'scale'});
1131                     t1.melt(t4);
1132                 }
1133                 t5 = this.create('transform', [ np1[1], np1[2]], {type: 'translate'});
1134                 t1.melt(t5);
1135 
1136                 t1.applyOnce([drag.center]);
1137 
1138                 if (drag.method === 'twoPoints') {
1139                     t1.applyOnce([drag.point2]);
1140                 } else if (drag.method === 'pointRadius') {
1141                     if (Type.isNumber(drag.updateRadius.origin)) {
1142                         drag.setRadius(drag.radius * d);
1143                     }
1144                 }
1145                 this.update(drag.center);
1146                 drag.highlight(true);
1147             }
1148         },
1149 
1150         highlightElements: function (x, y, evt, target) {
1151             var el, pEl, pId,
1152                 overObjects = {},
1153                 len = this.objectsList.length;
1154 
1155             // Elements  below the mouse pointer which are not highlighted yet will be highlighted.
1156             for (el = 0; el < len; el++) {
1157                 pEl = this.objectsList[el];
1158                 pId = pEl.id;
1159                 if (Type.exists(pEl.hasPoint) && pEl.visProp.visible && pEl.hasPoint(x, y)) {
1160                     // this is required in any case because otherwise the box won't be shown until the point is dragged
1161                     this.updateInfobox(pEl);
1162 
1163                     if (!Type.exists(this.highlightedObjects[pId])) { // highlight only if not highlighted
1164                         overObjects[pId] = pEl;
1165                         pEl.highlight();
1166                         this.triggerEventHandlers(['mousehit', 'hit'], [evt, pEl, target]);
1167                     }
1168 
1169                     if (pEl.mouseover) {
1170                         pEl.triggerEventHandlers(['mousemove', 'move'], [evt]);
1171                     } else {
1172                         pEl.triggerEventHandlers(['mouseover', 'over'], [evt]);
1173                         pEl.mouseover = true;
1174                     }
1175                 }
1176             }
1177 
1178             for (el = 0; el < len; el++) {
1179                 pEl = this.objectsList[el];
1180                 pId = pEl.id;
1181                 if (pEl.mouseover) {
1182                     if (!overObjects[pId]) {
1183                         pEl.triggerEventHandlers(['mouseout', 'out'], [evt]);
1184                         pEl.mouseover = false;
1185                     }
1186                 }
1187             }
1188         },
1189 
1190         /**
1191          * Helper function which returns a reasonable starting point for the object being dragged.
1192          * Formerly known as initXYstart().
1193          * @private
1194          * @param {JXG.GeometryElement} obj The object to be dragged
1195          * @param {Array} targets Array of targets. It is changed by this function.
1196          */
1197         saveStartPos: function (obj, targets) {
1198             var xy = [], i, len;
1199 
1200             if (obj.type === Const.OBJECT_TYPE_TICKS) {
1201                 xy.push([1, NaN, NaN]);
1202             } else if (obj.elementClass === Const.OBJECT_CLASS_LINE) {
1203                 xy.push(obj.point1.coords.usrCoords);
1204                 xy.push(obj.point2.coords.usrCoords);
1205             } else if (obj.elementClass === Const.OBJECT_CLASS_CIRCLE) {
1206                 xy.push(obj.center.coords.usrCoords);
1207                 if (obj.method === "twoPoints") {
1208                     xy.push(obj.point2.coords.usrCoords);
1209                 }
1210             } else if (obj.type === Const.OBJECT_TYPE_POLYGON) {
1211                 len = obj.vertices.length - 1;
1212                 for (i = 0; i < len; i++) {
1213                     xy.push(obj.vertices[i].coords.usrCoords);
1214                 }
1215             } else if (obj.elementClass === Const.OBJECT_CLASS_POINT || obj.type === Const.OBJECT_TYPE_GLIDER) {
1216                 xy.push(obj.coords.usrCoords);
1217             //} else if (obj.elementClass === Const.OBJECT_CLASS_CURVE) {
1218             // TODO
1219             } else {
1220                 try {
1221                     xy.push(obj.coords.usrCoords);
1222                 } catch (e) {
1223                     JXG.debug('JSXGraph+ saveStartPos: obj.coords.usrCoords not available: ' + e);
1224                 }
1225             }
1226 
1227             len = xy.length;
1228             for (i = 0; i < len; i++) {
1229                 targets.Zstart.push(xy[i][0]);
1230                 targets.Xstart.push(xy[i][1]);
1231                 targets.Ystart.push(xy[i][2]);
1232             }
1233         },
1234 
1235         mouseOriginMoveStart: function (evt) {
1236             var r = this.attr.pan.enabled && (!this.attr.pan.needshift || evt.shiftKey),
1237                 pos;
1238 
1239             if (r) {
1240                 pos = this.getMousePosition(evt);
1241                 this.initMoveOrigin(pos[0], pos[1]);
1242             }
1243 
1244             return r;
1245         },
1246 
1247         mouseOriginMove: function (evt) {
1248             var r = (this.mode === this.BOARD_MODE_MOVE_ORIGIN),
1249                 pos;
1250 
1251             if (r) {
1252                 pos = this.getMousePosition(evt);
1253                 this.moveOrigin(pos[0], pos[1], true);
1254             }
1255 
1256             return r;
1257         },
1258 
1259         touchOriginMoveStart: function (evt) {
1260             var touches = evt[JXG.touchProperty],
1261                 twoFingersCondition = (touches.length === 2 && Geometry.distance([touches[0].screenX, touches[0].screenY], [touches[1].screenX, touches[1].screenY]) < 80),
1262                 r = this.attr.pan.enabled && (!this.attr.pan.needtwofingers || twoFingersCondition),
1263                 pos;
1264 
1265             if (r) {
1266                 pos = this.getMousePosition(evt, 0);
1267                 this.initMoveOrigin(pos[0], pos[1]);
1268             }
1269 
1270             return r;
1271         },
1272 
1273         touchOriginMove: function (evt) {
1274             var r = (this.mode === this.BOARD_MODE_MOVE_ORIGIN),
1275                 pos;
1276 
1277             if (r) {
1278                 pos = this.getMousePosition(evt, 0);
1279                 this.moveOrigin(pos[0], pos[1], true);
1280             }
1281 
1282             return r;
1283         },
1284 
1285         originMoveEnd: function () {
1286             this.updateQuality = this.BOARD_QUALITY_HIGH;
1287             this.mode = this.BOARD_MODE_NONE;
1288         },
1289 
1290         /**********************************************************
1291          *
1292          * Event Handler
1293          *
1294          **********************************************************/
1295 
1296         /**
1297          *  Add all possible event handlers to the board object
1298          */
1299         addEventHandlers: function () {
1300             if (Env.supportsPointerEvents()) {
1301                 this.addPointerEventHandlers();
1302             } else {
1303                 this.addMouseEventHandlers();
1304                 this.addTouchEventHandlers();
1305             }
1306         },
1307 
1308         /**
1309          * Registers the MSPointer* event handlers.
1310          */
1311         addPointerEventHandlers: function () {
1312             if (!this.hasPointerHandlers && Env.isBrowser) {
1313                 if (window.navigator.pointerEnabled) {  // IE11+
1314                     Env.addEvent(this.containerObj, 'pointerdown', this.pointerDownListener, this);
1315                     Env.addEvent(this.containerObj, 'pointermove', this.pointerMoveListener, this);
1316                 } else {
1317                     Env.addEvent(this.containerObj, 'MSPointerDown', this.pointerDownListener, this);
1318                     Env.addEvent(this.containerObj, 'MSPointerMove', this.pointerMoveListener, this);
1319                 }
1320                 this.hasPointerHandlers = true;
1321             }
1322         },
1323 
1324         /**
1325          * Registers mouse move, down and wheel event handlers.
1326          */
1327         addMouseEventHandlers: function () {
1328             if (!this.hasMouseHandlers && Env.isBrowser) {
1329                 Env.addEvent(this.containerObj, 'mousedown', this.mouseDownListener, this);
1330                 Env.addEvent(this.containerObj, 'mousemove', this.mouseMoveListener, this);
1331 
1332                 Env.addEvent(this.containerObj, 'mousewheel', this.mouseWheelListener, this);
1333                 Env.addEvent(this.containerObj, 'DOMMouseScroll', this.mouseWheelListener, this);
1334 
1335                 this.hasMouseHandlers = true;
1336 
1337                 // This one produces errors on IE
1338                 //   Env.addEvent(this.containerObj, 'contextmenu', function (e) { e.preventDefault(); return false;}, this);
1339 
1340                 // This one works on IE, Firefox and Chromium with default configurations. On some Safari
1341                 // or Opera versions the user must explicitly allow the deactivation of the context menu.
1342                 this.containerObj.oncontextmenu = function (e) {
1343                     if (Type.exists(e)) {
1344                         e.preventDefault();
1345                     }
1346 
1347                     return false;
1348                 };
1349             }
1350         },
1351 
1352         /**
1353          * Register touch start and move and gesture start and change event handlers.
1354          * @param {Boolean} appleGestures If set to false the gesturestart and gesturechange event handlers
1355          * will not be registered.
1356          */
1357         addTouchEventHandlers: function (appleGestures) {
1358             if (!this.hasTouchHandlers && Env.isBrowser) {
1359                 Env.addEvent(this.containerObj, 'touchstart', this.touchStartListener, this);
1360                 Env.addEvent(this.containerObj, 'touchmove', this.touchMoveListener, this);
1361 
1362                 if (!Type.exists(appleGestures) || appleGestures) {
1363                     Env.addEvent(this.containerObj, 'gesturestart', this.gestureStartListener, this);
1364                     Env.addEvent(this.containerObj, 'gesturechange', this.gestureChangeListener, this);
1365                     this.hasGestureHandlers = true;
1366                 }
1367 
1368                 this.hasTouchHandlers = true;
1369             }
1370         },
1371 
1372         /**
1373          * Remove MSPointer* Event handlers.
1374          */
1375         removePointerEventHandlers: function () {
1376             if (this.hasPointerHandlers && Env.isBrowser) {
1377                 if (window.navigator.pointerEnabled) {  // IE11+
1378                     Env.removeEvent(this.containerObj, 'pointerdown', this.pointerDownListener, this);
1379                     Env.removeEvent(this.containerObj, 'pointermove', this.pointerMoveListener, this);
1380                 } else {
1381                     Env.removeEvent(this.containerObj, 'MSPointerDown', this.pointerDownListener, this);
1382                     Env.removeEvent(this.containerObj, 'MSPointerMove', this.pointerMoveListener, this);
1383                 }
1384 
1385                 if (this.hasPointerUp) {
1386                     if (window.navigator.pointerEnabled) {  // IE11+
1387                         Env.removeEvent(this.document, 'pointerup', this.pointerUpListener, this);
1388                     } else {
1389                         Env.removeEvent(this.document, 'MSPointerUp', this.pointerUpListener, this);
1390                     }
1391                     this.hasPointerUp = false;
1392                 }
1393 
1394                 this.hasPointerHandlers = false;
1395             }
1396         },
1397 
1398         /**
1399          * De-register mouse event handlers.
1400          */
1401         removeMouseEventHandlers: function () {
1402             if (this.hasMouseHandlers && Env.isBrowser) {
1403                 Env.removeEvent(this.containerObj, 'mousedown', this.mouseDownListener, this);
1404                 Env.removeEvent(this.containerObj, 'mousemove', this.mouseMoveListener, this);
1405 
1406                 if (this.hasMouseUp) {
1407                     Env.removeEvent(this.document, 'mouseup', this.mouseUpListener, this);
1408                     this.hasMouseUp = false;
1409                 }
1410 
1411                 Env.removeEvent(this.containerObj, 'mousewheel', this.mouseWheelListener, this);
1412                 Env.removeEvent(this.containerObj, 'DOMMouseScroll', this.mouseWheelListener, this);
1413 
1414                 this.hasMouseHandlers = false;
1415             }
1416         },
1417 
1418         /**
1419          * Remove all registered touch event handlers.
1420          */
1421         removeTouchEventHandlers: function () {
1422             if (this.hasTouchHandlers && Env.isBrowser) {
1423                 Env.removeEvent(this.containerObj, 'touchstart', this.touchStartListener, this);
1424                 Env.removeEvent(this.containerObj, 'touchmove', this.touchMoveListener, this);
1425 
1426                 if (this.hasTouchEnd) {
1427                     Env.removeEvent(this.document, 'touchend', this.touchEndListener, this);
1428                     this.hasTouchEnd = false;
1429                 }
1430 
1431                 if (this.hasGestureHandlers) {
1432                     Env.removeEvent(this.containerObj, 'gesturestart', this.gestureStartListener, this);
1433                     Env.removeEvent(this.containerObj, 'gesturechange', this.gestureChangeListener, this);
1434                     this.hasGestureHandlers = false;
1435                 }
1436 
1437                 this.hasTouchHandlers = false;
1438             }
1439         },
1440 
1441         /**
1442          * Remove all event handlers from the board object
1443          */
1444         removeEventHandlers: function () {
1445             this.removeMouseEventHandlers();
1446             this.removeTouchEventHandlers();
1447             this.removePointerEventHandlers();
1448         },
1449 
1450         /**
1451          * Handler for click on left arrow in the navigation bar
1452          */
1453         clickLeftArrow: function () {
1454             this.moveOrigin(this.origin.scrCoords[1] + this.canvasWidth * 0.1, this.origin.scrCoords[2]);
1455             return false;
1456         },
1457 
1458         /**
1459          * Handler for click on right arrow in the navigation bar
1460          */
1461         clickRightArrow: function () {
1462             this.moveOrigin(this.origin.scrCoords[1] - this.canvasWidth * 0.1, this.origin.scrCoords[2]);
1463             return false;
1464         },
1465 
1466         /**
1467          * Handler for click on up arrow in the navigation bar
1468          */
1469         clickUpArrow: function () {
1470             this.moveOrigin(this.origin.scrCoords[1], this.origin.scrCoords[2] - this.canvasHeight * 0.1);
1471             return false;
1472         },
1473 
1474         /**
1475          * Handler for click on down arrow in the navigation bar
1476          */
1477         clickDownArrow: function () {
1478             this.moveOrigin(this.origin.scrCoords[1], this.origin.scrCoords[2] + this.canvasHeight * 0.1);
1479             return false;
1480         },
1481 
1482         /**
1483          * Triggered on iOS/Safari while the user inputs a gesture (e.g. pinch) and is used to zoom into the board. Only works on iOS/Safari.
1484          * @param {Event} evt Browser event object
1485          * @return {Boolean}
1486          */
1487         gestureChangeListener: function (evt) {
1488             var c,
1489                 zx = this.attr.zoom.factorx,
1490                 zy = this.attr.zoom.factory;
1491 
1492             if (!this.attr.zoom.wheel) {
1493                 return true;
1494             }
1495 
1496             evt.preventDefault();
1497 
1498             if (this.mode === this.BOARD_MODE_ZOOM) {
1499                 c = new Coords(Const.COORDS_BY_SCREEN, this.getMousePosition(evt), this);
1500 
1501                 this.attr.zoom.factorx = evt.scale / this.prevScale;
1502                 this.attr.zoom.factory = evt.scale / this.prevScale;
1503 
1504                 this.zoomIn(c.usrCoords[1], c.usrCoords[2]);
1505                 this.prevScale = evt.scale;
1506 
1507                 this.attr.zoom.factorx = zx;
1508                 this.attr.zoom.factory = zy;
1509             }
1510 
1511             return false;
1512         },
1513 
1514         /**
1515          * Called by iOS/Safari as soon as the user starts a gesture (only works on iOS/Safari).
1516          * @param {Event} evt
1517          * @return {Boolean}
1518          */
1519         gestureStartListener: function (evt) {
1520 
1521             if (!this.attr.zoom.wheel) {
1522                 return true;
1523             }
1524 
1525             evt.preventDefault();
1526             this.prevScale = 1;
1527 
1528             if (this.mode === this.BOARD_MODE_NONE) {
1529                 this.mode = this.BOARD_MODE_ZOOM;
1530             }
1531 
1532             return false;
1533         },
1534 
1535         /**
1536          * pointer-Events
1537          */
1538 
1539         /**
1540          * This method is called by the browser when a pointing device is pressed on the screen.
1541          * @param {Event} evt The browsers event object.
1542          * @param {Object} object If the object to be dragged is already known, it can be submitted via this parameter
1543          * @returns {Boolean} ...
1544          */
1545         pointerDownListener: function (evt, object) {
1546             var i, j, k, pos, elements,
1547                 eps = this.options.precision.touch,
1548                 found, target, result;
1549 
1550             if (!this.hasPointerUp) {
1551                 if (window.navigator.pointerEnabled) {  // IE11+
1552                     Env.addEvent(this.document, 'pointerup', this.pointerUpListener, this);
1553                 } else {
1554                     Env.addEvent(this.document, 'MSPointerUp', this.pointerUpListener, this);
1555                 }
1556                 this.hasPointerUp = true;
1557             } 
1558             
1559             if (this.hasMouseHandlers) {
1560                 this.removeMouseEventHandlers();
1561             }
1562 
1563             if (this.hasTouchHandlers) {
1564                 this.removeTouchEventHandlers();
1565             }
1566 
1567             // prevent accidental selection of text
1568             if (this.document.selection && typeof this.document.selection.empty === 'function') {
1569                 this.document.selection.empty();
1570             } else if (window.getSelection) {
1571                 window.getSelection().removeAllRanges();
1572             }
1573 
1574             // Touch or pen device
1575             if (JXG.isBrowser && (window.navigator.msMaxTouchPoints && window.navigator.msMaxTouchPoints > 1)) {
1576                 this.options.precision.hasPoint = eps;
1577             }
1578 
1579             // This should be easier than the touch events. Every pointer device gets its own pointerId, e.g. the mouse
1580             // always has id 1, fingers and pens get unique ids every time a pointerDown event is fired and they will
1581             // keep this id until a pointerUp event is fired. What we have to do here is:
1582             //  1. collect all elements under the current pointer
1583             //  2. run through the touches control structure
1584             //    a. look for the object collected in step 1.
1585             //    b. if an object is found, check the number of pointers. if appropriate, add the pointer.
1586 
1587             pos = this.getMousePosition(evt);
1588 
1589             if (object) {
1590                 elements = [ object ];
1591                 this.mode = this.BOARD_MODE_DRAG;
1592             } else {
1593                 elements = this.initMoveObject(pos[0], pos[1], evt, 'mouse');
1594             }
1595 
1596             // if no draggable object can be found, get out here immediately
1597             if (elements.length > 0) {
1598                 // check touches structure
1599                 target = elements[elements.length - 1];
1600                 found = false;
1601 
1602                 for (i = 0; i < this.touches.length; i++) {
1603                     // the target is already in our touches array, try to add the pointer to the existing touch
1604                     if (this.touches[i].obj === target) {
1605                         j = i;
1606                         k = this.touches[i].targets.push({
1607                             num: evt.pointerId,
1608                             X: pos[0],
1609                             Y: pos[1],
1610                             Xprev: NaN,
1611                             Yprev: NaN,
1612                             Xstart: [],
1613                             Ystart: [],
1614                             Zstart: []
1615                         }) - 1;
1616 
1617                         found = true;
1618                         break;
1619                     }
1620                 }
1621 
1622                 if (!found) {
1623                     k = 0;
1624                     j = this.touches.push({
1625                         obj: target,
1626                         targets: [{
1627                             num: evt.pointerId,
1628                             X: pos[0],
1629                             Y: pos[1],
1630                             Xprev: NaN,
1631                             Yprev: NaN,
1632                             Xstart: [],
1633                             Ystart: [],
1634                             Zstart: []
1635                         }]
1636                     }) - 1;
1637                 }
1638 
1639                 this.dehighlightAll();
1640                 target.highlight(true);
1641 
1642                 this.saveStartPos(target, this.touches[j].targets[k]);
1643 
1644                 // prevent accidental text selection
1645                 // this could get us new trouble: input fields, links and drop down boxes placed as text
1646                 // on the board don't work anymore.
1647                 if (evt && evt.preventDefault) {
1648                     evt.preventDefault();
1649                 } else if (window.event) {
1650                     window.event.returnValue = false;
1651                 }
1652             }
1653 
1654             if (this.touches.length > 0) {
1655                 evt.preventDefault();
1656                 evt.stopPropagation();
1657             }
1658 
1659             // move origin - but only if we're not in drag mode
1660             if (this.mode === this.BOARD_MODE_NONE && this.mouseOriginMoveStart(evt)) {
1661                 this.triggerEventHandlers(['touchstart', 'down', 'pointerdown', 'MSPointerDown'], [evt]);
1662                 return false;
1663             }
1664 
1665             this.options.precision.hasPoint = this.options.precision.mouse;
1666             this.triggerEventHandlers(['touchstart', 'down', 'pointerdown', 'MSPointerDown'], [evt]);
1667 
1668             return result;
1669         },
1670 
1671         /**
1672          * Called periodically by the browser while the user moves a pointing device across the screen.
1673          * @param {Event} evt
1674          * @return {Boolean}
1675          */
1676         pointerMoveListener: function (evt) {
1677             var i, j, pos, time,
1678                 evtTouches = evt[JXG.touchProperty];
1679 
1680             if (this.mode !== this.BOARD_MODE_DRAG) {
1681                 this.dehighlightAll();
1682                 this.renderer.hide(this.infobox);
1683             }
1684 
1685             if (this.mode !== this.BOARD_MODE_NONE) {
1686                 evt.preventDefault();
1687                 evt.stopPropagation();
1688             }
1689 
1690             // Touch or pen device
1691             if (JXG.isBrowser && (window.navigator.msMaxTouchPoints && window.navigator.msMaxTouchPoints > 1)) {
1692                 this.options.precision.hasPoint = this.options.precision.touch;
1693             }
1694             this.updateQuality = this.BOARD_QUALITY_LOW;
1695 
1696             // try with mouseOriginMove because the evt objects are quite similar
1697             if (!this.mouseOriginMove(evt)) {
1698                 if (this.mode === this.BOARD_MODE_DRAG) {
1699                     // Runs through all elements which are touched by at least one finger.
1700                     for (i = 0; i < this.touches.length; i++) {
1701                         for (j = 0; j < this.touches[i].targets.length; j++) {
1702                             if (this.touches[i].targets[j].num === evt.pointerId) {
1703                                 // Touch by one finger:  this is possible for all elements that can be dragged
1704                                 if (this.touches[i].targets.length === 1) {
1705                                     this.touches[i].targets[j].X = evt.pageX;
1706                                     this.touches[i].targets[j].Y = evt.pageY;
1707                                     pos = this.getMousePosition(evt);
1708                                     this.moveObject(pos[0], pos[1], this.touches[i], evt, 'touch');
1709                                 // Touch by two fingers: moving lines
1710                                 } else if (this.touches[i].targets.length === 2 &&
1711                                         this.touches[i].targets[0].num > -1 && this.touches[i].targets[1].num > -1) {
1712 
1713                                     this.touches[i].targets[j].X = evt.pageX;
1714                                     this.touches[i].targets[j].Y = evt.pageY;
1715 
1716                                     this.twoFingerMove(
1717                                         this.getMousePosition({
1718                                             pageX: this.touches[i].targets[0].X,
1719                                             pageY: this.touches[i].targets[0].Y
1720                                         }),
1721                                         this.getMousePosition({
1722                                             pageX: this.touches[i].targets[1].X,
1723                                             pageY: this.touches[i].targets[1].Y
1724                                         }),
1725                                         this.touches[i],
1726                                         evt
1727                                     );
1728                                 }
1729 
1730                                 // there is only one pointer in the evt object, there's no point in looking further
1731                                 break;
1732                             }
1733                         }
1734 
1735                     }
1736                 } else {
1737                     pos = this.getMousePosition(evt);
1738                     this.highlightElements(pos[0], pos[1], evt, -1);
1739                 }
1740             }
1741 
1742             if (this.mode !== this.BOARD_MODE_DRAG) {
1743                 this.renderer.hide(this.infobox);
1744             }
1745 
1746             this.options.precision.hasPoint = this.options.precision.mouse;
1747             this.triggerEventHandlers(['touchmove', 'move', 'pointermove', 'MSPointerMove'], [evt, this.mode]);
1748 
1749             return this.mode === this.BOARD_MODE_NONE;
1750         },
1751 
1752         /**
1753          * Triggered as soon as the user stops touching the device with at least one finger.
1754          * @param {Event} evt
1755          * @return {Boolean}
1756          */
1757         pointerUpListener: function (evt) {
1758             var i, j, k, found, foundNumber,
1759                 tmpTouches = [],
1760                 eps = this.options.precision.touch;
1761 
1762             this.triggerEventHandlers(['touchend', 'up', 'pointerup', 'MSPointerUp'], [evt]);
1763             this.renderer.hide(this.infobox);
1764 
1765             if (evt) {
1766                 for (i = 0; i < this.touches.length; i++) {
1767                     for (j = 0; j < this.touches[i].targets.length; j++) {
1768                         if (this.touches[i].targets[j].num === evt.pointerId) {
1769                             this.touches[i].targets.splice(j, 1);
1770 
1771                             if (this.touches[i].targets.length === 0) {
1772                                 this.touches.splice(i, 1);
1773                             }
1774 
1775                             break;
1776                         }
1777                     }
1778                 }
1779             }
1780 
1781             for (i = this.downObjects.length - 1; i > -1; i--) {
1782                 found = false;
1783                 for (j = 0; j < this.touches.length; j++) {
1784                     if (this.touches[j].obj.id === this.downObjects[i].id) {
1785                         found = true;
1786                     }
1787                 }
1788                 if (!found) {
1789                     this.downObjects[i].triggerEventHandlers(['touchend', 'up', 'pointerup', 'MSPointerUp'], [evt]);
1790                     this.downObjects[i].snapToGrid();
1791                     this.downObjects[i].snapToPoints();
1792                     this.downObjects.splice(i, 1);
1793                 }
1794             }
1795 
1796             if (this.touches.length === 0) {
1797                 if (this.hasPointerUp) {
1798                     if (window.navigator.pointerEnabled) {  // IE11+
1799                         Env.removeEvent(this.document, 'pointerup', this.pointerUpListener, this);
1800                     } else {
1801                         Env.removeEvent(this.document, 'MSPointerUp', this.pointerUpListener, this);
1802                     }
1803                     this.hasPointerUp = false;
1804                 }
1805 
1806                 this.dehighlightAll();
1807                 this.updateQuality = this.BOARD_QUALITY_HIGH;
1808 
1809                 this.originMoveEnd();
1810                 this.update();
1811             }
1812 
1813             return true;
1814         },
1815 
1816         /**
1817          * Touch-Events
1818          */
1819 
1820         /**
1821          * This method is called by the browser when a finger touches the surface of the touch-device.
1822          * @param {Event} evt The browsers event object.
1823          * @returns {Boolean} ...
1824          */
1825         touchStartListener: function (evt) {
1826             var i, pos, elements, j, k, time,
1827                 eps = this.options.precision.touch,
1828                 obj, found, targets,
1829                 evtTouches = evt[JXG.touchProperty],
1830                 target;
1831 
1832             if (!this.hasTouchEnd) {
1833                 Env.addEvent(this.document, 'touchend', this.touchEndListener, this);
1834                 this.hasTouchEnd = true;
1835             }
1836 
1837             if (this.hasMouseHandlers) {
1838                 this.removeMouseEventHandlers();
1839             }
1840 
1841             // prevent accidental selection of text
1842             if (this.document.selection && typeof this.document.selection.empty === 'function') {
1843                 this.document.selection.empty();
1844             } else if (window.getSelection) {
1845                 window.getSelection().removeAllRanges();
1846             }
1847 
1848             // multitouch
1849             this.options.precision.hasPoint = this.options.precision.touch;
1850 
1851             // this is the most critical part. first we should run through the existing touches and collect all targettouches that don't belong to our
1852             // previous touches. once this is done we run through the existing touches again and watch out for free touches that can be attached to our existing
1853             // touches, e.g. we translate (parallel translation) a line with one finger, now a second finger is over this line. this should change the operation to
1854             // a rotational translation. or one finger moves a circle, a second finger can be attached to the circle: this now changes the operation from translation to
1855             // stretching. as a last step we're going through the rest of the targettouches and initiate new move operations:
1856             //  * points have higher priority over other elements.
1857             //  * if we find a targettouch over an element that could be transformed with more than one finger, we search the rest of the targettouches, if they are over
1858             //    this element and add them.
1859             // ADDENDUM 11/10/11:
1860             //  (1) run through the touches control object,
1861             //  (2) try to find the targetTouches for every touch. on touchstart only new touches are added, hence we can find a targettouch
1862             //      for every target in our touches objects
1863             //  (3) if one of the targettouches was bound to a touches targets array, mark it
1864             //  (4) run through the targettouches. if the targettouch is marked, continue. otherwise check for elements below the targettouch:
1865             //      (a) if no element could be found: mark the target touches and continue
1866             //      --- in the following cases, "init" means:
1867             //           (i) check if the element is already used in another touches element, if so, mark the targettouch and continue
1868             //          (ii) if not, init a new touches element, add the targettouch to the touches property and mark it
1869             //      (b) if the element is a point, init
1870             //      (c) if the element is a line, init and try to find a second targettouch on that line. if a second one is found, add and mark it
1871             //      (d) if the element is a circle, init and try to find TWO other targettouches on that circle. if only one is found, mark it and continue. otherwise
1872             //          add both to the touches array and mark them.
1873             for (i = 0; i < evtTouches.length; i++) {
1874                 evtTouches[i].jxg_isused = false;
1875             }
1876 
1877             for (i = 0; i < this.touches.length; i++) {
1878                 for (j = 0; j < this.touches[i].targets.length; j++) {
1879                     this.touches[i].targets[j].num = -1;
1880                     eps = this.options.precision.touch;
1881 
1882                     do {
1883                         for (k = 0; k < evtTouches.length; k++) {
1884                             // find the new targettouches
1885                             if (Math.abs(Math.pow(evtTouches[k].screenX - this.touches[i].targets[j].X, 2) +
1886                                     Math.pow(evtTouches[k].screenY - this.touches[i].targets[j].Y, 2)) < eps * eps) {
1887                                 this.touches[i].targets[j].num = k;
1888 
1889                                 this.touches[i].targets[j].X = evtTouches[k].screenX;
1890                                 this.touches[i].targets[j].Y = evtTouches[k].screenY;
1891                                 evtTouches[k].jxg_isused = true;
1892                                 break;
1893                             }
1894                         }
1895 
1896                         eps *= 2;
1897 
1898                     } while (this.touches[i].targets[j].num === -1 && eps < this.options.precision.touchMax);
1899 
1900                     if (this.touches[i].targets[j].num === -1) {
1901                         JXG.debug('i couldn\'t find a targettouches for target no ' + j + ' on ' + this.touches[i].obj.name + ' (' + this.touches[i].obj.id + '). Removed the target.');
1902                         JXG.debug('eps = ' + eps + ', touchMax = ' + Options.precision.touchMax);
1903                         this.touches[i].targets.splice(i, 1);
1904                     }
1905 
1906                 }
1907             }
1908 
1909             // we just re-mapped the targettouches to our existing touches list. now we have to initialize some touches from additional targettouches
1910             for (i = 0; i < evtTouches.length; i++) {
1911                 if (!evtTouches[i].jxg_isused) {
1912                     pos = this.getMousePosition(evt, i);
1913                     elements = this.initMoveObject(pos[0], pos[1], evt, 'touch');
1914 
1915                     if (elements.length !== 0) {
1916                         obj = elements[elements.length - 1];
1917 
1918                         if (Type.isPoint(obj) || 
1919                             obj.type === Const.OBJECT_TYPE_TEXT || 
1920                             obj.type === Const.OBJECT_TYPE_TICKS ||
1921                             obj.type === Const.OBJECT_TYPE_IMAGE) {
1922                             // it's a point, so it's single touch, so we just push it to our touches
1923                             targets = [{ num: i, X: evtTouches[i].screenX, Y: evtTouches[i].screenY, Xprev: NaN, Yprev: NaN, Xstart: [], Ystart: [], Zstart: [] }];
1924 
1925                             // For the UNDO/REDO of object moves
1926                             this.saveStartPos(obj, targets[0]);
1927 
1928                             this.touches.push({ obj: obj, targets: targets });
1929                             obj.highlight(true);
1930 
1931                         } else if (obj.elementClass === Const.OBJECT_CLASS_LINE ||
1932                                 obj.elementClass === Const.OBJECT_CLASS_CIRCLE ||
1933                                 obj.type === Const.OBJECT_TYPE_POLYGON) {
1934                             found = false;
1935 
1936                             // first check if this geometric object is already capture in this.touches
1937                             for (j = 0; j < this.touches.length; j++) {
1938                                 if (obj.id === this.touches[j].obj.id) {
1939                                     found = true;
1940                                     // only add it, if we don't have two targets in there already
1941                                     if (this.touches[j].targets.length === 1) {
1942                                         target = { num: i, X: evtTouches[i].screenX, Y: evtTouches[i].screenY, Xprev: NaN, Yprev: NaN, Xstart: [], Ystart: [], Zstart: [] };
1943 
1944                                         // For the UNDO/REDO of object moves
1945                                         this.saveStartPos(obj, target);
1946                                         this.touches[j].targets.push(target);
1947                                     }
1948 
1949                                     evtTouches[i].jxg_isused = true;
1950                                 }
1951                             }
1952 
1953                             // we couldn't find it in touches, so we just init a new touches
1954                             // IF there is a second touch targetting this line, we will find it later on, and then add it to
1955                             // the touches control object.
1956                             if (!found) {
1957                                 targets = [{ num: i, X: evtTouches[i].screenX, Y: evtTouches[i].screenY, Xprev: NaN, Yprev: NaN, Xstart: [], Ystart: [], Zstart: [] }];
1958 
1959                                 // For the UNDO/REDO of object moves
1960                                 this.saveStartPos(obj, targets[0]);
1961                                 this.touches.push({ obj: obj, targets: targets });
1962                                 obj.highlight(true);
1963                             }
1964                         }
1965                     }
1966 
1967                     evtTouches[i].jxg_isused = true;
1968                 }
1969             }
1970 
1971             if (this.touches.length > 0) {
1972                 evt.preventDefault();
1973                 evt.stopPropagation();
1974             }
1975 
1976             // move origin - but only if we're not in drag mode
1977             if (this.mode === this.BOARD_MODE_NONE && this.touchOriginMoveStart(evt)) {
1978                 this.triggerEventHandlers(['touchstart', 'down'], [evt]);
1979                 return false;
1980             }
1981 
1982             if (Env.isWebkitAndroid()) {
1983                 time = new Date();
1984                 this.touchMoveLast = time.getTime() - 200;
1985             }
1986 
1987             this.options.precision.hasPoint = this.options.precision.mouse;
1988 
1989             this.triggerEventHandlers(['touchstart', 'down'], [evt]);
1990 
1991             return this.touches.length > 0;
1992         },
1993 
1994         /**
1995          * Called periodically by the browser while the user moves his fingers across the device.
1996          * @param {Event} evt
1997          * @return {Boolean}
1998          */
1999         touchMoveListener: function (evt) {
2000             var i, pos, time,
2001                 evtTouches = evt[JXG.touchProperty];
2002 
2003             if (this.mode !== this.BOARD_MODE_NONE) {
2004                 evt.preventDefault();
2005                 evt.stopPropagation();
2006             }
2007 
2008             // Reduce update frequency for Android devices
2009             if (Env.isWebkitAndroid()) {
2010                 time = new Date();
2011                 time = time.getTime();
2012 
2013                 if (time - this.touchMoveLast < 80) {
2014                     this.updateQuality = this.BOARD_QUALITY_HIGH;
2015                     this.triggerEventHandlers(['touchmove', 'move'], [evt, this.mode]);
2016 
2017                     return false;
2018                 }
2019 
2020                 this.touchMoveLast = time;
2021             }
2022 
2023             if (this.mode !== this.BOARD_MODE_DRAG) {
2024                 this.renderer.hide(this.infobox);
2025             }
2026 
2027             this.options.precision.hasPoint = this.options.precision.touch;
2028             this.updateQuality = this.BOARD_QUALITY_LOW;
2029 
2030             if (!this.touchOriginMove(evt)) {
2031                 if (this.mode === this.BOARD_MODE_DRAG) {
2032                     // Runs over through all elements which are touched
2033                     // by at least one finger.
2034                     for (i = 0; i < this.touches.length; i++) {
2035                         // Touch by one finger:  this is possible for all elements that can be dragged
2036                         if (this.touches[i].targets.length === 1) {
2037                             if (evtTouches[this.touches[i].targets[0].num]) {
2038                                 this.touches[i].targets[0].X = evtTouches[this.touches[i].targets[0].num].screenX;
2039                                 this.touches[i].targets[0].Y = evtTouches[this.touches[i].targets[0].num].screenY;
2040                                 pos = this.getMousePosition(evt, this.touches[i].targets[0].num);
2041                                 this.moveObject(pos[0], pos[1], this.touches[i], evt, 'touch');
2042                             }
2043                             // Touch by two fingers: moving lines
2044                         } else if (this.touches[i].targets.length === 2 && this.touches[i].targets[0].num > -1 && this.touches[i].targets[1].num > -1) {
2045                             if (evtTouches[this.touches[i].targets[0].num] && evtTouches[this.touches[i].targets[1].num]) {
2046                                 this.touches[i].targets[0].X = evtTouches[this.touches[i].targets[0].num].screenX;
2047                                 this.touches[i].targets[0].Y = evtTouches[this.touches[i].targets[0].num].screenY;
2048                                 this.touches[i].targets[1].X = evtTouches[this.touches[i].targets[1].num].screenX;
2049                                 this.touches[i].targets[1].Y = evtTouches[this.touches[i].targets[1].num].screenY;
2050                                 this.twoFingerMove(
2051                                     this.getMousePosition(evt, this.touches[i].targets[0].num),
2052                                     this.getMousePosition(evt, this.touches[i].targets[1].num),
2053                                     this.touches[i],
2054                                     evt
2055                                 );
2056 
2057                             }
2058                         }
2059                     }
2060                 }
2061             }
2062 
2063             if (this.mode !== this.BOARD_MODE_DRAG) {
2064                 this.renderer.hide(this.infobox);
2065             }
2066 
2067             /*
2068               this.updateQuality = this.BOARD_QUALITY_HIGH; is set in touchEnd
2069             */
2070             this.options.precision.hasPoint = this.options.precision.mouse;
2071             this.triggerEventHandlers(['touchmove', 'move'], [evt, this.mode]);
2072 
2073             return this.mode === this.BOARD_MODE_NONE;
2074         },
2075 
2076         /**
2077          * Triggered as soon as the user stops touching the device with at least one finger.
2078          * @param {Event} evt
2079          * @return {Boolean}
2080          */
2081         touchEndListener: function (evt) {
2082             var i, j, k,
2083                 eps = this.options.precision.touch,
2084                 tmpTouches = [], found, foundNumber,
2085                 evtTouches = evt && evt[JXG.touchProperty];
2086 
2087             this.triggerEventHandlers(['touchend', 'up'], [evt]);
2088             this.renderer.hide(this.infobox);
2089 
2090             if (evtTouches && evtTouches.length > 0) {
2091                 for (i = 0; i < this.touches.length; i++) {
2092                     tmpTouches[i] = this.touches[i];
2093                 }
2094                 this.touches.length = 0;
2095 
2096                 // try to convert the operation, e.g. if a lines is rotated and translated with two fingers and one finger is lifted,
2097                 // convert the operation to a simple one-finger-translation.
2098                 // ADDENDUM 11/10/11:
2099                 // see addendum to touchStartListener from 11/10/11
2100                 // (1) run through the tmptouches
2101                 // (2) check the touches.obj, if it is a
2102                 //     (a) point, try to find the targettouch, if found keep it and mark the targettouch, else drop the touch.
2103                 //     (b) line with
2104                 //          (i) one target: try to find it, if found keep it mark the targettouch, else drop the touch.
2105                 //         (ii) two targets: if none can be found, drop the touch. if one can be found, remove the other target. mark all found targettouches
2106                 //     (c) circle with [proceed like in line]
2107 
2108                 // init the targettouches marker
2109                 for (i = 0; i < evtTouches.length; i++) {
2110                     evtTouches[i].jxg_isused = false;
2111                 }
2112 
2113                 for (i = 0; i < tmpTouches.length; i++) {
2114                     // could all targets of the current this.touches.obj be assigned to targettouches?
2115                     found = false;
2116                     foundNumber = 0;
2117 
2118                     for (j = 0; j < tmpTouches[i].targets.length; j++) {
2119                         tmpTouches[i].targets[j].found = false;
2120                         for (k = 0; k < evtTouches.length; k++) {
2121                             if (Math.abs(Math.pow(evtTouches[k].screenX - tmpTouches[i].targets[j].X, 2) + Math.pow(evtTouches[k].screenY - tmpTouches[i].targets[j].Y, 2)) < eps * eps) {
2122                                 tmpTouches[i].targets[j].found = true;
2123                                 tmpTouches[i].targets[j].num = k;
2124                                 tmpTouches[i].targets[j].X = evtTouches[k].screenX;
2125                                 tmpTouches[i].targets[j].Y = evtTouches[k].screenY;
2126                                 foundNumber += 1;
2127                                 break;
2128                             }
2129                         }
2130                     }
2131 
2132                     if (Type.isPoint(tmpTouches[i].obj)) {
2133                         found = (tmpTouches[i].targets[0] && tmpTouches[i].targets[0].found);
2134                     } else if (tmpTouches[i].obj.elementClass === Const.OBJECT_CLASS_LINE) {
2135                         found = (tmpTouches[i].targets[0] && tmpTouches[i].targets[0].found) || (tmpTouches[i].targets[1] && tmpTouches[i].targets[1].found);
2136                     } else if (tmpTouches[i].obj.elementClass === Const.OBJECT_CLASS_CIRCLE) {
2137                         found = foundNumber === 1 || foundNumber === 3;
2138                     }
2139 
2140                     // if we found this object to be still dragged by the user, add it back to this.touches
2141                     if (found) {
2142                         this.touches.push({
2143                             obj: tmpTouches[i].obj,
2144                             targets: []
2145                         });
2146 
2147                         for (j = 0; j < tmpTouches[i].targets.length; j++) {
2148                             if (tmpTouches[i].targets[j].found) {
2149                                 this.touches[this.touches.length - 1].targets.push({
2150                                     num: tmpTouches[i].targets[j].num,
2151                                     X: tmpTouches[i].targets[j].screenX,
2152                                     Y: tmpTouches[i].targets[j].screenY,
2153                                     Xprev: NaN,
2154                                     Yprev: NaN,
2155                                     Xstart: tmpTouches[i].targets[j].Xstart,
2156                                     Ystart: tmpTouches[i].targets[j].Ystart,
2157                                     Zstart: tmpTouches[i].targets[j].Zstart
2158                                 });
2159                             }
2160                         }
2161 
2162                     } else {
2163                         tmpTouches[i].obj.noHighlight();
2164                     }
2165                 }
2166 
2167             } else {
2168                 this.touches.length = 0;
2169             }
2170 
2171             for (i = this.downObjects.length - 1; i > -1; i--) {
2172                 found = false;
2173                 for (j = 0; j < this.touches.length; j++) {
2174                     if (this.touches[j].obj.id === this.downObjects[i].id) {
2175                         found = true;
2176                     }
2177                 }
2178                 if (!found) {
2179                     this.downObjects[i].triggerEventHandlers(['touchup', 'up'], [evt]);
2180                     this.downObjects[i].snapToGrid();
2181                     this.downObjects[i].snapToPoints();
2182                     this.downObjects.splice(i, 1);
2183                 }
2184             }
2185 
2186             if (!evtTouches || evtTouches.length === 0) {
2187 
2188                 if (this.hasTouchEnd) {
2189                     Env.removeEvent(this.document, 'touchend', this.touchEndListener, this);
2190                     this.hasTouchEnd = false;
2191                 }
2192 
2193                 this.dehighlightAll();
2194                 this.updateQuality = this.BOARD_QUALITY_HIGH;
2195 
2196                 this.originMoveEnd();
2197                 this.update();
2198             }
2199 
2200             return true;
2201         },
2202 
2203         /**
2204          * This method is called by the browser when the mouse button is clicked.
2205          * @param {Event} evt The browsers event object.
2206          * @returns {Boolean} True if no element is found under the current mouse pointer, false otherwise.
2207          */
2208         mouseDownListener: function (evt) {
2209             var pos, elements, result;
2210 
2211             // prevent accidental selection of text
2212             if (this.document.selection && typeof this.document.selection.empty === 'function') {
2213                 this.document.selection.empty();
2214             } else if (window.getSelection) {
2215                 window.getSelection().removeAllRanges();
2216             }
2217 
2218             if (!this.hasMouseUp) {
2219                 Env.addEvent(this.document, 'mouseup', this.mouseUpListener, this);
2220                 this.hasMouseUp = true;
2221             } else {
2222                 // In case this.hasMouseUp==true, it may be that there was a 
2223                 // mousedown event before which was not followed by an mouseup event.
2224                 // This seems to happen with interactive whiteboard pens sometimes.
2225                 return;
2226             }
2227 
2228             pos = this.getMousePosition(evt);
2229             elements = this.initMoveObject(pos[0], pos[1], evt, 'mouse');
2230 
2231             // if no draggable object can be found, get out here immediately
2232             if (elements.length === 0) {
2233                 this.mode = this.BOARD_MODE_NONE;
2234                 result = true;
2235             } else {
2236                 this.mouse = {
2237                     obj: null,
2238                     targets: [{
2239                         X: pos[0],
2240                         Y: pos[1],
2241                         Xprev: NaN,
2242                         Yprev: NaN
2243                     }]
2244                 };
2245                 this.mouse.obj = elements[elements.length - 1];
2246 
2247                 this.dehighlightAll();
2248                 this.mouse.obj.highlight(true);
2249 
2250                 this.mouse.targets[0].Xstart = [];
2251                 this.mouse.targets[0].Ystart = [];
2252                 this.mouse.targets[0].Zstart = [];
2253 
2254                 this.saveStartPos(this.mouse.obj, this.mouse.targets[0]);
2255 
2256                 // prevent accidental text selection
2257                 // this could get us new trouble: input fields, links and drop down boxes placed as text
2258                 // on the board don't work anymore.
2259                 if (evt && evt.preventDefault) {
2260                     evt.preventDefault();
2261                 } else if (window.event) {
2262                     window.event.returnValue = false;
2263                 }
2264             }
2265 
2266             if (this.mode === this.BOARD_MODE_NONE) {
2267                 result = this.mouseOriginMoveStart(evt);
2268             }
2269 
2270             this.triggerEventHandlers(['mousedown', 'down'], [evt]);
2271 
2272             return result;
2273         },
2274 
2275         /**
2276          * This method is called by the browser when the mouse button is released.
2277          * @param {Event} evt
2278          */
2279         mouseUpListener: function (evt) {
2280             var i;
2281 
2282             this.triggerEventHandlers(['mouseup', 'up'], [evt]);
2283 
2284             // redraw with high precision
2285             this.updateQuality = this.BOARD_QUALITY_HIGH;
2286 
2287             if (this.mouse && this.mouse.obj) {
2288                 // The parameter is needed for lines with snapToGrid enabled
2289                 this.mouse.obj.snapToGrid(this.mouse.targets[0]);
2290                 this.mouse.obj.snapToPoints();
2291             }
2292 
2293             this.originMoveEnd();
2294             this.dehighlightAll();
2295             this.update();
2296 
2297             for (i = 0; i < this.downObjects.length; i++) {
2298                 this.downObjects[i].triggerEventHandlers(['mouseup', 'up'], [evt]);
2299             }
2300 
2301             this.downObjects.length = 0;
2302 
2303             if (this.hasMouseUp) {
2304                 Env.removeEvent(this.document, 'mouseup', this.mouseUpListener, this);
2305                 this.hasMouseUp = false;
2306             }
2307 
2308             // release dragged mouse object
2309             this.mouse = null;
2310         },
2311 
2312         /**
2313          * This method is called by the browser when the mouse is moved.
2314          * @param {Event} evt The browsers event object.
2315          */
2316         mouseMoveListener: function (evt) {
2317             var pos;
2318 
2319             pos = this.getMousePosition(evt);
2320 
2321             this.updateQuality = this.BOARD_QUALITY_LOW;
2322 
2323             if (this.mode !== this.BOARD_MODE_DRAG) {
2324                 this.dehighlightAll();
2325                 this.renderer.hide(this.infobox);
2326             }
2327 
2328             // we have to check for three cases:
2329             //   * user moves origin
2330             //   * user drags an object
2331             //   * user just moves the mouse, here highlight all elements at
2332             //     the current mouse position
2333 
2334             if (!this.mouseOriginMove(evt)) {
2335                 if (this.mode === this.BOARD_MODE_DRAG) {
2336                     this.moveObject(pos[0], pos[1], this.mouse, evt, 'mouse');
2337                 } else { // BOARD_MODE_NONE
2338                     this.highlightElements(pos[0], pos[1], evt, -1);
2339                 }
2340             }
2341 
2342             this.updateQuality = this.BOARD_QUALITY_HIGH;
2343 
2344             this.triggerEventHandlers(['mousemove', 'move'], [evt, this.mode]);
2345         },
2346 
2347         /**
2348          * Handler for mouse wheel events. Used to zoom in and out of the board.
2349          * @param {Event} evt
2350          * @returns {Boolean}
2351          */
2352         mouseWheelListener: function (evt) {
2353             if (!this.attr.zoom.wheel || (this.attr.zoom.needshift && !evt.shiftKey)) {
2354                 return true;
2355             }
2356 
2357             evt = evt || window.event;
2358             var wd = evt.detail ? -evt.detail : evt.wheelDelta / 40,
2359                 pos = new Coords(Const.COORDS_BY_SCREEN, this.getMousePosition(evt), this);
2360 
2361             if (wd > 0) {
2362                 this.zoomIn(pos.usrCoords[1], pos.usrCoords[2]);
2363             } else {
2364                 this.zoomOut(pos.usrCoords[1], pos.usrCoords[2]);
2365             }
2366 
2367             evt.preventDefault();
2368             return false;
2369         },
2370 
2371         /**********************************************************
2372          *
2373          * End of Event Handlers
2374          *
2375          **********************************************************/
2376 
2377         /**
2378          * Updates and displays a little info box to show coordinates of current selected points.
2379          * @param {JXG.GeometryElement} el A GeometryElement
2380          * @returns {JXG.Board} Reference to the board
2381          */
2382         updateInfobox: function (el) {
2383             var x, y, xc, yc;
2384 
2385             if (!el.visProp.showinfobox) {
2386                 return this;
2387             }
2388             if (el.elementClass === Const.OBJECT_CLASS_POINT) {
2389                 xc = el.coords.usrCoords[1];
2390                 yc = el.coords.usrCoords[2];
2391 
2392                 this.infobox.setCoords(xc + this.infobox.distanceX / this.unitX, yc + this.infobox.distanceY / this.unitY);
2393 
2394                 if (typeof el.infoboxText !== 'string') {
2395                     if (el.visProp.infoboxdigits === 'auto') {
2396                         x = Type.autoDigits(xc);
2397                         y = Type.autoDigits(yc);
2398                     } else if (Type.isNumber(el.visProp.infoboxdigits)) {
2399                         x = xc.toFixed(el.visProp.infoboxdigits);
2400                         y = yc.toFixed(el.visProp.infoboxdigits);
2401                     } else {
2402                         x = xc;
2403                         y = yc;
2404                     }
2405 
2406                     this.highlightInfobox(x, y, el);
2407                 } else {
2408                     this.highlightCustomInfobox(el.infoboxText, el);
2409                 }
2410 
2411                 this.renderer.show(this.infobox);
2412             }
2413             return this;
2414         },
2415 
2416         /**
2417          * Changes the text of the info box to what is provided via text.
2418          * @param {String} text
2419          * @param {JXG.GeometryElement} [el]
2420          * @returns {JXG.Board} Reference to the board.
2421          */
2422         highlightCustomInfobox: function (text, el) {
2423             this.infobox.setText(text);
2424             return this;
2425         },
2426 
2427         /**
2428          * Changes the text of the info box to show the given coordinates.
2429          * @param {Number} x
2430          * @param {Number} y
2431          * @param {JXG.GeometryElement} [el] The element the mouse is pointing at
2432          * @returns {JXG.Board} Reference to the board.
2433          */
2434         highlightInfobox: function (x, y, el) {
2435             this.highlightCustomInfobox('(' + x + ', ' + y + ')', el);
2436             return this;
2437         },
2438 
2439         /**
2440          * Remove highlighting of all elements.
2441          * @returns {JXG.Board} Reference to the board.
2442          */
2443         dehighlightAll: function () {
2444             var el, pEl, needsDehighlight = false;
2445 
2446             for (el in this.highlightedObjects) {
2447                 if (this.highlightedObjects.hasOwnProperty(el)) {
2448                     pEl = this.highlightedObjects[el];
2449 
2450                     if (this.hasMouseHandlers || this.hasPointerHandlers) {
2451                         pEl.noHighlight();
2452                     }
2453 
2454                     needsDehighlight = true;
2455 
2456                     // In highlightedObjects should only be objects which fulfill all these conditions
2457                     // And in case of complex elements, like a turtle based fractal, it should be faster to
2458                     // just de-highlight the element instead of checking hasPoint...
2459                     // if ((!Type.exists(pEl.hasPoint)) || !pEl.hasPoint(x, y) || !pEl.visProp.visible)
2460                 }
2461             }
2462 
2463             this.highlightedObjects = {};
2464 
2465             // We do not need to redraw during dehighlighting in CanvasRenderer
2466             // because we are redrawing anyhow
2467             //  -- We do need to redraw during dehighlighting. Otherwise objects won't be dehighlighted until
2468             // another object is highlighted.
2469             if (this.renderer.type === 'canvas' && needsDehighlight) {
2470                 this.prepareUpdate();
2471                 this.renderer.suspendRedraw(this);
2472                 this.updateRenderer();
2473                 this.renderer.unsuspendRedraw();
2474             }
2475 
2476             return this;
2477         },
2478 
2479         /**
2480          * Returns the input parameters in an array. This method looks pointless and it really is, but it had a purpose
2481          * once.
2482          * @param {Number} x X coordinate in screen coordinates
2483          * @param {Number} y Y coordinate in screen coordinates
2484          * @returns {Array} Coordinates of the mouse in screen coordinates.
2485          */
2486         getScrCoordsOfMouse: function (x, y) {
2487             return [x, y];
2488         },
2489 
2490         /**
2491          * This method calculates the user coords of the current mouse coordinates.
2492          * @param {Event} evt Event object containing the mouse coordinates.
2493          * @returns {Array} Coordinates of the mouse in screen coordinates.
2494          */
2495         getUsrCoordsOfMouse: function (evt) {
2496             var cPos = this.getCoordsTopLeftCorner(),
2497                 absPos = Env.getPosition(evt, null, this.document),
2498                 x = absPos[0] - cPos[0],
2499                 y = absPos[1] - cPos[1],
2500                 newCoords = new Coords(Const.COORDS_BY_SCREEN, [x, y], this);
2501 
2502             return newCoords.usrCoords.slice(1);
2503         },
2504 
2505         /**
2506          * Collects all elements under current mouse position plus current user coordinates of mouse cursor.
2507          * @param {Event} evt Event object containing the mouse coordinates.
2508          * @returns {Array} Array of elements at the current mouse position plus current user coordinates of mouse.
2509          */
2510         getAllUnderMouse: function (evt) {
2511             var elList = this.getAllObjectsUnderMouse(evt);
2512             elList.push(this.getUsrCoordsOfMouse(evt));
2513 
2514             return elList;
2515         },
2516 
2517         /**
2518          * Collects all elements under current mouse position.
2519          * @param {Event} evt Event object containing the mouse coordinates.
2520          * @returns {Array} Array of elements at the current mouse position.
2521          */
2522         getAllObjectsUnderMouse: function (evt) {
2523             var cPos = this.getCoordsTopLeftCorner(),
2524                 absPos = Env.getPosition(evt, null, this.document),
2525                 dx = absPos[0] - cPos[0],
2526                 dy = absPos[1] - cPos[1],
2527                 elList = [],
2528                 el,
2529                 pEl,
2530                 len = this.objectsList.length;
2531 
2532             for (el = 0; el < len; el++) {
2533                 pEl = this.objectsList[el];
2534                 if (pEl.visProp.visible && pEl.hasPoint && pEl.hasPoint(dx, dy)) {
2535                     elList[elList.length] = pEl;
2536                 }
2537             }
2538 
2539             return elList;
2540         },
2541 
2542         /**
2543          * Update the coords object of all elements which possess this
2544          * property. This is necessary after changing the viewport.
2545          * @returns {JXG.Board} Reference to this board.
2546          **/
2547         updateCoords: function () {
2548             var el, ob, len = this.objectsList.length;
2549 
2550             for (ob = 0; ob < len; ob++) {
2551                 el = this.objectsList[ob];
2552 
2553                 if (Type.exists(el.coords)) {
2554                     if (el.visProp.frozen) {
2555                         el.coords.screen2usr();
2556                     } else {
2557                         el.coords.usr2screen();
2558                     }
2559                 }
2560             }
2561             return this;
2562         },
2563 
2564         /**
2565          * Moves the origin and initializes an update of all elements.
2566          * @param {Number} x
2567          * @param {Number} y
2568          * @param {Boolean} [diff=false]
2569          * @returns {JXG.Board} Reference to this board.
2570          */
2571         moveOrigin: function (x, y, diff) {
2572             if (Type.exists(x) && Type.exists(y)) {
2573                 this.origin.scrCoords[1] = x;
2574                 this.origin.scrCoords[2] = y;
2575 
2576                 if (diff) {
2577                     this.origin.scrCoords[1] -= this.drag_dx;
2578                     this.origin.scrCoords[2] -= this.drag_dy;
2579                 }
2580             }
2581 
2582             this.updateCoords().clearTraces().fullUpdate();
2583 
2584             this.triggerEventHandlers(['boundingbox']);
2585 
2586             return this;
2587         },
2588 
2589         /**
2590          * Add conditional updates to the elements.
2591          * @param {String} str String containing coniditional update in geonext syntax
2592          */
2593         addConditions: function (str) {
2594             var term, m, left, right, name, el, property,
2595                 functions = [],
2596                 plaintext = 'var el, x, y, c, rgbo;\n',
2597                 i = str.indexOf('<data>'),
2598                 j = str.indexOf('<' + '/data>'),
2599 
2600                 xyFun = function (board, el, f, what) {
2601                     return function () {
2602                         var e, t;
2603 
2604                         e = board.select(el.id);
2605                         t = e.coords.usrCoords[what];
2606 
2607                         if (what === 2) {
2608                             e.setPositionDirectly(JXG.COORDS_BY_USER, [f(), t]);
2609                         } else {
2610                             e.setPositionDirectly(JXG.COORDS_BY_USER, [t, f()]);
2611                         }
2612                         e.prepareUpdate().update();
2613                     };
2614                 },
2615 
2616                 visFun = function (board, el, f) {
2617                     return function () {
2618                         var e, v;
2619 
2620                         e = board.select(el.id);
2621                         v = f();
2622 
2623                         e.setAttribute({visible: v});
2624                     };
2625                 },
2626 
2627                 colFun = function (board, el, f, what) {
2628                     return function () {
2629                         var e, v;
2630 
2631                         e = board.select(el.id);
2632                         v = f();
2633 
2634                         if (what === 'strokewidth') {
2635                             e.visProp.strokewidth = v;
2636                         } else {
2637                             v = Color.rgba2rgbo(v);
2638                             e.visProp[what + 'color'] = v[0];
2639                             e.visProp[what + 'opacity'] = v[1];
2640                         }
2641                     };
2642                 },
2643 
2644                 posFun = function (board, el, f) {
2645                     return function () {
2646                         var e = board.select(el.id);
2647 
2648                         e.position = f();
2649                     };
2650                 },
2651 
2652                 styleFun = function (board, el, f) {
2653                     return function () {
2654                         var e = board.select(el.id);
2655 
2656                         e.setStyle(f());
2657                     };
2658                 };
2659 
2660             if (i < 0) {
2661                 return;
2662             }
2663 
2664             while (i >= 0) {
2665                 term = str.slice(i + 6, j);   // throw away <data>
2666                 m = term.indexOf('=');
2667                 left = term.slice(0, m);
2668                 right = term.slice(m + 1);
2669                 m = left.indexOf('.');     // Dies erzeugt Probleme bei Variablennamen der Form " Steuern akt."
2670                 name = left.slice(0, m);    //.replace(/\s+$/,''); // do NOT cut out name (with whitespace)
2671                 el = this.elementsByName[Type.unescapeHTML(name)];
2672 
2673                 property = left.slice(m + 1).replace(/\s+/g, '').toLowerCase(); // remove whitespace in property
2674                 right = Type.createFunction(right, this, '', true);
2675 
2676                 // Debug
2677                 if (!Type.exists(this.elementsByName[name])) {
2678                     JXG.debug("debug conditions: |" + name + "| undefined");
2679                 } else {
2680                     plaintext += "el = this.objects[\"" + el.id + "\"];\n";
2681 
2682                     switch (property) {
2683                     case 'x':
2684                         functions.push(xyFun(this, el, right, 2));
2685                         break;
2686                     case 'y':
2687                         functions.push(xyFun(this, el, right, 1));
2688                         break;
2689                     case 'visible':
2690                         functions.push(visFun(this, el, right));
2691                         break;
2692                     case 'position':
2693                         functions.push(posFun(this, el, right));
2694                         break;
2695                     case 'stroke':
2696                         functions.push(colFun(this, el, right, 'stroke'));
2697                         break;
2698                     case 'style':
2699                         functions.push(styleFun(this, el, right));
2700                         break;
2701                     case 'strokewidth':
2702                         functions.push(colFun(this, el, right, 'strokewidth'));
2703                         break;
2704                     case 'fill':
2705                         functions.push(colFun(this, el, right, 'fill'));
2706                         break;
2707                     case 'label':
2708                         break;
2709                     default:
2710                         JXG.debug("property '" + property + "' in conditions not yet implemented:" + right);
2711                         break;
2712                     }
2713                 }
2714                 str = str.slice(j + 7); // cut off "</data>"
2715                 i = str.indexOf('<data>');
2716                 j = str.indexOf('<' + '/data>');
2717             }
2718 
2719             this.updateConditions = function () {
2720                 var i;
2721 
2722                 for (i = 0; i < functions.length; i++) {
2723                     functions[i]();
2724                 }
2725 
2726                 this.prepareUpdate().updateElements();
2727                 return true;
2728             };
2729             this.updateConditions();
2730         },
2731 
2732         /**
2733          * Computes the commands in the conditions-section of the gxt file.
2734          * It is evaluated after an update, before the unsuspendRedraw.
2735          * The function is generated in
2736          * @see JXG.Board#addConditions
2737          * @private
2738          */
2739         updateConditions: function () {
2740             return false;
2741         },
2742 
2743         /**
2744          * Calculates adequate snap sizes.
2745          * @returns {JXG.Board} Reference to the board.
2746          */
2747         calculateSnapSizes: function () {
2748             var p1 = new Coords(Const.COORDS_BY_USER, [0, 0], this),
2749                 p2 = new Coords(Const.COORDS_BY_USER, [this.options.grid.gridX, this.options.grid.gridY], this),
2750                 x = p1.scrCoords[1] - p2.scrCoords[1],
2751                 y = p1.scrCoords[2] - p2.scrCoords[2];
2752 
2753             this.options.grid.snapSizeX = this.options.grid.gridX;
2754             while (Math.abs(x) > 25) {
2755                 this.options.grid.snapSizeX *= 2;
2756                 x /= 2;
2757             }
2758 
2759             this.options.grid.snapSizeY = this.options.grid.gridY;
2760             while (Math.abs(y) > 25) {
2761                 this.options.grid.snapSizeY *= 2;
2762                 y /= 2;
2763             }
2764 
2765             return this;
2766         },
2767 
2768         /**
2769          * Apply update on all objects with the new zoom-factors. Clears all traces.
2770          * @returns {JXG.Board} Reference to the board.
2771          */
2772         applyZoom: function () {
2773             this.updateCoords().calculateSnapSizes().clearTraces().fullUpdate();
2774 
2775             return this;
2776         },
2777 
2778         /**
2779          * Zooms into the board by the factors board.attr.zoom.factorX and board.attr.zoom.factorY and applies the zoom.
2780          * @param {Number} [x]
2781          * @param {Number} [y]
2782          * @returns {JXG.Board} Reference to the board
2783          */
2784         zoomIn: function (x, y) {
2785             var bb = this.getBoundingBox(),
2786                 zX = this.attr.zoom.factorx,
2787                 zY = this.attr.zoom.factory,
2788                 dX = (bb[2] - bb[0]) * (1.0 - 1.0 / zX),
2789                 dY = (bb[1] - bb[3]) * (1.0 - 1.0 / zY),
2790                 lr = 0.5,
2791                 tr = 0.5;
2792 
2793             if (typeof x === 'number' && typeof y === 'number') {
2794                 lr = (x - bb[0]) / (bb[2] - bb[0]);
2795                 tr = (bb[1] - y) / (bb[1] - bb[3]);
2796             }
2797 
2798             this.setBoundingBox([bb[0] + dX * lr, bb[1] - dY * tr, bb[2] - dX * (1 - lr), bb[3] + dY * (1 - tr)], false);
2799             this.zoomX *= zX;
2800             this.zoomY *= zY;
2801             this.applyZoom();
2802 
2803             return false;
2804         },
2805 
2806         /**
2807          * Zooms out of the board by the factors board.attr.zoom.factorX and board.attr.zoom.factorY and applies the zoom.
2808          * @param {Number} [x]
2809          * @param {Number} [y]
2810          * @returns {JXG.Board} Reference to the board
2811          */
2812         zoomOut: function (x, y) {
2813             var bb = this.getBoundingBox(),
2814                 zX = this.attr.zoom.factorx,
2815                 zY = this.attr.zoom.factory,
2816                 dX = (bb[2] - bb[0]) * (1.0 - zX),
2817                 dY = (bb[1] - bb[3]) * (1.0 - zY),
2818                 lr = 0.5,
2819                 tr = 0.5;
2820 
2821             if (this.zoomX < this.attr.zoom.eps || this.zoomY < this.attr.zoom.eps) {
2822                 return false;
2823             }
2824 
2825             if (typeof x === 'number' && typeof y === 'number') {
2826                 lr = (x - bb[0]) / (bb[2] - bb[0]);
2827                 tr = (bb[1] - y) / (bb[1] - bb[3]);
2828             }
2829 
2830             this.setBoundingBox([bb[0] + dX * lr, bb[1] - dY * tr, bb[2] - dX * (1 - lr), bb[3] + dY * (1 - tr)], false);
2831             this.zoomX /= zX;
2832             this.zoomY /= zY;
2833 
2834             this.applyZoom();
2835             return false;
2836         },
2837 
2838         /**
2839          * Resets zoom factor to 100%.
2840          * @returns {JXG.Board} Reference to the board
2841          */
2842         zoom100: function () {
2843             var bb = this.getBoundingBox(),
2844                 dX = (bb[2] - bb[0]) * (1.0 - this.zoomX) * 0.5,
2845                 dY = (bb[1] - bb[3]) * (1.0 - this.zoomY) * 0.5;
2846 
2847             this.setBoundingBox([bb[0] + dX, bb[1] - dY, bb[2] - dX, bb[3] + dY], false);
2848             this.zoomX = 1.0;
2849             this.zoomY = 1.0;
2850             this.applyZoom();
2851             return false;
2852         },
2853 
2854         /**
2855          * Zooms the board so every visible point is shown. Keeps aspect ratio.
2856          * @returns {JXG.Board} Reference to the board
2857          */
2858         zoomAllPoints: function () {
2859             var el, border, borderX, borderY, pEl,
2860                 minX = 0,
2861                 maxX = 0,
2862                 minY = 0,
2863                 maxY = 0,
2864                 len = this.objectsList.length;
2865 
2866             for (el = 0; el < len; el++) {
2867                 pEl = this.objectsList[el];
2868 
2869                 if (Type.isPoint(pEl) && pEl.visProp.visible) {
2870                     if (pEl.coords.usrCoords[1] < minX) {
2871                         minX = pEl.coords.usrCoords[1];
2872                     } else if (pEl.coords.usrCoords[1] > maxX) {
2873                         maxX = pEl.coords.usrCoords[1];
2874                     }
2875                     if (pEl.coords.usrCoords[2] > maxY) {
2876                         maxY = pEl.coords.usrCoords[2];
2877                     } else if (pEl.coords.usrCoords[2] < minY) {
2878                         minY = pEl.coords.usrCoords[2];
2879                     }
2880                 }
2881             }
2882 
2883             border = 50;
2884             borderX = border / this.unitX;
2885             borderY = border / this.unitY;
2886 
2887             this.zoomX = 1.0;
2888             this.zoomY = 1.0;
2889 
2890             this.setBoundingBox([minX - borderX, maxY + borderY, maxX + borderX, minY - borderY], true);
2891 
2892             this.applyZoom();
2893 
2894             return this;
2895         },
2896 
2897         /**
2898          * Reset the bounding box and the zoom level to 100% such that a given set of elements is within the board's viewport.
2899          * @param {Array} elements A set of elements given by id, reference, or name.
2900          * @returns {JXG.Board} Reference to the board.
2901          */
2902         zoomElements: function (elements) {
2903             var i, j, e, box,
2904                 newBBox = [0, 0, 0, 0],
2905                 dir = [1, -1, -1, 1];
2906 
2907             if (!Type.isArray(elements) || elements.length === 0) {
2908                 return this;
2909             }
2910 
2911             for (i = 0; i < elements.length; i++) {
2912                 e = this.select(elements[i]);
2913 
2914                 box = e.bounds();
2915                 if (Type.isArray(box)) {
2916                     if (Type.isArray(newBBox)) {
2917                         for (j = 0; j < 4; j++) {
2918                             if (dir[j] * box[j] < dir[j] * newBBox[j]) {
2919                                 newBBox[j] = box[j];
2920                             }
2921                         }
2922                     } else {
2923                         newBBox = box;
2924                     }
2925                 }
2926             }
2927 
2928             if (Type.isArray(newBBox)) {
2929                 for (j = 0; j < 4; j++) {
2930                     newBBox[j] -= dir[j];
2931                 }
2932 
2933                 this.zoomX = 1.0;
2934                 this.zoomY = 1.0;
2935                 this.setBoundingBox(newBBox, true);
2936             }
2937 
2938             return this;
2939         },
2940 
2941         /**
2942          * Sets the zoom level to <tt>fX</tt> resp <tt>fY</tt>.
2943          * @param {Number} fX
2944          * @param {Number} fY
2945          * @returns {JXG.Board}
2946          */
2947         setZoom: function (fX, fY) {
2948             var oX = this.attr.zoom.factorx,
2949                 oY = this.attr.zoom.factory;
2950 
2951             this.attr.zoom.factorx = fX / this.zoomX;
2952             this.attr.zoom.factory = fY / this.zoomY;
2953 
2954             this.zoomIn();
2955 
2956             this.attr.zoom.factorx = oX;
2957             this.attr.zoom.factory = oY;
2958 
2959             return this;
2960         },
2961 
2962         /**
2963          * Removes object from board and renderer.
2964          * @param {JXG.GeometryElement} object The object to remove.
2965          * @returns {JXG.Board} Reference to the board
2966          */
2967         removeObject: function (object) {
2968             var el, i;
2969 
2970             if (Type.isArray(object)) {
2971                 for (i = 0; i < object.length; i++) {
2972                     this.removeObject(object[i]);
2973                 }
2974 
2975                 return this;
2976             }
2977 
2978             object = this.select(object);
2979 
2980             // If the object which is about to be removed unknown or a string, do nothing.
2981             // it is a string if a string was given and could not be resolved to an element.
2982             if (!Type.exists(object) || Type.isString(object)) {
2983                 return this;
2984             }
2985 
2986             try {
2987                 // remove all children.
2988                 for (el in object.childElements) {
2989                     if (object.childElements.hasOwnProperty(el)) {
2990                         object.childElements[el].board.removeObject(object.childElements[el]);
2991                     }
2992                 }
2993 
2994                 for (el in this.objects) {
2995                     if (this.objects.hasOwnProperty(el) && Type.exists(this.objects[el].childElements)) {
2996                         delete this.objects[el].childElements[object.id];
2997                         delete this.objects[el].descendants[object.id];
2998                     }
2999                 }
3000 
3001                 // remove the object itself from our control structures
3002                 if (object._pos > -1) {
3003                     this.objectsList.splice(object._pos, 1);
3004                     for (el = object._pos; el < this.objectsList.length; el++) {
3005                         this.objectsList[el]._pos--;
3006                     }
3007                 } else {
3008                     JXG.debug('Board.removeObject: object ' + object.id + ' not found in list.');
3009                 }
3010                 delete this.objects[object.id];
3011                 delete this.elementsByName[object.name];
3012 
3013                 if (object.visProp && object.visProp.trace) {
3014                     object.clearTrace();
3015                 }
3016 
3017                 // the object deletion itself is handled by the object.
3018                 if (Type.exists(object.remove)) {
3019                     object.remove();
3020                 }
3021             } catch (e) {
3022                 JXG.debug(object.id + ': Could not be removed: ' + e);
3023             }
3024 
3025             this.update();
3026 
3027             return this;
3028         },
3029 
3030 
3031         /**
3032          * Removes the ancestors of an object an the object itself from board and renderer.
3033          * @param {JXG.GeometryElement} object The object to remove.
3034          * @returns {JXG.Board} Reference to the board
3035          */
3036         removeAncestors: function (object) {
3037             var anc;
3038 
3039             for (anc in object.ancestors) {
3040                 if (object.ancestors.hasOwnProperty(anc)) {
3041                     this.removeAncestors(object.ancestors[anc]);
3042                 }
3043             }
3044 
3045             this.removeObject(object);
3046 
3047             return this;
3048         },
3049 
3050         /**
3051          * Initialize some objects which are contained in every GEONExT construction by default,
3052          * but are not contained in the gxt files.
3053          * @returns {JXG.Board} Reference to the board
3054          */
3055         initGeonextBoard: function () {
3056             var p1, p2, p3;
3057 
3058             p1 = this.create('point', [0, 0], {
3059                 id: this.id + 'g00e0',
3060                 name: 'Ursprung',
3061                 withLabel: false,
3062                 visible: false,
3063                 fixed: true
3064             });
3065 
3066             p2 = this.create('point', [1, 0], {
3067                 id: this.id + 'gX0e0',
3068                 name: 'Punkt_1_0',
3069                 withLabel: false,
3070                 visible: false,
3071                 fixed: true
3072             });
3073 
3074             p3 = this.create('point', [0, 1], {
3075                 id: this.id + 'gY0e0',
3076                 name: 'Punkt_0_1',
3077                 withLabel: false,
3078                 visible: false,
3079                 fixed: true
3080             });
3081 
3082             this.create('line', [p1, p2], {
3083                 id: this.id + 'gXLe0',
3084                 name: 'X-Achse',
3085                 withLabel: false,
3086                 visible: false
3087             });
3088 
3089             this.create('line', [p1, p3], {
3090                 id: this.id + 'gYLe0',
3091                 name: 'Y-Achse',
3092                 withLabel: false,
3093                 visible: false
3094             });
3095 
3096             return this;
3097         },
3098 
3099         /**
3100          * Initialize the info box object which is used to display
3101          * the coordinates of points near the mouse pointer,
3102          * @returns {JXG.Board} Reference to the board
3103          */
3104         initInfobox: function () {
3105             var  attr = Type.copyAttributes({}, this.options, 'infobox');
3106 
3107             attr.id = this.id + '_infobox';
3108 
3109             this.infobox = this.create('text', [0, 0, '0,0'], attr);
3110 
3111             this.infobox.distanceX = -20;
3112             this.infobox.distanceY = 25;
3113             // this.infobox.needsUpdateSize = false;  // That is not true, but it speeds drawing up.
3114 
3115             this.infobox.dump = false;
3116 
3117             this.renderer.hide(this.infobox);
3118             return this;
3119         },
3120 
3121         /**
3122          * Change the height and width of the board's container.
3123          * @param {Number} canvasWidth New width of the container.
3124          * @param {Number} canvasHeight New height of the container.
3125          * @param {Boolean} [dontset=false] Do not set the height of the DOM element.
3126          * @returns {JXG.Board} Reference to the board
3127          */
3128         resizeContainer: function (canvasWidth, canvasHeight, dontset) {
3129             this.canvasWidth = parseInt(canvasWidth, 10);
3130             this.canvasHeight = parseInt(canvasHeight, 10);
3131 
3132             if (!dontset) {
3133                 this.containerObj.style.width = (this.canvasWidth) + 'px';
3134                 this.containerObj.style.height = (this.canvasHeight) + 'px';
3135             }
3136 
3137             this.renderer.resize(this.canvasWidth, this.canvasHeight);
3138 
3139             return this;
3140         },
3141 
3142         /**
3143          * Lists the dependencies graph in a new HTML-window.
3144          * @returns {JXG.Board} Reference to the board
3145          */
3146         showDependencies: function () {
3147             var el, t, c, f, i;
3148 
3149             t = '<p>\n';
3150             for (el in this.objects) {
3151                 if (this.objects.hasOwnProperty(el)) {
3152                     i = 0;
3153                     for (c in this.objects[el].childElements) {
3154                         if (this.objects[el].childElements.hasOwnProperty(c)) {
3155                             i += 1;
3156                         }
3157                     }
3158                     if (i >= 0) {
3159                         t += '<strong>' + this.objects[el].id + ':<' + '/strong> ';
3160                     }
3161 
3162                     for (c in this.objects[el].childElements) {
3163                         if (this.objects[el].childElements.hasOwnProperty(c)) {
3164                             t += this.objects[el].childElements[c].id + '(' + this.objects[el].childElements[c].name + ')' + ', ';
3165                         }
3166                     }
3167                     t += '<p>\n';
3168                 }
3169             }
3170             t += '<' + '/p>\n';
3171             f = window.open();
3172             f.document.open();
3173             f.document.write(t);
3174             f.document.close();
3175             return this;
3176         },
3177 
3178         /**
3179          * Lists the XML code of the construction in a new HTML-window.
3180          * @returns {JXG.Board} Reference to the board
3181          */
3182         showXML: function () {
3183             var f = window.open('');
3184             f.document.open();
3185             f.document.write('<pre>' + Type.escapeHTML(this.xmlString) + '<' + '/pre>');
3186             f.document.close();
3187             return this;
3188         },
3189 
3190         /**
3191          * Sets for all objects the needsUpdate flag to "true".
3192          * @returns {JXG.Board} Reference to the board
3193          */
3194         prepareUpdate: function () {
3195             var el, pEl, len = this.objectsList.length;
3196 
3197             for (el = 0; el < len; el++) {
3198                 pEl = this.objectsList[el];
3199                 pEl.needsUpdate = pEl.needsRegularUpdate || this.needsFullUpdate;
3200             }
3201             return this;
3202         },
3203 
3204         /**
3205          * Runs through all elements and calls their update() method.
3206          * @param {JXG.GeometryElement} drag Element that caused the update.
3207          * @returns {JXG.Board} Reference to the board
3208          */
3209         updateElements: function (drag) {
3210             var el, pEl;
3211 
3212             drag = this.select(drag);
3213 
3214             for (el = 0; el < this.objectsList.length; el++) {
3215                 pEl = this.objectsList[el];
3216                 // For updates of an element we distinguish if the dragged element is updated or
3217                 // other elements are updated.
3218                 // The difference lies in the treatment of gliders.
3219                 pEl.update(!Type.exists(drag) || pEl.id !== drag.id);
3220             }
3221 
3222             // update groups last
3223             for (el in this.groups) {
3224                 if (this.groups.hasOwnProperty(el)) {
3225                     this.groups[el].update(drag);
3226                 }
3227             }
3228 
3229             return this;
3230         },
3231 
3232         /**
3233          * Runs through all elements and calls their update() method.
3234          * @returns {JXG.Board} Reference to the board
3235          */
3236         updateRenderer: function () {
3237             var el, pEl,
3238                 len = this.objectsList.length;
3239 
3240             /*
3241             objs = this.objectsList.slice(0);
3242             objs.sort(function(a, b) {
3243                 if (a.visProp.layer < b.visProp.layer) {
3244                     return -1;
3245                 } else if (a.visProp.layer === b.visProp.layer) {
3246                     return b.lastDragTime.getTime() - a.lastDragTime.getTime();
3247                 } else {
3248                     return 1;
3249                 }
3250             });
3251             */
3252 
3253             if (this.renderer.type === 'canvas') {
3254                 this.updateRendererCanvas();
3255             } else {
3256                 for (el = 0; el < len; el++) {
3257                     pEl = this.objectsList[el];
3258                     pEl.updateRenderer();
3259                 }
3260             }
3261             return this;
3262         },
3263 
3264         /**
3265          * Runs through all elements and calls their update() method.
3266          * This is a special version for the CanvasRenderer.
3267          * Here, we have to do our own layer handling.
3268          * @returns {JXG.Board} Reference to the board
3269          */
3270         updateRendererCanvas: function () {
3271             var el, pEl, i, mini, la,
3272                 olen = this.objectsList.length,
3273                 layers = this.options.layer,
3274                 len = this.options.layer.numlayers,
3275                 last = Number.NEGATIVE_INFINITY;
3276 
3277             for (i = 0; i < len; i++) {
3278                 mini = Number.POSITIVE_INFINITY;
3279 
3280                 for (la in layers) {
3281                     if (layers.hasOwnProperty(la)) {
3282                         if (layers[la] > last && layers[la] < mini) {
3283                             mini = layers[la];
3284                         }
3285                     }
3286                 }
3287 
3288                 last = mini;
3289 
3290                 for (el = 0; el < olen; el++) {
3291                     pEl = this.objectsList[el];
3292 
3293                     if (pEl.visProp.layer === mini) {
3294                         pEl.prepareUpdate().updateRenderer();
3295                     }
3296                 }
3297             }
3298             return this;
3299         },
3300 
3301         /**
3302          * Please use {@link JXG.Board#on} instead.
3303          * @param {Function} hook A function to be called by the board after an update occured.
3304          * @param {String} [m='update'] When the hook is to be called. Possible values are <i>mouseup</i>, <i>mousedown</i> and <i>update</i>.
3305          * @param {Object} [context=board] Determines the execution context the hook is called. This parameter is optional, default is the
3306          * board object the hook is attached to.
3307          * @returns {Number} Id of the hook, required to remove the hook from the board.
3308          * @deprecated
3309          */
3310         addHook: function (hook, m, context) {
3311             m = Type.def(m, 'update');
3312 
3313             context = Type.def(context, this);
3314 
3315             this.hooks.push([m, hook]);
3316             this.on(m, hook, context);
3317 
3318             return this.hooks.length - 1;
3319         },
3320 
3321         /**
3322          * Alias of {@link JXG.Board#on}.
3323          */
3324         addEvent: JXG.shortcut(JXG.Board.prototype, 'on'),
3325 
3326         /**
3327          * Please use {@link JXG.Board#off} instead.
3328          * @param {Number|function} id The number you got when you added the hook or a reference to the event handler.
3329          * @returns {JXG.Board} Reference to the board
3330          * @deprecated
3331          */
3332         removeHook: function (id) {
3333             if (this.hooks[id]) {
3334                 this.off(this.hooks[id][0], this.hooks[id][1]);
3335                 this.hooks[id] = null;
3336             }
3337 
3338             return this;
3339         },
3340 
3341         /**
3342          * Alias of {@link JXG.Board#off}.
3343          */
3344         removeEvent: JXG.shortcut(JXG.Board.prototype, 'off'),
3345 
3346         /**
3347          * Runs through all hooked functions and calls them.
3348          * @returns {JXG.Board} Reference to the board
3349          * @deprecated
3350          */
3351         updateHooks: function (m) {
3352             var arg = Array.prototype.slice.call(arguments, 0);
3353 
3354             arg[0] = Type.def(arg[0], 'update');
3355             this.triggerEventHandlers([arg[0]], arguments);
3356 
3357             return this;
3358         },
3359 
3360         /**
3361          * Adds a dependent board to this board.
3362          * @param {JXG.Board} board A reference to board which will be updated after an update of this board occured.
3363          * @returns {JXG.Board} Reference to the board
3364          */
3365         addChild: function (board) {
3366             if (Type.exists(board) && Type.exists(board.containerObj)) {
3367                 this.dependentBoards.push(board);
3368                 this.update();
3369             }
3370             return this;
3371         },
3372 
3373         /**
3374          * Deletes a board from the list of dependent boards.
3375          * @param {JXG.Board} board Reference to the board which will be removed.
3376          * @returns {JXG.Board} Reference to the board
3377          */
3378         removeChild: function (board) {
3379             var i;
3380 
3381             for (i = this.dependentBoards.length - 1; i >= 0; i--) {
3382                 if (this.dependentBoards[i] === board) {
3383                     this.dependentBoards.splice(i, 1);
3384                 }
3385             }
3386             return this;
3387         },
3388 
3389         /**
3390          * Runs through most elements and calls their update() method and update the conditions.
3391          * @param {JXG.GeometryElement} [drag] Element that caused the update.
3392          * @returns {JXG.Board} Reference to the board
3393          */
3394         update: function (drag) {
3395             var i, len, b, insert;
3396 
3397             if (this.inUpdate || this.isSuspendedUpdate) {
3398                 return this;
3399             }
3400             this.inUpdate = true;
3401 
3402             if (this.attr.minimizereflow === 'all' && this.containerObj && this.renderer.type !== 'vml') {
3403                 insert = this.renderer.removeToInsertLater(this.containerObj);
3404             }
3405 
3406             if (this.attr.minimizereflow === 'svg' && this.renderer.type === 'svg') {
3407                 insert = this.renderer.removeToInsertLater(this.renderer.svgRoot);
3408             }
3409 
3410             this.prepareUpdate().updateElements(drag).updateConditions();
3411             this.renderer.suspendRedraw(this);
3412             this.updateRenderer();
3413             this.renderer.unsuspendRedraw();
3414             this.triggerEventHandlers(['update'], []);
3415 
3416             if (insert) {
3417                 insert();
3418             }
3419 
3420             // To resolve dependencies between boards
3421             // for (var board in JXG.boards) {
3422             len = this.dependentBoards.length;
3423             for (i = 0; i < len; i++) {
3424                 b = this.dependentBoards[i];
3425                 if (Type.exists(b) && b !== this) {
3426                     b.updateQuality = this.updateQuality;
3427                     b.prepareUpdate().updateElements().updateConditions();
3428                     b.renderer.suspendRedraw();
3429                     b.updateRenderer();
3430                     b.renderer.unsuspendRedraw();
3431                     b.triggerEventHandlers(['update'], []);
3432                 }
3433 
3434             }
3435 
3436             this.inUpdate = false;
3437             return this;
3438         },
3439 
3440         /**
3441          * Runs through all elements and calls their update() method and update the conditions.
3442          * This is necessary after zooming and changing the bounding box.
3443          * @returns {JXG.Board} Reference to the board
3444          */
3445         fullUpdate: function () {
3446             this.needsFullUpdate = true;
3447             this.update();
3448             this.needsFullUpdate = false;
3449             return this;
3450         },
3451 
3452         /**
3453          * Adds a grid to the board according to the settings given in board.options.
3454          * @returns {JXG.Board} Reference to the board.
3455          */
3456         addGrid: function () {
3457             this.create('grid', []);
3458 
3459             return this;
3460         },
3461 
3462         /**
3463          * Removes all grids assigned to this board. Warning: This method also removes all objects depending on one or
3464          * more of the grids.
3465          * @returns {JXG.Board} Reference to the board object.
3466          */
3467         removeGrids: function () {
3468             var i;
3469 
3470             for (i = 0; i < this.grids.length; i++) {
3471                 this.removeObject(this.grids[i]);
3472             }
3473 
3474             this.grids.length = 0;
3475             this.update(); // required for canvas renderer
3476 
3477             return this;
3478         },
3479 
3480         /**
3481          * Creates a new geometric element of type elementType.
3482          * @param {String} elementType Type of the element to be constructed given as a string e.g. 'point' or 'circle'.
3483          * @param {Array} parents Array of parent elements needed to construct the element e.g. coordinates for a point or two
3484          * points to construct a line. This highly depends on the elementType that is constructed. See the corresponding JXG.create*
3485          * methods for a list of possible parameters.
3486          * @param {Object} [attributes] An object containing the attributes to be set. This also depends on the elementType.
3487          * Common attributes are name, visible, strokeColor.
3488          * @returns {Object} Reference to the created element. This is usually a GeometryElement, but can be an array containing
3489          * two or more elements.
3490          */
3491         create: function (elementType, parents, attributes) {
3492             var el, i;
3493 
3494             elementType = elementType.toLowerCase();
3495 
3496             if (!Type.exists(parents)) {
3497                 parents = [];
3498             }
3499 
3500             if (!Type.exists(attributes)) {
3501                 attributes = {};
3502             }
3503 
3504             for (i = 0; i < parents.length; i++) {
3505                 if (typeof parents[i] === 'string' && (elementType !== 'text' || i !== 2)) {
3506                     parents[i] = this.select(parents[i]);
3507                 }
3508             }
3509 
3510             if (typeof JXG.elements[elementType] === 'function') {
3511                 el = JXG.elements[elementType](this, parents, attributes);
3512             } else {
3513                 throw new Error("JSXGraph: create: Unknown element type given: " + elementType);
3514             }
3515 
3516             if (!Type.exists(el)) {
3517                 JXG.debug("JSXGraph: create: failure creating " + elementType);
3518                 return el;
3519             }
3520 
3521             if (el.prepareUpdate && el.update && el.updateRenderer) {
3522                 el.prepareUpdate().update().updateRenderer();
3523             }
3524             return el;
3525         },
3526 
3527         /**
3528          * Deprecated name for {@link JXG.Board#create}.
3529          * @deprecated
3530          */
3531         createElement: JXG.shortcut(JXG.Board.prototype, 'create'),
3532 
3533 
3534         /**
3535          * Delete the elements drawn as part of a trace of an element.
3536          * @returns {JXG.Board} Reference to the board
3537          */
3538         clearTraces: function () {
3539             var el;
3540 
3541             for (el = 0; el < this.objectsList.length; el++) {
3542                 this.objectsList[el].clearTrace();
3543             }
3544 
3545             this.numTraces = 0;
3546             return this;
3547         },
3548 
3549         /**
3550          * Stop updates of the board.
3551          * @returns {JXG.Board} Reference to the board
3552          */
3553         suspendUpdate: function () {
3554             if (!this.inUpdate) {
3555                 this.isSuspendedUpdate = true;
3556             }
3557             return this;
3558         },
3559 
3560         /**
3561          * Enable updates of the board.
3562          * @returns {JXG.Board} Reference to the board
3563          */
3564         unsuspendUpdate: function () {
3565             if (this.isSuspendedUpdate) {
3566                 this.isSuspendedUpdate = false;
3567                 this.update();
3568             }
3569             return this;
3570         },
3571 
3572         /**
3573          * Set the bounding box of the board.
3574          * @param {Array} bbox New bounding box [x1,y1,x2,y2]
3575          * @param {Boolean} [keepaspectratio=false] If set to true, the aspect ratio will be 1:1, but
3576          * the resulting viewport may be larger.
3577          * @returns {JXG.Board} Reference to the board
3578          */
3579         setBoundingBox: function (bbox, keepaspectratio) {
3580             var h, w,
3581                 dim = Env.getDimensions(this.container, this.document);
3582 
3583             if (!Type.isArray(bbox)) {
3584                 return this;
3585             }
3586 
3587             this.plainBB = bbox;
3588 
3589             this.canvasWidth = parseInt(dim.width, 10);
3590             this.canvasHeight = parseInt(dim.height, 10);
3591             w = this.canvasWidth;
3592             h = this.canvasHeight;
3593 
3594             if (keepaspectratio) {
3595                 this.unitX = w / (bbox[2] - bbox[0]);
3596                 this.unitY = h / (bbox[1] - bbox[3]);
3597                 if (Math.abs(this.unitX) < Math.abs(this.unitY)) {
3598                     this.unitY = Math.abs(this.unitX) * this.unitY / Math.abs(this.unitY);
3599                 } else {
3600                     this.unitX = Math.abs(this.unitY) * this.unitX / Math.abs(this.unitX);
3601                 }
3602             } else {
3603                 this.unitX = w / (bbox[2] - bbox[0]);
3604                 this.unitY = h / (bbox[1] - bbox[3]);
3605             }
3606 
3607             this.moveOrigin(-this.unitX * bbox[0], this.unitY * bbox[1]);
3608 
3609             return this;
3610         },
3611 
3612         /**
3613          * Get the bounding box of the board.
3614          * @returns {Array} bounding box [x1,y1,x2,y2] upper left corner, lower right corner
3615          */
3616         getBoundingBox: function () {
3617             var ul = new Coords(Const.COORDS_BY_SCREEN, [0, 0], this),
3618                 lr = new Coords(Const.COORDS_BY_SCREEN, [this.canvasWidth, this.canvasHeight], this);
3619 
3620             return [ul.usrCoords[1], ul.usrCoords[2], lr.usrCoords[1], lr.usrCoords[2]];
3621         },
3622 
3623         /**
3624          * Adds an animation. Animations are controlled by the boards, so the boards need to be aware of the
3625          * animated elements. This function tells the board about new elements to animate.
3626          * @param {JXG.GeometryElement} element The element which is to be animated.
3627          * @returns {JXG.Board} Reference to the board
3628          */
3629         addAnimation: function (element) {
3630             var that = this;
3631 
3632             this.animationObjects[element.id] = element;
3633 
3634             if (!this.animationIntervalCode) {
3635                 this.animationIntervalCode = window.setInterval(function () {
3636                     that.animate();
3637                 }, element.board.attr.animationdelay);
3638             }
3639 
3640             return this;
3641         },
3642 
3643         /**
3644          * Cancels all running animations.
3645          * @returns {JXG.Board} Reference to the board
3646          */
3647         stopAllAnimation: function () {
3648             var el;
3649 
3650             for (el in this.animationObjects) {
3651                 if (this.animationObjects.hasOwnProperty(el) && Type.exists(this.animationObjects[el])) {
3652                     this.animationObjects[el] = null;
3653                     delete this.animationObjects[el];
3654                 }
3655             }
3656 
3657             window.clearInterval(this.animationIntervalCode);
3658             delete this.animationIntervalCode;
3659 
3660             return this;
3661         },
3662 
3663         /**
3664          * General purpose animation function. This currently only supports moving points from one place to another. This
3665          * is faster than managing the animation per point, especially if there is more than one animated point at the same time.
3666          * @returns {JXG.Board} Reference to the board
3667          */
3668         animate: function () {
3669             var props, el, o, newCoords, r, p, c, cbtmp,
3670                 count = 0,
3671                 obj = null;
3672 
3673             for (el in this.animationObjects) {
3674                 if (this.animationObjects.hasOwnProperty(el) && Type.exists(this.animationObjects[el])) {
3675                     count += 1;
3676                     o = this.animationObjects[el];
3677 
3678                     if (o.animationPath) {
3679                         if (Type.isFunction(o.animationPath)) {
3680                             newCoords = o.animationPath(new Date().getTime() - o.animationStart);
3681                         } else {
3682                             newCoords = o.animationPath.pop();
3683                         }
3684 
3685                         if ((!Type.exists(newCoords)) || (!Type.isArray(newCoords) && isNaN(newCoords))) {
3686                             delete o.animationPath;
3687                         } else {
3688                             o.setPositionDirectly(Const.COORDS_BY_USER, newCoords);
3689                             o.prepareUpdate().update().updateRenderer();
3690                             obj = o;
3691                         }
3692                     }
3693                     if (o.animationData) {
3694                         c = 0;
3695 
3696                         for (r in o.animationData) {
3697                             if (o.animationData.hasOwnProperty(r)) {
3698                                 p = o.animationData[r].pop();
3699 
3700                                 if (!Type.exists(p)) {
3701                                     delete o.animationData[p];
3702                                 } else {
3703                                     c += 1;
3704                                     props = {};
3705                                     props[r] = p;
3706                                     o.setAttribute(props);
3707                                 }
3708                             }
3709                         }
3710 
3711                         if (c === 0) {
3712                             delete o.animationData;
3713                         }
3714                     }
3715 
3716                     if (!Type.exists(o.animationData) && !Type.exists(o.animationPath)) {
3717                         this.animationObjects[el] = null;
3718                         delete this.animationObjects[el];
3719 
3720                         if (Type.exists(o.animationCallback)) {
3721                             cbtmp = o.animationCallback;
3722                             o.animationCallback = null;
3723                             cbtmp();
3724                         }
3725                     }
3726                 }
3727             }
3728 
3729             if (count === 0) {
3730                 window.clearInterval(this.animationIntervalCode);
3731                 delete this.animationIntervalCode;
3732             } else {
3733                 this.update(obj);
3734             }
3735 
3736             return this;
3737         },
3738 
3739         /**
3740          * Migrate the dependency properties of the point src
3741          * to the point dest and  delete the point src.
3742          * For example, a circle around the point src
3743          * receives the new center dest. The old center src
3744          * will be deleted.
3745          * @param {JXG.Point} src Original point which will be deleted
3746          * @param {JXG.Point} dest New point with the dependencies of src.
3747          * @param {Boolean} copyName Flag which decides if the name of the src element is copied to the
3748          *  dest element.
3749          * @returns {JXG.Board} Reference to the board
3750          */
3751         migratePoint: function (src, dest, copyName) {
3752             var child, childId, prop, found, i, srcLabelId, srcHasLabel = false;
3753 
3754             src = this.select(src);
3755             dest = this.select(dest);
3756 
3757             if (JXG.exists(src.label)) {
3758                 srcLabelId = src.label.id;
3759                 srcHasLabel = true;
3760                 this.removeObject(src.label);
3761             }
3762 
3763             for (childId in src.childElements) {
3764                 if (src.childElements.hasOwnProperty(childId)) {
3765                     child = src.childElements[childId];
3766                     found = false;
3767 
3768                     for (prop in child) {
3769                         if (child.hasOwnProperty(prop)) {
3770                             if (child[prop] ===  src) {
3771                                 child[prop] = dest;
3772                                 found = true;
3773                             }
3774                         }
3775                     }
3776 
3777                     if (found) {
3778                         delete src.childElements[childId];
3779                     }
3780 
3781                     for (i = 0; i < child.parents.length; i++) {
3782                         if (child.parents[i] === src.id) {
3783                             child.parents[i] = dest.id;
3784                         }
3785                     }
3786 
3787                     dest.addChild(child);
3788                 }
3789             }
3790 
3791             // The destination object should receive the name
3792             // and the label of the originating (src) object
3793             if (copyName) {
3794                 if (srcHasLabel) {
3795                     delete dest.childElements[srcLabelId];
3796                     delete dest.descendants[srcLabelId];
3797                 }
3798 
3799                 if (dest.label) {
3800                     this.removeObject(dest.label);
3801                 }
3802 
3803                 delete this.elementsByName[dest.name];
3804                 dest.name = src.name;
3805                 if (srcHasLabel) {
3806                     dest.createLabel();
3807                 }
3808             }
3809 
3810             this.removeObject(src);
3811 
3812             if (Type.exists(dest.name) && dest.name !== '') {
3813                 this.elementsByName[dest.name] = dest;
3814             }
3815 
3816             this.update();
3817 
3818             return this;
3819         },
3820 
3821         /**
3822          * Initializes color blindness simulation.
3823          * @param {String} deficiency Describes the color blindness deficiency which is simulated. Accepted values are 'protanopia', 'deuteranopia', and 'tritanopia'.
3824          * @returns {JXG.Board} Reference to the board
3825          */
3826         emulateColorblindness: function (deficiency) {
3827             var e, o;
3828 
3829             if (!Type.exists(deficiency)) {
3830                 deficiency = 'none';
3831             }
3832 
3833             if (this.currentCBDef === deficiency) {
3834                 return this;
3835             }
3836 
3837             for (e in this.objects) {
3838                 if (this.objects.hasOwnProperty(e)) {
3839                     o = this.objects[e];
3840 
3841                     if (deficiency !== 'none') {
3842                         if (this.currentCBDef === 'none') {
3843                             // this could be accomplished by JXG.extend, too. But do not use
3844                             // JXG.deepCopy as this could result in an infinite loop because in
3845                             // visProp there could be geometry elements which contain the board which
3846                             // contains all objects which contain board etc.
3847                             o.visPropOriginal = {
3848                                 strokecolor: o.visProp.strokecolor,
3849                                 fillcolor: o.visProp.fillcolor,
3850                                 highlightstrokecolor: o.visProp.highlightstrokecolor,
3851                                 highlightfillcolor: o.visProp.highlightfillcolor
3852                             };
3853                         }
3854                         o.setAttribute({
3855                             strokecolor: Color.rgb2cb(o.visPropOriginal.strokecolor, deficiency),
3856                             fillcolor: Color.rgb2cb(o.visPropOriginal.fillcolor, deficiency),
3857                             highlightstrokecolor: Color.rgb2cb(o.visPropOriginal.highlightstrokecolor, deficiency),
3858                             highlightfillcolor: Color.rgb2cb(o.visPropOriginal.highlightfillcolor, deficiency)
3859                         });
3860                     } else if (Type.exists(o.visPropOriginal)) {
3861                         JXG.extend(o.visProp, o.visPropOriginal);
3862                     }
3863                 }
3864             }
3865             this.currentCBDef = deficiency;
3866             this.update();
3867 
3868             return this;
3869         },
3870 
3871         /**
3872          * Select a single or multiple elements at once.
3873          * @param {String|Object|function} str The name, id or a reference to a JSXGraph element on this board. An object will
3874          * be used as a filter to return multiple elements at once filtered by the properties of the object.
3875          * @returns {JXG.GeometryElement|JXG.Composition}
3876          * @example
3877          * // select the element with name A
3878          * board.select('A');
3879          *
3880          * // select all elements with strokecolor set to 'red' (but not '#ff0000')
3881          * board.select({
3882          *   strokeColor: 'red'
3883          * });
3884          *
3885          * // select all points on or below the x axis and make them black.
3886          * board.select({
3887          *   elementClass: JXG.OBJECT_CLASS_POINT,
3888          *   Y: function (v) {
3889          *     return v <= 0;
3890          *   }
3891          * }).setAttribute({color: 'black'});
3892          *
3893          * // select all elements
3894          * board.select(function (el) {
3895          *   return true;
3896          * });
3897          */
3898         select: function (str) {
3899             var flist, olist, i, l,
3900                 s = str;
3901 
3902             if (s === null) {
3903                 return s;
3904             }
3905 
3906             // it's a string, most likely an id or a name.
3907             if (typeof s === 'string' && s !== '') {
3908                 // Search by ID
3909                 if (Type.exists(this.objects[s])) {
3910                     s = this.objects[s];
3911                 // Search by name
3912                 } else if (Type.exists(this.elementsByName[s])) {
3913                     s = this.elementsByName[s];
3914                 // Search by group ID
3915                 } else if (Type.exists(this.groups[s])) {
3916                     s = this.groups[s];
3917                 }
3918             // it's a function or an object, but not an element
3919             } else if (typeof s === 'function' || (typeof s === 'object' && !JXG.isArray(s) && typeof s.setAttribute !== 'function')) {
3920 
3921                 flist = Type.filterElements(this.objectsList, s);
3922 
3923                 olist = {};
3924                 l = flist.length;
3925                 for (i = 0; i < l; i++) {
3926                     olist[flist[i].id] = flist[i];
3927                 }
3928                 s = new EComposition(olist);
3929             // it's an element which has been deleted (and still hangs around, e.g. in an attractor list
3930             } else if (typeof s === 'object' && JXG.exists(s.id) && !JXG.exists(this.objects[s.id])) {
3931                 s = null;
3932             }
3933 
3934             return s;
3935         },
3936 
3937         /**
3938          * Checks if the given point is inside the boundingbox.
3939          * @param {Number|JXG.Coords} x User coordinate or {@link JXG.Coords} object.
3940          * @param {Number} [y] User coordinate. May be omitted in case <tt>x</tt> is a {@link JXG.Coords} object.
3941          * @returns {Boolean}
3942          */
3943         hasPoint: function (x, y) {
3944             var px = x,
3945                 py = y,
3946                 bbox = this.getBoundingBox();
3947 
3948             if (JXG.exists(x) && JXG.isArray(x.usrCoords)) {
3949                 px = x.usrCoords[1];
3950                 py = x.usrCoords[2];
3951             }
3952 
3953             if (typeof px === 'number' && typeof py === 'number' &&
3954                     bbox[0] < px && px < bbox[2] && bbox[1] > py && py > bbox[3]) {
3955                 return true;
3956             }
3957 
3958             return false;
3959         },
3960 
3961         /**
3962          * Update CSS transformations of sclaing type. It is used to correct the mouse position
3963          * in {@link JXG.Board#getMousePosition}.
3964          * The inverse transformation matrix is updated on each mouseDown and touchStart event.
3965          *
3966          * It is up to the user to call this method after an update of the CSS transformation
3967          * in the DOM.
3968          */
3969         updateCSSTransforms: function () {
3970             var obj = this.containerObj,
3971                 o = obj,
3972                 o2 = obj;
3973 
3974             this.cssTransMat = Env.getCSSTransformMatrix(o);
3975 
3976             /*
3977              * In Mozilla and Webkit: offsetParent seems to jump at least to the next iframe,
3978              * if not to the body. In IE and if we are in an position:absolute environment
3979              * offsetParent walks up the DOM hierarchy.
3980              * In order to walk up the DOM hierarchy also in Mozilla and Webkit
3981              * we need the parentNode steps.
3982              */
3983             o = o.offsetParent;
3984             while (o) {
3985                 this.cssTransMat = Mat.matMatMult(Env.getCSSTransformMatrix(o), this.cssTransMat);
3986 
3987                 o2 = o2.parentNode;
3988                 while (o2 !== o) {
3989                     this.cssTransMat = Mat.matMatMult(Env.getCSSTransformMatrix(o), this.cssTransMat);
3990                     o2 = o2.parentNode;
3991                 }
3992 
3993                 o = o.offsetParent;
3994             }
3995             this.cssTransMat = Mat.inverse(this.cssTransMat);
3996 
3997             return this;
3998         },
3999 
4000 
4001         /* **************************
4002          *     EVENT DEFINITION
4003          * for documentation purposes
4004          * ************************** */
4005 
4006         //region Event handler documentation
4007 
4008         /**
4009          * @event
4010          * @description Whenever the user starts to touch or click the board.
4011          * @name JXG.Board#down
4012          * @param {Event} e The browser's event object.
4013          */
4014         __evt__down: function (e) { },
4015 
4016         /**
4017          * @event
4018          * @description Whenever the user starts to click on the board.
4019          * @name JXG.Board#mousedown
4020          * @param {Event} e The browser's event object.
4021          */
4022         __evt__mousedown: function (e) { },
4023 
4024         /**
4025          * @event
4026          * @description Whenever the user starts to touch the board.
4027          * @name JXG.Board#touchstart
4028          * @param {Event} e The browser's event object.
4029          */
4030         __evt__touchstart: function (e) { },
4031 
4032         /**
4033          * @event
4034          * @description Whenever the user stops to touch or click the board.
4035          * @name JXG.Board#up
4036          * @param {Event} e The browser's event object.
4037          */
4038         __evt__up: function (e) { },
4039 
4040         /**
4041          * @event
4042          * @description Whenever the user releases the mousebutton over the board.
4043          * @name JXG.Board#mouseup
4044          * @param {Event} e The browser's event object.
4045          */
4046         __evt__mouseup: function (e) { },
4047 
4048         /**
4049          * @event
4050          * @description Whenever the user stops touching the board.
4051          * @name JXG.Board#touchend
4052          * @param {Event} e The browser's event object.
4053          */
4054         __evt__touchend: function (e) { },
4055 
4056         /**
4057          * @event
4058          * @description This event is fired whenever the user is moving the finger or mouse pointer over the board.
4059          * @name JXG.Board#move
4060          * @param {Event} e The browser's event object.
4061          * @param {Number} mode The mode the board currently is in
4062          * @see {JXG.Board#mode}
4063          */
4064         __evt__move: function (e, mode) { },
4065 
4066         /**
4067          * @event
4068          * @description This event is fired whenever the user is moving the mouse over the board.
4069          * @name JXG.Board#mousemove
4070          * @param {Event} e The browser's event object.
4071          * @param {Number} mode The mode the board currently is in
4072          * @see {JXG.Board#mode}
4073          */
4074         __evt__mousemove: function (e, mode) { },
4075 
4076         /**
4077          * @event
4078          * @description This event is fired whenever the user is moving the finger over the board.
4079          * @name JXG.Board#touchmove
4080          * @param {Event} e The browser's event object.
4081          * @param {Number} mode The mode the board currently is in
4082          * @see {JXG.Board#mode}
4083          */
4084         __evt__touchmove: function (e, mode) { },
4085 
4086         /**
4087          * @event
4088          * @description Whenever an element is highlighted this event is fired.
4089          * @name JXG.Board#hit
4090          * @param {Event} e The browser's event object.
4091          * @param {JXG.GeometryElement} el The hit element.
4092          * @param target
4093          */
4094         __evt__hit: function (e, el, target) { },
4095 
4096         /**
4097          * @event
4098          * @description Whenever an element is highlighted this event is fired.
4099          * @name JXG.Board#mousehit
4100          * @param {Event} e The browser's event object.
4101          * @param {JXG.GeometryElement} el The hit element.
4102          * @param target
4103          */
4104         __evt__mousehit: function (e, el, target) { },
4105 
4106         /**
4107          * @event
4108          * @description This board is updated.
4109          * @name JXG.Board#update
4110          */
4111         __evt__update: function () { },
4112 
4113         /**
4114          * @event
4115          * @description The bounding box of the board has changed.
4116          * @name JXG.Board#boundingbox
4117          */
4118         __evt__boundingbox: function () { },
4119 
4120         /**
4121          * @ignore
4122          */
4123         __evt: function () {},
4124 
4125         //endregion
4126 
4127         /**
4128          * Function to animate a curve rolling on another curve.
4129          * @param {Curve} c1 JSXGraph curve building the floor where c2 rolls
4130          * @param {Curve} c2 JSXGraph curve which rolls on c1.
4131          * @param {number} start_c1 The parameter t such that c1(t) touches c2. This is the start position of the
4132          *                          rolling process
4133          * @param {Number} stepsize Increase in t in each step for the curve c1
4134          * @param {Number} direction
4135          * @param {Number} time Delay time for setInterval()
4136          * @param {Array} pointlist Array of points which are rolled in each step. This list should contain
4137          *      all points which define c2 and gliders on c2.
4138          *
4139          * @example
4140          *
4141          * // Line which will be the floor to roll upon.
4142          * var line = brd.create('curve', [function (t) { return t;}, function (t){ return 1;}], {strokeWidth:6});
4143          * // Center of the rolling circle
4144          * var C = brd.create('point',[0,2],{name:'C'});
4145          * // Starting point of the rolling circle
4146          * var P = brd.create('point',[0,1],{name:'P', trace:true});
4147          * // Circle defined as a curve. The circle "starts" at P, i.e. circle(0) = P
4148          * var circle = brd.create('curve',[
4149          *           function (t){var d = P.Dist(C),
4150          *                           beta = JXG.Math.Geometry.rad([C.X()+1,C.Y()],C,P);
4151          *                       t += beta;
4152          *                       return C.X()+d*Math.cos(t);
4153          *           },
4154          *           function (t){var d = P.Dist(C),
4155          *                           beta = JXG.Math.Geometry.rad([C.X()+1,C.Y()],C,P);
4156          *                       t += beta;
4157          *                       return C.Y()+d*Math.sin(t);
4158          *           },
4159          *           0,2*Math.PI],
4160          *           {strokeWidth:6, strokeColor:'green'});
4161          *
4162          * // Point on circle
4163          * var B = brd.create('glider',[0,2,circle],{name:'B', color:'blue',trace:false});
4164          * var roll = brd.createRoulette(line, circle, 0, Math.PI/20, 1, 100, [C,P,B]);
4165          * roll.start() // Start the rolling, to be stopped by roll.stop()
4166          *
4167          * </pre><div id="e5e1b53c-a036-4a46-9e35-190d196beca5" style="width: 300px; height: 300px;"></div>
4168          * <script type="text/javascript">
4169          * var brd = JXG.JSXGraph.initBoard('e5e1b53c-a036-4a46-9e35-190d196beca5', {boundingbox: [-5, 5, 5, -5], axis: true, showcopyright:false, shownavigation: false});
4170          * // Line which will be the floor to roll upon.
4171          * var line = brd.create('curve', [function (t) { return t;}, function (t){ return 1;}], {strokeWidth:6});
4172          * // Center of the rolling circle
4173          * var C = brd.create('point',[0,2],{name:'C'});
4174          * // Starting point of the rolling circle
4175          * var P = brd.create('point',[0,1],{name:'P', trace:true});
4176          * // Circle defined as a curve. The circle "starts" at P, i.e. circle(0) = P
4177          * var circle = brd.create('curve',[
4178          *           function (t){var d = P.Dist(C),
4179          *                           beta = JXG.Math.Geometry.rad([C.X()+1,C.Y()],C,P);
4180          *                       t += beta;
4181          *                       return C.X()+d*Math.cos(t);
4182          *           },
4183          *           function (t){var d = P.Dist(C),
4184          *                           beta = JXG.Math.Geometry.rad([C.X()+1,C.Y()],C,P);
4185          *                       t += beta;
4186          *                       return C.Y()+d*Math.sin(t);
4187          *           },
4188          *           0,2*Math.PI],
4189          *           {strokeWidth:6, strokeColor:'green'});
4190          *
4191          * // Point on circle
4192          * var B = brd.create('glider',[0,2,circle],{name:'B', color:'blue',trace:false});
4193          * var roll = brd.createRoulette(line, circle, 0, Math.PI/20, 1, 100, [C,P,B]);
4194          * roll.start() // Start the rolling, to be stopped by roll.stop()
4195          * </script><pre>
4196          */
4197         createRoulette: function (c1, c2, start_c1, stepsize, direction, time, pointlist) {
4198             var brd = this,
4199                 Roulette = function () {
4200                     var alpha = 0, Tx = 0, Ty = 0,
4201                         t1 = start_c1,
4202                         t2 = Numerics.root(
4203                             function (t) {
4204                                 var c1x = c1.X(t1),
4205                                     c1y = c1.Y(t1),
4206                                     c2x = c2.X(t),
4207                                     c2y = c2.Y(t);
4208 
4209                                 return (c1x - c2x) * (c1x - c2x) + (c1y - c2y) * (c1y - c2y);
4210                             },
4211                             [0, Math.PI * 2]
4212                         ),
4213                         t1_new = 0.0, t2_new = 0.0,
4214                         c1dist,
4215 
4216                         rotation = brd.create('transform', [
4217                             function () {
4218                                 return alpha;
4219                             }
4220                         ], {type: 'rotate'}),
4221 
4222                         rotationLocal = brd.create('transform', [
4223                             function () {
4224                                 return alpha;
4225                             },
4226                             function () {
4227                                 return c1.X(t1);
4228                             },
4229                             function () {
4230                                 return c1.Y(t1);
4231                             }
4232                         ], {type: 'rotate'}),
4233 
4234                         translate = brd.create('transform', [
4235                             function () {
4236                                 return Tx;
4237                             },
4238                             function () {
4239                                 return Ty;
4240                             }
4241                         ], {type: 'translate'}),
4242 
4243                         // arc length via Simpson's rule.
4244                         arclen = function (c, a, b) {
4245                             var cpxa = Numerics.D(c.X)(a),
4246                                 cpya = Numerics.D(c.Y)(a),
4247                                 cpxb = Numerics.D(c.X)(b),
4248                                 cpyb = Numerics.D(c.Y)(b),
4249                                 cpxab = Numerics.D(c.X)((a + b) * 0.5),
4250                                 cpyab = Numerics.D(c.Y)((a + b) * 0.5),
4251 
4252                                 fa = Math.sqrt(cpxa * cpxa + cpya * cpya),
4253                                 fb = Math.sqrt(cpxb * cpxb + cpyb * cpyb),
4254                                 fab = Math.sqrt(cpxab * cpxab + cpyab * cpyab);
4255 
4256                             return (fa + 4 * fab + fb) * (b - a) / 6;
4257                         },
4258 
4259                         exactDist = function (t) {
4260                             return c1dist - arclen(c2, t2, t);
4261                         },
4262 
4263                         beta = Math.PI / 18,
4264                         beta9 = beta * 9,
4265                         interval = null;
4266 
4267                     this.rolling = function () {
4268                         var h, g, hp, gp, z;
4269 
4270                         t1_new = t1 + direction * stepsize;
4271 
4272                         // arc length between c1(t1) and c1(t1_new)
4273                         c1dist = arclen(c1, t1, t1_new);
4274 
4275                         // find t2_new such that arc length between c2(t2) and c1(t2_new) equals c1dist.
4276                         t2_new = Numerics.root(exactDist, t2);
4277 
4278                         // c1(t) as complex number
4279                         h = new Complex(c1.X(t1_new), c1.Y(t1_new));
4280 
4281                         // c2(t) as complex number
4282                         g = new Complex(c2.X(t2_new), c2.Y(t2_new));
4283 
4284                         hp = new Complex(Numerics.D(c1.X)(t1_new), Numerics.D(c1.Y)(t1_new));
4285                         gp = new Complex(Numerics.D(c2.X)(t2_new), Numerics.D(c2.Y)(t2_new));
4286 
4287                         // z is angle between the tangents of c1 at t1_new, and c2 at t2_new
4288                         z = Complex.C.div(hp, gp);
4289 
4290                         alpha = Math.atan2(z.imaginary, z.real);
4291                         // Normalizing the quotient
4292                         z.div(Complex.C.abs(z));
4293                         z.mult(g);
4294                         Tx = h.real - z.real;
4295 
4296                         // T = h(t1_new)-g(t2_new)*h'(t1_new)/g'(t2_new);
4297                         Ty = h.imaginary - z.imaginary;
4298 
4299                         // -(10-90) degrees: make corners roll smoothly
4300                         if (alpha < -beta && alpha > -beta9) {
4301                             alpha = -beta;
4302                             rotationLocal.applyOnce(pointlist);
4303                         } else if (alpha > beta && alpha < beta9) {
4304                             alpha = beta;
4305                             rotationLocal.applyOnce(pointlist);
4306                         } else {
4307                             rotation.applyOnce(pointlist);
4308                             translate.applyOnce(pointlist);
4309                             t1 = t1_new;
4310                             t2 = t2_new;
4311                         }
4312                         brd.update();
4313                     };
4314 
4315                     this.start = function () {
4316                         if (time > 0) {
4317                             interval = window.setInterval(this.rolling, time);
4318                         }
4319                         return this;
4320                     };
4321 
4322                     this.stop = function () {
4323                         window.clearInterval(interval);
4324                         return this;
4325                     };
4326                     return this;
4327                 };
4328             return new Roulette();
4329         }
4330     });
4331 
4332     return JXG.Board;
4333 });
4334