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                 // In ownerDoc we need the "real" document object.
820                 // The first version is used in the case of shadowDom,
821                 // the second case in the "normal" case.
822                 ownerDoc = this.document.ownerDocument || this.document, 
823                 docElement = ownerDoc.documentElement || this.document.body.parentNode,
824                 docBody = ownerDoc.body,
825                 container = this.containerObj,
826                 // viewport, content,
827                 zoom, o;
828 
829             /**
830              * During drags and origin moves the container element is usually not changed.
831              * Check the position of the upper left corner at most every 1000 msecs
832              */
833             if (this.cPos.length > 0 &&
834                     (this.mode === this.BOARD_MODE_DRAG || this.mode === this.BOARD_MODE_MOVE_ORIGIN ||
835                     (new Date()).getTime() - this.positionAccessLast < 1000)) {
836                 return this.cPos;
837             }
838             this.positionAccessLast = (new Date()).getTime();
839 
840             // Check if getBoundingClientRect exists. If so, use this as this covers *everything*
841             // even CSS3D transformations etc.
842             // Supported by all browsers but IE 6, 7.
843 
844             if (container.getBoundingClientRect) {
845                 crect = container.getBoundingClientRect();
846 
847 
848                 zoom = 1.0;
849                 // Recursively search for zoom style entries.
850                 // This is necessary for reveal.js on webkit.
851                 // It fails if the user does zooming
852                 o = container;
853                 while (o && Type.exists(o.parentNode)) {
854                     if (Type.exists(o.style) && Type.exists(o.style.zoom) && o.style.zoom !== '') {
855                         zoom *= parseFloat(o.style.zoom);
856                     }
857                     o = o.parentNode;
858                 }
859                 cPos = [crect.left * zoom, crect.top * zoom];
860 
861                 // add border width
862                 cPos[0] += Env.getProp(container, 'border-left-width');
863                 cPos[1] += Env.getProp(container, 'border-top-width');
864 
865                 // vml seems to ignore paddings
866                 if (this.renderer.type !== 'vml') {
867                     // add padding
868                     cPos[0] += Env.getProp(container, 'padding-left');
869                     cPos[1] += Env.getProp(container, 'padding-top');
870                 }
871 
872                 this.cPos = cPos.slice();
873                 return this.cPos;
874             }
875 
876             //
877             //  OLD CODE
878             //  IE 6-7 only:
879             //
880             cPos = Env.getOffset(container);
881             doc = this.document.documentElement.ownerDocument;
882 
883             if (!this.containerObj.currentStyle && doc.defaultView) {     // Non IE
884                 // this is for hacks like this one used in wordpress for the admin bar:
885                 // html { margin-top: 28px }
886                 // seems like it doesn't work in IE
887 
888                 cPos[0] += Env.getProp(docElement, 'margin-left');
889                 cPos[1] += Env.getProp(docElement, 'margin-top');
890 
891                 cPos[0] += Env.getProp(docElement, 'border-left-width');
892                 cPos[1] += Env.getProp(docElement, 'border-top-width');
893 
894                 cPos[0] += Env.getProp(docElement, 'padding-left');
895                 cPos[1] += Env.getProp(docElement, 'padding-top');
896             }
897 
898             if (docBody) {
899                 cPos[0] += Env.getProp(docBody, 'left');
900                 cPos[1] += Env.getProp(docBody, 'top');
901             }
902 
903             // Google Translate offers widgets for web authors. These widgets apparently tamper with the clientX
904             // and clientY coordinates of the mouse events. The minified sources seem to be the only publicly
905             // available version so we're doing it the hacky way: Add a fixed offset.


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