1 /*
  2     Copyright 2008-2026
  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 <https://www.gnu.org/licenses/>
 29     and <https://opensource.org/licenses/MIT/>.
 30  */
 31 
 32 /*global JXG: true, define: true, AMprocessNode: true, MathJax: true, window: true, document: true, init: true, translateASCIIMath: true, google: true*/
 33 
 34 /*jslint nomen: true, plusplus: true*/
 35 
 36 /**
 37  * @fileoverview The JXG.Board class is defined in this file. JXG.Board controls all properties and methods
 38  * used to manage a geonext board like managing geometric elements, managing mouse and touch events, etc.
 39  */
 40 
 41 import JXG from '../jxg.js';
 42 import Const from './constants.js';
 43 import Coords from './coords.js';
 44 import Options from '../options.js';
 45 import Numerics from '../math/numerics.js';
 46 import Mat from '../math/math.js';
 47 import Geometry from '../math/geometry.js';
 48 import Complex from '../math/complex.js';
 49 import Statistics from '../math/statistics.js';
 50 import JessieCode from '../parser/jessiecode.js';
 51 import Color from '../utils/color.js';
 52 import Type from '../utils/type.js';
 53 import EventEmitter from '../utils/event.js';
 54 import Env from '../utils/env.js';
 55 import Composition from './composition.js';
 56 
 57 /**
 58  * Constructs a new Board object.
 59  * @class JXG.Board controls all properties and methods used to manage a geonext board like managing geometric
 60  * elements, managing mouse and touch events, etc. You probably don't want to use this constructor directly.
 61  * Please use {@link JXG.JSXGraph.initBoard} to initialize a board.
 62  * @constructor
 63  * @param {String|Object} container The id of or reference to the HTML DOM element
 64  * the board is drawn in. This is usually a HTML div. If it is the reference to an HTML element and this element does not have an attribute "id",
 65  * this attribute "id" is set to a random value.
 66  * @param {JXG.AbstractRenderer} renderer The reference of a renderer.
 67  * @param {String} id Unique identifier for the board, may be an empty string or null or even undefined.
 68  * @param {JXG.Coords} origin The coordinates where the origin is placed, in user coordinates.
 69  * @param {Number} zoomX Zoom factor in x-axis direction
 70  * @param {Number} zoomY Zoom factor in y-axis direction
 71  * @param {Number} unitX Units in x-axis direction
 72  * @param {Number} unitY Units in y-axis direction
 73  * @param {Number} canvasWidth  The width of canvas
 74  * @param {Number} canvasHeight The height of canvas
 75  * @param {Object} attributes The attributes object given to {@link JXG.JSXGraph.initBoard}
 76  * @borrows JXG.EventEmitter#on as this.on
 77  * @borrows JXG.EventEmitter#off as this.off
 78  * @borrows JXG.EventEmitter#triggerEventHandlers as this.triggerEventHandlers
 79  * @borrows JXG.EventEmitter#eventHandlers as this.eventHandlers
 80  */
 81 JXG.Board = function (container, renderer, id,
 82     origin, zoomX, zoomY, unitX, unitY,
 83     canvasWidth, canvasHeight, attributes) {
 84     /**
 85      * Board is in no special mode, objects are highlighted on mouse over and objects may be
 86      * clicked to start drag&drop.
 87      * @type Number
 88      * @constant
 89      */
 90     this.BOARD_MODE_NONE = 0x0000;
 91 
 92     /**
 93      * Board is in drag mode, objects aren't highlighted on mouse over and the object referenced in
 94      * {@link JXG.Board#mouse} is updated on mouse movement.
 95      * @type Number
 96      * @constant
 97      */
 98     this.BOARD_MODE_DRAG = 0x0001;
 99 
100     /**
101      * In this mode a mouse move changes the origin's screen coordinates.
102      * @type Number
103      * @constant
104      */
105     this.BOARD_MODE_MOVE_ORIGIN = 0x0002;
106 
107     /**
108      * This mode is active when the user zooms
109      * @type Number
110      * @constant
111      */
112     this.BOARD_MODE_ZOOM = 0x0011;
113 
114     /**
115      * Update is made with low quality, e.g. graphs are evaluated at a lesser amount of points.
116      * @type Number
117      * @constant
118      * @see JXG.Board#updateQuality
119      */
120     this.BOARD_QUALITY_LOW = 0x1;
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_QUALITY_HIGH = 0x2;
129 
130     /**
131      * Pointer to the document element containing the board.
132      * @type Object
133      */
134     if (Type.exists(attributes.document) && attributes.document !== false) {
135         this.document = attributes.document;
136     } else if (Env.isBrowser) {
137         this.document = document;
138     }
139 
140     /**
141      * The html-id of the html element containing the board.
142      * @type String
143      */
144     this.container = ''; // container
145 
146     /**
147      * ID of the board
148      * @type String
149      */
150     this.id = '';
151 
152     /**
153      * Pointer to the html element containing the board.
154      * @type Object
155      */
156     this.containerObj = null; // (Env.isBrowser ? this.document.getElementById(this.container) : null);
157 
158     // Set this.container and this.containerObj
159     if (Type.isString(container)) {
160         // Hosting div is given as string
161         this.container = container; // container
162         this.containerObj = (Env.isBrowser ? this.document.getElementById(this.container) : null);
163 
164     } else if (Env.isBrowser) {
165 
166         // Hosting div is given as object pointer
167         this.containerObj = container;
168         this.container = this.containerObj.getAttribute('id');
169         if (this.container === null) {
170             // Set random ID to this.container, but not to the DOM element
171 
172             this.container = 'null' + parseInt(Math.random() * 16777216).toString();
173         }
174     }
175 
176     if (Env.isBrowser && renderer.type !== 'no' && this.containerObj === null) {
177         throw new Error('\nJSXGraph: HTML container element "' + container + '" not found.');
178     }
179 
180     // TODO
181     // Why do we need this.id AND this.container?
182     // There was never a board attribute "id".
183     // The origin seems to be that in the geonext renderer we use a separate id, extracted from the GEONExT file.
184     if (Type.exists(id) && id !== '' && Env.isBrowser && !Type.exists(this.document.getElementById(id))) {
185         // If the given id is not valid, generate an unique id
186         this.id = id;
187     } else {
188         this.id = this.generateId();
189     }
190 
191     /**
192      * A reference to this boards renderer.
193      * @type JXG.AbstractRenderer
194      * @name JXG.Board#renderer
195      * @private
196      * @ignore
197      */
198     this.renderer = renderer;
199 
200     /**
201      * Grids keeps track of all grids attached to this board.
202      * @type Array
203      * @private
204      */
205     this.grids = [];
206 
207     /**
208      * Copy of the default options
209      * @type JXG.Options
210      */
211     this.options = Type.deepCopy(Options);  // A possible theme is not yet merged in
212 
213     /**
214      * Board attributes
215      * @type Object
216      */
217     this.attr = attributes;
218 
219     if (this.attr.theme !== 'default' && Type.exists(JXG.themes[this.attr.theme])) {
220         Type.mergeAttr(this.options, JXG.themes[this.attr.theme], true);
221     }
222 
223     /**
224      * Dimension of the board.
225      * @default 2
226      * @type Number
227      */
228     this.dimension = 2;
229     this.jc = new JessieCode();
230     this.jc.use(this);
231 
232     /**
233      * Coordinates of the boards origin. This a object with the two properties
234      * usrCoords and scrCoords. usrCoords always equals [1, 0, 0] and scrCoords
235      * stores the boards origin in homogeneous screen coordinates.
236      * @type Object
237      * @private
238      */
239     this.origin = {};
240     this.origin.usrCoords = [1, 0, 0];
241     this.origin.scrCoords = [1, origin[0], origin[1]];
242 
243     /**
244      * Zoom factor in X direction. It only stores the zoom factor to be able
245      * to get back to 100% in zoom100().
246      * @name JXG.Board.zoomX
247      * @type Number
248      * @private
249      * @ignore
250      */
251     this.zoomX = zoomX;
252 
253     /**
254      * Zoom factor in Y direction. It only stores the zoom factor to be able
255      * to get back to 100% in zoom100().
256      * @name JXG.Board.zoomY
257      * @type Number
258      * @private
259      * @ignore
260      */
261     this.zoomY = zoomY;
262 
263     /**
264      * The number of pixels which represent one unit in user-coordinates in x direction.
265      * @type Number
266      * @private
267      */
268     this.unitX = unitX * this.zoomX;
269 
270     /**
271      * The number of pixels which represent one unit in user-coordinates in y direction.
272      * @type Number
273      * @private
274      */
275     this.unitY = unitY * this.zoomY;
276 
277     /**
278      * Keep aspect ratio if bounding box is set and the width/height ratio differs from the
279      * width/height ratio of the canvas.
280      * @type Boolean
281      * @private
282      */
283     this.keepaspectratio = false;
284 
285     /**
286      * Canvas width.
287      * @type Number
288      * @private
289      */
290     this.canvasWidth = canvasWidth;
291 
292     /**
293      * Canvas Height
294      * @type Number
295      * @private
296      */
297     this.canvasHeight = canvasHeight;
298 
299     EventEmitter.eventify(this);
300 
301     this.hooks = [];
302 
303     /**
304      * An array containing all other boards that are updated after this board has been updated.
305      * @type Array
306      * @see JXG.Board#addChild
307      * @see JXG.Board#removeChild
308      */
309     this.dependentBoards = [];
310 
311     /**
312      * During the update process this is set to false to prevent an endless loop.
313      * @default false
314      * @type Boolean
315      */
316     this.inUpdate = false;
317 
318     /**
319      * 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.
320      * @type Object
321      */
322     this.objects = {};
323 
324     /**
325      * An array containing all geometric objects on the board in the order of construction.
326      * @type Array
327      */
328     this.objectsList = [];
329 
330     /**
331      * 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.
332      * @type Object
333      */
334     this.groups = {};
335 
336     /**
337      * Stores all the objects that are currently running an animation.
338      * @type Object
339      */
340     this.animationObjects = {};
341 
342     /**
343      * An associative array containing all highlighted elements belonging to the board.
344      * @type Object
345      */
346     this.highlightedObjects = {};
347 
348     /**
349      * Number of objects ever created on this board. This includes every object, even invisible and deleted ones.
350      * @type Number
351      */
352     this.numObjects = 0;
353 
354     /**
355      * An associative array / dictionary 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.
356      * @type Object
357      */
358     this.elementsByName = {};
359 
360     /**
361      * The board mode the board is currently in. Possible values are
362      * <ul>
363      * <li>JXG.Board.BOARD_MODE_NONE</li>
364      * <li>JXG.Board.BOARD_MODE_DRAG</li>
365      * <li>JXG.Board.BOARD_MODE_MOVE_ORIGIN</li>
366      * </ul>
367      * @type Number
368      */
369     this.mode = this.BOARD_MODE_NONE;
370 
371     /**
372      * The update quality of the board. In most cases this is set to {@link JXG.Board#BOARD_QUALITY_HIGH}.
373      * If {@link JXG.Board#mode} equals {@link JXG.Board#BOARD_MODE_DRAG} this is set to
374      * {@link JXG.Board#BOARD_QUALITY_LOW} to speed up the update process by e.g. reducing the number of
375      * evaluation points when plotting functions. Possible values are
376      * <ul>
377      * <li>BOARD_QUALITY_LOW</li>
378      * <li>BOARD_QUALITY_HIGH</li>
379      * </ul>
380      * @type Number
381      * @see JXG.Board#mode
382      */
383     this.updateQuality = this.BOARD_QUALITY_HIGH;
384 
385     /**
386      * If true updates are skipped.
387      * @type Boolean
388      */
389     this.isSuspendedRedraw = false;
390 
391     this.calculateSnapSizes();
392 
393     /**
394      * The distance from the mouse to the dragged object in x direction when the user clicked the mouse button.
395      * @type Number
396      * @see JXG.Board#drag_dy
397      */
398     this.drag_dx = 0;
399 
400     /**
401      * The distance from the mouse to the dragged object in y direction when the user clicked the mouse button.
402      * @type Number
403      * @see JXG.Board#drag_dx
404      */
405     this.drag_dy = 0;
406 
407     /**
408      * The last position where a drag event has been fired.
409      * @type Array
410      * @see JXG.Board#moveObject
411      */
412     this.drag_position = [0, 0];
413 
414     /**
415      * References to the object that is dragged with the mouse on the board.
416      * @type JXG.GeometryElement
417      * @see JXG.Board#touches
418      */
419     this.mouse = {};
420 
421     /**
422      * Keeps track on touched elements, like {@link JXG.Board#mouse} does for mouse events.
423      * @type Array
424      * @see JXG.Board#mouse
425      */
426     this.touches = [];
427 
428     /**
429      * A string containing the XML text of the construction.
430      * This is set in {@link JXG.FileReader.parseString}.
431      * Only useful if a construction is read from a GEONExT-, Intergeo-, Geogebra-, or Cinderella-File.
432      * @type String
433      */
434     this.xmlString = '';
435 
436     /**
437      * Cached result of getCoordsTopLeftCorner for touch/mouseMove-Events to save some DOM operations.
438      * @type Array
439      */
440     this.cPos = [];
441 
442     /**
443      * Contains the last time (epoch, msec) since the last touchMove event which was not thrown away or since
444      * touchStart because Android's Webkit browser fires too much of them.
445      * @type Number
446      */
447     this.touchMoveLast = 0;
448 
449     /**
450      * Contains the pointerId of the last touchMove event which was not thrown away or since
451      * touchStart because Android's Webkit browser fires too much of them.
452      * @type Number
453      */
454     this.touchMoveLastId = Infinity;
455 
456     /**
457      * Contains the last time (epoch, msec) since the last getCoordsTopLeftCorner call which was not thrown away.
458      * @type Number
459      */
460     this.positionAccessLast = 0;
461 
462     /**
463      * Collects all elements that triggered a mouse down event.
464      * @type Array
465      */
466     this.downObjects = [];
467     this.clickObjects = {};
468 
469     /**
470      * Collects all elements that have keyboard focus. Should be either one or no element.
471      * Elements are stored with their id.
472      * @type Array
473      */
474     this.focusObjects = [];
475 
476     if (this.attr.showcopyright || this.attr.showlogo) {
477         this.renderer.displayLogo(Const.licenseLogo, parseInt(this.options.text.fontSize, 10), this);
478     }
479 
480     if (this.attr.showcopyright) {
481         this.renderer.displayCopyright(Const.licenseText, parseInt(this.options.text.fontSize, 10));
482     }
483 
484     /**
485      * Full updates are needed after zoom and axis translates. This saves some time during an update.
486      * @default false
487      * @type Boolean
488      */
489     this.needsFullUpdate = false;
490 
491     /**
492      * If reducedUpdate is set to true then only the dragged element and few (e.g. 2) following
493      * elements are updated during mouse move. On mouse up the whole construction is
494      * updated. This enables us to be fast even on very slow devices.
495      * @type Boolean
496      * @default false
497      */
498     this.reducedUpdate = false;
499 
500     /**
501      * The current color blindness deficiency is stored in this property. If color blindness is not emulated
502      * at the moment, it's value is 'none'.
503      */
504     this.currentCBDef = 'none';
505 
506     /**
507      * If GEONExT constructions are displayed, then this property should be set to true.
508      * At the moment there should be no difference. But this may change.
509      * This is set in {@link JXG.GeonextReader.readGeonext}.
510      * @type Boolean
511      * @default false
512      * @see JXG.GeonextReader.readGeonext
513      */
514     this.geonextCompatibilityMode = false;
515 
516     if (this.options.text.useASCIIMathML && translateASCIIMath) {
517         init();
518     } else {
519         this.options.text.useASCIIMathML = false;
520     }
521 
522     /**
523      * A flag which tells if the board registers mouse events.
524      * @type Boolean
525      * @default false
526      */
527     this.hasMouseHandlers = false;
528 
529     /**
530      * A flag which tells if the board registers touch events.
531      * @type Boolean
532      * @default false
533      */
534     this.hasTouchHandlers = false;
535 
536     /**
537      * A flag which stores if the board registered pointer events.
538      * @type Boolean
539      * @default false
540      */
541     this.hasPointerHandlers = false;
542 
543     /**
544      * A flag which stores if the board registered zoom events, i.e. mouse wheel scroll events.
545      * @type Boolean
546      * @default false
547      */
548     this.hasWheelHandlers = false;
549 
550     /**
551      * A flag which tells if the board the JXG.Board#mouseUpListener is currently registered.
552      * @type Boolean
553      * @default false
554      */
555     this.hasMouseUp = false;
556 
557     /**
558      * A flag which tells if the board the JXG.Board#touchEndListener is currently registered.
559      * @type Boolean
560      * @default false
561      */
562     this.hasTouchEnd = false;
563 
564     /**
565      * A flag which tells us if the board has a pointerUp event registered at the moment.
566      * @type Boolean
567      * @default false
568      */
569     this.hasPointerUp = false;
570 
571     /**
572      * Array containing the events related to resizing that have event listeners.
573      * @type Array
574      * @default []
575      */
576     this.resizeHandlers = [];
577 
578     /**
579      * Offset for large coords elements like images
580      * @type Array
581      * @private
582      * @default [0, 0]
583      */
584     this._drag_offset = [0, 0];
585 
586     /**
587      * Stores the input device used in the last down or move event.
588      * @type String
589      * @private
590      * @default 'mouse'
591      */
592     this._inputDevice = 'mouse';
593 
594     /**
595      * Keeps a list of pointer devices which are currently touching the screen.
596      * @type Array
597      * @private
598      */
599     this._board_touches = [];
600 
601     /**
602      * A flag which tells us if the board is in the selecting mode
603      * @type Boolean
604      * @default false
605      */
606     this.selectingMode = false;
607 
608     /**
609      * A flag which tells us if the user is selecting
610      * @type Boolean
611      * @default false
612      */
613     this.isSelecting = false;
614 
615     /**
616      * A flag which tells us if the user is scrolling the viewport
617      * @type Boolean
618      * @private
619      * @default false
620      * @see JXG.Board#scrollListener
621      */
622     this._isScrolling = false;
623 
624     /**
625      * A flag which tells us if a resize is in process
626      * @type Boolean
627      * @private
628      * @default false
629      * @see JXG.Board#resizeListener
630      */
631     this._isResizing = false;
632 
633     /**
634      * A flag which tells us if the update is triggered by a change of the
635      * 3D view. In that case we only have to update the projection of
636      * the 3D elements and can avoid a full board update.
637      *
638      * @type Boolean
639      * @private
640      * @default false
641      */
642     this._change3DView = false;
643 
644     /**
645      * A bounding box for the selection
646      * @type Array
647      * @default [ [0,0], [0,0] ]
648      */
649     this.selectingBox = [[0, 0], [0, 0]];
650 
651     /**
652      * Array to log user activity.
653      * Entries are objects of the form '{type, id, start, end}' notifying
654      * the start time as well as the last time of a single event of type 'type'
655      * on a JSXGraph element of id 'id'.
656      * <p> 'start' and 'end' contain the amount of milliseconds elapsed between 1 January 1970 00:00:00 UTC
657      * and the time the event happened.
658      * <p>
659      * For the time being (i.e. v1.5.0) the only supported type is 'drag'.
660      * @type Array
661      */
662     this.userLog = [];
663 
664     /**
665      * Array of length two containing sketchcurves of the board. In case of mouse or pen
666      * only the first entry is used. In case of finger input, sketchcurves
667      * for the first and second finger are possible.
668      *
669      * @example
670      *  const board = JXG.JSXGraph.initBoard('jxgbox', {
671      *      boundingbox: [-10, 10, 10, -10],
672      *      axis: true,
673      *      sketches: {
674      *          enabled: true,
675      *          0: {strokeWidth: 2, visible: true, maxLength: 20},
676      *          1: {strokeWidth: 3, visible: true}
677      *      }
678      *  });
679      *
680      *  // Use event handler to access the actual curve
681      *  board.on('move', function(evt) {
682      *    console.log('JSXGraph example: move', this.sketches[0].dataX.length);
683      *  });
684      *
685      *  // Use event handler to access the actual curve
686      *  board.on('up', function(evt) {
687      *    console.log('JSXGraph example: up', this.sketches[0].dataX.length);
688      *  });
689      *
690      * </pre><div id="JXGf62e7217-a3ee-45b8-92e4-ce0d0d789df5" class="jxgbox" style="width: 300px; height: 300px;"></div>
691      * <script type="text/javascript">
692      *     (function() {
693      *         var board = JXG.JSXGraph.initBoard('JXGf62e7217-a3ee-45b8-92e4-ce0d0d789df5',
694      *             {   boundingbox: [-10, 10, 10, -10],
695      *                 axis: true,
696      *                 sketches: {
697      *                     enabled: true,
698      *                     0: {strokeWidth: 2, visible: true, maxLength: 20},
699      *                     1: {strokeWidth: 3, visible: true}
700      *                 }
701      *             });
702      *  // Use event handler to access the actual curve
703      *  board.on('move', function(evt) {
704      *    console.log('JSXGraph example: move', this.sketches[0].dataX.length);
705      *  });
706      *
707      *  // Use event handler to access the actual curve
708      *  board.on('up', function(evt) {
709      *    console.log('JSXGraph example: up', this.sketches[0].dataX.length);
710      *  });
711      *
712      *     })();
713      *
714      * </script><pre>
715      *
716      * @type Array
717      * @see SketchCurve
718      * @see JXG.Board#sketch
719      */
720     this.sketches = [null, null];
721 
722     /**
723      * Alias for the first sketchcurve, i.e. for board.sketches[0].
724      * @type {JXG.Curve}
725      * @see JXG.Board#sketches
726      */
727     this.sketch = null; //this.sketches[0];
728 
729     /**
730      * Array of length two of Boolean flags indicating if a pointer device (finger, mouse, pen) is
731      * adding points to board.sketches[i] (i=0,1). i=1 is only used for multi-touch with fingers.
732      * <p>
733      * User-supplied events might use this flag to test if sketching is active.
734      * Usually, this flag is true starting with a down event and ends with the up event.
735      * @type {Array}
736      * @see JXG.Board#sketches
737      *
738      */
739     this.isSketching = [false, false];
740 
741     /**
742      *
743      */
744     this.mathLib = Math;        // Math or JXG.Math.IntervalArithmetic
745 
746     /**
747      *
748      */
749     this.mathLibJXG = JXG.Math; // JXG.Math or JXG.Math.IntervalArithmetic
750 
751     if (this.attr.registerevents === true) {
752         this.attr.registerevents = {
753             fullscreen: true,
754             keyboard: true,
755             pointer: true,
756             resize: true,
757             wheel: true
758         };
759     } else if (typeof this.attr.registerevents === 'object') {
760         if (!Type.exists(this.attr.registerevents.fullscreen)) {
761             this.attr.registerevents.fullscreen = true;
762         }
763         if (!Type.exists(this.attr.registerevents.keyboard)) {
764             this.attr.registerevents.keyboard = true;
765         }
766         if (!Type.exists(this.attr.registerevents.pointer)) {
767             this.attr.registerevents.pointer = true;
768         }
769         if (!Type.exists(this.attr.registerevents.resize)) {
770             this.attr.registerevents.resize = true;
771         }
772         if (!Type.exists(this.attr.registerevents.wheel)) {
773             this.attr.registerevents.wheel = true;
774         }
775     }
776     if (this.attr.registerevents !== false) {
777         if (this.attr.registerevents.fullscreen) {
778             this.addFullscreenEventHandlers();
779         }
780         if (this.attr.registerevents.keyboard) {
781             this.addKeyboardEventHandlers();
782         }
783         if (this.attr.registerevents.pointer) {
784             this.addEventHandlers();
785         }
786         if (this.attr.registerevents.resize) {
787             this.addResizeEventHandlers();
788         }
789         if (this.attr.registerevents.wheel) {
790             this.addWheelEventHandlers();
791         }
792     }
793 };
794 
795 Type.copyMethodMap(JXG.Board, {
796     update: 'update',
797     fullUpdate: 'fullUpdate',
798     on: 'on',
799     off: 'off',
800     trigger: 'trigger',
801     setAttribute: 'setAttribute',
802     setBoundingBox: 'setBoundingBox',
803     setView: 'setBoundingBox',
804     getBoundingBox: 'getBoundingBox',
805     BoundingBox: 'getBoundingBox',
806     getView: 'getBoundingBox',
807     View: 'getBoundingBox',
808     migratePoint: 'migratePoint',
809     colorblind: 'emulateColorblindness',
810     suspendUpdate: 'suspendUpdate',
811     unsuspendUpdate: 'unsuspendUpdate',
812     clearTraces: 'clearTraces',
813     left: 'clickLeftArrow',
814     right: 'clickRightArrow',
815     up: 'clickUpArrow',
816     down: 'clickDownArrow',
817     zoomIn: 'zoomIn',
818     zoomOut: 'zoomOut',
819     zoom100: 'zoom100',
820     zoomElements: 'zoomElements',
821     remove: 'removeObject',
822     removeObject: 'removeObject'
823 });
824 
825 JXG.extend(
826     JXG.Board.prototype,
827     /** @lends JXG.Board.prototype */ {
828         /**
829          * Generates an unique name for the given object. The result depends on the objects type, if the
830          * object is a {@link JXG.Point}, capital characters are used, if it is of type {@link JXG.Line}
831          * only lower case characters are used. If object is of type {@link JXG.Polygon}, a bunch of lower
832          * case characters prefixed with P_ are used. If object is of type {@link JXG.Circle} the name is
833          * generated using lower case characters. prefixed with k_ is used. In any other case, lower case
834          * chars prefixed with s_ is used.
835          * @param {Object} object Reference of an JXG.GeometryElement that is to be named.
836          * @returns {String} Unique name for the object.
837          */
838         generateName: function (object) {
839             var possibleNames, i,
840                 maxNameLength = this.attr.maxnamelength,
841                 pre = '',
842                 post = '',
843                 indices = [],
844                 name = '';
845 
846             if (object.type === Const.OBJECT_TYPE_TICKS) {
847                 return '';
848             }
849 
850             if (Type.isPoint(object) || Type.isPoint3D(object)) {
851                 // points have capital letters
852                 possibleNames = [
853                     '', 'A', 'B', 'C', 'D', 'E', 'F', 'G', 'H', 'I', 'J', 'K', 'L', 'M', 'N', 'O', 'P', 'Q', 'R', 'S', 'T', 'U', 'V', 'W', 'X', 'Y', 'Z'
854                 ];
855             } else if (object.type === Const.OBJECT_TYPE_ANGLE) {
856                 possibleNames = [
857                     '', 'α', 'β', 'γ', 'δ', 'ε', 'ζ', 'η', 'θ', 'ι', 'κ', 'λ',
858                     'μ', 'ν', 'ξ', 'ο', 'π', 'ρ', 'σ', 'τ', 'υ', 'φ', 'χ', 'ψ', 'ω'
859                 ];
860             } else {
861                 // all other elements get lowercase labels
862                 possibleNames = [
863                     '', 'a', 'b', 'c', 'd', 'e', 'f', 'g', 'h', 'i', 'j', 'k', 'l', 'm', 'n', 'o', 'p', 'q', 'r', 's', 't', 'u', 'v', 'w', 'x', 'y', 'z'
864                 ];
865             }
866 
867             if (
868                 !Type.isPoint(object) &&
869                 !Type.isPoint3D(object) &&
870                 object.elementClass !== Const.OBJECT_CLASS_LINE &&
871                 object.type !== Const.OBJECT_TYPE_ANGLE
872             ) {
873                 if (object.type === Const.OBJECT_TYPE_POLYGON) {
874                     pre = 'P_{';
875                 } else if (object.elementClass === Const.OBJECT_CLASS_CIRCLE) {
876                     pre = 'k_{';
877                 } else if (object.elementClass === Const.OBJECT_CLASS_TEXT) {
878                     pre = 't_{';
879                 } else {
880                     pre = 's_{';
881                 }
882                 post = '}';
883             }
884 
885             for (i = 0; i < maxNameLength; i++) {
886                 indices[i] = 0;
887             }
888 
889             while (indices[maxNameLength - 1] < possibleNames.length) {
890                 for (indices[0] = 1; indices[0] < possibleNames.length; indices[0]++) {
891                     name = pre;
892 
893                     for (i = maxNameLength; i > 0; i--) {
894                         name += possibleNames[indices[i - 1]];
895                     }
896 
897                     if (!Type.exists(this.elementsByName[name + post])) {
898                         return name + post;
899                     }
900                 }
901                 indices[0] = possibleNames.length;
902 
903                 for (i = 1; i < maxNameLength; i++) {
904                     if (indices[i - 1] === possibleNames.length) {
905                         indices[i - 1] = 1;
906                         indices[i] += 1;
907                     }
908                 }
909             }
910 
911             return '';
912         },
913 
914         /**
915          * Generates unique id for a board. The result is randomly generated and prefixed with 'jxgBoard'.
916          * @returns {String} Unique id for a board.
917          */
918         generateId: function () {
919             var r = 1;
920 
921             // as long as we don't have a unique id generate a new one
922             while (Type.exists(JXG.boards['jxgBoard' + r])) {
923                 r = Math.round(Math.random() * 16777216);
924             }
925 
926             return 'jxgBoard' + r;
927         },
928 
929         /**
930          * Composes an id for an element. If the ID is empty ('' or null) a new ID is generated, depending on the
931          * object type. As a side effect {@link JXG.Board#numObjects}
932          * is updated.
933          * @param {Object} obj Reference of an geometry object that needs an id.
934          * @param {Number} type Type of the object.
935          * @returns {String} Unique id for an element.
936          */
937         setId: function (obj, type) {
938             var randomNumber,
939                 num = this.numObjects,
940                 elId = obj.id;
941 
942             this.numObjects += 1;
943 
944             // If no id is provided or id is empty string, a new one is chosen
945             if (elId === '' || !Type.exists(elId)) {
946                 elId = this.id + type + num;
947                 while (Type.exists(this.objects[elId])) {
948                     randomNumber = Math.round(Math.random() * 65535);
949                     elId = this.id + type + num + '-' + randomNumber;
950                 }
951             }
952 
953             obj.id = elId;
954             this.objects[elId] = obj;
955             obj._pos = this.objectsList.length;
956             this.objectsList[this.objectsList.length] = obj;
957 
958             return elId;
959         },
960 
961         /**
962          * After construction of the object the visibility is set
963          * and the label is constructed if necessary.
964          * @param {Object} obj The object to add.
965          */
966         finalizeAdding: function (obj) {
967             if (obj.evalVisProp('visible') === false) {
968                 this.renderer.display(obj, false);
969             }
970         },
971 
972         finalizeLabel: function (obj) {
973             if (
974                 obj.hasLabel &&
975                 !obj.label.evalVisProp('islabel') &&
976                 obj.label.evalVisProp('visible') === false
977             ) {
978                 this.renderer.display(obj.label, false);
979             }
980         },
981 
982         /**********************************************************
983          *
984          * Event Handler helpers
985          *
986          **********************************************************/
987 
988         /**
989          * Returns false if the event has been triggered faster than the maximum frame rate.
990          *
991          * @param {Event} evt Event object given by the browser (unused)
992          * @returns {Boolean} If the event has been triggered faster than the maximum frame rate, false is returned.
993          * @private
994          * @see JXG.Board#pointerMoveListener
995          * @see JXG.Board#touchMoveListener
996          * @see JXG.Board#mouseMoveListener
997          */
998         checkFrameRate: function (evt) {
999             var handleEvt = false,
1000                 time = new Date().getTime();
1001 
1002             if (Type.exists(evt.pointerId) && this.touchMoveLastId !== evt.pointerId) {
1003                 handleEvt = true;
1004                 this.touchMoveLastId = evt.pointerId;
1005             }
1006             if (!handleEvt && (time - this.touchMoveLast) * this.attr.maxframerate >= 1000) {
1007                 handleEvt = true;
1008             }
1009             if (handleEvt) {
1010                 this.touchMoveLast = time;
1011             }
1012             return handleEvt;
1013         },
1014 
1015         /**
1016          * Calculates mouse coordinates relative to the boards container.
1017          * @returns {Array} Array of coordinates relative the boards container top left corner.
1018          */
1019         getCoordsTopLeftCorner: function () {
1020             var cPos,
1021                 doc,
1022                 crect,
1023                 // In ownerDoc we need the 'real' document object.
1024                 // The first version is used in the case of shadowDOM,
1025                 // the second case in the 'normal' case.
1026                 ownerDoc = this.document.ownerDocument || this.document,
1027                 docElement = ownerDoc.documentElement || this.document.body.parentNode,
1028                 docBody = ownerDoc.body,
1029                 container = this.containerObj,
1030                 zoom,
1031                 o;
1032 
1033             /**
1034              * During drags and origin moves the container element is usually not changed.
1035              * Check the position of the upper left corner at most every 1000 msecs
1036              */
1037             if (
1038                 this.cPos.length > 0 &&
1039                 (this.mode === this.BOARD_MODE_DRAG ||
1040                     this.mode === this.BOARD_MODE_MOVE_ORIGIN ||
1041                     new Date().getTime() - this.positionAccessLast < 1000)
1042             ) {
1043                 return this.cPos;
1044             }
1045             this.positionAccessLast = new Date().getTime();
1046 
1047             // Check if getBoundingClientRect exists. If so, use this as this covers *everything*
1048             // even CSS3D transformations etc.
1049             // Supported by all browsers but IE 6, 7.
1050             if (container.getBoundingClientRect) {
1051                 crect = container.getBoundingClientRect();
1052 
1053                 zoom = 1.0;
1054                 // Recursively search for zoom style entries.
1055                 // This is necessary for reveal.js on webkit.
1056                 // It fails if the user does zooming
1057                 o = container;
1058                 while (o && Type.exists(o.parentNode)) {
1059                     if (
1060                         Type.exists(o.style) &&
1061                         Type.exists(o.style.zoom) &&
1062                         o.style.zoom !== ''
1063                     ) {
1064                         zoom *= parseFloat(o.style.zoom);
1065                     }
1066                     o = o.parentNode;
1067                 }
1068                 cPos = [crect.left * zoom, crect.top * zoom];
1069 
1070                 // add border width
1071                 cPos[0] += Env.getProp(container, 'border-left-width');
1072                 cPos[1] += Env.getProp(container, 'border-top-width');
1073 
1074                 // vml seems to ignore paddings
1075                 if (this.renderer.type !== 'vml') {
1076                     // add padding
1077                     cPos[0] += Env.getProp(container, 'padding-left');
1078                     cPos[1] += Env.getProp(container, 'padding-top');
1079                 }
1080 
1081                 this.cPos = cPos.slice();
1082                 return this.cPos;
1083             }
1084 
1085             //
1086             //  OLD CODE
1087             //  IE 6-7 only:
1088             //
1089             cPos = Env.getOffset(container);
1090             doc = this.document.documentElement.ownerDocument;
1091 
1092             if (!this.containerObj.currentStyle && doc.defaultView) {
1093                 // Non IE
1094                 // this is for hacks like this one used in wordpress for the admin bar:
1095                 // html { margin-top: 28px }
1096                 // seems like it doesn't work in IE
1097 
1098                 cPos[0] += Env.getProp(docElement, 'margin-left');
1099                 cPos[1] += Env.getProp(docElement, 'margin-top');
1100 
1101                 cPos[0] += Env.getProp(docElement, 'border-left-width');
1102                 cPos[1] += Env.getProp(docElement, 'border-top-width');
1103 
1104                 cPos[0] += Env.getProp(docElement, 'padding-left');
1105                 cPos[1] += Env.getProp(docElement, 'padding-top');
1106             }
1107 
1108             if (docBody) {
1109                 cPos[0] += Env.getProp(docBody, 'left');
1110                 cPos[1] += Env.getProp(docBody, 'top');
1111             }
1112 
1113             // Google Translate offers widgets for web authors. These widgets apparently tamper with the clientX
1114             // and clientY coordinates of the mouse events. The minified sources seem to be the only publicly
1115             // available version so we're doing it the hacky way: Add a fixed offset.
1116             // see https://groups.google.com/d/msg/google-translate-general/H2zj0TNjjpY/jw6irtPlCw8J
1117             if (typeof google === 'object' && google.translate) {
1118                 cPos[0] += 10;
1119                 cPos[1] += 25;
1120             }
1121 
1122             // add border width
1123             cPos[0] += Env.getProp(container, 'border-left-width');
1124             cPos[1] += Env.getProp(container, 'border-top-width');
1125 
1126             // vml seems to ignore paddings
1127             if (this.renderer.type !== 'vml') {
1128                 // add padding
1129                 cPos[0] += Env.getProp(container, 'padding-left');
1130                 cPos[1] += Env.getProp(container, 'padding-top');
1131             }
1132 
1133             cPos[0] += this.attr.offsetx;
1134             cPos[1] += this.attr.offsety;
1135 
1136             this.cPos = cPos.slice();
1137             return this.cPos;
1138         },
1139 
1140         /**
1141          * This function divides the board into 9 sections and returns an array <tt>[u,v]</tt> which symbolizes the location of <tt>position</tt>.
1142          * Optional a <tt>margin</tt> to the inner of the board is respected.<br>
1143          *
1144          * @name Board#getPointLoc
1145          * @param {Array} position Array of requested position <tt>[x, y]</tt> or <tt>[w, x, y]</tt>.
1146          * @param {Array|Number} [margin] Optional margin for the inner of the board: <tt>[top, right, bottom, left]</tt>. A single number <tt>m</tt> is interpreted as <tt>[m, m, m, m]</tt>.
1147          * @returns {Array} [u,v] with the following meanings:
1148          * <pre>
1149          *     v    u > |   -1    |    0   |    1   |
1150          * ------------------------------------------
1151          *     1        | [-1,1]  |  [0,1] |  [1,1] |
1152          * ------------------------------------------
1153          *     0        | [-1,0]  |  Board |  [1,0] |
1154          * ------------------------------------------
1155          *    -1        | [-1,-1] | [0,-1] | [1,-1] |
1156          * </pre>
1157          * Positions inside the board (minus margin) return the value <tt>[0,0]</tt>.
1158          *
1159          * @example
1160          *      var point1, point2, point3, point4, margin,
1161          *             p1Location, p2Location, p3Location, p4Location,
1162          *             helppoint1, helppoint2, helppoint3, helppoint4;
1163          *
1164          *      // margin to make the boundingBox virtually smaller
1165          *      margin = [2,2,2,2];
1166          *
1167          *      // Points which are seen on screen
1168          *      point1 = board.create('point', [0,0]);
1169          *      point2 = board.create('point', [0,7]);
1170          *      point3 = board.create('point', [7,7]);
1171          *      point4 = board.create('point', [-7,-5]);
1172          *
1173          *      p1Location = board.getPointLoc(point1.coords.usrCoords, margin);
1174          *      p2Location = board.getPointLoc(point2.coords.usrCoords, margin);
1175          *      p3Location = board.getPointLoc(point3.coords.usrCoords, margin);
1176          *      p4Location = board.getPointLoc(point4.coords.usrCoords, margin);
1177          *
1178          *      // Text seen on screen
1179          *      board.create('text', [1,-1, "getPointLoc(A): " + "[" + p1Location + "]"])
1180          *      board.create('text', [1,-2, "getPointLoc(B): " + "[" + p2Location + "]"])
1181          *      board.create('text', [1,-3, "getPointLoc(C): " + "[" + p3Location + "]"])
1182          *      board.create('text', [1,-4, "getPointLoc(D): " + "[" + p4Location + "]"])
1183          *
1184          *
1185          *      // Helping points that are used to create the helping lines
1186          *      helppoint1 = board.create('point', [(function (){
1187          *          var bbx = board.getBoundingBox();
1188          *          return [bbx[2] - 2, bbx[1] -2];
1189          *      })], {
1190          *          visible: false,
1191          *      })
1192          *
1193          *      helppoint2 = board.create('point', [(function (){
1194          *          var bbx = board.getBoundingBox();
1195          *          return [bbx[0] + 2, bbx[1] -2];
1196          *      })], {
1197          *          visible: false,
1198          *      })
1199          *
1200          *      helppoint3 = board.create('point', [(function (){
1201          *          var bbx = board.getBoundingBox();
1202          *          return [bbx[0]+ 2, bbx[3] + 2];
1203          *      })],{
1204          *          visible: false,
1205          *      })
1206          *
1207          *      helppoint4 = board.create('point', [(function (){
1208          *          var bbx = board.getBoundingBox();
1209          *          return [bbx[2] -2, bbx[3] + 2];
1210          *      })], {
1211          *          visible: false,
1212          *      })
1213          *
1214          *      // Helping lines to visualize the 9 sectors and the margin
1215          *      board.create('line', [helppoint1, helppoint2]);
1216          *      board.create('line', [helppoint2, helppoint3]);
1217          *      board.create('line', [helppoint3, helppoint4]);
1218          *      board.create('line', [helppoint4, helppoint1]);
1219          *
1220          * </pre><div id="JXG4b3efef5-839d-4fac-bad1-7a14c0a89c70" class="jxgbox" style="width: 500px; height: 500px;"></div>
1221          * <script type="text/javascript">
1222          *     (function() {
1223          *         var board = JXG.JSXGraph.initBoard('JXG4b3efef5-839d-4fac-bad1-7a14c0a89c70',
1224          *             {boundingbox: [-8, 8, 8,-8], maxboundingbox: [-7.5,7.5,7.5,-7.5], axis: true, showcopyright: false, shownavigation: false, showZoom: false});
1225          *     var point1, point2, point3, point4, margin,
1226          *             p1Location, p2Location, p3Location, p4Location,
1227          *             helppoint1, helppoint2, helppoint3, helppoint4;
1228          *
1229          *      // margin to make the boundingBox virtually smaller
1230          *      margin = [2,2,2,2];
1231          *
1232          *      // Points which are seen on screen
1233          *      point1 = board.create('point', [0,0]);
1234          *      point2 = board.create('point', [0,7]);
1235          *      point3 = board.create('point', [7,7]);
1236          *      point4 = board.create('point', [-7,-5]);
1237          *
1238          *      p1Location = board.getPointLoc(point1.coords.usrCoords, margin);
1239          *      p2Location = board.getPointLoc(point2.coords.usrCoords, margin);
1240          *      p3Location = board.getPointLoc(point3.coords.usrCoords, margin);
1241          *      p4Location = board.getPointLoc(point4.coords.usrCoords, margin);
1242          *
1243          *      // Text seen on screen
1244          *      board.create('text', [1,-1, "getPointLoc(A): " + "[" + p1Location + "]"])
1245          *      board.create('text', [1,-2, "getPointLoc(B): " + "[" + p2Location + "]"])
1246          *      board.create('text', [1,-3, "getPointLoc(C): " + "[" + p3Location + "]"])
1247          *      board.create('text', [1,-4, "getPointLoc(D): " + "[" + p4Location + "]"])
1248          *
1249          *
1250          *      // Helping points that are used to create the helping lines
1251          *      helppoint1 = board.create('point', [(function (){
1252          *          var bbx = board.getBoundingBox();
1253          *          return [bbx[2] - 2, bbx[1] -2];
1254          *      })], {
1255          *          visible: false,
1256          *      })
1257          *
1258          *      helppoint2 = board.create('point', [(function (){
1259          *          var bbx = board.getBoundingBox();
1260          *          return [bbx[0] + 2, bbx[1] -2];
1261          *      })], {
1262          *          visible: false,
1263          *      })
1264          *
1265          *      helppoint3 = board.create('point', [(function (){
1266          *          var bbx = board.getBoundingBox();
1267          *          return [bbx[0]+ 2, bbx[3] + 2];
1268          *      })],{
1269          *          visible: false,
1270          *      })
1271          *
1272          *      helppoint4 = board.create('point', [(function (){
1273          *          var bbx = board.getBoundingBox();
1274          *          return [bbx[2] -2, bbx[3] + 2];
1275          *      })], {
1276          *          visible: false,
1277          *      })
1278          *
1279          *      // Helping lines to visualize the 9 sectors and the margin
1280          *      board.create('line', [helppoint1, helppoint2]);
1281          *      board.create('line', [helppoint2, helppoint3]);
1282          *      board.create('line', [helppoint3, helppoint4]);
1283          *      board.create('line', [helppoint4, helppoint1]);
1284          *  })();
1285          *
1286          * </script><pre>
1287          *
1288          */
1289         getPointLoc: function (position, margin) {
1290             var bbox, pos, res, marg;
1291 
1292             bbox = this.getBoundingBox();
1293             pos = position;
1294             if (pos.length === 2) {
1295                 pos.unshift(undefined);
1296             }
1297             res = [0, 0];
1298             marg = margin || 0;
1299             if (Type.isNumber(marg)) {
1300                 marg = [marg, marg, marg, marg];
1301             }
1302 
1303             if (pos[1] > (bbox[2] - marg[1])) {
1304                 res[0] = 1;
1305             }
1306             if (pos[1] < (bbox[0] + marg[3])) {
1307                 res[0] = -1;
1308             }
1309 
1310             if (pos[2] > (bbox[1] - marg[0])) {
1311                 res[1] = 1;
1312             }
1313             if (pos[2] < (bbox[3] + marg[2])) {
1314                 res[1] = -1;
1315             }
1316 
1317             return res;
1318         },
1319 
1320         /**
1321          * This function calculates where the origin is located (@link Board#getPointLoc).
1322          * Optional a <tt>margin</tt> to the inner of the board is respected.<br>
1323          *
1324          * @name Board#getLocationOrigin
1325          * @param {Array|Number} [margin] Optional margin for the inner of the board: <tt>[top, right, bottom, left]</tt>. A single number <tt>m</tt> is interpreted as <tt>[m, m, m, m]</tt>.
1326          * @returns {Array} [u,v] which shows where the origin is located (@link Board#getPointLoc).
1327          */
1328         getLocationOrigin: function (margin) {
1329             return this.getPointLoc([0, 0], margin);
1330         },
1331 
1332         /**
1333          * Get the position of the pointing device in screen coordinates, relative to the upper left corner
1334          * of the host tag.
1335          * @param {Event} e Event object given by the browser.
1336          * @param {Number} [i] Only use in case of touch events. This determines which finger to use and should not be set
1337          * for mouseevents.
1338          * @returns {Array} Contains the mouse coordinates in screen coordinates, ready for {@link JXG.Coords}
1339          */
1340         getMousePosition: function (e, i) {
1341             var cPos = this.getCoordsTopLeftCorner(),
1342                 absPos,
1343                 v;
1344 
1345             // Position of cursor using clientX/Y
1346             absPos = Env.getPosition(e, i, this.document);
1347 
1348             // Old:
1349             // This seems to be obsolete anyhow:
1350             // "In case there has been no down event before."
1351             // if (!Type.exists(this.cssTransMat)) {
1352             // this.updateCSSTransforms();
1353             // }
1354             // New:
1355             // We have to update the CSS transform matrix all the time,
1356             // since libraries like ZIMJS do not notify JSXGraph about a change.
1357             // In particular, sending a resize event event to JSXGraph
1358             // would be necessary.
1359             this.updateCSSTransforms();
1360 
1361             // Position relative to the top left corner
1362             v = [1, absPos[0] - cPos[0], absPos[1] - cPos[1]];
1363             v = Mat.matVecMult(this.cssTransMat, v);
1364             v[1] /= v[0];
1365             v[2] /= v[0];
1366             return [v[1], v[2]];
1367 
1368             // Method without CSS transformation
1369             /*
1370              return [absPos[0] - cPos[0], absPos[1] - cPos[1]];
1371              */
1372         },
1373 
1374         /**
1375          * Initiate moving the origin. This is used in mouseDown and touchStart listeners.
1376          * @param {Number} x Current mouse/touch coordinates
1377          * @param {Number} y Current mouse/touch coordinates
1378          */
1379         initMoveOrigin: function (x, y) {
1380             this.drag_dx = x - this.origin.scrCoords[1];
1381             this.drag_dy = y - this.origin.scrCoords[2];
1382 
1383             this.mode = this.BOARD_MODE_MOVE_ORIGIN;
1384             this._change3DView = false;
1385             this.updateQuality = this.BOARD_QUALITY_LOW;
1386         },
1387 
1388         /**
1389          * Collects all elements below the current mouse pointer and fulfilling the following constraints:
1390          * <ul>
1391          * <li>isDraggable</li>
1392          * <li>visible</li>
1393          * <li>not fixed</li>
1394          * <li>not frozen</li>
1395          * </ul>
1396          * @param {Number} x Current mouse/touch coordinates
1397          * @param {Number} y current mouse/touch coordinates
1398          * @param {Object} evt An event object
1399          * @param {String} type What type of event? 'touch', 'mouse' or 'pen'.
1400          * @returns {Array} A list of geometric elements.
1401          */
1402         initMoveObject: function (x, y, evt, type) {
1403             var pEl,
1404                 el,
1405                 collect = [],
1406                 offset = [],
1407                 haspoint,
1408                 len = this.objectsList.length,
1409                 dragEl = { visProp: { layer: -10000 } };
1410 
1411             // Store status of key presses for 3D movement
1412             this._shiftKey = evt.shiftKey;
1413             this._ctrlKey = evt.ctrlKey;
1414 
1415             //for (el in this.objects) {
1416             for (el = 0; el < len; el++) {
1417                 pEl = this.objectsList[el];
1418                 haspoint = pEl.hasPoint && pEl.hasPoint(x, y);
1419 
1420                 if (pEl.visPropCalc.visible && haspoint) {
1421                     pEl.triggerEventHandlers([type + 'down', 'down'], [evt]);
1422                     this.downObjects.push(pEl);
1423                 }
1424 
1425                 if (haspoint &&
1426                     pEl.isDraggable &&
1427                     pEl.visPropCalc.visible &&
1428                     ((this.geonextCompatibilityMode &&
1429                         (Type.isPoint(pEl) || pEl.elementClass === Const.OBJECT_CLASS_TEXT)) ||
1430                         !this.geonextCompatibilityMode) &&
1431                     !pEl.evalVisProp('fixed')
1432                     /*(!pEl.visProp.frozen) &&*/
1433                 ) {
1434                     // Elements in the highest layer get priority.
1435                     if (
1436                         pEl.visProp.layer > dragEl.visProp.layer ||
1437                         (pEl.visProp.layer === dragEl.visProp.layer &&
1438                             pEl.lastDragTime.getTime() >= dragEl.lastDragTime.getTime())
1439                     ) {
1440                         // If an element and its label have the focus
1441                         // simultaneously, the element is taken.
1442                         // This only works if we assume that every browser runs
1443                         // through this.objects in the right order, i.e. an element A
1444                         // added before element B turns up here before B does.
1445                         if (
1446                             !this.attr.ignorelabels ||
1447                             !Type.exists(dragEl.label) ||
1448                             pEl !== dragEl.label
1449                         ) {
1450                             dragEl = pEl;
1451                             collect.push(dragEl);
1452 
1453                             // Store offset for large coords elements.
1454                             if (Type.exists(dragEl.coords)) {
1455                                 if (dragEl.elementClass === Const.OBJECT_CLASS_POINT ||
1456                                     dragEl.relativeCoords    // Relative texts like labels
1457                                 ) {
1458                                     offset.push(Statistics.subtract(dragEl.coords.scrCoords.slice(1), [x, y]));
1459                                 } else {
1460                                    // Images and texts
1461                                     offset.push(Statistics.subtract(dragEl.actualCoords.scrCoords.slice(1), [x, y]));
1462                                 }
1463                             } else {
1464                                 offset.push([0, 0]);
1465                             }
1466 
1467                             // We can't drop out of this loop because of the event handling system
1468                             //if (this.attr.takefirst) {
1469                             //    return collect;
1470                             //}
1471                         }
1472                     }
1473                 }
1474             }
1475 
1476             if (this.attr.drag.enabled && collect.length > 0) {
1477                 this.mode = this.BOARD_MODE_DRAG;
1478             }
1479 
1480             // A one-element array is returned.
1481             if (this.attr.takefirst) {
1482                 collect.length = 1;
1483                 this._drag_offset = offset[0];
1484             } else {
1485                 collect = collect.slice(-1);
1486                 this._drag_offset = offset[offset.length - 1];
1487             }
1488 
1489             if (!this._drag_offset) {
1490                 this._drag_offset = [0, 0];
1491             }
1492 
1493             // Move drag element to the top of the layer
1494             if (this.renderer.type === 'svg' && Type.exists(collect[0]) &&
1495                 collect.length === 1 && Type.exists(collect[0].rendNode)
1496             ) {
1497                 // Move object to top
1498                 if (collect[0].evalVisProp('dragtotopoflayer')) {
1499                     collect[0].rendNode.parentNode.appendChild(collect[0].rendNode);
1500                 }
1501                 // Move object's label to top
1502                 if (collect[0].hasLabel &&
1503                     collect[0].label.evalVisProp('display') === 'html' &&
1504                     collect[0].label.evalVisProp('dragtotopoflayer')
1505                 ) {
1506                     collect[0].label.rendNode.parentNode.appendChild(collect[0].label.rendNode);
1507                 }
1508             }
1509 
1510             // // Init rotation angle and scale factor for two finger movements
1511             // this.previousRotation = 0.0;
1512             // this.previousScale = 1.0;
1513 
1514             if (collect.length >= 1) {
1515                 collect[0].highlight(true);
1516                 this.triggerEventHandlers(['mousehit', 'hit'], [evt, collect[0]]);
1517             }
1518 
1519             return collect;
1520         },
1521 
1522         /**
1523          * Moves an object.
1524          * @param {Number} x Coordinate
1525          * @param {Number} y Coordinate
1526          * @param {Object} o The touch object that is dragged: {JXG.Board#mouse} or {JXG.Board#touches}.
1527          * @param {Object} evt The event object.
1528          * @param {String} type Mouse or touch event?
1529          */
1530         moveObject: function (x, y, o, evt, type) {
1531             var newPos = new Coords(
1532                     Const.COORDS_BY_SCREEN,
1533                     this.getScrCoordsOfMouse(x, y),
1534                     this
1535                 ),
1536                 drag,
1537                 dragScrCoords,
1538                 newDragScrCoords;
1539 
1540             if (!(o && o.obj)) {
1541                 return;
1542             }
1543             drag = o.obj;
1544 
1545             // Avoid updates for very small movements of coordsElements, see below
1546             if (drag.coords) {
1547                 dragScrCoords = drag.coords.scrCoords.slice();
1548             }
1549 
1550             this.addLogEntry('drag', drag, newPos.usrCoords.slice(1));
1551 
1552             // Store the position and add the correctionvector from the mouse
1553             // position to the object's coords.
1554             this.drag_position = [newPos.scrCoords[1], newPos.scrCoords[2]];
1555             this.drag_position = Statistics.add(this.drag_position, this._drag_offset);
1556 
1557             // Store status of key presses for 3D movement
1558             this._shiftKey = evt.shiftKey;
1559             this._ctrlKey = evt.ctrlKey;
1560 
1561             //
1562             // We have to distinguish between CoordsElements and other elements like lines.
1563             // The latter need the difference between two move events.
1564             if (Type.exists(drag.coords)) {
1565                 drag.setPositionDirectly(Const.COORDS_BY_SCREEN, this.drag_position, [x, y]);
1566             } else {
1567                 this.displayInfobox(false);
1568                 // Hide infobox in case the user has touched an intersection point
1569                 // and drags the underlying line now.
1570 
1571                 if (!isNaN(o.targets[0].Xprev + o.targets[0].Yprev)) {
1572                     drag.setPositionDirectly(
1573                         Const.COORDS_BY_SCREEN,
1574                         [newPos.scrCoords[1], newPos.scrCoords[2]],
1575                         [o.targets[0].Xprev, o.targets[0].Yprev]
1576                     );
1577                 }
1578                 // Remember the actual position for the next move event. Then we are able to
1579                 // compute the difference vector.
1580                 o.targets[0].Xprev = newPos.scrCoords[1];
1581                 o.targets[0].Yprev = newPos.scrCoords[2];
1582             }
1583             // This may be necessary for some gliders and labels
1584             if (Type.exists(drag.coords)) {
1585                 drag.prepareUpdate().update(false).updateRenderer();
1586                 this.updateInfobox(drag);
1587                 drag.prepareUpdate().update(true).updateRenderer();
1588             }
1589 
1590             if (drag.coords) {
1591                 newDragScrCoords = drag.coords.scrCoords;
1592             }
1593             // No updates for very small movements of coordsElements
1594             if (
1595                 !drag.coords ||
1596                 dragScrCoords[1] !== newDragScrCoords[1] ||
1597                 dragScrCoords[2] !== newDragScrCoords[2]
1598             ) {
1599                 drag.triggerEventHandlers([type + 'drag', 'drag'], [evt]);
1600                 // Update all elements of the board
1601                 this.update(drag);
1602             }
1603             drag.highlight(true);
1604             this.triggerEventHandlers(['mousehit', 'hit'], [evt, drag]);
1605 
1606             drag.lastDragTime = new Date();
1607         },
1608 
1609         /**
1610          * Moves elements in multitouch mode.
1611          * @param {Array} p1 x,y coordinates of first touch
1612          * @param {Array} p2 x,y coordinates of second touch
1613          * @param {Object} o The touch object that is dragged: {JXG.Board#touches}.
1614          * @param {Object} evt The event object that lead to this movement.
1615          */
1616         twoFingerMove: function (o, id, evt) {
1617             var drag;
1618 
1619             if (Type.exists(o) && Type.exists(o.obj)) {
1620                 drag = o.obj;
1621             } else {
1622                 return;
1623             }
1624 
1625             if (
1626                 drag.elementClass === Const.OBJECT_CLASS_LINE ||
1627                 drag.type === Const.OBJECT_TYPE_POLYGON
1628             ) {
1629                 this.twoFingerTouchObject(o.targets, drag, id);
1630             } else if (drag.elementClass === Const.OBJECT_CLASS_CIRCLE) {
1631                 this.twoFingerTouchCircle(o.targets, drag, id);
1632             }
1633 
1634             if (evt) {
1635                 drag.triggerEventHandlers(['touchdrag', 'drag'], [evt]);
1636             }
1637         },
1638 
1639         /**
1640          * Compute the transformation matrix to move an element according to the
1641          * previous and actual positions of finger 1 and finger 2.
1642          * See also https://math.stackexchange.com/questions/4010538/solve-for-2d-translation-rotation-and-scale-given-two-touch-point-movements
1643          *
1644          * @param {Object} finger1 Actual and previous position of finger 1
1645          * @param {Object} finger1 Actual and previous position of finger 1
1646          * @param {Boolean} scalable Flag if element may be scaled
1647          * @param {Boolean} rotatable Flag if element may be rotated
1648          * @returns {Array}
1649          */
1650         getTwoFingerTransform(finger1, finger2, scalable, rotatable) {
1651             var crd,
1652                 x1, y1, x2, y2,
1653                 dx, dy,
1654                 xx1, yy1, xx2, yy2,
1655                 dxx, dyy,
1656                 C, S, LL, tx, ty, lbda;
1657 
1658             crd = new Coords(Const.COORDS_BY_SCREEN, [finger1.Xprev, finger1.Yprev], this).usrCoords;
1659             x1 = crd[1];
1660             y1 = crd[2];
1661             crd = new Coords(Const.COORDS_BY_SCREEN, [finger2.Xprev, finger2.Yprev], this).usrCoords;
1662             x2 = crd[1];
1663             y2 = crd[2];
1664 
1665             crd = new Coords(Const.COORDS_BY_SCREEN, [finger1.X, finger1.Y], this).usrCoords;
1666             xx1 = crd[1];
1667             yy1 = crd[2];
1668             crd = new Coords(Const.COORDS_BY_SCREEN, [finger2.X, finger2.Y], this).usrCoords;
1669             xx2 = crd[1];
1670             yy2 = crd[2];
1671 
1672             dx = x2 - x1;
1673             dy = y2 - y1;
1674             dxx = xx2 - xx1;
1675             dyy = yy2 - yy1;
1676 
1677             LL = dx * dx + dy * dy;
1678             C = (dxx * dx + dyy * dy) / LL;
1679             S = (dyy * dx - dxx * dy) / LL;
1680             if (!scalable) {
1681                 lbda = Mat.hypot(C, S);
1682                 C /= lbda;
1683                 S /= lbda;
1684             }
1685             if (!rotatable) {
1686                 S = 0;
1687             }
1688             tx = 0.5 * (xx1 + xx2 - C * (x1 + x2) + S * (y1 + y2));
1689             ty = 0.5 * (yy1 + yy2 - S * (x1 + x2) - C * (y1 + y2));
1690 
1691             return [1, 0, 0,
1692                 tx, C, -S,
1693                 ty, S, C];
1694         },
1695 
1696         /**
1697          * Moves, rotates and scales a line or polygon with two fingers.
1698          * <p>
1699          * If one vertex of the polygon snaps to the grid or to points or is not draggable,
1700          * two-finger-movement is cancelled.
1701          *
1702          * @param {Array} tar Array containing touch event objects: {JXG.Board#touches.targets}.
1703          * @param {object} drag The object that is dragged:
1704          * @param {Number} id pointerId of the event. In case of old touch event this is emulated.
1705          */
1706         twoFingerTouchObject: function (tar, drag, id) {
1707             var t, T,
1708                 ar, i, len,
1709                 snap = false;
1710 
1711             if (
1712                 Type.exists(tar[0]) &&
1713                 Type.exists(tar[1]) &&
1714                 !isNaN(tar[0].Xprev + tar[0].Yprev + tar[1].Xprev + tar[1].Yprev)
1715             ) {
1716 
1717                 T = this.getTwoFingerTransform(
1718                     tar[0], tar[1],
1719                     drag.evalVisProp('scalable'),
1720                     drag.evalVisProp('rotatable'));
1721                 t = this.create('transform', T, { type: 'generic' });
1722                 t.update();
1723 
1724                 if (drag.elementClass === Const.OBJECT_CLASS_LINE) {
1725                     ar = [];
1726                     if (drag.point1.draggable()) {
1727                         ar.push(drag.point1);
1728                     }
1729                     if (drag.point2.draggable()) {
1730                         ar.push(drag.point2);
1731                     }
1732                     t.applyOnce(ar);
1733                 } else if (drag.type === Const.OBJECT_TYPE_POLYGON) {
1734                     len = drag.vertices.length - 1;
1735                     snap = drag.evalVisProp('snaptogrid') || drag.evalVisProp('snaptopoints');
1736                     for (i = 0; i < len && !snap; ++i) {
1737                         snap = snap || drag.vertices[i].evalVisProp('snaptogrid') || drag.vertices[i].evalVisProp('snaptopoints');
1738                         snap = snap || (!drag.vertices[i].draggable());
1739                     }
1740                     if (!snap) {
1741                         ar = [];
1742                         for (i = 0; i < len; ++i) {
1743                             if (drag.vertices[i].draggable()) {
1744                                 ar.push(drag.vertices[i]);
1745                             }
1746                         }
1747                         t.applyOnce(ar);
1748                     }
1749                 }
1750 
1751                 this.update();
1752                 drag.highlight(true);
1753             }
1754         },
1755 
1756         /*
1757          * Moves, rotates and scales a circle with two fingers.
1758          * @param {Array} tar Array containing touch event objects: {JXG.Board#touches.targets}.
1759          * @param {object} drag The object that is dragged:
1760          * @param {Number} id pointerId of the event. In case of old touch event this is emulated.
1761          */
1762         twoFingerTouchCircle: function (tar, drag, id) {
1763             var fixEl, moveEl, np, op, fix, d, alpha, t1, t2, t3, t4;
1764 
1765             if (drag.method === 'pointCircle' || drag.method === 'pointLine') {
1766                 return;
1767             }
1768 
1769             if (
1770                 Type.exists(tar[0]) &&
1771                 Type.exists(tar[1]) &&
1772                 !isNaN(tar[0].Xprev + tar[0].Yprev + tar[1].Xprev + tar[1].Yprev)
1773             ) {
1774                 if (id === tar[0].num) {
1775                     fixEl = tar[1];
1776                     moveEl = tar[0];
1777                 } else {
1778                     fixEl = tar[0];
1779                     moveEl = tar[1];
1780                 }
1781 
1782                 fix = new Coords(Const.COORDS_BY_SCREEN, [fixEl.Xprev, fixEl.Yprev], this)
1783                     .usrCoords;
1784                 // Previous finger position
1785                 op = new Coords(Const.COORDS_BY_SCREEN, [moveEl.Xprev, moveEl.Yprev], this)
1786                     .usrCoords;
1787                 // New finger position
1788                 np = new Coords(Const.COORDS_BY_SCREEN, [moveEl.X, moveEl.Y], this).usrCoords;
1789 
1790                 alpha = Geometry.rad(op.slice(1), fix.slice(1), np.slice(1));
1791 
1792                 // Rotate and scale by the movement of the second finger
1793                 t1 = this.create('transform', [-fix[1], -fix[2]], {
1794                     type: 'translate'
1795                 });
1796                 t2 = this.create('transform', [alpha], { type: 'rotate' });
1797                 t1.melt(t2);
1798                 if (drag.evalVisProp('scalable')) {
1799                     d = Geometry.distance(fix, np) / Geometry.distance(fix, op);
1800                     t3 = this.create('transform', [d, d], { type: 'scale' });
1801                     t1.melt(t3);
1802                 }
1803                 t4 = this.create('transform', [fix[1], fix[2]], {
1804                     type: 'translate'
1805                 });
1806                 t1.melt(t4);
1807 
1808                 if (drag.center.draggable()) {
1809                     t1.applyOnce([drag.center]);
1810                 }
1811 
1812                 if (drag.method === 'twoPoints') {
1813                     if (drag.point2.draggable()) {
1814                         t1.applyOnce([drag.point2]);
1815                     }
1816                 } else if (drag.method === 'pointRadius') {
1817                     if (Type.isNumber(drag.updateRadius.origin)) {
1818                         drag.setRadius(drag.radius * d);
1819                     }
1820                 }
1821 
1822                 this.update(drag.center);
1823                 drag.highlight(true);
1824             }
1825         },
1826 
1827         highlightElements: function (x, y, evt, target) {
1828             var el,
1829                 pEl,
1830                 pId,
1831                 overObjects = {},
1832                 len = this.objectsList.length;
1833 
1834             // Elements  below the mouse pointer which are not highlighted yet will be highlighted.
1835             for (el = 0; el < len; el++) {
1836                 pEl = this.objectsList[el];
1837                 pId = pEl.id;
1838                 if (
1839                     Type.exists(pEl.hasPoint) &&
1840                     pEl.visPropCalc.visible &&
1841                     pEl.hasPoint(x, y)
1842                 ) {
1843                     // this is required in any case because otherwise the box won't be shown until the point is dragged
1844                     this.updateInfobox(pEl);
1845 
1846                     if (!Type.exists(this.highlightedObjects[pId])) {
1847                         // highlight only if not highlighted
1848                         overObjects[pId] = pEl;
1849                         pEl.highlight();
1850                         // triggers board event.
1851                         this.triggerEventHandlers(['mousehit', 'hit'], [evt, pEl, target]);
1852                     }
1853 
1854                     if (pEl.mouseover) {
1855                         pEl.triggerEventHandlers(['mousemove', 'move'], [evt]);
1856                     } else {
1857                         pEl.triggerEventHandlers(['mouseover', 'over'], [evt]);
1858                         pEl.mouseover = true;
1859                     }
1860                 }
1861             }
1862 
1863             for (el = 0; el < len; el++) {
1864                 pEl = this.objectsList[el];
1865                 pId = pEl.id;
1866                 if (pEl.mouseover) {
1867                     if (!overObjects[pId]) {
1868                         pEl.triggerEventHandlers(['mouseout', 'out'], [evt]);
1869                         pEl.mouseover = false;
1870                     }
1871                 }
1872             }
1873         },
1874 
1875         /**
1876          * Helper function which returns a reasonable starting point for the object being dragged.
1877          * Formerly known as initXYstart().
1878          * @private
1879          * @param {JXG.GeometryElement} obj The object to be dragged
1880          * @param {Array} targets Array of targets. It is changed by this function.
1881          */
1882         saveStartPos: function (obj, targets) {
1883             var xy = [],
1884                 i,
1885                 len;
1886 
1887             if (obj.type === Const.OBJECT_TYPE_TICKS) {
1888                 xy.push([1, NaN, NaN]);
1889             } else if (obj.elementClass === Const.OBJECT_CLASS_LINE) {
1890                 xy.push(obj.point1.coords.usrCoords);
1891                 xy.push(obj.point2.coords.usrCoords);
1892             } else if (obj.elementClass === Const.OBJECT_CLASS_CIRCLE) {
1893                 xy.push(obj.center.coords.usrCoords);
1894                 if (obj.method === 'twoPoints') {
1895                     xy.push(obj.point2.coords.usrCoords);
1896                 }
1897             } else if (obj.type === Const.OBJECT_TYPE_POLYGON) {
1898                 len = obj.vertices.length - 1;
1899                 for (i = 0; i < len; i++) {
1900                     xy.push(obj.vertices[i].coords.usrCoords);
1901                 }
1902             } else if (obj.type === Const.OBJECT_TYPE_SECTOR) {
1903                 xy.push(obj.point1.coords.usrCoords);
1904                 xy.push(obj.point2.coords.usrCoords);
1905                 xy.push(obj.point3.coords.usrCoords);
1906             } else if (Type.isPoint(obj) || obj.type === Const.OBJECT_TYPE_GLIDER) {
1907                 xy.push(obj.coords.usrCoords);
1908             } else if (obj.elementClass === Const.OBJECT_CLASS_CURVE) {
1909                 // if (Type.exists(obj.parents)) {
1910                 //     len = obj.parents.length;
1911                 //     if (len > 0) {
1912                 //         for (i = 0; i < len; i++) {
1913                 //             xy.push(this.select(obj.parents[i]).coords.usrCoords);
1914                 //         }
1915                 //     } else
1916                 // }
1917                 if (obj.points.length > 0) {
1918                     xy.push(obj.points[0].usrCoords);
1919                 }
1920             } else {
1921                 try {
1922                     xy.push(obj.coords.usrCoords);
1923                 } catch (e) {
1924                     JXG.debug(
1925                         'JSXGraph+ saveStartPos: obj.coords.usrCoords not available: ' + e
1926                     );
1927                 }
1928             }
1929 
1930             len = xy.length;
1931             for (i = 0; i < len; i++) {
1932                 targets.Zstart.push(xy[i][0]);
1933                 targets.Xstart.push(xy[i][1]);
1934                 targets.Ystart.push(xy[i][2]);
1935             }
1936         },
1937 
1938         mouseOriginMoveStart: function (evt) {
1939             var r, pos;
1940 
1941             r = this._isRequiredKeyPressed(evt, 'pan');
1942             if (r) {
1943                 pos = this.getMousePosition(evt);
1944                 this.initMoveOrigin(pos[0], pos[1]);
1945             }
1946 
1947             return r;
1948         },
1949 
1950         mouseOriginMove: function (evt) {
1951             var r = this.mode === this.BOARD_MODE_MOVE_ORIGIN,
1952                 pos;
1953 
1954             if (r) {
1955                 pos = this.getMousePosition(evt);
1956                 this.moveOrigin(pos[0], pos[1], true);
1957             }
1958 
1959             return r;
1960         },
1961 
1962         /**
1963          * Start moving the origin with one finger.
1964          * @private
1965          * @param  {Object} evt Event from touchStartListener
1966          * @return {Boolean}   returns if the origin is moved.
1967          */
1968         touchStartMoveOriginOneFinger: function (evt) {
1969             var touches = evt['touches'],
1970                 conditions,
1971                 pos;
1972 
1973             conditions =
1974                 this.attr.pan.enabled && !this.attr.pan.needtwofingers && touches.length === 1;
1975 
1976             if (conditions) {
1977                 pos = this.getMousePosition(evt, 0);
1978                 this.initMoveOrigin(pos[0], pos[1]);
1979             }
1980 
1981             return conditions;
1982         },
1983 
1984         /**
1985          * Move the origin with one finger
1986          * @private
1987          * @param  {Object} evt Event from touchMoveListener
1988          * @return {Boolean}     returns if the origin is moved.
1989          */
1990         touchOriginMove: function (evt) {
1991             var r = this.mode === this.BOARD_MODE_MOVE_ORIGIN,
1992                 pos;
1993 
1994             if (r) {
1995                 pos = this.getMousePosition(evt, 0);
1996                 this.moveOrigin(pos[0], pos[1], true);
1997             }
1998 
1999             return r;
2000         },
2001 
2002         /**
2003          * Stop moving the origin with one finger
2004          * @return {null} null
2005          * @private
2006          */
2007         originMoveEnd: function () {
2008             this.updateQuality = this.BOARD_QUALITY_HIGH;
2009             this.mode = this.BOARD_MODE_NONE;
2010         },
2011 
2012         /**********************************************************
2013          *
2014          * Event Handler
2015          *
2016          **********************************************************/
2017 
2018         /**
2019          * Suppresses the default event handling.
2020          * Used for context menu.
2021          *
2022          * @param {Event} e
2023          * @returns {Boolean} false
2024          */
2025         suppressDefault: function (e) {
2026             if (Type.exists(e)) {
2027                 e.preventDefault();
2028             }
2029             return false;
2030         },
2031 
2032         /**
2033          * Add all possible event handlers to the board object
2034          * that move objects, i.e. mouse, pointer and touch events.
2035          */
2036         addEventHandlers: function () {
2037             if (Env.supportsPointerEvents()) {
2038                 this.addPointerEventHandlers();
2039             } else {
2040                 this.addMouseEventHandlers();
2041                 this.addTouchEventHandlers();
2042             }
2043 
2044             if (this.containerObj !== null) {
2045                 // this.containerObj.oncontextmenu = this.suppressDefault;
2046                 Env.addEvent(this.containerObj, 'contextmenu', this.suppressDefault, this);
2047             }
2048 
2049             // This one produces errors on IE
2050             // // Env.addEvent(this.containerObj, 'contextmenu', function (e) { e.preventDefault(); return false;}, this);
2051             // This one works on IE, Firefox and Chromium with default configurations. On some Safari
2052             // or Opera versions the user must explicitly allow the deactivation of the context menu.
2053         },
2054 
2055         /**
2056          * Remove all event handlers from the board object
2057          */
2058         removeEventHandlers: function () {
2059             if ((this.hasPointerHandlers || this.hasMouseHandlers || this.hasTouchHandlers) &&
2060                 this.containerObj !== null
2061             ) {
2062                 Env.removeEvent(this.containerObj, 'contextmenu', this.suppressDefault, this);
2063             }
2064 
2065             this.removeMouseEventHandlers();
2066             this.removeTouchEventHandlers();
2067             this.removePointerEventHandlers();
2068 
2069             this.removeFullscreenEventHandlers();
2070             this.removeKeyboardEventHandlers();
2071             this.removeResizeEventHandlers();
2072 
2073             // if (Env.isBrowser) {
2074             //     if (Type.exists(this.resizeObserver)) {
2075             //         this.stopResizeObserver();
2076             //     } else {
2077             //         Env.removeEvent(window, 'resize', this.resizeListener, this);
2078             //         this.stopIntersectionObserver();
2079             //     }
2080             //     Env.removeEvent(window, 'scroll', this.scrollListener, this);
2081             // }
2082         },
2083 
2084         /**
2085          * Add resize related event handlers
2086          *
2087          */
2088         addResizeEventHandlers: function () {
2089             // var that = this;
2090 
2091             this.resizeHandlers = [];
2092             if (Env.isBrowser) {
2093                 try {
2094                     // Supported by all new browsers
2095                     // resizeObserver: triggered if size of the JSXGraph div changes.
2096                     this.startResizeObserver();
2097                     this.resizeHandlers.push('resizeobserver');
2098                 } catch (err) {
2099                     // Certain Safari and edge version do not support
2100                     // resizeObserver, but intersectionObserver.
2101                     // resize event: triggered if size of window changes
2102                     Env.addEvent(window, 'resize', this.resizeListener, this);
2103                     // intersectionObserver: triggered if JSXGraph becomes visible.
2104                     this.startIntersectionObserver();
2105                     this.resizeHandlers.push('resize');
2106                 }
2107                 // Scroll event: needs to be captured since on mobile devices
2108                 // sometimes a header bar is displayed / hidden, which triggers a
2109                 // resize event.
2110                 Env.addEvent(window, 'scroll', this.scrollListener, this);
2111                 this.resizeHandlers.push('scroll');
2112 
2113                 // On browser print:
2114                 // we need to call the listener when having @media: print.
2115                 try {
2116                     // window.matchMedia('print').addEventListener('change', this.printListenerMatch.apply(this, arguments));
2117                     window.matchMedia('print').addEventListener('change', this.printListenerMatch.bind(this));
2118                     window.matchMedia('screen').addEventListener('change', this.printListenerMatch.bind(this));
2119                     this.resizeHandlers.push('print');
2120                 } catch (err) {
2121                     JXG.debug("Error adding printListener", err);
2122                 }
2123                 // if (Type.isFunction(MediaQueryList.prototype.addEventListener)) {
2124                 //     window.matchMedia('print').addEventListener('change', function (mql) {
2125                 //         if (mql.matches) {
2126                 //             that.printListener();
2127                 //         }
2128                 //     });
2129                 // } else if (Type.isFunction(MediaQueryList.prototype.addListener)) { // addListener might be deprecated
2130                 //     window.matchMedia('print').addListener(function (mql, ev) {
2131                 //         if (mql.matches) {
2132                 //             that.printListener(ev);
2133                 //         }
2134                 //     });
2135                 // }
2136 
2137                 // When closing the print dialog we again have to resize.
2138                 // Env.addEvent(window, 'afterprint', this.printListener, this);
2139                 // this.resizeHandlers.push('afterprint');
2140             }
2141         },
2142 
2143         /**
2144          * Remove resize related event handlers
2145          *
2146          */
2147         removeResizeEventHandlers: function () {
2148             var i, e;
2149             if (this.resizeHandlers.length > 0 && Env.isBrowser) {
2150                 for (i = 0; i < this.resizeHandlers.length; i++) {
2151                     e = this.resizeHandlers[i];
2152                     switch (e) {
2153                         case 'resizeobserver':
2154                             if (Type.exists(this.resizeObserver)) {
2155                                 this.stopResizeObserver();
2156                             }
2157                             break;
2158                         case 'resize':
2159                             Env.removeEvent(window, 'resize', this.resizeListener, this);
2160                             if (Type.exists(this.intersectionObserver)) {
2161                                 this.stopIntersectionObserver();
2162                             }
2163                             break;
2164                         case 'scroll':
2165                             Env.removeEvent(window, 'scroll', this.scrollListener, this);
2166                             break;
2167                         case 'print':
2168                             window.matchMedia('print').removeEventListener('change', this.printListenerMatch.bind(this), false);
2169                             window.matchMedia('screen').removeEventListener('change', this.printListenerMatch.bind(this), false);
2170                             break;
2171                         // case 'afterprint':
2172                         //     Env.removeEvent(window, 'afterprint', this.printListener, this);
2173                         //     break;
2174                     }
2175                 }
2176                 this.resizeHandlers = [];
2177             }
2178         },
2179 
2180 
2181         /**
2182          * Registers pointer event handlers.
2183          */
2184         addPointerEventHandlers: function () {
2185             if (!this.hasPointerHandlers && Env.isBrowser) {
2186                 var moveTarget = this.attr.movetarget || this.containerObj;
2187 
2188                 if (window.navigator.msPointerEnabled) {
2189                     // IE10-
2190                     // Env.addEvent(this.containerObj, 'MSPointerDown', this.pointerDownListener, this);
2191                     Env.addEvent(moveTarget, 'MSPointerDown', this.pointerDownListener, this);
2192                     Env.addEvent(moveTarget, 'MSPointerMove', this.pointerMoveListener, this);
2193                 } else {
2194                     // Env.addEvent(this.containerObj, 'pointerdown', this.pointerDownListener, this);
2195                     Env.addEvent(moveTarget, 'pointerdown', this.pointerDownListener, this);
2196                     Env.addEvent(moveTarget, 'pointermove', this.pointerMoveListener, this);
2197                     Env.addEvent(moveTarget, 'pointerleave', this.pointerLeaveListener, this);
2198                     Env.addEvent(moveTarget, 'click', this.pointerClickListener, this);
2199                     Env.addEvent(moveTarget, 'dblclick', this.pointerDblClickListener, this);
2200                 }
2201 
2202                 if (this.containerObj !== null) {
2203                     // This is needed for capturing touch events.
2204                     // It is in jsxgraph.css, for ms-touch-action...
2205                     this.containerObj.style.touchAction = 'none';
2206                     // this.containerObj.style.touchAction = 'auto';
2207                 }
2208 
2209                 this.hasPointerHandlers = true;
2210             }
2211         },
2212 
2213         /**
2214          * Registers mouse move, down and wheel event handlers.
2215          */
2216         addMouseEventHandlers: function () {
2217             if (!this.hasMouseHandlers && Env.isBrowser) {
2218                 var moveTarget = this.attr.movetarget || this.containerObj;
2219 
2220                 // Env.addEvent(this.containerObj, 'mousedown', this.mouseDownListener, this);
2221                 Env.addEvent(moveTarget, 'mousedown', this.mouseDownListener, this);
2222                 Env.addEvent(moveTarget, 'mousemove', this.mouseMoveListener, this);
2223                 Env.addEvent(moveTarget, 'click', this.mouseClickListener, this);
2224                 Env.addEvent(moveTarget, 'dblclick', this.mouseDblClickListener, this);
2225 
2226                 this.hasMouseHandlers = true;
2227             }
2228         },
2229 
2230         /**
2231          * Register touch start and move and gesture start and change event handlers.
2232          * @param {Boolean} appleGestures If set to false the gesturestart and gesturechange event handlers
2233          * will not be registered.
2234          *
2235          * Since iOS 13, touch events were abandoned in favour of pointer events
2236          */
2237         addTouchEventHandlers: function (appleGestures) {
2238             if (!this.hasTouchHandlers && Env.isBrowser) {
2239                 var moveTarget = this.attr.movetarget || this.containerObj;
2240 
2241                 // Env.addEvent(this.containerObj, 'touchstart', this.touchStartListener, this);
2242                 Env.addEvent(moveTarget, 'touchstart', this.touchStartListener, this);
2243                 Env.addEvent(moveTarget, 'touchmove', this.touchMoveListener, this);
2244 
2245                 /*
2246                 if (!Type.exists(appleGestures) || appleGestures) {
2247                     // Gesture listener are called in touchStart and touchMove.
2248                     //Env.addEvent(this.containerObj, 'gesturestart', this.gestureStartListener, this);
2249                     //Env.addEvent(this.containerObj, 'gesturechange', this.gestureChangeListener, this);
2250                 }
2251                 */
2252 
2253                 this.hasTouchHandlers = true;
2254             }
2255         },
2256 
2257         /**
2258          * Registers pointer event handlers.
2259          */
2260         addWheelEventHandlers: function () {
2261             if (!this.hasWheelHandlers && Env.isBrowser) {
2262                 Env.addEvent(this.containerObj, 'mousewheel', this.mouseWheelListener, this);
2263                 Env.addEvent(this.containerObj, 'DOMMouseScroll', this.mouseWheelListener, this);
2264                 this.hasWheelHandlers = true;
2265             }
2266         },
2267 
2268         /**
2269          * Add fullscreen events which update the CSS transformation matrix to correct
2270          * the mouse/touch/pointer positions in case of CSS transformations.
2271          */
2272         addFullscreenEventHandlers: function () {
2273             var i,
2274                 // standard/Edge, firefox, chrome/safari, IE11
2275                 events = [
2276                     'fullscreenchange',
2277                     'mozfullscreenchange',
2278                     'webkitfullscreenchange',
2279                     'msfullscreenchange'
2280                 ],
2281                 le = events.length;
2282 
2283             if (!this.hasFullscreenEventHandlers && Env.isBrowser) {
2284                 for (i = 0; i < le; i++) {
2285                     Env.addEvent(this.document, events[i], this.fullscreenListener, this);
2286                 }
2287                 this.hasFullscreenEventHandlers = true;
2288             }
2289         },
2290 
2291         /**
2292          * Register keyboard event handlers.
2293          */
2294         addKeyboardEventHandlers: function () {
2295             if (this.attr.keyboard.enabled && !this.hasKeyboardHandlers && Env.isBrowser) {
2296                 Env.addEvent(this.containerObj, 'keydown', this.keyDownListener, this);
2297                 Env.addEvent(this.containerObj, 'focusin', this.keyFocusInListener, this);
2298                 Env.addEvent(this.containerObj, 'focusout', this.keyFocusOutListener, this);
2299                 this.hasKeyboardHandlers = true;
2300             }
2301         },
2302 
2303         /**
2304          * Remove all registered touch event handlers.
2305          */
2306         removeKeyboardEventHandlers: function () {
2307             if (this.hasKeyboardHandlers && Env.isBrowser) {
2308                 Env.removeEvent(this.containerObj, 'keydown', this.keyDownListener, this);
2309                 Env.removeEvent(this.containerObj, 'focusin', this.keyFocusInListener, this);
2310                 Env.removeEvent(this.containerObj, 'focusout', this.keyFocusOutListener, this);
2311                 this.hasKeyboardHandlers = false;
2312             }
2313         },
2314 
2315         /**
2316          * Remove all registered event handlers regarding fullscreen mode.
2317          */
2318         removeFullscreenEventHandlers: function () {
2319             var i,
2320                 // standard/Edge, firefox, chrome/safari, IE11
2321                 events = [
2322                     'fullscreenchange',
2323                     'mozfullscreenchange',
2324                     'webkitfullscreenchange',
2325                     'msfullscreenchange'
2326                 ],
2327                 le = events.length;
2328 
2329             if (this.hasFullscreenEventHandlers && Env.isBrowser) {
2330                 for (i = 0; i < le; i++) {
2331                     Env.removeEvent(this.document, events[i], this.fullscreenListener, this);
2332                 }
2333                 this.hasFullscreenEventHandlers = false;
2334             }
2335         },
2336 
2337         /**
2338          * Remove MSPointer* Event handlers.
2339          */
2340         removePointerEventHandlers: function () {
2341             if (this.hasPointerHandlers && Env.isBrowser) {
2342                 var moveTarget = this.attr.movetarget || this.containerObj;
2343 
2344                 if (window.navigator.msPointerEnabled) {
2345                     // IE10-
2346                     // Env.removeEvent(this.containerObj, 'MSPointerDown', this.pointerDownListener, this);
2347                     Env.removeEvent(moveTarget, 'MSPointerDown', this.pointerDownListener, this);
2348                     Env.removeEvent(moveTarget, 'MSPointerMove', this.pointerMoveListener, this);
2349                 } else {
2350                     // Env.removeEvent(this.containerObj, 'pointerdown', this.pointerDownListener, this);
2351                     Env.removeEvent(moveTarget, 'pointerdown', this.pointerDownListener, this);
2352                     Env.removeEvent(moveTarget, 'pointermove', this.pointerMoveListener, this);
2353                     Env.removeEvent(moveTarget, 'pointerleave', this.pointerLeaveListener, this);
2354                     Env.removeEvent(moveTarget, 'click', this.pointerClickListener, this);
2355                     Env.removeEvent(moveTarget, 'dblclick', this.pointerDblClickListener, this);
2356                 }
2357 
2358                 if (this.hasWheelHandlers) {
2359                     Env.removeEvent(this.containerObj, 'mousewheel', this.mouseWheelListener, this);
2360                     Env.removeEvent(this.containerObj, 'DOMMouseScroll', this.mouseWheelListener, this);
2361                 }
2362 
2363                 if (this.hasPointerUp) {
2364                     if (window.navigator.msPointerEnabled) {
2365                         // IE10-
2366                         Env.removeEvent(this.document, 'MSPointerUp', this.pointerUpListener, this);
2367                     } else {
2368                         Env.removeEvent(this.document, 'pointerup', this.pointerUpListener, this);
2369                         Env.removeEvent(this.document, 'pointercancel', this.pointerUpListener, this);
2370                     }
2371                     this.hasPointerUp = false;
2372                 }
2373 
2374                 this.hasPointerHandlers = false;
2375             }
2376         },
2377 
2378         /**
2379          * De-register mouse event handlers.
2380          */
2381         removeMouseEventHandlers: function () {
2382             if (this.hasMouseHandlers && Env.isBrowser) {
2383                 var moveTarget = this.attr.movetarget || this.containerObj;
2384 
2385                 // Env.removeEvent(this.containerObj, 'mousedown', this.mouseDownListener, this);
2386                 Env.removeEvent(moveTarget, 'mousedown', this.mouseDownListener, this);
2387                 Env.removeEvent(moveTarget, 'mousemove', this.mouseMoveListener, this);
2388                 Env.removeEvent(moveTarget, 'click', this.mouseClickListener, this);
2389                 Env.removeEvent(moveTarget, 'dblclick', this.mouseDblClickListener, this);
2390 
2391                 if (this.hasMouseUp) {
2392                     Env.removeEvent(this.document, 'mouseup', this.mouseUpListener, this);
2393                     this.hasMouseUp = false;
2394                 }
2395 
2396                 if (this.hasWheelHandlers) {
2397                     Env.removeEvent(this.containerObj, 'mousewheel', this.mouseWheelListener, this);
2398                     Env.removeEvent(
2399                         this.containerObj,
2400                         'DOMMouseScroll',
2401                         this.mouseWheelListener,
2402                         this
2403                     );
2404                 }
2405 
2406                 this.hasMouseHandlers = false;
2407             }
2408         },
2409 
2410         /**
2411          * Remove all registered touch event handlers.
2412          */
2413         removeTouchEventHandlers: function () {
2414             if (this.hasTouchHandlers && Env.isBrowser) {
2415                 var moveTarget = this.attr.movetarget || this.containerObj;
2416 
2417                 // Env.removeEvent(this.containerObj, 'touchstart', this.touchStartListener, this);
2418                 Env.removeEvent(moveTarget, 'touchstart', this.touchStartListener, this);
2419                 Env.removeEvent(moveTarget, 'touchmove', this.touchMoveListener, this);
2420 
2421                 if (this.hasTouchEnd) {
2422                     Env.removeEvent(this.document, 'touchend', this.touchEndListener, this);
2423                     this.hasTouchEnd = false;
2424                 }
2425 
2426                 this.hasTouchHandlers = false;
2427             }
2428         },
2429 
2430         /**
2431          * Handler for click on left arrow in the navigation bar
2432          * @returns {JXG.Board} Reference to the board
2433          */
2434         clickLeftArrow: function () {
2435             this.moveOrigin(
2436                 this.origin.scrCoords[1] + this.canvasWidth * 0.1,
2437                 this.origin.scrCoords[2]
2438             );
2439             return this;
2440         },
2441 
2442         /**
2443          * Handler for click on right arrow in the navigation bar
2444          * @returns {JXG.Board} Reference to the board
2445          */
2446         clickRightArrow: function () {
2447             this.moveOrigin(
2448                 this.origin.scrCoords[1] - this.canvasWidth * 0.1,
2449                 this.origin.scrCoords[2]
2450             );
2451             return this;
2452         },
2453 
2454         /**
2455          * Handler for click on up arrow in the navigation bar
2456          * @returns {JXG.Board} Reference to the board
2457          */
2458         clickUpArrow: function () {
2459             this.moveOrigin(
2460                 this.origin.scrCoords[1],
2461                 this.origin.scrCoords[2] - this.canvasHeight * 0.1
2462             );
2463             return this;
2464         },
2465 
2466         /**
2467          * Handler for click on down arrow in the navigation bar
2468          * @returns {JXG.Board} Reference to the board
2469          */
2470         clickDownArrow: function () {
2471             this.moveOrigin(
2472                 this.origin.scrCoords[1],
2473                 this.origin.scrCoords[2] + this.canvasHeight * 0.1
2474             );
2475             return this;
2476         },
2477 
2478         /**
2479          * Triggered on iOS/Safari while the user inputs a gesture (e.g. pinch) and is used to zoom into the board.
2480          * Works on iOS/Safari and Android.
2481          * @param {Event} evt Browser event object
2482          * @returns {Boolean}
2483          */
2484         gestureChangeListener: function (evt) {
2485             var c,
2486                 dir1 = [],
2487                 dir2 = [],
2488                 angle,
2489                 mi = 10,
2490                 isPinch = false,
2491                 // Save zoomFactors
2492                 zx = this.attr.zoom.factorx,
2493                 zy = this.attr.zoom.factory,
2494                 factor, dist, theta, bound,
2495                 zoomCenter,
2496                 doZoom = false,
2497                 dx, dy, cx, cy;
2498 
2499             if (this.mode !== this.BOARD_MODE_ZOOM) {
2500                 return true;
2501             }
2502             evt.preventDefault();
2503 
2504             dist = Geometry.distance(
2505                 [evt.touches[0].clientX, evt.touches[0].clientY],
2506                 [evt.touches[1].clientX, evt.touches[1].clientY],
2507                 2
2508             );
2509 
2510             // Android pinch to zoom
2511             // evt.scale was available in iOS touch events (pre iOS 13)
2512             // evt.scale is undefined in Android
2513             if (evt.scale === undefined) {
2514                 evt.scale = dist / this.prevDist;
2515             }
2516 
2517             if (!Type.exists(this.prevCoords)) {
2518                 return false;
2519             }
2520             // Compute the angle of the two finger directions
2521             dir1 = [
2522                 evt.touches[0].clientX - this.prevCoords[0][0],
2523                 evt.touches[0].clientY - this.prevCoords[0][1]
2524             ];
2525             dir2 = [
2526                 evt.touches[1].clientX - this.prevCoords[1][0],
2527                 evt.touches[1].clientY - this.prevCoords[1][1]
2528             ];
2529 
2530             if (
2531                 dir1[0] * dir1[0] + dir1[1] * dir1[1] < mi * mi &&
2532                 dir2[0] * dir2[0] + dir2[1] * dir2[1] < mi * mi
2533             ) {
2534                 return false;
2535             }
2536 
2537             angle = Geometry.rad(dir1, [0, 0], dir2);
2538             if (
2539                 this.isPreviousGesture !== 'pan' &&
2540                 Math.abs(angle) > Math.PI * 0.2 &&
2541                 Math.abs(angle) < Math.PI * 1.8
2542             ) {
2543                 isPinch = true;
2544             }
2545 
2546             if (this.isPreviousGesture !== 'pan' && !isPinch) {
2547                 if (Math.abs(evt.scale) < 0.77 || Math.abs(evt.scale) > 1.3) {
2548                     isPinch = true;
2549                 }
2550             }
2551 
2552             factor = evt.scale / this.prevScale;
2553             this.prevScale = evt.scale;
2554             this.prevCoords = [
2555                 [evt.touches[0].clientX, evt.touches[0].clientY],
2556                 [evt.touches[1].clientX, evt.touches[1].clientY]
2557             ];
2558 
2559             c = new Coords(Const.COORDS_BY_SCREEN, this.getMousePosition(evt, 0), this);
2560 
2561             if (this.attr.pan.enabled && this.attr.pan.needtwofingers && !isPinch) {
2562                 // Pan detected
2563                 this.isPreviousGesture = 'pan';
2564                 this.moveOrigin(c.scrCoords[1], c.scrCoords[2], true);
2565 
2566             } else if (this.attr.zoom.enabled && Math.abs(factor - 1.0) < 0.5) {
2567                 doZoom = false;
2568                 zoomCenter = this.attr.zoom.center;
2569                 // Pinch detected
2570                 if (this.attr.zoom.pinchhorizontal || this.attr.zoom.pinchvertical) {
2571                     dx = Math.abs(evt.touches[0].clientX - evt.touches[1].clientX);
2572                     dy = Math.abs(evt.touches[0].clientY - evt.touches[1].clientY);
2573                     theta = Math.abs(Math.atan2(dy, dx));
2574                     bound = (Math.PI * this.attr.zoom.pinchsensitivity) / 90.0;
2575                 }
2576 
2577                 if (!this.keepaspectratio &&
2578                     this.attr.zoom.pinchhorizontal &&
2579                     theta < bound) {
2580                     this.attr.zoom.factorx = factor;
2581                     this.attr.zoom.factory = 1.0;
2582                     cx = 0;
2583                     cy = 0;
2584                     doZoom = true;
2585                 } else if (!this.keepaspectratio &&
2586                     this.attr.zoom.pinchvertical &&
2587                     Math.abs(theta - Math.PI * 0.5) < bound
2588                 ) {
2589                     this.attr.zoom.factorx = 1.0;
2590                     this.attr.zoom.factory = factor;
2591                     cx = 0;
2592                     cy = 0;
2593                     doZoom = true;
2594                 } else if (this.attr.zoom.pinch) {
2595                     this.attr.zoom.factorx = factor;
2596                     this.attr.zoom.factory = factor;
2597                     cx = c.usrCoords[1];
2598                     cy = c.usrCoords[2];
2599                     doZoom = true;
2600                 }
2601 
2602                 if (doZoom) {
2603                     if (zoomCenter === 'board') {
2604                         this.zoomIn();
2605                     } else { // including zoomCenter === 'auto'
2606                         this.zoomIn(cx, cy);
2607                     }
2608 
2609                     // Restore zoomFactors
2610                     this.attr.zoom.factorx = zx;
2611                     this.attr.zoom.factory = zy;
2612                 }
2613             }
2614 
2615             return false;
2616         },
2617 
2618         /**
2619          * Called by iOS/Safari as soon as the user starts a gesture. Works natively on iOS/Safari,
2620          * on Android we emulate it.
2621          * @param {Event} evt
2622          * @returns {Boolean}
2623          */
2624         gestureStartListener: function (evt) {
2625             var pos;
2626 
2627             evt.preventDefault();
2628             this.prevScale = 1.0;
2629             // Android pinch to zoom
2630             this.prevDist = Geometry.distance(
2631                 [evt.touches[0].clientX, evt.touches[0].clientY],
2632                 [evt.touches[1].clientX, evt.touches[1].clientY],
2633                 2
2634             );
2635             this.prevCoords = [
2636                 [evt.touches[0].clientX, evt.touches[0].clientY],
2637                 [evt.touches[1].clientX, evt.touches[1].clientY]
2638             ];
2639             this.isPreviousGesture = 'none';
2640 
2641             // If pinch-to-zoom is interpreted as panning
2642             // we have to prepare move origin
2643             pos = this.getMousePosition(evt, 0);
2644             this.initMoveOrigin(pos[0], pos[1]);
2645 
2646             this.mode = this.BOARD_MODE_ZOOM;
2647             this._change3DView = false;
2648             return false;
2649         },
2650 
2651         /**
2652          * Test if the required key combination is pressed for wheel zoom, move origin and
2653          * selection
2654          * @private
2655          * @param  {Object}  evt    Mouse or pen event
2656          * @param  {String}  action String containing the action: 'zoom', 'pan', 'selection'.
2657          * Corresponds to the attribute subobject.
2658          * @return {Boolean}        true or false.
2659          */
2660         _isRequiredKeyPressed: function (evt, action) {
2661             var obj = this.attr[action];
2662             if (!obj.enabled) {
2663                 return false;
2664             }
2665 
2666             if (
2667                 ((obj.needshift && evt.shiftKey) || (!obj.needshift && !evt.shiftKey)) &&
2668                 ((obj.needctrl && evt.ctrlKey) || (!obj.needctrl && !evt.ctrlKey))
2669             ) {
2670                 return true;
2671             }
2672 
2673             return false;
2674         },
2675 
2676         /*
2677          * Pointer events
2678          */
2679 
2680         /**
2681          *
2682          * Check if pointer event is already registered in {@link JXG.Board#_board_touches}.
2683          *
2684          * @param  {Object} evt Event object
2685          * @return {Boolean} true if down event has already been sent.
2686          * @private
2687          */
2688         _isPointerRegistered: function (evt) {
2689             var i,
2690                 len = this._board_touches.length;
2691 
2692             for (i = 0; i < len; i++) {
2693                 if (this._board_touches[i].pointerId === evt.pointerId) {
2694                     return true;
2695                 }
2696             }
2697             return false;
2698         },
2699 
2700         /**
2701          *
2702          * Store the position of a pointer event.
2703          * If not yet done, registers a pointer event in {@link JXG.Board#_board_touches}.
2704          * Allows to follow the path of that finger on the screen.
2705          * Only two simultaneous touches are supported.
2706          *
2707          * @param {Object} evt Event object
2708          * @returns {JXG.Board} Reference to the board
2709          * @private
2710          */
2711         _pointerStorePosition: function (evt) {
2712             var i, found;
2713 
2714             for (i = 0, found = false; i < this._board_touches.length; i++) {
2715                 if (this._board_touches[i].pointerId === evt.pointerId) {
2716                     this._board_touches[i].clientX = evt.clientX;
2717                     this._board_touches[i].clientY = evt.clientY;
2718                     found = true;
2719                     break;
2720                 }
2721             }
2722 
2723             // Restrict the number of simultaneous touches to 2
2724             if (!found && this._board_touches.length < 2) {
2725                 this._board_touches.push({
2726                     pointerId: evt.pointerId,
2727                     clientX: evt.clientX,
2728                     clientY: evt.clientY
2729                 });
2730             }
2731 
2732             return this;
2733         },
2734 
2735         /**
2736          * Deregisters a pointer event in {@link JXG.Board#_board_touches}.
2737          * It happens if a finger has been lifted from the screen.
2738          *
2739          * @param {Object} evt Event object
2740          * @returns {JXG.Board} Reference to the board
2741          * @private
2742          */
2743         _pointerRemoveTouches: function (evt) {
2744             var i;
2745             for (i = 0; i < this._board_touches.length; i++) {
2746                 if (this._board_touches[i].pointerId === evt.pointerId) {
2747                     this._board_touches.splice(i, 1);
2748                     break;
2749                 }
2750             }
2751 
2752             return this;
2753         },
2754 
2755         /**
2756          * Remove all registered fingers from {@link JXG.Board#_board_touches}.
2757          * This might be necessary if too many fingers have been registered.
2758          * @returns {JXG.Board} Reference to the board
2759          * @private
2760          */
2761         _pointerClearTouches: function (pId) {
2762             // var i;
2763             // if (pId) {
2764             //     for (i = 0; i < this._board_touches.length; i++) {
2765             //         if (pId === this._board_touches[i].pointerId) {
2766             //             this._board_touches.splice(i, i);
2767             //             break;
2768             //         }
2769             //     }
2770             // } else {
2771             // }
2772             if (this._board_touches.length > 0) {
2773                 this.dehighlightAll();
2774             }
2775             this.updateQuality = this.BOARD_QUALITY_HIGH;
2776             this.mode = this.BOARD_MODE_NONE;
2777             this._board_touches = [];
2778             this.touches = [];
2779         },
2780 
2781         /**
2782          * Determine which input device is used for this action.
2783          * Possible devices are 'touch', 'pen' and 'mouse'.
2784          * This affects the precision and certain events.
2785          * In case of no browser, 'mouse' is used.
2786          *
2787          * @see JXG.Board#pointerDownListener
2788          * @see JXG.Board#pointerMoveListener
2789          * @see JXG.Board#initMoveObject
2790          * @see JXG.Board#moveObject
2791          *
2792          * @param {Event} evt The browsers event object.
2793          * @returns {String} 'mouse', 'pen', or 'touch'
2794          * @private
2795          */
2796         _getPointerInputDevice: function (evt) {
2797             if (Env.isBrowser) {
2798                 if (
2799                     evt.pointerType === 'touch' || // New
2800                     (window.navigator.msMaxTouchPoints && // Old
2801                         window.navigator.msMaxTouchPoints > 1)
2802                 ) {
2803                     return 'touch';
2804                 }
2805                 if (evt.pointerType === 'mouse') {
2806                     return 'mouse';
2807                 }
2808                 if (evt.pointerType === 'pen') {
2809                     return 'pen';
2810                 }
2811             }
2812             return 'mouse';
2813         },
2814 
2815         /**
2816          * This method is called by the browser when a pointing device is pressed on the screen.
2817          * @param {Event} evt The browsers event object.
2818          * @param {Object} object If the object to be dragged is already known, it can be submitted via this parameter
2819          * @param {Boolean} [allowDefaultEventHandling=false] If true event is not canceled, i.e. prevent call of evt.preventDefault()
2820          * @returns {Boolean} false if the first finger event is sent twice, or not a browser, or in selection mode. Otherwise returns true.
2821          */
2822         pointerDownListener: function (evt, object, allowDefaultEventHandling) {
2823             var i, j, k, pos,
2824                 elements, sel, target_obj,
2825                 type = 'mouse', // Used in case of no browser
2826                 found, target, ta;
2827 
2828             // Fix for Firefox browser: When using a second finger, the
2829             // touch event for the first finger is sent again.
2830             if (!object && this._isPointerRegistered(evt)) {
2831                 return false;
2832             }
2833 
2834             if (Type.evaluate(this.attr.movetarget) === null &&
2835                 Type.exists(evt.target) && Type.exists(evt.target.releasePointerCapture)) {
2836                 evt.target.releasePointerCapture(evt.pointerId);
2837             }
2838 
2839             if (!object && evt.isPrimary) {
2840                 // First finger down. To be on the safe side this._board_touches is cleared.
2841                 // this._pointerClearTouches();
2842             }
2843 
2844             if (!this.hasPointerUp) {
2845                 if (window.navigator.msPointerEnabled) {
2846                     // IE10-
2847                     Env.addEvent(this.document, 'MSPointerUp', this.pointerUpListener, this);
2848                 } else {
2849                     // 'pointercancel' is fired e.g. if the finger leaves the browser and drags down the system menu on Android
2850                     Env.addEvent(this.document, 'pointerup', this.pointerUpListener, this);
2851                     Env.addEvent(this.document, 'pointercancel', this.pointerUpListener, this);
2852                 }
2853                 this.hasPointerUp = true;
2854             }
2855 
2856             if (this.hasMouseHandlers) {
2857                 this.removeMouseEventHandlers();
2858             }
2859 
2860             if (this.hasTouchHandlers) {
2861                 this.removeTouchEventHandlers();
2862             }
2863 
2864             // Prevent accidental selection of text
2865             if (this.document.selection && Type.isFunction(this.document.selection.empty)) {
2866                 this.document.selection.empty();
2867             } else if (window.getSelection) {
2868                 sel = window.getSelection();
2869                 if (sel.removeAllRanges) {
2870                     try {
2871                         sel.removeAllRanges();
2872                     } catch (e) { }
2873                 }
2874             }
2875 
2876             // Mouse, touch or pen device
2877             this._inputDevice = this._getPointerInputDevice(evt);
2878             type = this._inputDevice;
2879             this.options.precision.hasPoint = this.options.precision[type];
2880 
2881             // Handling of multi touch with pointer events should be easier than with touch events.
2882             // Every pointer device has its own pointerId, e.g. the mouse
2883             // always has id 1 or 0, fingers and pens get unique ids every time a pointerDown event is fired and they will
2884             // keep this id until a pointerUp event is fired. What we have to do here is:
2885             //  1. collect all elements under the current pointer
2886             //  2. run through the touches control structure
2887             //    a. look for the object collected in step 1.
2888             //    b. if an object is found, check the number of pointers. If appropriate, add the pointer.
2889             pos = this.getMousePosition(evt);
2890 
2891             // Handle selection rectangle
2892             this._testForSelection(evt);
2893             if (this.selectingMode) {
2894                 this._startSelecting(pos);
2895                 this.triggerEventHandlers(
2896                     ['touchstartselecting', 'pointerstartselecting', 'startselecting'],
2897                     [evt]
2898                 );
2899                 return; // don't continue as a normal click
2900             }
2901 
2902             if (this.attr.drag.enabled && object) {
2903                 elements = [object];
2904                 this.mode = this.BOARD_MODE_DRAG;
2905             } else {
2906                 elements = this.initMoveObject(pos[0], pos[1], evt, type);
2907             }
2908 
2909             target_obj = {
2910                 num: evt.pointerId,
2911                 X: pos[0],
2912                 Y: pos[1],
2913                 Xprev: NaN,
2914                 Yprev: NaN,
2915                 Xstart: [],
2916                 Ystart: [],
2917                 Zstart: []
2918             };
2919 
2920             // If no draggable object can be found, get out here immediately
2921             if (elements.length > 0) {
2922                 // check touches structure
2923                 target = elements[elements.length - 1];
2924                 found = false;
2925 
2926                 // Reminder: this.touches is the list of elements which
2927                 // currently 'possess' a pointer (mouse, pen, finger)
2928                 for (i = 0; i < this.touches.length; i++) {
2929                     // An element receives a further touch, i.e.
2930                     // the target is already in our touches array, add the pointer to the existing touch
2931                     if (this.touches[i].obj === target) {
2932                         j = i;
2933                         k = this.touches[i].targets.push(target_obj) - 1;
2934                         found = true;
2935                         break;
2936                     }
2937                 }
2938                 if (!found) {
2939                     // A new element has been touched.
2940                     k = 0;
2941                     j =
2942                         this.touches.push({
2943                             obj: target,
2944                             targets: [target_obj]
2945                         }) - 1;
2946                 }
2947 
2948                 this.dehighlightAll();
2949                 target.highlight(true);
2950 
2951                 this.saveStartPos(target, this.touches[j].targets[k]);
2952 
2953                 // Prevent accidental text selection
2954                 // this could get us new trouble: input fields, links and drop down boxes placed as text
2955                 // on the board don't work anymore.
2956                 if (evt && evt.preventDefault && !allowDefaultEventHandling) {
2957                     // All browser supporting pointer events know preventDefault()
2958                     evt.preventDefault();
2959                 }
2960             }
2961 
2962             if (this.touches.length > 0 && !allowDefaultEventHandling) {
2963                 evt.preventDefault();
2964                 evt.stopPropagation();
2965             }
2966 
2967             if (!Env.isBrowser) {
2968                 return false;
2969             }
2970             if (this._getPointerInputDevice(evt) !== 'touch') {
2971                 if (this.mode === this.BOARD_MODE_NONE) {
2972                     this.mouseOriginMoveStart(evt);
2973                 }
2974             } else {
2975                 this._pointerStorePosition(evt);
2976                 evt.touches = this._board_touches;
2977 
2978                 // Touch events on empty areas of the board are handled here, see also touchStartListener
2979                 // 1. case: one finger. If allowed, this triggers pan with one finger
2980                 if (
2981                     evt.touches.length === 1 &&
2982                     this.mode === this.BOARD_MODE_NONE &&
2983                     this.touchStartMoveOriginOneFinger(evt)
2984                 ) {
2985                     // Empty by purpose
2986                 } else if (
2987                     evt.touches.length === 2 &&
2988                     (this.mode === this.BOARD_MODE_NONE ||
2989                         this.mode === this.BOARD_MODE_MOVE_ORIGIN)
2990                 ) {
2991                     // 2. case: two fingers: pinch to zoom or pan with two fingers needed.
2992                     // This happens when the second finger hits the device. First, the
2993                     // 'one finger pan mode' has to be cancelled.
2994                     if (this.mode === this.BOARD_MODE_MOVE_ORIGIN) {
2995                         this.originMoveEnd();
2996                     }
2997 
2998                     this.gestureStartListener(evt);
2999                 }
3000             }
3001 
3002             this.initSketchCurve(evt);
3003 
3004             // Allow browser scrolling
3005             // For this: pan by one finger has to be disabled
3006 
3007             ta = 'none';   // JSXGraph catches all user touch events
3008             if (this.mode === this.BOARD_MODE_NONE &&
3009                 (Type.evaluate(this.attr.browserpan) === true || Type.evaluate(this.attr.browserpan.enabled) === true) &&
3010                 // One-finger pan has priority over browserPan
3011                 (Type.evaluate(this.attr.pan.enabled) === false || Type.evaluate(this.attr.pan.needtwofingers) === true)
3012             ) {
3013                 // ta = 'pan-x pan-y';  // JSXGraph allows browser scrolling
3014                 ta = 'auto';  // JSXGraph allows browser scrolling
3015             }
3016             this.containerObj.style.touchAction = ta;
3017 
3018             this.triggerEventHandlers(['touchstart', 'down', 'pointerdown', 'MSPointerDown'], [evt]);
3019 
3020             return true;
3021         },
3022 
3023         /**
3024          * Internal handling of click events for pointers and mouse.
3025          *
3026          * @param {Event} evt The browsers event object.
3027          * @param {Array} evtArray list of event names
3028          * @private
3029          */
3030         _handleClicks: function(evt, evtArray) {
3031             var that = this,
3032                 el, delay, suppress;
3033 
3034             if (this.selectingMode) {
3035                 evt.stopPropagation();
3036                 return;
3037             }
3038 
3039             delay = Type.evaluate(this.attr.clickdelay);
3040             suppress = Type.evaluate(this.attr.dblclicksuppressclick);
3041 
3042             if (suppress) {
3043                 // dblclick suppresses previous click events
3044                 this._preventSingleClick = false;
3045 
3046                 // Wait if there is a dblclick event.
3047                 // If not fire a click event
3048                 this._singleClickTimer = setTimeout(function() {
3049                     if (!that._preventSingleClick) {
3050                         // Fire click event and remove element from click list
3051                         that.triggerEventHandlers(evtArray, [evt]);
3052                         for (el in that.clickObjects) {
3053                             if (that.clickObjects.hasOwnProperty(el)) {
3054                                 that.clickObjects[el].triggerEventHandlers(evtArray, [evt]);
3055                                 delete that.clickObjects[el];
3056                             }
3057                         }
3058                     }
3059                 }, delay);
3060             } else {
3061                 // dblclick is preceded by two click events
3062 
3063                 // Fire click events
3064                 that.triggerEventHandlers(evtArray, [evt]);
3065                 for (el in that.clickObjects) {
3066                     if (that.clickObjects.hasOwnProperty(el)) {
3067                         that.clickObjects[el].triggerEventHandlers(evtArray, [evt]);
3068                     }
3069                 }
3070 
3071                 // Clear list of clicked elements with a delay
3072                 setTimeout(function() {
3073                     for (el in that.clickObjects) {
3074                         if (that.clickObjects.hasOwnProperty(el)) {
3075                             delete that.clickObjects[el];
3076                         }
3077                     }
3078                 }, delay);
3079             }
3080             evt.stopPropagation();
3081         },
3082 
3083         /**
3084          * Internal handling of dblclick events for pointers and mouse.
3085          *
3086          * @param {Event} evt The browsers event object.
3087          * @param {Array} evtArray list of event names
3088          * @private
3089          */
3090         _handleDblClicks: function(evt, evtArray) {
3091             var el;
3092 
3093             if (this.selectingMode) {
3094                 evt.stopPropagation();
3095                 return;
3096             }
3097 
3098             // Notify that a dblclick has happened
3099             this._preventSingleClick = true;
3100             clearTimeout(this._singleClickTimer);
3101 
3102             // Fire dblclick event
3103             this.triggerEventHandlers(evtArray, [evt]);
3104             for (el in this.clickObjects) {
3105                 if (this.clickObjects.hasOwnProperty(el)) {
3106                     this.clickObjects[el].triggerEventHandlers(evtArray, [evt]);
3107                     delete this.clickObjects[el];
3108                 }
3109             }
3110 
3111             evt.stopPropagation();
3112         },
3113 
3114         /**
3115          * This method is called by the browser when a pointer device clicks on the screen.
3116          * @param {Event} evt The browsers event object.
3117          */
3118         pointerClickListener: function (evt) {
3119             this._handleClicks(evt, ['click', 'pointerclick']);
3120         },
3121 
3122         /**
3123          * This method is called by the browser when a pointer device double clicks on the screen.
3124          * @param {Event} evt The browsers event object.
3125          */
3126         pointerDblClickListener: function (evt) {
3127             this._handleDblClicks(evt, ['dblclick', 'pointerdblclick']);
3128         },
3129 
3130         /**
3131          * This method is called by the browser when the mouse device clicks on the screen.
3132          * @param {Event} evt The browsers event object.
3133          */
3134         mouseClickListener: function (evt) {
3135             this._handleClicks(evt, ['click', 'mouseclick']);
3136         },
3137 
3138         /**
3139          * This method is called by the browser when the mouse device double clicks on the screen.
3140          * @param {Event} evt The browsers event object.
3141          */
3142         mouseDblClickListener: function (evt) {
3143             this._handleDblClicks(evt, ['dblclick', 'mousedblclick']);
3144         },
3145 
3146         // /**
3147         //  * Called if pointer leaves an HTML tag. It is called by the inner-most tag.
3148         //  * That means, if a JSXGraph text, i.e. an HTML div, is placed close
3149         //  * to the border of the board, this pointerout event will be ignored.
3150         //  * @param  {Event} evt
3151         //  * @return {Boolean}
3152         //  */
3153         // pointerOutListener: function (evt) {
3154         //     if (evt.target === this.containerObj ||
3155         //         (this.renderer.type === 'svg' && evt.target === this.renderer.foreignObjLayer)) {
3156         //         this.pointerUpListener(evt);
3157         //     }
3158         //     return this.mode === this.BOARD_MODE_NONE;
3159         // },
3160 
3161         /**
3162          * Called periodically by the browser while the user moves a pointing device across the screen.
3163          * @param {Event} evt
3164          * @returns {Boolean}
3165          */
3166         pointerMoveListener: function (evt) {
3167             var i, j, pos,
3168                 eps,
3169                 touchTargets,
3170                 type = 'mouse'; // in case of no browser
3171 
3172             if (
3173                 this._getPointerInputDevice(evt) === 'touch' &&
3174                 !this._isPointerRegistered(evt)
3175             ) {
3176                 // Test, if there was a previous down event of this _getPointerId
3177                 // (in case it is a touch event).
3178                 // Otherwise this move event is ignored. This is necessary e.g. for sketchometry.
3179                 return this.BOARD_MODE_NONE;
3180             }
3181 
3182             if (!this.checkFrameRate(evt)) {
3183                 return false;
3184             }
3185 
3186             if (this.mode !== this.BOARD_MODE_DRAG) {
3187                 this.dehighlightAll();
3188                 this.displayInfobox(false);
3189             }
3190 
3191             if (this.mode !== this.BOARD_MODE_NONE) {
3192                 evt.preventDefault();
3193                 evt.stopPropagation();
3194             }
3195 
3196             this.updateQuality = this.BOARD_QUALITY_LOW;
3197             // Mouse, touch or pen device
3198             this._inputDevice = this._getPointerInputDevice(evt);
3199             type = this._inputDevice;
3200             this.options.precision.hasPoint = this.options.precision[type];
3201             eps = this.options.precision.hasPoint * 0.3333;
3202 
3203             pos = this.getMousePosition(evt);
3204             // Ignore pointer move event if too close at the border
3205             // and setPointerCapture is off
3206             if (Type.evaluate(this.attr.movetarget) === null &&
3207                 (pos[0] <= eps || pos[1] <= eps ||
3208                  pos[0] >= this.canvasWidth - eps ||
3209                  pos[1] >= this.canvasHeight - eps)
3210             ) {
3211                 return this.mode === this.BOARD_MODE_NONE;
3212             }
3213 
3214             // selection
3215             if (this.selectingMode) {
3216                 this._moveSelecting(pos);
3217                 this.triggerEventHandlers(
3218                     ['touchmoveselecting', 'moveselecting', 'pointermoveselecting'],
3219                     [evt, this.mode]
3220                 );
3221             } else if (!this.mouseOriginMove(evt)) {
3222 
3223                 this.addToSketchCurve(evt);
3224 
3225                 if (this.mode === this.BOARD_MODE_DRAG) {
3226                     // Run through all jsxgraph elements which are touched by at least one finger.
3227                     for (i = 0; i < this.touches.length; i++) {
3228                         touchTargets = this.touches[i].targets;
3229                         // Run through all touch events which have been started on this jsxgraph element.
3230                         for (j = 0; j < touchTargets.length; j++) {
3231                             if (touchTargets[j].num === evt.pointerId) {
3232                                 touchTargets[j].X = pos[0];
3233                                 touchTargets[j].Y = pos[1];
3234 
3235                                 if (touchTargets.length === 1) {
3236                                     // Touch by one finger: this is possible for all elements that can be dragged
3237                                     this.moveObject(pos[0], pos[1], this.touches[i], evt, type);
3238                                 } else if (touchTargets.length === 2) {
3239                                     // Touch by two fingers: e.g. moving lines
3240                                     this.twoFingerMove(this.touches[i], evt.pointerId, evt);
3241 
3242                                     touchTargets[j].Xprev = pos[0];
3243                                     touchTargets[j].Yprev = pos[1];
3244                                 }
3245 
3246                                 // There is only one pointer in the evt object, so there's no point in looking further
3247                                 break;
3248                             }
3249                         }
3250                     }
3251                 } else {
3252                     if (this._getPointerInputDevice(evt) === 'touch') {
3253                         this._pointerStorePosition(evt);
3254 
3255                         if (this._board_touches.length === 2) {
3256                             evt.touches = this._board_touches;
3257                             this.gestureChangeListener(evt);
3258                         }
3259                     }
3260 
3261                     // Move event without dragging an element
3262                     this.highlightElements(pos[0], pos[1], evt, -1);
3263                 }
3264             }
3265 
3266             // Hiding the infobox is commented out, since it prevents showing the infobox
3267             // on IE 11+ on 'over'
3268             //if (this.mode !== this.BOARD_MODE_DRAG) {
3269             //this.displayInfobox(false);
3270             //}
3271             this.triggerEventHandlers(['pointermove', 'MSPointerMove', 'move'], [evt, this.mode]);
3272             this.updateQuality = this.BOARD_QUALITY_HIGH;
3273 
3274             return this.mode === this.BOARD_MODE_NONE;
3275         },
3276 
3277         /**
3278          * Triggered as soon as the user stops touching the device with at least one finger.
3279          *
3280          * @param {Event} evt
3281          * @returns {Boolean}
3282          */
3283         pointerUpListener: function (evt) {
3284             var i, j, found, eh,
3285                 touchTargets,
3286                 updateNeeded = false;
3287 
3288             this.triggerEventHandlers(['touchend', 'up', 'pointerup', 'MSPointerUp'], [evt]);
3289             this.displayInfobox(false);
3290 
3291             if (evt) {
3292                 for (i = 0; i < this.touches.length; i++) {
3293                     touchTargets = this.touches[i].targets;
3294                     for (j = 0; j < touchTargets.length; j++) {
3295                         if (touchTargets[j].num === evt.pointerId) {
3296                             touchTargets.splice(j, 1);
3297                             if (touchTargets.length === 0) {
3298                                 this.touches.splice(i, 1);
3299                             }
3300                             break;
3301                         }
3302                     }
3303                 }
3304             }
3305 
3306             this.finalizeSketchCurve(evt);
3307             this.originMoveEnd();
3308             this.update();
3309 
3310             // selection
3311             if (this.selectingMode) {
3312                 this._stopSelecting(evt);
3313                 this.triggerEventHandlers(
3314                     ['touchstopselecting', 'pointerstopselecting', 'stopselecting'],
3315                     [evt]
3316                 );
3317                 this.stopSelectionMode();
3318             } else {
3319                 for (i = this.downObjects.length - 1; i > -1; i--) {
3320                     found = false;
3321                     for (j = 0; j < this.touches.length; j++) {
3322                         if (this.touches[j].obj.id === this.downObjects[i].id) {
3323                             found = true;
3324                         }
3325                     }
3326                     if (!found) {
3327                         this.downObjects[i].triggerEventHandlers(
3328                             ['touchend', 'up', 'pointerup', 'MSPointerUp'],
3329                             [evt]
3330                         );
3331                         if (!Type.exists(this.downObjects[i].coords)) {
3332                             // snapTo methods have to be called e.g. for line elements here.
3333                             // For coordsElements there might be a conflict with
3334                             // attractors, see commit from 2022.04.08, 11:12:18.
3335                             this.downObjects[i].snapToGrid();
3336                             this.downObjects[i].snapToPoints();
3337                             updateNeeded = true;
3338                         }
3339 
3340                         // Check if we have to keep the element for a click or dblclick event
3341                         // Otherwise remove it from downObjects
3342                         eh = this.downObjects[i].eventHandlers;
3343                         if ((Type.exists(eh.click) && eh.click.length > 0) ||
3344                             (Type.exists(eh.pointerclick) && eh.pointerclick.length > 0) ||
3345                             (Type.exists(eh.dblclick) && eh.dblclick.length > 0) ||
3346                             (Type.exists(eh.pointerdblclick) && eh.pointerdblclick.length > 0)
3347                         ) {
3348                             this.clickObjects[this.downObjects[i].id] = this.downObjects[i];
3349                         }
3350                         this.downObjects.splice(i, 1);
3351                     }
3352                 }
3353             }
3354 
3355             if (this.hasPointerUp) {
3356                 if (window.navigator.msPointerEnabled) {
3357                     // IE10-
3358                     Env.removeEvent(this.document, 'MSPointerUp', this.pointerUpListener, this);
3359                 } else {
3360                     Env.removeEvent(this.document, 'pointerup', this.pointerUpListener, this);
3361                     Env.removeEvent(this.document, 'pointercancel', this.pointerUpListener, this);
3362                 }
3363                 this.hasPointerUp = false;
3364             }
3365 
3366             // After one finger leaves the screen the gesture is stopped.
3367             this._pointerClearTouches(evt.pointerId);
3368             if (this._getPointerInputDevice(evt) !== 'touch') {
3369                 this.dehighlightAll();
3370             }
3371 
3372             if (updateNeeded) {
3373                 this.update();
3374             }
3375 
3376             return true;
3377         },
3378 
3379         /**
3380          * Triggered by the pointerleave event. This is needed in addition to
3381          * {@link JXG.Board#pointerUpListener} in the situation that a pen is used
3382          * and after an up event the pen leaves the hover range vertically. Here, it happens that
3383          * after the pointerup event further pointermove events are fired and elements get highlighted.
3384          * This highlighting has to be cancelled.
3385          *
3386          * @param {Event} evt
3387          * @returns {Boolean}
3388          */
3389         pointerLeaveListener: function (evt) {
3390             this.displayInfobox(false);
3391             this.dehighlightAll();
3392 
3393             return true;
3394         },
3395 
3396         /**
3397          * Touch-Events
3398          */
3399 
3400         /**
3401          * This method is called by the browser when a finger touches the surface of the touch-device.
3402          * @param {Event} evt The browsers event object.
3403          * @returns {Boolean} ...
3404          */
3405         touchStartListener: function (evt) {
3406             var i, j, k,
3407                 pos, elements, obj,
3408                 eps = this.options.precision.touch,
3409                 evtTouches = evt['touches'],
3410                 found,
3411                 targets, target,
3412                 touchTargets;
3413 
3414             if (!this.hasTouchEnd) {
3415                 Env.addEvent(this.document, 'touchend', this.touchEndListener, this);
3416                 this.hasTouchEnd = true;
3417             }
3418 
3419             // Do not remove mouseHandlers, since Chrome on win tablets sends mouseevents if used with pen.
3420             //if (this.hasMouseHandlers) { this.removeMouseEventHandlers(); }
3421 
3422             // prevent accidental selection of text
3423             if (this.document.selection && Type.isFunction(this.document.selection.empty)) {
3424                 this.document.selection.empty();
3425             } else if (window.getSelection) {
3426                 window.getSelection().removeAllRanges();
3427             }
3428 
3429             // multitouch
3430             this._inputDevice = 'touch';
3431             this.options.precision.hasPoint = this.options.precision.touch;
3432 
3433             // This is the most critical part. first we should run through the existing touches and collect all targettouches that don't belong to our
3434             // 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
3435             // 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
3436             // 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
3437             // stretching. as a last step we're going through the rest of the targettouches and initiate new move operations:
3438             //  * points have higher priority over other elements.
3439             //  * 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
3440             //    this element and add them.
3441             // ADDENDUM 11/10/11:
3442             //  (1) run through the touches control object,
3443             //  (2) try to find the targetTouches for every touch. on touchstart only new touches are added, hence we can find a targettouch
3444             //      for every target in our touches objects
3445             //  (3) if one of the targettouches was bound to a touches targets array, mark it
3446             //  (4) run through the targettouches. if the targettouch is marked, continue. otherwise check for elements below the targettouch:
3447             //      (a) if no element could be found: mark the target touches and continue
3448             //      --- in the following cases, 'init' means:
3449             //           (i) check if the element is already used in another touches element, if so, mark the targettouch and continue
3450             //          (ii) if not, init a new touches element, add the targettouch to the touches property and mark it
3451             //      (b) if the element is a point, init
3452             //      (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
3453             //      (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
3454             //          add both to the touches array and mark them.
3455             for (i = 0; i < evtTouches.length; i++) {
3456                 evtTouches[i].jxg_isused = false;
3457             }
3458 
3459             for (i = 0; i < this.touches.length; i++) {
3460                 touchTargets = this.touches[i].targets;
3461                 for (j = 0; j < touchTargets.length; j++) {
3462                     touchTargets[j].num = -1;
3463                     eps = this.options.precision.touch;
3464 
3465                     do {
3466                         for (k = 0; k < evtTouches.length; k++) {
3467                             // find the new targettouches
3468                             if (
3469                                 Math.abs(
3470                                     Math.pow(evtTouches[k].screenX - touchTargets[j].X, 2) +
3471                                     Math.pow(evtTouches[k].screenY - touchTargets[j].Y, 2)
3472                                 ) <
3473                                 eps * eps
3474                             ) {
3475                                 touchTargets[j].num = k;
3476                                 touchTargets[j].X = evtTouches[k].screenX;
3477                                 touchTargets[j].Y = evtTouches[k].screenY;
3478                                 evtTouches[k].jxg_isused = true;
3479                                 break;
3480                             }
3481                         }
3482 
3483                         eps *= 2;
3484                     } while (
3485                         touchTargets[j].num === -1 &&
3486                         eps < this.options.precision.touchMax
3487                     );
3488 
3489                     if (touchTargets[j].num === -1) {
3490                         JXG.debug(
3491                             "i couldn't find a targettouches for target no " +
3492                             j +
3493                             ' on ' +
3494                             this.touches[i].obj.name +
3495                             ' (' +
3496                             this.touches[i].obj.id +
3497                             '). Removed the target.'
3498                         );
3499                         JXG.debug(
3500                             'eps = ' + eps + ', touchMax = ' + Options.precision.touchMax
3501                         );
3502                         touchTargets.splice(i, 1);
3503                     }
3504                 }
3505             }
3506 
3507             // we just re-mapped the targettouches to our existing touches list.
3508             // now we have to initialize some touches from additional targettouches
3509             for (i = 0; i < evtTouches.length; i++) {
3510                 if (!evtTouches[i].jxg_isused) {
3511                     pos = this.getMousePosition(evt, i);
3512                     // selection
3513                     // this._testForSelection(evt); // we do not have shift or ctrl keys yet.
3514                     if (this.selectingMode) {
3515                         this._startSelecting(pos);
3516                         this.triggerEventHandlers(
3517                             ['touchstartselecting', 'startselecting'],
3518                             [evt]
3519                         );
3520                         evt.preventDefault();
3521                         evt.stopPropagation();
3522                         this.options.precision.hasPoint = this.options.precision.mouse;
3523                         return this.touches.length > 0; // don't continue as a normal click
3524                     }
3525 
3526                     elements = this.initMoveObject(pos[0], pos[1], evt, 'touch');
3527                     if (elements.length !== 0) {
3528                         obj = elements[elements.length - 1];
3529                         target = {
3530                             num: i,
3531                             X: evtTouches[i].screenX,
3532                             Y: evtTouches[i].screenY,
3533                             Xprev: NaN,
3534                             Yprev: NaN,
3535                             Xstart: [],
3536                             Ystart: [],
3537                             Zstart: []
3538                         };
3539 
3540                         if (
3541                             Type.isPoint(obj) ||
3542                             obj.elementClass === Const.OBJECT_CLASS_TEXT ||
3543                             obj.type === Const.OBJECT_TYPE_TICKS ||
3544                             obj.type === Const.OBJECT_TYPE_IMAGE
3545                         ) {
3546                             // It's a point, so it's single touch, so we just push it to our touches
3547                             targets = [target];
3548 
3549                             // For the UNDO/REDO of object moves
3550                             this.saveStartPos(obj, targets[0]);
3551 
3552                             this.touches.push({ obj: obj, targets: targets });
3553                             obj.highlight(true);
3554                         } else if (
3555                             obj.elementClass === Const.OBJECT_CLASS_LINE ||
3556                             obj.elementClass === Const.OBJECT_CLASS_CIRCLE ||
3557                             obj.elementClass === Const.OBJECT_CLASS_CURVE ||
3558                             obj.type === Const.OBJECT_TYPE_POLYGON
3559                         ) {
3560                             found = false;
3561 
3562                             // first check if this geometric object is already captured in this.touches
3563                             for (j = 0; j < this.touches.length; j++) {
3564                                 if (obj.id === this.touches[j].obj.id) {
3565                                     found = true;
3566                                     // only add it, if we don't have two targets in there already
3567                                     if (this.touches[j].targets.length === 1) {
3568                                         // For the UNDO/REDO of object moves
3569                                         this.saveStartPos(obj, target);
3570                                         this.touches[j].targets.push(target);
3571                                     }
3572 
3573                                     evtTouches[i].jxg_isused = true;
3574                                 }
3575                             }
3576 
3577                             // we couldn't find it in touches, so we just init a new touches
3578                             // IF there is a second touch targetting this line, we will find it later on, and then add it to
3579                             // the touches control object.
3580                             if (!found) {
3581                                 targets = [target];
3582 
3583                                 // For the UNDO/REDO of object moves
3584                                 this.saveStartPos(obj, targets[0]);
3585                                 this.touches.push({ obj: obj, targets: targets });
3586                                 obj.highlight(true);
3587                             }
3588                         }
3589                     }
3590 
3591                     evtTouches[i].jxg_isused = true;
3592                 }
3593             }
3594 
3595             if (this.touches.length > 0) {
3596                 evt.preventDefault();
3597                 evt.stopPropagation();
3598             }
3599 
3600             // Touch events on empty areas of the board are handled here:
3601             // 1. case: one finger. If allowed, this triggers pan with one finger
3602             if (
3603                 evtTouches.length === 1 &&
3604                 this.mode === this.BOARD_MODE_NONE &&
3605                 this.touchStartMoveOriginOneFinger(evt)
3606             ) {
3607             } else if (
3608                 evtTouches.length === 2 &&
3609                 (this.mode === this.BOARD_MODE_NONE ||
3610                     this.mode === this.BOARD_MODE_MOVE_ORIGIN)
3611             ) {
3612                 // 2. case: two fingers: pinch to zoom or pan with two fingers needed.
3613                 // This happens when the second finger hits the device. First, the
3614                 // 'one finger pan mode' has to be cancelled.
3615                 if (this.mode === this.BOARD_MODE_MOVE_ORIGIN) {
3616                     this.originMoveEnd();
3617                 }
3618                 this.gestureStartListener(evt);
3619             }
3620 
3621             this.initSketchCurve(evt);
3622 
3623             this.options.precision.hasPoint = this.options.precision.mouse;
3624             this.triggerEventHandlers(['touchstart', 'down'], [evt]);
3625 
3626             return false;
3627             //return this.touches.length > 0;
3628         },
3629 
3630         /**
3631          * Called periodically by the browser while the user moves his fingers across the device.
3632          * @param {Event} evt
3633          * @returns {Boolean}
3634          */
3635         touchMoveListener: function (evt) {
3636             var i,
3637                 pos1, pos2,
3638                 touchTargets,
3639                 evtTouches = evt['touches'];
3640 
3641             if (!this.checkFrameRate(evt)) {
3642                 return false;
3643             }
3644 
3645             if (this.mode !== this.BOARD_MODE_NONE) {
3646                 evt.preventDefault();
3647                 evt.stopPropagation();
3648             }
3649 
3650             if (this.mode !== this.BOARD_MODE_DRAG) {
3651                 this.dehighlightAll();
3652                 this.displayInfobox(false);
3653             }
3654 
3655             this._inputDevice = 'touch';
3656             this.options.precision.hasPoint = this.options.precision.touch;
3657             this.updateQuality = this.BOARD_QUALITY_LOW;
3658 
3659             // selection
3660             if (this.selectingMode) {
3661                 for (i = 0; i < evtTouches.length; i++) {
3662                     if (!evtTouches[i].jxg_isused) {
3663                         pos1 = this.getMousePosition(evt, i);
3664                         this._moveSelecting(pos1);
3665                         this.triggerEventHandlers(
3666                             ['touchmoves', 'moveselecting'],
3667                             [evt, this.mode]
3668                         );
3669                         break;
3670                     }
3671                 }
3672             } else {
3673                 if (!this.touchOriginMove(evt)) {
3674 
3675                     this.addToSketchCurve(evt);
3676 
3677                     if (this.mode === this.BOARD_MODE_DRAG) {
3678                         // Runs over through all elements which are touched
3679                         // by at least one finger.
3680                         for (i = 0; i < this.touches.length; i++) {
3681                             touchTargets = this.touches[i].targets;
3682                             if (touchTargets.length === 1) {
3683                                 // Touch by one finger:  this is possible for all elements that can be dragged
3684                                 if (evtTouches[touchTargets[0].num]) {
3685                                     pos1 = this.getMousePosition(evt, touchTargets[0].num);
3686                                     if (
3687                                         pos1[0] < 0 ||
3688                                         pos1[0] > this.canvasWidth ||
3689                                         pos1[1] < 0 ||
3690                                         pos1[1] > this.canvasHeight
3691                                     ) {
3692                                         return;
3693                                     }
3694                                     touchTargets[0].X = pos1[0];
3695                                     touchTargets[0].Y = pos1[1];
3696                                     this.moveObject(
3697                                         pos1[0],
3698                                         pos1[1],
3699                                         this.touches[i],
3700                                         evt,
3701                                         'touch'
3702                                     );
3703                                 }
3704                             } else if (
3705                                 touchTargets.length === 2 &&
3706                                 touchTargets[0].num > -1 &&
3707                                 touchTargets[1].num > -1
3708                             ) {
3709                                 // Touch by two fingers: moving lines, ...
3710                                 if (
3711                                     evtTouches[touchTargets[0].num] &&
3712                                     evtTouches[touchTargets[1].num]
3713                                 ) {
3714                                     // Get coordinates of the two touches
3715                                     pos1 = this.getMousePosition(evt, touchTargets[0].num);
3716                                     pos2 = this.getMousePosition(evt, touchTargets[1].num);
3717                                     if (
3718                                         pos1[0] < 0 ||
3719                                         pos1[0] > this.canvasWidth ||
3720                                         pos1[1] < 0 ||
3721                                         pos1[1] > this.canvasHeight ||
3722                                         pos2[0] < 0 ||
3723                                         pos2[0] > this.canvasWidth ||
3724                                         pos2[1] < 0 ||
3725                                         pos2[1] > this.canvasHeight
3726                                     ) {
3727                                         return;
3728                                     }
3729 
3730                                     touchTargets[0].X = pos1[0];
3731                                     touchTargets[0].Y = pos1[1];
3732                                     touchTargets[1].X = pos2[0];
3733                                     touchTargets[1].Y = pos2[1];
3734 
3735                                     this.twoFingerMove(
3736                                         this.touches[i],
3737                                         touchTargets[0].num,
3738                                         evt
3739                                     );
3740 
3741                                     touchTargets[0].Xprev = pos1[0];
3742                                     touchTargets[0].Yprev = pos1[1];
3743                                     touchTargets[1].Xprev = pos2[0];
3744                                     touchTargets[1].Yprev = pos2[1];
3745                                 }
3746                             }
3747                         }
3748                     } else {
3749                         if (evtTouches.length === 2) {
3750                             this.gestureChangeListener(evt);
3751                         }
3752                         // Move event without dragging an element
3753                         pos1 = this.getMousePosition(evt, 0);
3754                         this.highlightElements(pos1[0], pos1[1], evt, -1);
3755                     }
3756                 }
3757             }
3758 
3759             if (this.mode !== this.BOARD_MODE_DRAG) {
3760                 this.displayInfobox(false);
3761             }
3762 
3763             this.triggerEventHandlers(['touchmove', 'move'], [evt, this.mode]);
3764             this.options.precision.hasPoint = this.options.precision.mouse;
3765             this.updateQuality = this.BOARD_QUALITY_HIGH;
3766 
3767             return this.mode === this.BOARD_MODE_NONE;
3768         },
3769 
3770         /**
3771          * Triggered as soon as the user stops touching the device with at least one finger.
3772          * @param {Event} evt
3773          * @returns {Boolean}
3774          */
3775         touchEndListener: function (evt) {
3776             var i,
3777                 j,
3778                 k,
3779                 eps = this.options.precision.touch,
3780                 tmpTouches = [],
3781                 found,
3782                 foundNumber,
3783                 evtTouches = evt && evt['touches'],
3784                 touchTargets,
3785                 updateNeeded = false;
3786 
3787             this.triggerEventHandlers(['touchend', 'up'], [evt]);
3788             this.displayInfobox(false);
3789 
3790             this.finalizeSketchCurve(evt);
3791 
3792             // selection
3793             if (this.selectingMode) {
3794                 this._stopSelecting(evt);
3795                 this.triggerEventHandlers(['touchstopselecting', 'stopselecting'], [evt]);
3796                 this.stopSelectionMode();
3797             } else if (evtTouches && evtTouches.length > 0) {
3798                 for (i = 0; i < this.touches.length; i++) {
3799                     tmpTouches[i] = this.touches[i];
3800                 }
3801                 this.touches.length = 0;
3802 
3803                 // try to convert the operation, e.g. if a lines is rotated and translated with two fingers and one finger is lifted,
3804                 // convert the operation to a simple one-finger-translation.
3805                 // ADDENDUM 11/10/11:
3806                 // see addendum to touchStartListener from 11/10/11
3807                 // (1) run through the tmptouches
3808                 // (2) check the touches.obj, if it is a
3809                 //     (a) point, try to find the targettouch, if found keep it and mark the targettouch, else drop the touch.
3810                 //     (b) line with
3811                 //          (i) one target: try to find it, if found keep it mark the targettouch, else drop the touch.
3812                 //         (ii) two targets: if none can be found, drop the touch. if one can be found, remove the other target. mark all found targettouches
3813                 //     (c) circle with [proceed like in line]
3814 
3815                 // init the targettouches marker
3816                 for (i = 0; i < evtTouches.length; i++) {
3817                     evtTouches[i].jxg_isused = false;
3818                 }
3819 
3820                 for (i = 0; i < tmpTouches.length; i++) {
3821                     // could all targets of the current this.touches.obj be assigned to targettouches?
3822                     found = false;
3823                     foundNumber = 0;
3824                     touchTargets = tmpTouches[i].targets;
3825 
3826                     for (j = 0; j < touchTargets.length; j++) {
3827                         touchTargets[j].found = false;
3828                         for (k = 0; k < evtTouches.length; k++) {
3829                             if (
3830                                 Math.abs(
3831                                     Math.pow(evtTouches[k].screenX - touchTargets[j].X, 2) +
3832                                     Math.pow(evtTouches[k].screenY - touchTargets[j].Y, 2)
3833                                 ) <
3834                                 eps * eps
3835                             ) {
3836                                 touchTargets[j].found = true;
3837                                 touchTargets[j].num = k;
3838                                 touchTargets[j].X = evtTouches[k].screenX;
3839                                 touchTargets[j].Y = evtTouches[k].screenY;
3840                                 foundNumber += 1;
3841                                 break;
3842                             }
3843                         }
3844                     }
3845 
3846                     if (Type.isPoint(tmpTouches[i].obj)) {
3847                         found = touchTargets[0] && touchTargets[0].found;
3848                     } else if (tmpTouches[i].obj.elementClass === Const.OBJECT_CLASS_LINE) {
3849                         found =
3850                             (touchTargets[0] && touchTargets[0].found) ||
3851                             (touchTargets[1] && touchTargets[1].found);
3852                     } else if (tmpTouches[i].obj.elementClass === Const.OBJECT_CLASS_CIRCLE) {
3853                         found = foundNumber === 1 || foundNumber === 3;
3854                     }
3855 
3856                     // if we found this object to be still dragged by the user, add it back to this.touches
3857                     if (found) {
3858                         this.touches.push({
3859                             obj: tmpTouches[i].obj,
3860                             targets: []
3861                         });
3862 
3863                         for (j = 0; j < touchTargets.length; j++) {
3864                             if (touchTargets[j].found) {
3865                                 this.touches[this.touches.length - 1].targets.push({
3866                                     num: touchTargets[j].num,
3867                                     X: touchTargets[j].screenX,
3868                                     Y: touchTargets[j].screenY,
3869                                     Xprev: NaN,
3870                                     Yprev: NaN,
3871                                     Xstart: touchTargets[j].Xstart,
3872                                     Ystart: touchTargets[j].Ystart,
3873                                     Zstart: touchTargets[j].Zstart
3874                                 });
3875                             }
3876                         }
3877                     } else {
3878                         tmpTouches[i].obj.noHighlight();
3879                     }
3880                 }
3881             } else {
3882                 this.touches.length = 0;
3883             }
3884 
3885             for (i = this.downObjects.length - 1; i > -1; i--) {
3886                 found = false;
3887                 for (j = 0; j < this.touches.length; j++) {
3888                     if (this.touches[j].obj.id === this.downObjects[i].id) {
3889                         found = true;
3890                     }
3891                 }
3892                 if (!found) {
3893                     this.downObjects[i].triggerEventHandlers(['touchup', 'up'], [evt]);
3894                     if (!Type.exists(this.downObjects[i].coords)) {
3895                         // snapTo methods have to be called e.g. for line elements here.
3896                         // For coordsElements there might be a conflict with
3897                         // attractors, see commit from 2022.04.08, 11:12:18.
3898                         this.downObjects[i].snapToGrid();
3899                         this.downObjects[i].snapToPoints();
3900                         updateNeeded = true;
3901                     }
3902                     this.downObjects.splice(i, 1);
3903                 }
3904             }
3905 
3906             if (!evtTouches || evtTouches.length === 0) {
3907                 if (this.hasTouchEnd) {
3908                     Env.removeEvent(this.document, 'touchend', this.touchEndListener, this);
3909                     this.hasTouchEnd = false;
3910                 }
3911 
3912                 this.dehighlightAll();
3913                 this.updateQuality = this.BOARD_QUALITY_HIGH;
3914 
3915                 this.originMoveEnd();
3916                 if (updateNeeded) {
3917                     this.update();
3918                 }
3919             }
3920 
3921             return true;
3922         },
3923 
3924         /**
3925          * This method is called by the browser when the mouse button is clicked.
3926          * @param {Event} evt The browsers event object.
3927          * @returns {Boolean} True if no element is found under the current mouse pointer, false otherwise.
3928          */
3929         mouseDownListener: function (evt) {
3930             var pos, elements, result;
3931 
3932             // prevent accidental selection of text
3933             if (this.document.selection && Type.isFunction(this.document.selection.empty)) {
3934                 this.document.selection.empty();
3935             } else if (window.getSelection) {
3936                 window.getSelection().removeAllRanges();
3937             }
3938 
3939             if (!this.hasMouseUp) {
3940                 Env.addEvent(this.document, 'mouseup', this.mouseUpListener, this);
3941                 this.hasMouseUp = true;
3942             } else {
3943                 // In case this.hasMouseUp==true, it may be that there was a
3944                 // mousedown event before which was not followed by an mouseup event.
3945                 // This seems to happen with interactive whiteboard pens sometimes.
3946                 return;
3947             }
3948 
3949             this._inputDevice = 'mouse';
3950             this.options.precision.hasPoint = this.options.precision.mouse;
3951             pos = this.getMousePosition(evt);
3952 
3953             // selection
3954             this._testForSelection(evt);
3955             if (this.selectingMode) {
3956                 this._startSelecting(pos);
3957                 this.triggerEventHandlers(['mousestartselecting', 'startselecting'], [evt]);
3958                 return; // don't continue as a normal click
3959             }
3960 
3961             elements = this.initMoveObject(pos[0], pos[1], evt, 'mouse');
3962 
3963             // if no draggable object can be found, get out here immediately
3964             if (elements.length === 0) {
3965                 this.mode = this.BOARD_MODE_NONE;
3966                 result = true;
3967             } else {
3968                 this.mouse = {
3969                     obj: null,
3970                     targets: [
3971                         {
3972                             X: pos[0],
3973                             Y: pos[1],
3974                             Xprev: NaN,
3975                             Yprev: NaN
3976                         }
3977                     ]
3978                 };
3979                 this.mouse.obj = elements[elements.length - 1];
3980 
3981                 this.dehighlightAll();
3982                 this.mouse.obj.highlight(true);
3983 
3984                 this.mouse.targets[0].Xstart = [];
3985                 this.mouse.targets[0].Ystart = [];
3986                 this.mouse.targets[0].Zstart = [];
3987 
3988                 this.saveStartPos(this.mouse.obj, this.mouse.targets[0]);
3989 
3990                 // prevent accidental text selection
3991                 // this could get us new trouble: input fields, links and drop down boxes placed as text
3992                 // on the board don't work anymore.
3993                 if (evt && evt.preventDefault) {
3994                     evt.preventDefault();
3995                 } else if (window.event) {
3996                     window.event.returnValue = false;
3997                 }
3998             }
3999 
4000             if (this.mode === this.BOARD_MODE_NONE) {
4001                 result = this.mouseOriginMoveStart(evt);
4002             }
4003 
4004             this.initSketchCurve(evt);
4005             this.triggerEventHandlers(['mousedown', 'down'], [evt]);
4006 
4007             return result;
4008         },
4009 
4010         /**
4011          * This method is called by the browser when the mouse is moved.
4012          * @param {Event} evt The browsers event object.
4013          */
4014         mouseMoveListener: function (evt) {
4015             var pos;
4016 
4017             if (!this.checkFrameRate(evt)) {
4018                 return false;
4019             }
4020 
4021             pos = this.getMousePosition(evt);
4022 
4023             this.updateQuality = this.BOARD_QUALITY_LOW;
4024 
4025             if (this.mode !== this.BOARD_MODE_DRAG) {
4026                 this.dehighlightAll();
4027                 this.displayInfobox(false);
4028             }
4029 
4030             // we have to check for four cases:
4031             //   * user moves origin
4032             //   * user drags an object
4033             //   * user just moves the mouse, here highlight all elements at
4034             //     the current mouse position
4035             //   * the user is selecting
4036 
4037             // selection
4038             if (this.selectingMode) {
4039                 this._moveSelecting(pos);
4040                 this.triggerEventHandlers(
4041                     ['mousemoveselecting', 'moveselecting'],
4042                     [evt, this.mode]
4043                 );
4044             } else if (!this.mouseOriginMove(evt)) {
4045 
4046                 this.addToSketchCurve(evt);
4047 
4048                 if (this.mode === this.BOARD_MODE_DRAG) {
4049                     this.moveObject(pos[0], pos[1], this.mouse, evt, 'mouse');
4050                 } else {
4051                     // BOARD_MODE_NONE
4052                     // Move event without dragging an element
4053                     this.highlightElements(pos[0], pos[1], evt, -1);
4054                 }
4055                 this.triggerEventHandlers(['mousemove', 'move'], [evt, this.mode]);
4056             }
4057             this.updateQuality = this.BOARD_QUALITY_HIGH;
4058         },
4059 
4060         /**
4061          * This method is called by the browser when the mouse button is released.
4062          * @param {Event} evt
4063          */
4064         mouseUpListener: function (evt) {
4065             var i;
4066 
4067             if (this.selectingMode === false) {
4068                 this.triggerEventHandlers(['mouseup', 'up'], [evt]);
4069             }
4070 
4071             // redraw with high precision
4072             this.updateQuality = this.BOARD_QUALITY_HIGH;
4073 
4074             if (this.mouse && this.mouse.obj) {
4075                 if (!Type.exists(this.mouse.obj.coords)) {
4076                     // snapTo methods have to be called e.g. for line elements here.
4077                     // For coordsElements there might be a conflict with
4078                     // attractors, see commit from 2022.04.08, 11:12:18.
4079                     // The parameter is needed for lines with snapToGrid enabled
4080                     this.mouse.obj.snapToGrid(this.mouse.targets[0]);
4081                     this.mouse.obj.snapToPoints();
4082                 }
4083             }
4084 
4085             this.finalizeSketchCurve(evt);
4086             this.originMoveEnd();
4087             this.dehighlightAll();
4088             this.update();
4089 
4090             // selection
4091             if (this.selectingMode) {
4092                 this._stopSelecting(evt);
4093                 this.triggerEventHandlers(['mousestopselecting', 'stopselecting'], [evt]);
4094                 this.stopSelectionMode();
4095             } else {
4096                 for (i = 0; i < this.downObjects.length; i++) {
4097                     this.downObjects[i].triggerEventHandlers(['mouseup', 'up'], [evt]);
4098                 }
4099             }
4100 
4101             this.downObjects.length = 0;
4102 
4103             if (this.hasMouseUp) {
4104                 Env.removeEvent(this.document, 'mouseup', this.mouseUpListener, this);
4105                 this.hasMouseUp = false;
4106             }
4107 
4108             // release dragged mouse object
4109             this.mouse = null;
4110         },
4111 
4112         /**
4113          * Handler for mouse wheel events. Used to zoom in and out of the board.
4114          * @param {Event} evt
4115          * @returns {Boolean}
4116          */
4117         mouseWheelListener: function (evt) {
4118             var wd, zoomCenter, pos;
4119 
4120             if (!this.attr.zoom.enabled ||
4121                 !this.attr.zoom.wheel ||
4122                 !this._isRequiredKeyPressed(evt, 'zoom')) {
4123 
4124                 return true;
4125             }
4126 
4127             evt = evt || window.event;
4128             wd = evt.detail ? -evt.detail : evt.wheelDelta / 40;
4129             zoomCenter = this.attr.zoom.center;
4130 
4131             if (zoomCenter === 'board') {
4132                 pos = [];
4133             } else { // including zoomCenter === 'auto'
4134                 pos = new Coords(Const.COORDS_BY_SCREEN, this.getMousePosition(evt), this).usrCoords;
4135             }
4136 
4137             // pos == [] does not throw an error
4138             if (wd > 0) {
4139                 this.zoomIn(pos[1], pos[2]);
4140             } else {
4141                 this.zoomOut(pos[1], pos[2]);
4142             }
4143 
4144             this.triggerEventHandlers(['mousewheel'], [evt]);
4145 
4146             evt.preventDefault();
4147             return false;
4148         },
4149 
4150         /**
4151          * Allow moving of JSXGraph elements with arrow keys.
4152          * The selection of the element is done with the tab key. For this,
4153          * the attribute 'tabindex' of the element has to be set to some number (default=0).
4154          * tabindex corresponds to the HTML and SVG attribute of the same name.
4155          * <p>
4156          * Panning of the construction is done with arrow keys
4157          * if the pan key (shift or ctrl - depending on the board attributes) is pressed.
4158          * <p>
4159          * Zooming is triggered with the keys +, o, -, if
4160          * the pan key (shift or ctrl - depending on the board attributes) is pressed.
4161          * <p>
4162          * Keyboard control (move, pan, and zoom) is disabled if an HTML element of type input or textarea has received focus.
4163          *
4164          * @param  {Event} evt The browser's event object
4165          *
4166          * @see JXG.Board#keyboard
4167          * @see JXG.Board#keyFocusInListener
4168          * @see JXG.Board#keyFocusOutListener
4169          *
4170          */
4171         keyDownListener: function (evt) {
4172             var id_node = evt.target.id,
4173                 id, el, res, doc,
4174                 sX = 0,
4175                 sY = 0,
4176                 // dx, dy are provided in screen units and
4177                 // are converted to user coordinates
4178                 dx = Type.evaluate(this.attr.keyboard.dx) / this.unitX,
4179                 dy = Type.evaluate(this.attr.keyboard.dy) / this.unitY,
4180                 // u = 100,
4181                 doZoom = false,
4182                 done = true,
4183                 dir,
4184                 actPos;
4185 
4186             if (!this.attr.keyboard.enabled || id_node === '') {
4187                 return false;
4188             }
4189 
4190             // Tab key should be handled by the browser
4191             if (evt.keyCode === 9) {
4192                 return false;
4193             }
4194 
4195             // dx = Math.round(dx * u) / u;
4196             // dy = Math.round(dy * u) / u;
4197 
4198             // An element of type input or textarea has focus, get out of here.
4199             doc = this.containerObj.shadowRoot || document;
4200             if (doc.activeElement) {
4201                 el = doc.activeElement;
4202                 if (el.tagName === 'INPUT' || el.tagName === 'textarea') {
4203                     return false;
4204                 }
4205             }
4206 
4207             // Get the JSXGraph id from the id of the SVG node.
4208             id = id_node.replace(this.containerObj.id + '_', '');
4209             el = this.select(id);
4210 
4211             if (Type.exists(el.coords)) {
4212                 actPos = el.coords.usrCoords.slice(1);
4213             }
4214 
4215             if (
4216                 (Type.evaluate(this.attr.keyboard.panshift) && evt.shiftKey) ||
4217                 (Type.evaluate(this.attr.keyboard.panctrl) && evt.ctrlKey)
4218             ) {
4219                 // Pan key has been pressed
4220 
4221                 if (Type.evaluate(this.attr.zoom.enabled) === true) {
4222                     doZoom = true;
4223                 }
4224 
4225                 // Arrow keys
4226                 if (evt.keyCode === 38) {
4227                     // up
4228                     this.clickUpArrow();
4229                 } else if (evt.keyCode === 40) {
4230                     // down
4231                     this.clickDownArrow();
4232                 } else if (evt.keyCode === 37) {
4233                     // left
4234                     this.clickLeftArrow();
4235                 } else if (evt.keyCode === 39) {
4236                     // right
4237                     this.clickRightArrow();
4238 
4239                     // Zoom keys
4240                 } else if (doZoom && evt.keyCode === 171) {
4241                     // +
4242                     this.zoomIn();
4243                 } else if (doZoom && evt.keyCode === 173) {
4244                     // -
4245                     this.zoomOut();
4246                 } else if (doZoom && evt.keyCode === 79) {
4247                     // o
4248                     this.zoom100();
4249                 } else {
4250                     done = false;
4251                 }
4252             } else if (!evt.shiftKey && !evt.ctrlKey) {         // Move an element if neither shift or ctrl are pressed
4253                 // Adapt dx, dy to snapToGrid and attractToGrid.
4254                 // snapToGrid has priority.
4255                 if (Type.exists(el.visProp)) {
4256                     if (
4257                         Type.exists(el.visProp.snaptogrid) &&
4258                         el.visProp.snaptogrid &&
4259                         el.evalVisProp('snapsizex') &&
4260                         el.evalVisProp('snapsizey')
4261                     ) {
4262                         // Adapt dx, dy such that snapToGrid is possible
4263                         res = el.getSnapSizes();
4264                         sX = res[0];
4265                         sY = res[1];
4266                         // If snaptogrid is true,
4267                         // we can only jump from grid point to grid point.
4268                         dx = sX;
4269                         dy = sY;
4270                     } else if (
4271                         Type.exists(el.visProp.attracttogrid) &&
4272                         el.visProp.attracttogrid &&
4273                         el.evalVisProp('attractordistance') &&
4274                         el.evalVisProp('attractorunit')
4275                     ) {
4276                         // Adapt dx, dy such that attractToGrid is possible
4277                         sX = 1.1 * el.evalVisProp('attractordistance');
4278                         sY = sX;
4279 
4280                         if (el.evalVisProp('attractorunit') === 'screen') {
4281                             sX /= this.unitX;
4282                             sY /= this.unitX;
4283                         }
4284                         dx = Math.max(sX, dx);
4285                         dy = Math.max(sY, dy);
4286                     }
4287                 }
4288 
4289                 if (evt.keyCode === 38) {
4290                     // up
4291                     dir = [0, dy];
4292                 } else if (evt.keyCode === 40) {
4293                     // down
4294                     dir = [0, -dy];
4295                 } else if (evt.keyCode === 37) {
4296                     // left
4297                     dir = [-dx, 0];
4298                 } else if (evt.keyCode === 39) {
4299                     // right
4300                     dir = [dx, 0];
4301                 } else {
4302                     done = false;
4303                 }
4304 
4305                 if (dir && el.isDraggable &&
4306                     el.visPropCalc.visible &&
4307                     ((this.geonextCompatibilityMode &&
4308                         (Type.isPoint(el) ||
4309                             el.elementClass === Const.OBJECT_CLASS_TEXT)
4310                     ) || !this.geonextCompatibilityMode) &&
4311                     !el.evalVisProp('fixed')
4312                 ) {
4313                     this.mode = this.BOARD_MODE_DRAG;
4314                     if (Type.exists(el.coords)) {
4315                         dir[0] += actPos[0];
4316                         dir[1] += actPos[1];
4317                     }
4318                     // For coordsElement setPosition has to call setPositionDirectly.
4319                     // Otherwise the position is set by a translation.
4320                     if (Type.exists(el.coords)) {
4321                         el.setPosition(JXG.COORDS_BY_USER, dir);
4322                         this.updateInfobox(el);
4323                     } else {
4324                         this.displayInfobox(false);
4325                         el.setPositionDirectly(
4326                             Const.COORDS_BY_USER,
4327                             dir,
4328                             [0, 0]
4329                         );
4330                     }
4331 
4332                     this.triggerEventHandlers(['keymove', 'move'], [evt, this.mode]);
4333                     el.triggerEventHandlers(['keydrag', 'drag'], [evt]);
4334                     this.mode = this.BOARD_MODE_NONE;
4335                 }
4336             }
4337 
4338             this.update();
4339 
4340             if (done && Type.exists(evt.preventDefault)) {
4341                 evt.preventDefault();
4342             }
4343             return done;
4344         },
4345 
4346         /**
4347          * Event listener for SVG elements getting focus.
4348          * This is needed for highlighting when using keyboard control.
4349          * Only elements having the attribute 'tabindex' can receive focus.
4350          *
4351          * @see JXG.Board#keyFocusOutListener
4352          * @see JXG.Board#keyDownListener
4353          * @see JXG.Board#keyboard
4354          *
4355          * @param  {Event} evt The browser's event object
4356          */
4357         keyFocusInListener: function (evt) {
4358             var id_node = evt.target.id,
4359                 id,
4360                 el;
4361 
4362             if (!this.attr.keyboard.enabled || id_node === '') {
4363                 return false;
4364             }
4365 
4366             // Get JSXGraph id from node id
4367             id = id_node.replace(this.containerObj.id + '_', '');
4368             el = this.select(id);
4369             if (Type.exists(el.highlight)) {
4370                 el.highlight(true);
4371                 this.focusObjects = [id];
4372                 el.triggerEventHandlers(['hit'], [evt]);
4373             }
4374             if (Type.exists(el.coords)) {
4375                 this.updateInfobox(el);
4376             }
4377         },
4378 
4379         /**
4380          * Event listener for SVG elements losing focus.
4381          * This is needed for dehighlighting when using keyboard control.
4382          * Only elements having the attribute 'tabindex' can receive focus.
4383          *
4384          * @see JXG.Board#keyFocusInListener
4385          * @see JXG.Board#keyDownListener
4386          * @see JXG.Board#keyboard
4387          *
4388          * @param  {Event} evt The browser's event object
4389          */
4390         keyFocusOutListener: function (evt) {
4391             if (!this.attr.keyboard.enabled) {
4392                 return false;
4393             }
4394             this.focusObjects = []; // This has to be before displayInfobox(false)
4395             this.dehighlightAll();
4396             this.displayInfobox(false);
4397         },
4398 
4399         /**
4400          * Update the width and height of the JSXGraph container div element.
4401          * If width and height are not supplied, read actual values with offsetWidth/Height,
4402          * and call board.resizeContainer() with this values.
4403          * <p>
4404          * If necessary, also call setBoundingBox().
4405          * @param {Number} [width=this.containerObj.offsetWidth] Width of the container element
4406          * @param {Number} [height=this.containerObj.offsetHeight] Height of the container element
4407          * @returns {JXG.Board} Reference to the board
4408          *
4409          * @see JXG.Board#startResizeObserver
4410          * @see JXG.Board#resizeListener
4411          * @see JXG.Board#resizeContainer
4412          * @see JXG.Board#setBoundingBox
4413          *
4414          */
4415         updateContainerDims: function (width, height) {
4416             var w = width,
4417                 h = height,
4418                 // bb,
4419                 css,
4420                 width_adjustment, height_adjustment;
4421 
4422             if (width === undefined) {
4423                 // Get size of the board's container div
4424                 //
4425                 // offsetWidth/Height ignores CSS transforms,
4426                 // getBoundingClientRect includes CSS transforms
4427                 //
4428                 // bb = this.containerObj.getBoundingClientRect();
4429                 // w = bb.width;
4430                 // h = bb.height;
4431                 w = this.containerObj.offsetWidth;
4432                 h = this.containerObj.offsetHeight;
4433             }
4434 
4435             if (width === undefined && window && window.getComputedStyle) {
4436                 // Subtract the border size
4437                 css = window.getComputedStyle(this.containerObj, null);
4438                 width_adjustment = parseFloat(css.getPropertyValue('border-left-width')) + parseFloat(css.getPropertyValue('border-right-width'));
4439                 if (!isNaN(width_adjustment)) {
4440                     w -= width_adjustment;
4441                 }
4442                 height_adjustment = parseFloat(css.getPropertyValue('border-top-width')) + parseFloat(css.getPropertyValue('border-bottom-width'));
4443                 if (!isNaN(height_adjustment)) {
4444                     h -= height_adjustment;
4445                 }
4446             }
4447 
4448             // If div is invisible - do nothing
4449             if (w <= 0 || h <= 0 || isNaN(w) || isNaN(h)) {
4450                 return this;
4451             }
4452 
4453             // If bounding box is not yet initialized, do it now.
4454             if (isNaN(this.getBoundingBox()[0])) {
4455                 this.setBoundingBox(this.attr.boundingbox, this.keepaspectratio, 'keep');
4456             }
4457 
4458             // Do nothing if the dimension did not change since being visible
4459             // the last time. Note that if the div had display:none in the mean time,
4460             // we did not store this._prevDim.
4461             if (Type.exists(this._prevDim) && this._prevDim.w === w && this._prevDim.h === h) {
4462                 return this;
4463             }
4464             // Set the size of the SVG or canvas element
4465             this.resizeContainer(w, h, true);
4466             this._prevDim = {
4467                 w: w,
4468                 h: h
4469             };
4470             return this;
4471         },
4472 
4473         /**
4474          * Start observer which reacts to size changes of the JSXGraph
4475          * container div element. Calls updateContainerDims().
4476          * If not available, an event listener for the window-resize event is started.
4477          * On mobile devices also scrolling might trigger resizes.
4478          * However, resize events triggered by scrolling events should be ignored.
4479          * Therefore, also a scrollListener is started.
4480          * Resize can be controlled with the board attribute resize.
4481          *
4482          * @see JXG.Board#updateContainerDims
4483          * @see JXG.Board#resizeListener
4484          * @see JXG.Board#scrollListener
4485          * @see JXG.Board#resize
4486          *
4487          */
4488         startResizeObserver: function () {
4489             var that = this;
4490 
4491             if (!Env.isBrowser || !this.attr.resize || !this.attr.resize.enabled) {
4492                 return;
4493             }
4494 
4495             this.resizeObserver = new ResizeObserver(function (entries) {
4496                 var bb;
4497                 if (!that._isResizing) {
4498                     that._isResizing = true;
4499                     bb = entries[0].contentRect;
4500                     window.setTimeout(function () {
4501                         try {
4502                             that.updateContainerDims(bb.width, bb.height);
4503                         } catch (e) {
4504                             JXG.debug(e);   // Used to log errors during board.update()
4505                             that.stopResizeObserver();
4506                         } finally {
4507                             that._isResizing = false;
4508                         }
4509                     }, that.attr.resize.throttle);
4510                 }
4511             });
4512             this.resizeObserver.observe(this.containerObj);
4513         },
4514 
4515         /**
4516          * Stops the resize observer.
4517          * @see JXG.Board#startResizeObserver
4518          *
4519          */
4520         stopResizeObserver: function () {
4521             if (!Env.isBrowser || !this.attr.resize || !this.attr.resize.enabled) {
4522                 return;
4523             }
4524 
4525             if (Type.exists(this.resizeObserver)) {
4526                 this.resizeObserver.unobserve(this.containerObj);
4527             }
4528         },
4529 
4530         /**
4531          * Fallback solutions if there is no resizeObserver available in the browser.
4532          * Reacts to resize events of the window (only). Otherwise similar to
4533          * startResizeObserver(). To handle changes of the visibility
4534          * of the JSXGraph container element, additionally an intersection observer is used.
4535          * which watches changes in the visibility of the JSXGraph container element.
4536          * This is necessary e.g. for register tabs or dia shows.
4537          *
4538          * @see JXG.Board#startResizeObserver
4539          * @see JXG.Board#startIntersectionObserver
4540          */
4541         resizeListener: function () {
4542             var that = this;
4543 
4544             if (!Env.isBrowser || !this.attr.resize || !this.attr.resize.enabled) {
4545                 return;
4546             }
4547             if (!this._isScrolling && !this._isResizing) {
4548                 this._isResizing = true;
4549                 window.setTimeout(function () {
4550                     that.updateContainerDims();
4551                     that._isResizing = false;
4552                 }, this.attr.resize.throttle);
4553             }
4554         },
4555 
4556         /**
4557          * Listener to watch for scroll events. Sets board._isScrolling = true
4558          * @param  {Event} evt The browser's event object
4559          *
4560          * @see JXG.Board#startResizeObserver
4561          * @see JXG.Board#resizeListener
4562          *
4563          */
4564         scrollListener: function (evt) {
4565             var that = this;
4566 
4567             if (!Env.isBrowser) {
4568                 return;
4569             }
4570             if (!this._isScrolling) {
4571                 this._isScrolling = true;
4572                 window.setTimeout(function () {
4573                     that._isScrolling = false;
4574                 }, 66);
4575             }
4576         },
4577 
4578         /**
4579          * Watch for changes of the visibility of the JSXGraph container element.
4580          *
4581          * @see JXG.Board#startResizeObserver
4582          * @see JXG.Board#resizeListener
4583          *
4584          */
4585         startIntersectionObserver: function () {
4586             var that = this,
4587                 options = {
4588                     root: null,
4589                     rootMargin: '0px',
4590                     threshold: 0.8
4591                 };
4592 
4593             try {
4594                 this.intersectionObserver = new IntersectionObserver(function (entries) {
4595                     // If bounding box is not yet initialized, do it now.
4596                     if (isNaN(that.getBoundingBox()[0])) {
4597                         that.updateContainerDims();
4598                     }
4599                 }, options);
4600                 this.intersectionObserver.observe(that.containerObj);
4601             } catch (err) {
4602                 JXG.debug('JSXGraph: IntersectionObserver not available in this browser.');
4603             }
4604         },
4605 
4606         /**
4607          * Stop the intersection observer
4608          *
4609          * @see JXG.Board#startIntersectionObserver
4610          *
4611          */
4612         stopIntersectionObserver: function () {
4613             if (Type.exists(this.intersectionObserver)) {
4614                 this.intersectionObserver.unobserve(this.containerObj);
4615             }
4616         },
4617 
4618         /**
4619          * Update the container before and after printing.
4620          * @param {Event} [evt]
4621          */
4622         printListener: function(evt) {
4623             this.updateContainerDims();
4624         },
4625 
4626         /**
4627          * Wrapper for printListener to be used in mediaQuery matches.
4628          * @param {MediaQueryList} mql
4629          */
4630         printListenerMatch: function (mql) {
4631             if (mql.matches) {
4632                 this.printListener();
4633             }
4634         },
4635 
4636         /**********************************************************
4637          *
4638          * End of Event Handlers
4639          *
4640          **********************************************************/
4641 
4642         /**
4643          * Initialize the info box object which is used to display
4644          * the coordinates of points near the mouse pointer,
4645          * @returns {JXG.Board} Reference to the board
4646          */
4647         initInfobox: function (attributes) {
4648             var attr = Type.copyAttributes(attributes, this.options, 'infobox');
4649 
4650             attr.id = this.id + '_infobox';
4651 
4652             /**
4653              * Infobox close to points in which the points' coordinates are displayed.
4654              * This is simply a JXG.Text element. Access through board.infobox.
4655              * Uses CSS class .JXGinfobox.
4656              *
4657              * @namespace
4658              * @name JXG.Board.infobox
4659              * @type JXG.Text
4660              *
4661              * @example
4662              * const board = JXG.JSXGraph.initBoard(BOARDID, {
4663              *     boundingbox: [-0.5, 0.5, 0.5, -0.5],
4664              *     intl: {
4665              *         enabled: false,
4666              *         locale: 'de-DE'
4667              *     },
4668              *     keepaspectratio: true,
4669              *     axis: true,
4670              *     infobox: {
4671              *         distanceY: 40,
4672              *         intl: {
4673              *             enabled: true,
4674              *             options: {
4675              *                 minimumFractionDigits: 1,
4676              *                 maximumFractionDigits: 2
4677              *             }
4678              *         }
4679              *     }
4680              * });
4681              * var p = board.create('point', [0.1, 0.1], {});
4682              *
4683              * </pre><div id="JXG822161af-fe77-4769-850f-cdf69935eab0" class="jxgbox" style="width: 300px; height: 300px;"></div>
4684              * <script type="text/javascript">
4685              *     (function() {
4686              *     const board = JXG.JSXGraph.initBoard('JXG822161af-fe77-4769-850f-cdf69935eab0', {
4687              *         boundingbox: [-0.5, 0.5, 0.5, -0.5], showcopyright: false, shownavigation: false,
4688              *         intl: {
4689              *             enabled: false,
4690              *             locale: 'de-DE'
4691              *         },
4692              *         keepaspectratio: true,
4693              *         axis: true,
4694              *         infobox: {
4695              *             distanceY: 40,
4696              *             intl: {
4697              *                 enabled: true,
4698              *                 options: {
4699              *                     minimumFractionDigits: 1,
4700              *                     maximumFractionDigits: 2
4701              *                 }
4702              *             }
4703              *         }
4704              *     });
4705              *     var p = board.create('point', [0.1, 0.1], {});
4706              *     })();
4707              *
4708              * </script><pre>
4709              *
4710              */
4711             this.infobox = this.create('text', [0, 0, '0,0'], attr);
4712             // this.infobox.needsUpdateSize = false;  // That is not true, but it speeds drawing up.
4713             this.infobox.dump = false;
4714 
4715             this.displayInfobox(false);
4716             return this;
4717         },
4718 
4719         /**
4720          * Updates and displays a little info box to show coordinates of current selected points.
4721          * @param {JXG.GeometryElement} el A GeometryElement
4722          * @returns {JXG.Board} Reference to the board
4723          * @see JXG.Board#displayInfobox
4724          * @see JXG.Board#showInfobox
4725          * @see Point#showInfobox
4726          *
4727          */
4728         updateInfobox: function (el) {
4729             var x, y, xc, yc,
4730                 vpinfoboxdigits,
4731                 distX, distY,
4732                 vpsi = el.evalVisProp('showinfobox');
4733 
4734             if ((!Type.evaluate(this.attr.showinfobox) && vpsi === 'inherit') || !vpsi) {
4735                 return this;
4736             }
4737 
4738             if (Type.isPoint(el)) {
4739                 xc = el.coords.usrCoords[1];
4740                 yc = el.coords.usrCoords[2];
4741                 distX = this.infobox.evalVisProp('distancex');
4742                 distY = this.infobox.evalVisProp('distancey');
4743 
4744                 this.infobox.setCoords(
4745                     xc + distX / this.unitX,
4746                     yc + distY / this.unitY
4747                 );
4748 
4749                 vpinfoboxdigits = el.evalVisProp('infoboxdigits');
4750                 if (typeof el.infoboxText !== 'string') {
4751                     if (vpinfoboxdigits === 'auto') {
4752                         if (this.infobox.useLocale()) {
4753                             x = this.infobox.formatNumberLocale(xc);
4754                             y = this.infobox.formatNumberLocale(yc);
4755                         } else {
4756                             x = Type.autoDigits(xc);
4757                             y = Type.autoDigits(yc);
4758                         }
4759                     } else if (Type.isNumber(vpinfoboxdigits)) {
4760                         if (this.infobox.useLocale()) {
4761                             x = this.infobox.formatNumberLocale(xc, vpinfoboxdigits);
4762                             y = this.infobox.formatNumberLocale(yc, vpinfoboxdigits);
4763                         } else {
4764                             x = Type.toFixed(xc, vpinfoboxdigits);
4765                             y = Type.toFixed(yc, vpinfoboxdigits);
4766                         }
4767 
4768                     } else {
4769                         x = xc;
4770                         y = yc;
4771                     }
4772 
4773                     this.highlightInfobox(x, y, el);
4774                 } else {
4775                     this.highlightCustomInfobox(el.infoboxText, el);
4776                 }
4777 
4778                 this.displayInfobox(true);
4779             }
4780             return this;
4781         },
4782 
4783         /**
4784          * Set infobox visible / invisible.
4785          *
4786          * It uses its property hiddenByParent to memorize its status.
4787          * In this way, many DOM access can be avoided.
4788          *
4789          * @param  {Boolean} val true for visible, false for invisible
4790          * @returns {JXG.Board} Reference to the board.
4791          * @see JXG.Board#updateInfobox
4792          *
4793          */
4794         displayInfobox: function (val) {
4795             if (!val && this.focusObjects.length > 0 &&
4796                 this.select(this.focusObjects[0]).elementClass === Const.OBJECT_CLASS_POINT) {
4797                 // If an element has focus we do not hide its infobox
4798                 return this;
4799             }
4800             if (this.infobox.hiddenByParent === val) {
4801                 this.infobox.hiddenByParent = !val;
4802                 this.infobox.prepareUpdate().updateVisibility(val).updateRenderer();
4803             }
4804             return this;
4805         },
4806 
4807         // Alias for displayInfobox to be backwards compatible.
4808         // The method showInfobox clashes with the board attribute showInfobox
4809         showInfobox: function (val) {
4810             return this.displayInfobox(val);
4811         },
4812 
4813         /**
4814          * Changes the text of the info box to show the given coordinates.
4815          * @param {Number} x
4816          * @param {Number} y
4817          * @param {JXG.GeometryElement} [el] The element the mouse is pointing at
4818          * @returns {JXG.Board} Reference to the board.
4819          */
4820         highlightInfobox: function (x, y, el) {
4821             this.highlightCustomInfobox('(' + x + ', ' + y + ')', el);
4822             return this;
4823         },
4824 
4825         /**
4826          * Changes the text of the info box to what is provided via text.
4827          * @param {String} text
4828          * @param {JXG.GeometryElement} [el]
4829          * @returns {JXG.Board} Reference to the board.
4830          */
4831         highlightCustomInfobox: function (text, el) {
4832             this.infobox.setText(text);
4833             return this;
4834         },
4835 
4836         /**
4837          * Remove highlighting of all elements.
4838          * @returns {JXG.Board} Reference to the board.
4839          */
4840         dehighlightAll: function () {
4841             var el,
4842                 pEl,
4843                 stillHighlighted = {},
4844                 needsDeHighlight = false;
4845 
4846             for (el in this.highlightedObjects) {
4847                 if (this.highlightedObjects.hasOwnProperty(el)) {
4848 
4849                     pEl = this.highlightedObjects[el];
4850                     if (this.focusObjects.indexOf(el) < 0) { // Element does not have focus
4851                         if (this.hasMouseHandlers || this.hasPointerHandlers) {
4852                             pEl.noHighlight();
4853                         }
4854                         needsDeHighlight = true;
4855                     } else {
4856                         stillHighlighted[el] = pEl;
4857                     }
4858                     // In highlightedObjects should only be objects which fulfill all these conditions
4859                     // And in case of complex elements, like a turtle based fractal, it should be faster to
4860                     // just de-highlight the element instead of checking hasPoint...
4861                     // if ((!Type.exists(pEl.hasPoint)) || !pEl.hasPoint(x, y) || !pEl.visPropCalc.visible)
4862                 }
4863             }
4864 
4865             this.highlightedObjects = stillHighlighted;
4866 
4867             // We do not need to redraw during dehighlighting in CanvasRenderer
4868             // because we are redrawing anyhow
4869             //  -- We do need to redraw during dehighlighting. Otherwise objects won't be dehighlighted until
4870             // another object is highlighted.
4871             if (this.renderer.type === 'canvas' && needsDeHighlight) {
4872                 this.prepareUpdate();
4873                 this.renderer.suspendRedraw(this);
4874                 this.updateRenderer();
4875                 this.renderer.unsuspendRedraw();
4876             }
4877 
4878             return this;
4879         },
4880 
4881         /**
4882          * Returns the input parameters in an array. This method looks pointless and it really is, but it had a purpose
4883          * once.
4884          * @private
4885          * @param {Number} x X coordinate in screen coordinates
4886          * @param {Number} y Y coordinate in screen coordinates
4887          * @returns {Array} Coordinates [x, y] of the mouse in screen coordinates.
4888          * @see JXG.Board#getUsrCoordsOfMouse
4889          */
4890         getScrCoordsOfMouse: function (x, y) {
4891             return [x, y];
4892         },
4893 
4894         /**
4895          * This method calculates the user coords of the current mouse coordinates.
4896          * @param {Event} evt Event object containing the mouse coordinates.
4897          * @returns {Array} Coordinates [x, y] of the mouse in user coordinates.
4898          * @example
4899          * board.on('up', function (evt) {
4900          *         var a = board.getUsrCoordsOfMouse(evt),
4901          *             x = a[0],
4902          *             y = a[1],
4903          *             somePoint = board.create('point', [x,y], {name:'SomePoint',size:4});
4904          *             // Shorter version:
4905          *             //somePoint = board.create('point', a, {name:'SomePoint',size:4});
4906          *         });
4907          *
4908          * </pre><div id='JXG48d5066b-16ba-4920-b8ea-a4f8eff6b746' class='jxgbox' style='width: 300px; height: 300px;'></div>
4909          * <script type='text/javascript'>
4910          *     (function() {
4911          *         var board = JXG.JSXGraph.initBoard('JXG48d5066b-16ba-4920-b8ea-a4f8eff6b746',
4912          *             {boundingbox: [-8, 8, 8,-8], axis: true, showcopyright: false, shownavigation: false});
4913          *     board.on('up', function (evt) {
4914          *             var a = board.getUsrCoordsOfMouse(evt),
4915          *                 x = a[0],
4916          *                 y = a[1],
4917          *                 somePoint = board.create('point', [x,y], {name:'SomePoint',size:4});
4918          *                 // Shorter version:
4919          *                 //somePoint = board.create('point', a, {name:'SomePoint',size:4});
4920          *             });
4921          *
4922          *     })();
4923          *
4924          * </script><pre>
4925          *
4926          * @see JXG.Board#getScrCoordsOfMouse
4927          * @see JXG.Board#getAllUnderMouse
4928          */
4929         getUsrCoordsOfMouse: function (evt) {
4930             var cPos = this.getCoordsTopLeftCorner(),
4931                 absPos = Env.getPosition(evt, null, this.document),
4932                 x = absPos[0] - cPos[0],
4933                 y = absPos[1] - cPos[1],
4934                 newCoords = new Coords(Const.COORDS_BY_SCREEN, [x, y], this);
4935 
4936             return newCoords.usrCoords.slice(1);
4937         },
4938 
4939         /**
4940          * Collects all elements under current mouse position plus current user coordinates of mouse cursor.
4941          * @param {Event} evt Event object containing the mouse coordinates.
4942          * @returns {Array} Array of elements at the current mouse position plus current user coordinates of mouse.
4943          * @see JXG.Board#getUsrCoordsOfMouse
4944          * @see JXG.Board#getAllObjectsUnderMouse
4945          */
4946         getAllUnderMouse: function (evt) {
4947             var elList = this.getAllObjectsUnderMouse(evt);
4948             elList.push(this.getUsrCoordsOfMouse(evt));
4949 
4950             return elList;
4951         },
4952 
4953         /**
4954          * Collects all elements under current mouse position.
4955          * @param {Event} evt Event object containing the mouse coordinates.
4956          * @returns {Array} Array of elements at the current mouse position.
4957          * @see JXG.Board#getAllUnderMouse
4958          */
4959         getAllObjectsUnderMouse: function (evt) {
4960             var cPos = this.getCoordsTopLeftCorner(),
4961                 absPos = Env.getPosition(evt, null, this.document),
4962                 dx = absPos[0] - cPos[0],
4963                 dy = absPos[1] - cPos[1],
4964                 elList = [],
4965                 el,
4966                 pEl,
4967                 len = this.objectsList.length;
4968 
4969             for (el = 0; el < len; el++) {
4970                 pEl = this.objectsList[el];
4971                 if (pEl.visPropCalc.visible && pEl.hasPoint && pEl.hasPoint(dx, dy)) {
4972                     elList[elList.length] = pEl;
4973                 }
4974             }
4975 
4976             return elList;
4977         },
4978 
4979         /**
4980          * Update the coords object of all elements which possess this
4981          * property. This is necessary after changing the viewport.
4982          * @returns {JXG.Board} Reference to this board.
4983          **/
4984         updateCoords: function () {
4985             var el, ob,
4986                 len = this.objectsList.length;
4987 
4988             for (ob = 0; ob < len; ob++) {
4989                 el = this.objectsList[ob];
4990 
4991                 if (Type.exists(el.coords)) {
4992                     if (el.evalVisProp('frozen') === true) {
4993                         if (el.is3D) {
4994                             el.element2D.coords.screen2usr();
4995                         } else {
4996                             el.coords.screen2usr();
4997                         }
4998                     } else {
4999                         if (el.is3D) {
5000                             el.element2D.coords.usr2screen();
5001                         } else {
5002                             el.coords.usr2screen();
5003                             if (Type.exists(el.actualCoords)) {
5004                                 el.actualCoords.usr2screen();
5005                             }
5006                         }
5007                     }
5008                 }
5009             }
5010             return this;
5011         },
5012 
5013         /**
5014          * Moves the origin and initializes an update of all elements.
5015          * @param {Number} x
5016          * @param {Number} y
5017          * @param {Boolean} [diff=false]
5018          * @returns {JXG.Board} Reference to this board.
5019          */
5020         moveOrigin: function (x, y, diff) {
5021             var ox, oy, ul, lr;
5022             if (Type.exists(x) && Type.exists(y)) {
5023                 ox = this.origin.scrCoords[1];
5024                 oy = this.origin.scrCoords[2];
5025 
5026                 this.origin.scrCoords[1] = x;
5027                 this.origin.scrCoords[2] = y;
5028 
5029                 if (diff) {
5030                     this.origin.scrCoords[1] -= this.drag_dx;
5031                     this.origin.scrCoords[2] -= this.drag_dy;
5032                 }
5033 
5034                 ul = new Coords(Const.COORDS_BY_SCREEN, [0, 0], this).usrCoords;
5035                 lr = new Coords(
5036                     Const.COORDS_BY_SCREEN,
5037                     [this.canvasWidth, this.canvasHeight],
5038                     this
5039                 ).usrCoords;
5040                 if (
5041                     ul[1] < this.maxboundingbox[0] - Mat.eps ||
5042                     ul[2] > this.maxboundingbox[1] + Mat.eps ||
5043                     lr[1] > this.maxboundingbox[2] + Mat.eps ||
5044                     lr[2] < this.maxboundingbox[3] - Mat.eps
5045                 ) {
5046                     this.origin.scrCoords[1] = ox;
5047                     this.origin.scrCoords[2] = oy;
5048                 }
5049             }
5050 
5051             this.updateCoords().clearTraces().fullUpdate();
5052             this.triggerEventHandlers(['boundingbox']);
5053 
5054             return this;
5055         },
5056 
5057         /**
5058          * Add conditional updates to the elements.
5059          * @param {String} str String containing conditional update in geonext syntax
5060          */
5061         addConditions: function (str) {
5062             var term,
5063                 m,
5064                 left,
5065                 right,
5066                 name,
5067                 el,
5068                 property,
5069                 functions = [],
5070                 // plaintext = 'var el, x, y, c, rgbo;\n',
5071                 i = str.indexOf('<data>'),
5072                 j = str.indexOf('<' + '/data>'),
5073                 xyFun = function (board, el, f, what) {
5074                     return function () {
5075                         var e, t;
5076 
5077                         e = board.select(el.id);
5078                         t = e.coords.usrCoords[what];
5079 
5080                         if (what === 2) {
5081                             e.setPositionDirectly(Const.COORDS_BY_USER, [f(), t]);
5082                         } else {
5083                             e.setPositionDirectly(Const.COORDS_BY_USER, [t, f()]);
5084                         }
5085                         e.prepareUpdate().update();
5086                     };
5087                 },
5088                 visFun = function (board, el, f) {
5089                     return function () {
5090                         var e, v;
5091 
5092                         e = board.select(el.id);
5093                         v = f();
5094 
5095                         e.setAttribute({ visible: v });
5096                     };
5097                 },
5098                 colFun = function (board, el, f, what) {
5099                     return function () {
5100                         var e, v;
5101 
5102                         e = board.select(el.id);
5103                         v = f();
5104 
5105                         if (what === 'strokewidth') {
5106                             e.visProp.strokewidth = v;
5107                         } else {
5108                             v = Color.rgba2rgbo(v);
5109                             e.visProp[what + 'color'] = v[0];
5110                             e.visProp[what + 'opacity'] = v[1];
5111                         }
5112                     };
5113                 },
5114                 posFun = function (board, el, f) {
5115                     return function () {
5116                         var e = board.select(el.id);
5117 
5118                         e.position = f();
5119                     };
5120                 },
5121                 styleFun = function (board, el, f) {
5122                     return function () {
5123                         var e = board.select(el.id);
5124 
5125                         e.setStyle(f());
5126                     };
5127                 };
5128 
5129             if (i < 0) {
5130                 return;
5131             }
5132 
5133             while (i >= 0) {
5134                 term = str.slice(i + 6, j); // throw away <data>
5135                 m = term.indexOf('=');
5136                 left = term.slice(0, m);
5137                 right = term.slice(m + 1);
5138                 m = left.indexOf('.');   // Resulting variable names must not contain dots, e.g. ' Steuern akt.'
5139                 name = left.slice(0, m); //.replace(/\s+$/,''); // do NOT cut out name (with whitespace)
5140                 el = this.elementsByName[Type.unescapeHTML(name)];
5141 
5142                 property = left
5143                     .slice(m + 1)
5144                     .replace(/\s+/g, '')
5145                     .toLowerCase(); // remove whitespace in property
5146                 right = Type.createFunction(right, this, '', true);
5147 
5148                 // Debug
5149                 if (!Type.exists(this.elementsByName[name])) {
5150                     JXG.debug('debug conditions: |' + name + '| undefined');
5151                 } else {
5152                     // plaintext += 'el = this.objects[\'' + el.id + '\'];\n';
5153 
5154                     switch (property) {
5155                         case 'x':
5156                             functions.push(xyFun(this, el, right, 2));
5157                             break;
5158                         case 'y':
5159                             functions.push(xyFun(this, el, right, 1));
5160                             break;
5161                         case 'visible':
5162                             functions.push(visFun(this, el, right));
5163                             break;
5164                         case 'position':
5165                             functions.push(posFun(this, el, right));
5166                             break;
5167                         case 'stroke':
5168                             functions.push(colFun(this, el, right, 'stroke'));
5169                             break;
5170                         case 'style':
5171                             functions.push(styleFun(this, el, right));
5172                             break;
5173                         case 'strokewidth':
5174                             functions.push(colFun(this, el, right, 'strokewidth'));
5175                             break;
5176                         case 'fill':
5177                             functions.push(colFun(this, el, right, 'fill'));
5178                             break;
5179                         case 'label':
5180                             break;
5181                         default:
5182                             JXG.debug(
5183                                 'property "' +
5184                                 property +
5185                                 '" in conditions not yet implemented:' +
5186                                 right
5187                             );
5188                             break;
5189                     }
5190                 }
5191                 str = str.slice(j + 7); // cut off '</data>'
5192                 i = str.indexOf('<data>');
5193                 j = str.indexOf('<' + '/data>');
5194             }
5195 
5196             this.updateConditions = function () {
5197                 var i;
5198 
5199                 for (i = 0; i < functions.length; i++) {
5200                     functions[i]();
5201                 }
5202 
5203                 this.prepareUpdate().updateElements();
5204                 return true;
5205             };
5206             this.updateConditions();
5207         },
5208 
5209         /**
5210          * Computes the commands in the conditions-section of the gxt file.
5211          * It is evaluated after an update, before the unsuspendRedraw.
5212          * The function is generated in
5213          * @see JXG.Board#addConditions
5214          * @private
5215          */
5216         updateConditions: function () {
5217             return false;
5218         },
5219 
5220         /**
5221          * Calculates adequate snap sizes.
5222          * @returns {JXG.Board} Reference to the board.
5223          */
5224         calculateSnapSizes: function () {
5225             var p1, p2,
5226                 bbox = this.getBoundingBox(),
5227                 gridStep = Type.evaluate(this.options.grid.majorStep),
5228                 gridX = Type.evaluate(this.options.grid.gridX),
5229                 gridY = Type.evaluate(this.options.grid.gridY),
5230                 x, y;
5231 
5232             if (!Type.isArray(gridStep)) {
5233                 gridStep = [gridStep, gridStep];
5234             }
5235             if (gridStep.length < 2) {
5236                 gridStep = [gridStep[0], gridStep[0]];
5237             }
5238             if (Type.exists(gridX)) {
5239                 gridStep[0] = gridX;
5240             }
5241             if (Type.exists(gridY)) {
5242                 gridStep[1] = gridY;
5243             }
5244 
5245             if (gridStep[0] === 'auto') {
5246                 gridStep[0] = 1;
5247             } else {
5248                 gridStep[0] = Type.parseNumber(gridStep[0], Math.abs(bbox[1] - bbox[3]), 1 / this.unitX);
5249             }
5250             if (gridStep[1] === 'auto') {
5251                 gridStep[1] = 1;
5252             } else {
5253                 gridStep[1] = Type.parseNumber(gridStep[1], Math.abs(bbox[0] - bbox[2]), 1 / this.unitY);
5254             }
5255 
5256             p1 = new Coords(Const.COORDS_BY_USER, [0, 0], this);
5257             p2 = new Coords(
5258                 Const.COORDS_BY_USER,
5259                 [gridStep[0], gridStep[1]],
5260                 this
5261             );
5262             x = p1.scrCoords[1] - p2.scrCoords[1];
5263             y = p1.scrCoords[2] - p2.scrCoords[2];
5264 
5265             this.options.grid.snapSizeX = gridStep[0];
5266             while (Math.abs(x) > 25) {
5267                 this.options.grid.snapSizeX *= 2;
5268                 x /= 2;
5269             }
5270 
5271             this.options.grid.snapSizeY = gridStep[1];
5272             while (Math.abs(y) > 25) {
5273                 this.options.grid.snapSizeY *= 2;
5274                 y /= 2;
5275             }
5276 
5277             return this;
5278         },
5279 
5280         /**
5281          * Apply update on all objects with the new zoom-factors. Clears all traces.
5282          * @returns {JXG.Board} Reference to the board.
5283          */
5284         applyZoom: function () {
5285             this.updateCoords().calculateSnapSizes().clearTraces().fullUpdate();
5286 
5287             return this;
5288         },
5289 
5290         /**
5291          * Zooms into the board by the factors board.attr.zoom.factorX and board.attr.zoom.factorY and applies the zoom.
5292          * The zoom operation is centered at x, y.
5293          * @param {Number} [x]
5294          * @param {Number} [y]
5295          * @returns {JXG.Board} Reference to the board
5296          */
5297         zoomIn: function (x, y) {
5298             var bb = this.getBoundingBox(),
5299                 zX = Type.evaluate(this.attr.zoom.factorx),
5300                 zY =  Type.evaluate(this.attr.zoom.factory),
5301                 dX = (bb[2] - bb[0]) * (1.0 - 1.0 / zX),
5302                 dY = (bb[1] - bb[3]) * (1.0 - 1.0 / zY),
5303                 lr = 0.5,
5304                 tr = 0.5,
5305                 ma = Type.evaluate(this.attr.zoom.max),
5306                 mi =  Type.evaluate(this.attr.zoom.eps) || Type.evaluate(this.attr.zoom.min) || 0.001; // this.attr.zoom.eps is deprecated
5307 
5308             if (
5309                 (this.zoomX > ma && zX > 1.0) ||
5310                 (this.zoomY > ma && zY > 1.0) ||
5311                 (this.zoomX < mi && zX < 1.0) || // zoomIn is used for all zooms on touch devices
5312                 (this.zoomY < mi && zY < 1.0)
5313             ) {
5314                 return this;
5315             }
5316 
5317             if (Type.isNumber(x) && Type.isNumber(y)) {
5318                 lr = (x - bb[0]) / (bb[2] - bb[0]);
5319                 tr = (bb[1] - y) / (bb[1] - bb[3]);
5320             }
5321 
5322             this.setBoundingBox(
5323                 [
5324                     bb[0] + dX * lr,
5325                     bb[1] - dY * tr,
5326                     bb[2] - dX * (1 - lr),
5327                     bb[3] + dY * (1 - tr)
5328                 ],
5329                 this.keepaspectratio,
5330                 'update'
5331             );
5332             return this.applyZoom();
5333         },
5334 
5335         /**
5336          * Zooms out of the board by the factors board.attr.zoom.factorX and board.attr.zoom.factorY and applies the zoom.
5337          * The zoom operation is centered at x, y.
5338          *
5339          * @param {Number} [x]
5340          * @param {Number} [y]
5341          * @returns {JXG.Board} Reference to the board
5342          */
5343         zoomOut: function (x, y) {
5344             var bb = this.getBoundingBox(),
5345                 zX = Type.evaluate(this.attr.zoom.factorx),
5346                 zY = Type.evaluate(this.attr.zoom.factory),
5347                 dX = (bb[2] - bb[0]) * (1.0 - zX),
5348                 dY = (bb[1] - bb[3]) * (1.0 - zY),
5349                 lr = 0.5,
5350                 tr = 0.5,
5351                 mi = Type.evaluate(this.attr.zoom.eps) || Type.evaluate(this.attr.zoom.min) || 0.001; // this.attr.zoom.eps is deprecated
5352 
5353             if (this.zoomX < mi || this.zoomY < mi) {
5354                 return this;
5355             }
5356 
5357             if (Type.isNumber(x) && Type.isNumber(y)) {
5358                 lr = (x - bb[0]) / (bb[2] - bb[0]);
5359                 tr = (bb[1] - y) / (bb[1] - bb[3]);
5360             }
5361 
5362             this.setBoundingBox(
5363                 [
5364                     bb[0] + dX * lr,
5365                     bb[1] - dY * tr,
5366                     bb[2] - dX * (1 - lr),
5367                     bb[3] + dY * (1 - tr)
5368                 ],
5369                 this.keepaspectratio,
5370                 'update'
5371             );
5372 
5373             return this.applyZoom();
5374         },
5375 
5376         /**
5377          * Reset the zoom level to the original zoom level from initBoard();
5378          * Additionally, if the board as been initialized with a boundingBox (which is the default),
5379          * restore the viewport to the original viewport during initialization. Otherwise,
5380          * (i.e. if the board as been initialized with unitX/Y and originX/Y),
5381          * just set the zoom level to 100%.
5382          *
5383          * @returns {JXG.Board} Reference to the board
5384          */
5385         zoom100: function () {
5386             var bb, dX, dY;
5387 
5388             if (Type.exists(this.attr.boundingbox)) {
5389                 this.setBoundingBox(this.attr.boundingbox, this.keepaspectratio, 'reset');
5390             } else {
5391                 // Board has been set up with unitX/Y and originX/Y
5392                 bb = this.getBoundingBox();
5393                 dX = (bb[2] - bb[0]) * (1.0 - this.zoomX) * 0.5;
5394                 dY = (bb[1] - bb[3]) * (1.0 - this.zoomY) * 0.5;
5395                 this.setBoundingBox(
5396                     [bb[0] + dX, bb[1] - dY, bb[2] - dX, bb[3] + dY],
5397                     this.keepaspectratio,
5398                     'reset'
5399                 );
5400             }
5401             return this.applyZoom();
5402         },
5403 
5404         /**
5405          * Zooms the board so every visible point is shown. Keeps aspect ratio.
5406          * @returns {JXG.Board} Reference to the board
5407          */
5408         zoomAllPoints: function () {
5409             var el,
5410                 border,
5411                 borderX,
5412                 borderY,
5413                 pEl,
5414                 minX = 0,
5415                 maxX = 0,
5416                 minY = 0,
5417                 maxY = 0,
5418                 len = this.objectsList.length;
5419 
5420             for (el = 0; el < len; el++) {
5421                 pEl = this.objectsList[el];
5422 
5423                 if (Type.isPoint(pEl) && pEl.visPropCalc.visible) {
5424                     if (pEl.coords.usrCoords[1] < minX) {
5425                         minX = pEl.coords.usrCoords[1];
5426                     } else if (pEl.coords.usrCoords[1] > maxX) {
5427                         maxX = pEl.coords.usrCoords[1];
5428                     }
5429                     if (pEl.coords.usrCoords[2] > maxY) {
5430                         maxY = pEl.coords.usrCoords[2];
5431                     } else if (pEl.coords.usrCoords[2] < minY) {
5432                         minY = pEl.coords.usrCoords[2];
5433                     }
5434                 }
5435             }
5436 
5437             border = 50;
5438             borderX = border / this.unitX;
5439             borderY = border / this.unitY;
5440 
5441             this.setBoundingBox(
5442                 [minX - borderX, maxY + borderY, maxX + borderX, minY - borderY],
5443                 this.keepaspectratio,
5444                 'update'
5445             );
5446 
5447             return this.applyZoom();
5448         },
5449 
5450         /**
5451          * Reset the bounding box and the zoom level to 100% such that a given set of elements is
5452          * within the board's viewport.
5453          * @param {Array} elements A set of elements given by id, reference, or name.
5454          * @returns {JXG.Board} Reference to the board.
5455          */
5456         zoomElements: function (elements) {
5457             var i, e,
5458                 box,
5459                 newBBox = [Infinity, -Infinity, -Infinity, Infinity],
5460                 cx, cy,
5461                 dx, dy,
5462                 d;
5463 
5464             if (!Type.isArray(elements) || elements.length === 0) {
5465                 return this;
5466             }
5467 
5468             for (i = 0; i < elements.length; i++) {
5469                 e = this.select(elements[i]);
5470 
5471                 box = e.bounds();
5472                 if (Type.isArray(box)) {
5473                     if (box[0] < newBBox[0]) {
5474                         newBBox[0] = box[0];
5475                     }
5476                     if (box[1] > newBBox[1]) {
5477                         newBBox[1] = box[1];
5478                     }
5479                     if (box[2] > newBBox[2]) {
5480                         newBBox[2] = box[2];
5481                     }
5482                     if (box[3] < newBBox[3]) {
5483                         newBBox[3] = box[3];
5484                     }
5485                 }
5486             }
5487 
5488             if (Type.isArray(newBBox)) {
5489                 cx = 0.5 * (newBBox[0] + newBBox[2]);
5490                 cy = 0.5 * (newBBox[1] + newBBox[3]);
5491                 dx = 1.5 * (newBBox[2] - newBBox[0]) * 0.5;
5492                 dy = 1.5 * (newBBox[1] - newBBox[3]) * 0.5;
5493                 d = Math.max(dx, dy);
5494                 this.setBoundingBox(
5495                     [cx - d, cy + d, cx + d, cy - d],
5496                     this.keepaspectratio,
5497                     'update'
5498                 );
5499             }
5500 
5501             return this;
5502         },
5503 
5504         /**
5505          * Sets the zoom level to <tt>fX</tt> resp <tt>fY</tt>.
5506          * @param {Number} fX
5507          * @param {Number} fY
5508          * @returns {JXG.Board} Reference to the board.
5509          */
5510         setZoom: function (fX, fY) {
5511             var oX = this.attr.zoom.factorx,
5512                 oY = this.attr.zoom.factory;
5513 
5514             this.attr.zoom.factorx = fX / this.zoomX;
5515             this.attr.zoom.factory = fY / this.zoomY;
5516 
5517             this.zoomIn();
5518 
5519             this.attr.zoom.factorx = oX;
5520             this.attr.zoom.factory = oY;
5521 
5522             return this;
5523         },
5524 
5525         /**
5526          * Inner, recursive method of removeObject.
5527          *
5528          * @param {JXG.GeometryElement|Array} object The object to remove or array of objects to be removed.
5529          * The element(s) is/are given by name, id or a reference.
5530          * @param {Boolean} [saveMethod=false] If saveMethod=true, the algorithm runs through all elements
5531          * and tests if the element to be deleted is a child element. If this is the case, it will be
5532          * removed from the list of child elements. If saveMethod=false (default), the element
5533          * is removed from the lists of child elements of all its ancestors.
5534          * The latter should be much faster.
5535          * @returns {JXG.Board} Reference to the board
5536          * @private
5537          */
5538         _removeObj: function (object, saveMethod) {
5539             var el, o, i;
5540 
5541             if (Type.isArray(object)) {
5542                 for (i = 0; i < object.length; i++) {
5543                     this._removeObj(object[i], saveMethod);
5544                 }
5545 
5546                 return this;
5547             }
5548 
5549             object = this.select(object);
5550 
5551             // If the object which is about to be removed is unknown or a string, do nothing.
5552             // it is a string if a string was given and could not be resolved to an element.
5553             if (!Type.exists(object) || Type.isString(object)) {
5554                 return this;
5555             }
5556 
5557             try {
5558                 // remove all children.
5559                 for (el in object.childElements) {
5560                     if (object.childElements.hasOwnProperty(el)) {
5561                         object.childElements[el].board._removeObj(object.childElements[el]);
5562                     }
5563                 }
5564 
5565                 // Remove all children in elements like turtle
5566                 for (el in object.objects) {
5567                     if (object.objects.hasOwnProperty(el)) {
5568                         object.objects[el].board._removeObj(object.objects[el]);
5569                     }
5570                 }
5571 
5572                 // Remove the element from the childElement list and the descendant list of all elements.
5573                 if (saveMethod) {
5574                     // Running through all objects has quadratic complexity if many objects are deleted.
5575                     for (el in this.objects) {
5576                         if (this.objects.hasOwnProperty(el)) {
5577                             if (
5578                                 Type.exists(this.objects[el].childElements) &&
5579                                 Type.exists(
5580                                     this.objects[el].childElements.hasOwnProperty(object.id)
5581                                 )
5582                             ) {
5583                                 delete this.objects[el].childElements[object.id];
5584                                 delete this.objects[el].descendants[object.id];
5585                             }
5586                         }
5587                     }
5588                 } else if (Type.exists(object.ancestors)) {
5589                     // Running through the ancestors should be much more efficient.
5590                     for (el in object.ancestors) {
5591                         if (object.ancestors.hasOwnProperty(el)) {
5592                             if (
5593                                 Type.exists(object.ancestors[el].childElements) &&
5594                                 Type.exists(
5595                                     object.ancestors[el].childElements.hasOwnProperty(object.id)
5596                                 )
5597                             ) {
5598                                 delete object.ancestors[el].childElements[object.id];
5599                                 delete object.ancestors[el].descendants[object.id];
5600                             }
5601                         }
5602                     }
5603                 }
5604 
5605                 // remove the object itself from our control structures
5606                 if (object._pos > -1) {
5607                     this.objectsList.splice(object._pos, 1);
5608                     // Quadratic complexity for reindexing the positions:
5609                     for (i = object._pos; i < this.objectsList.length; i++) {
5610                         o = this.objectsList[i];
5611                         if (o._pos > -1) {
5612                             o._pos--;
5613                         }
5614                     }
5615                 } else if (object.type !== Const.OBJECT_TYPE_TURTLE) {
5616                     JXG.debug(
5617                         'Board.removeObject: object ' + object.id + ' not found in list.'
5618                     );
5619                 }
5620 
5621                 delete this.objects[object.id];
5622                 delete this.elementsByName[object.name];
5623 
5624                 if (object.visProp && object.evalVisProp('trace')) {
5625                     object.clearTrace();
5626                 }
5627 
5628                 // the object deletion itself is handled by the object.
5629                 if (Type.exists(object.remove)) {
5630                     object.remove();
5631                 }
5632             } catch (e) {
5633                 JXG.debug(object.id + ': Could not be removed: ' + e);
5634             }
5635 
5636             return this;
5637         },
5638 
5639         /**
5640          * Removes object from board and from the renderer object.
5641          * <p>
5642          * <b>Performance hints:</b> It is recommended to use the JSXGraph object's id.
5643          * If many elements are removed, it is best to either
5644          * <ul>
5645          *   <li> remove the whole array if the elements are contained in an array instead
5646          *    of looping through the array OR
5647          *   <li> call <tt>board.suspendUpdate()</tt>
5648          * before looping through the elements to be removed and call
5649          * <tt>board.unsuspendUpdate()</tt> after the loop. Further, it is advisable to loop
5650          * in reverse order, i.e. remove the object in reverse order of their creation time.
5651          * </ul>
5652          * @param {JXG.GeometryElement|Array} object The object to remove or array of objects to be removed.
5653          * The element(s) is/are given by name, id or a reference.
5654          * @param {Boolean} saveMethod If true, the algorithm runs through all elements
5655          * and tests if the element to be deleted is a child element. If yes, it will be
5656          * removed from the list of child elements. If false (default), the element
5657          * is removed from the lists of child elements of all its ancestors.
5658          * This should be much faster.
5659          * @returns {JXG.Board} Reference to the board
5660          */
5661         removeObject: function (object, saveMethod) {
5662             var i;
5663 
5664             this.renderer.suspendRedraw(this);
5665             if (Type.isArray(object)) {
5666                 for (i = 0; i < object.length; i++) {
5667                     this._removeObj(object[i], saveMethod);
5668                 }
5669             } else {
5670                 this._removeObj(object, saveMethod);
5671             }
5672             this.renderer.unsuspendRedraw();
5673 
5674             this.update();
5675             return this;
5676         },
5677 
5678         /**
5679          * Removes the ancestors of an object an the object itself from board and renderer.
5680          * @param {JXG.GeometryElement} object The object to remove.
5681          * @returns {JXG.Board} Reference to the board
5682          */
5683         removeAncestors: function (object) {
5684             var anc;
5685 
5686             for (anc in object.ancestors) {
5687                 if (object.ancestors.hasOwnProperty(anc)) {
5688                     this.removeAncestors(object.ancestors[anc]);
5689                 }
5690             }
5691 
5692             this.removeObject(object);
5693 
5694             return this;
5695         },
5696 
5697         /**
5698          * Initialize some objects which are contained in every GEONExT construction by default,
5699          * but are not contained in the gxt files.
5700          * @returns {JXG.Board} Reference to the board
5701          */
5702         initGeonextBoard: function () {
5703             var p1, p2, p3;
5704 
5705             p1 = this.create('point', [0, 0], {
5706                 id: this.id + 'g00e0',
5707                 name: 'Ursprung',
5708                 withLabel: false,
5709                 visible: false,
5710                 fixed: true
5711             });
5712 
5713             p2 = this.create('point', [1, 0], {
5714                 id: this.id + 'gX0e0',
5715                 name: 'Punkt_1_0',
5716                 withLabel: false,
5717                 visible: false,
5718                 fixed: true
5719             });
5720 
5721             p3 = this.create('point', [0, 1], {
5722                 id: this.id + 'gY0e0',
5723                 name: 'Punkt_0_1',
5724                 withLabel: false,
5725                 visible: false,
5726                 fixed: true
5727             });
5728 
5729             this.create('line', [p1, p2], {
5730                 id: this.id + 'gXLe0',
5731                 name: 'X-Achse',
5732                 withLabel: false,
5733                 visible: false
5734             });
5735 
5736             this.create('line', [p1, p3], {
5737                 id: this.id + 'gYLe0',
5738                 name: 'Y-Achse',
5739                 withLabel: false,
5740                 visible: false
5741             });
5742 
5743             return this;
5744         },
5745 
5746         /**
5747          * Change the height and width of the board's container.
5748          * After doing so, {@link JXG.JSXGraph.setBoundingBox} is called using
5749          * the actual size of the bounding box and the actual value of keepaspectratio.
5750          * If setBoundingbox() should not be called automatically,
5751          * call resizeContainer with dontSetBoundingBox == true.
5752          * @param {Number} canvasWidth New width of the container.
5753          * @param {Number} canvasHeight New height of the container.
5754          * @param {Boolean} [dontset=false] If true do not set the CSS width and height of the DOM element.
5755          * @param {Boolean} [dontSetBoundingBox=false] If true do not call setBoundingBox(), but keep view centered around original visible center.
5756          * @returns {JXG.Board} Reference to the board
5757          */
5758         resizeContainer: function (canvasWidth, canvasHeight, dontset, dontSetBoundingBox) {
5759             var box,
5760                 oldWidth, oldHeight,
5761                 oX, oY;
5762 
5763             oldWidth = this.canvasWidth;
5764             oldHeight = this.canvasHeight;
5765 
5766             if (!dontSetBoundingBox) {
5767                 box = this.getBoundingBox();    // This is the actual bounding box.
5768             }
5769 
5770             // this.canvasWidth = Math.max(parseFloat(canvasWidth), Mat.eps);
5771             // this.canvasHeight = Math.max(parseFloat(canvasHeight), Mat.eps);
5772             this.canvasWidth = parseFloat(canvasWidth);
5773             this.canvasHeight = parseFloat(canvasHeight);
5774 
5775             if (!dontset) {
5776                 this.containerObj.style.width = this.canvasWidth + 'px';
5777                 this.containerObj.style.height = this.canvasHeight + 'px';
5778             }
5779             this.renderer.resize(this.canvasWidth, this.canvasHeight);
5780 
5781             if (!dontSetBoundingBox) {
5782                 this.setBoundingBox(box, this.keepaspectratio, 'keep');
5783             } else {
5784                 oX = (this.canvasWidth - oldWidth) * 0.5;
5785                 oY = (this.canvasHeight - oldHeight) * 0.5;
5786 
5787                 this.moveOrigin(
5788                     this.origin.scrCoords[1] + oX,
5789                     this.origin.scrCoords[2] + oY
5790                 );
5791             }
5792 
5793             return this;
5794         },
5795 
5796         /**
5797          * Lists the dependencies graph in a new HTML-window.
5798          * @returns {JXG.Board} Reference to the board
5799          */
5800         showDependencies: function () {
5801             var el, t, c, f, i;
5802 
5803             t = '<p>\n';
5804             for (el in this.objects) {
5805                 if (this.objects.hasOwnProperty(el)) {
5806                     i = 0;
5807                     for (c in this.objects[el].childElements) {
5808                         if (this.objects[el].childElements.hasOwnProperty(c)) {
5809                             i += 1;
5810                         }
5811                     }
5812                     if (i >= 0) {
5813                         t += '<strong>' + this.objects[el].id + ':<' + '/strong> ';
5814                     }
5815 
5816                     for (c in this.objects[el].childElements) {
5817                         if (this.objects[el].childElements.hasOwnProperty(c)) {
5818                             t +=
5819                                 this.objects[el].childElements[c].id +
5820                                 '(' +
5821                                 this.objects[el].childElements[c].name +
5822                                 ')' +
5823                                 ', ';
5824                         }
5825                     }
5826                     t += '<p>\n';
5827                 }
5828             }
5829             t += '<' + '/p>\n';
5830             f = window.open();
5831             f.document.open();
5832             f.document.write(t);
5833             f.document.close();
5834             return this;
5835         },
5836 
5837         /**
5838          * Lists the XML code of the construction in a new HTML-window.
5839          * @returns {JXG.Board} Reference to the board
5840          */
5841         showXML: function () {
5842             var f = window.open('');
5843             f.document.open();
5844             f.document.write('<pre>' + Type.escapeHTML(this.xmlString) + '<' + '/pre>');
5845             f.document.close();
5846             return this;
5847         },
5848 
5849         /**
5850          * Sets for all objects the needsUpdate flag to 'true'.
5851          * @param{JXG.GeometryElement} [drag=undefined] Optional element that is dragged.
5852          * @returns {JXG.Board} Reference to the board
5853          */
5854         prepareUpdate: function (drag) {
5855             var el, i,
5856                 pEl,
5857                 len = this.objectsList.length;
5858 
5859             /*
5860             if (this.attr.updatetype === 'hierarchical') {
5861                 return this;
5862             }
5863             */
5864 
5865             for (el = 0; el < len; el++) {
5866                 pEl = this.objectsList[el];
5867                 if (this._change3DView ||
5868                     (Type.exists(drag) && drag.elType === 'view3d_slider')
5869                 ) {
5870                     // The 3D view has changed. No elements are recomputed,
5871                     // only 3D elements are projected to the new view.
5872                     pEl.needsUpdate =
5873                         pEl.visProp.element3d ||
5874                         pEl.elType === 'view3d' ||
5875                         pEl.elType === 'view3d_slider' ||
5876                         this.needsFullUpdate;
5877 
5878                     // Special case sphere3d in central projection:
5879                     // We have to update the defining points of the ellipse
5880                     if (pEl.visProp.element3d &&
5881                         pEl.visProp.element3d.type === Const.OBJECT_TYPE_SPHERE3D
5882                         ) {
5883                         for (i = 0; i < pEl.parents.length; i++) {
5884                             this.objects[pEl.parents[i]].needsUpdate = true;
5885                         }
5886                     }
5887                 } else {
5888                     pEl.needsUpdate = pEl.needsRegularUpdate || this.needsFullUpdate;
5889                 }
5890             }
5891 
5892             for (el in this.groups) {
5893                 if (this.groups.hasOwnProperty(el)) {
5894                     pEl = this.groups[el];
5895                     pEl.needsUpdate = pEl.needsRegularUpdate || this.needsFullUpdate;
5896                 }
5897             }
5898 
5899             return this;
5900         },
5901 
5902         /**
5903          * Runs through all elements and calls their update() method.
5904          * @param {JXG.GeometryElement} drag Element that caused the update.
5905          * @returns {JXG.Board} Reference to the board
5906          */
5907         updateElements: function (drag) {
5908             var el, pEl;
5909             //var childId, i = 0;
5910 
5911             drag = this.select(drag);
5912 
5913             /*
5914             if (Type.exists(drag)) {
5915                 for (el = 0; el < this.objectsList.length; el++) {
5916                     pEl = this.objectsList[el];
5917                     if (pEl.id === drag.id) {
5918                         i = el;
5919                         break;
5920                     }
5921                 }
5922             }
5923             */
5924             for (el = 0; el < this.objectsList.length; el++) {
5925                 pEl = this.objectsList[el];
5926                 if (this.needsFullUpdate && pEl.elementClass === Const.OBJECT_CLASS_TEXT) {
5927                     pEl.updateSize();
5928                 }
5929 
5930                 // For updates of an element we distinguish if the dragged element is updated or
5931                 // other elements are updated.
5932                 // The difference lies in the treatment of gliders and points based on transformations.
5933                 pEl.update(!Type.exists(drag) || pEl.id !== drag.id).updateVisibility();
5934             }
5935 
5936             // update groups last
5937             for (el in this.groups) {
5938                 if (this.groups.hasOwnProperty(el)) {
5939                     this.groups[el].update(drag);
5940                 }
5941             }
5942 
5943             return this;
5944         },
5945 
5946         /**
5947          * Runs through all elements and calls their update() method.
5948          * @returns {JXG.Board} Reference to the board
5949          */
5950         updateRenderer: function () {
5951             var el,
5952                 len = this.objectsList.length,
5953                 autoPositionLabelList = [],
5954                 currentIndex, randomIndex;
5955 
5956             if (!this.renderer) {
5957                 return;
5958             }
5959 
5960             /*
5961             objs = this.objectsList.slice(0);
5962             objs.sort(function (a, b) {
5963                 if (a.visProp.layer < b.visProp.layer) {
5964                     return -1;
5965                 } else if (a.visProp.layer === b.visProp.layer) {
5966                     return b.lastDragTime.getTime() - a.lastDragTime.getTime();
5967                 } else {
5968                     return 1;
5969                 }
5970             });
5971             */
5972 
5973             if (this.renderer.type === 'canvas') {
5974                 this.updateRendererCanvas();
5975             } else {
5976                 for (el = 0; el < len; el++) {
5977                     if (this.objectsList[el].visProp.islabel && this.objectsList[el].visProp.autoposition) {
5978                         autoPositionLabelList.push(el);
5979                     } else {
5980                         this.objectsList[el].updateRenderer();
5981                     }
5982                 }
5983 
5984                 currentIndex = autoPositionLabelList.length;
5985 
5986                 // Randomize the order of the labels
5987                 while (currentIndex !== 0) {
5988                     randomIndex = Math.floor(Math.random() * currentIndex);
5989                     currentIndex--;
5990                     [autoPositionLabelList[currentIndex], autoPositionLabelList[randomIndex]] = [autoPositionLabelList[randomIndex], autoPositionLabelList[currentIndex]];
5991                 }
5992 
5993                 for (el = 0; el < autoPositionLabelList.length; el++) {
5994                     this.objectsList[autoPositionLabelList[el]].updateRenderer();
5995                 }
5996                 /*
5997                 for (el = autoPositionLabelList.length - 1; el >= 0; el--) {
5998                     this.objectsList[autoPositionLabelList[el]].updateRenderer();
5999                 }
6000                 */
6001             }
6002             return this;
6003         },
6004 
6005         /**
6006          * Runs through all elements and calls their update() method.
6007          * This is a special version for the CanvasRenderer.
6008          * Here, we have to do our own layer handling.
6009          * @returns {JXG.Board} Reference to the board
6010          */
6011         updateRendererCanvas: function () {
6012             var el, pEl,
6013                 olen = this.objectsList.length,
6014                 // i, minim, lay,
6015                 // layers = this.options.layer,
6016                 // len = this.options.layer.numlayers,
6017                 // last = Number.NEGATIVE_INFINITY.toExponential,
6018                 depth_order_layers = [],
6019                 objects_sorted,
6020 
6021                 /**
6022                  * Function to sort elements for depth ordering in canvas renderer.
6023                  * Only relevant for elements having a zIndex.
6024                  * Sort the elements for the canvas rendering according to
6025                  * their layer, _pos, depthOrder (with this priority).
6026                  * @param {JXG.GeometryObject} a
6027                  * @param {JXG.GeometryObject} b
6028                  * @returns Number
6029                  * @private
6030                  */
6031                 _compareDepth = function(a, b) {
6032                     if (a.visProp.layer !== b.visProp.layer) {
6033                         // For elements in different layers, the element in the
6034                         // higher layer is in front.
6035                         return a.visProp.layer - b.visProp.layer;
6036                     }
6037 
6038                     // From here on, both objects are in the same layer.
6039 
6040                     if (depth_order_layers.indexOf(a.visProp.layer) === -1) {
6041                         // The layer is not depth ordered.
6042                         return a._pos - b._pos;
6043                     }
6044 
6045                     // From here on, both objects are in the same layer
6046                     // and the layer is depth ordered.
6047 
6048                     // The objects are in the same layer and the layer is depth ordered
6049                     // But neither element is the 2D element of a 3D element.
6050                     if (!a.visProp.element3d && !b.visProp.element3d) {
6051                         return a._pos - b._pos;
6052                     }
6053 
6054                     if (a.visProp.element3d && !b.visProp.element3d) {
6055                         return -1;
6056                     }
6057 
6058                     if (!a.visProp.element3d && b.visProp.element3d) {
6059                         return 1;
6060                     }
6061 
6062                     // Finqally, both elements are 2D elements of a 3D element.
6063                     return a.visProp.element3d.zIndex - b.visProp.element3d.zIndex;
6064                 };
6065 
6066             // Only one view3d element is supported. Get the depth order layers and
6067             // update the zIndices of the 3D elements.
6068             for (el = 0; el < olen; el++) {
6069                 pEl = this.objectsList[el];
6070                 if (pEl.elType === 'view3d' &&
6071                     pEl.evalVisProp('depthorder.enabled')
6072                 ) {
6073                     depth_order_layers = pEl.evalVisProp('depthorder.layers');
6074                     pEl.updateRenderer();
6075                     break;
6076                 }
6077             }
6078 
6079             // objects_sorted = this.objectsList.toSorted(_compareDepth);
6080 
6081             // 3D elements are not rendered, but their subelements element2D
6082             objects_sorted = this.objectsList.filter(function(e) { return !e.is3D; }).toSorted(_compareDepth);
6083             olen = objects_sorted.length;
6084             for (el = 0; el < olen; el++) {
6085                 if (
6086                     objects_sorted[el].visPropCalc.visible &&
6087                     objects_sorted[el].type !== Const.OBJECT_TYPE_FACE3D // For these, updateRenderer is triggered in polyhedron3d.updateRenderer
6088                 ) {
6089                     objects_sorted[el].prepareUpdate().updateRenderer();
6090                 }
6091             }
6092 
6093             return this;
6094         },
6095 
6096         /**
6097          * Please use {@link JXG.Board.on} instead.
6098          * @param {Function} hook A function to be called by the board after an update occurred.
6099          * @param {String} [m='update'] When the hook is to be called. Possible values are <i>mouseup</i>, <i>mousedown</i> and <i>update</i>.
6100          * @param {Object} [context=board] Determines the execution context the hook is called. This parameter is optional, default is the
6101          * board object the hook is attached to.
6102          * @returns {Number} Id of the hook, required to remove the hook from the board.
6103          * @deprecated
6104          */
6105         addHook: function (hook, m, context) {
6106             JXG.deprecated('Board.addHook()', 'Board.on()');
6107             m = Type.def(m, 'update');
6108 
6109             context = Type.def(context, this);
6110 
6111             this.hooks.push([m, hook]);
6112             this.on(m, hook, context);
6113 
6114             return this.hooks.length - 1;
6115         },
6116 
6117         /**
6118          * Alias of {@link JXG.Board.on}.
6119          */
6120         addEvent: JXG.shortcut(JXG.Board.prototype, 'on'),
6121 
6122         /**
6123          * Please use {@link JXG.Board.off} instead.
6124          * @param {Number|function} id The number you got when you added the hook or a reference to the event handler.
6125          * @returns {JXG.Board} Reference to the board
6126          * @deprecated
6127          */
6128         removeHook: function (id) {
6129             JXG.deprecated('Board.removeHook()', 'Board.off()');
6130             if (this.hooks[id]) {
6131                 this.off(this.hooks[id][0], this.hooks[id][1]);
6132                 this.hooks[id] = null;
6133             }
6134 
6135             return this;
6136         },
6137 
6138         /**
6139          * Alias of {@link JXG.Board.off}.
6140          */
6141         removeEvent: JXG.shortcut(JXG.Board.prototype, 'off'),
6142 
6143         /**
6144          * Runs through all hooked functions and calls them.
6145          * @returns {JXG.Board} Reference to the board
6146          * @deprecated
6147          */
6148         updateHooks: function (m) {
6149             var arg = Array.prototype.slice.call(arguments, 0);
6150 
6151             JXG.deprecated('Board.updateHooks()', 'Board.triggerEventHandlers()');
6152 
6153             arg[0] = Type.def(arg[0], 'update');
6154             this.triggerEventHandlers([arg[0]], arguments);
6155 
6156             return this;
6157         },
6158 
6159         /**
6160          * Adds a dependent board to this board.
6161          * @param {JXG.Board} board A reference to board which will be updated after an update of this board occurred.
6162          * @returns {JXG.Board} Reference to the board
6163          */
6164         addChild: function (board) {
6165             if (Type.exists(board) && Type.exists(board.containerObj)) {
6166                 this.dependentBoards.push(board);
6167                 this.update();
6168             }
6169             return this;
6170         },
6171 
6172         /**
6173          * Deletes a board from the list of dependent boards.
6174          * @param {JXG.Board} board Reference to the board which will be removed.
6175          * @returns {JXG.Board} Reference to the board
6176          */
6177         removeChild: function (board) {
6178             var i;
6179 
6180             for (i = this.dependentBoards.length - 1; i >= 0; i--) {
6181                 if (this.dependentBoards[i] === board) {
6182                     this.dependentBoards.splice(i, 1);
6183                 }
6184             }
6185             return this;
6186         },
6187 
6188         /**
6189          * Runs through most elements and calls their update() method and update the conditions.
6190          * @param {JXG.GeometryElement} [drag] Element that caused the update.
6191          * @returns {JXG.Board} Reference to the board
6192          */
6193         update: function (drag) {
6194             var i, len, b, insert, storeActiveEl;
6195 
6196             if (this.inUpdate || this.isSuspendedUpdate) {
6197                 return this;
6198             }
6199             this.inUpdate = true;
6200 
6201             if (
6202                 this.attr.minimizereflow === 'all' &&
6203                 this.containerObj &&
6204                 this.renderer.type !== 'vml'
6205             ) {
6206                 storeActiveEl = this.document.activeElement; // Store focus element
6207                 insert = this.renderer.removeToInsertLater(this.containerObj);
6208             }
6209 
6210             if (this.attr.minimizereflow === 'svg' && this.renderer.type === 'svg') {
6211                 storeActiveEl = this.document.activeElement;
6212                 insert = this.renderer.removeToInsertLater(this.renderer.svgRoot);
6213             }
6214 
6215             this.prepareUpdate(drag).updateElements(drag).updateConditions();
6216 
6217             this.renderer.suspendRedraw(this);
6218             this.updateRenderer();
6219             this.renderer.unsuspendRedraw();
6220             this.triggerEventHandlers(['update'], []);
6221 
6222             if (insert) {
6223                 insert();
6224                 storeActiveEl.focus(); // Restore focus element
6225             }
6226 
6227             // To resolve dependencies between boards
6228             // for (var board in JXG.boards) {
6229             len = this.dependentBoards.length;
6230             for (i = 0; i < len; i++) {
6231                 b = this.dependentBoards[i];
6232                 if (Type.exists(b) && b !== this) {
6233                     b.updateQuality = this.updateQuality;
6234                     b.prepareUpdate().updateElements().updateConditions();
6235                     b.renderer.suspendRedraw(this);
6236                     b.updateRenderer();
6237                     b.renderer.unsuspendRedraw();
6238                     b.triggerEventHandlers(['update'], []);
6239                 }
6240             }
6241 
6242             this.inUpdate = false;
6243             return this;
6244         },
6245 
6246         /**
6247          * Runs through all elements and calls their update() method and update the conditions.
6248          * This is necessary after zooming and changing the bounding box.
6249          * @returns {JXG.Board} Reference to the board
6250          */
6251         fullUpdate: function () {
6252             this.needsFullUpdate = true;
6253             this.update();
6254             this.needsFullUpdate = false;
6255             return this;
6256         },
6257 
6258         /**
6259          * Adds a grid to the board according to the settings given in board.options.
6260          * @returns {JXG.Board} Reference to the board.
6261          */
6262         addGrid: function () {
6263             this.create('grid', []);
6264 
6265             return this;
6266         },
6267 
6268         /**
6269          * Removes all grids assigned to this board. Warning: This method also removes all objects depending on one or
6270          * more of the grids.
6271          * @returns {JXG.Board} Reference to the board object.
6272          */
6273         removeGrids: function () {
6274             var i;
6275 
6276             for (i = 0; i < this.grids.length; i++) {
6277                 this.removeObject(this.grids[i]);
6278             }
6279 
6280             this.grids.length = 0;
6281             this.update(); // required for canvas renderer
6282 
6283             return this;
6284         },
6285 
6286         /**
6287          * Creates a new geometric element of type elementType.
6288          * @param {String} elementType Type of the element to be constructed given as a string e.g. 'point' or 'circle'.
6289          * @param {Array} parents Array of parent elements needed to construct the element e.g. coordinates for a point or two
6290          * points to construct a line. This highly depends on the elementType that is constructed. See the corresponding JXG.create*
6291          * methods for a list of possible parameters.
6292          * @param {Object} [attributes] An object containing the attributes to be set. This also depends on the elementType.
6293          * Common attributes are name, visible, strokeColor.
6294          * @returns {Object} Reference to the created element. This is usually a GeometryElement, but can be an array containing
6295          * two or more elements.
6296          */
6297         create: function (elementType, parents, attributes) {
6298             var el, i;
6299 
6300             elementType = elementType.toLowerCase();
6301 
6302             if (!Type.exists(parents)) {
6303                 parents = [];
6304             }
6305 
6306             if (!Type.exists(attributes)) {
6307                 attributes = {};
6308             }
6309 
6310             for (i = 0; i < parents.length; i++) {
6311                 if (
6312                     Type.isString(parents[i]) &&
6313                     !(elementType === 'text' && i === 2) &&
6314                     !(elementType === 'solidofrevolution3d' && i === 2) &&
6315                     !(elementType === 'text3d' && (i === 2 || i === 4)) &&
6316                     !(
6317                         (elementType === 'input' ||
6318                             elementType === 'checkbox' ||
6319                             elementType === 'button') &&
6320                         (i === 2 || i === 3)
6321                     ) &&
6322                     !(elementType === 'curve' /*&& i > 0*/) && // Allow curve plots with jessiecode, parents[0] is the
6323                                                                // variable name
6324                     !(elementType === 'functiongraph') && // Prevent problems with function terms like 'x', 'y'
6325                     !(elementType === 'implicitcurve')
6326                 ) {
6327                     if (i > 0 && parents[0].elType === 'view3d') {
6328                         // 3D elements are based on 3D elements, only
6329                         parents[i] = parents[0].select(parents[i]);
6330                     } else {
6331                         parents[i] = this.select(parents[i]);
6332                     }
6333                 }
6334             }
6335 
6336             if (Type.isFunction(JXG.elements[elementType])) {
6337                 el = JXG.elements[elementType](this, parents, attributes);
6338             } else {
6339                 throw new Error('JSXGraph: create: Unknown element type given: ' + elementType);
6340             }
6341 
6342             if (!Type.exists(el)) {
6343                 JXG.debug('JSXGraph: create: failure creating ' + elementType);
6344                 return el;
6345             }
6346 
6347             if (el.prepareUpdate && el.update && el.updateRenderer) {
6348                 el.fullUpdate();
6349             }
6350             return el;
6351         },
6352 
6353         /**
6354          * Deprecated name for {@link JXG.Board.create}.
6355          * @deprecated
6356          */
6357         createElement: function () {
6358             JXG.deprecated('Board.createElement()', 'Board.create()');
6359             return this.create.apply(this, arguments);
6360         },
6361 
6362         /**
6363          * Delete the elements drawn as part of a trace of an element.
6364          * @returns {JXG.Board} Reference to the board
6365          */
6366         clearTraces: function () {
6367             var el;
6368 
6369             for (el = 0; el < this.objectsList.length; el++) {
6370                 this.objectsList[el].clearTrace();
6371             }
6372 
6373             this.numTraces = 0;
6374             return this;
6375         },
6376 
6377         /**
6378          * Stop updates of the board.
6379          * @returns {JXG.Board} Reference to the board
6380          */
6381         suspendUpdate: function () {
6382             if (!this.inUpdate) {
6383                 this.isSuspendedUpdate = true;
6384             }
6385             return this;
6386         },
6387 
6388         /**
6389          * Enable updates of the board.
6390          * @returns {JXG.Board} Reference to the board
6391          */
6392         unsuspendUpdate: function () {
6393             if (this.isSuspendedUpdate) {
6394                 this.isSuspendedUpdate = false;
6395                 this.fullUpdate();
6396             }
6397             return this;
6398         },
6399 
6400         /**
6401          * Set the bounding box of the board.
6402          * @param {Array} bbox New bounding box [x1,y1,x2,y2]
6403          * @param {Boolean} [keepaspectratio=false] If set to true, the aspect ratio will be 1:1, but
6404          * the resulting viewport may be larger.
6405          * @param {String} [setZoom='reset'] Reset, keep or update the zoom level of the board. 'reset'
6406          * sets {@link JXG.Board#zoomX} and {@link JXG.Board#zoomY} to the start values (or 1.0).
6407          * 'update' adapts these values accoring to the new bounding box and 'keep' does nothing.
6408          * @returns {JXG.Board} Reference to the board
6409          */
6410         setBoundingBox: function (bbox, keepaspectratio, setZoom) {
6411             var h, w, ux, uy,
6412                 offX = 0,
6413                 offY = 0,
6414                 zoom_ratio = 1,
6415                 ratio, dx, dy, prev_w, prev_h,
6416                 dim = Env.getDimensions(this.containerObj, this.document);
6417 
6418             if (!Type.isArray(bbox)) {
6419                 return this;
6420             }
6421 
6422             if (
6423                 bbox[0] < this.maxboundingbox[0] - Mat.eps ||
6424                 bbox[1] > this.maxboundingbox[1] + Mat.eps ||
6425                 bbox[2] > this.maxboundingbox[2] + Mat.eps ||
6426                 bbox[3] < this.maxboundingbox[3] - Mat.eps
6427             ) {
6428                 return this;
6429             }
6430 
6431             if (!Type.exists(setZoom)) {
6432                 setZoom = 'reset';
6433             }
6434 
6435             ux = this.unitX;
6436             uy = this.unitY;
6437             this.canvasWidth = parseFloat(dim.width);   // parseInt(dim.width, 10);
6438             this.canvasHeight = parseFloat(dim.height); // parseInt(dim.height, 10);
6439             w = this.canvasWidth;
6440             h = this.canvasHeight;
6441             if (keepaspectratio) {
6442                 if (this.keepaspectratio) {
6443                     ratio = ux / uy;        // Keep this ratio if keepaspectratio was true
6444                     if (isNaN(ratio)) {
6445                         ratio = 1.0;
6446                     }
6447                 } else {
6448                     ratio = 1.0;
6449                 }
6450                 if (setZoom === 'keep') {
6451                     zoom_ratio = this.zoomX / this.zoomY;
6452                 }
6453                 dx = bbox[2] - bbox[0];
6454                 dy = bbox[1] - bbox[3];
6455                 prev_w = ux * dx;
6456                 prev_h = uy * dy;
6457                 if (w >= h) {
6458                     if (prev_w >= prev_h) {
6459                         this.unitY = h / dy;
6460                         this.unitX = this.unitY * ratio;
6461                     } else {
6462                         // Switch dominating interval
6463                         this.unitY = h / Math.abs(dx) * Mat.sign(dy) / zoom_ratio;
6464                         this.unitX = this.unitY * ratio;
6465                     }
6466                 } else {
6467                     if (prev_h > prev_w) {
6468                         this.unitX = w / dx;
6469                         this.unitY = this.unitX / ratio;
6470                     } else {
6471                         // Switch dominating interval
6472                         this.unitX = w / Math.abs(dy) * Mat.sign(dx) * zoom_ratio;
6473                         this.unitY = this.unitX / ratio;
6474                     }
6475                 }
6476                 // Add the additional units in equal portions left and right
6477                 offX = (w / this.unitX - dx) * 0.5;
6478                 // Add the additional units in equal portions above and below
6479                 offY = (h / this.unitY - dy) * 0.5;
6480                 this.keepaspectratio = true;
6481             } else {
6482                 this.unitX = w / (bbox[2] - bbox[0]);
6483                 this.unitY = h / (bbox[1] - bbox[3]);
6484                 this.keepaspectratio = false;
6485             }
6486 
6487             this.moveOrigin(-this.unitX * (bbox[0] - offX), this.unitY * (bbox[1] + offY));
6488 
6489             if (setZoom === 'update') {
6490                 this.zoomX *= this.unitX / ux;
6491                 this.zoomY *= this.unitY / uy;
6492             } else if (setZoom === 'reset') {
6493                 this.zoomX = Type.exists(this.attr.zoomx) ? this.attr.zoomx : 1.0;
6494                 this.zoomY = Type.exists(this.attr.zoomy) ? this.attr.zoomy : 1.0;
6495             }
6496 
6497             return this;
6498         },
6499 
6500         /**
6501          * Get the bounding box of the board.
6502          * @returns {Array} bounding box [x1,y1,x2,y2] upper left corner, lower right corner
6503          */
6504         getBoundingBox: function () {
6505             var ul = new Coords(Const.COORDS_BY_SCREEN, [0, 0], this).usrCoords,
6506                 lr = new Coords(
6507                     Const.COORDS_BY_SCREEN,
6508                     [this.canvasWidth, this.canvasHeight],
6509                     this
6510                 ).usrCoords;
6511             return [ul[1], ul[2], lr[1], lr[2]];
6512         },
6513 
6514         /**
6515          * Sets the value of attribute <tt>key</tt> to <tt>value</tt>.
6516          * @param {String} key The attribute's name.
6517          * @param value The new value
6518          * @private
6519          */
6520         _set: function (key, value) {
6521             key = key.toLocaleLowerCase();
6522 
6523             if (
6524                 value !== null &&
6525                 Type.isObject(value) &&
6526                 !Type.exists(value.id) &&
6527                 !Type.exists(value.name)
6528             ) {
6529                 // value is of type {prop: val, prop: val,...}
6530                 // Convert these attributes to lowercase, too
6531                 // this.attr[key] = {};
6532                 // for (el in value) {
6533                 //     if (value.hasOwnProperty(el)) {
6534                 //         this.attr[key][el.toLocaleLowerCase()] = value[el];
6535                 //     }
6536                 // }
6537                 Type.mergeAttr(this.attr[key], value);
6538             } else {
6539                 this.attr[key] = value;
6540             }
6541         },
6542 
6543         /**
6544          * Sets an arbitrary number of attributes. This method has one or more
6545          * parameters of the following types:
6546          * <ul>
6547          * <li> object: {key1:value1,key2:value2,...}
6548          * <li> string: 'key:value'
6549          * <li> array: ['key', value]
6550          * </ul>
6551          * Some board attributes are immutable, like e.g. the renderer type.
6552          *
6553          * @param {Object} attributes An object with attributes
6554          * @param {Boolean} [force=false] if true the attributes are set regardless of the previous setting was identical.
6555          * @returns {JXG.Board} Reference to the board
6556          *
6557          * @example
6558          * const board = JXG.JSXGraph.initBoard('jxgbox', {
6559          *     boundingbox: [-5, 5, 5, -5],
6560          *     keepAspectRatio: false,
6561          *     axis:true,
6562          *     showFullscreen: true,
6563          *     showScreenshot: true,
6564          *     showCopyright: false
6565          * });
6566          *
6567          * board.setAttribute({
6568          *     animationDelay: 10,
6569          *     boundingbox: [-10, 5, 10, -5],
6570          *     defaultAxes: {
6571          *         x: { strokeColor: 'blue', ticks: { strokeColor: 'blue'}}
6572          *     },
6573          *     description: 'test',
6574          *     fullscreen: {
6575          *         scale: 0.5
6576          *     },
6577          *     intl: {
6578          *         enabled: true,
6579          *         locale: 'de-DE'
6580          *     }
6581          * });
6582          *
6583          * board.setAttribute({
6584          *     selection: {
6585          *         enabled: true,
6586          *         fillColor: 'blue'
6587          *     },
6588          *     showInfobox: false,
6589          *     zoomX: 0.5,
6590          *     zoomY: 2,
6591          *     fullscreen: { symbol: 'x' },
6592          *     screenshot: { symbol: 'y' },
6593          *     showCopyright: true,
6594          *     showFullscreen: false,
6595          *     showScreenshot: false,
6596          *     showZoom: false,
6597          *     showNavigation: false
6598          * });
6599          * board.setAttribute('showCopyright:false');
6600          *
6601          * var p = board.create('point', [1, 1], {size: 10,
6602          *     label: {
6603          *         fontSize: 24,
6604          *         highlightStrokeOpacity: 0.1,
6605          *         offset: [5, 0]
6606          *     }
6607          * });
6608          *
6609          *
6610          * </pre><div id="JXGea7b8e09-beac-4d95-9a0c-5fc1c761ffbc" class="jxgbox" style="width: 300px; height: 300px;"></div>
6611          * <script type="text/javascript">
6612          *     (function() {
6613          *     const board = JXG.JSXGraph.initBoard('JXGea7b8e09-beac-4d95-9a0c-5fc1c761ffbc', {
6614          *         boundingbox: [-5, 5, 5, -5],
6615          *         keepAspectRatio: false,
6616          *         axis:true,
6617          *         showFullscreen: true,
6618          *         showScreenshot: true,
6619          *         showCopyright: false
6620          *     });
6621          *
6622          *     board.setAttribute({
6623          *         animationDelay: 10,
6624          *         boundingbox: [-10, 5, 10, -5],
6625          *         defaultAxes: {
6626          *             x: { strokeColor: 'blue', ticks: { strokeColor: 'blue'}}
6627          *         },
6628          *         description: 'test',
6629          *         fullscreen: {
6630          *             scale: 0.5
6631          *         },
6632          *         intl: {
6633          *             enabled: true,
6634          *             locale: 'de-DE'
6635          *         }
6636          *     });
6637          *
6638          *     board.setAttribute({
6639          *         selection: {
6640          *             enabled: true,
6641          *             fillColor: 'blue'
6642          *         },
6643          *         showInfobox: false,
6644          *         zoomX: 0.5,
6645          *         zoomY: 2,
6646          *         fullscreen: { symbol: 'x' },
6647          *         screenshot: { symbol: 'y' },
6648          *         showCopyright: true,
6649          *         showFullscreen: false,
6650          *         showScreenshot: false,
6651          *         showZoom: false,
6652          *         showNavigation: false
6653          *     });
6654          *
6655          *     board.setAttribute('showCopyright:false');
6656          *
6657          *     var p = board.create('point', [1, 1], {size: 10,
6658          *         label: {
6659          *             fontSize: 24,
6660          *             highlightStrokeOpacity: 0.1,
6661          *             offset: [5, 0]
6662          *         }
6663          *     });
6664          *
6665          *
6666          *     })();
6667          *
6668          * </script><pre>
6669          *
6670          *
6671          */
6672         setAttribute: function (attr, force) {
6673             var i, arg, pair,
6674                 key, value, oldvalue,// j, le,
6675                 node, lst, e,
6676                 attributes = {};
6677 
6678             // Normalize the user input
6679             for (i = 0; i < arguments.length; i++) {
6680                 arg = arguments[i];
6681                 if (Type.isString(arg)) {
6682                     // pairRaw is string of the form 'key:value'
6683                     pair = arg.split(":");
6684                     attributes[Type.trim(pair[0])] = Type.trim(pair[1]);
6685                 } else if (!Type.isArray(arg)) {
6686                     // pairRaw consists of objects of the form {key1:value1,key2:value2,...}
6687                     JXG.extend(attributes, arg);
6688                 } else {
6689                     // pairRaw consists of array [key,value]
6690                     attributes[arg[0]] = arg[1];
6691                 }
6692             }
6693 
6694             for (i in attributes) {
6695                 if (attributes.hasOwnProperty(i)) {
6696                     key = i.replace(/\s+/g, "").toLowerCase();
6697                     value = attributes[i];
6698                 }
6699                 value = (value.toLowerCase && value.toLowerCase() === 'false')
6700                     ? false
6701                     : value;
6702                 oldvalue = this.attr[key];
6703                 if (!force && oldvalue === value) {
6704                     continue;
6705                 }
6706                 switch (key) {
6707                     case 'axis':
6708                         if (value === false) {
6709                             if (Type.exists(this.defaultAxes)) {
6710                                 this.defaultAxes.x.setAttribute({ visible: false });
6711                                 this.defaultAxes.y.setAttribute({ visible: false });
6712                             }
6713                         } else {
6714                             // TODO
6715                         }
6716                         break;
6717                     case 'cssstyle':
6718                         lst = Type.css2js(value);
6719                         node = this.containerObj;
6720                         // node = this.renderer.svgRoot;
6721                         for (e in lst) if (lst.hasOwnProperty(e)) {
6722                             pair = lst[e];
6723                             node.style[pair.key] = pair.val;
6724                         }
6725 
6726                         this._set(key, value);
6727                         break;
6728                     case 'boundingbox':
6729                         this.setBoundingBox(value, this.keepaspectratio);
6730                         this._set(key, value);
6731                         break;
6732                     case 'defaultaxes':
6733                         if (Type.exists(this.defaultAxes.x) && Type.exists(value.x)) {
6734                             this.defaultAxes.x.setAttribute(value.x);
6735                         }
6736                         if (Type.exists(this.defaultAxes.y) && Type.exists(value.y)) {
6737                             this.defaultAxes.y.setAttribute(value.y);
6738                         }
6739                         break;
6740                     case 'title':
6741                         this.document.getElementById(this.container + '_ARIAlabel')
6742                             .innerText = value;
6743                         this._set(key, value);
6744                         break;
6745                     case 'keepaspectratio':
6746                         this._set(key, value);
6747                         this.setBoundingBox(this.getBoundingBox(), value, 'keep');
6748                         break;
6749 
6750                     // /* eslint-disable no-fallthrough */
6751                     case 'document':
6752                     case 'maxboundingbox':
6753                         this[key] = value;
6754                         this._set(key, value);
6755                         break;
6756 
6757                     case 'zoomx':
6758                     case 'zoomy':
6759                         this[key] = value;
6760                         this._set(key, value);
6761                         this.setZoom(this.attr.zoomx, this.attr.zoomy);
6762                         break;
6763 
6764                     case 'registerevents':
6765                     case 'renderer':
6766                         // immutable, i.e. ignored
6767                         break;
6768 
6769                     case 'fullscreen':
6770                     case 'screenshot':
6771                         node = this.containerObj.ownerDocument.getElementById(
6772                             this.container + '_navigation_' + key);
6773                         if (node && Type.exists(value.symbol)) {
6774                             node.innerText = Type.evaluate(value.symbol);
6775                         }
6776                         this._set(key, value);
6777                         break;
6778 
6779                     case 'selection':
6780                         value.visible = false;
6781                         value.withLines = false;
6782                         value.vertices = { visible: false };
6783                         this._set(key, value);
6784                         break;
6785 
6786                     case 'showcopyright':
6787                         if (this.renderer.type === 'svg') {
6788                             node = this.containerObj.ownerDocument.getElementById(
6789                                 this.renderer.uniqName('licenseText')
6790                             );
6791                             if (node) {
6792                                 node.style.display = ((Type.evaluate(value)) ? 'inline' : 'none');
6793                             } else if (Type.evaluate(value)) {
6794                                 this.renderer.displayCopyright(Const.licenseText, parseInt(this.options.text.fontSize, 10));
6795                             }
6796                         }
6797                         this._set(key, value);
6798                         break;
6799 
6800                     case 'showlogo':
6801                         if (this.renderer.type === 'svg') {
6802                             node = this.containerObj.ownerDocument.getElementById(
6803                                 this.renderer.uniqName('licenseLogo')
6804                             );
6805                             if (node) {
6806                                 node.style.display = ((Type.evaluate(value)) ? 'inline' : 'none');
6807                             } else if (Type.evaluate(value)) {
6808                                 this.renderer.displayLogo(Const.licenseLogo, parseInt(this.options.text.fontSize, 10));
6809                             }
6810                         }
6811                         this._set(key, value);
6812                         break;
6813 
6814                     default:
6815                         if (Type.exists(this.attr[key])) {
6816                             this._set(key, value);
6817                         }
6818                         break;
6819                     // /* eslint-enable no-fallthrough */
6820                 }
6821             }
6822 
6823             // Redraw navbar to handle the remaining show* attributes
6824             node = this.containerObj.ownerDocument.getElementById(this.container + "_navigationbar");
6825             if (Type.exists(node)) {
6826                 node.remove();
6827                 this.renderer.drawNavigationBar(this, this.attr.navbar);
6828             }
6829 
6830             this.triggerEventHandlers(["attribute"], [attributes, this]);
6831             this.fullUpdate();
6832 
6833             return this;
6834         },
6835 
6836         /**
6837          * Adds an animation. Animations are controlled by the boards, so the boards need to be aware of the
6838          * animated elements. This function tells the board about new elements to animate.
6839          * @param {JXG.GeometryElement} element The element which is to be animated.
6840          * @returns {JXG.Board} Reference to the board
6841          */
6842         addAnimation: function (element) {
6843             var that = this;
6844 
6845             this.animationObjects[element.id] = element;
6846 
6847             if (!this.animationIntervalCode) {
6848                 this.animationIntervalCode = window.setInterval(function () {
6849                     that.animate();
6850                 }, element.board.attr.animationdelay);
6851             }
6852 
6853             return this;
6854         },
6855 
6856         /**
6857          * Cancels all running animations.
6858          * @returns {JXG.Board} Reference to the board
6859          */
6860         stopAllAnimation: function () {
6861             var el;
6862 
6863             for (el in this.animationObjects) {
6864                 if (
6865                     this.animationObjects.hasOwnProperty(el) &&
6866                     Type.exists(this.animationObjects[el])
6867                 ) {
6868                     this.animationObjects[el] = null;
6869                     delete this.animationObjects[el];
6870                 }
6871             }
6872 
6873             window.clearInterval(this.animationIntervalCode);
6874             delete this.animationIntervalCode;
6875 
6876             return this;
6877         },
6878 
6879         /**
6880          * General purpose animation function. This currently only supports moving points from one place to another. This
6881          * is faster than managing the animation per point, especially if there is more than one animated point at the same time.
6882          * @returns {JXG.Board} Reference to the board
6883          */
6884         animate: function () {
6885             var props,
6886                 el,
6887                 o,
6888                 newCoords,
6889                 r,
6890                 p,
6891                 c,
6892                 cbtmp,
6893                 count = 0,
6894                 obj = null;
6895 
6896             for (el in this.animationObjects) {
6897                 if (
6898                     this.animationObjects.hasOwnProperty(el) &&
6899                     Type.exists(this.animationObjects[el])
6900                 ) {
6901                     count += 1;
6902                     o = this.animationObjects[el];
6903 
6904                     if (o.animationPath) {
6905                         if (Type.isFunction(o.animationPath)) {
6906                             newCoords = o.animationPath(
6907                                 new Date().getTime() - o.animationStart
6908                             );
6909                         } else {
6910                             newCoords = o.animationPath.pop();
6911                         }
6912 
6913                         if (
6914                             !Type.exists(newCoords) ||
6915                             (!Type.isArray(newCoords) && isNaN(newCoords))
6916                         ) {
6917                             delete o.animationPath;
6918                         } else {
6919                             o.setPositionDirectly(Const.COORDS_BY_USER, newCoords);
6920                             o.fullUpdate();
6921                             obj = o;
6922                         }
6923                     }
6924                     if (o.animationData) {
6925                         c = 0;
6926 
6927                         for (r in o.animationData) {
6928                             if (o.animationData.hasOwnProperty(r)) {
6929                                 p = o.animationData[r].pop();
6930 
6931                                 if (!Type.exists(p)) {
6932                                     delete o.animationData[p];
6933                                 } else {
6934                                     c += 1;
6935                                     props = {};
6936                                     props[r] = p;
6937                                     o.setAttribute(props);
6938                                 }
6939                             }
6940                         }
6941 
6942                         if (c === 0) {
6943                             delete o.animationData;
6944                         }
6945                     }
6946 
6947                     if (!Type.exists(o.animationData) && !Type.exists(o.animationPath)) {
6948                         this.animationObjects[el] = null;
6949                         delete this.animationObjects[el];
6950 
6951                         if (Type.exists(o.animationCallback)) {
6952                             cbtmp = o.animationCallback;
6953                             o.animationCallback = null;
6954                             cbtmp();
6955                         }
6956                     }
6957                 }
6958             }
6959 
6960             if (count === 0) {
6961                 window.clearInterval(this.animationIntervalCode);
6962                 delete this.animationIntervalCode;
6963             } else {
6964                 this.update(obj);
6965             }
6966 
6967             return this;
6968         },
6969 
6970         /**
6971          * Migrate the dependency properties of the point src
6972          * to the point dest and delete the point src.
6973          * For example, a circle around the point src
6974          * receives the new center dest. The old center src
6975          * will be deleted.
6976          * @param {JXG.Point} src Original point which will be deleted
6977          * @param {JXG.Point} dest New point with the dependencies of src.
6978          * @param {Boolean} copyName Flag which decides if the name of the src element is copied to the
6979          *  dest element.
6980          * @returns {JXG.Board} Reference to the board
6981          */
6982         migratePoint: function (src, dest, copyName) {
6983             var child,
6984                 childId,
6985                 prop,
6986                 found,
6987                 i,
6988                 srcLabelId,
6989                 srcHasLabel = false;
6990 
6991             src = this.select(src);
6992             dest = this.select(dest);
6993 
6994             if (Type.exists(src.label)) {
6995                 srcLabelId = src.label.id;
6996                 srcHasLabel = true;
6997                 this.removeObject(src.label);
6998             }
6999 
7000             for (childId in src.childElements) {
7001                 if (src.childElements.hasOwnProperty(childId)) {
7002                     child = src.childElements[childId];
7003                     found = false;
7004 
7005                     for (prop in child) {
7006                         if (child.hasOwnProperty(prop)) {
7007                             if (child[prop] === src) {
7008                                 child[prop] = dest;
7009                                 found = true;
7010                             }
7011                         }
7012                     }
7013 
7014                     if (found) {
7015                         delete src.childElements[childId];
7016                     }
7017 
7018                     for (i = 0; i < child.parents.length; i++) {
7019                         if (child.parents[i] === src.id) {
7020                             child.parents[i] = dest.id;
7021                         }
7022                     }
7023 
7024                     dest.addChild(child);
7025                 }
7026             }
7027 
7028             // The destination object should receive the name
7029             // and the label of the originating (src) object
7030             if (copyName) {
7031                 if (srcHasLabel) {
7032                     delete dest.childElements[srcLabelId];
7033                     delete dest.descendants[srcLabelId];
7034                 }
7035 
7036                 if (dest.label) {
7037                     this.removeObject(dest.label);
7038                 }
7039 
7040                 delete this.elementsByName[dest.name];
7041                 dest.name = src.name;
7042                 if (srcHasLabel) {
7043                     dest.createLabel();
7044                 }
7045             }
7046 
7047             this.removeObject(src);
7048 
7049             if (Type.exists(dest.name) && dest.name !== '') {
7050                 this.elementsByName[dest.name] = dest;
7051             }
7052 
7053             this.fullUpdate();
7054 
7055             return this;
7056         },
7057 
7058         /**
7059          * Initializes color blindness simulation.
7060          * @param {String} deficiency Describes the color blindness deficiency which is simulated. Accepted values are 'protanopia', 'deuteranopia', and 'tritanopia'.
7061          * @returns {JXG.Board} Reference to the board
7062          */
7063         emulateColorblindness: function (deficiency) {
7064             var e, o;
7065 
7066             if (!Type.exists(deficiency)) {
7067                 deficiency = 'none';
7068             }
7069 
7070             if (this.currentCBDef === deficiency) {
7071                 return this;
7072             }
7073 
7074             for (e in this.objects) {
7075                 if (this.objects.hasOwnProperty(e)) {
7076                     o = this.objects[e];
7077 
7078                     if (deficiency !== 'none') {
7079                         if (this.currentCBDef === 'none') {
7080                             // this could be accomplished by JXG.extend, too. But do not use
7081                             // JXG.deepCopy as this could result in an infinite loop because in
7082                             // visProp there could be geometry elements which contain the board which
7083                             // contains all objects which contain board etc.
7084                             o.visPropOriginal = {
7085                                 strokecolor: o.visProp.strokecolor,
7086                                 fillcolor: o.visProp.fillcolor,
7087                                 highlightstrokecolor: o.visProp.highlightstrokecolor,
7088                                 highlightfillcolor: o.visProp.highlightfillcolor
7089                             };
7090                         }
7091                         o.setAttribute({
7092                             strokecolor: Color.rgb2cb(
7093                                 o.eval(o.visPropOriginal.strokecolor),
7094                                 deficiency
7095                             ),
7096                             fillcolor: Color.rgb2cb(
7097                                 o.eval(o.visPropOriginal.fillcolor),
7098                                 deficiency
7099                             ),
7100                             highlightstrokecolor: Color.rgb2cb(
7101                                 o.eval(o.visPropOriginal.highlightstrokecolor),
7102                                 deficiency
7103                             ),
7104                             highlightfillcolor: Color.rgb2cb(
7105                                 o.eval(o.visPropOriginal.highlightfillcolor),
7106                                 deficiency
7107                             )
7108                         });
7109                     } else if (Type.exists(o.visPropOriginal)) {
7110                         JXG.extend(o.visProp, o.visPropOriginal);
7111                     }
7112                 }
7113             }
7114             this.currentCBDef = deficiency;
7115             this.update();
7116 
7117             return this;
7118         },
7119 
7120         /**
7121          * Select a single or multiple elements at once.
7122          * @param {String|Object|function} str The name, id or a reference to a JSXGraph element on this board. An object will
7123          * be used as a filter to return multiple elements at once filtered by the properties of the object.
7124          * @param {Boolean} onlyByIdOrName If true (default:false) elements are only filtered by their id, name or groupId.
7125          * The advanced filters consisting of objects or functions are ignored.
7126          * @returns {JXG.GeometryElement|JXG.Composition}
7127          * @example
7128          * // select the element with name A
7129          * board.select('A');
7130          *
7131          * // select all elements with strokecolor set to 'red' (but not '#ff0000')
7132          * board.select({
7133          *   strokeColor: 'red'
7134          * });
7135          *
7136          * // select all points on or below the x axis and make them black.
7137          * board.select({
7138          *   elementClass: JXG.OBJECT_CLASS_POINT,
7139          *   Y: function (v) {
7140          *     return v <= 0;
7141          *   }
7142          * }).setAttribute({color: 'black'});
7143          *
7144          * // select all elements
7145          * board.select(function (el) {
7146          *   return true;
7147          * });
7148          */
7149         select: function (str, onlyByIdOrName) {
7150             var flist,
7151                 olist,
7152                 i,
7153                 l,
7154                 s = str;
7155 
7156             if (s === null) {
7157                 return s;
7158             }
7159 
7160             // It's a string, most likely an id or a name.
7161             if (Type.isString(s) && s !== '') {
7162                 // Search by ID
7163                 if (Type.exists(this.objects[s])) {
7164                     s = this.objects[s];
7165                     // Search by name
7166                 } else if (Type.exists(this.elementsByName[s])) {
7167                     s = this.elementsByName[s];
7168                     // Search by group ID
7169                 } else if (Type.exists(this.groups[s])) {
7170                     s = this.groups[s];
7171                 }
7172 
7173                 // It's a function or an object, but not an element
7174             } else if (
7175                 !onlyByIdOrName &&
7176                 (Type.isFunction(s) || (Type.isObject(s) && !Type.isFunction(s.setAttribute)))
7177             ) {
7178                 flist = Type.filterElements(this.objectsList, s);
7179 
7180                 olist = {};
7181                 l = flist.length;
7182                 for (i = 0; i < l; i++) {
7183                     olist[flist[i].id] = flist[i];
7184                 }
7185                 s = new Composition(olist);
7186 
7187                 // It's an element which has been deleted (and still hangs around, e.g. in an attractor list
7188             } else if (
7189                 Type.isObject(s) &&
7190                 Type.exists(s.id) &&
7191                 !Type.exists(this.objects[s.id])
7192             ) {
7193                 s = null;
7194             }
7195 
7196             return s;
7197         },
7198 
7199         /**
7200          * Checks if the given point is inside the boundingbox.
7201          * @param {Number|JXG.Coords} x User coordinate or {@link JXG.Coords} object.
7202          * @param {Number} [y] User coordinate. May be omitted in case <tt>x</tt> is a {@link JXG.Coords} object.
7203          * @returns {Boolean}
7204          */
7205         hasPoint: function (x, y) {
7206             var px = x,
7207                 py = y,
7208                 bbox = this.getBoundingBox();
7209 
7210             if (Type.exists(x) && Type.isArray(x.usrCoords)) {
7211                 px = x.usrCoords[1];
7212                 py = x.usrCoords[2];
7213             }
7214 
7215             return !!(
7216                 Type.isNumber(px) &&
7217                 Type.isNumber(py) &&
7218                 bbox[0] < px &&
7219                 px < bbox[2] &&
7220                 bbox[1] > py &&
7221                 py > bbox[3]
7222             );
7223         },
7224 
7225         /**
7226          * Update CSS transformations of type scaling. It is used to correct the mouse position
7227          * in {@link JXG.Board.getMousePosition}.
7228          * The inverse transformation matrix is updated on each mouseDown and touchStart event.
7229          *
7230          * It is up to the user to call this method after an update of the CSS transformation
7231          * in the DOM.
7232          */
7233         updateCSSTransforms: function () {
7234             var obj = this.containerObj,
7235                 o = obj,
7236                 o2 = obj;
7237 
7238             this.cssTransMat = Env.getCSSTransformMatrix(o);
7239 
7240             // Newer variant of walking up the tree.
7241             // We walk up all parent nodes and collect possible CSS transforms.
7242             // Works also for ShadowDOM
7243             if (Type.exists(o.getRootNode)) {
7244                 o = o.parentNode === o.getRootNode() ? o.parentNode.host : o.parentNode;
7245                 while (o) {
7246                     this.cssTransMat = Mat.matMatMult(Env.getCSSTransformMatrix(o), this.cssTransMat);
7247                     o = o.parentNode === o.getRootNode() ? o.parentNode.host : o.parentNode;
7248                 }
7249                 this.cssTransMat = Mat.inverse(this.cssTransMat);
7250             } else {
7251                 /*
7252                  * This is necessary for IE11
7253                  */
7254                 o = o.offsetParent;
7255                 while (o) {
7256                     this.cssTransMat = Mat.matMatMult(Env.getCSSTransformMatrix(o), this.cssTransMat);
7257 
7258                     o2 = o2.parentNode;
7259                     while (o2 !== o) {
7260                         this.cssTransMat = Mat.matMatMult(Env.getCSSTransformMatrix(o), this.cssTransMat);
7261                         o2 = o2.parentNode;
7262                     }
7263                     o = o.offsetParent;
7264                 }
7265                 this.cssTransMat = Mat.inverse(this.cssTransMat);
7266             }
7267             return this;
7268         },
7269 
7270         /**
7271          * Start selection mode. This function can either be triggered from outside or by
7272          * a down event together with correct key pressing. The default keys are
7273          * shift+ctrl. But this can be changed in the options.
7274          *
7275          * Starting from out side can be realized for example with a button like this:
7276          * <pre>
7277          * 	<button onclick='board.startSelectionMode()'>Start</button>
7278          * </pre>
7279          * @example
7280          * //
7281          * // Set a new bounding box from the selection rectangle
7282          * //
7283          * var board = JXG.JSXGraph.initBoard('jxgbox', {
7284          *         boundingBox:[-3,2,3,-2],
7285          *         keepAspectRatio: false,
7286          *         axis:true,
7287          *         selection: {
7288          *             enabled: true,
7289          *             needShift: false,
7290          *             needCtrl: true,
7291          *             withLines: false,
7292          *             vertices: {
7293          *                 visible: false
7294          *             },
7295          *             fillColor: '#ffff00',
7296          *         }
7297          *      });
7298          *
7299          * var f = function f(x) { return Math.cos(x); },
7300          *     curve = board.create('functiongraph', [f]);
7301          *
7302          * board.on('stopselecting', function(){
7303          *     var box = board.stopSelectionMode(),
7304          *
7305          *         // bbox has the coordinates of the selection rectangle.
7306          *         // Attention: box[i].usrCoords have the form [1, x, y], i.e.
7307          *         // are homogeneous coordinates.
7308          *         bbox = box[0].usrCoords.slice(1).concat(box[1].usrCoords.slice(1));
7309          *
7310          *         // Set a new bounding box
7311          *         board.setBoundingBox(bbox, false);
7312          *  });
7313          *
7314          *
7315          * </pre><div class='jxgbox' id='JXG11eff3a6-8c50-11e5-b01d-901b0e1b8723' style='width: 300px; height: 300px;'></div>
7316          * <script type='text/javascript'>
7317          *     (function() {
7318          *     //
7319          *     // Set a new bounding box from the selection rectangle
7320          *     //
7321          *     var board = JXG.JSXGraph.initBoard('JXG11eff3a6-8c50-11e5-b01d-901b0e1b8723', {
7322          *             boundingBox:[-3,2,3,-2],
7323          *             keepAspectRatio: false,
7324          *             axis:true,
7325          *             selection: {
7326          *                 enabled: true,
7327          *                 needShift: false,
7328          *                 needCtrl: true,
7329          *                 withLines: false,
7330          *                 vertices: {
7331          *                     visible: false
7332          *                 },
7333          *                 fillColor: '#ffff00',
7334          *             }
7335          *        });
7336          *
7337          *     var f = function f(x) { return Math.cos(x); },
7338          *         curve = board.create('functiongraph', [f]);
7339          *
7340          *     board.on('stopselecting', function(){
7341          *         var box = board.stopSelectionMode(),
7342          *
7343          *             // bbox has the coordinates of the selection rectangle.
7344          *             // Attention: box[i].usrCoords have the form [1, x, y], i.e.
7345          *             // are homogeneous coordinates.
7346          *             bbox = box[0].usrCoords.slice(1).concat(box[1].usrCoords.slice(1));
7347          *
7348          *             // Set a new bounding box
7349          *             board.setBoundingBox(bbox, false);
7350          *      });
7351          *     })();
7352          *
7353          * </script><pre>
7354          *
7355          */
7356         startSelectionMode: function () {
7357             this.selectingMode = true;
7358             this.selectionPolygon.setAttribute({ visible: true });
7359             this.selectingBox = [
7360                 [0, 0],
7361                 [0, 0]
7362             ];
7363             this._setSelectionPolygonFromBox();
7364             this.selectionPolygon.fullUpdate();
7365         },
7366 
7367         /**
7368          * Finalize the selection: disable selection mode and return the coordinates
7369          * of the selection rectangle.
7370          * @returns {Array} Coordinates of the selection rectangle. The array
7371          * contains two {@link JXG.Coords} objects. One the upper left corner and
7372          * the second for the lower right corner.
7373          */
7374         stopSelectionMode: function () {
7375             this.selectingMode = false;
7376             this.selectionPolygon.setAttribute({ visible: false });
7377             return [
7378                 this.selectionPolygon.vertices[0].coords,
7379                 this.selectionPolygon.vertices[2].coords
7380             ];
7381         },
7382 
7383         /**
7384          * Start the selection of a region.
7385          * @private
7386          * @param  {Array} pos Screen coordiates of the upper left corner of the
7387          * selection rectangle.
7388          */
7389         _startSelecting: function (pos) {
7390             this.isSelecting = true;
7391             this.selectingBox = [
7392                 [pos[0], pos[1]],
7393                 [pos[0], pos[1]]
7394             ];
7395             this._setSelectionPolygonFromBox();
7396         },
7397 
7398         /**
7399          * Update the selection rectangle during a move event.
7400          * @private
7401          * @param  {Array} pos Screen coordiates of the move event
7402          */
7403         _moveSelecting: function (pos) {
7404             if (this.isSelecting) {
7405                 this.selectingBox[1] = [pos[0], pos[1]];
7406                 this._setSelectionPolygonFromBox();
7407                 this.selectionPolygon.fullUpdate();
7408             }
7409         },
7410 
7411         /**
7412          * Update the selection rectangle during an up event. Stop selection.
7413          * @private
7414          * @param  {Object} evt Event object
7415          */
7416         _stopSelecting: function (evt) {
7417             var pos = this.getMousePosition(evt);
7418 
7419             this.isSelecting = false;
7420             this.selectingBox[1] = [pos[0], pos[1]];
7421             this._setSelectionPolygonFromBox();
7422         },
7423 
7424         /**
7425          * Update the Selection rectangle.
7426          * @private
7427          */
7428         _setSelectionPolygonFromBox: function () {
7429             var A = this.selectingBox[0],
7430                 B = this.selectingBox[1];
7431 
7432             this.selectionPolygon.vertices[0].setPositionDirectly(JXG.COORDS_BY_SCREEN, [
7433                 A[0],
7434                 A[1]
7435             ]);
7436             this.selectionPolygon.vertices[1].setPositionDirectly(JXG.COORDS_BY_SCREEN, [
7437                 A[0],
7438                 B[1]
7439             ]);
7440             this.selectionPolygon.vertices[2].setPositionDirectly(JXG.COORDS_BY_SCREEN, [
7441                 B[0],
7442                 B[1]
7443             ]);
7444             this.selectionPolygon.vertices[3].setPositionDirectly(JXG.COORDS_BY_SCREEN, [
7445                 B[0],
7446                 A[1]
7447             ]);
7448         },
7449 
7450         /**
7451          * Test if a down event should start a selection. Test if the
7452          * required keys are pressed. If yes, {@link JXG.Board.startSelectionMode} is called.
7453          * @param  {Object} evt Event object
7454          */
7455         _testForSelection: function (evt) {
7456             if (this._isRequiredKeyPressed(evt, 'selection')) {
7457                 if (!Type.exists(this.selectionPolygon)) {
7458                     this._createSelectionPolygon(this.attr);
7459                 }
7460                 this.startSelectionMode();
7461             }
7462         },
7463 
7464         /**
7465          * Create the internal selection polygon, which will be available as board.selectionPolygon.
7466          * @private
7467          * @param  {Object} attr board attributes, e.g. the subobject board.attr.
7468          * @returns {Object} pointer to the board to enable chaining.
7469          */
7470         _createSelectionPolygon: function (attr) {
7471             var selectionattr;
7472 
7473             if (!Type.exists(this.selectionPolygon)) {
7474                 selectionattr = Type.copyAttributes(attr, Options, 'board', 'selection');
7475                 if (selectionattr.enabled === true) {
7476                     this.selectionPolygon = this.create(
7477                         'polygon',
7478                         [
7479                             [0, 0],
7480                             [0, 0],
7481                             [0, 0],
7482                             [0, 0]
7483                         ],
7484                         selectionattr
7485                     );
7486                 }
7487             }
7488 
7489             return this;
7490         },
7491 
7492         /**
7493          * Reset the sketchcurves in board.sketches[] to length 0 and add the position
7494          * of the event as first point of the sketch curve. Called at down events.
7495          * <p>
7496          * Sets board.isSketching[i] = true where i depends on the finger (1st or 2nd).
7497          *
7498          * @private
7499          * @param {Object} evt Event object
7500          * @see JXG.Board#addToSketchCurve
7501          * @see JXG.Board#finalizeSketchCurve
7502          */
7503         initSketchCurve: function(evt) {
7504             var i, c;
7505             // Init sketchcurves
7506             if (this.mode !== this.BOARD_MODE_MOVE_ORIGIN) {
7507                 // Add coords to sketch curves
7508                 // Only first and second finger are stored
7509                 c = this.getUsrCoordsOfMouse(evt);
7510                 i = (evt.isPrimary) ? 0 : 1;
7511                 if (Type.exists(this.sketches[i])) {
7512                     this.sketches[i].dataX = [c[0]];
7513                     this.sketches[i].dataY = [c[1]];
7514                     this.isSketching[i] = true;
7515                 }
7516             }
7517         },
7518 
7519         /**
7520          * Add the position of the event to the sketchcurve i in board.sketches[].
7521          * Called at move events.
7522          * Point is only added if board.isSketching[i] = true.
7523          *
7524          * @private
7525          * @param {Object} evt Event object
7526          * @see JXG.Board#initSketchCurve
7527          * @see JXG.Board#finalizeSketchCurve
7528          */
7529         addToSketchCurve: function(evt) {
7530             var i, c, len;
7531 
7532             // Add coords to sketchcurves
7533             // Only first and second finger are stored
7534             i = (evt.isPrimary) ? 0 : 1;
7535             if (this.attr.sketches.enabled && this.isSketching[i] === true) {
7536                 if (Type.exists(this.sketches[i])) {
7537                     c = this.getUsrCoordsOfMouse(evt);
7538                     this.sketches[i].dataX.push(c[0]);
7539                     this.sketches[i].dataY.push(c[1]);
7540 
7541                     len = this.sketches[i].evalVisProp('maxlength');
7542                     if (len !== null && this.sketches[i].dataX.length > len) {
7543                         this.sketches[i].dataX = this.sketches[i].dataX.slice(-len);
7544                         this.sketches[i].dataY = this.sketches[i].dataY.slice(-len);
7545                     }
7546                     if (this.sketches[i].evalVisProp('visible')) {
7547                         this.update();
7548                     }
7549                 }
7550             }
7551         },
7552 
7553         /**
7554          * Ends adding points to the sketchcurve i in board.sketches[].
7555          * Called at up events.
7556          * Sets board.isSketching[i] = false.
7557          * Empties the curve if deleteOnUp==true;
7558          *
7559          * @private
7560          * @param {Object} evt Event object
7561          * @see JXG.Board#initSketchCurve
7562          * @see JXG.Board#addToSketchCurve
7563          */
7564         finalizeSketchCurve: function(evt) {
7565             var i;
7566 
7567             // Stop sketching into this.sketches
7568             i = (evt.isPrimary) ? 0 : 1;
7569             if (this.attr.sketches.enabled) {
7570                 if (Type.exists(this.sketches[i])) {
7571                     this.isSketching[i] = false;
7572                     if (this.sketches[i].evalVisProp('deleteOnUp')) {
7573                         this.sketches[i].dataX = [];
7574                         this.sketches[i].dataY = [];
7575                     }
7576                 }
7577             }
7578         },
7579 
7580         /* **************************
7581          *     EVENT DEFINITION
7582          * for documentation purposes
7583          * ************************** */
7584 
7585         //region Event handler documentation
7586 
7587         /**
7588          * @event
7589          * @description Whenever the {@link JXG.Board#setAttribute} is called.
7590          * @name JXG.Board#attribute
7591          * @param {Event} e The browser's event object.
7592          */
7593         __evt__attribute: function (e) { },
7594 
7595         /**
7596          * @event
7597          * @description Whenever the user starts to touch or click the board.
7598          * @name JXG.Board#down
7599          * @param {Event} e The browser's event object.
7600          */
7601         __evt__down: function (e) { },
7602 
7603         /**
7604          * @event
7605          * @description Whenever the user starts to click on the board.
7606          * @name JXG.Board#mousedown
7607          * @param {Event} e The browser's event object.
7608          */
7609         __evt__mousedown: function (e) { },
7610 
7611         /**
7612          * @event
7613          * @description Whenever the user taps the pen on the board.
7614          * @name JXG.Board#pendown
7615          * @param {Event} e The browser's event object.
7616          */
7617         __evt__pendown: function (e) { },
7618 
7619         /**
7620          * @event
7621          * @description Whenever the user starts to click on the board with a
7622          * device sending pointer events.
7623          * @name JXG.Board#pointerdown
7624          * @param {Event} e The browser's event object.
7625          */
7626         __evt__pointerdown: function (e) { },
7627 
7628         /**
7629          * @event
7630          * @description Whenever the user starts to touch the board.
7631          * @name JXG.Board#touchstart
7632          * @param {Event} e The browser's event object.
7633          */
7634         __evt__touchstart: function (e) { },
7635 
7636         /**
7637          * @event
7638          * @description Whenever the user stops to touch or click the board.
7639          * @name JXG.Board#up
7640          * @param {Event} e The browser's event object.
7641          */
7642         __evt__up: function (e) { },
7643 
7644         /**
7645          * @event
7646          * @description Whenever the user releases the mousebutton over the board.
7647          * @name JXG.Board#mouseup
7648          * @param {Event} e The browser's event object.
7649          */
7650         __evt__mouseup: function (e) { },
7651 
7652         /**
7653          * @event
7654          * @description Whenever the user releases the mousebutton over the board with a
7655          * device sending pointer events.
7656          * @name JXG.Board#pointerup
7657          * @param {Event} e The browser's event object.
7658          */
7659         __evt__pointerup: function (e) { },
7660 
7661         /**
7662          * @event
7663          * @description Whenever the user stops touching the board.
7664          * @name JXG.Board#touchend
7665          * @param {Event} e The browser's event object.
7666          */
7667         __evt__touchend: function (e) { },
7668 
7669         /**
7670          * @event
7671          * @description Whenever the user clicks on the board.
7672          * @name JXG.Board#click
7673          * @see JXG.Board#clickDelay
7674          * @param {Event} e The browser's event object.
7675          */
7676         __evt__click: function (e) { },
7677 
7678         /**
7679          * @event
7680          * @description Whenever the user double clicks on the board.
7681          * This event works on desktop browser, but is undefined
7682          * on mobile browsers.
7683          * @name JXG.Board#dblclick
7684          * @see JXG.Board#clickDelay
7685          * @see JXG.Board#dblClickSuppressClick
7686          * @param {Event} e The browser's event object.
7687          */
7688         __evt__dblclick: function (e) { },
7689 
7690         /**
7691          * @event
7692          * @description Whenever the user clicks on the board with a mouse device.
7693          * @name JXG.Board#mouseclick
7694          * @param {Event} e The browser's event object.
7695          */
7696         __evt__mouseclick: function (e) { },
7697 
7698         /**
7699          * @event
7700          * @description Whenever the user double clicks on the board with a mouse device.
7701          * @name JXG.Board#mousedblclick
7702          * @see JXG.Board#clickDelay
7703          * @param {Event} e The browser's event object.
7704          */
7705         __evt__mousedblclick: function (e) { },
7706 
7707         /**
7708          * @event
7709          * @description Whenever the user clicks on the board with a pointer device.
7710          * @name JXG.Board#pointerclick
7711          * @param {Event} e The browser's event object.
7712          */
7713         __evt__pointerclick: function (e) { },
7714 
7715         /**
7716          * @event
7717          * @description Whenever the user double clicks on the board with a pointer device.
7718          * This event works on desktop browser, but is undefined
7719          * on mobile browsers.
7720          * @name JXG.Board#pointerdblclick
7721          * @see JXG.Board#clickDelay
7722          * @param {Event} e The browser's event object.
7723          */
7724         __evt__pointerdblclick: function (e) { },
7725 
7726         /**
7727          * @event
7728          * @description This event is fired whenever the user is moving the finger or mouse pointer over the board.
7729          * @name JXG.Board#move
7730          * @param {Event} e The browser's event object.
7731          * @param {Number} mode The mode the board currently is in
7732          * @see JXG.Board#mode
7733          */
7734         __evt__move: function (e, mode) { },
7735 
7736         /**
7737          * @event
7738          * @description This event is fired whenever the user is moving the mouse over the board.
7739          * @name JXG.Board#mousemove
7740          * @param {Event} e The browser's event object.
7741          * @param {Number} mode The mode the board currently is in
7742          * @see JXG.Board#mode
7743          */
7744         __evt__mousemove: function (e, mode) { },
7745 
7746         /**
7747          * @event
7748          * @description This event is fired whenever the user is moving the pen over the board.
7749          * @name JXG.Board#penmove
7750          * @param {Event} e The browser's event object.
7751          * @param {Number} mode The mode the board currently is in
7752          * @see JXG.Board#mode
7753          */
7754         __evt__penmove: function (e, mode) { },
7755 
7756         /**
7757          * @event
7758          * @description This event is fired whenever the user is moving the mouse over the board with a
7759          * device sending pointer events.
7760          * @name JXG.Board#pointermove
7761          * @param {Event} e The browser's event object.
7762          * @param {Number} mode The mode the board currently is in
7763          * @see JXG.Board#mode
7764          */
7765         __evt__pointermove: function (e, mode) { },
7766 
7767         /**
7768          * @event
7769          * @description This event is fired whenever the user is moving the finger over the board.
7770          * @name JXG.Board#touchmove
7771          * @param {Event} e The browser's event object.
7772          * @param {Number} mode The mode the board currently is in
7773          * @see JXG.Board#mode
7774          */
7775         __evt__touchmove: function (e, mode) { },
7776 
7777         /**
7778          * @event
7779          * @description This event is fired whenever the user is moving an element over the board by
7780          * pressing arrow keys on a keyboard.
7781          * @name JXG.Board#keymove
7782          * @param {Event} e The browser's event object.
7783          * @param {Number} mode The mode the board currently is in
7784          * @see JXG.Board#mode
7785          */
7786         __evt__keymove: function (e, mode) { },
7787 
7788         /**
7789          * @event
7790          * @description Whenever an element is highlighted this event is fired.
7791          * @name JXG.Board#hit
7792          * @param {Event} e The browser's event object.
7793          * @param {JXG.GeometryElement} el The hit element.
7794          * @param target
7795          *
7796          * @example
7797          * var c = board.create('circle', [[1, 1], 2]);
7798          * board.on('hit', function(evt, el) {
7799          *     console.log('JSXGraph example: Hit element', el);
7800          * });
7801          *
7802          * </pre><div id='JXG19eb31ac-88e6-11e8-bcb5-901b0e1b8723' class='jxgbox' style='width: 300px; height: 300px;'></div>
7803          * <script type='text/javascript'>
7804          *     (function() {
7805          *         var board = JXG.JSXGraph.initBoard('JXG19eb31ac-88e6-11e8-bcb5-901b0e1b8723',
7806          *             {boundingbox: [-8, 8, 8,-8], axis: true, showcopyright: false, shownavigation: false});
7807          *     var c = board.create('circle', [[1, 1], 2]);
7808          *     board.on('hit', function(evt, el) {
7809          *         console.log('JSXGraph example: Hit element', el);
7810          *     });
7811          *
7812          *     })();
7813          *
7814          * </script><pre>
7815          */
7816         __evt__hit: function (e, el, target) { },
7817 
7818         /**
7819          * @event
7820          * @description Whenever an element is highlighted this event is fired.
7821          * @name JXG.Board#mousehit
7822          * @see JXG.Board#hit
7823          * @param {Event} e The browser's event object.
7824          * @param {JXG.GeometryElement} el The hit element.
7825          * @param target
7826          */
7827         __evt__mousehit: function (e, el, target) { },
7828 
7829         /**
7830          * @event
7831          * @description This board is updated.
7832          * @name JXG.Board#update
7833          */
7834         __evt__update: function () { },
7835 
7836         /**
7837          * @event
7838          * @description The bounding box of the board has changed.
7839          * @name JXG.Board#boundingbox
7840          */
7841         __evt__boundingbox: function () { },
7842 
7843         /**
7844          * @event
7845          * @description Select a region is started during a down event or by calling
7846          * {@link JXG.Board.startSelectionMode}
7847          * @name JXG.Board#startselecting
7848          */
7849         __evt__startselecting: function () { },
7850 
7851         /**
7852          * @event
7853          * @description Select a region is started during a down event
7854          * from a device sending mouse events or by calling
7855          * {@link JXG.Board.startSelectionMode}.
7856          * @name JXG.Board#mousestartselecting
7857          */
7858         __evt__mousestartselecting: function () { },
7859 
7860         /**
7861          * @event
7862          * @description Select a region is started during a down event
7863          * from a device sending pointer events or by calling
7864          * {@link JXG.Board.startSelectionMode}.
7865          * @name JXG.Board#pointerstartselecting
7866          */
7867         __evt__pointerstartselecting: function () { },
7868 
7869         /**
7870          * @event
7871          * @description Select a region is started during a down event
7872          * from a device sending touch events or by calling
7873          * {@link JXG.Board.startSelectionMode}.
7874          * @name JXG.Board#touchstartselecting
7875          */
7876         __evt__touchstartselecting: function () { },
7877 
7878         /**
7879          * @event
7880          * @description Selection of a region is stopped during an up event.
7881          * @name JXG.Board#stopselecting
7882          */
7883         __evt__stopselecting: function () { },
7884 
7885         /**
7886          * @event
7887          * @description Selection of a region is stopped during an up event
7888          * from a device sending mouse events.
7889          * @name JXG.Board#mousestopselecting
7890          */
7891         __evt__mousestopselecting: function () { },
7892 
7893         /**
7894          * @event
7895          * @description Selection of a region is stopped during an up event
7896          * from a device sending pointer events.
7897          * @name JXG.Board#pointerstopselecting
7898          */
7899         __evt__pointerstopselecting: function () { },
7900 
7901         /**
7902          * @event
7903          * @description Selection of a region is stopped during an up event
7904          * from a device sending touch events.
7905          * @name JXG.Board#touchstopselecting
7906          */
7907         __evt__touchstopselecting: function () { },
7908 
7909         /**
7910          * @event
7911          * @description A move event while selecting of a region is active.
7912          * @name JXG.Board#moveselecting
7913          */
7914         __evt__moveselecting: function () { },
7915 
7916         /**
7917          * @event
7918          * @description A move event while selecting of a region is active
7919          * from a device sending mouse events.
7920          * @name JXG.Board#mousemoveselecting
7921          */
7922         __evt__mousemoveselecting: function () { },
7923 
7924         /**
7925          * @event
7926          * @description Select a region is started during a down event
7927          * from a device sending mouse events.
7928          * @name JXG.Board#pointermoveselecting
7929          */
7930         __evt__pointermoveselecting: function () { },
7931 
7932         /**
7933          * @event
7934          * @description Select a region is started during a down event
7935          * from a device sending touch events.
7936          * @name JXG.Board#touchmoveselecting
7937          */
7938         __evt__touchmoveselecting: function () { },
7939 
7940         /**
7941          * @ignore
7942          */
7943         __evt: function () { },
7944 
7945         //endregion
7946 
7947         /**
7948          * Expand the JSXGraph construction to fullscreen.
7949          * In order to preserve the proportions of the JSXGraph element,
7950          * a wrapper div is created which is set to fullscreen.
7951          * This function is called when fullscreen mode is triggered
7952          * <b>and</b> when it is closed.
7953          * <p>
7954          * The wrapping div has the CSS class 'jxgbox_wrap_private' which is
7955          * defined in the file 'jsxgraph.css'
7956          * <p>
7957          * This feature is not available on iPhones (as of December 2021).
7958          *
7959          * @param {String} id (Optional) id of the div element which is brought to fullscreen.
7960          * If not provided, this defaults to the JSXGraph div. However, it may be necessary for the aspect ratio trick
7961          * which using padding-bottom/top and an out div element. Then, the id of the outer div has to be supplied.
7962          *
7963          * @return {JXG.Board} Reference to the board
7964          *
7965          * @example
7966          * <div id='jxgbox' class='jxgbox' style='width:500px; height:200px;'></div>
7967          * <button onClick='board.toFullscreen()'>Fullscreen</button>
7968          *
7969          * <script language='Javascript' type='text/javascript'>
7970          * var board = JXG.JSXGraph.initBoard('jxgbox', {axis:true, boundingbox:[-5,5,5,-5]});
7971          * var p = board.create('point', [0, 1]);
7972          * </script>
7973          *
7974          * </pre><div id='JXGd5bab8b6-fd40-11e8-ab14-901b0e1b8723' class='jxgbox' style='width: 300px; height: 300px;'></div>
7975          * <script type='text/javascript'>
7976          *      var board_d5bab8b6;
7977          *     (function() {
7978          *         var board = JXG.JSXGraph.initBoard('JXGd5bab8b6-fd40-11e8-ab14-901b0e1b8723',
7979          *             {boundingbox:[-5,5,5,-5], axis: true, showcopyright: false, shownavigation: false});
7980          *         var p = board.create('point', [0, 1]);
7981          *         board_d5bab8b6 = board;
7982          *     })();
7983          * </script>
7984          * <button onClick='board_d5bab8b6.toFullscreen()'>Fullscreen</button>
7985          * <pre>
7986          *
7987          * @example
7988          * <div id='outer' style='max-width: 500px; margin: 0 auto;'>
7989          * <div id='jxgbox' class='jxgbox' style='height: 0; padding-bottom: 100%'></div>
7990          * </div>
7991          * <button onClick='board.toFullscreen('outer')'>Fullscreen</button>
7992          *
7993          * <script language='Javascript' type='text/javascript'>
7994          * var board = JXG.JSXGraph.initBoard('jxgbox', {
7995          *     axis:true,
7996          *     boundingbox:[-5,5,5,-5],
7997          *     fullscreen: { id: 'outer' },
7998          *     showFullscreen: true
7999          * });
8000          * var p = board.create('point', [-2, 3], {});
8001          * </script>
8002          *
8003          * </pre><div id='JXG7103f6b_outer' style='max-width: 500px; margin: 0 auto;'>
8004          * <div id='JXG7103f6be-6993-4ff8-8133-c78e50a8afac' class='jxgbox' style='height: 0; padding-bottom: 100%;'></div>
8005          * </div>
8006          * <button onClick='board_JXG7103f6be.toFullscreen('JXG7103f6b_outer')'>Fullscreen</button>
8007          * <script type='text/javascript'>
8008          *     var board_JXG7103f6be;
8009          *     (function() {
8010          *         var board = JXG.JSXGraph.initBoard('JXG7103f6be-6993-4ff8-8133-c78e50a8afac',
8011          *             {boundingbox: [-8, 8, 8,-8], axis: true, fullscreen: { id: 'JXG7103f6b_outer' }, showFullscreen: true,
8012          *              showcopyright: false, shownavigation: false});
8013          *     var p = board.create('point', [-2, 3], {});
8014          *     board_JXG7103f6be = board;
8015          *     })();
8016          *
8017          * </script><pre>
8018          *
8019          *
8020          */
8021         toFullscreen: function (id) {
8022             var wrap_id,
8023                 wrap_node,
8024                 inner_node,
8025                 dim,
8026                 doc = this.document,
8027                 fullscreenElement;
8028 
8029             id = id || this.container;
8030             this._fullscreen_inner_id = id;
8031             inner_node = doc.getElementById(id);
8032             wrap_id = 'fullscreenwrap_' + id;
8033 
8034             if (!Type.exists(inner_node._cssFullscreenStore)) {
8035                 // Store the actual, absolute size of the div
8036                 // This is used in scaleJSXGraphDiv
8037                 dim = this.containerObj.getBoundingClientRect();
8038                 inner_node._cssFullscreenStore = {
8039                     w: dim.width,
8040                     h: dim.height
8041                 };
8042             }
8043 
8044             // Wrap a div around the JSXGraph div.
8045             // It is removed when fullscreen mode is closed.
8046             if (doc.getElementById(wrap_id)) {
8047                 wrap_node = doc.getElementById(wrap_id);
8048             } else {
8049                 wrap_node = document.createElement('div');
8050                 wrap_node.classList.add('JXG_wrap_private');
8051                 wrap_node.setAttribute('id', wrap_id);
8052                 inner_node.parentNode.insertBefore(wrap_node, inner_node);
8053                 wrap_node.appendChild(inner_node);
8054             }
8055 
8056             // Trigger fullscreen mode
8057             wrap_node.requestFullscreen =
8058                 wrap_node.requestFullscreen ||
8059                 wrap_node.webkitRequestFullscreen ||
8060                 wrap_node.mozRequestFullScreen ||
8061                 wrap_node.msRequestFullscreen;
8062 
8063             if (doc.fullscreenElement !== undefined) {
8064                 fullscreenElement = doc.fullscreenElement;
8065             } else if (doc.webkitFullscreenElement !== undefined) {
8066                 fullscreenElement = doc.webkitFullscreenElement;
8067             } else {
8068                 fullscreenElement = doc.msFullscreenElement;
8069             }
8070 
8071             if (fullscreenElement === null) {
8072                 // Start fullscreen mode
8073                 if (wrap_node.requestFullscreen) {
8074                     wrap_node.requestFullscreen();
8075                     this.startFullscreenResizeObserver(wrap_node);
8076                 }
8077             } else {
8078                 this.stopFullscreenResizeObserver(wrap_node);
8079                 if (Type.exists(document.exitFullscreen)) {
8080                     document.exitFullscreen();
8081                 } else if (Type.exists(document.webkitExitFullscreen)) {
8082                     document.webkitExitFullscreen();
8083                 }
8084             }
8085 
8086             return this;
8087         },
8088 
8089         /**
8090          * If fullscreen mode is toggled, the possible CSS transformations
8091          * which are applied to the JSXGraph canvas have to be reread.
8092          * Otherwise the position of upper left corner is wrongly interpreted.
8093          *
8094          * @param  {Object} evt fullscreen event object (unused)
8095          */
8096         fullscreenListener: function (evt) {
8097             var inner_id,
8098                 inner_node,
8099                 fullscreenElement,
8100                 doc = this.document;
8101 
8102             inner_id = this._fullscreen_inner_id;
8103             if (!Type.exists(inner_id)) {
8104                 return;
8105             }
8106 
8107             if (doc.fullscreenElement !== undefined) {
8108                 fullscreenElement = doc.fullscreenElement;
8109             } else if (doc.webkitFullscreenElement !== undefined) {
8110                 fullscreenElement = doc.webkitFullscreenElement;
8111             } else {
8112                 fullscreenElement = doc.msFullscreenElement;
8113             }
8114 
8115             inner_node = doc.getElementById(inner_id);
8116             // If full screen mode is started we have to remove CSS margin around the JSXGraph div.
8117             // Otherwise, the positioning of the fullscreen div will be false.
8118             // When leaving the fullscreen mode, the margin is put back in.
8119             if (fullscreenElement) {
8120                 // Just entered fullscreen mode
8121 
8122                 // Store the original data.
8123                 // Further, the CSS margin has to be removed when in fullscreen mode,
8124                 // and must be restored later.
8125                 //
8126                 // Obsolete:
8127                 // It is used in AbstractRenderer.updateText to restore the scaling matrix
8128                 // which is removed by MathJax.
8129                 inner_node._cssFullscreenStore.id = fullscreenElement.id;
8130                 inner_node._cssFullscreenStore.isFullscreen = true;
8131                 inner_node._cssFullscreenStore.margin = inner_node.style.margin;
8132                 inner_node._cssFullscreenStore.width = inner_node.style.width;
8133                 inner_node._cssFullscreenStore.height = inner_node.style.height;
8134                 inner_node._cssFullscreenStore.transform = inner_node.style.transform;
8135                 // Be sure to replace relative width / height units by absolute units
8136                 inner_node.style.width = inner_node._cssFullscreenStore.w + 'px';
8137                 inner_node.style.height = inner_node._cssFullscreenStore.h + 'px';
8138                 inner_node.style.margin = '';
8139 
8140                 // Do the shifting and scaling via CSS properties
8141                 // We do this after fullscreen mode has been established to get the correct size
8142                 // of the JSXGraph div.
8143                 Env.scaleJSXGraphDiv(fullscreenElement.id, inner_id, doc,
8144                     Type.evaluate(this.attr.fullscreen.scale));
8145 
8146                 // Clear this.doc.fullscreenElement, because Safari doesn't to it and
8147                 // when leaving full screen mode it is still set.
8148                 fullscreenElement = null;
8149             } else if (Type.exists(inner_node._cssFullscreenStore)) {
8150                 // Just left the fullscreen mode
8151 
8152                 inner_node._cssFullscreenStore.isFullscreen = false;
8153                 inner_node.style.margin = inner_node._cssFullscreenStore.margin;
8154                 inner_node.style.width = inner_node._cssFullscreenStore.width;
8155                 inner_node.style.height = inner_node._cssFullscreenStore.height;
8156                 inner_node.style.transform = inner_node._cssFullscreenStore.transform;
8157                 inner_node._cssFullscreenStore = null;
8158 
8159                 // Remove the wrapper div
8160                 inner_node.parentElement.replaceWith(inner_node);
8161             }
8162 
8163             this.updateCSSTransforms();
8164         },
8165 
8166         /**
8167          * Start resize observer to handle
8168          * orientation changes in fullscreen mode.
8169          *
8170          * @param {Object} node DOM object which is in fullscreen mode. It is the wrapper element
8171          * around the JSXGraph div.
8172          * @returns {JXG.Board} Reference to the board
8173          * @private
8174          * @see JXG.Board#toFullscreen
8175          *
8176          */
8177         startFullscreenResizeObserver: function(node) {
8178             var that = this;
8179 
8180             if (!Env.isBrowser || !this.attr.resize || !this.attr.resize.enabled) {
8181                 return this;
8182             }
8183 
8184             this.resizeObserver = new ResizeObserver(function (entries) {
8185                 var inner_id,
8186                     fullscreenElement,
8187                     doc = that.document;
8188 
8189                 if (!that._isResizing) {
8190                     that._isResizing = true;
8191                     window.setTimeout(function () {
8192                         try {
8193                             inner_id = that._fullscreen_inner_id;
8194                             if (doc.fullscreenElement !== undefined) {
8195                                 fullscreenElement = doc.fullscreenElement;
8196                             } else if (doc.webkitFullscreenElement !== undefined) {
8197                                 fullscreenElement = doc.webkitFullscreenElement;
8198                             } else {
8199                                 fullscreenElement = doc.msFullscreenElement;
8200                             }
8201                             if (fullscreenElement !== null) {
8202                                 Env.scaleJSXGraphDiv(fullscreenElement.id, inner_id, doc,
8203                                     Type.evaluate(that.attr.fullscreen.scale));
8204                             }
8205                         } catch (err) {
8206                             that.stopFullscreenResizeObserver(node);
8207                         } finally {
8208                             that._isResizing = false;
8209                         }
8210                     }, that.attr.resize.throttle);
8211                 }
8212             });
8213             this.resizeObserver.observe(node);
8214             return this;
8215         },
8216 
8217         /**
8218          * Remove resize observer to handle orientation changes in fullscreen mode.
8219          * @param {Object} node DOM object which is in fullscreen mode. It is the wrapper element
8220          * around the JSXGraph div.
8221          * @returns {JXG.Board} Reference to the board
8222          * @private
8223          * @see JXG.Board#toFullscreen
8224          */
8225         stopFullscreenResizeObserver: function(node) {
8226             if (!Env.isBrowser || !this.attr.resize || !this.attr.resize.enabled) {
8227                 return this;
8228             }
8229 
8230             if (Type.exists(this.resizeObserver)) {
8231                 this.resizeObserver.unobserve(node);
8232             }
8233             return this;
8234         },
8235 
8236         /**
8237          * Add user activity to the array 'board.userLog'.
8238          *
8239          * @param {String} type Event type, e.g. 'drag'
8240          * @param {Object} obj JSXGraph element object
8241          *
8242          * @see JXG.Board#userLog
8243          * @return {JXG.Board} Reference to the board
8244          */
8245         addLogEntry: function (type, obj, pos) {
8246             var t, id,
8247                 last = this.userLog.length - 1;
8248 
8249             if (Type.exists(obj.elementClass)) {
8250                 id = obj.id;
8251             }
8252             if (Type.evaluate(this.attr.logging.enabled)) {
8253                 t = (new Date()).getTime();
8254                 if (last >= 0 &&
8255                     this.userLog[last].type === type &&
8256                     this.userLog[last].id === id &&
8257                     // Distinguish consecutive drag events of
8258                     // the same element
8259                     t - this.userLog[last].end < 500) {
8260 
8261                     this.userLog[last].end = t;
8262                     this.userLog[last].endpos = pos;
8263                 } else {
8264                     this.userLog.push({
8265                         type: type,
8266                         id: id,
8267                         start: t,
8268                         startpos: pos,
8269                         end: t,
8270                         endpos: pos,
8271                         bbox: this.getBoundingBox(),
8272                         canvas: [this.canvasWidth, this.canvasHeight],
8273                         zoom: [this.zoomX, this.zoomY]
8274                     });
8275                 }
8276             }
8277             return this;
8278         },
8279 
8280         /**
8281          * Function to animate a curve rolling on another curve.
8282          * @param {Curve} c1 JSXGraph curve building the floor where c2 rolls
8283          * @param {Curve} c2 JSXGraph curve which rolls on c1.
8284          * @param {number} start_c1 The parameter t such that c1(t) touches c2. This is the start position of the
8285          *                          rolling process
8286          * @param {Number} stepsize Increase in t in each step for the curve c1
8287          * @param {Number} direction
8288          * @param {Number} time Delay time for setInterval()
8289          * @param {Array} pointlist Array of points which are rolled in each step. This list should contain
8290          *      all points which define c2 and gliders on c2.
8291          *
8292          * @example
8293          *
8294          * // Line which will be the floor to roll upon.
8295          * var line = board.create('curve', [function (t) { return t;}, function (t){ return 1;}], {strokeWidth:6});
8296          * // Center of the rolling circle
8297          * var C = board.create('point',[0,2],{name:'C'});
8298          * // Starting point of the rolling circle
8299          * var P = board.create('point',[0,1],{name:'P', trace:true});
8300          * // Circle defined as a curve. The circle 'starts' at P, i.e. circle(0) = P
8301          * var circle = board.create('curve',[
8302          *           function (t){var d = P.Dist(C),
8303          *                           beta = JXG.Math.Geometry.rad([C.X()+1,C.Y()],C,P);
8304          *                       t += beta;
8305          *                       return C.X()+d*Math.cos(t);
8306          *           },
8307          *           function (t){var d = P.Dist(C),
8308          *                           beta = JXG.Math.Geometry.rad([C.X()+1,C.Y()],C,P);
8309          *                       t += beta;
8310          *                       return C.Y()+d*Math.sin(t);
8311          *           },
8312          *           0,2*Math.PI],
8313          *           {strokeWidth:6, strokeColor:'green'});
8314          *
8315          * // Point on circle
8316          * var B = board.create('glider',[0,2,circle],{name:'B', color:'blue',trace:false});
8317          * var roll = board.createRoulette(line, circle, 0, Math.PI/20, 1, 100, [C,P,B]);
8318          * roll.start() // Start the rolling, to be stopped by roll.stop()
8319          *
8320          * </pre><div class='jxgbox' id='JXGe5e1b53c-a036-4a46-9e35-190d196beca5' style='width: 300px; height: 300px;'></div>
8321          * <script type='text/javascript'>
8322          * var brd = JXG.JSXGraph.initBoard('JXGe5e1b53c-a036-4a46-9e35-190d196beca5', {boundingbox: [-5, 5, 5, -5], axis: true, showcopyright:false, shownavigation: false});
8323          * // Line which will be the floor to roll upon.
8324          * var line = brd.create('curve', [function (t) { return t;}, function (t){ return 1;}], {strokeWidth:6});
8325          * // Center of the rolling circle
8326          * var C = brd.create('point',[0,2],{name:'C'});
8327          * // Starting point of the rolling circle
8328          * var P = brd.create('point',[0,1],{name:'P', trace:true});
8329          * // Circle defined as a curve. The circle 'starts' at P, i.e. circle(0) = P
8330          * var circle = brd.create('curve',[
8331          *           function (t){var d = P.Dist(C),
8332          *                           beta = JXG.Math.Geometry.rad([C.X()+1,C.Y()],C,P);
8333          *                       t += beta;
8334          *                       return C.X()+d*Math.cos(t);
8335          *           },
8336          *           function (t){var d = P.Dist(C),
8337          *                           beta = JXG.Math.Geometry.rad([C.X()+1,C.Y()],C,P);
8338          *                       t += beta;
8339          *                       return C.Y()+d*Math.sin(t);
8340          *           },
8341          *           0,2*Math.PI],
8342          *           {strokeWidth:6, strokeColor:'green'});
8343          *
8344          * // Point on circle
8345          * var B = brd.create('glider',[0,2,circle],{name:'B', color:'blue',trace:false});
8346          * var roll = brd.createRoulette(line, circle, 0, Math.PI/20, 1, 100, [C,P,B]);
8347          * roll.start() // Start the rolling, to be stopped by roll.stop()
8348          * </script><pre>
8349          */
8350         createRoulette: function (c1, c2, start_c1, stepsize, direction, time, pointlist) {
8351             var brd = this,
8352                 Roulette = function () {
8353                     var alpha = 0,
8354                         Tx = 0,
8355                         Ty = 0,
8356                         t1 = start_c1,
8357                         t2 = Numerics.root(
8358                             function (t) {
8359                                 var c1x = c1.X(t1),
8360                                     c1y = c1.Y(t1),
8361                                     c2x = c2.X(t),
8362                                     c2y = c2.Y(t);
8363 
8364                                 return (c1x - c2x) * (c1x - c2x) + (c1y - c2y) * (c1y - c2y);
8365                             },
8366                             [0, Math.PI * 2]
8367                         ),
8368                         t1_new = 0.0,
8369                         t2_new = 0.0,
8370                         c1dist,
8371                         rotation = brd.create(
8372                             'transform',
8373                             [
8374                                 function () {
8375                                     return alpha;
8376                                 }
8377                             ],
8378                             { type: 'rotate' }
8379                         ),
8380                         rotationLocal = brd.create(
8381                             'transform',
8382                             [
8383                                 function () {
8384                                     return alpha;
8385                                 },
8386                                 function () {
8387                                     return c1.X(t1);
8388                                 },
8389                                 function () {
8390                                     return c1.Y(t1);
8391                                 }
8392                             ],
8393                             { type: 'rotate' }
8394                         ),
8395                         translate = brd.create(
8396                             'transform',
8397                             [
8398                                 function () {
8399                                     return Tx;
8400                                 },
8401                                 function () {
8402                                     return Ty;
8403                                 }
8404                             ],
8405                             { type: 'translate' }
8406                         ),
8407                         // arc length via Simpson's rule.
8408                         arclen = function (c, a, b) {
8409                             var cpxa = Numerics.D(c.X)(a),
8410                                 cpya = Numerics.D(c.Y)(a),
8411                                 cpxb = Numerics.D(c.X)(b),
8412                                 cpyb = Numerics.D(c.Y)(b),
8413                                 cpxab = Numerics.D(c.X)((a + b) * 0.5),
8414                                 cpyab = Numerics.D(c.Y)((a + b) * 0.5),
8415                                 fa = Mat.hypot(cpxa, cpya),
8416                                 fb = Mat.hypot(cpxb, cpyb),
8417                                 fab = Mat.hypot(cpxab, cpyab);
8418 
8419                             return ((fa + 4 * fab + fb) * (b - a)) / 6;
8420                         },
8421                         exactDist = function (t) {
8422                             return c1dist - arclen(c2, t2, t);
8423                         },
8424                         beta = Math.PI / 18,
8425                         beta9 = beta * 9,
8426                         interval = null;
8427 
8428                     this.rolling = function () {
8429                         var h, g, hp, gp, z;
8430 
8431                         t1_new = t1 + direction * stepsize;
8432 
8433                         // arc length between c1(t1) and c1(t1_new)
8434                         c1dist = arclen(c1, t1, t1_new);
8435 
8436                         // find t2_new such that arc length between c2(t2) and c1(t2_new) equals c1dist.
8437                         t2_new = Numerics.root(exactDist, t2);
8438 
8439                         // c1(t) as complex number
8440                         h = new Complex(c1.X(t1_new), c1.Y(t1_new));
8441 
8442                         // c2(t) as complex number
8443                         g = new Complex(c2.X(t2_new), c2.Y(t2_new));
8444 
8445                         hp = new Complex(Numerics.D(c1.X)(t1_new), Numerics.D(c1.Y)(t1_new));
8446                         gp = new Complex(Numerics.D(c2.X)(t2_new), Numerics.D(c2.Y)(t2_new));
8447 
8448                         // z is angle between the tangents of c1 at t1_new, and c2 at t2_new
8449                         z = Complex.C.div(hp, gp);
8450 
8451                         alpha = Math.atan2(z.imaginary, z.real);
8452                         // Normalizing the quotient
8453                         z.div(Complex.C.abs(z));
8454                         z.mult(g);
8455                         Tx = h.real - z.real;
8456 
8457                         // T = h(t1_new)-g(t2_new)*h'(t1_new)/g'(t2_new);
8458                         Ty = h.imaginary - z.imaginary;
8459 
8460                         // -(10-90) degrees: make corners roll smoothly
8461                         if (alpha < -beta && alpha > -beta9) {
8462                             alpha = -beta;
8463                             rotationLocal.applyOnce(pointlist);
8464                         } else if (alpha > beta && alpha < beta9) {
8465                             alpha = beta;
8466                             rotationLocal.applyOnce(pointlist);
8467                         } else {
8468                             rotation.applyOnce(pointlist);
8469                             translate.applyOnce(pointlist);
8470                             t1 = t1_new;
8471                             t2 = t2_new;
8472                         }
8473                         brd.update();
8474                     };
8475 
8476                     this.start = function () {
8477                         if (time > 0) {
8478                             interval = window.setInterval(this.rolling, time);
8479                         }
8480                         return this;
8481                     };
8482 
8483                     this.stop = function () {
8484                         window.clearInterval(interval);
8485                         return this;
8486                     };
8487                     return this;
8488                 };
8489             return new Roulette();
8490         }
8491     }
8492 );
8493 
8494 export default JXG.Board;
8495