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