1 /*
  2     Copyright 2008-2022
  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 

 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', 'utils/color', 'utils/type', 'utils/event', 'utils/env',
 69     'base/composition'
 70 ], function (JXG, Const, Coords, Options, Numerics, Mat, Geometry, Complex, Statistics, JessieCode, Color, Type,
 71                 EventEmitter, Env, Composition) {
 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 high quality, e.g. graphs are evaluated at much more points.
124          * @type Number
125          * @constant
126          * @see JXG.Board#updateQuality
127          */
128         this.BOARD_MODE_ZOOM = 0x0011;
129 
130         /**
131          * Update is made with low quality, e.g. graphs are evaluated at a lesser amount of points.
132          * @type Number
133          * @constant
134          * @see JXG.Board#updateQuality
135          */
136         this.BOARD_QUALITY_LOW = 0x1;
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_QUALITY_HIGH = 0x2;
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          * @name JXG.Board#renderer
178          * @private
179          * @ignore
180          */
181         this.renderer = renderer;
182 
183         /**
184          * Grids keeps track of all grids attached to this board.
185          * @type Array
186          * @private
187          */
188         this.grids = [];
189 
190         /**
191          * Some standard options
192          * @type JXG.Options
193          */
194         this.options = Type.deepCopy(Options);
195         this.attr = attributes;
196 
197         /**
198          * Dimension of the board.
199          * @default 2
200          * @type Number
201          */
202         this.dimension = 2;
203 
204         this.jc = new JessieCode();
205         this.jc.use(this);
206 
207         /**
208          * Coordinates of the boards origin. This a object with the two properties
209          * usrCoords and scrCoords. usrCoords always equals [1, 0, 0] and scrCoords
210          * stores the boards origin in homogeneous screen coordinates.
211          * @type Object
212          * @private
213          */
214         this.origin = {};
215         this.origin.usrCoords = [1, 0, 0];
216         this.origin.scrCoords = [1, origin[0], origin[1]];
217 
218         /**
219          * Zoom factor in X direction. It only stores the zoom factor to be able
220          * to get back to 100% in zoom100().
221          * @name JXG.Board.zoomX
222          * @type Number
223          * @private
224          * @ignore
225          */
226         this.zoomX = zoomX;
227 
228         /**
229          * Zoom factor in Y direction. It only stores the zoom factor to be able
230          * to get back to 100% in zoom100().
231          * @name JXG.Board.zoomY
232          * @type Number
233          * @private
234          * @ignore
235          */
236         this.zoomY = zoomY;
237 
238         /**
239          * The number of pixels which represent one unit in user-coordinates in x direction.
240          * @type Number
241          * @private
242          */
243         this.unitX = unitX * this.zoomX;
244 
245         /**
246          * The number of pixels which represent one unit in user-coordinates in y direction.
247          * @type Number
248          * @private
249          */
250         this.unitY = unitY * this.zoomY;
251 
252         /**
253          * Keep aspect ratio if bounding box is set and the width/height ratio differs from the
254          * width/height ratio of the canvas.
255          * @type Boolean
256          * @private
257          */
258         this.keepaspectratio = false;
259 
260         /**
261          * Canvas width.
262          * @type Number
263          * @private
264          */
265         this.canvasWidth = canvasWidth;
266 
267         /**
268          * Canvas Height
269          * @type Number
270          * @private
271          */
272         this.canvasHeight = canvasHeight;
273 
274         // If the given id is not valid, generate an unique id
275         if (Type.exists(id) && id !== '' && Env.isBrowser && !Type.exists(this.document.getElementById(id))) {
276             this.id = id;
277         } else {
278             this.id = this.generateId();
279         }
280 
281         EventEmitter.eventify(this);
282 
283         this.hooks = [];
284 
285         /**
286          * An array containing all other boards that are updated after this board has been updated.
287          * @type Array
288          * @see JXG.Board#addChild
289          * @see JXG.Board#removeChild
290          */
291         this.dependentBoards = [];
292 
293         /**
294          * During the update process this is set to false to prevent an endless loop.
295          * @default false
296          * @type Boolean
297          */
298         this.inUpdate = false;
299 
300         /**
301          * 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.
302          * @type Object
303          */
304         this.objects = {};
305 
306         /**
307          * An array containing all geometric objects on the board in the order of construction.
308          * @type Array
309          */
310         this.objectsList = [];
311 
312         /**
313          * 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.
314          * @type Object
315          */
316         this.groups = {};
317 
318         /**
319          * Stores all the objects that are currently running an animation.
320          * @type Object
321          */
322         this.animationObjects = {};
323 
324         /**
325          * An associative array containing all highlighted elements belonging to the board.
326          * @type Object
327          */
328         this.highlightedObjects = {};
329 
330         /**
331          * Number of objects ever created on this board. This includes every object, even invisible and deleted ones.
332          * @type Number
333          */
334         this.numObjects = 0;
335 
336         /**
337          * 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.
338          * @type Object
339          */
340         this.elementsByName = {};
341 
342         /**
343          * The board mode the board is currently in. Possible values are
344          * <ul>
345          * <li>JXG.Board.BOARD_MODE_NONE</li>
346          * <li>JXG.Board.BOARD_MODE_DRAG</li>
347          * <li>JXG.Board.BOARD_MODE_MOVE_ORIGIN</li>
348          * </ul>
349          * @type Number
350          */
351         this.mode = this.BOARD_MODE_NONE;
352 
353         /**
354          * The update quality of the board. In most cases this is set to {@link JXG.Board#BOARD_QUALITY_HIGH}.
355          * If {@link JXG.Board#mode} equals {@link JXG.Board#BOARD_MODE_DRAG} this is set to
356          * {@link JXG.Board#BOARD_QUALITY_LOW} to speed up the update process by e.g. reducing the number of
357          * evaluation points when plotting functions. Possible values are
358          * <ul>
359          * <li>BOARD_QUALITY_LOW</li>
360          * <li>BOARD_QUALITY_HIGH</li>
361          * </ul>
362          * @type Number
363          * @see JXG.Board#mode
364          */
365         this.updateQuality = this.BOARD_QUALITY_HIGH;
366 
367         /**
368          * If true updates are skipped.
369          * @type Boolean
370          */
371         this.isSuspendedRedraw = false;
372 
373         this.calculateSnapSizes();
374 
375         /**
376          * The distance from the mouse to the dragged object in x direction when the user clicked the mouse button.
377          * @type Number
378          * @see JXG.Board#drag_dy
379          * @see JXG.Board#drag_obj
380          */
381         this.drag_dx = 0;
382 
383         /**
384          * The distance from the mouse to the dragged object in y direction when the user clicked the mouse button.
385          * @type Number
386          * @see JXG.Board#drag_dx
387          * @see JXG.Board#drag_obj
388          */
389         this.drag_dy = 0;
390 
391         /**
392          * The last position where a drag event has been fired.
393          * @type Array
394          * @see JXG.Board#moveObject
395          */
396         this.drag_position = [0, 0];
397 
398         /**
399          * References to the object that is dragged with the mouse on the board.
400          * @type JXG.GeometryElement
401          * @see JXG.Board#touches
402          */
403         this.mouse = {};
404 
405         /**
406          * Keeps track on touched elements, like {@link JXG.Board#mouse} does for mouse events.
407          * @type Array
408          * @see JXG.Board#mouse
409          */
410         this.touches = [];
411 
412         /**
413          * A string containing the XML text of the construction.
414          * This is set in {@link JXG.FileReader.parseString}.
415          * Only useful if a construction is read from a GEONExT-, Intergeo-, Geogebra-, or Cinderella-File.
416          * @type String
417          */
418         this.xmlString = '';
419 
420         /**
421          * Cached result of getCoordsTopLeftCorner for touch/mouseMove-Events to save some DOM operations.
422          * @type Array
423          */
424         this.cPos = [];
425 
426         /**
427          * Contains the last time (epoch, msec) since the last touchMove event which was not thrown away or since
428          * touchStart because Android's Webkit browser fires too much of them.
429          * @type Number
430          */
431         this.touchMoveLast = 0;
432 
433         /**
434          * Contains the pointerId of the last touchMove event which was not thrown away or since
435          * touchStart because Android's Webkit browser fires too much of them.
436          * @type Number
437          */
438          this.touchMoveLastId = Infinity;
439 
440         /**
441          * Contains the last time (epoch, msec) since the last getCoordsTopLeftCorner call which was not thrown away.
442          * @type Number
443          */
444         this.positionAccessLast = 0;
445 
446         /**
447          * Collects all elements that triggered a mouse down event.
448          * @type Array
449          */
450         this.downObjects = [];
451 
452         if (this.attr.showcopyright) {
453             this.renderer.displayCopyright(Const.licenseText, parseInt(this.options.text.fontSize, 10));
454         }
455 
456         /**
457          * Full updates are needed after zoom and axis translates. This saves some time during an update.
458          * @default false
459          * @type Boolean
460          */
461         this.needsFullUpdate = false;
462 
463         /**
464          * If reducedUpdate is set to true then only the dragged element and few (e.g. 2) following
465          * elements are updated during mouse move. On mouse up the whole construction is
466          * updated. This enables us to be fast even on very slow devices.
467          * @type Boolean
468          * @default false
469          */
470         this.reducedUpdate = false;
471 
472         /**
473          * The current color blindness deficiency is stored in this property. If color blindness is not emulated
474          * at the moment, it's value is 'none'.
475          */
476         this.currentCBDef = 'none';
477 
478         /**
479          * If GEONExT constructions are displayed, then this property should be set to true.
480          * At the moment there should be no difference. But this may change.
481          * This is set in {@link JXG.GeonextReader.readGeonext}.
482          * @type Boolean
483          * @default false
484          * @see JXG.GeonextReader.readGeonext
485          */
486         this.geonextCompatibilityMode = false;
487 
488         if (this.options.text.useASCIIMathML && translateASCIIMath) {
489             init();
490         } else {
491             this.options.text.useASCIIMathML = false;
492         }
493 
494         /**
495          * A flag which tells if the board registers mouse events.
496          * @type Boolean
497          * @default false
498          */
499         this.hasMouseHandlers = false;
500 
501         /**
502          * A flag which tells if the board registers touch events.
503          * @type Boolean
504          * @default false
505          */
506         this.hasTouchHandlers = false;
507 
508         /**
509          * A flag which stores if the board registered pointer events.
510          * @type Boolean
511          * @default false
512          */
513         this.hasPointerHandlers = false;
514 
515         /**
516          * A flag which tells if the board the JXG.Board#mouseUpListener is currently registered.
517          * @type Boolean
518          * @default false
519          */
520         this.hasMouseUp = false;
521 
522         /**
523          * A flag which tells if the board the JXG.Board#touchEndListener is currently registered.
524          * @type Boolean
525          * @default false
526          */
527         this.hasTouchEnd = false;
528 
529         /**
530          * A flag which tells us if the board has a pointerUp event registered at the moment.
531          * @type Boolean
532          * @default false
533          */
534         this.hasPointerUp = false;
535 
536         /**
537          * Offset for large coords elements like images
538          * @type Array
539          * @private
540          * @default [0, 0]
541          */
542         this._drag_offset = [0, 0];
543 
544         /**
545          * Stores the input device used in the last down or move event.
546          * @type String
547          * @private
548          * @default 'mouse'
549          */
550         this._inputDevice = 'mouse';
551 
552         /**
553          * Keeps a list of pointer devices which are currently touching the screen.
554          * @type Array
555          * @private
556          */
557         this._board_touches = [];
558 
559         /**
560          * A flag which tells us if the board is in the selecting mode
561          * @type Boolean
562          * @default false
563          */
564         this.selectingMode = false;
565 
566         /**
567          * A flag which tells us if the user is selecting
568          * @type Boolean
569          * @default false
570          */
571         this.isSelecting = false;
572 
573         /**
574          * A flag which tells us if the user is scrolling the viewport
575          * @type Boolean
576          * @private
577          * @default false
578          * @see JXG.Board#scrollListener
579          */
580         this._isScrolling = false;
581 
582         /**
583          * A flag which tells us if a resize is in process
584          * @type Boolean
585          * @private
586          * @default false
587          * @see JXG.Board#resizeListener
588          */
589         this._isResizing = false;
590 
591         /**
592          * A bounding box for the selection
593          * @type Array
594          * @default [ [0,0], [0,0] ]
595          */
596         this.selectingBox = [[0, 0], [0, 0]];
597 
598         this.mathLib = Math;        // Math or JXG.Math.IntervalArithmetic
599         this.mathLibJXG = JXG.Math; // JXG.Math or JXG.Math.IntervalArithmetic
600 
601         if (this.attr.registerevents) {
602             this.addEventHandlers();
603         }
604 
605         this.methodMap = {
606             update: 'update',
607             fullUpdate: 'fullUpdate',
608             on: 'on',
609             off: 'off',
610             trigger: 'trigger',
611             setView: 'setBoundingBox',
612             setBoundingBox: 'setBoundingBox',
613             migratePoint: 'migratePoint',
614             colorblind: 'emulateColorblindness',
615             suspendUpdate: 'suspendUpdate',
616             unsuspendUpdate: 'unsuspendUpdate',
617             clearTraces: 'clearTraces',
618             left: 'clickLeftArrow',
619             right: 'clickRightArrow',
620             up: 'clickUpArrow',
621             down: 'clickDownArrow',
622             zoomIn: 'zoomIn',
623             zoomOut: 'zoomOut',
624             zoom100: 'zoom100',
625             zoomElements: 'zoomElements',
626             remove: 'removeObject',
627             removeObject: 'removeObject'
628         };
629     };
630 
631     JXG.extend(JXG.Board.prototype, /** @lends JXG.Board.prototype */ {
632 
633         /**
634          * Generates an unique name for the given object. The result depends on the objects type, if the
635          * object is a {@link JXG.Point}, capital characters are used, if it is of type {@link JXG.Line}
636          * only lower case characters are used. If object is of type {@link JXG.Polygon}, a bunch of lower
637          * case characters prefixed with P_ are used. If object is of type {@link JXG.Circle} the name is
638          * generated using lower case characters. prefixed with k_ is used. In any other case, lower case
639          * chars prefixed with s_ is used.
640          * @param {Object} object Reference of an JXG.GeometryElement that is to be named.
641          * @returns {String} Unique name for the object.
642          */
643         generateName: function (object) {
644             var possibleNames, i,
645                 maxNameLength = this.attr.maxnamelength,
646                 pre = '',
647                 post = '',
648                 indices = [],
649                 name = '';
650 
651             if (object.type === Const.OBJECT_TYPE_TICKS) {
652                 return '';
653             }
654 
655             if (Type.isPoint(object)) {
656                 // points have capital letters
657                 possibleNames = ['', 'A', 'B', 'C', 'D', 'E', 'F', 'G', 'H', 'I', 'J', 'K', 'L', 'M', 'N', 'O',
658                     'P', 'Q', 'R', 'S', 'T', 'U', 'V', 'W', 'X', 'Y', 'Z'];
659             } else if (object.type === Const.OBJECT_TYPE_ANGLE) {
660                 possibleNames = ['', 'α', 'β', 'γ', 'δ', 'ε', 'ζ', 'η', 'θ',
661                     'ι', 'κ', 'λ', 'μ', 'ν', 'ξ', 'ο', 'π', 'ρ',
662                     'σ', 'τ', 'υ', 'φ', 'χ', 'ψ', 'ω'];
663             } else {
664                 // all other elements get lowercase labels
665                 possibleNames = ['', 'a', 'b', 'c', 'd', 'e', 'f', 'g', 'h', 'i', 'j', 'k', 'l', 'm', 'n', 'o',
666                     'p', 'q', 'r', 's', 't', 'u', 'v', 'w', 'x', 'y', 'z'];
667             }
668 
669             if (!Type.isPoint(object) &&
670                     object.elementClass !== Const.OBJECT_CLASS_LINE &&
671                     object.type !== Const.OBJECT_TYPE_ANGLE) {
672                 if (object.type === Const.OBJECT_TYPE_POLYGON) {
673                     pre = 'P_{';
674                 } else if (object.elementClass === Const.OBJECT_CLASS_CIRCLE) {
675                     pre = 'k_{';
676                 } else if (object.elementClass === Const.OBJECT_CLASS_TEXT) {
677                     pre = 't_{';
678                 } else {
679                     pre = 's_{';
680                 }
681                 post = '}';
682             }
683 
684             for (i = 0; i < maxNameLength; i++) {
685                 indices[i] = 0;
686             }
687 
688             while (indices[maxNameLength - 1] < possibleNames.length) {
689                 for (indices[0] = 1; indices[0] < possibleNames.length; indices[0]++) {
690                     name = pre;
691 
692                     for (i = maxNameLength; i > 0; i--) {
693                         name += possibleNames[indices[i - 1]];
694                     }
695 
696                     if (!Type.exists(this.elementsByName[name + post])) {
697                         return name + post;
698                     }
699 
700                 }
701                 indices[0] = possibleNames.length;
702 
703                 for (i = 1; i < maxNameLength; i++) {
704                     if (indices[i - 1] === possibleNames.length) {
705                         indices[i - 1] = 1;
706                         indices[i] += 1;
707                     }
708                 }
709             }
710 
711             return '';
712         },
713 
714         /**
715          * Generates unique id for a board. The result is randomly generated and prefixed with 'jxgBoard'.
716          * @returns {String} Unique id for a board.
717          */
718         generateId: function () {
719             var r = 1;
720 
721             // as long as we don't have a unique id generate a new one
722             while (Type.exists(JXG.boards['jxgBoard' + r])) {
723                 r = Math.round(Math.random() * 65535);
724             }
725 
726             return ('jxgBoard' + r);
727         },
728 
729         /**
730          * Composes an id for an element. If the ID is empty ('' or null) a new ID is generated, depending on the
731          * object type. As a side effect {@link JXG.Board#numObjects}
732          * is updated.
733          * @param {Object} obj Reference of an geometry object that needs an id.
734          * @param {Number} type Type of the object.
735          * @returns {String} Unique id for an element.
736          */
737         setId: function (obj, type) {
738             var randomNumber,
739                 num = this.numObjects,
740                 elId = obj.id;
741 
742             this.numObjects += 1;
743 
744             // If no id is provided or id is empty string, a new one is chosen
745             if (elId === '' || !Type.exists(elId)) {
746                 elId = this.id + type + num;
747                 while (Type.exists(this.objects[elId])) {
748                     randomNumber = Math.round(Math.random() * 65535);
749                     elId = this.id + type + num + '-' + randomNumber;
750                 }
751             }
752 
753             obj.id = elId;
754             this.objects[elId] = obj;
755             obj._pos = this.objectsList.length;
756             this.objectsList[this.objectsList.length] = obj;
757 
758             return elId;
759         },
760 
761         /**
762          * After construction of the object the visibility is set
763          * and the label is constructed if necessary.
764          * @param {Object} obj The object to add.
765          */
766         finalizeAdding: function (obj) {
767             if (Type.evaluate(obj.visProp.visible) === false) {
768                 this.renderer.display(obj, false);
769             }
770         },
771 
772         finalizeLabel: function (obj) {
773             if (obj.hasLabel &&
774                 !Type.evaluate(obj.label.visProp.islabel) &&
775                 Type.evaluate(obj.label.visProp.visible) === false) {
776                 this.renderer.display(obj.label, false);
777             }
778         },
779 
780         /**********************************************************
781          *
782          * Event Handler helpers
783          *
784          **********************************************************/
785 
786         /**
787          * Returns false if the event has been triggered faster than the maximum frame rate.
788          *
789          * @param {Event} evt Event object given by the browser (unused)
790          * @returns {Boolean} If the event has been triggered faster than the maximum frame rate, false is returned.
791          * @private
792          * @see JXG.Board#pointerMoveListener
793          * @see JXG.Board#touchMoveListener
794          * @see JXG.Board#mouseMoveListener
795          */
796         checkFrameRate: function(evt) {
797             var handleEvt = false,
798                 time = new Date().getTime();
799 
800             if (Type.exists(evt.pointerId) && this.touchMoveLastId !== evt.pointerId) {
801                 handleEvt = true;
802                 this.touchMoveLastId = evt.pointerId;
803             }
804             if (!handleEvt && (time - this.touchMoveLast) * this.attr.maxframerate >= 1000) {
805                 handleEvt = true;
806             }
807             if (handleEvt) {
808                 this.touchMoveLast = time;
809             }
810             return handleEvt;
811         },
812 
813         /**
814          * Calculates mouse coordinates relative to the boards container.
815          * @returns {Array} Array of coordinates relative the boards container top left corner.
816          */
817         getCoordsTopLeftCorner: function () {
818             var cPos, doc, crect,
819                 docElement = this.document.documentElement || this.document.body.parentNode,
820                 docBody = this.document.body,
821                 container = this.containerObj,
822                 // viewport, content,
823                 zoom, o;
824 
825             /**
826              * During drags and origin moves the container element is usually not changed.
827              * Check the position of the upper left corner at most every 1000 msecs
828              */
829             if (this.cPos.length > 0 &&
830                     (this.mode === this.BOARD_MODE_DRAG || this.mode === this.BOARD_MODE_MOVE_ORIGIN ||
831                     (new Date()).getTime() - this.positionAccessLast < 1000)) {
832                 return this.cPos;
833             }
834             this.positionAccessLast = (new Date()).getTime();
835 
836             // Check if getBoundingClientRect exists. If so, use this as this covers *everything*
837             // even CSS3D transformations etc.
838             // Supported by all browsers but IE 6, 7.
839 
840             if (container.getBoundingClientRect) {
841                 crect = container.getBoundingClientRect();
842 
843 
844                 zoom = 1.0;
845                 // Recursively search for zoom style entries.
846                 // This is necessary for reveal.js on webkit.
847                 // It fails if the user does zooming
848                 o = container;
849                 while (o && Type.exists(o.parentNode)) {
850                     if (Type.exists(o.style) && Type.exists(o.style.zoom) && o.style.zoom !== '') {
851                         zoom *= parseFloat(o.style.zoom);
852                     }
853                     o = o.parentNode;
854                 }
855                 cPos = [crect.left * zoom, crect.top * zoom];
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                 this.cPos = cPos.slice();
869                 return this.cPos;
870             }
871 
872             //
873             //  OLD CODE
874             //  IE 6-7 only:
875             //
876             cPos = Env.getOffset(container);
877             doc = this.document.documentElement.ownerDocument;
878 
879             if (!this.containerObj.currentStyle && doc.defaultView) {     // Non IE
880                 // this is for hacks like this one used in wordpress for the admin bar:
881                 // html { margin-top: 28px }
882                 // seems like it doesn't work in IE
883 
884                 cPos[0] += Env.getProp(docElement, 'margin-left');
885                 cPos[1] += Env.getProp(docElement, 'margin-top');
886 
887                 cPos[0] += Env.getProp(docElement, 'border-left-width');
888                 cPos[1] += Env.getProp(docElement, 'border-top-width');
889 
890                 cPos[0] += Env.getProp(docElement, 'padding-left');
891                 cPos[1] += Env.getProp(docElement, 'padding-top');
892             }
893 
894             if (docBody) {
895                 cPos[0] += Env.getProp(docBody, 'left');
896                 cPos[1] += Env.getProp(docBody, 'top');
897             }
898 
899             // Google Translate offers widgets for web authors. These widgets apparently tamper with the clientX
900             // and clientY coordinates of the mouse events. The minified sources seem to be the only publicly
901             // available version so we're doing it the hacky way: Add a fixed offset.


904                 cPos[0] += 10;
905                 cPos[1] += 25;
906             }
907 
908             // add border width
909             cPos[0] += Env.getProp(container, 'border-left-width');
910             cPos[1] += Env.getProp(container, 'border-top-width');
911 
912             // vml seems to ignore paddings
913             if (this.renderer.type !== 'vml') {
914                 // add padding
915                 cPos[0] += Env.getProp(container, 'padding-left');
916                 cPos[1] += Env.getProp(container, 'padding-top');
917             }
918 
919             cPos[0] += this.attr.offsetx;
920             cPos[1] += this.attr.offsety;
921 
922             this.cPos = cPos.slice();
923             return this.cPos;
924         },
925 
926         /**
927          * Get the position of the mouse in screen coordinates, relative to the upper left corner
928          * of the host tag.
929          * @param {Event} e Event object given by the browser.
930          * @param {Number} [i] Only use in case of touch events. This determines which finger to use and should not be set
931          * for mouseevents.
932          * @returns {Array} Contains the mouse coordinates in screen coordinates, ready for {@link JXG.Coords}
933          */
934         getMousePosition: function (e, i) {
935             var cPos = this.getCoordsTopLeftCorner(),
936                 absPos,
937                 v;
938 
939             // Position of cursor using clientX/Y
940             absPos = Env.getPosition(e, i, this.document);
941 
942             /**
943              * In case there has been no down event before.
944              */
945             if (!Type.exists(this.cssTransMat)) {
946                 this.updateCSSTransforms();
947             }
948             // Position relative to the top left corner
949             v = [1, absPos[0] - cPos[0], absPos[1] - cPos[1]];
950             v = Mat.matVecMult(this.cssTransMat, v);
951             v[1] /= v[0];
952             v[2] /= v[0];
953             return [v[1], v[2]];
954 
955             // Method without CSS transformation
956             /*
957              return [absPos[0] - cPos[0], absPos[1] - cPos[1]];
958              */
959         },
960 
961         /**
962          * Initiate moving the origin. This is used in mouseDown and touchStart listeners.
963          * @param {Number} x Current mouse/touch coordinates
964          * @param {Number} y Current mouse/touch coordinates
965          */
966         initMoveOrigin: function (x, y) {
967             this.drag_dx = x - this.origin.scrCoords[1];
968             this.drag_dy = y - this.origin.scrCoords[2];
969 
970             this.mode = this.BOARD_MODE_MOVE_ORIGIN;
971             this.updateQuality = this.BOARD_QUALITY_LOW;
972         },
973 
974         /**
975          * Collects all elements below the current mouse pointer and fulfilling the following constraints:
976          * <ul><li>isDraggable</li><li>visible</li><li>not fixed</li><li>not frozen</li></ul>
977          * @param {Number} x Current mouse/touch coordinates
978          * @param {Number} y current mouse/touch coordinates
979          * @param {Object} evt An event object
980          * @param {String} type What type of event? 'touch', 'mouse' or 'pen'.
981          * @returns {Array} A list of geometric elements.
982          */
983         initMoveObject: function (x, y, evt, type) {
984             var pEl,
985                 el,
986                 collect = [],
987                 offset = [],
988                 haspoint,
989                 len = this.objectsList.length,
990                 dragEl = {visProp: {layer: -10000}};
991 
992             //for (el in this.objects) {
993             for (el = 0; el < len; el++) {
994                 pEl = this.objectsList[el];
995                 haspoint = pEl.hasPoint && pEl.hasPoint(x, y);
996 
997                 if (pEl.visPropCalc.visible && haspoint) {
998                     pEl.triggerEventHandlers([type + 'down', 'down'], [evt]);
999                     this.downObjects.push(pEl);
1000                 }
1001 
1002                 if (haspoint &&
1003                     pEl.isDraggable &&
1004                     pEl.visPropCalc.visible &&
1005                     ((this.geonextCompatibilityMode &&
1006                         (Type.isPoint(pEl) ||
1007                          pEl.elementClass === Const.OBJECT_CLASS_TEXT)
1008                      ) ||
1009                      !this.geonextCompatibilityMode
1010                     ) &&
1011                     !Type.evaluate(pEl.visProp.fixed)
1012                     /*(!pEl.visProp.frozen) &&*/
1013                     ) {
1014 
1015                     // Elements in the highest layer get priority.
1016                     if (pEl.visProp.layer > dragEl.visProp.layer ||
1017                             (pEl.visProp.layer === dragEl.visProp.layer &&
1018                              pEl.lastDragTime.getTime() >= dragEl.lastDragTime.getTime()
1019                             )) {
1020                         // If an element and its label have the focus
1021                         // simultaneously, the element is taken.
1022                         // This only works if we assume that every browser runs
1023                         // through this.objects in the right order, i.e. an element A
1024                         // added before element B turns up here before B does.
1025                         if (!this.attr.ignorelabels ||
1026                             (!Type.exists(dragEl.label) || pEl !== dragEl.label)) {
1027                             dragEl = pEl;
1028                             collect.push(dragEl);
1029 
1030                             // Save offset for large coords elements.
1031                             if (Type.exists(dragEl.coords)) {
1032                                 offset.push(Statistics.subtract(dragEl.coords.scrCoords.slice(1), [x, y]));
1033                             } else {
1034                                 offset.push([0, 0]);
1035                             }
1036 
1037                             // we can't drop out of this loop because of the event handling system
1038                             //if (this.attr.takefirst) {
1039                             //    return collect;
1040                             //}
1041                         }
1042                     }
1043                 }
1044             }
1045 
1046             if (this.attr.drag.enabled && collect.length > 0) {
1047                 this.mode = this.BOARD_MODE_DRAG;
1048             }
1049 
1050             // A one-element array is returned.
1051             if (this.attr.takefirst) {
1052                 collect.length = 1;
1053                 this._drag_offset = offset[0];
1054             } else {
1055                 collect = collect.slice(-1);
1056                 this._drag_offset = offset[offset.length - 1];
1057             }
1058 
1059             if (!this._drag_offset) {
1060                 this._drag_offset = [0, 0];
1061             }
1062 
1063             // Move drag element to the top of the layer
1064             if (this.renderer.type === 'svg' &&
1065                 Type.exists(collect[0]) &&
1066                 Type.evaluate(collect[0].visProp.dragtotopoflayer) &&
1067                 collect.length === 1 &&
1068                 Type.exists(collect[0].rendNode)) {
1069 
1070                 collect[0].rendNode.parentNode.appendChild(collect[0].rendNode);
1071             }
1072 
1073             // Init rotation angle and scale factor for two finger movements
1074             this.previousRotation = 0.0;
1075             this.previousScale = 1.0;
1076 
1077             if (collect.length >= 1) {
1078                 collect[0].highlight(true);
1079                 this.triggerEventHandlers(['mousehit', 'hit'], [evt, collect[0]]);
1080             }
1081 
1082             return collect;
1083         },
1084 
1085         /**
1086          * Moves an object.
1087          * @param {Number} x Coordinate
1088          * @param {Number} y Coordinate
1089          * @param {Object} o The touch object that is dragged: {JXG.Board#mouse} or {JXG.Board#touches}.
1090          * @param {Object} evt The event object.
1091          * @param {String} type Mouse or touch event?
1092          */
1093         moveObject: function (x, y, o, evt, type) {
1094             var newPos = new Coords(Const.COORDS_BY_SCREEN, this.getScrCoordsOfMouse(x, y), this),
1095                 drag,
1096                 dragScrCoords, newDragScrCoords;
1097 
1098             if (!(o && o.obj)) {
1099                 return;
1100             }
1101             drag = o.obj;
1102 
1103             // Save updates for very small movements of coordsElements, see below
1104             if (drag.coords) {
1105                 dragScrCoords = drag.coords.scrCoords.slice();
1106             }
1107 
1108             /*
1109              * Save the position.
1110              */
1111             this.drag_position = [newPos.scrCoords[1], newPos.scrCoords[2]];
1112             this.drag_position = Statistics.add(this.drag_position, this._drag_offset);
1113             //
1114             // We have to distinguish between CoordsElements and other elements like lines.
1115             // The latter need the difference between two move events.
1116             if (Type.exists(drag.coords)) {
1117                 drag.setPositionDirectly(Const.COORDS_BY_SCREEN, this.drag_position);
1118             } else {
1119                 this.displayInfobox(false);
1120                                     // Hide infobox in case the user has touched an intersection point
1121                                     // and drags the underlying line now.
1122 
1123                 if (!isNaN(o.targets[0].Xprev + o.targets[0].Yprev)) {
1124                     drag.setPositionDirectly(Const.COORDS_BY_SCREEN,
1125                         [newPos.scrCoords[1], newPos.scrCoords[2]],
1126                         [o.targets[0].Xprev, o.targets[0].Yprev]
1127                         );
1128                 }
1129                 // Remember the actual position for the next move event. Then we are able to
1130                 // compute the difference vector.
1131                 o.targets[0].Xprev = newPos.scrCoords[1];
1132                 o.targets[0].Yprev = newPos.scrCoords[2];
1133             }
1134             // This may be necessary for some gliders and labels
1135             if (Type.exists(drag.coords)) {
1136                 drag.prepareUpdate().update(false).updateRenderer();
1137                 this.updateInfobox(drag);
1138                 drag.prepareUpdate().update(true).updateRenderer();
1139             }
1140 
1141             if (drag.coords) {
1142                 newDragScrCoords = drag.coords.scrCoords;
1143             }
1144             // No updates for very small movements of coordsElements
1145             if (!drag.coords ||
1146                 dragScrCoords[1] !== newDragScrCoords[1] ||
1147                 dragScrCoords[2] !== newDragScrCoords[2]) {
1148 
1149                 drag.triggerEventHandlers([type + 'drag', 'drag'], [evt]);
1150 
1151                 this.update();
1152             }
1153             drag.highlight(true);
1154             this.triggerEventHandlers(['mousehit', 'hit'], [evt, drag]);
1155 
1156             drag.lastDragTime = new Date();
1157         },
1158 
1159         /**
1160          * Moves elements in multitouch mode.
1161          * @param {Array} p1 x,y coordinates of first touch
1162          * @param {Array} p2 x,y coordinates of second touch
1163          * @param {Object} o The touch object that is dragged: {JXG.Board#touches}.
1164          * @param {Object} evt The event object that lead to this movement.
1165          */
1166         twoFingerMove: function (o, id, evt) {
1167             var drag;
1168 
1169             if (Type.exists(o) && Type.exists(o.obj)) {
1170                 drag = o.obj;
1171             } else {
1172                 return;
1173             }
1174 
1175             if (drag.elementClass === Const.OBJECT_CLASS_LINE ||
1176                 drag.type === Const.OBJECT_TYPE_POLYGON) {
1177                 this.twoFingerTouchObject(o.targets, drag, id);
1178             } else if (drag.elementClass === Const.OBJECT_CLASS_CIRCLE) {
1179                 this.twoFingerTouchCircle(o.targets, drag, id);
1180             }
1181 
1182             if (evt) {
1183                 drag.triggerEventHandlers(['touchdrag', 'drag'], [evt]);
1184             }
1185         },
1186 
1187         /**
1188          * Moves, rotates and scales a line or polygon with two fingers.
1189          * @param {Array} tar Array conatining touch event objects: {JXG.Board#touches.targets}.
1190          * @param {object} drag The object that is dragged:
1191          * @param {Number} id pointerId of the event. In case of old touch event this is emulated.
1192          */
1193         twoFingerTouchObject: function (tar, drag, id) {
1194             var np, op, nd, od,
1195                 d, alpha,
1196                 S, t1, t3, t4, t5,
1197                 ar, i, len,
1198                 fixEl, moveEl, fix;
1199 
1200             if (Type.exists(tar[0]) && Type.exists(tar[1]) &&
1201                 !isNaN(tar[0].Xprev + tar[0].Yprev + tar[1].Xprev + tar[1].Yprev)) {
1202 
1203                 if (id === tar[0].num) {
1204                     fixEl  = tar[1];
1205                     moveEl = tar[0];
1206                 } else {
1207                     fixEl  = tar[0];
1208                     moveEl = tar[1];
1209                 }
1210 
1211                 fix = (new Coords(Const.COORDS_BY_SCREEN, [fixEl.Xprev, fixEl.Yprev], this)).usrCoords;
1212                 // Previous finger position
1213                 op = (new Coords(Const.COORDS_BY_SCREEN, [moveEl.Xprev, moveEl.Yprev], this)).usrCoords;
1214                 // New finger position
1215                 np = (new Coords(Const.COORDS_BY_SCREEN, [moveEl.X, moveEl.Y], this)).usrCoords;
1216 
1217                 // Old and new directions
1218                 od = Mat.crossProduct(fix, op);
1219                 nd = Mat.crossProduct(fix, np);
1220 
1221                 // Intersection between the two directions
1222                 S = Mat.crossProduct(od, nd);
1223 
1224                 // If parallel translate, otherwise rotate
1225                 if (Math.abs(S[0]) < Mat.eps) {
1226                     return;
1227                 }
1228 
1229                 alpha = Geometry.rad(op.slice(1), fix.slice(1), np.slice(1));
1230 
1231                 t1 = this.create('transform', [alpha, [fix[1], fix[2]]], {type: 'rotate'});
1232                 t1.update();
1233 
1234                 if (Type.evaluate(drag.visProp.scalable)) {
1235                     // Scale
1236                     d = Geometry.distance(np, fix) / Geometry.distance(op, fix);
1237 
1238                     t3 = this.create('transform', [-fix[1], -fix[2]], {type: 'translate'});
1239                     t4 = this.create('transform', [d, d], {type: 'scale'});
1240                     t5 = this.create('transform', [fix[1], fix[2]], {type: 'translate'});
1241                     t1.melt(t3).melt(t4).melt(t5);
1242                 }
1243 
1244                 if (drag.elementClass === Const.OBJECT_CLASS_LINE) {
1245                     ar = [];
1246                     if (drag.point1.draggable()) {
1247                         ar.push(drag.point1);
1248                     }
1249                     if (drag.point2.draggable()) {
1250                         ar.push(drag.point2);
1251                     }
1252                     t1.applyOnce(ar);
1253                 } else if (drag.type === Const.OBJECT_TYPE_POLYGON) {
1254                     ar = [];
1255                     len = drag.vertices.length - 1;
1256                     for (i = 0; i < len; ++i) {
1257                         if (drag.vertices[i].draggable()) {
1258                             ar.push(drag.vertices[i]);
1259                         }
1260                     }
1261                     t1.applyOnce(ar);
1262                 }
1263 
1264                 this.update();
1265                 drag.highlight(true);
1266             }
1267         },
1268 
1269         /*
1270          * Moves, rotates and scales a circle with two fingers.
1271          * @param {Array} tar Array conatining touch event objects: {JXG.Board#touches.targets}.
1272          * @param {object} drag The object that is dragged:
1273          * @param {Number} id pointerId of the event. In case of old touch event this is emulated.
1274          */
1275         twoFingerTouchCircle: function (tar, drag, id) {
1276             var fixEl, moveEl, np, op, fix,
1277                 d, alpha, t1, t2, t3, t4;
1278 
1279             if (drag.method === 'pointCircle' || drag.method === 'pointLine') {
1280                 return;
1281             }
1282 
1283             if (Type.exists(tar[0]) && Type.exists(tar[1]) &&
1284                 !isNaN(tar[0].Xprev + tar[0].Yprev + tar[1].Xprev + tar[1].Yprev)) {
1285 
1286                 if (id === tar[0].num) {
1287                     fixEl  = tar[1];
1288                     moveEl = tar[0];
1289                 } else {
1290                     fixEl  = tar[0];
1291                     moveEl = tar[1];
1292                 }
1293 
1294                 fix = (new Coords(Const.COORDS_BY_SCREEN, [fixEl.Xprev, fixEl.Yprev], this)).usrCoords;
1295                 // Previous finger position
1296                 op = (new Coords(Const.COORDS_BY_SCREEN, [moveEl.Xprev, moveEl.Yprev], this)).usrCoords;
1297                 // New finger position
1298                 np = (new Coords(Const.COORDS_BY_SCREEN, [moveEl.X, moveEl.Y], this)).usrCoords;
1299 
1300                 alpha = Geometry.rad(op.slice(1), fix.slice(1), np.slice(1));
1301 
1302                 // Rotate and scale by the movement of the second finger
1303                 t1 = this.create('transform', [-fix[1], -fix[2]], {type: 'translate'});
1304                 t2 = this.create('transform', [alpha], {type: 'rotate'});
1305                 t1.melt(t2);
1306                 if (Type.evaluate(drag.visProp.scalable)) {
1307                     d = Geometry.distance(fix, np) / Geometry.distance(fix, op);
1308                     t3 = this.create('transform', [d, d], {type: 'scale'});
1309                     t1.melt(t3);
1310                 }
1311                 t4 = this.create('transform', [fix[1], fix[2]], {type: 'translate'});
1312                 t1.melt(t4);
1313 
1314                 if (drag.center.draggable()) {
1315                     t1.applyOnce([drag.center]);
1316                 }
1317 
1318                 if (drag.method === 'twoPoints') {
1319                     if (drag.point2.draggable()) {
1320                         t1.applyOnce([drag.point2]);
1321                     }
1322                 } else if (drag.method === 'pointRadius') {
1323                     if (Type.isNumber(drag.updateRadius.origin)) {
1324                         drag.setRadius(drag.radius * d);
1325                     }
1326                 }
1327 
1328                 this.update(drag.center);
1329                 drag.highlight(true);
1330             }
1331         },
1332 
1333         highlightElements: function (x, y, evt, target) {
1334             var el, pEl, pId,
1335                 overObjects = {},
1336                 len = this.objectsList.length;
1337 
1338             // Elements  below the mouse pointer which are not highlighted yet will be highlighted.
1339             for (el = 0; el < len; el++) {
1340                 pEl = this.objectsList[el];
1341                 pId = pEl.id;
1342                 if (Type.exists(pEl.hasPoint) && pEl.visPropCalc.visible && pEl.hasPoint(x, y)) {
1343                     // this is required in any case because otherwise the box won't be shown until the point is dragged
1344                     this.updateInfobox(pEl);
1345 
1346                     if (!Type.exists(this.highlightedObjects[pId])) { // highlight only if not highlighted
1347                         overObjects[pId] = pEl;
1348                         pEl.highlight();
1349                         // triggers board event.
1350                         this.triggerEventHandlers(['mousehit', 'hit'], [evt, pEl, target]);
1351                     }
1352 
1353                     if (pEl.mouseover) {
1354                         pEl.triggerEventHandlers(['mousemove', 'move'], [evt]);
1355                     } else {
1356                         pEl.triggerEventHandlers(['mouseover', 'over'], [evt]);
1357                         pEl.mouseover = true;
1358                     }
1359                 }
1360             }
1361 
1362             for (el = 0; el < len; el++) {
1363                 pEl = this.objectsList[el];
1364                 pId = pEl.id;
1365                 if (pEl.mouseover) {
1366                     if (!overObjects[pId]) {
1367                         pEl.triggerEventHandlers(['mouseout', 'out'], [evt]);
1368                         pEl.mouseover = false;
1369                     }
1370                 }
1371             }
1372         },
1373 
1374         /**
1375          * Helper function which returns a reasonable starting point for the object being dragged.
1376          * Formerly known as initXYstart().
1377          * @private
1378          * @param {JXG.GeometryElement} obj The object to be dragged
1379          * @param {Array} targets Array of targets. It is changed by this function.
1380          */
1381         saveStartPos: function (obj, targets) {
1382             var xy = [], i, len;
1383 
1384             if (obj.type === Const.OBJECT_TYPE_TICKS) {
1385                 xy.push([1, NaN, NaN]);
1386             } else if (obj.elementClass === Const.OBJECT_CLASS_LINE) {
1387                 xy.push(obj.point1.coords.usrCoords);
1388                 xy.push(obj.point2.coords.usrCoords);
1389             } else if (obj.elementClass === Const.OBJECT_CLASS_CIRCLE) {
1390                 xy.push(obj.center.coords.usrCoords);
1391                 if (obj.method === 'twoPoints') {
1392                     xy.push(obj.point2.coords.usrCoords);
1393                 }
1394             } else if (obj.type === Const.OBJECT_TYPE_POLYGON) {
1395                 len = obj.vertices.length - 1;
1396                 for (i = 0; i < len; i++) {
1397                     xy.push(obj.vertices[i].coords.usrCoords);
1398                 }
1399             } else if (obj.type === Const.OBJECT_TYPE_SECTOR) {
1400                 xy.push(obj.point1.coords.usrCoords);
1401                 xy.push(obj.point2.coords.usrCoords);
1402                 xy.push(obj.point3.coords.usrCoords);
1403             } else if (Type.isPoint(obj) || obj.type === Const.OBJECT_TYPE_GLIDER) {
1404                 xy.push(obj.coords.usrCoords);
1405             } else if (obj.elementClass === Const.OBJECT_CLASS_CURVE) {
1406                 // if (Type.exists(obj.parents)) {
1407                 //     len = obj.parents.length;
1408                 //     if (len > 0) {
1409                 //         for (i = 0; i < len; i++) {
1410                 //             xy.push(this.select(obj.parents[i]).coords.usrCoords);
1411                 //         }
1412                 //     } else
1413                 // }
1414                 if (obj.points.length > 0) {
1415                     xy.push(obj.points[0].usrCoords);
1416                 }
1417             } else {
1418                 try {
1419                     xy.push(obj.coords.usrCoords);
1420                 } catch (e) {
1421                     JXG.debug('JSXGraph+ saveStartPos: obj.coords.usrCoords not available: ' + e);
1422                 }
1423             }
1424 
1425             len = xy.length;
1426             for (i = 0; i < len; i++) {
1427                 targets.Zstart.push(xy[i][0]);
1428                 targets.Xstart.push(xy[i][1]);
1429                 targets.Ystart.push(xy[i][2]);
1430             }
1431         },
1432 
1433         mouseOriginMoveStart: function (evt) {
1434             var r, pos;
1435 
1436             r = this._isRequiredKeyPressed(evt, 'pan');
1437             if (r) {
1438                 pos = this.getMousePosition(evt);
1439                 this.initMoveOrigin(pos[0], pos[1]);
1440             }
1441 
1442             return r;
1443         },
1444 
1445         mouseOriginMove: function (evt) {
1446             var r = (this.mode === this.BOARD_MODE_MOVE_ORIGIN),
1447                 pos;
1448 
1449             if (r) {
1450                 pos = this.getMousePosition(evt);
1451                 this.moveOrigin(pos[0], pos[1], true);
1452             }
1453 
1454             return r;
1455         },
1456 
1457         /**
1458          * Start moving the origin with one finger.
1459          * @private
1460          * @param  {Object} evt Event from touchStartListener
1461          * @return {Boolean}   returns if the origin is moved.
1462          */
1463         touchStartMoveOriginOneFinger: function (evt) {
1464             var touches = evt[JXG.touchProperty],
1465                 conditions, pos;
1466 
1467             conditions = this.attr.pan.enabled &&
1468                 !this.attr.pan.needtwofingers &&
1469                 touches.length === 1;
1470 
1471             if (conditions) {
1472                 pos = this.getMousePosition(evt, 0);
1473                 this.initMoveOrigin(pos[0], pos[1]);
1474             }
1475 
1476             return conditions;
1477         },
1478 
1479         /**
1480          * Move the origin with one finger
1481          * @private
1482          * @param  {Object} evt Event from touchMoveListener
1483          * @return {Boolean}     returns if the origin is moved.
1484          */
1485         touchOriginMove: function (evt) {
1486             var r = (this.mode === this.BOARD_MODE_MOVE_ORIGIN),
1487                 pos;
1488 
1489             if (r) {
1490                 pos = this.getMousePosition(evt, 0);
1491                 this.moveOrigin(pos[0], pos[1], true);
1492             }
1493 
1494             return r;
1495         },
1496 
1497         /**
1498          * Stop moving the origin with one finger
1499          * @return {null} null
1500          * @private
1501          */
1502         originMoveEnd: function () {
1503             this.updateQuality = this.BOARD_QUALITY_HIGH;
1504             this.mode = this.BOARD_MODE_NONE;
1505         },
1506 
1507         /**********************************************************
1508          *
1509          * Event Handler
1510          *
1511          **********************************************************/
1512 
1513         /**
1514          *  Add all possible event handlers to the board object
1515          */
1516         addEventHandlers: function () {
1517             if (Env.supportsPointerEvents()) {
1518                 this.addPointerEventHandlers();
1519             } else {
1520                 this.addMouseEventHandlers();
1521                 this.addTouchEventHandlers();
1522             }
1523 
1524             // This one produces errors on IE
1525             //Env.addEvent(this.containerObj, 'contextmenu', function (e) { e.preventDefault(); return false;}, this);
1526             // This one works on IE, Firefox and Chromium with default configurations. On some Safari
1527             // or Opera versions the user must explicitly allow the deactivation of the context menu.
1528             if (this.containerObj !== null) {
1529                 this.containerObj.oncontextmenu = function (e) {
1530                     if (Type.exists(e)) {
1531                         e.preventDefault();
1532                     }
1533                     return false;
1534                 };
1535             }
1536 
1537             this.addFullscreenEventHandlers();
1538             this.addKeyboardEventHandlers();
1539 
1540             if (Env.isBrowser) {
1541                 try {
1542                     // resizeObserver: triggered if size of the JSXGraph div changes.
1543                     this.startResizeObserver();
1544                 } catch (err) {
1545                     // resize event: triggered if size of window changes
1546                     Env.addEvent(window, 'resize', this.resizeListener, this);
1547                     // intersectionObserver: triggered if JSXGraph becomes visible.
1548                     this.startIntersectionObserver();
1549                 }
1550                 // Scroll event: needs to be captured since on mobile devices
1551                 // sometimes a header bar is displayed / hidden, which triggers a
1552                 // resize event.
1553                 Env.addEvent(window, 'scroll', this.scrollListener, this);
1554             }
1555         },
1556 
1557         /**
1558          * Remove all event handlers from the board object
1559          */
1560         removeEventHandlers: function () {
1561             this.removeMouseEventHandlers();
1562             this.removeTouchEventHandlers();
1563             this.removePointerEventHandlers();
1564 
1565             this.removeFullscreenEventHandlers();
1566             this.removeKeyboardEventHandlers();
1567             if (Env.isBrowser) {
1568                 if (Type.exists(this.resizeObserver)) {
1569                     this.stopResizeObserver();
1570                 } else {
1571                     Env.removeEvent(window, 'resize', this.resizeListener, this);
1572                     this.stopIntersectionObserver();
1573                 }
1574                 Env.removeEvent(window, 'scroll', this.scrollListener, this);
1575             }
1576         },
1577 
1578         /**
1579          * Registers the MSPointer* event handlers.
1580          */
1581         addPointerEventHandlers: function () {
1582             if (!this.hasPointerHandlers && Env.isBrowser) {
1583                 var moveTarget = this.attr.movetarget || this.containerObj;
1584 
1585                 if (window.navigator.msPointerEnabled) {  // IE10-
1586                     Env.addEvent(this.containerObj, 'MSPointerDown', this.pointerDownListener, this);
1587                     Env.addEvent(moveTarget, 'MSPointerMove', this.pointerMoveListener, this);
1588                 } else {
1589                     Env.addEvent(this.containerObj, 'pointerdown', this.pointerDownListener, this);
1590                     Env.addEvent(moveTarget, 'pointermove', this.pointerMoveListener, this);
1591                 }
1592                 Env.addEvent(this.containerObj, 'mousewheel', this.mouseWheelListener, this);
1593                 Env.addEvent(this.containerObj, 'DOMMouseScroll', this.mouseWheelListener, this);
1594 
1595                 if (this.containerObj !== null) {
1596                     // This is needed for capturing touch events.
1597                     // It is also in jsxgraph.css, but one never knows...
1598                     this.containerObj.style.touchAction = 'none';
1599                 }
1600 
1601                 this.hasPointerHandlers = true;
1602             }
1603         },
1604 
1605         /**
1606          * Registers mouse move, down and wheel event handlers.
1607          */
1608         addMouseEventHandlers: function () {
1609             if (!this.hasMouseHandlers && Env.isBrowser) {
1610                 var moveTarget = this.attr.movetarget || this.containerObj;
1611 
1612                 Env.addEvent(this.containerObj, 'mousedown', this.mouseDownListener, this);
1613                 Env.addEvent(moveTarget, 'mousemove', this.mouseMoveListener, this);
1614 
1615                 Env.addEvent(this.containerObj, 'mousewheel', this.mouseWheelListener, this);
1616                 Env.addEvent(this.containerObj, 'DOMMouseScroll', this.mouseWheelListener, this);
1617 
1618                 this.hasMouseHandlers = true;
1619             }
1620         },
1621 
1622         /**
1623          * Register touch start and move and gesture start and change event handlers.
1624          * @param {Boolean} appleGestures If set to false the gesturestart and gesturechange event handlers
1625          * will not be registered.
1626          *
1627          * Since iOS 13, touch events were abandoned in favour of pointer events
1628          */
1629         addTouchEventHandlers: function (appleGestures) {
1630             if (!this.hasTouchHandlers && Env.isBrowser) {
1631                 var moveTarget = this.attr.movetarget || this.containerObj;
1632 
1633                 Env.addEvent(this.containerObj, 'touchstart', this.touchStartListener, this);
1634                 Env.addEvent(moveTarget, 'touchmove', this.touchMoveListener, this);
1635 
1636                 /*
1637                 if (!Type.exists(appleGestures) || appleGestures) {
1638                     // Gesture listener are called in touchStart and touchMove.
1639                     //Env.addEvent(this.containerObj, 'gesturestart', this.gestureStartListener, this);
1640                     //Env.addEvent(this.containerObj, 'gesturechange', this.gestureChangeListener, this);
1641                 }
1642                 */
1643 
1644                 this.hasTouchHandlers = true;
1645             }
1646         },
1647 
1648         /**
1649          * Add fullscreen events which update the CSS transformation matrix to correct
1650          * the mouse/touch/pointer positions in case of CSS transformations.
1651          */
1652         addFullscreenEventHandlers: function() {
1653             var i,
1654                 // standard/Edge, firefox, chrome/safari, IE11
1655                 events = ['fullscreenchange', 'mozfullscreenchange', 'webkitfullscreenchange', 'msfullscreenchange'],
1656                 le = events.length;
1657 
1658             if (!this.hasFullsceenEventHandlers && Env.isBrowser) {
1659                 for (i = 0; i < le; i++) {
1660                     Env.addEvent(this.document, events[i], this.fullscreenListener, this);
1661                 }
1662                 this.hasFullsceenEventHandlers = true;
1663             }
1664         },
1665 
1666         addKeyboardEventHandlers: function() {
1667             if (this.attr.keyboard.enabled && !this.hasKeyboardHandlers && Env.isBrowser) {
1668                 Env.addEvent(this.containerObj, 'keydown', this.keyDownListener, this);
1669                 Env.addEvent(this.containerObj, 'focusin', this.keyFocusInListener, this);
1670                 Env.addEvent(this.containerObj, 'focusout', this.keyFocusOutListener, this);
1671                 this.hasKeyboardHandlers = true;
1672             }
1673         },
1674 
1675         /**
1676          * Remove all registered touch event handlers.
1677          */
1678         removeKeyboardEventHandlers: function () {
1679             if (this.hasKeyboardHandlers && Env.isBrowser) {
1680                 Env.removeEvent(this.containerObj, 'keydown', this.keyDownListener, this);
1681                 Env.removeEvent(this.containerObj, 'focusin', this.keyFocusInListener, this);
1682                 Env.removeEvent(this.containerObj, 'focusout', this.keyFocusOutListener, this);
1683                 this.hasKeyboardHandlers = false;
1684             }
1685         },
1686 
1687         /**
1688          * Remove all registered event handlers regarding fullscreen mode.
1689          */
1690         removeFullscreenEventHandlers: function() {
1691             var i,
1692                 // standard/Edge, firefox, chrome/safari, IE11
1693                 events = ['fullscreenchange', 'mozfullscreenchange', 'webkitfullscreenchange', 'msfullscreenchange'],
1694                 le = events.length;
1695 
1696             if (this.hasFullsceenEventHandlers && Env.isBrowser) {
1697                 for (i = 0; i < le; i++) {
1698                     Env.removeEvent(this.document, events[i], this.fullscreenListener, this);
1699                 }
1700                 this.hasFullsceenEventHandlers = false;
1701             }
1702         },
1703 
1704         /**
1705          * Remove MSPointer* Event handlers.
1706          */
1707         removePointerEventHandlers: function () {
1708             if (this.hasPointerHandlers && Env.isBrowser) {
1709                 var moveTarget = this.attr.movetarget || this.containerObj;
1710 
1711                 if (window.navigator.msPointerEnabled) {  // IE10-
1712                     Env.removeEvent(this.containerObj, 'MSPointerDown', this.pointerDownListener, this);
1713                     Env.removeEvent(moveTarget, 'MSPointerMove', this.pointerMoveListener, this);
1714                 } else {
1715                     Env.removeEvent(this.containerObj, 'pointerdown', this.pointerDownListener, this);
1716                     Env.removeEvent(moveTarget, 'pointermove', this.pointerMoveListener, this);
1717                 }
1718 
1719                 Env.removeEvent(this.containerObj, 'mousewheel', this.mouseWheelListener, this);
1720                 Env.removeEvent(this.containerObj, 'DOMMouseScroll', this.mouseWheelListener, this);
1721 
1722                 if (this.hasPointerUp) {
1723                     if (window.navigator.msPointerEnabled) {  // IE10-
1724                         Env.removeEvent(this.document, 'MSPointerUp',   this.pointerUpListener, this);
1725                     } else {
1726                         Env.removeEvent(this.document, 'pointerup',     this.pointerUpListener, this);
1727                         Env.removeEvent(this.document, 'pointercancel', this.pointerUpListener, this);
1728                     }
1729                     this.hasPointerUp = false;
1730                 }
1731 
1732                 this.hasPointerHandlers = false;
1733             }
1734         },
1735 
1736         /**
1737          * De-register mouse event handlers.
1738          */
1739         removeMouseEventHandlers: function () {
1740             if (this.hasMouseHandlers && Env.isBrowser) {
1741                 var moveTarget = this.attr.movetarget || this.containerObj;
1742 
1743                 Env.removeEvent(this.containerObj, 'mousedown', this.mouseDownListener, this);
1744                 Env.removeEvent(moveTarget, 'mousemove', this.mouseMoveListener, this);
1745 
1746                 if (this.hasMouseUp) {
1747                     Env.removeEvent(this.document, 'mouseup', this.mouseUpListener, this);
1748                     this.hasMouseUp = false;
1749                 }
1750 
1751                 Env.removeEvent(this.containerObj, 'mousewheel', this.mouseWheelListener, this);
1752                 Env.removeEvent(this.containerObj, 'DOMMouseScroll', this.mouseWheelListener, this);
1753 
1754                 this.hasMouseHandlers = false;
1755             }
1756         },
1757 
1758         /**
1759          * Remove all registered touch event handlers.
1760          */
1761         removeTouchEventHandlers: function () {
1762             if (this.hasTouchHandlers && Env.isBrowser) {
1763                 var moveTarget = this.attr.movetarget || this.containerObj;
1764 
1765                 Env.removeEvent(this.containerObj, 'touchstart', this.touchStartListener, this);
1766                 Env.removeEvent(moveTarget, 'touchmove', this.touchMoveListener, this);
1767 
1768                 if (this.hasTouchEnd) {
1769                     Env.removeEvent(this.document, 'touchend', this.touchEndListener, this);
1770                     this.hasTouchEnd = false;
1771                 }
1772 
1773                 this.hasTouchHandlers = false;
1774             }
1775         },
1776 
1777         /**
1778          * Handler for click on left arrow in the navigation bar
1779          * @returns {JXG.Board} Reference to the board
1780          */
1781         clickLeftArrow: function () {
1782             this.moveOrigin(this.origin.scrCoords[1] + this.canvasWidth * 0.1, this.origin.scrCoords[2]);
1783             return this;
1784         },
1785 
1786         /**
1787          * Handler for click on right arrow in the navigation bar
1788          * @returns {JXG.Board} Reference to the board
1789          */
1790         clickRightArrow: function () {
1791             this.moveOrigin(this.origin.scrCoords[1] - this.canvasWidth * 0.1, this.origin.scrCoords[2]);
1792             return this;
1793         },
1794 
1795         /**
1796          * Handler for click on up arrow in the navigation bar
1797          * @returns {JXG.Board} Reference to the board
1798          */
1799         clickUpArrow: function () {
1800             this.moveOrigin(this.origin.scrCoords[1], this.origin.scrCoords[2] - this.canvasHeight * 0.1);
1801             return this;
1802         },
1803 
1804         /**
1805          * Handler for click on down arrow in the navigation bar
1806          * @returns {JXG.Board} Reference to the board
1807          */
1808         clickDownArrow: function () {
1809             this.moveOrigin(this.origin.scrCoords[1], this.origin.scrCoords[2] + this.canvasHeight * 0.1);
1810             return this;
1811         },
1812 
1813         /**
1814          * Triggered on iOS/Safari while the user inputs a gesture (e.g. pinch) and is used to zoom into the board.
1815          * Works on iOS/Safari and Android.
1816          * @param {Event} evt Browser event object
1817          * @returns {Boolean}
1818          */
1819         gestureChangeListener: function (evt) {
1820             var c,
1821                 dir1 = [],
1822                 dir2 = [],
1823                 angle,
1824                 mi = 10,
1825                 isPinch = false,
1826                 // Save zoomFactors
1827                 zx = this.attr.zoom.factorx,
1828                 zy = this.attr.zoom.factory,
1829                 factor,
1830                 dist,
1831                 dx, dy, theta, cx, cy, bound;
1832 
1833             if (this.mode !== this.BOARD_MODE_ZOOM) {
1834                 return true;
1835             }
1836             evt.preventDefault();
1837 
1838             dist = Geometry.distance([evt.touches[0].clientX, evt.touches[0].clientY],
1839                 [evt.touches[1].clientX, evt.touches[1].clientY], 2);
1840 
1841             // Android pinch to zoom
1842             // evt.scale was available in iOS touch events (pre iOS 13)
1843             // evt.scale is undefined in Android
1844             if (evt.scale === undefined) {
1845                 evt.scale = dist / this.prevDist;
1846             }
1847 
1848             if (!Type.exists(this.prevCoords)) {
1849                 return false;
1850             }
1851             // Compute the angle of the two finger directions
1852             dir1 = [evt.touches[0].clientX - this.prevCoords[0][0],
1853                     evt.touches[0].clientY - this.prevCoords[0][1]];
1854             dir2 = [evt.touches[1].clientX - this.prevCoords[1][0],
1855                     evt.touches[1].clientY - this.prevCoords[1][1]];
1856 
1857             if ((dir1[0] * dir1[0] + dir1[1] * dir1[1] < mi * mi) &&
1858                 (dir2[0] * dir2[0] + dir2[1] * dir2[1] < mi * mi)) {
1859                     return false;
1860             }
1861 
1862             angle = Geometry.rad(dir1, [0,0], dir2);
1863             if (this.isPreviousGesture !== 'pan' &&
1864                 Math.abs(angle) > Math.PI * 0.2 &&
1865                 Math.abs(angle) < Math.PI * 1.8) {
1866                 isPinch = true;
1867             }
1868 
1869             if (this.isPreviousGesture !== 'pan' && !isPinch) {
1870                 if (Math.abs(evt.scale) < 0.77 || Math.abs(evt.scale) > 1.3) {
1871                     isPinch = true;
1872                 }
1873             }
1874 
1875             factor = evt.scale / this.prevScale;
1876             this.prevScale = evt.scale;
1877             this.prevCoords = [[evt.touches[0].clientX, evt.touches[0].clientY],
1878                                [evt.touches[1].clientX, evt.touches[1].clientY]];
1879 
1880             c = new Coords(Const.COORDS_BY_SCREEN, this.getMousePosition(evt, 0), this);
1881 
1882             if (this.attr.pan.enabled &&
1883                 this.attr.pan.needtwofingers &&
1884                 !isPinch) {
1885                 // Pan detected
1886 
1887                 this.isPreviousGesture = 'pan';
1888 
1889                 this.moveOrigin(c.scrCoords[1], c.scrCoords[2], true);
1890             } else if (this.attr.zoom.enabled &&
1891                         Math.abs(factor - 1.0) < 0.5) {
1892                 // Pinch detected
1893 
1894                 if (this.attr.zoom.pinchhorizontal || this.attr.zoom.pinchvertical) {
1895                     dx = Math.abs(evt.touches[0].clientX - evt.touches[1].clientX);
1896                     dy = Math.abs(evt.touches[0].clientY - evt.touches[1].clientY);
1897                     theta = Math.abs(Math.atan2(dy, dx));
1898                     bound = Math.PI * this.attr.zoom.pinchsensitivity / 90.0;
1899                 }
1900 
1901                 if (this.attr.zoom.pinchhorizontal && theta < bound) {
1902                     this.attr.zoom.factorx = factor;
1903                     this.attr.zoom.factory = 1.0;
1904                     cx = 0;
1905                     cy = 0;
1906                 } else if (this.attr.zoom.pinchvertical && Math.abs(theta - Math.PI * 0.5) < bound) {
1907                     this.attr.zoom.factorx = 1.0;
1908                     this.attr.zoom.factory = factor;
1909                     cx = 0;
1910                     cy = 0;
1911                 } else {
1912                     this.attr.zoom.factorx = factor;
1913                     this.attr.zoom.factory = factor;
1914                     cx = c.usrCoords[1];
1915                     cy = c.usrCoords[2];
1916                 }
1917 
1918                 this.zoomIn(cx, cy);
1919 
1920                 // Restore zoomFactors
1921                 this.attr.zoom.factorx = zx;
1922                 this.attr.zoom.factory = zy;
1923             }
1924 
1925             return false;
1926         },
1927 
1928         /**
1929          * Called by iOS/Safari as soon as the user starts a gesture. Works natively on iOS/Safari,
1930          * on Android we emulate it.
1931          * @param {Event} evt
1932          * @returns {Boolean}
1933          */
1934         gestureStartListener: function (evt) {
1935             var pos;
1936 
1937             evt.preventDefault();
1938             this.prevScale = 1.0;
1939             // Android pinch to zoom
1940             this.prevDist = Geometry.distance([evt.touches[0].clientX, evt.touches[0].clientY],
1941                             [evt.touches[1].clientX, evt.touches[1].clientY], 2);
1942             this.prevCoords = [[evt.touches[0].clientX, evt.touches[0].clientY],
1943                                [evt.touches[1].clientX, evt.touches[1].clientY]];
1944             this.isPreviousGesture = 'none';
1945 
1946             // If pinch-to-zoom is interpreted as panning
1947             // we have to prepare move origin
1948             pos = this.getMousePosition(evt, 0);
1949             this.initMoveOrigin(pos[0], pos[1]);
1950 
1951             this.mode = this.BOARD_MODE_ZOOM;
1952             return false;
1953         },
1954 
1955         /**
1956          * Test if the required key combination is pressed for wheel zoom, move origin and
1957          * selection
1958          * @private
1959          * @param  {Object}  evt    Mouse or pen event
1960          * @param  {String}  action String containing the action: 'zoom', 'pan', 'selection'.
1961          * Corresponds to the attribute subobject.
1962          * @return {Boolean}        true or false.
1963          */
1964         _isRequiredKeyPressed: function (evt, action) {
1965             var obj = this.attr[action];
1966             if (!obj.enabled) {
1967                 return false;
1968             }
1969 
1970             if (((obj.needshift && evt.shiftKey) || (!obj.needshift && !evt.shiftKey)) &&
1971                 ((obj.needctrl && evt.ctrlKey) || (!obj.needctrl && !evt.ctrlKey))
1972             )  {
1973                 return true;
1974             }
1975 
1976             return false;
1977         },
1978 
1979         /*
1980          * Pointer events
1981          */
1982 
1983         /**
1984          *
1985          * Check if pointer event is already registered in {@link JXG.Board#_board_touches}.
1986          *
1987          * @param  {Object} evt Event object
1988          * @return {Boolean} true if down event has already been sent.
1989          * @private
1990          */
1991          _isPointerRegistered: function(evt) {
1992             var i, len = this._board_touches.length;
1993 
1994             for (i = 0; i < len; i++) {
1995                 if (this._board_touches[i].pointerId === evt.pointerId) {
1996                     return true;
1997                 }
1998             }
1999             return false;
2000         },
2001 
2002         /**
2003          *
2004          * Store the position of a pointer event.
2005          * If not yet done, registers a pointer event in {@link JXG.Board#_board_touches}.
2006          * Allows to follow the path of that finger on the screen.
2007          * Only two simultaneous touches are supported.
2008          *
2009          * @param {Object} evt Event object
2010          * @returns {JXG.Board} Reference to the board
2011          * @private
2012          */
2013          _pointerStorePosition: function (evt) {
2014             var i, found;
2015 
2016             for (i = 0, found = false; i < this._board_touches.length; i++) {
2017                 if (this._board_touches[i].pointerId === evt.pointerId) {
2018                     this._board_touches[i].clientX = evt.clientX;
2019                     this._board_touches[i].clientY = evt.clientY;
2020                     found = true;
2021                     break;
2022                 }
2023             }
2024 
2025             // Restrict the number of simultaneous touches to 2
2026             if (!found && this._board_touches.length < 2) {
2027                 this._board_touches.push({
2028                     pointerId: evt.pointerId,
2029                     clientX: evt.clientX,
2030                     clientY: evt.clientY
2031                 });
2032             }
2033 
2034             return this;
2035         },
2036 
2037         /**
2038          * Deregisters a pointer event in {@link JXG.Board#_board_touches}.
2039          * It happens if a finger has been lifted from the screen.
2040          *
2041          * @param {Object} evt Event object
2042          * @returns {JXG.Board} Reference to the board
2043          * @private
2044          */
2045         _pointerRemoveTouches: function (evt) {
2046             var i;
2047             for (i = 0; i < this._board_touches.length; i++) {
2048                 if (this._board_touches[i].pointerId === evt.pointerId) {
2049                     this._board_touches.splice(i, 1);
2050                     break;
2051                 }
2052             }
2053 
2054             return this;
2055         },
2056 
2057         /**
2058          * Remove all registered fingers from {@link JXG.Board#_board_touches}.
2059          * This might be necessary if too many fingers have been registered.
2060          * @returns {JXG.Board} Reference to the board
2061          * @private
2062          */
2063         _pointerClearTouches: function() {
2064             if (this._board_touches.length > 0) {
2065                 this.dehighlightAll();
2066             }
2067             this.updateQuality = this.BOARD_QUALITY_HIGH;
2068             this.mode = this.BOARD_MODE_NONE;
2069             this._board_touches = [];
2070             this.touches = [];
2071         },
2072 
2073         /**
2074          * Determine which input device is used for this action.
2075          * Possible devices are 'touch', 'pen' and 'mouse'.
2076          * This affects the precision and certain events.
2077          * In case of no browser, 'mouse' is used.
2078          *
2079          * @see JXG.Board#pointerDownListener
2080          * @see JXG.Board#pointerMoveListener
2081          * @see JXG.Board#initMoveObject
2082          * @see JXG.Board#moveObject
2083          *
2084          * @param {Event} evt The browsers event object.
2085          * @returns {String} 'mouse', 'pen', or 'touch'
2086          * @private
2087          */
2088         _getPointerInputDevice: function(evt) {
2089             if (Env.isBrowser) {
2090                 if (evt.pointerType === 'touch' ||        // New
2091                     (window.navigator.msMaxTouchPoints && // Old
2092                         window.navigator.msMaxTouchPoints > 1)) {
2093                     return 'touch';
2094                 }
2095                 if (evt.pointerType === 'mouse') {
2096                     return 'mouse';
2097                 }
2098                 if (evt.pointerType === 'pen') {
2099                     return 'pen';
2100                 }
2101             }
2102             return 'mouse';
2103         },
2104 
2105         /**
2106          * This method is called by the browser when a pointing device is pressed on the screen.
2107          * @param {Event} evt The browsers event object.
2108          * @param {Object} object If the object to be dragged is already known, it can be submitted via this parameter
2109          * @returns {Boolean} ...
2110          */
2111         pointerDownListener: function (evt, object) {
2112             var i, j, k, pos, elements, sel,
2113                 target_obj,
2114                 type = 'mouse', // Used in case of no browser
2115                 found, target;
2116 
2117             // Fix for Firefox browser: When using a second finger, the
2118             // touch event for the first finger is sent again.
2119             if (!object && this._isPointerRegistered(evt)) {
2120                 return false;
2121             }
2122 
2123             if (!object && evt.isPrimary) {
2124                 // First finger down. To be on the safe side this._board_touches is cleared.
2125                 this._pointerClearTouches();
2126             }
2127 
2128             if (!this.hasPointerUp) {
2129                 if (window.navigator.msPointerEnabled) {  // IE10-
2130                     Env.addEvent(this.document, 'MSPointerUp',   this.pointerUpListener, this);
2131                 } else {
2132                     // 'pointercancel' is fired e.g. if the finger leaves the browser and drags down the system menu on Android
2133                     Env.addEvent(this.document, 'pointerup',     this.pointerUpListener, this);
2134                     Env.addEvent(this.document, 'pointercancel', this.pointerUpListener, this);
2135                 }
2136                 this.hasPointerUp = true;
2137             }
2138 
2139             if (this.hasMouseHandlers) {
2140                 this.removeMouseEventHandlers();
2141             }
2142 
2143             if (this.hasTouchHandlers) {
2144                 this.removeTouchEventHandlers();
2145             }
2146 
2147             // Prevent accidental selection of text
2148             if (this.document.selection && Type.isFunction(this.document.selection.empty)) {
2149                 this.document.selection.empty();
2150             } else if (window.getSelection) {
2151                 sel = window.getSelection();
2152                 if (sel.removeAllRanges) {
2153                     try {
2154                         sel.removeAllRanges();
2155                     } catch (e) {}
2156                 }
2157             }
2158 
2159             // Mouse, touch or pen device
2160             this._inputDevice = this._getPointerInputDevice(evt);
2161             type = this._inputDevice;
2162             this.options.precision.hasPoint = this.options.precision[type];
2163 
2164             // Handling of multi touch with pointer events should be easier than the touch events.
2165             // Every pointer device has its own pointerId, e.g. the mouse
2166             // always has id 1 or 0, fingers and pens get unique ids every time a pointerDown event is fired and they will
2167             // keep this id until a pointerUp event is fired. What we have to do here is:
2168             //  1. collect all elements under the current pointer
2169             //  2. run through the touches control structure
2170             //    a. look for the object collected in step 1.
2171             //    b. if an object is found, check the number of pointers. If appropriate, add the pointer.
2172             pos = this.getMousePosition(evt);
2173 
2174             // selection
2175             this._testForSelection(evt);
2176             if (this.selectingMode) {
2177                 this._startSelecting(pos);
2178                 this.triggerEventHandlers(['touchstartselecting', 'pointerstartselecting', 'startselecting'], [evt]);
2179                 return;     // don't continue as a normal click
2180             }
2181 
2182             if (this.attr.drag.enabled && object) {
2183                 elements = [ object ];
2184                 this.mode = this.BOARD_MODE_DRAG;
2185             } else {
2186                 elements = this.initMoveObject(pos[0], pos[1], evt, type);
2187             }
2188 
2189             target_obj = {
2190                 num: evt.pointerId,
2191                 X: pos[0],
2192                 Y: pos[1],
2193                 Xprev: NaN,
2194                 Yprev: NaN,
2195                 Xstart: [],
2196                 Ystart: [],
2197                 Zstart: []
2198             };
2199 
2200             // If no draggable object can be found, get out here immediately
2201             if (elements.length > 0) {
2202                 // check touches structure
2203                 target = elements[elements.length - 1];
2204                 found = false;
2205 
2206                 // Reminder: this.touches is the list of elements which
2207                 // currently "possess" a pointer (mouse, pen, finger)
2208                 for (i = 0; i < this.touches.length; i++) {
2209                     // An element receives a further touch, i.e.
2210                     // the target is already in our touches array, add the pointer to the existing touch
2211                     if (this.touches[i].obj === target) {
2212                         j = i;
2213                         k = this.touches[i].targets.push(target_obj) - 1;
2214                         found = true;
2215                         break;
2216                     }
2217                 }
2218                 if (!found) {
2219                     // An new element hae been touched.
2220                     k = 0;
2221                     j = this.touches.push({
2222                         obj: target,
2223                         targets: [target_obj]
2224                     }) - 1;
2225                 }
2226 
2227                 this.dehighlightAll();
2228                 target.highlight(true);
2229 
2230                 this.saveStartPos(target, this.touches[j].targets[k]);
2231 
2232                 // Prevent accidental text selection
2233                 // this could get us new trouble: input fields, links and drop down boxes placed as text
2234                 // on the board don't work anymore.
2235                 if (evt && evt.preventDefault) {
2236                     evt.preventDefault();
2237                 } else if (window.event) {
2238                     window.event.returnValue = false;
2239                 }
2240             }
2241 
2242             if (this.touches.length > 0) {
2243                 evt.preventDefault();
2244                 evt.stopPropagation();
2245             }
2246 
2247             if (!Env.isBrowser) {
2248                 return false;
2249             }
2250             if (this._getPointerInputDevice(evt) !== 'touch') {
2251                 if (this.mode === this.BOARD_MODE_NONE) {
2252                     this.mouseOriginMoveStart(evt);
2253                 }
2254             } else {
2255                 this._pointerStorePosition(evt);
2256                 evt.touches = this._board_touches;
2257 
2258                 // Touch events on empty areas of the board are handled here, see also touchStartListener
2259                 // 1. case: one finger. If allowed, this triggers pan with one finger
2260                 if (evt.touches.length === 1 &&
2261                     this.mode === this.BOARD_MODE_NONE &&
2262                     this.touchStartMoveOriginOneFinger(evt)) {
2263                         // Empty by purpose
2264                 } else if (evt.touches.length === 2 &&
2265                             (this.mode === this.BOARD_MODE_NONE || this.mode === this.BOARD_MODE_MOVE_ORIGIN)
2266                         ) {
2267                     // 2. case: two fingers: pinch to zoom or pan with two fingers needed.
2268                     // This happens when the second finger hits the device. First, the
2269                     // "one finger pan mode" has to be cancelled.
2270                     if (this.mode === this.BOARD_MODE_MOVE_ORIGIN) {
2271                         this.originMoveEnd();
2272                     }
2273 
2274                     this.gestureStartListener(evt);
2275                 }
2276             }
2277 
2278             this.triggerEventHandlers(['touchstart', 'down', 'pointerdown', 'MSPointerDown'], [evt]);
2279             return false;
2280         },
2281 
2282         // /**
2283         //  * Called if pointer leaves an HTML tag. It is called by the inner-most tag.
2284         //  * That means, if a JSXGraph text, i.e. an HTML div, is placed close
2285         //  * to the border of the board, this pointerout event will be ignored.
2286         //  * @param  {Event} evt
2287         //  * @return {Boolean}
2288         //  */
2289         // pointerOutListener: function (evt) {
2290         //     if (evt.target === this.containerObj ||
2291         //         (this.renderer.type === 'svg' && evt.target === this.renderer.foreignObjLayer)) {
2292         //         this.pointerUpListener(evt);
2293         //     }
2294         //     return this.mode === this.BOARD_MODE_NONE;
2295         // },
2296 
2297         /**
2298          * Called periodically by the browser while the user moves a pointing device across the screen.
2299          * @param {Event} evt
2300          * @returns {Boolean}
2301          */
2302         pointerMoveListener: function (evt) {
2303             var i, j, pos, touchTargets,
2304                 type = 'mouse'; // in case of no browser
2305 
2306             if (this._getPointerInputDevice(evt) === 'touch' && !this._isPointerRegistered(evt)) {
2307                 // Test, if there was a previous down event of this _getPointerId
2308                 // (in case it is a touch event).
2309                 // Otherwise this move event is ignored. This is necessary e.g. for sketchometry.
2310                 return this.BOARD_MODE_NONE;
2311             }
2312 
2313             if (!this.checkFrameRate(evt)) {
2314                 return false;
2315             }
2316 
2317             if (this.mode !== this.BOARD_MODE_DRAG) {
2318                 this.dehighlightAll();
2319                 this.displayInfobox(false);
2320             }
2321 
2322             if (this.mode !== this.BOARD_MODE_NONE) {
2323                 evt.preventDefault();
2324                 evt.stopPropagation();
2325             }
2326 
2327             this.updateQuality = this.BOARD_QUALITY_LOW;
2328             // Mouse, touch or pen device
2329             this._inputDevice = this._getPointerInputDevice(evt);
2330             type = this._inputDevice;
2331             this.options.precision.hasPoint = this.options.precision[type];
2332 
2333             // selection
2334             if (this.selectingMode) {
2335                 pos = this.getMousePosition(evt);
2336                 this._moveSelecting(pos);
2337                 this.triggerEventHandlers(['touchmoveselecting', 'moveselecting', 'pointermoveselecting'], [evt, this.mode]);
2338             } else if (!this.mouseOriginMove(evt)) {
2339                 if (this.mode === this.BOARD_MODE_DRAG) {
2340                     // Run through all jsxgraph elements which are touched by at least one finger.
2341                     for (i = 0; i < this.touches.length; i++) {
2342                         touchTargets = this.touches[i].targets;
2343                         // Run through all touch events which have been started on this jsxgraph element.
2344                         for (j = 0; j < touchTargets.length; j++) {
2345                             if (touchTargets[j].num === evt.pointerId) {
2346 
2347                                 pos = this.getMousePosition(evt);
2348                                 touchTargets[j].X = pos[0];
2349                                 touchTargets[j].Y = pos[1];
2350 
2351                                 if (touchTargets.length === 1) {
2352                                     // Touch by one finger: this is possible for all elements that can be dragged
2353                                     this.moveObject(pos[0], pos[1], this.touches[i], evt, type);
2354                                 } else if (touchTargets.length === 2) {
2355                                     // Touch by two fingers: e.g. moving lines
2356                                     this.twoFingerMove(this.touches[i], evt.pointerId, evt);
2357 
2358                                     touchTargets[j].Xprev = pos[0];
2359                                     touchTargets[j].Yprev = pos[1];
2360                                 }
2361 
2362                                 // There is only one pointer in the evt object, so there's no point in looking further
2363                                 break;
2364                             }
2365                         }
2366                     }
2367                 } else {
2368                     if (this._getPointerInputDevice(evt) === 'touch') {
2369                         this._pointerStorePosition(evt);
2370 
2371                         if (this._board_touches.length === 2) {
2372                             evt.touches = this._board_touches;
2373                             this.gestureChangeListener(evt);
2374                         }
2375                     }
2376 
2377                     // Move event without dragging an element
2378                     pos = this.getMousePosition(evt);
2379                     this.highlightElements(pos[0], pos[1], evt, -1);
2380                 }
2381             }
2382 
2383             // Hiding the infobox is commented out, since it prevents showing the infobox
2384             // on IE 11+ on 'over'
2385             //if (this.mode !== this.BOARD_MODE_DRAG) {
2386                 //this.displayInfobox(false);
2387             //}
2388             this.triggerEventHandlers(['touchmove', 'move', 'pointermove', 'MSPointerMove'], [evt, this.mode]);
2389             this.updateQuality = this.BOARD_QUALITY_HIGH;
2390 
2391             return this.mode === this.BOARD_MODE_NONE;
2392         },
2393 
2394         /**
2395          * Triggered as soon as the user stops touching the device with at least one finger.
2396          * @param {Event} evt
2397          * @returns {Boolean}
2398          */
2399         pointerUpListener: function (evt) {
2400             var i, j, found, touchTargets;
2401 
2402             this.triggerEventHandlers(['touchend', 'up', 'pointerup', 'MSPointerUp'], [evt]);
2403             this.displayInfobox(false);
2404 
2405             if (evt) {
2406                 for (i = 0; i < this.touches.length; i++) {
2407                     touchTargets = this.touches[i].targets;
2408                     for (j = 0; j < touchTargets.length; j++) {
2409                         if (touchTargets[j].num === evt.pointerId) {
2410                             touchTargets.splice(j, 1);
2411                             if (touchTargets.length === 0) {
2412                                 this.touches.splice(i, 1);
2413                             }
2414                             break;
2415                         }
2416                     }
2417                 }
2418             }
2419 
2420             this.originMoveEnd();
2421             this.update();
2422 
2423             // selection
2424             if (this.selectingMode) {
2425                 this._stopSelecting(evt);
2426                 this.triggerEventHandlers(['touchstopselecting', 'pointerstopselecting', 'stopselecting'], [evt]);
2427                 this.stopSelectionMode();
2428             } else {
2429                 for (i = this.downObjects.length - 1; i > -1; i--) {
2430                     found = false;
2431                     for (j = 0; j < this.touches.length; j++) {
2432                         if (this.touches[j].obj.id === this.downObjects[i].id) {
2433                             found = true;
2434                         }
2435                     }
2436                     if (!found) {
2437                         this.downObjects[i].triggerEventHandlers(['touchend', 'up', 'pointerup', 'MSPointerUp'], [evt]);
2438                         // this.downObjects[i].snapToGrid();
2439                         // this.downObjects[i].snapToPoints();
2440                         this.downObjects.splice(i, 1);
2441                     }
2442                 }
2443             }
2444 
2445             if (this.hasPointerUp) {
2446                 if (window.navigator.msPointerEnabled) {  // IE10-
2447                     Env.removeEvent(this.document, 'MSPointerUp',   this.pointerUpListener, this);
2448                 } else {
2449                     Env.removeEvent(this.document, 'pointerup',     this.pointerUpListener, this);
2450                     Env.removeEvent(this.document, 'pointercancel', this.pointerUpListener, this);
2451                 }
2452                 this.hasPointerUp = false;
2453             }
2454 
2455             // this.dehighlightAll();
2456             // this.updateQuality = this.BOARD_QUALITY_HIGH;
2457             // this.mode = this.BOARD_MODE_NONE;
2458 
2459             // this.originMoveEnd();
2460             // this.update();
2461 
2462             // After one finger leaves the screen the gesture is stopped.
2463             this._pointerClearTouches();
2464             return true;
2465         },
2466 
2467         /**
2468          * Touch-Events
2469          */
2470 
2471         /**
2472          * This method is called by the browser when a finger touches the surface of the touch-device.
2473          * @param {Event} evt The browsers event object.
2474          * @returns {Boolean} ...
2475          */
2476         touchStartListener: function (evt) {
2477             var i, pos, elements, j, k,
2478                 eps = this.options.precision.touch,
2479                 obj, found, targets,
2480                 evtTouches = evt[JXG.touchProperty],
2481                 target, touchTargets;
2482 
2483             if (!this.hasTouchEnd) {
2484                 Env.addEvent(this.document, 'touchend', this.touchEndListener, this);
2485                 this.hasTouchEnd = true;
2486             }
2487 
2488             // Do not remove mouseHandlers, since Chrome on win tablets sends mouseevents if used with pen.
2489             //if (this.hasMouseHandlers) { this.removeMouseEventHandlers(); }
2490 
2491             // prevent accidental selection of text
2492             if (this.document.selection && Type.isFunction(this.document.selection.empty)) {
2493                 this.document.selection.empty();
2494             } else if (window.getSelection) {
2495                 window.getSelection().removeAllRanges();
2496             }
2497 
2498             // multitouch
2499             this._inputDevice = 'touch';
2500             this.options.precision.hasPoint = this.options.precision.touch;
2501 
2502             // This is the most critical part. first we should run through the existing touches and collect all targettouches that don't belong to our
2503             // 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
2504             // 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
2505             // 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
2506             // stretching. as a last step we're going through the rest of the targettouches and initiate new move operations:
2507             //  * points have higher priority over other elements.
2508             //  * 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
2509             //    this element and add them.
2510             // ADDENDUM 11/10/11:
2511             //  (1) run through the touches control object,
2512             //  (2) try to find the targetTouches for every touch. on touchstart only new touches are added, hence we can find a targettouch
2513             //      for every target in our touches objects
2514             //  (3) if one of the targettouches was bound to a touches targets array, mark it
2515             //  (4) run through the targettouches. if the targettouch is marked, continue. otherwise check for elements below the targettouch:
2516             //      (a) if no element could be found: mark the target touches and continue
2517             //      --- in the following cases, "init" means:
2518             //           (i) check if the element is already used in another touches element, if so, mark the targettouch and continue
2519             //          (ii) if not, init a new touches element, add the targettouch to the touches property and mark it
2520             //      (b) if the element is a point, init
2521             //      (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
2522             //      (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
2523             //          add both to the touches array and mark them.
2524             for (i = 0; i < evtTouches.length; i++) {
2525                 evtTouches[i].jxg_isused = false;
2526             }
2527 
2528             for (i = 0; i < this.touches.length; i++) {
2529                 touchTargets = this.touches[i].targets;
2530                 for (j = 0; j < touchTargets.length; j++) {
2531                     touchTargets[j].num = -1;
2532                     eps = this.options.precision.touch;
2533 
2534                     do {
2535                         for (k = 0; k < evtTouches.length; k++) {
2536                             // find the new targettouches
2537                             if (Math.abs(Math.pow(evtTouches[k].screenX - touchTargets[j].X, 2) +
2538                                     Math.pow(evtTouches[k].screenY - touchTargets[j].Y, 2)) < eps * eps) {
2539                                 touchTargets[j].num = k;
2540                                 touchTargets[j].X = evtTouches[k].screenX;
2541                                 touchTargets[j].Y = evtTouches[k].screenY;
2542                                 evtTouches[k].jxg_isused = true;
2543                                 break;
2544                             }
2545                         }
2546 
2547                         eps *= 2;
2548 
2549                     } while (touchTargets[j].num === -1 &&
2550                              eps < this.options.precision.touchMax);
2551 
2552                     if (touchTargets[j].num === -1) {
2553                         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.');
2554                         JXG.debug('eps = ' + eps + ', touchMax = ' + Options.precision.touchMax);
2555                         touchTargets.splice(i, 1);
2556                     }
2557 
2558                 }
2559             }
2560 
2561             // we just re-mapped the targettouches to our existing touches list.
2562             // now we have to initialize some touches from additional targettouches
2563             for (i = 0; i < evtTouches.length; i++) {
2564                 if (!evtTouches[i].jxg_isused) {
2565 
2566                     pos = this.getMousePosition(evt, i);
2567                     // selection
2568                     // this._testForSelection(evt); // we do not have shift or ctrl keys yet.
2569                     if (this.selectingMode) {
2570                         this._startSelecting(pos);
2571                         this.triggerEventHandlers(['touchstartselecting', 'startselecting'], [evt]);
2572                         evt.preventDefault();
2573                         evt.stopPropagation();
2574                         this.options.precision.hasPoint = this.options.precision.mouse;
2575                         return this.touches.length > 0; // don't continue as a normal click
2576                     }
2577 
2578                     elements = this.initMoveObject(pos[0], pos[1], evt, 'touch');
2579                     if (elements.length !== 0) {
2580                         obj = elements[elements.length - 1];
2581                         target = {num: i,
2582                             X: evtTouches[i].screenX,
2583                             Y: evtTouches[i].screenY,
2584                             Xprev: NaN,
2585                             Yprev: NaN,
2586                             Xstart: [],
2587                             Ystart: [],
2588                             Zstart: []
2589                         };
2590 
2591                         if (Type.isPoint(obj) ||
2592                                 obj.elementClass === Const.OBJECT_CLASS_TEXT ||
2593                                 obj.type === Const.OBJECT_TYPE_TICKS ||
2594                                 obj.type === Const.OBJECT_TYPE_IMAGE) {
2595                             // It's a point, so it's single touch, so we just push it to our touches
2596                             targets = [target];
2597 
2598                             // For the UNDO/REDO of object moves
2599                             this.saveStartPos(obj, targets[0]);
2600 
2601                             this.touches.push({ obj: obj, targets: targets });
2602                             obj.highlight(true);
2603 
2604                         } else if (obj.elementClass === Const.OBJECT_CLASS_LINE ||
2605                                 obj.elementClass === Const.OBJECT_CLASS_CIRCLE ||
2606                                 obj.elementClass === Const.OBJECT_CLASS_CURVE ||
2607                                 obj.type === Const.OBJECT_TYPE_POLYGON) {
2608                             found = false;
2609 
2610                             // first check if this geometric object is already captured in this.touches
2611                             for (j = 0; j < this.touches.length; j++) {
2612                                 if (obj.id === this.touches[j].obj.id) {
2613                                     found = true;
2614                                     // only add it, if we don't have two targets in there already
2615                                     if (this.touches[j].targets.length === 1) {
2616                                         // For the UNDO/REDO of object moves
2617                                         this.saveStartPos(obj, target);
2618                                         this.touches[j].targets.push(target);
2619                                     }
2620 
2621                                     evtTouches[i].jxg_isused = true;
2622                                 }
2623                             }
2624 
2625                             // we couldn't find it in touches, so we just init a new touches
2626                             // IF there is a second touch targetting this line, we will find it later on, and then add it to
2627                             // the touches control object.
2628                             if (!found) {
2629                                 targets = [target];
2630 
2631                                 // For the UNDO/REDO of object moves
2632                                 this.saveStartPos(obj, targets[0]);
2633                                 this.touches.push({ obj: obj, targets: targets });
2634                                 obj.highlight(true);
2635                             }
2636                         }
2637                     }
2638 
2639                     evtTouches[i].jxg_isused = true;
2640                 }
2641             }
2642 
2643             if (this.touches.length > 0) {
2644                 evt.preventDefault();
2645                 evt.stopPropagation();
2646             }
2647 
2648             // Touch events on empty areas of the board are handled here:
2649             // 1. case: one finger. If allowed, this triggers pan with one finger
2650             if (evtTouches.length === 1 && this.mode === this.BOARD_MODE_NONE && this.touchStartMoveOriginOneFinger(evt)) {
2651             } else if (evtTouches.length === 2 &&
2652                         (this.mode === this.BOARD_MODE_NONE || this.mode === this.BOARD_MODE_MOVE_ORIGIN)
2653                     ) {
2654                 // 2. case: two fingers: pinch to zoom or pan with two fingers needed.
2655                 // This happens when the second finger hits the device. First, the
2656                 // "one finger pan mode" has to be cancelled.
2657                 if (this.mode === this.BOARD_MODE_MOVE_ORIGIN) {
2658                     this.originMoveEnd();
2659                 }
2660                 this.gestureStartListener(evt);
2661             }
2662 
2663             this.options.precision.hasPoint = this.options.precision.mouse;
2664             this.triggerEventHandlers(['touchstart', 'down'], [evt]);
2665 
2666             return false;
2667             //return this.touches.length > 0;
2668         },
2669 
2670         /**
2671          * Called periodically by the browser while the user moves his fingers across the device.
2672          * @param {Event} evt
2673          * @returns {Boolean}
2674          */
2675         touchMoveListener: function (evt) {
2676             var i, pos1, pos2,
2677                 touchTargets,
2678                 evtTouches = evt[JXG.touchProperty];
2679 
2680             if (!this.checkFrameRate(evt)) {
2681                 return false;
2682             }
2683 
2684             if (this.mode !== this.BOARD_MODE_NONE) {
2685                 evt.preventDefault();
2686                 evt.stopPropagation();
2687             }
2688 
2689             if (this.mode !== this.BOARD_MODE_DRAG) {
2690                 this.dehighlightAll();
2691                 this.displayInfobox(false);
2692             }
2693 
2694             this._inputDevice = 'touch';
2695             this.options.precision.hasPoint = this.options.precision.touch;
2696             this.updateQuality = this.BOARD_QUALITY_LOW;
2697 
2698             // selection
2699             if (this.selectingMode) {
2700                 for (i = 0; i < evtTouches.length; i++) {
2701                     if (!evtTouches[i].jxg_isused) {
2702                         pos1 = this.getMousePosition(evt, i);
2703                         this._moveSelecting(pos1);
2704                         this.triggerEventHandlers(['touchmoves', 'moveselecting'], [evt, this.mode]);
2705                         break;
2706                     }
2707                 }
2708             } else {
2709                 if (!this.touchOriginMove(evt)) {
2710                     if (this.mode === this.BOARD_MODE_DRAG) {
2711                         // Runs over through all elements which are touched
2712                         // by at least one finger.
2713                         for (i = 0; i < this.touches.length; i++) {
2714                             touchTargets = this.touches[i].targets;
2715                             if (touchTargets.length === 1) {
2716 
2717 
2718                                 // Touch by one finger:  this is possible for all elements that can be dragged
2719                                 if (evtTouches[touchTargets[0].num]) {
2720                                     pos1 = this.getMousePosition(evt, touchTargets[0].num);
2721                                     if (pos1[0] < 0 || pos1[0] > this.canvasWidth ||
2722                                         pos1[1] < 0 || pos1[1] > this.canvasHeight) {
2723                                         return;
2724                                     }
2725                                     touchTargets[0].X = pos1[0];
2726                                     touchTargets[0].Y = pos1[1];
2727                                     this.moveObject(pos1[0], pos1[1], this.touches[i], evt, 'touch');
2728                                 }
2729 
2730                             } else if (touchTargets.length === 2 &&
2731                                 touchTargets[0].num > -1 &&
2732                                 touchTargets[1].num > -1) {
2733 
2734                                 // Touch by two fingers: moving lines, ...
2735                                 if (evtTouches[touchTargets[0].num] &&
2736                                     evtTouches[touchTargets[1].num]) {
2737 
2738                                     // Get coordinates of the two touches
2739                                     pos1 = this.getMousePosition(evt, touchTargets[0].num);
2740                                     pos2 = this.getMousePosition(evt, touchTargets[1].num);
2741                                     if (pos1[0] < 0 || pos1[0] > this.canvasWidth ||
2742                                         pos1[1] < 0 || pos1[1] > this.canvasHeight ||
2743                                         pos2[0] < 0 || pos2[0] > this.canvasWidth ||
2744                                         pos2[1] < 0 || pos2[1] > this.canvasHeight) {
2745                                         return;
2746                                     }
2747 
2748                                     touchTargets[0].X = pos1[0];
2749                                     touchTargets[0].Y = pos1[1];
2750                                     touchTargets[1].X = pos2[0];
2751                                     touchTargets[1].Y = pos2[1];
2752 
2753                                     this.twoFingerMove(this.touches[i], touchTargets[0].num, evt);
2754                                     this.twoFingerMove(this.touches[i], touchTargets[1].num);
2755 
2756                                     touchTargets[0].Xprev = pos1[0];
2757                                     touchTargets[0].Yprev = pos1[1];
2758                                     touchTargets[1].Xprev = pos2[0];
2759                                     touchTargets[1].Yprev = pos2[1];
2760                                 }
2761                             }
2762                         }
2763                     } else {
2764                         if (evtTouches.length === 2) {
2765                             this.gestureChangeListener(evt);
2766                         }
2767                         // Move event without dragging an element
2768                         pos1 = this.getMousePosition(evt, 0);
2769                         this.highlightElements(pos1[0], pos1[1], evt, -1);
2770                     }
2771                 }
2772             }
2773 
2774             if (this.mode !== this.BOARD_MODE_DRAG) {
2775                 this.displayInfobox(false);
2776             }
2777 
2778             this.triggerEventHandlers(['touchmove', 'move'], [evt, this.mode]);
2779             this.options.precision.hasPoint = this.options.precision.mouse;
2780             this.updateQuality = this.BOARD_QUALITY_HIGH;
2781 
2782             return this.mode === this.BOARD_MODE_NONE;
2783         },
2784 
2785         /**
2786          * Triggered as soon as the user stops touching the device with at least one finger.
2787          * @param {Event} evt
2788          * @returns {Boolean}
2789          */
2790         touchEndListener: function (evt) {
2791             var i, j, k,
2792                 eps = this.options.precision.touch,
2793                 tmpTouches = [], found, foundNumber,
2794                 evtTouches = evt && evt[JXG.touchProperty],
2795                 touchTargets;
2796 
2797             this.triggerEventHandlers(['touchend', 'up'], [evt]);
2798             this.displayInfobox(false);
2799 
2800             // selection
2801             if (this.selectingMode) {
2802                 this._stopSelecting(evt);
2803                 this.triggerEventHandlers(['touchstopselecting', 'stopselecting'], [evt]);
2804                 this.stopSelectionMode();
2805             } else if (evtTouches && evtTouches.length > 0) {
2806                 for (i = 0; i < this.touches.length; i++) {
2807                     tmpTouches[i] = this.touches[i];
2808                 }
2809                 this.touches.length = 0;
2810 
2811                 // try to convert the operation, e.g. if a lines is rotated and translated with two fingers and one finger is lifted,
2812                 // convert the operation to a simple one-finger-translation.
2813                 // ADDENDUM 11/10/11:
2814                 // see addendum to touchStartListener from 11/10/11
2815                 // (1) run through the tmptouches
2816                 // (2) check the touches.obj, if it is a
2817                 //     (a) point, try to find the targettouch, if found keep it and mark the targettouch, else drop the touch.
2818                 //     (b) line with
2819                 //          (i) one target: try to find it, if found keep it mark the targettouch, else drop the touch.
2820                 //         (ii) two targets: if none can be found, drop the touch. if one can be found, remove the other target. mark all found targettouches
2821                 //     (c) circle with [proceed like in line]
2822 
2823                 // init the targettouches marker
2824                 for (i = 0; i < evtTouches.length; i++) {
2825                     evtTouches[i].jxg_isused = false;
2826                 }
2827 
2828                 for (i = 0; i < tmpTouches.length; i++) {
2829                     // could all targets of the current this.touches.obj be assigned to targettouches?
2830                     found = false;
2831                     foundNumber = 0;
2832                     touchTargets = tmpTouches[i].targets;
2833 
2834                     for (j = 0; j < touchTargets.length; j++) {
2835                         touchTargets[j].found = false;
2836                         for (k = 0; k < evtTouches.length; k++) {
2837                             if (Math.abs(Math.pow(evtTouches[k].screenX - touchTargets[j].X, 2) + Math.pow(evtTouches[k].screenY - touchTargets[j].Y, 2)) < eps * eps) {
2838                                 touchTargets[j].found = true;
2839                                 touchTargets[j].num = k;
2840                                 touchTargets[j].X = evtTouches[k].screenX;
2841                                 touchTargets[j].Y = evtTouches[k].screenY;
2842                                 foundNumber += 1;
2843                                 break;
2844                             }
2845                         }
2846                     }
2847 
2848                     if (Type.isPoint(tmpTouches[i].obj)) {
2849                         found = (touchTargets[0] && touchTargets[0].found);
2850                     } else if (tmpTouches[i].obj.elementClass === Const.OBJECT_CLASS_LINE) {
2851                         found = (touchTargets[0] && touchTargets[0].found) || (touchTargets[1] && touchTargets[1].found);
2852                     } else if (tmpTouches[i].obj.elementClass === Const.OBJECT_CLASS_CIRCLE) {
2853                         found = foundNumber === 1 || foundNumber === 3;
2854                     }
2855 
2856                     // if we found this object to be still dragged by the user, add it back to this.touches
2857                     if (found) {
2858                         this.touches.push({
2859                             obj: tmpTouches[i].obj,
2860                             targets: []
2861                         });
2862 
2863                         for (j = 0; j < touchTargets.length; j++) {
2864                             if (touchTargets[j].found) {
2865                                 this.touches[this.touches.length - 1].targets.push({
2866                                     num: touchTargets[j].num,
2867                                     X: touchTargets[j].screenX,
2868                                     Y: touchTargets[j].screenY,
2869                                     Xprev: NaN,
2870                                     Yprev: NaN,
2871                                     Xstart: touchTargets[j].Xstart,
2872                                     Ystart: touchTargets[j].Ystart,
2873                                     Zstart: touchTargets[j].Zstart
2874                                 });
2875                             }
2876                         }
2877 
2878                     } else {
2879                         tmpTouches[i].obj.noHighlight();
2880                     }
2881                 }
2882 
2883             } else {
2884                 this.touches.length = 0;
2885             }
2886 
2887             for (i = this.downObjects.length - 1; i > -1; i--) {
2888                 found = false;
2889                 for (j = 0; j < this.touches.length; j++) {
2890                     if (this.touches[j].obj.id === this.downObjects[i].id) {
2891                         found = true;
2892                     }
2893                 }
2894                 if (!found) {
2895                     this.downObjects[i].triggerEventHandlers(['touchup', 'up'], [evt]);
2896                     // this.downObjects[i].snapToGrid();
2897                     // this.downObjects[i].snapToPoints();
2898                     this.downObjects.splice(i, 1);
2899                 }
2900             }
2901 
2902             if (!evtTouches || evtTouches.length === 0) {
2903 
2904                 if (this.hasTouchEnd) {
2905                     Env.removeEvent(this.document, 'touchend', this.touchEndListener, this);
2906                     this.hasTouchEnd = false;
2907                 }
2908 
2909                 this.dehighlightAll();
2910                 this.updateQuality = this.BOARD_QUALITY_HIGH;
2911 
2912                 this.originMoveEnd();
2913                 this.update();
2914             }
2915 
2916             return true;
2917         },
2918 
2919         /**
2920          * This method is called by the browser when the mouse button is clicked.
2921          * @param {Event} evt The browsers event object.
2922          * @returns {Boolean} True if no element is found under the current mouse pointer, false otherwise.
2923          */
2924         mouseDownListener: function (evt) {
2925             var pos, elements, result;
2926 
2927             // prevent accidental selection of text
2928             if (this.document.selection && Type.isFunction(this.document.selection.empty)) {
2929                 this.document.selection.empty();
2930             } else if (window.getSelection) {
2931                 window.getSelection().removeAllRanges();
2932             }
2933 
2934             if (!this.hasMouseUp) {
2935                 Env.addEvent(this.document, 'mouseup', this.mouseUpListener, this);
2936                 this.hasMouseUp = true;
2937             } else {
2938                 // In case this.hasMouseUp==true, it may be that there was a
2939                 // mousedown event before which was not followed by an mouseup event.
2940                 // This seems to happen with interactive whiteboard pens sometimes.
2941                 return;
2942             }
2943 
2944             this._inputDevice = 'mouse';
2945             this.options.precision.hasPoint = this.options.precision.mouse;
2946             pos = this.getMousePosition(evt);
2947 
2948             // selection
2949             this._testForSelection(evt);
2950             if (this.selectingMode) {
2951                 this._startSelecting(pos);
2952                 this.triggerEventHandlers(['mousestartselecting', 'startselecting'], [evt]);
2953                 return;     // don't continue as a normal click
2954             }
2955 
2956             elements = this.initMoveObject(pos[0], pos[1], evt, 'mouse');
2957 
2958             // if no draggable object can be found, get out here immediately
2959             if (elements.length === 0) {
2960                 this.mode = this.BOARD_MODE_NONE;
2961                 result = true;
2962             } else {
2963                 /** @ignore */
2964                 this.mouse = {
2965                     obj: null,
2966                     targets: [{
2967                         X: pos[0],
2968                         Y: pos[1],
2969                         Xprev: NaN,
2970                         Yprev: NaN
2971                     }]
2972                 };
2973                 this.mouse.obj = elements[elements.length - 1];
2974 
2975                 this.dehighlightAll();
2976                 this.mouse.obj.highlight(true);
2977 
2978                 this.mouse.targets[0].Xstart = [];
2979                 this.mouse.targets[0].Ystart = [];
2980                 this.mouse.targets[0].Zstart = [];
2981 
2982                 this.saveStartPos(this.mouse.obj, this.mouse.targets[0]);
2983 
2984                 // prevent accidental text selection
2985                 // this could get us new trouble: input fields, links and drop down boxes placed as text
2986                 // on the board don't work anymore.
2987                 if (evt && evt.preventDefault) {
2988                     evt.preventDefault();
2989                 } else if (window.event) {
2990                     window.event.returnValue = false;
2991                 }
2992             }
2993 
2994             if (this.mode === this.BOARD_MODE_NONE) {
2995                 result = this.mouseOriginMoveStart(evt);
2996             }
2997 
2998             this.triggerEventHandlers(['mousedown', 'down'], [evt]);
2999 
3000             return result;
3001         },
3002 
3003         /**
3004          * This method is called by the browser when the mouse is moved.
3005          * @param {Event} evt The browsers event object.
3006          */
3007         mouseMoveListener: function (evt) {
3008             var pos;
3009 
3010             if (!this.checkFrameRate(evt)) {
3011                 return false;
3012             }
3013 
3014             pos = this.getMousePosition(evt);
3015 
3016             this.updateQuality = this.BOARD_QUALITY_LOW;
3017 
3018             if (this.mode !== this.BOARD_MODE_DRAG) {
3019                 this.dehighlightAll();
3020                 this.displayInfobox(false);
3021             }
3022 
3023             // we have to check for four cases:
3024             //   * user moves origin
3025             //   * user drags an object
3026             //   * user just moves the mouse, here highlight all elements at
3027             //     the current mouse position
3028             //   * the user is selecting
3029 
3030             // selection
3031             if (this.selectingMode) {
3032                 this._moveSelecting(pos);
3033                 this.triggerEventHandlers(['mousemoveselecting', 'moveselecting'], [evt, this.mode]);
3034             } else if (!this.mouseOriginMove(evt)) {
3035                 if (this.mode === this.BOARD_MODE_DRAG) {
3036                     this.moveObject(pos[0], pos[1], this.mouse, evt, 'mouse');
3037                 } else { // BOARD_MODE_NONE
3038                     // Move event without dragging an element
3039                     this.highlightElements(pos[0], pos[1], evt, -1);
3040                 }
3041                 this.triggerEventHandlers(['mousemove', 'move'], [evt, this.mode]);
3042             }
3043             this.updateQuality = this.BOARD_QUALITY_HIGH;
3044         },
3045 
3046         /**
3047          * This method is called by the browser when the mouse button is released.
3048          * @param {Event} evt
3049          */
3050         mouseUpListener: function (evt) {
3051             var i;
3052 
3053             if (this.selectingMode === false) {
3054                 this.triggerEventHandlers(['mouseup', 'up'], [evt]);
3055             }
3056 
3057             // redraw with high precision
3058             this.updateQuality = this.BOARD_QUALITY_HIGH;
3059 
3060             // if (this.mouse && this.mouse.obj) {
3061             //     // The parameter is needed for lines with snapToGrid enabled
3062             //     this.mouse.obj.snapToGrid(this.mouse.targets[0]);
3063             //     this.mouse.obj.snapToPoints();
3064             // }
3065 
3066             this.originMoveEnd();
3067             this.dehighlightAll();
3068             this.update();
3069 
3070             // selection
3071             if (this.selectingMode) {
3072                 this._stopSelecting(evt);
3073                 this.triggerEventHandlers(['mousestopselecting', 'stopselecting'], [evt]);
3074                 this.stopSelectionMode();
3075             } else {
3076                 for (i = 0; i < this.downObjects.length; i++) {
3077                     this.downObjects[i].triggerEventHandlers(['mouseup', 'up'], [evt]);
3078                 }
3079             }
3080 
3081             this.downObjects.length = 0;
3082 
3083             if (this.hasMouseUp) {
3084                 Env.removeEvent(this.document, 'mouseup', this.mouseUpListener, this);
3085                 this.hasMouseUp = false;
3086             }
3087 
3088             // release dragged mouse object
3089             /** @ignore */
3090             this.mouse = null;
3091         },
3092 
3093         /**
3094          * Handler for mouse wheel events. Used to zoom in and out of the board.
3095          * @param {Event} evt
3096          * @returns {Boolean}
3097          */
3098         mouseWheelListener: function (evt) {
3099             if (!this.attr.zoom.wheel || !this._isRequiredKeyPressed(evt, 'zoom')) {
3100                 return true;
3101             }
3102 
3103             evt = evt || window.event;
3104             var wd = evt.detail ? -evt.detail : evt.wheelDelta / 40,
3105                 pos = new Coords(Const.COORDS_BY_SCREEN, this.getMousePosition(evt), this);
3106 
3107             if (wd > 0) {
3108                 this.zoomIn(pos.usrCoords[1], pos.usrCoords[2]);
3109             } else {
3110                 this.zoomOut(pos.usrCoords[1], pos.usrCoords[2]);
3111             }
3112 
3113             this.triggerEventHandlers(['mousewheel'], [evt]);
3114 
3115             evt.preventDefault();
3116             return false;
3117         },
3118 
3119         /**
3120          * Allow moving of JSXGraph elements with arrow keys
3121          * and zooming of the construction with + / -.
3122          * Panning of the construction is done with arrow keys
3123          * if the pan key (shift or ctrl) is pressed.
3124          * The selection of the element is done with the tab key.
3125          *
3126          * @param  {Event} evt The browser's event object
3127          *
3128          * @see JXG.Board#keyboard
3129          * @see JXG.Board#keyFocusInListener
3130          * @see JXG.Board#keyFocusOutListener
3131          *
3132          */
3133         keyDownListener: function (evt) {
3134             var id_node = evt.target.id,
3135                 id, el, res,
3136                 sX = 0,
3137                 sY = 0,
3138                 // dx, dy are provided in screen units and
3139                 // are converted to user coordinates
3140                 dx = Type.evaluate(this.attr.keyboard.dx) / this.unitX,
3141                 dy = Type.evaluate(this.attr.keyboard.dy) / this.unitY,
3142                 doZoom = false,
3143                 done = true,
3144                 dir, actPos;
3145 
3146             if (!this.attr.keyboard.enabled || id_node === '') {
3147                 return false;
3148             }
3149 
3150             // Get the JSXGraph id from the id of the SVG node.
3151             id = id_node.replace(this.containerObj.id + '_', '');
3152             el = this.select(id);
3153 
3154             if (Type.exists(el.coords)) {
3155                 actPos = el.coords.usrCoords.slice(1);
3156             }
3157 
3158             if (Type.evaluate(this.attr.keyboard.panshift) || Type.evaluate(this.attr.keyboard.panctrl)) {
3159                 doZoom = true;
3160             }
3161 
3162             if ((Type.evaluate(this.attr.keyboard.panshift) && evt.shiftKey) ||
3163                 (Type.evaluate(this.attr.keyboard.panctrl) && evt.ctrlKey)) {
3164                 if (evt.keyCode === 38) {           // up
3165                     this.clickUpArrow();
3166                 } else if (evt.keyCode === 40) {    // down
3167                     this.clickDownArrow();
3168                 } else if (evt.keyCode === 37) {    // left
3169                     this.clickLeftArrow();
3170                 } else if (evt.keyCode === 39) {    // right
3171                     this.clickRightArrow();
3172                 } else {
3173                     done = false;
3174                 }
3175             } else {
3176                 // Adapt dx, dy to snapToGrid and attractToGrid
3177                 // snapToGrid has priority.
3178                 if (Type.exists(el.visProp)) {
3179                     if (Type.exists(el.visProp.snaptogrid) &&
3180                         el.visProp.snaptogrid &&
3181                         Type.evaluate(el.visProp.snapsizex) &&
3182                         Type.evaluate(el.visProp.snapsizey)) {
3183 
3184                         // Adapt dx, dy such that snapToGrid is possible
3185                         res = el.getSnapSizes();
3186                         sX = res[0];
3187                         sY = res[1];
3188                         dx = Math.max(sX, dx);
3189                         dy = Math.max(sY, dy);
3190 
3191                     } else if (Type.exists(el.visProp.attracttogrid) &&
3192                         el.visProp.attracttogrid &&
3193                         Type.evaluate(el.visProp.attractordistance) &&
3194                         Type.evaluate(el.visProp.attractorunit)) {
3195 
3196                         // Adapt dx, dy such that attractToGrid is possible
3197                         sX = 1.1 * Type.evaluate(el.visProp.attractordistance);
3198                         sY = sX;
3199 
3200                         if (Type.evaluate(el.visProp.attractorunit) === 'screen') {
3201                             sX /= this.unitX;
3202                             sY /= this.unitX;
3203                         }
3204                         dx = Math.max(sX, dx);
3205                         dy = Math.max(sY, dy);
3206                     }
3207 
3208                 }
3209 
3210                 if (evt.keyCode === 38) {           // up
3211                     dir = [0, dy];
3212                 } else if (evt.keyCode === 40) {    // down
3213                     dir = [0, -dy];
3214                 } else if (evt.keyCode === 37) {    // left
3215                     dir = [-dx, 0];
3216                 } else if (evt.keyCode === 39) {    // right
3217                     dir = [dx, 0];
3218                 // } else if (evt.keyCode === 9) {  // tab
3219 
3220                 } else if (doZoom && evt.key === '+') {   // +
3221                     this.zoomIn();
3222                 } else if (doZoom && evt.key === '-') {   // -
3223                     this.zoomOut();
3224                 } else if (doZoom && evt.key === 'o') {   // o
3225                     this.zoom100();
3226                 } else {
3227                     done = false;
3228                 }
3229 
3230                 if (dir && el.isDraggable &&
3231                         el.visPropCalc.visible &&
3232                         ((this.geonextCompatibilityMode &&
3233                             (Type.isPoint(el) ||
3234                             el.elementClass === Const.OBJECT_CLASS_TEXT)
3235                         ) || !this.geonextCompatibilityMode) &&
3236                         !Type.evaluate(el.visProp.fixed)
3237                     ) {
3238 
3239                     if (Type.exists(el.coords)) {
3240                         dir[0] += actPos[0];
3241                         dir[1] += actPos[1];
3242                     }
3243                     // For coordsElement setPosition has to call setPositionDirectly.
3244                     // Otherwise the position is set by a translation.
3245                     el.setPosition(JXG.COORDS_BY_USER, dir);
3246                     if (Type.exists(el.coords)) {
3247                         this.updateInfobox(el);
3248                     }
3249                     this.triggerEventHandlers(['hit'], [evt, el]);
3250                 }
3251             }
3252 
3253             this.update();
3254 
3255             if (done && Type.exists(evt.preventDefault)) {
3256                 evt.preventDefault();
3257             }
3258             return true;
3259         },
3260 
3261         /**
3262          * Event listener for SVG elements getting focus.
3263          * This is needed for highlighting when using keyboard control.
3264          *
3265          * @see JXG.Board#keyFocusOutListener
3266          * @see JXG.Board#keyDownListener
3267          * @see JXG.Board#keyboard
3268          *
3269          * @param  {Event} evt The browser's event object
3270          */
3271         keyFocusInListener: function (evt) {
3272             var id_node = evt.target.id,
3273                 id, el;
3274 
3275             if (!this.attr.keyboard.enabled || id_node === '') {
3276                 return false;
3277             }
3278 
3279             id = id_node.replace(this.containerObj.id + '_', '');
3280             el = this.select(id);
3281             if (Type.exists(el.highlight)) {
3282                 el.highlight(true);
3283             }
3284             if (Type.exists(el.coords)) {
3285                 this.updateInfobox(el);
3286             }
3287             this.triggerEventHandlers(['hit'], [evt, el]);
3288         },
3289 
3290         /**
3291          * Event listener for SVG elements losing focus.
3292          * This is needed for dehighlighting when using keyboard control.
3293          *
3294          * @see JXG.Board#keyFocusInListener
3295          * @see JXG.Board#keyDownListener
3296          * @see JXG.Board#keyboard
3297          *
3298          * @param  {Event} evt The browser's event object
3299          */
3300         keyFocusOutListener: function (evt) {
3301             if (!this.attr.keyboard.enabled) {
3302                 return false;
3303             }
3304             // var id_node = evt.target.id,
3305             //     id, el;
3306 
3307             // id = id_node.replace(this.containerObj.id + '_', '');
3308             // el = this.select(id);
3309             this.dehighlightAll();
3310             this.displayInfobox(false);
3311         },
3312 
3313         /**
3314          * Update the width and height of the JSXGraph container div element.
3315          * Read actual values with getBoundingClientRect(),
3316          * and call board.resizeContainer() with this values.
3317          * <p>
3318          * If necessary, also call setBoundingBox().
3319          *
3320          * @see JXG.Board#startResizeObserver
3321          * @see JXG.Board#resizeListener
3322          * @see JXG.Board#resizeContainer
3323          * @see JXG.Board#setBoundingBox
3324          *
3325          */
3326         updateContainerDims: function() {
3327             var w, h,
3328                 bb, css;
3329 
3330             // Get size of the board's container div
3331             bb = this.containerObj.getBoundingClientRect();
3332             w = bb.width;
3333             h = bb.height;
3334 
3335             // Subtract the border size
3336             if (window && window.getComputedStyle) {
3337                 css = window.getComputedStyle(this.containerObj, null);
3338                 w -= parseFloat(css.getPropertyValue('border-left-width')) + parseFloat(css.getPropertyValue('border-right-width'));
3339                 h -= parseFloat(css.getPropertyValue('border-top-width'))  + parseFloat(css.getPropertyValue('border-bottom-width'));
3340             }
3341 
3342             // If div is invisible - do nothing
3343             if (w <= 0 || h <= 0) {
3344                 return;
3345             }
3346 
3347             // If bounding box is not yet initialized, do it now.
3348             if (isNaN(this.getBoundingBox()[0])) {
3349                 this.setBoundingBox(this.attr.boundingbox, this.keepaspectratio, 'keep');
3350             }
3351 
3352             // Do nothing if the dimension did not change since being visible
3353             // the last time. Note that if the div had display:none in the mean time,
3354             // we did not store this._prevDim.
3355             if (Type.exists(this._prevDim) &&
3356                 this._prevDim.w === w && this._prevDim.h === h) {
3357                     return;
3358             }
3359 
3360             // Set the size of the SVG or canvas element
3361             this.resizeContainer(w, h, true);
3362             this._prevDim = {
3363                 w: w,
3364                 h: h
3365             };
3366         },
3367 
3368         /**
3369          * Start observer which reacts to size changes of the JSXGraph
3370          * container div element. Calls updateContainerDims().
3371          * If not available, an event listener for the window-resize event is started.
3372          * On mobile devices also scrolling might trigger resizes.
3373          * However, resize events triggered by scrolling events should be ignored.
3374          * Therefore, also a scrollListener is started.
3375          * Resize can be controlled with the board attribute resize.
3376          *
3377          * @see JXG.Board#updateContainerDims
3378          * @see JXG.Board#resizeListener
3379          * @see JXG.Board#scrollListener
3380          * @see JXG.Board#resize
3381          *
3382          */
3383         startResizeObserver: function() {
3384             var that = this;
3385 
3386             if (!Env.isBrowser || !this.attr.resize || !this.attr.resize.enabled) {
3387                 return;
3388             }
3389 
3390             this.resizeObserver = new ResizeObserver(function(entries) {
3391                 if (!that._isResizing) {
3392                     that._isResizing = true;
3393                     window.setTimeout(function() {
3394                         try {
3395                             that.updateContainerDims();
3396                         } catch (err) {
3397                             that.stopResizeObserver();
3398                         } finally {
3399                             that._isResizing = false;
3400                         }
3401                     }, that.attr.resize.throttle);
3402                 }
3403             });
3404             this.resizeObserver.observe(this.containerObj);
3405         },
3406 
3407         /**
3408          * Stops the resize observer.
3409          * @see JXG.Board#startResizeObserver
3410          *
3411          */
3412         stopResizeObserver: function() {
3413             if (!Env.isBrowser || !this.attr.resize || !this.attr.resize.enabled) {
3414                 return;
3415             }
3416 
3417             if (Type.exists(this.resizeObserver)) {
3418                 this.resizeObserver.unobserve(this.containerObj);
3419             }
3420         },
3421 
3422         /**
3423          * Fallback solutions if there is no resizeObserver available in the browser.
3424          * Reacts to resize events of the window (only). Otherwise similar to
3425          * startResizeObserver(). To handle changes of the visibility
3426          * of the JSXGraph container element, additionally an intersection observer is used.
3427          * which watches changes in the visibility of the JSXGraph container element.
3428          * This is necessary e.g. for register tabs or dia shows.
3429          *
3430          * @see JXG.Board#startResizeObserver
3431          * @see JXG.Board#startIntersectionObserver
3432          */
3433         resizeListener: function() {
3434             var that = this;
3435 
3436             if (!Env.isBrowser || !this.attr.resize || !this.attr.resize.enabled) {
3437                 return;
3438             }
3439             if (!this._isScrolling && !this._isResizing) {
3440                 this._isResizing = true;
3441                 window.setTimeout(function() {
3442                     that.updateContainerDims();
3443                     that._isResizing = false;
3444                 }, this.attr.resize.throttle);
3445             }
3446         },
3447 
3448         /**
3449          * Listener to watch for scroll events. Sets board._isScrolling = true
3450          * @param  {Event} evt The browser's event object
3451          *
3452          * @see JXG.Board#startResizeObserver
3453          * @see JXG.Board#resizeListener
3454          *
3455          */
3456         scrollListener: function(evt) {
3457             var that = this;
3458 
3459             if (!Env.isBrowser) {
3460                 return;
3461             }
3462             if (!this._isScrolling) {
3463                 this._isScrolling = true;
3464                 window.setTimeout(function() {
3465                     that._isScrolling = false;
3466                 }, 66);
3467             }
3468         },
3469 
3470         /**
3471          * Watch for changes of the visibility of the JSXGraph container element.
3472          *
3473          * @see JXG.Board#startResizeObserver
3474          * @see JXG.Board#resizeListener
3475          *
3476          */
3477         startIntersectionObserver: function() {
3478             var that = this,
3479                 options = {
3480                     root: null,
3481                     rootMargin: '0px',
3482                     threshold: 0.8
3483                 };
3484 
3485             try {
3486                 this.intersectionObserver = new IntersectionObserver(function(entries) {
3487                     // If bounding box is not yet initialized, do it now.
3488                     if (isNaN(that.getBoundingBox()[0])) {
3489                         that.updateContainerDims();
3490                     }
3491                 }, options);
3492                 this.intersectionObserver.observe(that.containerObj);
3493             } catch (err) {
3494                 console.log('JSXGraph: IntersectionObserver not available in this browser.');
3495             }
3496         },
3497 
3498         /**
3499          * Stop the intersection observer
3500          *
3501          * @see JXG.Board#startIntersectionObserver
3502          *
3503          */
3504         stopIntersectionObserver: function() {
3505             if (Type.exists(this.intersectionObserver)) {
3506                 this.intersectionObserver.unobserve(this.containerObj);
3507             }
3508         },
3509 
3510         /**********************************************************
3511          *
3512          * End of Event Handlers
3513          *
3514          **********************************************************/
3515 
3516         /**
3517          * Initialize the info box object which is used to display
3518          * the coordinates of points near the mouse pointer,
3519          * @returns {JXG.Board} Reference to the board
3520         */
3521         initInfobox: function () {
3522             var  attr = Type.copyAttributes({}, this.options, 'infobox');
3523 
3524             attr.id = this.id + '_infobox';
3525             /**
3526              * Infobox close to points in which the points' coordinates are displayed.
3527              * This is simply a JXG.Text element. Access through board.infobox.
3528              * Uses CSS class .JXGinfobox.
3529              * @type JXG.Text
3530              *
3531              */
3532             this.infobox = this.create('text', [0, 0, '0,0'], attr);
3533 
3534             this.infobox.distanceX = -20;
3535             this.infobox.distanceY = 25;
3536             // this.infobox.needsUpdateSize = false;  // That is not true, but it speeds drawing up.
3537 
3538             this.infobox.dump = false;
3539 
3540             this.displayInfobox(false);
3541             return this;
3542         },
3543 
3544         /**
3545          * Updates and displays a little info box to show coordinates of current selected points.
3546          * @param {JXG.GeometryElement} el A GeometryElement
3547          * @returns {JXG.Board} Reference to the board
3548          * @see JXG.Board#displayInfobox
3549          * @see JXG.Board#showInfobox
3550          * @see Point#showInfobox
3551          *
3552          */
3553         updateInfobox: function (el) {
3554             var x, y, xc, yc,
3555             vpinfoboxdigits,
3556             vpsi = Type.evaluate(el.visProp.showinfobox);
3557 
3558             if ((!Type.evaluate(this.attr.showinfobox) &&  vpsi === 'inherit') ||
3559                 !vpsi) {
3560                 return this;
3561             }
3562 
3563             if (Type.isPoint(el)) {
3564                 xc = el.coords.usrCoords[1];
3565                 yc = el.coords.usrCoords[2];
3566 
3567                 vpinfoboxdigits = Type.evaluate(el.visProp.infoboxdigits);
3568                 this.infobox.setCoords(xc + this.infobox.distanceX / this.unitX,
3569                                        yc + this.infobox.distanceY / this.unitY);
3570 
3571                 if (typeof el.infoboxText !== 'string') {
3572                     if (vpinfoboxdigits === 'auto') {
3573                         x = Type.autoDigits(xc);
3574                         y = Type.autoDigits(yc);
3575                     } else if (Type.isNumber(vpinfoboxdigits)) {
3576                         x = Type.toFixed(xc, vpinfoboxdigits);
3577                         y = Type.toFixed(yc, vpinfoboxdigits);
3578                     } else {
3579                         x = xc;
3580                         y = yc;
3581                     }
3582 
3583                     this.highlightInfobox(x, y, el);
3584                 } else {
3585                     this.highlightCustomInfobox(el.infoboxText, el);
3586                 }
3587 
3588                 this.displayInfobox(true);
3589             }
3590             return this;
3591         },
3592 
3593         /**
3594          * Set infobox visible / invisible.
3595          *
3596          * It uses its property hiddenByParent to memorize its status.
3597          * In this way, many DOM access can be avoided.
3598          *
3599          * @param  {Boolean} val true for visible, false for invisible
3600          * @returns {JXG.Board} Reference to the board.
3601          * @see JXG.Board#updateInfobox
3602          *
3603          */
3604         displayInfobox: function(val) {
3605             if (this.infobox.hiddenByParent === val) {
3606                 this.infobox.hiddenByParent = !val;
3607                 this.infobox.prepareUpdate().updateVisibility(val).updateRenderer();
3608             }
3609             return this;
3610         },
3611 
3612         // Alias for displayInfobox to be backwards compatible.
3613         // The method showInfobox clashes with the board attribute showInfobox
3614         showInfobox: function(val) {
3615             return this.displayInfobox(val);
3616         },
3617 
3618         /**
3619          * Changes the text of the info box to show the given coordinates.
3620          * @param {Number} x
3621          * @param {Number} y
3622          * @param {JXG.GeometryElement} [el] The element the mouse is pointing at
3623          * @returns {JXG.Board} Reference to the board.
3624          */
3625         highlightInfobox: function (x, y, el) {
3626             this.highlightCustomInfobox('(' + x + ', ' + y + ')', el);
3627             return this;
3628         },
3629 
3630         /**
3631          * Changes the text of the info box to what is provided via text.
3632          * @param {String} text
3633          * @param {JXG.GeometryElement} [el]
3634          * @returns {JXG.Board} Reference to the board.
3635          */
3636         highlightCustomInfobox: function (text, el) {
3637             this.infobox.setText(text);
3638             return this;
3639         },
3640 
3641         /**
3642          * Remove highlighting of all elements.
3643          * @returns {JXG.Board} Reference to the board.
3644          */
3645         dehighlightAll: function () {
3646             var el, pEl, needsDehighlight = false;
3647 
3648             for (el in this.highlightedObjects) {
3649                 if (this.highlightedObjects.hasOwnProperty(el)) {
3650                     pEl = this.highlightedObjects[el];
3651 
3652                     if (this.hasMouseHandlers || this.hasPointerHandlers) {
3653                         pEl.noHighlight();
3654                     }
3655 
3656                     needsDehighlight = true;
3657 
3658                     // In highlightedObjects should only be objects which fulfill all these conditions
3659                     // And in case of complex elements, like a turtle based fractal, it should be faster to
3660                     // just de-highlight the element instead of checking hasPoint...
3661                     // if ((!Type.exists(pEl.hasPoint)) || !pEl.hasPoint(x, y) || !pEl.visPropCalc.visible)
3662                 }
3663             }
3664 
3665             this.highlightedObjects = {};
3666 
3667             // We do not need to redraw during dehighlighting in CanvasRenderer
3668             // because we are redrawing anyhow
3669             //  -- We do need to redraw during dehighlighting. Otherwise objects won't be dehighlighted until
3670             // another object is highlighted.
3671             if (this.renderer.type === 'canvas' && needsDehighlight) {
3672                 this.prepareUpdate();
3673                 this.renderer.suspendRedraw(this);
3674                 this.updateRenderer();
3675                 this.renderer.unsuspendRedraw();
3676             }
3677 
3678             return this;
3679         },
3680 
3681         /**
3682          * Returns the input parameters in an array. This method looks pointless and it really is, but it had a purpose
3683          * once.
3684          * @private
3685          * @param {Number} x X coordinate in screen coordinates
3686          * @param {Number} y Y coordinate in screen coordinates
3687          * @returns {Array} Coordinates [x, y] of the mouse in screen coordinates.
3688          * @see JXG.Board#getUsrCoordsOfMouse
3689          */
3690         getScrCoordsOfMouse: function (x, y) {
3691             return [x, y];
3692         },
3693 
3694         /**
3695          * This method calculates the user coords of the current mouse coordinates.
3696          * @param {Event} evt Event object containing the mouse coordinates.
3697          * @returns {Array} Coordinates [x, y] of the mouse in user coordinates.
3698          * @example
3699          * board.on('up', function (evt) {
3700          *         var a = board.getUsrCoordsOfMouse(evt),
3701          *             x = a[0],
3702          *             y = a[1],
3703          *             somePoint = board.create('point', [x,y], {name:'SomePoint',size:4});
3704          *             // Shorter version:
3705          *             //somePoint = board.create('point', a, {name:'SomePoint',size:4});
3706          *         });
3707          *
3708          * </pre><div id="JXG48d5066b-16ba-4920-b8ea-a4f8eff6b746" class="jxgbox" style="width: 300px; height: 300px;"></div>
3709          * <script type="text/javascript">
3710          *     (function() {
3711          *         var board = JXG.JSXGraph.initBoard('JXG48d5066b-16ba-4920-b8ea-a4f8eff6b746',
3712          *             {boundingbox: [-8, 8, 8,-8], axis: true, showcopyright: false, shownavigation: false});
3713          *     board.on('up', function (evt) {
3714          *             var a = board.getUsrCoordsOfMouse(evt),
3715          *                 x = a[0],
3716          *                 y = a[1],
3717          *                 somePoint = board.create('point', [x,y], {name:'SomePoint',size:4});
3718          *                 // Shorter version:
3719          *                 //somePoint = board.create('point', a, {name:'SomePoint',size:4});
3720          *             });
3721          *
3722          *     })();
3723          *
3724          * </script><pre>
3725          *
3726          * @see JXG.Board#getScrCoordsOfMouse
3727          * @see JXG.Board#getAllUnderMouse
3728          */
3729         getUsrCoordsOfMouse: function (evt) {
3730             var cPos = this.getCoordsTopLeftCorner(),
3731                 absPos = Env.getPosition(evt, null, this.document),
3732                 x = absPos[0] - cPos[0],
3733                 y = absPos[1] - cPos[1],
3734                 newCoords = new Coords(Const.COORDS_BY_SCREEN, [x, y], this);
3735 
3736             return newCoords.usrCoords.slice(1);
3737         },
3738 
3739         /**
3740          * Collects all elements under current mouse position plus current user coordinates of mouse cursor.
3741          * @param {Event} evt Event object containing the mouse coordinates.
3742          * @returns {Array} Array of elements at the current mouse position plus current user coordinates of mouse.
3743          * @see JXG.Board#getUsrCoordsOfMouse
3744          * @see JXG.Board#getAllObjectsUnderMouse
3745          */
3746         getAllUnderMouse: function (evt) {
3747             var elList = this.getAllObjectsUnderMouse(evt);
3748             elList.push(this.getUsrCoordsOfMouse(evt));
3749 
3750             return elList;
3751         },
3752 
3753         /**
3754          * Collects all elements under current mouse position.
3755          * @param {Event} evt Event object containing the mouse coordinates.
3756          * @returns {Array} Array of elements at the current mouse position.
3757          * @see JXG.Board#getAllUnderMouse
3758          */
3759         getAllObjectsUnderMouse: function (evt) {
3760             var cPos = this.getCoordsTopLeftCorner(),
3761                 absPos = Env.getPosition(evt, null, this.document),
3762                 dx = absPos[0] - cPos[0],
3763                 dy = absPos[1] - cPos[1],
3764                 elList = [],
3765                 el,
3766                 pEl,
3767                 len = this.objectsList.length;
3768 
3769             for (el = 0; el < len; el++) {
3770                 pEl = this.objectsList[el];
3771                 if (pEl.visPropCalc.visible && pEl.hasPoint && pEl.hasPoint(dx, dy)) {
3772                     elList[elList.length] = pEl;
3773                 }
3774             }
3775 
3776             return elList;
3777         },
3778 
3779         /**
3780          * Update the coords object of all elements which possess this
3781          * property. This is necessary after changing the viewport.
3782          * @returns {JXG.Board} Reference to this board.
3783          **/
3784         updateCoords: function () {
3785             var el, ob, len = this.objectsList.length;
3786 
3787             for (ob = 0; ob < len; ob++) {
3788                 el = this.objectsList[ob];
3789 
3790                 if (Type.exists(el.coords)) {
3791                     if (Type.evaluate(el.visProp.frozen)) {
3792                         el.coords.screen2usr();
3793                     } else {
3794                         el.coords.usr2screen();
3795                     }
3796                 }
3797             }
3798             return this;
3799         },
3800 
3801         /**
3802          * Moves the origin and initializes an update of all elements.
3803          * @param {Number} x
3804          * @param {Number} y
3805          * @param {Boolean} [diff=false]
3806          * @returns {JXG.Board} Reference to this board.
3807          */
3808         moveOrigin: function (x, y, diff) {
3809             var ox, oy, ul, lr;
3810             if (Type.exists(x) && Type.exists(y)) {
3811                 ox = this.origin.scrCoords[1];
3812                 oy = this.origin.scrCoords[2];
3813 
3814                 this.origin.scrCoords[1] = x;
3815                 this.origin.scrCoords[2] = y;
3816 
3817                 if (diff) {
3818                     this.origin.scrCoords[1] -= this.drag_dx;
3819                     this.origin.scrCoords[2] -= this.drag_dy;
3820                 }
3821 
3822                 ul = (new Coords(Const.COORDS_BY_SCREEN, [0, 0], this)).usrCoords;
3823                 lr = (new Coords(Const.COORDS_BY_SCREEN, [this.canvasWidth, this.canvasHeight], this)).usrCoords;
3824                 if (ul[1] < this.maxboundingbox[0] ||
3825                     ul[2] > this.maxboundingbox[1] ||
3826                     lr[1] > this.maxboundingbox[2] ||
3827                     lr[2] < this.maxboundingbox[3]) {
3828 
3829                     this.origin.scrCoords[1] = ox;
3830                     this.origin.scrCoords[2] = oy;
3831                 }
3832             }
3833 
3834             this.updateCoords().clearTraces().fullUpdate();
3835             this.triggerEventHandlers(['boundingbox']);
3836 
3837             return this;
3838         },
3839 
3840         /**
3841          * Add conditional updates to the elements.
3842          * @param {String} str String containing coniditional update in geonext syntax
3843          */
3844         addConditions: function (str) {
3845             var term, m, left, right, name, el, property,
3846                 functions = [],
3847                 // plaintext = 'var el, x, y, c, rgbo;\n',
3848                 i = str.indexOf('<data>'),
3849                 j = str.indexOf('<' + '/data>'),
3850 
3851                 xyFun = function (board, el, f, what) {
3852                     return function () {
3853                         var e, t;
3854 
3855                         e = board.select(el.id);
3856                         t = e.coords.usrCoords[what];
3857 
3858                         if (what === 2) {
3859                             e.setPositionDirectly(Const.COORDS_BY_USER, [f(), t]);
3860                         } else {
3861                             e.setPositionDirectly(Const.COORDS_BY_USER, [t, f()]);
3862                         }
3863                         e.prepareUpdate().update();
3864                     };
3865                 },
3866 
3867                 visFun = function (board, el, f) {
3868                     return function () {
3869                         var e, v;
3870 
3871                         e = board.select(el.id);
3872                         v = f();
3873 
3874                         e.setAttribute({visible: v});
3875                     };
3876                 },
3877 
3878                 colFun = function (board, el, f, what) {
3879                     return function () {
3880                         var e, v;
3881 
3882                         e = board.select(el.id);
3883                         v = f();
3884 
3885                         if (what === 'strokewidth') {
3886                             e.visProp.strokewidth = v;
3887                         } else {
3888                             v = Color.rgba2rgbo(v);
3889                             e.visProp[what + 'color'] = v[0];
3890                             e.visProp[what + 'opacity'] = v[1];
3891                         }
3892                     };
3893                 },
3894 
3895                 posFun = function (board, el, f) {
3896                     return function () {
3897                         var e = board.select(el.id);
3898 
3899                         e.position = f();
3900                     };
3901                 },
3902 
3903                 styleFun = function (board, el, f) {
3904                     return function () {
3905                         var e = board.select(el.id);
3906 
3907                         e.setStyle(f());
3908                     };
3909                 };
3910 
3911             if (i < 0) {
3912                 return;
3913             }
3914 
3915             while (i >= 0) {
3916                 term = str.slice(i + 6, j);   // throw away <data>
3917                 m = term.indexOf('=');
3918                 left = term.slice(0, m);
3919                 right = term.slice(m + 1);
3920                 m = left.indexOf('.');     // Dies erzeugt Probleme bei Variablennamen der Form " Steuern akt."
3921                 name = left.slice(0, m);    //.replace(/\s+$/,''); // do NOT cut out name (with whitespace)
3922                 el = this.elementsByName[Type.unescapeHTML(name)];
3923 
3924                 property = left.slice(m + 1).replace(/\s+/g, '').toLowerCase(); // remove whitespace in property
3925                 right = Type.createFunction (right, this, '', true);
3926 
3927                 // Debug
3928                 if (!Type.exists(this.elementsByName[name])) {
3929                     JXG.debug("debug conditions: |" + name + "| undefined");
3930                 } else {
3931                     // plaintext += "el = this.objects[\"" + el.id + "\"];\n";
3932 
3933                     switch (property) {
3934                     case 'x':
3935                         functions.push(xyFun(this, el, right, 2));
3936                         break;
3937                     case 'y':
3938                         functions.push(xyFun(this, el, right, 1));
3939                         break;
3940                     case 'visible':
3941                         functions.push(visFun(this, el, right));
3942                         break;
3943                     case 'position':
3944                         functions.push(posFun(this, el, right));
3945                         break;
3946                     case 'stroke':
3947                         functions.push(colFun(this, el, right, 'stroke'));
3948                         break;
3949                     case 'style':
3950                         functions.push(styleFun(this, el, right));
3951                         break;
3952                     case 'strokewidth':
3953                         functions.push(colFun(this, el, right, 'strokewidth'));
3954                         break;
3955                     case 'fill':
3956                         functions.push(colFun(this, el, right, 'fill'));
3957                         break;
3958                     case 'label':
3959                         break;
3960                     default:
3961                         JXG.debug("property '" + property + "' in conditions not yet implemented:" + right);
3962                         break;
3963                     }
3964                 }
3965                 str = str.slice(j + 7); // cut off "</data>"
3966                 i = str.indexOf('<data>');
3967                 j = str.indexOf('<' + '/data>');
3968             }
3969 
3970             this.updateConditions = function () {
3971                 var i;
3972 
3973                 for (i = 0; i < functions.length; i++) {
3974                     functions[i]();
3975                 }
3976 
3977                 this.prepareUpdate().updateElements();
3978                 return true;
3979             };
3980             this.updateConditions();
3981         },
3982 
3983         /**
3984          * Computes the commands in the conditions-section of the gxt file.
3985          * It is evaluated after an update, before the unsuspendRedraw.
3986          * The function is generated in
3987          * @see JXG.Board#addConditions
3988          * @private
3989          */
3990         updateConditions: function () {
3991             return false;
3992         },
3993 
3994         /**
3995          * Calculates adequate snap sizes.
3996          * @returns {JXG.Board} Reference to the board.
3997          */
3998         calculateSnapSizes: function () {
3999             var p1 = new Coords(Const.COORDS_BY_USER, [0, 0], this),
4000                 p2 = new Coords(Const.COORDS_BY_USER, [this.options.grid.gridX, this.options.grid.gridY], this),
4001                 x = p1.scrCoords[1] - p2.scrCoords[1],
4002                 y = p1.scrCoords[2] - p2.scrCoords[2];
4003 
4004             this.options.grid.snapSizeX = this.options.grid.gridX;
4005             while (Math.abs(x) > 25) {
4006                 this.options.grid.snapSizeX *= 2;
4007                 x /= 2;
4008             }
4009 
4010             this.options.grid.snapSizeY = this.options.grid.gridY;
4011             while (Math.abs(y) > 25) {
4012                 this.options.grid.snapSizeY *= 2;
4013                 y /= 2;
4014             }
4015 
4016             return this;
4017         },
4018 
4019         /**
4020          * Apply update on all objects with the new zoom-factors. Clears all traces.
4021          * @returns {JXG.Board} Reference to the board.
4022          */
4023         applyZoom: function () {
4024             this.updateCoords().calculateSnapSizes().clearTraces().fullUpdate();
4025 
4026             return this;
4027         },
4028 
4029         /**
4030          * Zooms into the board by the factors board.attr.zoom.factorX and board.attr.zoom.factorY and applies the zoom.
4031          * The zoom operation is centered at x, y.
4032          * @param {Number} [x]
4033          * @param {Number} [y]
4034          * @returns {JXG.Board} Reference to the board
4035          */
4036         zoomIn: function (x, y) {
4037             var bb = this.getBoundingBox(),
4038                 zX = this.attr.zoom.factorx,
4039                 zY = this.attr.zoom.factory,
4040                 dX = (bb[2] - bb[0]) * (1.0 - 1.0 / zX),
4041                 dY = (bb[1] - bb[3]) * (1.0 - 1.0 / zY),
4042                 lr = 0.5,
4043                 tr = 0.5,
4044                 mi = this.attr.zoom.eps || this.attr.zoom.min || 0.001;  // this.attr.zoom.eps is deprecated
4045 
4046             if ((this.zoomX > this.attr.zoom.max && zX > 1.0) ||
4047                 (this.zoomY > this.attr.zoom.max && zY > 1.0) ||
4048                 (this.zoomX < mi && zX < 1.0) ||  // zoomIn is used for all zooms on touch devices
4049                 (this.zoomY < mi && zY < 1.0)) {
4050                 return this;
4051             }
4052 
4053             if (Type.isNumber(x) && Type.isNumber(y)) {
4054                 lr = (x - bb[0]) / (bb[2] - bb[0]);
4055                 tr = (bb[1] - y) / (bb[1] - bb[3]);
4056             }
4057 
4058             this.setBoundingBox([bb[0] + dX * lr, bb[1] - dY * tr, bb[2] - dX * (1 - lr), bb[3] + dY * (1 - tr)], this.keepaspectratio, 'update');
4059             return this.applyZoom();
4060         },
4061 
4062         /**
4063          * Zooms out of the board by the factors board.attr.zoom.factorX and board.attr.zoom.factorY and applies the zoom.
4064          * The zoom operation is centered at x, y.
4065          *
4066          * @param {Number} [x]
4067          * @param {Number} [y]
4068          * @returns {JXG.Board} Reference to the board
4069          */
4070         zoomOut: function (x, y) {
4071             var bb = this.getBoundingBox(),
4072                 zX = this.attr.zoom.factorx,
4073                 zY = this.attr.zoom.factory,
4074                 dX = (bb[2] - bb[0]) * (1.0 - zX),
4075                 dY = (bb[1] - bb[3]) * (1.0 - zY),
4076                 lr = 0.5,
4077                 tr = 0.5,
4078                 mi = this.attr.zoom.eps || this.attr.zoom.min || 0.001;  // this.attr.zoom.eps is deprecated
4079 
4080             if (this.zoomX < mi || this.zoomY < mi) {
4081                 return this;
4082             }
4083 
4084             if (Type.isNumber(x) && Type.isNumber(y)) {
4085                 lr = (x - bb[0]) / (bb[2] - bb[0]);
4086                 tr = (bb[1] - y) / (bb[1] - bb[3]);
4087             }
4088 
4089             this.setBoundingBox([bb[0] + dX * lr, bb[1] - dY * tr, bb[2] - dX * (1 - lr), bb[3] + dY * (1 - tr)], this.keepaspectratio, 'update');
4090 
4091             return this.applyZoom();
4092         },
4093 
4094         /**
4095          * Reset the zoom level to the original zoom level from initBoard();
4096          * Additionally, if the board as been initialized with a boundingBox (which is the default),
4097          * restore the viewport to the original viewport during initialization. Otherwise,
4098          * (i.e. if the board as been initialized with unitX/Y and originX/Y),
4099          * just set the zoom level to 100%.
4100          *
4101          * @returns {JXG.Board} Reference to the board
4102          */
4103         zoom100: function () {
4104             var bb, dX, dY;
4105 
4106             if (Type.exists(this.attr.boundingbox)) {
4107                 this.setBoundingBox(this.attr.boundingbox, this.keepaspectratio, 'reset');
4108             } else {
4109                 // Board has been set up with unitX/Y and originX/Y
4110                 bb = this.getBoundingBox();
4111                 dX = (bb[2] - bb[0]) * (1.0 - this.zoomX) * 0.5;
4112                 dY = (bb[1] - bb[3]) * (1.0 - this.zoomY) * 0.5;
4113                 this.setBoundingBox([bb[0] + dX, bb[1] - dY, bb[2] - dX, bb[3] + dY], this.keepaspectratio, 'reset');
4114             }
4115             return this.applyZoom();
4116         },
4117 
4118         /**
4119          * Zooms the board so every visible point is shown. Keeps aspect ratio.
4120          * @returns {JXG.Board} Reference to the board
4121          */
4122         zoomAllPoints: function () {
4123             var el, border, borderX, borderY, pEl,
4124                 minX = 0,
4125                 maxX = 0,
4126                 minY = 0,
4127                 maxY = 0,
4128                 len = this.objectsList.length;
4129 
4130             for (el = 0; el < len; el++) {
4131                 pEl = this.objectsList[el];
4132 
4133                 if (Type.isPoint(pEl) && pEl.visPropCalc.visible) {
4134                     if (pEl.coords.usrCoords[1] < minX) {
4135                         minX = pEl.coords.usrCoords[1];
4136                     } else if (pEl.coords.usrCoords[1] > maxX) {
4137                         maxX = pEl.coords.usrCoords[1];
4138                     }
4139                     if (pEl.coords.usrCoords[2] > maxY) {
4140                         maxY = pEl.coords.usrCoords[2];
4141                     } else if (pEl.coords.usrCoords[2] < minY) {
4142                         minY = pEl.coords.usrCoords[2];
4143                     }
4144                 }
4145             }
4146 
4147             border = 50;
4148             borderX = border / this.unitX;
4149             borderY = border / this.unitY;
4150 
4151             this.setBoundingBox([minX - borderX, maxY + borderY, maxX + borderX, minY - borderY], this.keepaspectratio, 'update');
4152 
4153             return this.applyZoom();
4154         },
4155 
4156         /**
4157          * Reset the bounding box and the zoom level to 100% such that a given set of elements is
4158          * within the board's viewport.
4159          * @param {Array} elements A set of elements given by id, reference, or name.
4160          * @returns {JXG.Board} Reference to the board.
4161          */
4162         zoomElements: function (elements) {
4163             var i, e, box,
4164                 newBBox = [Infinity, -Infinity, -Infinity, Infinity],
4165                 cx, cy, dx, dy, d;
4166 
4167             if (!Type.isArray(elements) || elements.length === 0) {
4168                 return this;
4169             }
4170 
4171             for (i = 0; i < elements.length; i++) {
4172                 e = this.select(elements[i]);
4173 
4174                 box = e.bounds();
4175                 if (Type.isArray(box)) {
4176                     if (box[0] < newBBox[0]) { newBBox[0] = box[0]; }
4177                     if (box[1] > newBBox[1]) { newBBox[1] = box[1]; }
4178                     if (box[2] > newBBox[2]) { newBBox[2] = box[2]; }
4179                     if (box[3] < newBBox[3]) { newBBox[3] = box[3]; }
4180                 }
4181             }
4182 
4183             if (Type.isArray(newBBox)) {
4184                 cx = 0.5 * (newBBox[0] + newBBox[2]);
4185                 cy = 0.5 * (newBBox[1] + newBBox[3]);
4186                 dx = 1.5 * (newBBox[2] - newBBox[0]) * 0.5;
4187                 dy = 1.5 * (newBBox[1] - newBBox[3]) * 0.5;
4188                 d = Math.max(dx, dy);
4189                 this.setBoundingBox([cx - d, cy + d, cx + d, cy - d], this.keepaspectratio, 'update');
4190             }
4191 
4192             return this;
4193         },
4194 
4195         /**
4196          * Sets the zoom level to <tt>fX</tt> resp <tt>fY</tt>.
4197          * @param {Number} fX
4198          * @param {Number} fY
4199          * @returns {JXG.Board} Reference to the board.
4200          */
4201         setZoom: function (fX, fY) {
4202             var oX = this.attr.zoom.factorx,
4203                 oY = this.attr.zoom.factory;
4204 
4205             this.attr.zoom.factorx = fX / this.zoomX;
4206             this.attr.zoom.factory = fY / this.zoomY;
4207 
4208             this.zoomIn();
4209 
4210             this.attr.zoom.factorx = oX;
4211             this.attr.zoom.factory = oY;
4212 
4213             return this;
4214         },
4215 
4216         /**
4217          * Removes object from board and renderer.
4218          * <p>
4219          * <b>Performance hints:</b> It is recommended to use the object's id.
4220          * If many elements are removed, it is best to call <tt>board.suspendUpdate()</tt>
4221          * before looping through the elements to be removed and call
4222          * <tt>board.unsuspendUpdate()</tt> after the loop. Further, it is advisable to loop
4223          * in reverse order, i.e. remove the object in reverse order of their creation time.
4224          *
4225          * @param {JXG.GeometryElement|Array} object The object to remove or array of objects to be removed.
4226          * The element(s) is/are given by name, id or a reference.
4227          * @param {Boolean} saveMethod If true, the algorithm runs through all elements
4228          * and tests if the element to be deleted is a child element. If yes, it will be
4229          * removed from the list of child elements. If false (default), the element
4230          * is removed from the lists of child elements of all its ancestors.
4231          * This should be much faster.
4232          * @returns {JXG.Board} Reference to the board
4233          */
4234         removeObject: function (object, saveMethod) {
4235             var el, i;
4236 
4237             if (Type.isArray(object)) {
4238                 for (i = 0; i < object.length; i++) {
4239                     this.removeObject(object[i]);
4240                 }
4241 
4242                 return this;
4243             }
4244 
4245             object = this.select(object);
4246 
4247             // If the object which is about to be removed unknown or a string, do nothing.
4248             // it is a string if a string was given and could not be resolved to an element.
4249             if (!Type.exists(object) || Type.isString(object)) {
4250                 return this;
4251             }
4252 
4253             try {
4254                 // remove all children.
4255                 for (el in object.childElements) {
4256                     if (object.childElements.hasOwnProperty(el)) {
4257                         object.childElements[el].board.removeObject(object.childElements[el]);
4258                     }
4259                 }
4260 
4261                 // Remove all children in elements like turtle
4262                 for (el in object.objects) {
4263                     if (object.objects.hasOwnProperty(el)) {
4264                         object.objects[el].board.removeObject(object.objects[el]);
4265                     }
4266                 }
4267 
4268                 // Remove the element from the childElement list and the descendant list of all elements.
4269                 if (saveMethod) {
4270                     // Running through all objects has quadratic complexity if many objects are deleted.
4271                     for (el in this.objects) {
4272                         if (this.objects.hasOwnProperty(el)) {
4273                             if (Type.exists(this.objects[el].childElements) &&
4274                                 Type.exists(this.objects[el].childElements.hasOwnProperty(object.id))
4275                             ) {
4276                                 delete this.objects[el].childElements[object.id];
4277                                 delete this.objects[el].descendants[object.id];
4278                             }
4279                         }
4280                     }
4281                 } else if (Type.exists(object.ancestors)) {
4282                     // Running through the ancestors should be much more efficient.
4283                     for (el in object.ancestors) {
4284                         if (object.ancestors.hasOwnProperty(el)) {
4285                             if (Type.exists(object.ancestors[el].childElements) &&
4286                                 Type.exists(object.ancestors[el].childElements.hasOwnProperty(object.id))
4287                             ) {
4288                                 delete object.ancestors[el].childElements[object.id];
4289                                 delete object.ancestors[el].descendants[object.id];
4290                             }
4291                         }
4292                     }
4293                 }
4294 
4295                 // remove the object itself from our control structures
4296                 if (object._pos > -1) {
4297                     this.objectsList.splice(object._pos, 1);
4298                     for (el = object._pos; el < this.objectsList.length; el++) {
4299                         this.objectsList[el]._pos--;
4300                     }
4301                 } else if (object.type !== Const.OBJECT_TYPE_TURTLE) {
4302                     JXG.debug('Board.removeObject: object ' + object.id + ' not found in list.');
4303                 }
4304 
4305                 delete this.objects[object.id];
4306                 delete this.elementsByName[object.name];
4307 
4308                 if (object.visProp && Type.evaluate(object.visProp.trace)) {
4309                     object.clearTrace();
4310                 }
4311 
4312                 // the object deletion itself is handled by the object.
4313                 if (Type.exists(object.remove)) {
4314                     object.remove();
4315                 }
4316             } catch (e) {
4317                 JXG.debug(object.id + ': Could not be removed: ' + e);
4318             }
4319 
4320             this.update();
4321 
4322             return this;
4323         },
4324 
4325         /**
4326          * Removes the ancestors of an object an the object itself from board and renderer.
4327          * @param {JXG.GeometryElement} object The object to remove.
4328          * @returns {JXG.Board} Reference to the board
4329          */
4330         removeAncestors: function (object) {
4331             var anc;
4332 
4333             for (anc in object.ancestors) {
4334                 if (object.ancestors.hasOwnProperty(anc)) {
4335                     this.removeAncestors(object.ancestors[anc]);
4336                 }
4337             }
4338 
4339             this.removeObject(object);
4340 
4341             return this;
4342         },
4343 
4344         /**
4345          * Initialize some objects which are contained in every GEONExT construction by default,
4346          * but are not contained in the gxt files.
4347          * @returns {JXG.Board} Reference to the board
4348          */
4349         initGeonextBoard: function () {
4350             var p1, p2, p3;
4351 
4352             p1 = this.create('point', [0, 0], {
4353                 id: this.id + 'g00e0',
4354                 name: 'Ursprung',
4355                 withLabel: false,
4356                 visible: false,
4357                 fixed: true
4358             });
4359 
4360             p2 = this.create('point', [1, 0], {
4361                 id: this.id + 'gX0e0',
4362                 name: 'Punkt_1_0',
4363                 withLabel: false,
4364                 visible: false,
4365                 fixed: true
4366             });
4367 
4368             p3 = this.create('point', [0, 1], {
4369                 id: this.id + 'gY0e0',
4370                 name: 'Punkt_0_1',
4371                 withLabel: false,
4372                 visible: false,
4373                 fixed: true
4374             });
4375 
4376             this.create('line', [p1, p2], {
4377                 id: this.id + 'gXLe0',
4378                 name: 'X-Achse',
4379                 withLabel: false,
4380                 visible: false
4381             });
4382 
4383             this.create('line', [p1, p3], {
4384                 id: this.id + 'gYLe0',
4385                 name: 'Y-Achse',
4386                 withLabel: false,
4387                 visible: false
4388             });
4389 
4390             return this;
4391         },
4392 
4393         /**
4394          * Change the height and width of the board's container.
4395          * After doing so, {@link JXG.JSXGraph.setBoundingBox} is called using
4396          * the actual size of the bounding box and the actual value of keepaspectratio.
4397          * If setBoundingbox() should not be called automatically,
4398          * call resizeContainer with dontSetBoundingBox == true.
4399          * @param {Number} canvasWidth New width of the container.
4400          * @param {Number} canvasHeight New height of the container.
4401          * @param {Boolean} [dontset=false] If true do not set the CSS width and height of the DOM element.
4402          * @param {Boolean} [dontSetBoundingBox=false] If true do not call setBoundingBox().
4403          * @returns {JXG.Board} Reference to the board
4404          */
4405         resizeContainer: function (canvasWidth, canvasHeight, dontset, dontSetBoundingBox) {
4406             var box;
4407                 // w, h, cx, cy;
4408                 // box_act,
4409                 // shift_x = 0,
4410                 // shift_y = 0;
4411 
4412             if (!dontSetBoundingBox) {
4413                 // box_act = this.getBoundingBox();    // This is the actual bounding box.
4414                 box = this.getBoundingBox();    // This is the actual bounding box.
4415             }
4416 
4417             this.canvasWidth = parseFloat(canvasWidth);
4418             this.canvasHeight = parseFloat(canvasHeight);
4419 
4420             // if (!dontSetBoundingBox) {
4421             //     box     = this.attr.boundingbox;    // This is the intended bounding box.
4422 
4423             //     // The shift values compensate the follow-up correction
4424             //     // in setBoundingBox in case of "this.keepaspectratio==true"
4425             //     // Otherwise, shift_x and shift_y will be zero.
4426             //     // Obsolet since setBoundingBox centers in case of "this.keepaspectratio==true".
4427             //     // shift_x = box_act[0] - box[0] / this.zoomX;
4428             //     // shift_y = box_act[1] - box[1] / this.zoomY;
4429 
4430             //     cx = (box[2] + box[0]) * 0.5; // + shift_x;
4431             //     cy = (box[3] + box[1]) * 0.5; // + shift_y;
4432 
4433             //     w = (box[2] - box[0]) * 0.5 / this.zoomX;
4434             //     h = (box[1] - box[3]) * 0.5 / this.zoomY;
4435 
4436             //     box = [cx - w, cy + h, cx + w, cy - h];
4437             // }
4438 
4439             if (!dontset) {
4440                 this.containerObj.style.width = (this.canvasWidth) + 'px';
4441                 this.containerObj.style.height = (this.canvasHeight) + 'px';
4442             }
4443             this.renderer.resize(this.canvasWidth, this.canvasHeight);
4444 
4445             if (!dontSetBoundingBox) {
4446                 this.setBoundingBox(box, this.keepaspectratio, 'keep');
4447             }
4448 
4449             return this;
4450         },
4451 
4452         /**
4453          * Lists the dependencies graph in a new HTML-window.
4454          * @returns {JXG.Board} Reference to the board
4455          */
4456         showDependencies: function () {
4457             var el, t, c, f, i;
4458 
4459             t = '<p>\n';
4460             for (el in this.objects) {
4461                 if (this.objects.hasOwnProperty(el)) {
4462                     i = 0;
4463                     for (c in this.objects[el].childElements) {
4464                         if (this.objects[el].childElements.hasOwnProperty(c)) {
4465                             i += 1;
4466                         }
4467                     }
4468                     if (i >= 0) {
4469                         t += '<strong>' + this.objects[el].id + ':<' + '/strong> ';
4470                     }
4471 
4472                     for (c in this.objects[el].childElements) {
4473                         if (this.objects[el].childElements.hasOwnProperty(c)) {
4474                             t += this.objects[el].childElements[c].id + '(' + this.objects[el].childElements[c].name + ')' + ', ';
4475                         }
4476                     }
4477                     t += '<p>\n';
4478                 }
4479             }
4480             t += '<' + '/p>\n';
4481             f = window.open();
4482             f.document.open();
4483             f.document.write(t);
4484             f.document.close();
4485             return this;
4486         },
4487 
4488         /**
4489          * Lists the XML code of the construction in a new HTML-window.
4490          * @returns {JXG.Board} Reference to the board
4491          */
4492         showXML: function () {
4493             var f = window.open('');
4494             f.document.open();
4495             f.document.write('<pre>' + Type.escapeHTML(this.xmlString) + '<' + '/pre>');
4496             f.document.close();
4497             return this;
4498         },
4499 
4500         /**
4501          * Sets for all objects the needsUpdate flag to "true".
4502          * @returns {JXG.Board} Reference to the board
4503          */
4504         prepareUpdate: function () {
4505             var el, pEl, len = this.objectsList.length;
4506 
4507             /*
4508             if (this.attr.updatetype === 'hierarchical') {
4509                 return this;
4510             }
4511             */
4512 
4513             for (el = 0; el < len; el++) {
4514                 pEl = this.objectsList[el];
4515                 pEl.needsUpdate = pEl.needsRegularUpdate || this.needsFullUpdate;
4516             }
4517 
4518             for (el in this.groups) {
4519                 if (this.groups.hasOwnProperty(el)) {
4520                     pEl = this.groups[el];
4521                     pEl.needsUpdate = pEl.needsRegularUpdate || this.needsFullUpdate;
4522                 }
4523             }
4524 
4525             return this;
4526         },
4527 
4528         /**
4529          * Runs through all elements and calls their update() method.
4530          * @param {JXG.GeometryElement} drag Element that caused the update.
4531          * @returns {JXG.Board} Reference to the board
4532          */
4533         updateElements: function (drag) {
4534             var el, pEl;
4535             //var childId, i = 0;
4536 
4537             drag = this.select(drag);
4538 
4539             /*
4540             if (Type.exists(drag)) {
4541                 for (el = 0; el < this.objectsList.length; el++) {
4542                     pEl = this.objectsList[el];
4543                     if (pEl.id === drag.id) {
4544                         i = el;
4545                         break;
4546                     }
4547                 }
4548             }
4549             */
4550 
4551             for (el = 0; el < this.objectsList.length; el++) {
4552                 pEl = this.objectsList[el];
4553                 if (this.needsFullUpdate && pEl.elementClass === Const.OBJECT_CLASS_TEXT) {
4554                     pEl.updateSize();
4555                 }
4556 
4557                 // For updates of an element we distinguish if the dragged element is updated or
4558                 // other elements are updated.
4559                 // The difference lies in the treatment of gliders and points based on transformations.
4560                 pEl.update(!Type.exists(drag) || pEl.id !== drag.id)
4561                    .updateVisibility();
4562             }
4563 
4564             // update groups last
4565             for (el in this.groups) {
4566                 if (this.groups.hasOwnProperty(el)) {
4567                     this.groups[el].update(drag);
4568                 }
4569             }
4570 
4571             return this;
4572         },
4573 
4574         /**
4575          * Runs through all elements and calls their update() method.
4576          * @returns {JXG.Board} Reference to the board
4577          */
4578         updateRenderer: function () {
4579             var el,
4580                 len = this.objectsList.length;
4581 
4582             /*
4583             objs = this.objectsList.slice(0);
4584             objs.sort(function (a, b) {
4585                 if (a.visProp.layer < b.visProp.layer) {
4586                     return -1;
4587                 } else if (a.visProp.layer === b.visProp.layer) {
4588                     return b.lastDragTime.getTime() - a.lastDragTime.getTime();
4589                 } else {
4590                     return 1;
4591                 }
4592             });
4593             */
4594 
4595             if (this.renderer.type === 'canvas') {
4596                 this.updateRendererCanvas();
4597             } else {
4598                 for (el = 0; el < len; el++) {
4599                     this.objectsList[el].updateRenderer();
4600                 }
4601             }
4602             return this;
4603         },
4604 
4605         /**
4606          * Runs through all elements and calls their update() method.
4607          * This is a special version for the CanvasRenderer.
4608          * Here, we have to do our own layer handling.
4609          * @returns {JXG.Board} Reference to the board
4610          */
4611         updateRendererCanvas: function () {
4612             var el, pEl, i, mini, la,
4613                 olen = this.objectsList.length,
4614                 layers = this.options.layer,
4615                 len = this.options.layer.numlayers,
4616                 last = Number.NEGATIVE_INFINITY;
4617 
4618             for (i = 0; i < len; i++) {
4619                 mini = Number.POSITIVE_INFINITY;
4620 
4621                 for (la in layers) {
4622                     if (layers.hasOwnProperty(la)) {
4623                         if (layers[la] > last && layers[la] < mini) {
4624                             mini = layers[la];
4625                         }
4626                     }
4627                 }
4628 
4629                 last = mini;
4630 
4631                 for (el = 0; el < olen; el++) {
4632                     pEl = this.objectsList[el];
4633 
4634                     if (pEl.visProp.layer === mini) {
4635                         pEl.prepareUpdate().updateRenderer();
4636                     }
4637                 }
4638             }
4639             return this;
4640         },
4641 
4642         /**
4643          * Please use {@link JXG.Board.on} instead.
4644          * @param {Function} hook A function to be called by the board after an update occurred.
4645          * @param {String} [m='update'] When the hook is to be called. Possible values are <i>mouseup</i>, <i>mousedown</i> and <i>update</i>.
4646          * @param {Object} [context=board] Determines the execution context the hook is called. This parameter is optional, default is the
4647          * board object the hook is attached to.
4648          * @returns {Number} Id of the hook, required to remove the hook from the board.
4649          * @deprecated
4650          */
4651         addHook: function (hook, m, context) {
4652             JXG.deprecated('Board.addHook()', 'Board.on()');
4653             m = Type.def(m, 'update');
4654 
4655             context = Type.def(context, this);
4656 
4657             this.hooks.push([m, hook]);
4658             this.on(m, hook, context);
4659 
4660             return this.hooks.length - 1;
4661         },
4662 
4663         /**
4664          * Alias of {@link JXG.Board.on}.
4665          */
4666         addEvent: JXG.shortcut(JXG.Board.prototype, 'on'),
4667 
4668         /**
4669          * Please use {@link JXG.Board.off} instead.
4670          * @param {Number|function} id The number you got when you added the hook or a reference to the event handler.
4671          * @returns {JXG.Board} Reference to the board
4672          * @deprecated
4673          */
4674         removeHook: function (id) {
4675             JXG.deprecated('Board.removeHook()', 'Board.off()');
4676             if (this.hooks[id]) {
4677                 this.off(this.hooks[id][0], this.hooks[id][1]);
4678                 this.hooks[id] = null;
4679             }
4680 
4681             return this;
4682         },
4683 
4684         /**
4685          * Alias of {@link JXG.Board.off}.
4686          */
4687         removeEvent: JXG.shortcut(JXG.Board.prototype, 'off'),
4688 
4689         /**
4690          * Runs through all hooked functions and calls them.
4691          * @returns {JXG.Board} Reference to the board
4692          * @deprecated
4693          */
4694         updateHooks: function (m) {
4695             var arg = Array.prototype.slice.call(arguments, 0);
4696 
4697             JXG.deprecated('Board.updateHooks()', 'Board.triggerEventHandlers()');
4698 
4699             arg[0] = Type.def(arg[0], 'update');
4700             this.triggerEventHandlers([arg[0]], arguments);
4701 
4702             return this;
4703         },
4704 
4705         /**
4706          * Adds a dependent board to this board.
4707          * @param {JXG.Board} board A reference to board which will be updated after an update of this board occurred.
4708          * @returns {JXG.Board} Reference to the board
4709          */
4710         addChild: function (board) {
4711             if (Type.exists(board) && Type.exists(board.containerObj)) {
4712                 this.dependentBoards.push(board);
4713                 this.update();
4714             }
4715             return this;
4716         },
4717 
4718         /**
4719          * Deletes a board from the list of dependent boards.
4720          * @param {JXG.Board} board Reference to the board which will be removed.
4721          * @returns {JXG.Board} Reference to the board
4722          */
4723         removeChild: function (board) {
4724             var i;
4725 
4726             for (i = this.dependentBoards.length - 1; i >= 0; i--) {
4727                 if (this.dependentBoards[i] === board) {
4728                     this.dependentBoards.splice(i, 1);
4729                 }
4730             }
4731             return this;
4732         },
4733 
4734         /**
4735          * Runs through most elements and calls their update() method and update the conditions.
4736          * @param {JXG.GeometryElement} [drag] Element that caused the update.
4737          * @returns {JXG.Board} Reference to the board
4738          */
4739         update: function (drag) {
4740             var i, len, b, insert,
4741                 storeActiveEl;
4742 
4743             if (this.inUpdate || this.isSuspendedUpdate) {
4744                 return this;
4745             }
4746             this.inUpdate = true;
4747 
4748             if (this.attr.minimizereflow === 'all' && this.containerObj && this.renderer.type !== 'vml') {
4749                 storeActiveEl = document.activeElement; // Store focus element
4750                 insert = this.renderer.removeToInsertLater(this.containerObj);
4751             }
4752 
4753             if (this.attr.minimizereflow === 'svg' && this.renderer.type === 'svg') {
4754                 storeActiveEl = document.activeElement;
4755                 insert = this.renderer.removeToInsertLater(this.renderer.svgRoot);
4756             }
4757 
4758             this.prepareUpdate().updateElements(drag).updateConditions();
4759             this.renderer.suspendRedraw(this);
4760             this.updateRenderer();
4761             this.renderer.unsuspendRedraw();
4762             this.triggerEventHandlers(['update'], []);
4763 
4764             if (insert) {
4765                 insert();
4766                 storeActiveEl.focus();     // Restore focus element
4767             }
4768 
4769             // To resolve dependencies between boards
4770             // for (var board in JXG.boards) {
4771             len = this.dependentBoards.length;
4772             for (i = 0; i < len; i++) {
4773                 b = this.dependentBoards[i];
4774                 if (Type.exists(b) && b !== this) {
4775                     b.updateQuality = this.updateQuality;
4776                     b.prepareUpdate().updateElements().updateConditions();
4777                     b.renderer.suspendRedraw();
4778                     b.updateRenderer();
4779                     b.renderer.unsuspendRedraw();
4780                     b.triggerEventHandlers(['update'], []);
4781                 }
4782 
4783             }
4784 
4785             this.inUpdate = false;
4786             return this;
4787         },
4788 
4789         /**
4790          * Runs through all elements and calls their update() method and update the conditions.
4791          * This is necessary after zooming and changing the bounding box.
4792          * @returns {JXG.Board} Reference to the board
4793          */
4794         fullUpdate: function () {
4795             this.needsFullUpdate = true;
4796             this.update();
4797             this.needsFullUpdate = false;
4798             return this;
4799         },
4800 
4801         /**
4802          * Adds a grid to the board according to the settings given in board.options.
4803          * @returns {JXG.Board} Reference to the board.
4804          */
4805         addGrid: function () {
4806             this.create('grid', []);
4807 
4808             return this;
4809         },
4810 
4811         /**
4812          * Removes all grids assigned to this board. Warning: This method also removes all objects depending on one or
4813          * more of the grids.
4814          * @returns {JXG.Board} Reference to the board object.
4815          */
4816         removeGrids: function () {
4817             var i;
4818 
4819             for (i = 0; i < this.grids.length; i++) {
4820                 this.removeObject(this.grids[i]);
4821             }
4822 
4823             this.grids.length = 0;
4824             this.update(); // required for canvas renderer
4825 
4826             return this;
4827         },
4828 
4829         /**
4830          * Creates a new geometric element of type elementType.
4831          * @param {String} elementType Type of the element to be constructed given as a string e.g. 'point' or 'circle'.
4832          * @param {Array} parents Array of parent elements needed to construct the element e.g. coordinates for a point or two
4833          * points to construct a line. This highly depends on the elementType that is constructed. See the corresponding JXG.create*
4834          * methods for a list of possible parameters.
4835          * @param {Object} [attributes] An object containing the attributes to be set. This also depends on the elementType.
4836          * Common attributes are name, visible, strokeColor.
4837          * @returns {Object} Reference to the created element. This is usually a GeometryElement, but can be an array containing
4838          * two or more elements.
4839          */
4840         create: function (elementType, parents, attributes) {
4841             var el, i;
4842 
4843             elementType = elementType.toLowerCase();
4844 
4845             if (!Type.exists(parents)) {
4846                 parents = [];
4847             }
4848 
4849             if (!Type.exists(attributes)) {
4850                 attributes = {};
4851             }
4852 
4853             for (i = 0; i < parents.length; i++) {
4854                 if (Type.isString(parents[i]) &&
4855                     !(elementType === 'text' && i === 2) &&
4856                     !(elementType === 'solidofrevolution3d' && i === 2) &&
4857                     !((elementType === 'input' || elementType === 'checkbox' || elementType === 'button') &&
4858                       (i === 2 || i === 3)) &&
4859                     !(elementType === 'curve' && i > 0) // Allow curve plots with jessiecode
4860                 ) {
4861                     parents[i] = this.select(parents[i]);
4862                 }
4863             }
4864 
4865             if (Type.isFunction(JXG.elements[elementType])) {
4866                 el = JXG.elements[elementType](this, parents, attributes);
4867             } else {
4868                 throw new Error("JSXGraph: create: Unknown element type given: " + elementType);
4869             }
4870 
4871             if (!Type.exists(el)) {
4872                 JXG.debug("JSXGraph: create: failure creating " + elementType);
4873                 return el;
4874             }
4875 
4876             if (el.prepareUpdate && el.update && el.updateRenderer) {
4877                 el.fullUpdate();
4878             }
4879             return el;
4880         },
4881 
4882         /**
4883          * Deprecated name for {@link JXG.Board.create}.
4884          * @deprecated
4885          */
4886         createElement: function () {
4887             JXG.deprecated('Board.createElement()', 'Board.create()');
4888             return this.create.apply(this, arguments);
4889         },
4890 
4891         /**
4892          * Delete the elements drawn as part of a trace of an element.
4893          * @returns {JXG.Board} Reference to the board
4894          */
4895         clearTraces: function () {
4896             var el;
4897 
4898             for (el = 0; el < this.objectsList.length; el++) {
4899                 this.objectsList[el].clearTrace();
4900             }
4901 
4902             this.numTraces = 0;
4903             return this;
4904         },
4905 
4906         /**
4907          * Stop updates of the board.
4908          * @returns {JXG.Board} Reference to the board
4909          */
4910         suspendUpdate: function () {
4911             if (!this.inUpdate) {
4912                 this.isSuspendedUpdate = true;
4913             }
4914             return this;
4915         },
4916 
4917         /**
4918          * Enable updates of the board.
4919          * @returns {JXG.Board} Reference to the board
4920          */
4921         unsuspendUpdate: function () {
4922             if (this.isSuspendedUpdate) {
4923                 this.isSuspendedUpdate = false;
4924                 this.fullUpdate();
4925             }
4926             return this;
4927         },
4928 
4929         /**
4930          * Set the bounding box of the board.
4931          * @param {Array} bbox New bounding box [x1,y1,x2,y2]
4932          * @param {Boolean} [keepaspectratio=false] If set to true, the aspect ratio will be 1:1, but
4933          * the resulting viewport may be larger.
4934          * @param {String} [setZoom='reset'] Reset, keep or update the zoom level of the board. 'reset'
4935          * sets {@link JXG.Board#zoomX} and {@link JXG.Board#zoomY} to the start values (or 1.0).
4936          * 'update' adapts these values accoring to the new bounding box and 'keep' does nothing.
4937          * @returns {JXG.Board} Reference to the board
4938          */
4939         setBoundingBox: function (bbox, keepaspectratio, setZoom) {
4940             var h, w, ux, uy,
4941                 offX = 0,
4942                 offY = 0,
4943                 dim = Env.getDimensions(this.container, this.document);
4944 
4945             if (!Type.isArray(bbox)) {
4946                 return this;
4947             }
4948 
4949             if (bbox[0] < this.maxboundingbox[0] ||
4950                 bbox[1] > this.maxboundingbox[1] ||
4951                 bbox[2] > this.maxboundingbox[2] ||
4952                 bbox[3] < this.maxboundingbox[3]) {
4953                 return this;
4954             }
4955 
4956             if (!Type.exists(setZoom)) {
4957                 setZoom = 'reset';
4958             }
4959 
4960             ux = this.unitX;
4961             uy = this.unitY;
4962 
4963             this.canvasWidth = parseInt(dim.width, 10);
4964             this.canvasHeight = parseInt(dim.height, 10);
4965             w = this.canvasWidth;
4966             h = this.canvasHeight;
4967             if (keepaspectratio) {
4968                 this.unitX = w / (bbox[2] - bbox[0]);
4969                 this.unitY = h / (bbox[1] - bbox[3]);
4970                 if (Math.abs(this.unitX) < Math.abs(this.unitY)) {
4971                     this.unitY = Math.abs(this.unitX) * this.unitY / Math.abs(this.unitY);
4972                     // Add the additional units in equal portions above and below
4973                     offY = (h / this.unitY - (bbox[1] - bbox[3])) * 0.5;
4974                 } else {
4975                     this.unitX = Math.abs(this.unitY) * this.unitX / Math.abs(this.unitX);
4976                     // Add the additional units in equal portions left and right
4977                     offX = (w / this.unitX - (bbox[2] - bbox[0])) * 0.5;
4978                 }
4979                 this.keepaspectratio = true;
4980             } else {
4981                 this.unitX = w / (bbox[2] - bbox[0]);
4982                 this.unitY = h / (bbox[1] - bbox[3]);
4983                 this.keepaspectratio = false;
4984             }
4985 
4986             this.moveOrigin(-this.unitX * (bbox[0] - offX), this.unitY * (bbox[1] + offY));
4987 
4988             if (setZoom === 'update') {
4989                 this.zoomX *= this.unitX / ux;
4990                 this.zoomY *= this.unitY / uy;
4991             } else if (setZoom === 'reset') {
4992                 this.zoomX = Type.exists(this.attr.zoomx) ? this.attr.zoomx : 1.0;
4993                 this.zoomY = Type.exists(this.attr.zoomy) ? this.attr.zoomy : 1.0;
4994             }
4995 
4996             return this;
4997         },
4998 
4999         /**
5000          * Get the bounding box of the board.
5001          * @returns {Array} bounding box [x1,y1,x2,y2] upper left corner, lower right corner
5002          */
5003         getBoundingBox: function () {
5004             var ul = (new Coords(Const.COORDS_BY_SCREEN, [0, 0], this)).usrCoords,
5005                 lr = (new Coords(Const.COORDS_BY_SCREEN, [this.canvasWidth, this.canvasHeight], this)).usrCoords;
5006 
5007             return [ul[1], ul[2], lr[1], lr[2]];
5008         },
5009 
5010         /**
5011          * Adds an animation. Animations are controlled by the boards, so the boards need to be aware of the
5012          * animated elements. This function tells the board about new elements to animate.
5013          * @param {JXG.GeometryElement} element The element which is to be animated.
5014          * @returns {JXG.Board} Reference to the board
5015          */
5016         addAnimation: function (element) {
5017             var that = this;
5018 
5019             this.animationObjects[element.id] = element;
5020 
5021             if (!this.animationIntervalCode) {
5022                 this.animationIntervalCode = window.setInterval(function () {
5023                     that.animate();
5024                 }, element.board.attr.animationdelay);
5025             }
5026 
5027             return this;
5028         },
5029 
5030         /**
5031          * Cancels all running animations.
5032          * @returns {JXG.Board} Reference to the board
5033          */
5034         stopAllAnimation: function () {
5035             var el;
5036 
5037             for (el in this.animationObjects) {
5038                 if (this.animationObjects.hasOwnProperty(el) && Type.exists(this.animationObjects[el])) {
5039                     this.animationObjects[el] = null;
5040                     delete this.animationObjects[el];
5041                 }
5042             }
5043 
5044             window.clearInterval(this.animationIntervalCode);
5045             delete this.animationIntervalCode;
5046 
5047             return this;
5048         },
5049 
5050         /**
5051          * General purpose animation function. This currently only supports moving points from one place to another. This
5052          * is faster than managing the animation per point, especially if there is more than one animated point at the same time.
5053          * @returns {JXG.Board} Reference to the board
5054          */
5055         animate: function () {
5056             var props, el, o, newCoords, r, p, c, cbtmp,
5057                 count = 0,
5058                 obj = null;
5059 
5060             for (el in this.animationObjects) {
5061                 if (this.animationObjects.hasOwnProperty(el) && Type.exists(this.animationObjects[el])) {
5062                     count += 1;
5063                     o = this.animationObjects[el];
5064 
5065                     if (o.animationPath) {
5066                         if (Type.isFunction(o.animationPath)) {
5067                             newCoords = o.animationPath(new Date().getTime() - o.animationStart);
5068                         } else {
5069                             newCoords = o.animationPath.pop();
5070                         }
5071 
5072                         if ((!Type.exists(newCoords)) || (!Type.isArray(newCoords) && isNaN(newCoords))) {
5073                             delete o.animationPath;
5074                         } else {
5075                             o.setPositionDirectly(Const.COORDS_BY_USER, newCoords);
5076                             o.fullUpdate();
5077                             obj = o;
5078                         }
5079                     }
5080                     if (o.animationData) {
5081                         c = 0;
5082 
5083                         for (r in o.animationData) {
5084                             if (o.animationData.hasOwnProperty(r)) {
5085                                 p = o.animationData[r].pop();
5086 
5087                                 if (!Type.exists(p)) {
5088                                     delete o.animationData[p];
5089                                 } else {
5090                                     c += 1;
5091                                     props = {};
5092                                     props[r] = p;
5093                                     o.setAttribute(props);
5094                                 }
5095                             }
5096                         }
5097 
5098                         if (c === 0) {
5099                             delete o.animationData;
5100                         }
5101                     }
5102 
5103                     if (!Type.exists(o.animationData) && !Type.exists(o.animationPath)) {
5104                         this.animationObjects[el] = null;
5105                         delete this.animationObjects[el];
5106 
5107                         if (Type.exists(o.animationCallback)) {
5108                             cbtmp = o.animationCallback;
5109                             o.animationCallback = null;
5110                             cbtmp();
5111                         }
5112                     }
5113                 }
5114             }
5115 
5116             if (count === 0) {
5117                 window.clearInterval(this.animationIntervalCode);
5118                 delete this.animationIntervalCode;
5119             } else {
5120                 this.update(obj);
5121             }
5122 
5123             return this;
5124         },
5125 
5126         /**
5127          * Migrate the dependency properties of the point src
5128          * to the point dest and  delete the point src.
5129          * For example, a circle around the point src
5130          * receives the new center dest. The old center src
5131          * will be deleted.
5132          * @param {JXG.Point} src Original point which will be deleted
5133          * @param {JXG.Point} dest New point with the dependencies of src.
5134          * @param {Boolean} copyName Flag which decides if the name of the src element is copied to the
5135          *  dest element.
5136          * @returns {JXG.Board} Reference to the board
5137          */
5138         migratePoint: function (src, dest, copyName) {
5139             var child, childId, prop, found, i, srcLabelId, srcHasLabel = false;
5140 
5141             src = this.select(src);
5142             dest = this.select(dest);
5143 
5144             if (Type.exists(src.label)) {
5145                 srcLabelId = src.label.id;
5146                 srcHasLabel = true;
5147                 this.removeObject(src.label);
5148             }
5149 
5150             for (childId in src.childElements) {
5151                 if (src.childElements.hasOwnProperty(childId)) {
5152                     child = src.childElements[childId];
5153                     found = false;
5154 
5155                     for (prop in child) {
5156                         if (child.hasOwnProperty(prop)) {
5157                             if (child[prop] ===  src) {
5158                                 child[prop] = dest;
5159                                 found = true;
5160                             }
5161                         }
5162                     }
5163 
5164                     if (found) {
5165                         delete src.childElements[childId];
5166                     }
5167 
5168                     for (i = 0; i < child.parents.length; i++) {
5169                         if (child.parents[i] === src.id) {
5170                             child.parents[i] = dest.id;
5171                         }
5172                     }
5173 
5174                     dest.addChild(child);
5175                 }
5176             }
5177 
5178             // The destination object should receive the name
5179             // and the label of the originating (src) object
5180             if (copyName) {
5181                 if (srcHasLabel) {
5182                     delete dest.childElements[srcLabelId];
5183                     delete dest.descendants[srcLabelId];
5184                 }
5185 
5186                 if (dest.label) {
5187                     this.removeObject(dest.label);
5188                 }
5189 
5190                 delete this.elementsByName[dest.name];
5191                 dest.name = src.name;
5192                 if (srcHasLabel) {
5193                     dest.createLabel();
5194                 }
5195             }
5196 
5197             this.removeObject(src);
5198 
5199             if (Type.exists(dest.name) && dest.name !== '') {
5200                 this.elementsByName[dest.name] = dest;
5201             }
5202 
5203             this.fullUpdate();
5204 
5205             return this;
5206         },
5207 
5208         /**
5209          * Initializes color blindness simulation.
5210          * @param {String} deficiency Describes the color blindness deficiency which is simulated. Accepted values are 'protanopia', 'deuteranopia', and 'tritanopia'.
5211          * @returns {JXG.Board} Reference to the board
5212          */
5213         emulateColorblindness: function (deficiency) {
5214             var e, o;
5215 
5216             if (!Type.exists(deficiency)) {
5217                 deficiency = 'none';
5218             }
5219 
5220             if (this.currentCBDef === deficiency) {
5221                 return this;
5222             }
5223 
5224             for (e in this.objects) {
5225                 if (this.objects.hasOwnProperty(e)) {
5226                     o = this.objects[e];
5227 
5228                     if (deficiency !== 'none') {
5229                         if (this.currentCBDef === 'none') {
5230                             // this could be accomplished by JXG.extend, too. But do not use
5231                             // JXG.deepCopy as this could result in an infinite loop because in
5232                             // visProp there could be geometry elements which contain the board which
5233                             // contains all objects which contain board etc.
5234                             o.visPropOriginal = {
5235                                 strokecolor: o.visProp.strokecolor,
5236                                 fillcolor: o.visProp.fillcolor,
5237                                 highlightstrokecolor: o.visProp.highlightstrokecolor,
5238                                 highlightfillcolor: o.visProp.highlightfillcolor
5239                             };
5240                         }
5241                         o.setAttribute({
5242                             strokecolor: Color.rgb2cb(Type.evaluate(o.visPropOriginal.strokecolor), deficiency),
5243                             fillcolor: Color.rgb2cb(Type.evaluate(o.visPropOriginal.fillcolor), deficiency),
5244                             highlightstrokecolor: Color.rgb2cb(Type.evaluate(o.visPropOriginal.highlightstrokecolor), deficiency),
5245                             highlightfillcolor: Color.rgb2cb(Type.evaluate(o.visPropOriginal.highlightfillcolor), deficiency)
5246                         });
5247                     } else if (Type.exists(o.visPropOriginal)) {
5248                         JXG.extend(o.visProp, o.visPropOriginal);
5249                     }
5250                 }
5251             }
5252             this.currentCBDef = deficiency;
5253             this.update();
5254 
5255             return this;
5256         },
5257 
5258         /**
5259          * Select a single or multiple elements at once.
5260          * @param {String|Object|function} str The name, id or a reference to a JSXGraph element on this board. An object will
5261          * be used as a filter to return multiple elements at once filtered by the properties of the object.
5262          * @param {Boolean} onlyByIdOrName If true (default:false) elements are only filtered by their id, name or groupId.
5263          * The advanced filters consisting of objects or functions are ignored.
5264          * @returns {JXG.GeometryElement|JXG.Composition}
5265          * @example
5266          * // select the element with name A
5267          * board.select('A');
5268          *
5269          * // select all elements with strokecolor set to 'red' (but not '#ff0000')
5270          * board.select({
5271          *   strokeColor: 'red'
5272          * });
5273          *
5274          * // select all points on or below the x axis and make them black.
5275          * board.select({
5276          *   elementClass: JXG.OBJECT_CLASS_POINT,
5277          *   Y: function (v) {
5278          *     return v <= 0;
5279          *   }
5280          * }).setAttribute({color: 'black'});
5281          *
5282          * // select all elements
5283          * board.select(function (el) {
5284          *   return true;
5285          * });
5286          */
5287         select: function (str, onlyByIdOrName) {
5288             var flist, olist, i, l,
5289                 s = str;
5290 
5291             if (s === null) {
5292                 return s;
5293             }
5294 
5295             // it's a string, most likely an id or a name.
5296             if (Type.isString(s) && s !== '') {
5297                 // Search by ID
5298                 if (Type.exists(this.objects[s])) {
5299                     s = this.objects[s];
5300                 // Search by name
5301                 } else if (Type.exists(this.elementsByName[s])) {
5302                     s = this.elementsByName[s];
5303                 // Search by group ID
5304                 } else if (Type.exists(this.groups[s])) {
5305                     s = this.groups[s];
5306                 }
5307             // it's a function or an object, but not an element
5308             } else if (!onlyByIdOrName &&
5309                 (Type.isFunction(s) ||
5310                  (Type.isObject(s) && !Type.isFunction(s.setAttribute))
5311                 )) {
5312                 flist = Type.filterElements(this.objectsList, s);
5313 
5314                 olist = {};
5315                 l = flist.length;
5316                 for (i = 0; i < l; i++) {
5317                     olist[flist[i].id] = flist[i];
5318                 }
5319                 s = new Composition(olist);
5320             // it's an element which has been deleted (and still hangs around, e.g. in an attractor list
5321             } else if (Type.isObject(s) && Type.exists(s.id) && !Type.exists(this.objects[s.id])) {
5322                 s = null;
5323             }
5324 
5325             return s;
5326         },
5327 
5328         /**
5329          * Checks if the given point is inside the boundingbox.
5330          * @param {Number|JXG.Coords} x User coordinate or {@link JXG.Coords} object.
5331          * @param {Number} [y] User coordinate. May be omitted in case <tt>x</tt> is a {@link JXG.Coords} object.
5332          * @returns {Boolean}
5333          */
5334         hasPoint: function (x, y) {
5335             var px = x,
5336                 py = y,
5337                 bbox = this.getBoundingBox();
5338 
5339             if (Type.exists(x) && Type.isArray(x.usrCoords)) {
5340                 px = x.usrCoords[1];
5341                 py = x.usrCoords[2];
5342             }
5343 
5344             return !!(Type.isNumber(px) && Type.isNumber(py) &&
5345                 bbox[0] < px && px < bbox[2] && bbox[1] > py && py > bbox[3]);
5346         },
5347 
5348         /**
5349          * Update CSS transformations of type scaling. It is used to correct the mouse position
5350          * in {@link JXG.Board.getMousePosition}.
5351          * The inverse transformation matrix is updated on each mouseDown and touchStart event.
5352          *
5353          * It is up to the user to call this method after an update of the CSS transformation
5354          * in the DOM.
5355          */
5356         updateCSSTransforms: function () {
5357             var obj = this.containerObj,
5358                 o = obj,
5359                 o2 = obj;
5360 
5361             this.cssTransMat = Env.getCSSTransformMatrix(o);
5362 
5363             /*
5364              * In Mozilla and Webkit: offsetParent seems to jump at least to the next iframe,
5365              * if not to the body. In IE and if we are in an position:absolute environment
5366              * offsetParent walks up the DOM hierarchy.
5367              * In order to walk up the DOM hierarchy also in Mozilla and Webkit
5368              * we need the parentNode steps.
5369              */
5370             o = o.offsetParent;
5371             while (o) {
5372                 this.cssTransMat = Mat.matMatMult(Env.getCSSTransformMatrix(o), this.cssTransMat);
5373 
5374                 o2 = o2.parentNode;
5375                 while (o2 !== o) {
5376                     this.cssTransMat = Mat.matMatMult(Env.getCSSTransformMatrix(o), this.cssTransMat);
5377                     o2 = o2.parentNode;
5378                 }
5379 
5380                 o = o.offsetParent;
5381             }
5382             this.cssTransMat = Mat.inverse(this.cssTransMat);
5383 
5384             return this;
5385         },
5386 
5387         /**
5388          * Start selection mode. This function can either be triggered from outside or by
5389          * a down event together with correct key pressing. The default keys are
5390          * shift+ctrl. But this can be changed in the options.
5391          *
5392          * Starting from out side can be realized for example with a button like this:
5393          * <pre>
5394          * 	<button onclick="board.startSelectionMode()">Start</button>
5395          * </pre>
5396          * @example
5397          * //
5398          * // Set a new bounding box from the selection rectangle
5399          * //
5400          * var board = JXG.JSXGraph.initBoard('jxgbox', {
5401          *         boundingBox:[-3,2,3,-2],
5402          *         keepAspectRatio: false,
5403          *         axis:true,
5404          *         selection: {
5405          *             enabled: true,
5406          *             needShift: false,
5407          *             needCtrl: true,
5408          *             withLines: false,
5409          *             vertices: {
5410          *                 visible: false
5411          *             },
5412          *             fillColor: '#ffff00',
5413          *         }
5414          *      });
5415          *
5416          * var f = function f(x) { return Math.cos(x); },
5417          *     curve = board.create('functiongraph', [f]);
5418          *
5419          * board.on('stopselecting', function(){
5420          *     var box = board.stopSelectionMode(),
5421          *
5422          *         // bbox has the coordinates of the selection rectangle.
5423          *         // Attention: box[i].usrCoords have the form [1, x, y], i.e.
5424          *         // are homogeneous coordinates.
5425          *         bbox = box[0].usrCoords.slice(1).concat(box[1].usrCoords.slice(1));
5426          *
5427          *         // Set a new bounding box
5428          *         board.setBoundingBox(bbox, false);
5429          *  });
5430          *
5431          *
5432          * </pre><div class="jxgbox" id="JXG11eff3a6-8c50-11e5-b01d-901b0e1b8723" style="width: 300px; height: 300px;"></div>
5433          * <script type="text/javascript">
5434          *     (function() {
5435          *     //
5436          *     // Set a new bounding box from the selection rectangle
5437          *     //
5438          *     var board = JXG.JSXGraph.initBoard('JXG11eff3a6-8c50-11e5-b01d-901b0e1b8723', {
5439          *             boundingBox:[-3,2,3,-2],
5440          *             keepAspectRatio: false,
5441          *             axis:true,
5442          *             selection: {
5443          *                 enabled: true,
5444          *                 needShift: false,
5445          *                 needCtrl: true,
5446          *                 withLines: false,
5447          *                 vertices: {
5448          *                     visible: false
5449          *                 },
5450          *                 fillColor: '#ffff00',
5451          *             }
5452          *        });
5453          *
5454          *     var f = function f(x) { return Math.cos(x); },
5455          *         curve = board.create('functiongraph', [f]);
5456          *
5457          *     board.on('stopselecting', function(){
5458          *         var box = board.stopSelectionMode(),
5459          *
5460          *             // bbox has the coordinates of the selection rectangle.
5461          *             // Attention: box[i].usrCoords have the form [1, x, y], i.e.
5462          *             // are homogeneous coordinates.
5463          *             bbox = box[0].usrCoords.slice(1).concat(box[1].usrCoords.slice(1));
5464          *
5465          *             // Set a new bounding box
5466          *             board.setBoundingBox(bbox, false);
5467          *      });
5468          *     })();
5469          *
5470          * </script><pre>
5471          *
5472          */
5473         startSelectionMode: function () {
5474             this.selectingMode = true;
5475             this.selectionPolygon.setAttribute({visible: true});
5476             this.selectingBox = [[0, 0], [0, 0]];
5477             this._setSelectionPolygonFromBox();
5478             this.selectionPolygon.fullUpdate();
5479         },
5480 
5481         /**
5482          * Finalize the selection: disable selection mode and return the coordinates
5483          * of the selection rectangle.
5484          * @returns {Array} Coordinates of the selection rectangle. The array
5485          * contains two {@link JXG.Coords} objects. One the upper left corner and
5486          * the second for the lower right corner.
5487          */
5488         stopSelectionMode: function () {
5489             this.selectingMode = false;
5490             this.selectionPolygon.setAttribute({visible: false});
5491             return [this.selectionPolygon.vertices[0].coords, this.selectionPolygon.vertices[2].coords];
5492         },
5493 
5494         /**
5495          * Start the selection of a region.
5496          * @private
5497          * @param  {Array} pos Screen coordiates of the upper left corner of the
5498          * selection rectangle.
5499          */
5500         _startSelecting: function (pos) {
5501             this.isSelecting = true;
5502             this.selectingBox = [ [pos[0], pos[1]], [pos[0], pos[1]] ];
5503             this._setSelectionPolygonFromBox();
5504         },
5505 
5506         /**
5507          * Update the selection rectangle during a move event.
5508          * @private
5509          * @param  {Array} pos Screen coordiates of the move event
5510          */
5511         _moveSelecting: function (pos) {
5512             if (this.isSelecting) {
5513                 this.selectingBox[1] = [pos[0], pos[1]];
5514                 this._setSelectionPolygonFromBox();
5515                 this.selectionPolygon.fullUpdate();
5516             }
5517         },
5518 
5519         /**
5520          * Update the selection rectangle during an up event. Stop selection.
5521          * @private
5522          * @param  {Object} evt Event object
5523          */
5524         _stopSelecting:  function (evt) {
5525             var pos = this.getMousePosition(evt);
5526 
5527             this.isSelecting = false;
5528             this.selectingBox[1] = [pos[0], pos[1]];
5529             this._setSelectionPolygonFromBox();
5530         },
5531 
5532         /**
5533          * Update the Selection rectangle.
5534          * @private
5535          */
5536         _setSelectionPolygonFromBox: function () {
5537                var A = this.selectingBox[0],
5538                 B = this.selectingBox[1];
5539 
5540                this.selectionPolygon.vertices[0].setPositionDirectly(JXG.COORDS_BY_SCREEN, [A[0], A[1]]);
5541                this.selectionPolygon.vertices[1].setPositionDirectly(JXG.COORDS_BY_SCREEN, [A[0], B[1]]);
5542                this.selectionPolygon.vertices[2].setPositionDirectly(JXG.COORDS_BY_SCREEN, [B[0], B[1]]);
5543                this.selectionPolygon.vertices[3].setPositionDirectly(JXG.COORDS_BY_SCREEN, [B[0], A[1]]);
5544         },
5545 
5546         /**
5547          * Test if a down event should start a selection. Test if the
5548          * required keys are pressed. If yes, {@link JXG.Board.startSelectionMode} is called.
5549          * @param  {Object} evt Event object
5550          */
5551         _testForSelection: function (evt) {
5552             if (this._isRequiredKeyPressed(evt, 'selection')) {
5553                 if (!Type.exists(this.selectionPolygon)) {
5554                     this._createSelectionPolygon(this.attr);
5555                 }
5556                 this.startSelectionMode();
5557             }
5558         },
5559 
5560         /**
5561          * Create the internal selection polygon, which will be available as board.selectionPolygon.
5562          * @private
5563          * @param  {Object} attr board attributes, e.g. the subobject board.attr.
5564          * @returns {Object} pointer to the board to enable chaining.
5565          */
5566         _createSelectionPolygon: function(attr) {
5567             var selectionattr;
5568 
5569             if (!Type.exists(this.selectionPolygon)) {
5570                 selectionattr = Type.copyAttributes(attr, Options, 'board', 'selection');
5571                 if (selectionattr.enabled === true) {
5572                     this.selectionPolygon = this.create('polygon', [[0, 0], [0, 0], [0, 0], [0, 0]], selectionattr);
5573                 }
5574             }
5575 
5576             return this;
5577         },
5578 
5579         /* **************************
5580          *     EVENT DEFINITION
5581          * for documentation purposes
5582          * ************************** */
5583 
5584         //region Event handler documentation
5585 
5586         /**
5587          * @event
5588          * @description Whenever the user starts to touch or click the board.
5589          * @name JXG.Board#down
5590          * @param {Event} e The browser's event object.
5591          */
5592         __evt__down: function (e) { },
5593 
5594         /**
5595          * @event
5596          * @description Whenever the user starts to click on the board.
5597          * @name JXG.Board#mousedown
5598          * @param {Event} e The browser's event object.
5599          */
5600         __evt__mousedown: function (e) { },
5601 
5602         /**
5603          * @event
5604          * @description Whenever the user taps the pen on the board.
5605          * @name JXG.Board#pendown
5606          * @param {Event} e The browser's event object.
5607          */
5608         __evt__pendown: function (e) { },
5609 
5610         /**
5611          * @event
5612          * @description Whenever the user starts to click on the board with a
5613          * device sending pointer events.
5614          * @name JXG.Board#pointerdown
5615          * @param {Event} e The browser's event object.
5616          */
5617         __evt__pointerdown: function (e) { },
5618 
5619         /**
5620          * @event
5621          * @description Whenever the user starts to touch the board.
5622          * @name JXG.Board#touchstart
5623          * @param {Event} e The browser's event object.
5624          */
5625         __evt__touchstart: function (e) { },
5626 
5627         /**
5628          * @event
5629          * @description Whenever the user stops to touch or click the board.
5630          * @name JXG.Board#up
5631          * @param {Event} e The browser's event object.
5632          */
5633         __evt__up: function (e) { },
5634 
5635         /**
5636          * @event
5637          * @description Whenever the user releases the mousebutton over the board.
5638          * @name JXG.Board#mouseup
5639          * @param {Event} e The browser's event object.
5640          */
5641         __evt__mouseup: function (e) { },
5642 
5643         /**
5644          * @event
5645          * @description Whenever the user releases the mousebutton over the board with a
5646          * device sending pointer events.
5647          * @name JXG.Board#pointerup
5648          * @param {Event} e The browser's event object.
5649          */
5650         __evt__pointerup: function (e) { },
5651 
5652         /**
5653          * @event
5654          * @description Whenever the user stops touching the board.
5655          * @name JXG.Board#touchend
5656          * @param {Event} e The browser's event object.
5657          */
5658         __evt__touchend: function (e) { },
5659 
5660         /**
5661          * @event
5662          * @description This event is fired whenever the user is moving the finger or mouse pointer over the board.
5663          * @name JXG.Board#move
5664          * @param {Event} e The browser's event object.
5665          * @param {Number} mode The mode the board currently is in
5666          * @see JXG.Board#mode
5667          */
5668         __evt__move: function (e, mode) { },
5669 
5670         /**
5671          * @event
5672          * @description This event is fired whenever the user is moving the mouse over the board.
5673          * @name JXG.Board#mousemove
5674          * @param {Event} e The browser's event object.
5675          * @param {Number} mode The mode the board currently is in
5676          * @see JXG.Board#mode
5677          */
5678         __evt__mousemove: function (e, mode) { },
5679 
5680         /**
5681          * @event
5682          * @description This event is fired whenever the user is moving the pen over the board.
5683          * @name JXG.Board#penmove
5684          * @param {Event} e The browser's event object.
5685          * @param {Number} mode The mode the board currently is in
5686          * @see JXG.Board#mode
5687          */
5688         __evt__penmove: function (e, mode) { },
5689 
5690         /**
5691          * @event
5692          * @description This event is fired whenever the user is moving the mouse over the board  with a
5693          * device sending pointer events.
5694          * @name JXG.Board#pointermove
5695          * @param {Event} e The browser's event object.
5696          * @param {Number} mode The mode the board currently is in
5697          * @see JXG.Board#mode
5698          */
5699         __evt__pointermove: function (e, mode) { },
5700 
5701         /**
5702          * @event
5703          * @description This event is fired whenever the user is moving the finger over the board.
5704          * @name JXG.Board#touchmove
5705          * @param {Event} e The browser's event object.
5706          * @param {Number} mode The mode the board currently is in
5707          * @see JXG.Board#mode
5708          */
5709         __evt__touchmove: function (e, mode) { },
5710 
5711         /**
5712          * @event
5713          * @description Whenever an element is highlighted this event is fired.
5714          * @name JXG.Board#hit
5715          * @param {Event} e The browser's event object.
5716          * @param {JXG.GeometryElement} el The hit element.
5717          * @param target
5718          *
5719          * @example
5720          * var c = board.create('circle', [[1, 1], 2]);
5721          * board.on('hit', function(evt, el) {
5722          *     console.log("Hit element", el);
5723          * });
5724          *
5725          * </pre><div id="JXG19eb31ac-88e6-11e8-bcb5-901b0e1b8723" class="jxgbox" style="width: 300px; height: 300px;"></div>
5726          * <script type="text/javascript">
5727          *     (function() {
5728          *         var board = JXG.JSXGraph.initBoard('JXG19eb31ac-88e6-11e8-bcb5-901b0e1b8723',
5729          *             {boundingbox: [-8, 8, 8,-8], axis: true, showcopyright: false, shownavigation: false});
5730          *     var c = board.create('circle', [[1, 1], 2]);
5731          *     board.on('hit', function(evt, el) {
5732          *         console.log("Hit element", el);
5733          *     });
5734          *
5735          *     })();
5736          *
5737          * </script><pre>
5738          */
5739         __evt__hit: function (e, el, target) { },
5740 
5741         /**
5742          * @event
5743          * @description Whenever an element is highlighted this event is fired.
5744          * @name JXG.Board#mousehit
5745          * @see JXG.Board#hit
5746          * @param {Event} e The browser's event object.
5747          * @param {JXG.GeometryElement} el The hit element.
5748          * @param target
5749          */
5750         __evt__mousehit: function (e, el, target) { },
5751 
5752         /**
5753          * @event
5754          * @description This board is updated.
5755          * @name JXG.Board#update
5756          */
5757         __evt__update: function () { },
5758 
5759         /**
5760          * @event
5761          * @description The bounding box of the board has changed.
5762          * @name JXG.Board#boundingbox
5763          */
5764         __evt__boundingbox: function () { },
5765 
5766         /**
5767          * @event
5768          * @description Select a region is started during a down event or by calling
5769          * {@link JXG.Board.startSelectionMode}
5770          * @name JXG.Board#startselecting
5771          */
5772          __evt__startselecting: function () { },
5773 
5774          /**
5775          * @event
5776          * @description Select a region is started during a down event
5777          * from a device sending mouse events or by calling
5778          * {@link JXG.Board.startSelectionMode}.
5779          * @name JXG.Board#mousestartselecting
5780          */
5781          __evt__mousestartselecting: function () { },
5782 
5783          /**
5784          * @event
5785          * @description Select a region is started during a down event
5786          * from a device sending pointer events or by calling
5787          * {@link JXG.Board.startSelectionMode}.
5788          * @name JXG.Board#pointerstartselecting
5789          */
5790          __evt__pointerstartselecting: function () { },
5791 
5792          /**
5793          * @event
5794          * @description Select a region is started during a down event
5795          * from a device sending touch events or by calling
5796          * {@link JXG.Board.startSelectionMode}.
5797          * @name JXG.Board#touchstartselecting
5798          */
5799          __evt__touchstartselecting: function () { },
5800 
5801          /**
5802           * @event
5803           * @description Selection of a region is stopped during an up event.
5804           * @name JXG.Board#stopselecting
5805           */
5806          __evt__stopselecting: function () { },
5807 
5808          /**
5809          * @event
5810          * @description Selection of a region is stopped during an up event
5811          * from a device sending mouse events.
5812          * @name JXG.Board#mousestopselecting
5813          */
5814          __evt__mousestopselecting: function () { },
5815 
5816          /**
5817          * @event
5818          * @description Selection of a region is stopped during an up event
5819          * from a device sending pointer events.
5820          * @name JXG.Board#pointerstopselecting
5821          */
5822          __evt__pointerstopselecting: function () { },
5823 
5824          /**
5825          * @event
5826          * @description Selection of a region is stopped during an up event
5827          * from a device sending touch events.
5828          * @name JXG.Board#touchstopselecting
5829          */
5830          __evt__touchstopselecting: function () { },
5831 
5832          /**
5833          * @event
5834          * @description A move event while selecting of a region is active.
5835          * @name JXG.Board#moveselecting
5836          */
5837          __evt__moveselecting: function () { },
5838 
5839          /**
5840          * @event
5841          * @description A move event while selecting of a region is active
5842          * from a device sending mouse events.
5843          * @name JXG.Board#mousemoveselecting
5844          */
5845          __evt__mousemoveselecting: function () { },
5846 
5847          /**
5848          * @event
5849          * @description Select a region is started during a down event
5850          * from a device sending mouse events.
5851          * @name JXG.Board#pointermoveselecting
5852          */
5853          __evt__pointermoveselecting: function () { },
5854 
5855          /**
5856          * @event
5857          * @description Select a region is started during a down event
5858          * from a device sending touch events.
5859          * @name JXG.Board#touchmoveselecting
5860          */
5861          __evt__touchmoveselecting: function () { },
5862 
5863         /**
5864          * @ignore
5865          */
5866         __evt: function () {},
5867 
5868         //endregion
5869 
5870         /**
5871          * Expand the JSXGraph construction to fullscreen.
5872          * In order to preserve the proportions of the JSXGraph element,
5873          * a wrapper div is created which is set to fullscreen.
5874          * <p>
5875          * The wrapping div has the CSS class 'jxgbox_wrap_private' which is
5876          * defined in the file 'jsxgraph.css'
5877          * <p>
5878          * This feature is not available on iPhones (as of December 2021).
5879          *
5880          * @param {String} id (Optional) id of the div element which is brought to fullscreen.
5881          * If not provided, this defaults to the JSXGraph div. However, it may be necessary for the aspect ratio trick
5882          * which using padding-bottom/top and an out div element. Then, the id of the outer div has to be supplied.
5883          *
5884          * @return {JXG.Board} Reference to the board
5885          *
5886          * @example
5887          * <div id='jxgbox' class='jxgbox' style='width:500px; height:200px;'></div>
5888          * <button onClick="board.toFullscreen()">Fullscreen</button>
5889          *
5890          * <script language="Javascript" type='text/javascript'>
5891          * var board = JXG.JSXGraph.initBoard('jxgbox', {axis:true, boundingbox:[-5,5,5,-5]});
5892          * var p = board.create('point', [0, 1]);
5893          * </script>
5894          *
5895          * </pre><div id="JXGd5bab8b6-fd40-11e8-ab14-901b0e1b8723" class="jxgbox" style="width: 300px; height: 300px;"></div>
5896          * <script type="text/javascript">
5897          *      var board_d5bab8b6;
5898          *     (function() {
5899          *         var board = JXG.JSXGraph.initBoard('JXGd5bab8b6-fd40-11e8-ab14-901b0e1b8723',
5900          *             {boundingbox:[-5,5,5,-5], axis: true, showcopyright: false, shownavigation: false});
5901          *         var p = board.create('point', [0, 1]);
5902          *         board_d5bab8b6 = board;
5903          *     })();
5904          * </script>
5905          * <button onClick="board_d5bab8b6.toFullscreen()">Fullscreen</button>
5906          * <pre>
5907          *
5908          * @example
5909          * <div id='outer' style='max-width: 500px; margin: 0 auto;'>
5910          * <div id='jxgbox' class='jxgbox' style='height: 0; padding-bottom: 100%'></div>
5911          * </div>
5912          * <button onClick="board.toFullscreen('outer')">Fullscreen</button>
5913          *
5914          * <script language="Javascript" type='text/javascript'>
5915          * var board = JXG.JSXGraph.initBoard('jxgbox', {
5916          *     axis:true,
5917          *     boundingbox:[-5,5,5,-5],
5918          *     fullscreen: { id: 'outer' },
5919          *     showFullscreen: true
5920          * });
5921          * var p = board.create('point', [-2, 3], {});
5922          * </script>
5923          *
5924          * </pre><div id="JXG7103f6b_outer" style='max-width: 500px; margin: 0 auto;'>
5925          * <div id="JXG7103f6be-6993-4ff8-8133-c78e50a8afac" class="jxgbox" style="height: 0; padding-bottom: 100%;"></div>
5926          * </div>
5927          * <button onClick="board_JXG7103f6be.toFullscreen('JXG7103f6b_outer')">Fullscreen</button>
5928          * <script type="text/javascript">
5929          *     var board_JXG7103f6be;
5930          *     (function() {
5931          *         var board = JXG.JSXGraph.initBoard('JXG7103f6be-6993-4ff8-8133-c78e50a8afac',
5932          *             {boundingbox: [-8, 8, 8,-8], axis: true, fullscreen: { id: 'JXG7103f6b_outer' }, showFullscreen: true,
5933          *              showcopyright: false, shownavigation: false});
5934          *     var p = board.create('point', [-2, 3], {});
5935          *     board_JXG7103f6be = board;
5936          *     })();
5937          *
5938          * </script><pre>
5939          *
5940          *
5941          */
5942         toFullscreen: function (id) {
5943             var wrap_id, wrap_node, inner_node;
5944 
5945             id = id || this.container;
5946             this._fullscreen_inner_id = id;
5947             inner_node = document.getElementById(id);
5948             wrap_id = 'fullscreenwrap_' + id;
5949 
5950             // Wrap a div around the JSXGraph div.
5951             if (this.document.getElementById(wrap_id)) {
5952                 wrap_node = this.document.getElementById(wrap_id);
5953             } else {
5954                 wrap_node = document.createElement('div');
5955                 wrap_node.classList.add('JXG_wrap_private');
5956                 wrap_node.setAttribute('id', wrap_id);
5957                 inner_node.parentNode.insertBefore(wrap_node, inner_node);
5958                 wrap_node.appendChild(inner_node);
5959             }
5960 
5961             // Get the real width and height of the JSXGraph div
5962             // and determine the scaling and vertical shift amount
5963             this._fullscreen_res = Env._getScaleFactors(inner_node);
5964 
5965             // Trigger fullscreen mode
5966             wrap_node.requestFullscreen = wrap_node.requestFullscreen ||
5967                 wrap_node.webkitRequestFullscreen ||
5968                 wrap_node.mozRequestFullScreen ||
5969                 wrap_node.msRequestFullscreen;
5970 
5971             if (wrap_node.requestFullscreen) {
5972                 wrap_node.requestFullscreen();
5973             }
5974 
5975             return this;
5976         },
5977 
5978         /**
5979          * If fullscreen mode is toggled, the possible CSS transformations
5980          * which are applied to the JSXGraph canvas have to be reread.
5981          * Otherwise the position of upper left corner is wrongly interpreted.
5982          *
5983          * @param  {Object} evt fullscreen event object (unused)
5984          */
5985         fullscreenListener: function (evt) {
5986             var res, inner_id, inner_node;
5987 
5988             inner_id = this._fullscreen_inner_id;
5989             if (!Type.exists(inner_id)) {
5990                 return;
5991             }
5992 
5993             document.fullscreenElement = document.fullscreenElement ||
5994                     document.webkitFullscreenElement ||
5995                     document.mozFullscreenElement ||
5996                     document.msFullscreenElement;
5997 
5998             inner_node = document.getElementById(inner_id);
5999             // If full screen mode is started we have to remove CSS margin around the JSXGraph div.
6000             // Otherwise, the positioning of the fullscreen div will be false.
6001             // When leaving the fullscreen mode, the margin is put back in.
6002             if (document.fullscreenElement) {
6003                 // Just entered fullscreen mode
6004 
6005                 // Get the data computed in board.toFullscreen()
6006                 res = this._fullscreen_res;
6007 
6008                 // Store the scaling data.
6009                 // It is used in AbstractRenderer.updateText to restore the scaling matrix
6010                 // which is removed by MathJax.
6011                 // Further, the CSS margin has to be removed when in fullscreen mode,
6012                 // and must be restored later.
6013                 inner_node._cssFullscreenStore = {
6014                     id: document.fullscreenElement.id,
6015                     isFullscreen: true,
6016                     margin: inner_node.style.margin,
6017                     width: inner_node.style.width,
6018                     scale: res.scale,
6019                     vshift: res.vshift
6020                 };
6021 
6022                 inner_node.style.margin = '';
6023                 inner_node.style.width = res.width + 'px';
6024 
6025                 // Do the shifting and scaling via CSS pseudo rules
6026                 // We do this after fullscreen mode has been established to get the correct size
6027                 // of the JSXGraph div.
6028                 Env.scaleJSXGraphDiv(document.fullscreenElement.id, inner_id, res.scale, res.vshift);
6029 
6030                 // Clear document.fullscreenElement, because Safari doesn't to it and
6031                 // when leaving full screen mode it is still set.
6032                 document.fullscreenElement = null;
6033 
6034             } else if (Type.exists(inner_node._cssFullscreenStore)) {
6035                 // Just left the fullscreen mode
6036 
6037                 // Remove the CSS rules added in Env.scaleJSXGraphDiv
6038                 try {
6039                     document.styleSheets[document.styleSheets.length - 1].deleteRule(0);
6040                 } catch (err) {
6041                     console.log('JSXGraph: Could not remove CSS rules for full screen mode');
6042                 }
6043 
6044                 inner_node._cssFullscreenStore.isFullscreen = false;
6045                 inner_node.style.margin = inner_node._cssFullscreenStore.margin;
6046                 inner_node.style.width = inner_node._cssFullscreenStore.width;
6047 
6048             }
6049 
6050             this.updateCSSTransforms();
6051         },
6052 
6053         /**
6054          * Function to animate a curve rolling on another curve.
6055          * @param {Curve} c1 JSXGraph curve building the floor where c2 rolls
6056          * @param {Curve} c2 JSXGraph curve which rolls on c1.
6057          * @param {number} start_c1 The parameter t such that c1(t) touches c2. This is the start position of the
6058          *                          rolling process
6059          * @param {Number} stepsize Increase in t in each step for the curve c1
6060          * @param {Number} direction
6061          * @param {Number} time Delay time for setInterval()
6062          * @param {Array} pointlist Array of points which are rolled in each step. This list should contain
6063          *      all points which define c2 and gliders on c2.
6064          *
6065          * @example
6066          *
6067          * // Line which will be the floor to roll upon.
6068          * var line = brd.create('curve', [function (t) { return t;}, function (t){ return 1;}], {strokeWidth:6});
6069          * // Center of the rolling circle
6070          * var C = brd.create('point',[0,2],{name:'C'});
6071          * // Starting point of the rolling circle
6072          * var P = brd.create('point',[0,1],{name:'P', trace:true});
6073          * // Circle defined as a curve. The circle "starts" at P, i.e. circle(0) = P
6074          * var circle = brd.create('curve',[
6075          *           function (t){var d = P.Dist(C),
6076          *                           beta = JXG.Math.Geometry.rad([C.X()+1,C.Y()],C,P);
6077          *                       t += beta;
6078          *                       return C.X()+d*Math.cos(t);
6079          *           },
6080          *           function (t){var d = P.Dist(C),
6081          *                           beta = JXG.Math.Geometry.rad([C.X()+1,C.Y()],C,P);
6082          *                       t += beta;
6083          *                       return C.Y()+d*Math.sin(t);
6084          *           },
6085          *           0,2*Math.PI],
6086          *           {strokeWidth:6, strokeColor:'green'});
6087          *
6088          * // Point on circle
6089          * var B = brd.create('glider',[0,2,circle],{name:'B', color:'blue',trace:false});
6090          * var roll = brd.createRoulette(line, circle, 0, Math.PI/20, 1, 100, [C,P,B]);
6091          * roll.start() // Start the rolling, to be stopped by roll.stop()
6092          *
6093          * </pre><div class="jxgbox" id="JXGe5e1b53c-a036-4a46-9e35-190d196beca5" style="width: 300px; height: 300px;"></div>
6094          * <script type="text/javascript">
6095          * var brd = JXG.JSXGraph.initBoard('JXGe5e1b53c-a036-4a46-9e35-190d196beca5', {boundingbox: [-5, 5, 5, -5], axis: true, showcopyright:false, shownavigation: false});
6096          * // Line which will be the floor to roll upon.
6097          * var line = brd.create('curve', [function (t) { return t;}, function (t){ return 1;}], {strokeWidth:6});
6098          * // Center of the rolling circle
6099          * var C = brd.create('point',[0,2],{name:'C'});
6100          * // Starting point of the rolling circle
6101          * var P = brd.create('point',[0,1],{name:'P', trace:true});
6102          * // Circle defined as a curve. The circle "starts" at P, i.e. circle(0) = P
6103          * var circle = brd.create('curve',[
6104          *           function (t){var d = P.Dist(C),
6105          *                           beta = JXG.Math.Geometry.rad([C.X()+1,C.Y()],C,P);
6106          *                       t += beta;
6107          *                       return C.X()+d*Math.cos(t);
6108          *           },
6109          *           function (t){var d = P.Dist(C),
6110          *                           beta = JXG.Math.Geometry.rad([C.X()+1,C.Y()],C,P);
6111          *                       t += beta;
6112          *                       return C.Y()+d*Math.sin(t);
6113          *           },
6114          *           0,2*Math.PI],
6115          *           {strokeWidth:6, strokeColor:'green'});
6116          *
6117          * // Point on circle
6118          * var B = brd.create('glider',[0,2,circle],{name:'B', color:'blue',trace:false});
6119          * var roll = brd.createRoulette(line, circle, 0, Math.PI/20, 1, 100, [C,P,B]);
6120          * roll.start() // Start the rolling, to be stopped by roll.stop()
6121          * </script><pre>
6122          */
6123         createRoulette: function (c1, c2, start_c1, stepsize, direction, time, pointlist) {
6124             var brd = this,
6125                 Roulette = function () {
6126                     var alpha = 0, Tx = 0, Ty = 0,
6127                         t1 = start_c1,
6128                         t2 = Numerics.root(
6129                             function (t) {
6130                                 var c1x = c1.X(t1),
6131                                     c1y = c1.Y(t1),
6132                                     c2x = c2.X(t),
6133                                     c2y = c2.Y(t);
6134 
6135                                 return (c1x - c2x) * (c1x - c2x) + (c1y - c2y) * (c1y - c2y);
6136                             },
6137                             [0, Math.PI * 2]
6138                         ),
6139                         t1_new = 0.0, t2_new = 0.0,
6140                         c1dist,
6141 
6142                         rotation = brd.create('transform', [
6143                             function () {
6144                                 return alpha;
6145                             }
6146                         ], {type: 'rotate'}),
6147 
6148                         rotationLocal = brd.create('transform', [
6149                             function () {
6150                                 return alpha;
6151                             },
6152                             function () {
6153                                 return c1.X(t1);
6154                             },
6155                             function () {
6156                                 return c1.Y(t1);
6157                             }
6158                         ], {type: 'rotate'}),
6159 
6160                         translate = brd.create('transform', [
6161                             function () {
6162                                 return Tx;
6163                             },
6164                             function () {
6165                                 return Ty;
6166                             }
6167                         ], {type: 'translate'}),
6168 
6169                         // arc length via Simpson's rule.
6170                         arclen = function (c, a, b) {
6171                             var cpxa = Numerics.D(c.X)(a),
6172                                 cpya = Numerics.D(c.Y)(a),
6173                                 cpxb = Numerics.D(c.X)(b),
6174                                 cpyb = Numerics.D(c.Y)(b),
6175                                 cpxab = Numerics.D(c.X)((a + b) * 0.5),
6176                                 cpyab = Numerics.D(c.Y)((a + b) * 0.5),
6177 
6178                                 fa = Math.sqrt(cpxa * cpxa + cpya * cpya),
6179                                 fb = Math.sqrt(cpxb * cpxb + cpyb * cpyb),
6180                                 fab = Math.sqrt(cpxab * cpxab + cpyab * cpyab);
6181 
6182                             return (fa + 4 * fab + fb) * (b - a) / 6;
6183                         },
6184 
6185                         exactDist = function (t) {
6186                             return c1dist - arclen(c2, t2, t);
6187                         },
6188 
6189                         beta = Math.PI / 18,
6190                         beta9 = beta * 9,
6191                         interval = null;
6192 
6193                     this.rolling = function () {
6194                         var h, g, hp, gp, z;
6195 
6196                         t1_new = t1 + direction * stepsize;
6197 
6198                         // arc length between c1(t1) and c1(t1_new)
6199                         c1dist = arclen(c1, t1, t1_new);
6200 
6201                         // find t2_new such that arc length between c2(t2) and c1(t2_new) equals c1dist.
6202                         t2_new = Numerics.root(exactDist, t2);
6203 
6204                         // c1(t) as complex number
6205                         h = new Complex(c1.X(t1_new), c1.Y(t1_new));
6206 
6207                         // c2(t) as complex number
6208                         g = new Complex(c2.X(t2_new), c2.Y(t2_new));
6209 
6210                         hp = new Complex(Numerics.D(c1.X)(t1_new), Numerics.D(c1.Y)(t1_new));
6211                         gp = new Complex(Numerics.D(c2.X)(t2_new), Numerics.D(c2.Y)(t2_new));
6212 
6213                         // z is angle between the tangents of c1 at t1_new, and c2 at t2_new
6214                         z = Complex.C.div(hp, gp);
6215 
6216                         alpha = Math.atan2(z.imaginary, z.real);
6217                         // Normalizing the quotient
6218                         z.div(Complex.C.abs(z));
6219                         z.mult(g);
6220                         Tx = h.real - z.real;
6221 
6222                         // T = h(t1_new)-g(t2_new)*h'(t1_new)/g'(t2_new);
6223                         Ty = h.imaginary - z.imaginary;
6224 
6225                         // -(10-90) degrees: make corners roll smoothly
6226                         if (alpha < -beta && alpha > -beta9) {
6227                             alpha = -beta;
6228                             rotationLocal.applyOnce(pointlist);
6229                         } else if (alpha > beta && alpha < beta9) {
6230                             alpha = beta;
6231                             rotationLocal.applyOnce(pointlist);
6232                         } else {
6233                             rotation.applyOnce(pointlist);
6234                             translate.applyOnce(pointlist);
6235                             t1 = t1_new;
6236                             t2 = t2_new;
6237                         }
6238                         brd.update();
6239                     };
6240 
6241                     this.start = function () {
6242                         if (time > 0) {
6243                             interval = window.setInterval(this.rolling, time);
6244                         }
6245                         return this;
6246                     };
6247 
6248                     this.stop = function () {
6249                         window.clearInterval(interval);
6250                         return this;
6251                     };
6252                     return this;
6253                 };
6254             return new Roulette();
6255         }
6256     });
6257 
6258     return JXG.Board;
6259 });
6260