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                     this.printListenerMatchBound = this.printListenerMatch.bind(this);
2118                     this.printMediaQuery = window.matchMedia('print');
2119                     this.screenMediaQuery = window.matchMedia('screen');
2120                     this.printMediaQuery.addEventListener('change', this.printListenerMatchBound);
2121                     this.screenMediaQuery.addEventListener('change', this.printListenerMatchBound);
2122                     this.resizeHandlers.push('print');
2123                 } catch (err) {
2124                     JXG.debug("Error adding printListener", err);
2125                 }
2126                 // if (Type.isFunction(MediaQueryList.prototype.addEventListener)) {
2127                 //     window.matchMedia('print').addEventListener('change', function (mql) {
2128                 //         if (mql.matches) {
2129                 //             that.printListener();
2130                 //         }
2131                 //     });
2132                 // } else if (Type.isFunction(MediaQueryList.prototype.addListener)) { // addListener might be deprecated
2133                 //     window.matchMedia('print').addListener(function (mql, ev) {
2134                 //         if (mql.matches) {
2135                 //             that.printListener(ev);
2136                 //         }
2137                 //     });
2138                 // }
2139 
2140                 // When closing the print dialog we again have to resize.
2141                 // Env.addEvent(window, 'afterprint', this.printListener, this);
2142                 // this.resizeHandlers.push('afterprint');
2143             }
2144         },
2145 
2146         /**
2147          * Remove resize related event handlers
2148          *
2149          */
2150         removeResizeEventHandlers: function () {
2151             var i, e;
2152             if (this.resizeHandlers.length > 0 && Env.isBrowser) {
2153                 for (i = 0; i < this.resizeHandlers.length; i++) {
2154                     e = this.resizeHandlers[i];
2155                     switch (e) {
2156                         case 'resizeobserver':
2157                             if (Type.exists(this.resizeObserver)) {
2158                                 this.stopResizeObserver();
2159                             }
2160                             break;
2161                         case 'resize':
2162                             Env.removeEvent(window, 'resize', this.resizeListener, this);
2163                             if (Type.exists(this.intersectionObserver)) {
2164                                 this.stopIntersectionObserver();
2165                             }
2166                             break;
2167                         case 'scroll':
2168                             Env.removeEvent(window, 'scroll', this.scrollListener, this);
2169                             break;
2170                         case 'print':
2171                             this.printMediaQuery.removeEventListener('change', this.printListenerMatchBound, false);
2172                             this.screenMediaQuery.removeEventListener('change', this.printListenerMatchBound, false);
2173                             this.printMediaQuery = null;
2174                             this.screenMediaQuery = null;
2175                             this.printListenerMatchBound = null;
2176                             break;
2177                         // case 'afterprint':
2178                         //     Env.removeEvent(window, 'afterprint', this.printListener, this);
2179                         //     break;
2180                     }
2181                 }
2182                 this.resizeHandlers = [];
2183             }
2184         },
2185 
2186 
2187         /**
2188          * Registers pointer event handlers.
2189          */
2190         addPointerEventHandlers: function () {
2191             if (!this.hasPointerHandlers && Env.isBrowser) {
2192                 var moveTarget = this.attr.movetarget || this.containerObj;
2193 
2194                 if (window.navigator.msPointerEnabled) {
2195                     // IE10-
2196                     // Env.addEvent(this.containerObj, 'MSPointerDown', this.pointerDownListener, this);
2197                     Env.addEvent(moveTarget, 'MSPointerDown', this.pointerDownListener, this);
2198                     Env.addEvent(moveTarget, 'MSPointerMove', this.pointerMoveListener, this);
2199                 } else {
2200                     // Env.addEvent(this.containerObj, 'pointerdown', this.pointerDownListener, this);
2201                     Env.addEvent(moveTarget, 'pointerdown', this.pointerDownListener, this);
2202                     Env.addEvent(moveTarget, 'pointermove', this.pointerMoveListener, this);
2203                     Env.addEvent(moveTarget, 'pointerleave', this.pointerLeaveListener, this);
2204                     Env.addEvent(moveTarget, 'click', this.pointerClickListener, this);
2205                     Env.addEvent(moveTarget, 'dblclick', this.pointerDblClickListener, this);
2206                 }
2207 
2208                 if (this.containerObj !== null) {
2209                     // This is needed for capturing touch events.
2210                     // It is in jsxgraph.css, for ms-touch-action...
2211                     this.containerObj.style.touchAction = 'none';
2212                     // this.containerObj.style.touchAction = 'auto';
2213                 }
2214 
2215                 this.hasPointerHandlers = true;
2216             }
2217         },
2218 
2219         /**
2220          * Registers mouse move, down and wheel event handlers.
2221          */
2222         addMouseEventHandlers: function () {
2223             if (!this.hasMouseHandlers && Env.isBrowser) {
2224                 var moveTarget = this.attr.movetarget || this.containerObj;
2225 
2226                 // Env.addEvent(this.containerObj, 'mousedown', this.mouseDownListener, this);
2227                 Env.addEvent(moveTarget, 'mousedown', this.mouseDownListener, this);
2228                 Env.addEvent(moveTarget, 'mousemove', this.mouseMoveListener, this);
2229                 Env.addEvent(moveTarget, 'click', this.mouseClickListener, this);
2230                 Env.addEvent(moveTarget, 'dblclick', this.mouseDblClickListener, this);
2231 
2232                 this.hasMouseHandlers = true;
2233             }
2234         },
2235 
2236         /**
2237          * Register touch start and move and gesture start and change event handlers.
2238          * @param {Boolean} appleGestures If set to false the gesturestart and gesturechange event handlers
2239          * will not be registered.
2240          *
2241          * Since iOS 13, touch events were abandoned in favour of pointer events
2242          */
2243         addTouchEventHandlers: function (appleGestures) {
2244             if (!this.hasTouchHandlers && Env.isBrowser) {
2245                 var moveTarget = this.attr.movetarget || this.containerObj;
2246 
2247                 // Env.addEvent(this.containerObj, 'touchstart', this.touchStartListener, this);
2248                 Env.addEvent(moveTarget, 'touchstart', this.touchStartListener, this);
2249                 Env.addEvent(moveTarget, 'touchmove', this.touchMoveListener, this);
2250 
2251                 /*
2252                 if (!Type.exists(appleGestures) || appleGestures) {
2253                     // Gesture listener are called in touchStart and touchMove.
2254                     //Env.addEvent(this.containerObj, 'gesturestart', this.gestureStartListener, this);
2255                     //Env.addEvent(this.containerObj, 'gesturechange', this.gestureChangeListener, this);
2256                 }
2257                 */
2258 
2259                 this.hasTouchHandlers = true;
2260             }
2261         },
2262 
2263         /**
2264          * Registers pointer event handlers.
2265          */
2266         addWheelEventHandlers: function () {
2267             if (!this.hasWheelHandlers && Env.isBrowser) {
2268                 Env.addEvent(this.containerObj, 'mousewheel', this.mouseWheelListener, this);
2269                 Env.addEvent(this.containerObj, 'DOMMouseScroll', this.mouseWheelListener, this);
2270                 this.hasWheelHandlers = true;
2271             }
2272         },
2273 
2274         /**
2275          * Add fullscreen events which update the CSS transformation matrix to correct
2276          * the mouse/touch/pointer positions in case of CSS transformations.
2277          */
2278         addFullscreenEventHandlers: function () {
2279             var i,
2280                 // standard/Edge, firefox, chrome/safari, IE11
2281                 events = [
2282                     'fullscreenchange',
2283                     'mozfullscreenchange',
2284                     'webkitfullscreenchange',
2285                     'msfullscreenchange'
2286                 ],
2287                 le = events.length;
2288 
2289             if (!this.hasFullscreenEventHandlers && Env.isBrowser) {
2290                 for (i = 0; i < le; i++) {
2291                     Env.addEvent(this.document, events[i], this.fullscreenListener, this);
2292                 }
2293                 this.hasFullscreenEventHandlers = true;
2294             }
2295         },
2296 
2297         /**
2298          * Register keyboard event handlers.
2299          */
2300         addKeyboardEventHandlers: function () {
2301             if (this.attr.keyboard.enabled && !this.hasKeyboardHandlers && Env.isBrowser) {
2302                 Env.addEvent(this.containerObj, 'keydown', this.keyDownListener, this);
2303                 Env.addEvent(this.containerObj, 'focusin', this.keyFocusInListener, this);
2304                 Env.addEvent(this.containerObj, 'focusout', this.keyFocusOutListener, this);
2305                 this.hasKeyboardHandlers = true;
2306             }
2307         },
2308 
2309         /**
2310          * Remove all registered touch event handlers.
2311          */
2312         removeKeyboardEventHandlers: function () {
2313             if (this.hasKeyboardHandlers && Env.isBrowser) {
2314                 Env.removeEvent(this.containerObj, 'keydown', this.keyDownListener, this);
2315                 Env.removeEvent(this.containerObj, 'focusin', this.keyFocusInListener, this);
2316                 Env.removeEvent(this.containerObj, 'focusout', this.keyFocusOutListener, this);
2317                 this.hasKeyboardHandlers = false;
2318             }
2319         },
2320 
2321         /**
2322          * Remove all registered event handlers regarding fullscreen mode.
2323          */
2324         removeFullscreenEventHandlers: function () {
2325             var i,
2326                 // standard/Edge, firefox, chrome/safari, IE11
2327                 events = [
2328                     'fullscreenchange',
2329                     'mozfullscreenchange',
2330                     'webkitfullscreenchange',
2331                     'msfullscreenchange'
2332                 ],
2333                 le = events.length;
2334 
2335             if (this.hasFullscreenEventHandlers && Env.isBrowser) {
2336                 for (i = 0; i < le; i++) {
2337                     Env.removeEvent(this.document, events[i], this.fullscreenListener, this);
2338                 }
2339                 this.hasFullscreenEventHandlers = false;
2340             }
2341         },
2342 
2343         /**
2344          * Remove MSPointer* Event handlers.
2345          */
2346         removePointerEventHandlers: function () {
2347             if (this.hasPointerHandlers && Env.isBrowser) {
2348                 var moveTarget = this.attr.movetarget || this.containerObj;
2349 
2350                 if (window.navigator.msPointerEnabled) {
2351                     // IE10-
2352                     // Env.removeEvent(this.containerObj, 'MSPointerDown', this.pointerDownListener, this);
2353                     Env.removeEvent(moveTarget, 'MSPointerDown', this.pointerDownListener, this);
2354                     Env.removeEvent(moveTarget, 'MSPointerMove', this.pointerMoveListener, this);
2355                 } else {
2356                     // Env.removeEvent(this.containerObj, 'pointerdown', this.pointerDownListener, this);
2357                     Env.removeEvent(moveTarget, 'pointerdown', this.pointerDownListener, this);
2358                     Env.removeEvent(moveTarget, 'pointermove', this.pointerMoveListener, this);
2359                     Env.removeEvent(moveTarget, 'pointerleave', this.pointerLeaveListener, this);
2360                     Env.removeEvent(moveTarget, 'click', this.pointerClickListener, this);
2361                     Env.removeEvent(moveTarget, 'dblclick', this.pointerDblClickListener, this);
2362                 }
2363 
2364                 if (this.hasWheelHandlers) {
2365                     Env.removeEvent(this.containerObj, 'mousewheel', this.mouseWheelListener, this);
2366                     Env.removeEvent(this.containerObj, 'DOMMouseScroll', this.mouseWheelListener, this);
2367                 }
2368 
2369                 if (this.hasPointerUp) {
2370                     if (window.navigator.msPointerEnabled) {
2371                         // IE10-
2372                         Env.removeEvent(this.document, 'MSPointerUp', this.pointerUpListener, this);
2373                     } else {
2374                         Env.removeEvent(this.document, 'pointerup', this.pointerUpListener, this);
2375                         Env.removeEvent(this.document, 'pointercancel', this.pointerUpListener, this);
2376                     }
2377                     this.hasPointerUp = false;
2378                 }
2379 
2380                 this.hasPointerHandlers = false;
2381             }
2382         },
2383 
2384         /**
2385          * De-register mouse event handlers.
2386          */
2387         removeMouseEventHandlers: function () {
2388             if (this.hasMouseHandlers && Env.isBrowser) {
2389                 var moveTarget = this.attr.movetarget || this.containerObj;
2390 
2391                 // Env.removeEvent(this.containerObj, 'mousedown', this.mouseDownListener, this);
2392                 Env.removeEvent(moveTarget, 'mousedown', this.mouseDownListener, this);
2393                 Env.removeEvent(moveTarget, 'mousemove', this.mouseMoveListener, this);
2394                 Env.removeEvent(moveTarget, 'click', this.mouseClickListener, this);
2395                 Env.removeEvent(moveTarget, 'dblclick', this.mouseDblClickListener, this);
2396 
2397                 if (this.hasMouseUp) {
2398                     Env.removeEvent(this.document, 'mouseup', this.mouseUpListener, this);
2399                     this.hasMouseUp = false;
2400                 }
2401 
2402                 if (this.hasWheelHandlers) {
2403                     Env.removeEvent(this.containerObj, 'mousewheel', this.mouseWheelListener, this);
2404                     Env.removeEvent(
2405                         this.containerObj,
2406                         'DOMMouseScroll',
2407                         this.mouseWheelListener,
2408                         this
2409                     );
2410                 }
2411 
2412                 this.hasMouseHandlers = false;
2413             }
2414         },
2415 
2416         /**
2417          * Remove all registered touch event handlers.
2418          */
2419         removeTouchEventHandlers: function () {
2420             if (this.hasTouchHandlers && Env.isBrowser) {
2421                 var moveTarget = this.attr.movetarget || this.containerObj;
2422 
2423                 // Env.removeEvent(this.containerObj, 'touchstart', this.touchStartListener, this);
2424                 Env.removeEvent(moveTarget, 'touchstart', this.touchStartListener, this);
2425                 Env.removeEvent(moveTarget, 'touchmove', this.touchMoveListener, this);
2426 
2427                 if (this.hasTouchEnd) {
2428                     Env.removeEvent(this.document, 'touchend', this.touchEndListener, this);
2429                     this.hasTouchEnd = false;
2430                 }
2431 
2432                 this.hasTouchHandlers = false;
2433             }
2434         },
2435 
2436         /**
2437          * Handler for click on left arrow in the navigation bar
2438          * @returns {JXG.Board} Reference to the board
2439          */
2440         clickLeftArrow: function () {
2441             this.moveOrigin(
2442                 this.origin.scrCoords[1] + this.canvasWidth * 0.1,
2443                 this.origin.scrCoords[2]
2444             );
2445             return this;
2446         },
2447 
2448         /**
2449          * Handler for click on right arrow in the navigation bar
2450          * @returns {JXG.Board} Reference to the board
2451          */
2452         clickRightArrow: function () {
2453             this.moveOrigin(
2454                 this.origin.scrCoords[1] - this.canvasWidth * 0.1,
2455                 this.origin.scrCoords[2]
2456             );
2457             return this;
2458         },
2459 
2460         /**
2461          * Handler for click on up arrow in the navigation bar
2462          * @returns {JXG.Board} Reference to the board
2463          */
2464         clickUpArrow: function () {
2465             this.moveOrigin(
2466                 this.origin.scrCoords[1],
2467                 this.origin.scrCoords[2] - this.canvasHeight * 0.1
2468             );
2469             return this;
2470         },
2471 
2472         /**
2473          * Handler for click on down arrow in the navigation bar
2474          * @returns {JXG.Board} Reference to the board
2475          */
2476         clickDownArrow: function () {
2477             this.moveOrigin(
2478                 this.origin.scrCoords[1],
2479                 this.origin.scrCoords[2] + this.canvasHeight * 0.1
2480             );
2481             return this;
2482         },
2483 
2484         /**
2485          * Triggered on iOS/Safari while the user inputs a gesture (e.g. pinch) and is used to zoom into the board.
2486          * Works on iOS/Safari and Android.
2487          * @param {Event} evt Browser event object
2488          * @returns {Boolean}
2489          */
2490         gestureChangeListener: function (evt) {
2491             var c,
2492                 dir1 = [],
2493                 dir2 = [],
2494                 angle,
2495                 mi = 10,
2496                 isPinch = false,
2497                 // Save zoomFactors
2498                 zx = this.attr.zoom.factorx,
2499                 zy = this.attr.zoom.factory,
2500                 factor, dist, theta, bound,
2501                 zoomCenter,
2502                 doZoom = false,
2503                 dx, dy, cx, cy;
2504 
2505             if (this.mode !== this.BOARD_MODE_ZOOM) {
2506                 return true;
2507             }
2508             evt.preventDefault();
2509 
2510             dist = Geometry.distance(
2511                 [evt.touches[0].clientX, evt.touches[0].clientY],
2512                 [evt.touches[1].clientX, evt.touches[1].clientY],
2513                 2
2514             );
2515 
2516             // Android pinch to zoom
2517             // evt.scale was available in iOS touch events (pre iOS 13)
2518             // evt.scale is undefined in Android
2519             if (evt.scale === undefined) {
2520                 evt.scale = dist / this.prevDist;
2521             }
2522 
2523             if (!Type.exists(this.prevCoords)) {
2524                 return false;
2525             }
2526             // Compute the angle of the two finger directions
2527             dir1 = [
2528                 evt.touches[0].clientX - this.prevCoords[0][0],
2529                 evt.touches[0].clientY - this.prevCoords[0][1]
2530             ];
2531             dir2 = [
2532                 evt.touches[1].clientX - this.prevCoords[1][0],
2533                 evt.touches[1].clientY - this.prevCoords[1][1]
2534             ];
2535 
2536             if (
2537                 dir1[0] * dir1[0] + dir1[1] * dir1[1] < mi * mi &&
2538                 dir2[0] * dir2[0] + dir2[1] * dir2[1] < mi * mi
2539             ) {
2540                 return false;
2541             }
2542 
2543             angle = Geometry.rad(dir1, [0, 0], dir2);
2544             if (
2545                 this.isPreviousGesture !== 'pan' &&
2546                 Math.abs(angle) > Math.PI * 0.2 &&
2547                 Math.abs(angle) < Math.PI * 1.8
2548             ) {
2549                 isPinch = true;
2550             }
2551 
2552             if (this.isPreviousGesture !== 'pan' && !isPinch) {
2553                 if (Math.abs(evt.scale) < 0.77 || Math.abs(evt.scale) > 1.3) {
2554                     isPinch = true;
2555                 }
2556             }
2557 
2558             factor = evt.scale / this.prevScale;
2559             this.prevScale = evt.scale;
2560             this.prevCoords = [
2561                 [evt.touches[0].clientX, evt.touches[0].clientY],
2562                 [evt.touches[1].clientX, evt.touches[1].clientY]
2563             ];
2564 
2565             c = new Coords(Const.COORDS_BY_SCREEN, this.getMousePosition(evt, 0), this);
2566 
2567             if (this.attr.pan.enabled && this.attr.pan.needtwofingers && !isPinch) {
2568                 // Pan detected
2569                 this.isPreviousGesture = 'pan';
2570                 this.moveOrigin(c.scrCoords[1], c.scrCoords[2], true);
2571 
2572             } else if (this.attr.zoom.enabled && Math.abs(factor - 1.0) < 0.5) {
2573                 doZoom = false;
2574                 zoomCenter = this.attr.zoom.center;
2575                 // Pinch detected
2576                 if (this.attr.zoom.pinchhorizontal || this.attr.zoom.pinchvertical) {
2577                     dx = Math.abs(evt.touches[0].clientX - evt.touches[1].clientX);
2578                     dy = Math.abs(evt.touches[0].clientY - evt.touches[1].clientY);
2579                     theta = Math.abs(Math.atan2(dy, dx));
2580                     bound = (Math.PI * this.attr.zoom.pinchsensitivity) / 90.0;
2581                 }
2582 
2583                 if (!this.keepaspectratio &&
2584                     this.attr.zoom.pinchhorizontal &&
2585                     theta < bound) {
2586                     this.attr.zoom.factorx = factor;
2587                     this.attr.zoom.factory = 1.0;
2588                     cx = 0;
2589                     cy = 0;
2590                     doZoom = true;
2591                 } else if (!this.keepaspectratio &&
2592                     this.attr.zoom.pinchvertical &&
2593                     Math.abs(theta - Math.PI * 0.5) < bound
2594                 ) {
2595                     this.attr.zoom.factorx = 1.0;
2596                     this.attr.zoom.factory = factor;
2597                     cx = 0;
2598                     cy = 0;
2599                     doZoom = true;
2600                 } else if (this.attr.zoom.pinch) {
2601                     this.attr.zoom.factorx = factor;
2602                     this.attr.zoom.factory = factor;
2603                     cx = c.usrCoords[1];
2604                     cy = c.usrCoords[2];
2605                     doZoom = true;
2606                 }
2607 
2608                 if (doZoom) {
2609                     if (zoomCenter === 'board') {
2610                         this.zoomIn();
2611                     } else { // including zoomCenter === 'auto'
2612                         this.zoomIn(cx, cy);
2613                     }
2614 
2615                     // Restore zoomFactors
2616                     this.attr.zoom.factorx = zx;
2617                     this.attr.zoom.factory = zy;
2618                 }
2619             }
2620 
2621             return false;
2622         },
2623 
2624         /**
2625          * Called by iOS/Safari as soon as the user starts a gesture. Works natively on iOS/Safari,
2626          * on Android we emulate it.
2627          * @param {Event} evt
2628          * @returns {Boolean}
2629          */
2630         gestureStartListener: function (evt) {
2631             var pos;
2632 
2633             evt.preventDefault();
2634             this.prevScale = 1.0;
2635             // Android pinch to zoom
2636             this.prevDist = Geometry.distance(
2637                 [evt.touches[0].clientX, evt.touches[0].clientY],
2638                 [evt.touches[1].clientX, evt.touches[1].clientY],
2639                 2
2640             );
2641             this.prevCoords = [
2642                 [evt.touches[0].clientX, evt.touches[0].clientY],
2643                 [evt.touches[1].clientX, evt.touches[1].clientY]
2644             ];
2645             this.isPreviousGesture = 'none';
2646 
2647             // If pinch-to-zoom is interpreted as panning
2648             // we have to prepare move origin
2649             pos = this.getMousePosition(evt, 0);
2650             this.initMoveOrigin(pos[0], pos[1]);
2651 
2652             this.mode = this.BOARD_MODE_ZOOM;
2653             this._change3DView = false;
2654             return false;
2655         },
2656 
2657         /**
2658          * Test if the required key combination is pressed for wheel zoom, move origin and
2659          * selection
2660          * @private
2661          * @param  {Object}  evt    Mouse or pen event
2662          * @param  {String}  action String containing the action: 'zoom', 'pan', 'selection'.
2663          * Corresponds to the attribute subobject.
2664          * @return {Boolean}        true or false.
2665          */
2666         _isRequiredKeyPressed: function (evt, action) {
2667             var obj = this.attr[action];
2668             if (!obj.enabled) {
2669                 return false;
2670             }
2671 
2672             if (
2673                 ((obj.needshift && evt.shiftKey) || (!obj.needshift && !evt.shiftKey)) &&
2674                 ((obj.needctrl && evt.ctrlKey) || (!obj.needctrl && !evt.ctrlKey))
2675             ) {
2676                 return true;
2677             }
2678 
2679             return false;
2680         },
2681 
2682         /*
2683          * Pointer events
2684          */
2685 
2686         /**
2687          *
2688          * Check if pointer event is already registered in {@link JXG.Board#_board_touches}.
2689          *
2690          * @param  {Object} evt Event object
2691          * @return {Boolean} true if down event has already been sent.
2692          * @private
2693          */
2694         _isPointerRegistered: function (evt) {
2695             var i,
2696                 len = this._board_touches.length;
2697 
2698             for (i = 0; i < len; i++) {
2699                 if (this._board_touches[i].pointerId === evt.pointerId) {
2700                     return true;
2701                 }
2702             }
2703             return false;
2704         },
2705 
2706         /**
2707          *
2708          * Store the position of a pointer event.
2709          * If not yet done, registers a pointer event in {@link JXG.Board#_board_touches}.
2710          * Allows to follow the path of that finger on the screen.
2711          * Only two simultaneous touches are supported.
2712          *
2713          * @param {Object} evt Event object
2714          * @returns {JXG.Board} Reference to the board
2715          * @private
2716          */
2717         _pointerStorePosition: function (evt) {
2718             var i, found;
2719 
2720             for (i = 0, found = false; i < this._board_touches.length; i++) {
2721                 if (this._board_touches[i].pointerId === evt.pointerId) {
2722                     this._board_touches[i].clientX = evt.clientX;
2723                     this._board_touches[i].clientY = evt.clientY;
2724                     found = true;
2725                     break;
2726                 }
2727             }
2728 
2729             // Restrict the number of simultaneous touches to 2
2730             if (!found && this._board_touches.length < 2) {
2731                 this._board_touches.push({
2732                     pointerId: evt.pointerId,
2733                     clientX: evt.clientX,
2734                     clientY: evt.clientY
2735                 });
2736             }
2737 
2738             return this;
2739         },
2740 
2741         /**
2742          * Deregisters a pointer event in {@link JXG.Board#_board_touches}.
2743          * It happens if a finger has been lifted from the screen.
2744          *
2745          * @param {Object} evt Event object
2746          * @returns {JXG.Board} Reference to the board
2747          * @private
2748          */
2749         _pointerRemoveTouches: function (evt) {
2750             var i;
2751             for (i = 0; i < this._board_touches.length; i++) {
2752                 if (this._board_touches[i].pointerId === evt.pointerId) {
2753                     this._board_touches.splice(i, 1);
2754                     break;
2755                 }
2756             }
2757 
2758             return this;
2759         },
2760 
2761         /**
2762          * Remove all registered fingers from {@link JXG.Board#_board_touches}.
2763          * This might be necessary if too many fingers have been registered.
2764          * @returns {JXG.Board} Reference to the board
2765          * @private
2766          */
2767         _pointerClearTouches: function (pId) {
2768             // var i;
2769             // if (pId) {
2770             //     for (i = 0; i < this._board_touches.length; i++) {
2771             //         if (pId === this._board_touches[i].pointerId) {
2772             //             this._board_touches.splice(i, i);
2773             //             break;
2774             //         }
2775             //     }
2776             // } else {
2777             // }
2778             if (this._board_touches.length > 0) {
2779                 this.dehighlightAll();
2780             }
2781             this.updateQuality = this.BOARD_QUALITY_HIGH;
2782             this.mode = this.BOARD_MODE_NONE;
2783             this._board_touches = [];
2784             this.touches = [];
2785         },
2786 
2787         /**
2788          * Determine which input device is used for this action.
2789          * Possible devices are 'touch', 'pen' and 'mouse'.
2790          * This affects the precision and certain events.
2791          * In case of no browser, 'mouse' is used.
2792          *
2793          * @see JXG.Board#pointerDownListener
2794          * @see JXG.Board#pointerMoveListener
2795          * @see JXG.Board#initMoveObject
2796          * @see JXG.Board#moveObject
2797          *
2798          * @param {Event} evt The browsers event object.
2799          * @returns {String} 'mouse', 'pen', or 'touch'
2800          * @private
2801          */
2802         _getPointerInputDevice: function (evt) {
2803             if (Env.isBrowser) {
2804                 if (
2805                     evt.pointerType === 'touch' || // New
2806                     (window.navigator.msMaxTouchPoints && // Old
2807                         window.navigator.msMaxTouchPoints > 1)
2808                 ) {
2809                     return 'touch';
2810                 }
2811                 if (evt.pointerType === 'mouse') {
2812                     return 'mouse';
2813                 }
2814                 if (evt.pointerType === 'pen') {
2815                     return 'pen';
2816                 }
2817             }
2818             return 'mouse';
2819         },
2820 
2821         /**
2822          * This method is called by the browser when a pointing device is pressed on the screen.
2823          * @param {Event} evt The browsers event object.
2824          * @param {Object} object If the object to be dragged is already known, it can be submitted via this parameter
2825          * @param {Boolean} [allowDefaultEventHandling=false] If true event is not canceled, i.e. prevent call of evt.preventDefault()
2826          * @returns {Boolean} false if the first finger event is sent twice, or not a browser, or in selection mode. Otherwise returns true.
2827          */
2828         pointerDownListener: function (evt, object, allowDefaultEventHandling) {
2829             var i, j, k, pos,
2830                 elements, sel, target_obj,
2831                 type = 'mouse', // Used in case of no browser
2832                 found, target, ta;
2833 
2834             // Fix for Firefox browser: When using a second finger, the
2835             // touch event for the first finger is sent again.
2836             if (!object && this._isPointerRegistered(evt)) {
2837                 return false;
2838             }
2839 
2840             if (Type.evaluate(this.attr.movetarget) === null &&
2841                 Type.exists(evt.target) && Type.exists(evt.target.releasePointerCapture)) {
2842                 evt.target.releasePointerCapture(evt.pointerId);
2843             }
2844 
2845             if (!object && evt.isPrimary) {
2846                 // First finger down. To be on the safe side this._board_touches is cleared.
2847                 // this._pointerClearTouches();
2848             }
2849 
2850             if (!this.hasPointerUp) {
2851                 if (window.navigator.msPointerEnabled) {
2852                     // IE10-
2853                     Env.addEvent(this.document, 'MSPointerUp', this.pointerUpListener, this);
2854                 } else {
2855                     // 'pointercancel' is fired e.g. if the finger leaves the browser and drags down the system menu on Android
2856                     Env.addEvent(this.document, 'pointerup', this.pointerUpListener, this);
2857                     Env.addEvent(this.document, 'pointercancel', this.pointerUpListener, this);
2858                 }
2859                 this.hasPointerUp = true;
2860             }
2861 
2862             if (this.hasMouseHandlers) {
2863                 this.removeMouseEventHandlers();
2864             }
2865 
2866             if (this.hasTouchHandlers) {
2867                 this.removeTouchEventHandlers();
2868             }
2869 
2870             // Prevent accidental selection of text
2871             if (this.document.selection && Type.isFunction(this.document.selection.empty)) {
2872                 this.document.selection.empty();
2873             } else if (window.getSelection) {
2874                 sel = window.getSelection();
2875                 if (sel.removeAllRanges) {
2876                     try {
2877                         sel.removeAllRanges();
2878                     } catch (e) { }
2879                 }
2880             }
2881 
2882             // Mouse, touch or pen device
2883             this._inputDevice = this._getPointerInputDevice(evt);
2884             type = this._inputDevice;
2885             this.options.precision.hasPoint = this.options.precision[type];
2886 
2887             // Handling of multi touch with pointer events should be easier than with touch events.
2888             // Every pointer device has its own pointerId, e.g. the mouse
2889             // always has id 1 or 0, fingers and pens get unique ids every time a pointerDown event is fired and they will
2890             // keep this id until a pointerUp event is fired. What we have to do here is:
2891             //  1. collect all elements under the current pointer
2892             //  2. run through the touches control structure
2893             //    a. look for the object collected in step 1.
2894             //    b. if an object is found, check the number of pointers. If appropriate, add the pointer.
2895             pos = this.getMousePosition(evt);
2896 
2897             // Handle selection rectangle
2898             this._testForSelection(evt);
2899             if (this.selectingMode) {
2900                 this._startSelecting(pos);
2901                 this.triggerEventHandlers(
2902                     ['touchstartselecting', 'pointerstartselecting', 'startselecting'],
2903                     [evt]
2904                 );
2905                 return; // don't continue as a normal click
2906             }
2907 
2908             if (this.attr.drag.enabled && object) {
2909                 elements = [object];
2910                 this.mode = this.BOARD_MODE_DRAG;
2911             } else {
2912                 elements = this.initMoveObject(pos[0], pos[1], evt, type);
2913             }
2914 
2915             target_obj = {
2916                 num: evt.pointerId,
2917                 X: pos[0],
2918                 Y: pos[1],
2919                 Xprev: NaN,
2920                 Yprev: NaN,
2921                 Xstart: [],
2922                 Ystart: [],
2923                 Zstart: []
2924             };
2925 
2926             // If no draggable object can be found, get out here immediately
2927             if (elements.length > 0) {
2928                 // check touches structure
2929                 target = elements[elements.length - 1];
2930                 found = false;
2931 
2932                 // Reminder: this.touches is the list of elements which
2933                 // currently 'possess' a pointer (mouse, pen, finger)
2934                 for (i = 0; i < this.touches.length; i++) {
2935                     // An element receives a further touch, i.e.
2936                     // the target is already in our touches array, add the pointer to the existing touch
2937                     if (this.touches[i].obj === target) {
2938                         j = i;
2939                         k = this.touches[i].targets.push(target_obj) - 1;
2940                         found = true;
2941                         break;
2942                     }
2943                 }
2944                 if (!found) {
2945                     // A new element has been touched.
2946                     k = 0;
2947                     j =
2948                         this.touches.push({
2949                             obj: target,
2950                             targets: [target_obj]
2951                         }) - 1;
2952                 }
2953 
2954                 this.dehighlightAll();
2955                 target.highlight(true);
2956 
2957                 this.saveStartPos(target, this.touches[j].targets[k]);
2958 
2959                 // Prevent accidental text selection
2960                 // this could get us new trouble: input fields, links and drop down boxes placed as text
2961                 // on the board don't work anymore.
2962                 if (evt && evt.preventDefault && !allowDefaultEventHandling) {
2963                     // All browser supporting pointer events know preventDefault()
2964                     evt.preventDefault();
2965                 }
2966             }
2967 
2968             if (this.touches.length > 0 && !allowDefaultEventHandling) {
2969                 evt.preventDefault();
2970                 evt.stopPropagation();
2971             }
2972 
2973             if (!Env.isBrowser) {
2974                 return false;
2975             }
2976             if (this._getPointerInputDevice(evt) !== 'touch') {
2977                 if (this.mode === this.BOARD_MODE_NONE) {
2978                     this.mouseOriginMoveStart(evt);
2979                 }
2980             } else {
2981                 this._pointerStorePosition(evt);
2982                 evt.touches = this._board_touches;
2983 
2984                 // Touch events on empty areas of the board are handled here, see also touchStartListener
2985                 // 1. case: one finger. If allowed, this triggers pan with one finger
2986                 if (
2987                     evt.touches.length === 1 &&
2988                     this.mode === this.BOARD_MODE_NONE &&
2989                     this.touchStartMoveOriginOneFinger(evt)
2990                 ) {
2991                     // Empty by purpose
2992                 } else if (
2993                     evt.touches.length === 2 &&
2994                     (this.mode === this.BOARD_MODE_NONE ||
2995                         this.mode === this.BOARD_MODE_MOVE_ORIGIN)
2996                 ) {
2997                     // 2. case: two fingers: pinch to zoom or pan with two fingers needed.
2998                     // This happens when the second finger hits the device. First, the
2999                     // 'one finger pan mode' has to be cancelled.
3000                     if (this.mode === this.BOARD_MODE_MOVE_ORIGIN) {
3001                         this.originMoveEnd();
3002                     }
3003 
3004                     this.gestureStartListener(evt);
3005                 }
3006             }
3007 
3008             this.initSketchCurve(evt);
3009 
3010             // Allow browser scrolling
3011             // For this: pan by one finger has to be disabled
3012 
3013             ta = 'none';   // JSXGraph catches all user touch events
3014             if (this.mode === this.BOARD_MODE_NONE &&
3015                 (Type.evaluate(this.attr.browserpan) === true || Type.evaluate(this.attr.browserpan.enabled) === true) &&
3016                 // One-finger pan has priority over browserPan
3017                 (Type.evaluate(this.attr.pan.enabled) === false || Type.evaluate(this.attr.pan.needtwofingers) === true)
3018             ) {
3019                 // ta = 'pan-x pan-y';  // JSXGraph allows browser scrolling
3020                 ta = 'auto';  // JSXGraph allows browser scrolling
3021             }
3022             this.containerObj.style.touchAction = ta;
3023 
3024             this.triggerEventHandlers(['touchstart', 'down', 'pointerdown', 'MSPointerDown'], [evt]);
3025 
3026             return true;
3027         },
3028 
3029         /**
3030          * Internal handling of click events for pointers and mouse.
3031          *
3032          * @param {Event} evt The browsers event object.
3033          * @param {Array} evtArray list of event names
3034          * @private
3035          */
3036         _handleClicks: function(evt, evtArray) {
3037             var that = this,
3038                 el, delay, suppress;
3039 
3040             if (this.selectingMode) {
3041                 evt.stopPropagation();
3042                 return;
3043             }
3044 
3045             delay = Type.evaluate(this.attr.clickdelay);
3046             suppress = Type.evaluate(this.attr.dblclicksuppressclick);
3047 
3048             if (suppress) {
3049                 // dblclick suppresses previous click events
3050                 this._preventSingleClick = false;
3051 
3052                 // Wait if there is a dblclick event.
3053                 // If not fire a click event
3054                 this._singleClickTimer = setTimeout(function() {
3055                     if (!that._preventSingleClick) {
3056                         // Fire click event and remove element from click list
3057                         that.triggerEventHandlers(evtArray, [evt]);
3058                         for (el in that.clickObjects) {
3059                             if (that.clickObjects.hasOwnProperty(el)) {
3060                                 that.clickObjects[el].triggerEventHandlers(evtArray, [evt]);
3061                                 delete that.clickObjects[el];
3062                             }
3063                         }
3064                     }
3065                 }, delay);
3066             } else {
3067                 // dblclick is preceded by two click events
3068 
3069                 // Fire click events
3070                 that.triggerEventHandlers(evtArray, [evt]);
3071                 for (el in that.clickObjects) {
3072                     if (that.clickObjects.hasOwnProperty(el)) {
3073                         that.clickObjects[el].triggerEventHandlers(evtArray, [evt]);
3074                     }
3075                 }
3076 
3077                 // Clear list of clicked elements with a delay
3078                 setTimeout(function() {
3079                     for (el in that.clickObjects) {
3080                         if (that.clickObjects.hasOwnProperty(el)) {
3081                             delete that.clickObjects[el];
3082                         }
3083                     }
3084                 }, delay);
3085             }
3086             evt.stopPropagation();
3087         },
3088 
3089         /**
3090          * Internal handling of dblclick events for pointers and mouse.
3091          *
3092          * @param {Event} evt The browsers event object.
3093          * @param {Array} evtArray list of event names
3094          * @private
3095          */
3096         _handleDblClicks: function(evt, evtArray) {
3097             var el;
3098 
3099             if (this.selectingMode) {
3100                 evt.stopPropagation();
3101                 return;
3102             }
3103 
3104             // Notify that a dblclick has happened
3105             this._preventSingleClick = true;
3106             clearTimeout(this._singleClickTimer);
3107 
3108             // Fire dblclick event
3109             this.triggerEventHandlers(evtArray, [evt]);
3110             for (el in this.clickObjects) {
3111                 if (this.clickObjects.hasOwnProperty(el)) {
3112                     this.clickObjects[el].triggerEventHandlers(evtArray, [evt]);
3113                     delete this.clickObjects[el];
3114                 }
3115             }
3116 
3117             evt.stopPropagation();
3118         },
3119 
3120         /**
3121          * This method is called by the browser when a pointer device clicks on the screen.
3122          * @param {Event} evt The browsers event object.
3123          */
3124         pointerClickListener: function (evt) {
3125             this._handleClicks(evt, ['click', 'pointerclick']);
3126         },
3127 
3128         /**
3129          * This method is called by the browser when a pointer device double clicks on the screen.
3130          * @param {Event} evt The browsers event object.
3131          */
3132         pointerDblClickListener: function (evt) {
3133             this._handleDblClicks(evt, ['dblclick', 'pointerdblclick']);
3134         },
3135 
3136         /**
3137          * This method is called by the browser when the mouse device clicks on the screen.
3138          * @param {Event} evt The browsers event object.
3139          */
3140         mouseClickListener: function (evt) {
3141             this._handleClicks(evt, ['click', 'mouseclick']);
3142         },
3143 
3144         /**
3145          * This method is called by the browser when the mouse device double clicks on the screen.
3146          * @param {Event} evt The browsers event object.
3147          */
3148         mouseDblClickListener: function (evt) {
3149             this._handleDblClicks(evt, ['dblclick', 'mousedblclick']);
3150         },
3151 
3152         // /**
3153         //  * Called if pointer leaves an HTML tag. It is called by the inner-most tag.
3154         //  * That means, if a JSXGraph text, i.e. an HTML div, is placed close
3155         //  * to the border of the board, this pointerout event will be ignored.
3156         //  * @param  {Event} evt
3157         //  * @return {Boolean}
3158         //  */
3159         // pointerOutListener: function (evt) {
3160         //     if (evt.target === this.containerObj ||
3161         //         (this.renderer.type === 'svg' && evt.target === this.renderer.foreignObjLayer)) {
3162         //         this.pointerUpListener(evt);
3163         //     }
3164         //     return this.mode === this.BOARD_MODE_NONE;
3165         // },
3166 
3167         /**
3168          * Called periodically by the browser while the user moves a pointing device across the screen.
3169          * @param {Event} evt
3170          * @returns {Boolean}
3171          */
3172         pointerMoveListener: function (evt) {
3173             var i, j, pos,
3174                 eps,
3175                 touchTargets,
3176                 type = 'mouse'; // in case of no browser
3177 
3178             if (
3179                 this._getPointerInputDevice(evt) === 'touch' &&
3180                 !this._isPointerRegistered(evt)
3181             ) {
3182                 // Test, if there was a previous down event of this _getPointerId
3183                 // (in case it is a touch event).
3184                 // Otherwise this move event is ignored. This is necessary e.g. for sketchometry.
3185                 return this.BOARD_MODE_NONE;
3186             }
3187 
3188             if (!this.checkFrameRate(evt)) {
3189                 return false;
3190             }
3191 
3192             if (this.mode !== this.BOARD_MODE_DRAG) {
3193                 this.dehighlightAll();
3194                 this.displayInfobox(false);
3195             }
3196 
3197             if (this.mode !== this.BOARD_MODE_NONE) {
3198                 evt.preventDefault();
3199                 evt.stopPropagation();
3200             }
3201 
3202             this.updateQuality = this.BOARD_QUALITY_LOW;
3203             // Mouse, touch or pen device
3204             this._inputDevice = this._getPointerInputDevice(evt);
3205             type = this._inputDevice;
3206             this.options.precision.hasPoint = this.options.precision[type];
3207             eps = this.options.precision.hasPoint * 0.3333;
3208 
3209             pos = this.getMousePosition(evt);
3210             // Ignore pointer move event if too close at the border
3211             // and setPointerCapture is off
3212             if (Type.evaluate(this.attr.movetarget) === null &&
3213                 (pos[0] <= eps || pos[1] <= eps ||
3214                  pos[0] >= this.canvasWidth - eps ||
3215                  pos[1] >= this.canvasHeight - eps)
3216             ) {
3217                 return this.mode === this.BOARD_MODE_NONE;
3218             }
3219 
3220             // selection
3221             if (this.selectingMode) {
3222                 this._moveSelecting(pos);
3223                 this.triggerEventHandlers(
3224                     ['touchmoveselecting', 'moveselecting', 'pointermoveselecting'],
3225                     [evt, this.mode]
3226                 );
3227             } else if (!this.mouseOriginMove(evt)) {
3228 
3229                 this.addToSketchCurve(evt);
3230 
3231                 if (this.mode === this.BOARD_MODE_DRAG) {
3232                     // Run through all jsxgraph elements which are touched by at least one finger.
3233                     for (i = 0; i < this.touches.length; i++) {
3234                         touchTargets = this.touches[i].targets;
3235                         // Run through all touch events which have been started on this jsxgraph element.
3236                         for (j = 0; j < touchTargets.length; j++) {
3237                             if (touchTargets[j].num === evt.pointerId) {
3238                                 touchTargets[j].X = pos[0];
3239                                 touchTargets[j].Y = pos[1];
3240 
3241                                 if (touchTargets.length === 1) {
3242                                     // Touch by one finger: this is possible for all elements that can be dragged
3243                                     this.moveObject(pos[0], pos[1], this.touches[i], evt, type);
3244                                 } else if (touchTargets.length === 2) {
3245                                     // Touch by two fingers: e.g. moving lines
3246                                     this.twoFingerMove(this.touches[i], evt.pointerId, evt);
3247 
3248                                     touchTargets[j].Xprev = pos[0];
3249                                     touchTargets[j].Yprev = pos[1];
3250                                 }
3251 
3252                                 // There is only one pointer in the evt object, so there's no point in looking further
3253                                 break;
3254                             }
3255                         }
3256                     }
3257                 } else {
3258                     if (this._getPointerInputDevice(evt) === 'touch') {
3259                         this._pointerStorePosition(evt);
3260 
3261                         if (this._board_touches.length === 2) {
3262                             evt.touches = this._board_touches;
3263                             this.gestureChangeListener(evt);
3264                         }
3265                     }
3266 
3267                     // Move event without dragging an element
3268                     this.highlightElements(pos[0], pos[1], evt, -1);
3269                 }
3270             }
3271 
3272             // Hiding the infobox is commented out, since it prevents showing the infobox
3273             // on IE 11+ on 'over'
3274             //if (this.mode !== this.BOARD_MODE_DRAG) {
3275             //this.displayInfobox(false);
3276             //}
3277             this.triggerEventHandlers(['pointermove', 'MSPointerMove', 'move'], [evt, this.mode]);
3278             this.updateQuality = this.BOARD_QUALITY_HIGH;
3279 
3280             return this.mode === this.BOARD_MODE_NONE;
3281         },
3282 
3283         /**
3284          * Triggered as soon as the user stops touching the device with at least one finger.
3285          *
3286          * @param {Event} evt
3287          * @returns {Boolean}
3288          */
3289         pointerUpListener: function (evt) {
3290             var i, j, found, eh,
3291                 touchTargets,
3292                 updateNeeded = false;
3293 
3294             this.triggerEventHandlers(['touchend', 'up', 'pointerup', 'MSPointerUp'], [evt]);
3295             this.displayInfobox(false);
3296 
3297             if (evt) {
3298                 for (i = 0; i < this.touches.length; i++) {
3299                     touchTargets = this.touches[i].targets;
3300                     for (j = 0; j < touchTargets.length; j++) {
3301                         if (touchTargets[j].num === evt.pointerId) {
3302                             touchTargets.splice(j, 1);
3303                             if (touchTargets.length === 0) {
3304                                 this.touches.splice(i, 1);
3305                             }
3306                             break;
3307                         }
3308                     }
3309                 }
3310             }
3311 
3312             this.finalizeSketchCurve(evt);
3313             this.originMoveEnd();
3314             this.update();
3315 
3316             // selection
3317             if (this.selectingMode) {
3318                 this._stopSelecting(evt);
3319                 this.triggerEventHandlers(
3320                     ['touchstopselecting', 'pointerstopselecting', 'stopselecting'],
3321                     [evt]
3322                 );
3323                 this.stopSelectionMode();
3324             } else {
3325                 for (i = this.downObjects.length - 1; i > -1; i--) {
3326                     found = false;
3327                     for (j = 0; j < this.touches.length; j++) {
3328                         if (this.touches[j].obj.id === this.downObjects[i].id) {
3329                             found = true;
3330                         }
3331                     }
3332                     if (!found) {
3333                         this.downObjects[i].triggerEventHandlers(
3334                             ['touchend', 'up', 'pointerup', 'MSPointerUp'],
3335                             [evt]
3336                         );
3337                         if (!Type.exists(this.downObjects[i].coords)) {
3338                             // snapTo methods have to be called e.g. for line elements here.
3339                             // For coordsElements there might be a conflict with
3340                             // attractors, see commit from 2022.04.08, 11:12:18.
3341                             this.downObjects[i].snapToGrid();
3342                             this.downObjects[i].snapToPoints();
3343                             updateNeeded = true;
3344                         }
3345 
3346                         // Check if we have to keep the element for a click or dblclick event
3347                         // Otherwise remove it from downObjects
3348                         eh = this.downObjects[i].eventHandlers;
3349                         if ((Type.exists(eh.click) && eh.click.length > 0) ||
3350                             (Type.exists(eh.pointerclick) && eh.pointerclick.length > 0) ||
3351                             (Type.exists(eh.dblclick) && eh.dblclick.length > 0) ||
3352                             (Type.exists(eh.pointerdblclick) && eh.pointerdblclick.length > 0)
3353                         ) {
3354                             this.clickObjects[this.downObjects[i].id] = this.downObjects[i];
3355                         }
3356                         this.downObjects.splice(i, 1);
3357                     }
3358                 }
3359             }
3360 
3361             if (this.hasPointerUp) {
3362                 if (window.navigator.msPointerEnabled) {
3363                     // IE10-
3364                     Env.removeEvent(this.document, 'MSPointerUp', this.pointerUpListener, this);
3365                 } else {
3366                     Env.removeEvent(this.document, 'pointerup', this.pointerUpListener, this);
3367                     Env.removeEvent(this.document, 'pointercancel', this.pointerUpListener, this);
3368                 }
3369                 this.hasPointerUp = false;
3370             }
3371 
3372             // After one finger leaves the screen the gesture is stopped.
3373             this._pointerClearTouches(evt.pointerId);
3374             if (this._getPointerInputDevice(evt) !== 'touch') {
3375                 this.dehighlightAll();
3376             }
3377 
3378             if (updateNeeded) {
3379                 this.update();
3380             }
3381 
3382             return true;
3383         },
3384 
3385         /**
3386          * Triggered by the pointerleave event. This is needed in addition to
3387          * {@link JXG.Board#pointerUpListener} in the situation that a pen is used
3388          * and after an up event the pen leaves the hover range vertically. Here, it happens that
3389          * after the pointerup event further pointermove events are fired and elements get highlighted.
3390          * This highlighting has to be cancelled.
3391          *
3392          * @param {Event} evt
3393          * @returns {Boolean}
3394          */
3395         pointerLeaveListener: function (evt) {
3396             this.displayInfobox(false);
3397             this.dehighlightAll();
3398 
3399             return true;
3400         },
3401 
3402         /**
3403          * Touch-Events
3404          */
3405 
3406         /**
3407          * This method is called by the browser when a finger touches the surface of the touch-device.
3408          * @param {Event} evt The browsers event object.
3409          * @returns {Boolean} ...
3410          */
3411         touchStartListener: function (evt) {
3412             var i, j, k,
3413                 pos, elements, obj,
3414                 eps = this.options.precision.touch,
3415                 evtTouches = evt['touches'],
3416                 found,
3417                 targets, target,
3418                 touchTargets;
3419 
3420             if (!this.hasTouchEnd) {
3421                 Env.addEvent(this.document, 'touchend', this.touchEndListener, this);
3422                 this.hasTouchEnd = true;
3423             }
3424 
3425             // Do not remove mouseHandlers, since Chrome on win tablets sends mouseevents if used with pen.
3426             //if (this.hasMouseHandlers) { this.removeMouseEventHandlers(); }
3427 
3428             // prevent accidental selection of text
3429             if (this.document.selection && Type.isFunction(this.document.selection.empty)) {
3430                 this.document.selection.empty();
3431             } else if (window.getSelection) {
3432                 window.getSelection().removeAllRanges();
3433             }
3434 
3435             // multitouch
3436             this._inputDevice = 'touch';
3437             this.options.precision.hasPoint = this.options.precision.touch;
3438 
3439             // This is the most critical part. first we should run through the existing touches and collect all targettouches that don't belong to our
3440             // 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
3441             // 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
3442             // 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
3443             // stretching. as a last step we're going through the rest of the targettouches and initiate new move operations:
3444             //  * points have higher priority over other elements.
3445             //  * 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
3446             //    this element and add them.
3447             // ADDENDUM 11/10/11:
3448             //  (1) run through the touches control object,
3449             //  (2) try to find the targetTouches for every touch. on touchstart only new touches are added, hence we can find a targettouch
3450             //      for every target in our touches objects
3451             //  (3) if one of the targettouches was bound to a touches targets array, mark it
3452             //  (4) run through the targettouches. if the targettouch is marked, continue. otherwise check for elements below the targettouch:
3453             //      (a) if no element could be found: mark the target touches and continue
3454             //      --- in the following cases, 'init' means:
3455             //           (i) check if the element is already used in another touches element, if so, mark the targettouch and continue
3456             //          (ii) if not, init a new touches element, add the targettouch to the touches property and mark it
3457             //      (b) if the element is a point, init
3458             //      (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
3459             //      (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
3460             //          add both to the touches array and mark them.
3461             for (i = 0; i < evtTouches.length; i++) {
3462                 evtTouches[i].jxg_isused = false;
3463             }
3464 
3465             for (i = 0; i < this.touches.length; i++) {
3466                 touchTargets = this.touches[i].targets;
3467                 for (j = 0; j < touchTargets.length; j++) {
3468                     touchTargets[j].num = -1;
3469                     eps = this.options.precision.touch;
3470 
3471                     do {
3472                         for (k = 0; k < evtTouches.length; k++) {
3473                             // find the new targettouches
3474                             if (
3475                                 Math.abs(
3476                                     Math.pow(evtTouches[k].screenX - touchTargets[j].X, 2) +
3477                                     Math.pow(evtTouches[k].screenY - touchTargets[j].Y, 2)
3478                                 ) <
3479                                 eps * eps
3480                             ) {
3481                                 touchTargets[j].num = k;
3482                                 touchTargets[j].X = evtTouches[k].screenX;
3483                                 touchTargets[j].Y = evtTouches[k].screenY;
3484                                 evtTouches[k].jxg_isused = true;
3485                                 break;
3486                             }
3487                         }
3488 
3489                         eps *= 2;
3490                     } while (
3491                         touchTargets[j].num === -1 &&
3492                         eps < this.options.precision.touchMax
3493                     );
3494 
3495                     if (touchTargets[j].num === -1) {
3496                         JXG.debug(
3497                             "i couldn't find a targettouches for target no " +
3498                             j +
3499                             ' on ' +
3500                             this.touches[i].obj.name +
3501                             ' (' +
3502                             this.touches[i].obj.id +
3503                             '). Removed the target.'
3504                         );
3505                         JXG.debug(
3506                             'eps = ' + eps + ', touchMax = ' + Options.precision.touchMax
3507                         );
3508                         touchTargets.splice(i, 1);
3509                     }
3510                 }
3511             }
3512 
3513             // we just re-mapped the targettouches to our existing touches list.
3514             // now we have to initialize some touches from additional targettouches
3515             for (i = 0; i < evtTouches.length; i++) {
3516                 if (!evtTouches[i].jxg_isused) {
3517                     pos = this.getMousePosition(evt, i);
3518                     // selection
3519                     // this._testForSelection(evt); // we do not have shift or ctrl keys yet.
3520                     if (this.selectingMode) {
3521                         this._startSelecting(pos);
3522                         this.triggerEventHandlers(
3523                             ['touchstartselecting', 'startselecting'],
3524                             [evt]
3525                         );
3526                         evt.preventDefault();
3527                         evt.stopPropagation();
3528                         this.options.precision.hasPoint = this.options.precision.mouse;
3529                         return this.touches.length > 0; // don't continue as a normal click
3530                     }
3531 
3532                     elements = this.initMoveObject(pos[0], pos[1], evt, 'touch');
3533                     if (elements.length !== 0) {
3534                         obj = elements[elements.length - 1];
3535                         target = {
3536                             num: i,
3537                             X: evtTouches[i].screenX,
3538                             Y: evtTouches[i].screenY,
3539                             Xprev: NaN,
3540                             Yprev: NaN,
3541                             Xstart: [],
3542                             Ystart: [],
3543                             Zstart: []
3544                         };
3545 
3546                         if (
3547                             Type.isPoint(obj) ||
3548                             obj.elementClass === Const.OBJECT_CLASS_TEXT ||
3549                             obj.type === Const.OBJECT_TYPE_TICKS ||
3550                             obj.type === Const.OBJECT_TYPE_IMAGE
3551                         ) {
3552                             // It's a point, so it's single touch, so we just push it to our touches
3553                             targets = [target];
3554 
3555                             // For the UNDO/REDO of object moves
3556                             this.saveStartPos(obj, targets[0]);
3557 
3558                             this.touches.push({ obj: obj, targets: targets });
3559                             obj.highlight(true);
3560                         } else if (
3561                             obj.elementClass === Const.OBJECT_CLASS_LINE ||
3562                             obj.elementClass === Const.OBJECT_CLASS_CIRCLE ||
3563                             obj.elementClass === Const.OBJECT_CLASS_CURVE ||
3564                             obj.type === Const.OBJECT_TYPE_POLYGON
3565                         ) {
3566                             found = false;
3567 
3568                             // first check if this geometric object is already captured in this.touches
3569                             for (j = 0; j < this.touches.length; j++) {
3570                                 if (obj.id === this.touches[j].obj.id) {
3571                                     found = true;
3572                                     // only add it, if we don't have two targets in there already
3573                                     if (this.touches[j].targets.length === 1) {
3574                                         // For the UNDO/REDO of object moves
3575                                         this.saveStartPos(obj, target);
3576                                         this.touches[j].targets.push(target);
3577                                     }
3578 
3579                                     evtTouches[i].jxg_isused = true;
3580                                 }
3581                             }
3582 
3583                             // we couldn't find it in touches, so we just init a new touches
3584                             // IF there is a second touch targetting this line, we will find it later on, and then add it to
3585                             // the touches control object.
3586                             if (!found) {
3587                                 targets = [target];
3588 
3589                                 // For the UNDO/REDO of object moves
3590                                 this.saveStartPos(obj, targets[0]);
3591                                 this.touches.push({ obj: obj, targets: targets });
3592                                 obj.highlight(true);
3593                             }
3594                         }
3595                     }
3596 
3597                     evtTouches[i].jxg_isused = true;
3598                 }
3599             }
3600 
3601             if (this.touches.length > 0) {
3602                 evt.preventDefault();
3603                 evt.stopPropagation();
3604             }
3605 
3606             // Touch events on empty areas of the board are handled here:
3607             // 1. case: one finger. If allowed, this triggers pan with one finger
3608             if (
3609                 evtTouches.length === 1 &&
3610                 this.mode === this.BOARD_MODE_NONE &&
3611                 this.touchStartMoveOriginOneFinger(evt)
3612             ) {
3613             } else if (
3614                 evtTouches.length === 2 &&
3615                 (this.mode === this.BOARD_MODE_NONE ||
3616                     this.mode === this.BOARD_MODE_MOVE_ORIGIN)
3617             ) {
3618                 // 2. case: two fingers: pinch to zoom or pan with two fingers needed.
3619                 // This happens when the second finger hits the device. First, the
3620                 // 'one finger pan mode' has to be cancelled.
3621                 if (this.mode === this.BOARD_MODE_MOVE_ORIGIN) {
3622                     this.originMoveEnd();
3623                 }
3624                 this.gestureStartListener(evt);
3625             }
3626 
3627             this.initSketchCurve(evt);
3628 
3629             this.options.precision.hasPoint = this.options.precision.mouse;
3630             this.triggerEventHandlers(['touchstart', 'down'], [evt]);
3631 
3632             return false;
3633             //return this.touches.length > 0;
3634         },
3635 
3636         /**
3637          * Called periodically by the browser while the user moves his fingers across the device.
3638          * @param {Event} evt
3639          * @returns {Boolean}
3640          */
3641         touchMoveListener: function (evt) {
3642             var i,
3643                 pos1, pos2,
3644                 touchTargets,
3645                 evtTouches = evt['touches'];
3646 
3647             if (!this.checkFrameRate(evt)) {
3648                 return false;
3649             }
3650 
3651             if (this.mode !== this.BOARD_MODE_NONE) {
3652                 evt.preventDefault();
3653                 evt.stopPropagation();
3654             }
3655 
3656             if (this.mode !== this.BOARD_MODE_DRAG) {
3657                 this.dehighlightAll();
3658                 this.displayInfobox(false);
3659             }
3660 
3661             this._inputDevice = 'touch';
3662             this.options.precision.hasPoint = this.options.precision.touch;
3663             this.updateQuality = this.BOARD_QUALITY_LOW;
3664 
3665             // selection
3666             if (this.selectingMode) {
3667                 for (i = 0; i < evtTouches.length; i++) {
3668                     if (!evtTouches[i].jxg_isused) {
3669                         pos1 = this.getMousePosition(evt, i);
3670                         this._moveSelecting(pos1);
3671                         this.triggerEventHandlers(
3672                             ['touchmoves', 'moveselecting'],
3673                             [evt, this.mode]
3674                         );
3675                         break;
3676                     }
3677                 }
3678             } else {
3679                 if (!this.touchOriginMove(evt)) {
3680 
3681                     this.addToSketchCurve(evt);
3682 
3683                     if (this.mode === this.BOARD_MODE_DRAG) {
3684                         // Runs over through all elements which are touched
3685                         // by at least one finger.
3686                         for (i = 0; i < this.touches.length; i++) {
3687                             touchTargets = this.touches[i].targets;
3688                             if (touchTargets.length === 1) {
3689                                 // Touch by one finger:  this is possible for all elements that can be dragged
3690                                 if (evtTouches[touchTargets[0].num]) {
3691                                     pos1 = this.getMousePosition(evt, touchTargets[0].num);
3692                                     if (
3693                                         pos1[0] < 0 ||
3694                                         pos1[0] > this.canvasWidth ||
3695                                         pos1[1] < 0 ||
3696                                         pos1[1] > this.canvasHeight
3697                                     ) {
3698                                         return;
3699                                     }
3700                                     touchTargets[0].X = pos1[0];
3701                                     touchTargets[0].Y = pos1[1];
3702                                     this.moveObject(
3703                                         pos1[0],
3704                                         pos1[1],
3705                                         this.touches[i],
3706                                         evt,
3707                                         'touch'
3708                                     );
3709                                 }
3710                             } else if (
3711                                 touchTargets.length === 2 &&
3712                                 touchTargets[0].num > -1 &&
3713                                 touchTargets[1].num > -1
3714                             ) {
3715                                 // Touch by two fingers: moving lines, ...
3716                                 if (
3717                                     evtTouches[touchTargets[0].num] &&
3718                                     evtTouches[touchTargets[1].num]
3719                                 ) {
3720                                     // Get coordinates of the two touches
3721                                     pos1 = this.getMousePosition(evt, touchTargets[0].num);
3722                                     pos2 = this.getMousePosition(evt, touchTargets[1].num);
3723                                     if (
3724                                         pos1[0] < 0 ||
3725                                         pos1[0] > this.canvasWidth ||
3726                                         pos1[1] < 0 ||
3727                                         pos1[1] > this.canvasHeight ||
3728                                         pos2[0] < 0 ||
3729                                         pos2[0] > this.canvasWidth ||
3730                                         pos2[1] < 0 ||
3731                                         pos2[1] > this.canvasHeight
3732                                     ) {
3733                                         return;
3734                                     }
3735 
3736                                     touchTargets[0].X = pos1[0];
3737                                     touchTargets[0].Y = pos1[1];
3738                                     touchTargets[1].X = pos2[0];
3739                                     touchTargets[1].Y = pos2[1];
3740 
3741                                     this.twoFingerMove(
3742                                         this.touches[i],
3743                                         touchTargets[0].num,
3744                                         evt
3745                                     );
3746 
3747                                     touchTargets[0].Xprev = pos1[0];
3748                                     touchTargets[0].Yprev = pos1[1];
3749                                     touchTargets[1].Xprev = pos2[0];
3750                                     touchTargets[1].Yprev = pos2[1];
3751                                 }
3752                             }
3753                         }
3754                     } else {
3755                         if (evtTouches.length === 2) {
3756                             this.gestureChangeListener(evt);
3757                         }
3758                         // Move event without dragging an element
3759                         pos1 = this.getMousePosition(evt, 0);
3760                         this.highlightElements(pos1[0], pos1[1], evt, -1);
3761                     }
3762                 }
3763             }
3764 
3765             if (this.mode !== this.BOARD_MODE_DRAG) {
3766                 this.displayInfobox(false);
3767             }
3768 
3769             this.triggerEventHandlers(['touchmove', 'move'], [evt, this.mode]);
3770             this.options.precision.hasPoint = this.options.precision.mouse;
3771             this.updateQuality = this.BOARD_QUALITY_HIGH;
3772 
3773             return this.mode === this.BOARD_MODE_NONE;
3774         },
3775 
3776         /**
3777          * Triggered as soon as the user stops touching the device with at least one finger.
3778          * @param {Event} evt
3779          * @returns {Boolean}
3780          */
3781         touchEndListener: function (evt) {
3782             var i,
3783                 j,
3784                 k,
3785                 eps = this.options.precision.touch,
3786                 tmpTouches = [],
3787                 found,
3788                 foundNumber,
3789                 evtTouches = evt && evt['touches'],
3790                 touchTargets,
3791                 updateNeeded = false;
3792 
3793             this.triggerEventHandlers(['touchend', 'up'], [evt]);
3794             this.displayInfobox(false);
3795 
3796             this.finalizeSketchCurve(evt);
3797 
3798             // selection
3799             if (this.selectingMode) {
3800                 this._stopSelecting(evt);
3801                 this.triggerEventHandlers(['touchstopselecting', 'stopselecting'], [evt]);
3802                 this.stopSelectionMode();
3803             } else if (evtTouches && evtTouches.length > 0) {
3804                 for (i = 0; i < this.touches.length; i++) {
3805                     tmpTouches[i] = this.touches[i];
3806                 }
3807                 this.touches.length = 0;
3808 
3809                 // try to convert the operation, e.g. if a lines is rotated and translated with two fingers and one finger is lifted,
3810                 // convert the operation to a simple one-finger-translation.
3811                 // ADDENDUM 11/10/11:
3812                 // see addendum to touchStartListener from 11/10/11
3813                 // (1) run through the tmptouches
3814                 // (2) check the touches.obj, if it is a
3815                 //     (a) point, try to find the targettouch, if found keep it and mark the targettouch, else drop the touch.
3816                 //     (b) line with
3817                 //          (i) one target: try to find it, if found keep it mark the targettouch, else drop the touch.
3818                 //         (ii) two targets: if none can be found, drop the touch. if one can be found, remove the other target. mark all found targettouches
3819                 //     (c) circle with [proceed like in line]
3820 
3821                 // init the targettouches marker
3822                 for (i = 0; i < evtTouches.length; i++) {
3823                     evtTouches[i].jxg_isused = false;
3824                 }
3825 
3826                 for (i = 0; i < tmpTouches.length; i++) {
3827                     // could all targets of the current this.touches.obj be assigned to targettouches?
3828                     found = false;
3829                     foundNumber = 0;
3830                     touchTargets = tmpTouches[i].targets;
3831 
3832                     for (j = 0; j < touchTargets.length; j++) {
3833                         touchTargets[j].found = false;
3834                         for (k = 0; k < evtTouches.length; k++) {
3835                             if (
3836                                 Math.abs(
3837                                     Math.pow(evtTouches[k].screenX - touchTargets[j].X, 2) +
3838                                     Math.pow(evtTouches[k].screenY - touchTargets[j].Y, 2)
3839                                 ) <
3840                                 eps * eps
3841                             ) {
3842                                 touchTargets[j].found = true;
3843                                 touchTargets[j].num = k;
3844                                 touchTargets[j].X = evtTouches[k].screenX;
3845                                 touchTargets[j].Y = evtTouches[k].screenY;
3846                                 foundNumber += 1;
3847                                 break;
3848                             }
3849                         }
3850                     }
3851 
3852                     if (Type.isPoint(tmpTouches[i].obj)) {
3853                         found = touchTargets[0] && touchTargets[0].found;
3854                     } else if (tmpTouches[i].obj.elementClass === Const.OBJECT_CLASS_LINE) {
3855                         found =
3856                             (touchTargets[0] && touchTargets[0].found) ||
3857                             (touchTargets[1] && touchTargets[1].found);
3858                     } else if (tmpTouches[i].obj.elementClass === Const.OBJECT_CLASS_CIRCLE) {
3859                         found = foundNumber === 1 || foundNumber === 3;
3860                     }
3861 
3862                     // if we found this object to be still dragged by the user, add it back to this.touches
3863                     if (found) {
3864                         this.touches.push({
3865                             obj: tmpTouches[i].obj,
3866                             targets: []
3867                         });
3868 
3869                         for (j = 0; j < touchTargets.length; j++) {
3870                             if (touchTargets[j].found) {
3871                                 this.touches[this.touches.length - 1].targets.push({
3872                                     num: touchTargets[j].num,
3873                                     X: touchTargets[j].screenX,
3874                                     Y: touchTargets[j].screenY,
3875                                     Xprev: NaN,
3876                                     Yprev: NaN,
3877                                     Xstart: touchTargets[j].Xstart,
3878                                     Ystart: touchTargets[j].Ystart,
3879                                     Zstart: touchTargets[j].Zstart
3880                                 });
3881                             }
3882                         }
3883                     } else {
3884                         tmpTouches[i].obj.noHighlight();
3885                     }
3886                 }
3887             } else {
3888                 this.touches.length = 0;
3889             }
3890 
3891             for (i = this.downObjects.length - 1; i > -1; i--) {
3892                 found = false;
3893                 for (j = 0; j < this.touches.length; j++) {
3894                     if (this.touches[j].obj.id === this.downObjects[i].id) {
3895                         found = true;
3896                     }
3897                 }
3898                 if (!found) {
3899                     this.downObjects[i].triggerEventHandlers(['touchup', 'up'], [evt]);
3900                     if (!Type.exists(this.downObjects[i].coords)) {
3901                         // snapTo methods have to be called e.g. for line elements here.
3902                         // For coordsElements there might be a conflict with
3903                         // attractors, see commit from 2022.04.08, 11:12:18.
3904                         this.downObjects[i].snapToGrid();
3905                         this.downObjects[i].snapToPoints();
3906                         updateNeeded = true;
3907                     }
3908                     this.downObjects.splice(i, 1);
3909                 }
3910             }
3911 
3912             if (!evtTouches || evtTouches.length === 0) {
3913                 if (this.hasTouchEnd) {
3914                     Env.removeEvent(this.document, 'touchend', this.touchEndListener, this);
3915                     this.hasTouchEnd = false;
3916                 }
3917 
3918                 this.dehighlightAll();
3919                 this.updateQuality = this.BOARD_QUALITY_HIGH;
3920 
3921                 this.originMoveEnd();
3922                 if (updateNeeded) {
3923                     this.update();
3924                 }
3925             }
3926 
3927             return true;
3928         },
3929 
3930         /**
3931          * This method is called by the browser when the mouse button is clicked.
3932          * @param {Event} evt The browsers event object.
3933          * @returns {Boolean} True if no element is found under the current mouse pointer, false otherwise.
3934          */
3935         mouseDownListener: function (evt) {
3936             var pos, elements, result;
3937 
3938             // prevent accidental selection of text
3939             if (this.document.selection && Type.isFunction(this.document.selection.empty)) {
3940                 this.document.selection.empty();
3941             } else if (window.getSelection) {
3942                 window.getSelection().removeAllRanges();
3943             }
3944 
3945             if (!this.hasMouseUp) {
3946                 Env.addEvent(this.document, 'mouseup', this.mouseUpListener, this);
3947                 this.hasMouseUp = true;
3948             } else {
3949                 // In case this.hasMouseUp==true, it may be that there was a
3950                 // mousedown event before which was not followed by an mouseup event.
3951                 // This seems to happen with interactive whiteboard pens sometimes.
3952                 return;
3953             }
3954 
3955             this._inputDevice = 'mouse';
3956             this.options.precision.hasPoint = this.options.precision.mouse;
3957             pos = this.getMousePosition(evt);
3958 
3959             // selection
3960             this._testForSelection(evt);
3961             if (this.selectingMode) {
3962                 this._startSelecting(pos);
3963                 this.triggerEventHandlers(['mousestartselecting', 'startselecting'], [evt]);
3964                 return; // don't continue as a normal click
3965             }
3966 
3967             elements = this.initMoveObject(pos[0], pos[1], evt, 'mouse');
3968 
3969             // if no draggable object can be found, get out here immediately
3970             if (elements.length === 0) {
3971                 this.mode = this.BOARD_MODE_NONE;
3972                 result = true;
3973             } else {
3974                 this.mouse = {
3975                     obj: null,
3976                     targets: [
3977                         {
3978                             X: pos[0],
3979                             Y: pos[1],
3980                             Xprev: NaN,
3981                             Yprev: NaN
3982                         }
3983                     ]
3984                 };
3985                 this.mouse.obj = elements[elements.length - 1];
3986 
3987                 this.dehighlightAll();
3988                 this.mouse.obj.highlight(true);
3989 
3990                 this.mouse.targets[0].Xstart = [];
3991                 this.mouse.targets[0].Ystart = [];
3992                 this.mouse.targets[0].Zstart = [];
3993 
3994                 this.saveStartPos(this.mouse.obj, this.mouse.targets[0]);
3995 
3996                 // prevent accidental text selection
3997                 // this could get us new trouble: input fields, links and drop down boxes placed as text
3998                 // on the board don't work anymore.
3999                 if (evt && evt.preventDefault) {
4000                     evt.preventDefault();
4001                 } else if (window.event) {
4002                     window.event.returnValue = false;
4003                 }
4004             }
4005 
4006             if (this.mode === this.BOARD_MODE_NONE) {
4007                 result = this.mouseOriginMoveStart(evt);
4008             }
4009 
4010             this.initSketchCurve(evt);
4011             this.triggerEventHandlers(['mousedown', 'down'], [evt]);
4012 
4013             return result;
4014         },
4015 
4016         /**
4017          * This method is called by the browser when the mouse is moved.
4018          * @param {Event} evt The browsers event object.
4019          */
4020         mouseMoveListener: function (evt) {
4021             var pos;
4022 
4023             if (!this.checkFrameRate(evt)) {
4024                 return false;
4025             }
4026 
4027             pos = this.getMousePosition(evt);
4028 
4029             this.updateQuality = this.BOARD_QUALITY_LOW;
4030 
4031             if (this.mode !== this.BOARD_MODE_DRAG) {
4032                 this.dehighlightAll();
4033                 this.displayInfobox(false);
4034             }
4035 
4036             // we have to check for four cases:
4037             //   * user moves origin
4038             //   * user drags an object
4039             //   * user just moves the mouse, here highlight all elements at
4040             //     the current mouse position
4041             //   * the user is selecting
4042 
4043             // selection
4044             if (this.selectingMode) {
4045                 this._moveSelecting(pos);
4046                 this.triggerEventHandlers(
4047                     ['mousemoveselecting', 'moveselecting'],
4048                     [evt, this.mode]
4049                 );
4050             } else if (!this.mouseOriginMove(evt)) {
4051 
4052                 this.addToSketchCurve(evt);
4053 
4054                 if (this.mode === this.BOARD_MODE_DRAG) {
4055                     this.moveObject(pos[0], pos[1], this.mouse, evt, 'mouse');
4056                 } else {
4057                     // BOARD_MODE_NONE
4058                     // Move event without dragging an element
4059                     this.highlightElements(pos[0], pos[1], evt, -1);
4060                 }
4061                 this.triggerEventHandlers(['mousemove', 'move'], [evt, this.mode]);
4062             }
4063             this.updateQuality = this.BOARD_QUALITY_HIGH;
4064         },
4065 
4066         /**
4067          * This method is called by the browser when the mouse button is released.
4068          * @param {Event} evt
4069          */
4070         mouseUpListener: function (evt) {
4071             var i;
4072 
4073             if (this.selectingMode === false) {
4074                 this.triggerEventHandlers(['mouseup', 'up'], [evt]);
4075             }
4076 
4077             // redraw with high precision
4078             this.updateQuality = this.BOARD_QUALITY_HIGH;
4079 
4080             if (this.mouse && this.mouse.obj) {
4081                 if (!Type.exists(this.mouse.obj.coords)) {
4082                     // snapTo methods have to be called e.g. for line elements here.
4083                     // For coordsElements there might be a conflict with
4084                     // attractors, see commit from 2022.04.08, 11:12:18.
4085                     // The parameter is needed for lines with snapToGrid enabled
4086                     this.mouse.obj.snapToGrid(this.mouse.targets[0]);
4087                     this.mouse.obj.snapToPoints();
4088                 }
4089             }
4090 
4091             this.finalizeSketchCurve(evt);
4092             this.originMoveEnd();
4093             this.dehighlightAll();
4094             this.update();
4095 
4096             // selection
4097             if (this.selectingMode) {
4098                 this._stopSelecting(evt);
4099                 this.triggerEventHandlers(['mousestopselecting', 'stopselecting'], [evt]);
4100                 this.stopSelectionMode();
4101             } else {
4102                 for (i = 0; i < this.downObjects.length; i++) {
4103                     this.downObjects[i].triggerEventHandlers(['mouseup', 'up'], [evt]);
4104                 }
4105             }
4106 
4107             this.downObjects.length = 0;
4108 
4109             if (this.hasMouseUp) {
4110                 Env.removeEvent(this.document, 'mouseup', this.mouseUpListener, this);
4111                 this.hasMouseUp = false;
4112             }
4113 
4114             // release dragged mouse object
4115             this.mouse = null;
4116         },
4117 
4118         /**
4119          * Handler for mouse wheel events. Used to zoom in and out of the board.
4120          * @param {Event} evt
4121          * @returns {Boolean}
4122          */
4123         mouseWheelListener: function (evt) {
4124             var wd, zoomCenter, pos;
4125 
4126             if (!this.attr.zoom.enabled ||
4127                 !this.attr.zoom.wheel ||
4128                 !this._isRequiredKeyPressed(evt, 'zoom')) {
4129 
4130                 return true;
4131             }
4132 
4133             evt = evt || window.event;
4134             wd = evt.detail ? -evt.detail : evt.wheelDelta / 40;
4135             zoomCenter = this.attr.zoom.center;
4136 
4137             if (zoomCenter === 'board') {
4138                 pos = [];
4139             } else { // including zoomCenter === 'auto'
4140                 pos = new Coords(Const.COORDS_BY_SCREEN, this.getMousePosition(evt), this).usrCoords;
4141             }
4142 
4143             // pos == [] does not throw an error
4144             if (wd > 0) {
4145                 this.zoomIn(pos[1], pos[2]);
4146             } else {
4147                 this.zoomOut(pos[1], pos[2]);
4148             }
4149 
4150             this.triggerEventHandlers(['mousewheel'], [evt]);
4151 
4152             evt.preventDefault();
4153             return false;
4154         },
4155 
4156         /**
4157          * Allow moving of JSXGraph elements with arrow keys.
4158          * The selection of the element is done with the tab key. For this,
4159          * the attribute 'tabindex' of the element has to be set to some number (default=0).
4160          * tabindex corresponds to the HTML and SVG attribute of the same name.
4161          * <p>
4162          * Panning of the construction is done with arrow keys
4163          * if the pan key (shift or ctrl - depending on the board attributes) is pressed.
4164          * <p>
4165          * Zooming is triggered with the keys +, o, -, if
4166          * the pan key (shift or ctrl - depending on the board attributes) is pressed.
4167          * <p>
4168          * Keyboard control (move, pan, and zoom) is disabled if an HTML element of type input or textarea has received focus.
4169          *
4170          * @param  {Event} evt The browser's event object
4171          *
4172          * @see JXG.Board#keyboard
4173          * @see JXG.Board#keyFocusInListener
4174          * @see JXG.Board#keyFocusOutListener
4175          *
4176          */
4177         keyDownListener: function (evt) {
4178             var id_node = evt.target.id,
4179                 id, el, res, doc,
4180                 sX = 0,
4181                 sY = 0,
4182                 // dx, dy are provided in screen units and
4183                 // are converted to user coordinates
4184                 dx = Type.evaluate(this.attr.keyboard.dx) / this.unitX,
4185                 dy = Type.evaluate(this.attr.keyboard.dy) / this.unitY,
4186                 // u = 100,
4187                 doZoom = false,
4188                 done = true,
4189                 dir,
4190                 actPos;
4191 
4192             if (!this.attr.keyboard.enabled || id_node === '') {
4193                 return false;
4194             }
4195 
4196             // Tab key should be handled by the browser
4197             if (evt.keyCode === 9) {
4198                 return false;
4199             }
4200 
4201             // dx = Math.round(dx * u) / u;
4202             // dy = Math.round(dy * u) / u;
4203 
4204             // An element of type input or textarea has focus, get out of here.
4205             doc = this.containerObj.shadowRoot || document;
4206             if (doc.activeElement) {
4207                 el = doc.activeElement;
4208                 if (el.tagName === 'INPUT' || el.tagName === 'textarea') {
4209                     return false;
4210                 }
4211             }
4212 
4213             // Get the JSXGraph id from the id of the SVG node.
4214             id = id_node.replace(this.containerObj.id + '_', '');
4215             el = this.select(id);
4216 
4217             if (Type.exists(el.coords)) {
4218                 actPos = el.coords.usrCoords.slice(1);
4219             }
4220 
4221             if (
4222                 (Type.evaluate(this.attr.keyboard.panshift) && evt.shiftKey) ||
4223                 (Type.evaluate(this.attr.keyboard.panctrl) && evt.ctrlKey)
4224             ) {
4225                 // Pan key has been pressed
4226 
4227                 if (Type.evaluate(this.attr.zoom.enabled) === true) {
4228                     doZoom = true;
4229                 }
4230 
4231                 // Arrow keys
4232                 if (evt.keyCode === 38) {
4233                     // up
4234                     this.clickUpArrow();
4235                 } else if (evt.keyCode === 40) {
4236                     // down
4237                     this.clickDownArrow();
4238                 } else if (evt.keyCode === 37) {
4239                     // left
4240                     this.clickLeftArrow();
4241                 } else if (evt.keyCode === 39) {
4242                     // right
4243                     this.clickRightArrow();
4244 
4245                     // Zoom keys
4246                 } else if (doZoom && evt.keyCode === 171) {
4247                     // +
4248                     this.zoomIn();
4249                 } else if (doZoom && evt.keyCode === 173) {
4250                     // -
4251                     this.zoomOut();
4252                 } else if (doZoom && evt.keyCode === 79) {
4253                     // o
4254                     this.zoom100();
4255                 } else {
4256                     done = false;
4257                 }
4258             } else if (!evt.shiftKey && !evt.ctrlKey) {         // Move an element if neither shift or ctrl are pressed
4259                 // Adapt dx, dy to snapToGrid and attractToGrid.
4260                 // snapToGrid has priority.
4261                 if (Type.exists(el.visProp)) {
4262                     if (
4263                         Type.exists(el.visProp.snaptogrid) &&
4264                         el.visProp.snaptogrid &&
4265                         el.evalVisProp('snapsizex') &&
4266                         el.evalVisProp('snapsizey')
4267                     ) {
4268                         // Adapt dx, dy such that snapToGrid is possible
4269                         res = el.getSnapSizes();
4270                         sX = res[0];
4271                         sY = res[1];
4272                         // If snaptogrid is true,
4273                         // we can only jump from grid point to grid point.
4274                         dx = sX;
4275                         dy = sY;
4276                     } else if (
4277                         Type.exists(el.visProp.attracttogrid) &&
4278                         el.visProp.attracttogrid &&
4279                         el.evalVisProp('attractordistance') &&
4280                         el.evalVisProp('attractorunit')
4281                     ) {
4282                         // Adapt dx, dy such that attractToGrid is possible
4283                         sX = 1.1 * el.evalVisProp('attractordistance');
4284                         sY = sX;
4285 
4286                         if (el.evalVisProp('attractorunit') === 'screen') {
4287                             sX /= this.unitX;
4288                             sY /= this.unitX;
4289                         }
4290                         dx = Math.max(sX, dx);
4291                         dy = Math.max(sY, dy);
4292                     }
4293                 }
4294 
4295                 if (evt.keyCode === 38) {
4296                     // up
4297                     dir = [0, dy];
4298                 } else if (evt.keyCode === 40) {
4299                     // down
4300                     dir = [0, -dy];
4301                 } else if (evt.keyCode === 37) {
4302                     // left
4303                     dir = [-dx, 0];
4304                 } else if (evt.keyCode === 39) {
4305                     // right
4306                     dir = [dx, 0];
4307                 } else {
4308                     done = false;
4309                 }
4310 
4311                 if (dir && el.isDraggable &&
4312                     el.visPropCalc.visible &&
4313                     ((this.geonextCompatibilityMode &&
4314                         (Type.isPoint(el) ||
4315                             el.elementClass === Const.OBJECT_CLASS_TEXT)
4316                     ) || !this.geonextCompatibilityMode) &&
4317                     !el.evalVisProp('fixed')
4318                 ) {
4319                     this.mode = this.BOARD_MODE_DRAG;
4320                     if (Type.exists(el.coords)) {
4321                         dir[0] += actPos[0];
4322                         dir[1] += actPos[1];
4323                     }
4324                     // For coordsElement setPosition has to call setPositionDirectly.
4325                     // Otherwise the position is set by a translation.
4326                     if (Type.exists(el.coords)) {
4327                         el.setPosition(JXG.COORDS_BY_USER, dir);
4328                         this.updateInfobox(el);
4329                     } else {
4330                         this.displayInfobox(false);
4331                         el.setPositionDirectly(
4332                             Const.COORDS_BY_USER,
4333                             dir,
4334                             [0, 0]
4335                         );
4336                     }
4337 
4338                     this.triggerEventHandlers(['keymove', 'move'], [evt, this.mode]);
4339                     el.triggerEventHandlers(['keydrag', 'drag'], [evt]);
4340                     this.mode = this.BOARD_MODE_NONE;
4341                 }
4342             }
4343 
4344             this.update();
4345 
4346             if (done && Type.exists(evt.preventDefault)) {
4347                 evt.preventDefault();
4348             }
4349             return done;
4350         },
4351 
4352         /**
4353          * Event listener for SVG elements getting focus.
4354          * This is needed for highlighting when using keyboard control.
4355          * Only elements having the attribute 'tabindex' can receive focus.
4356          *
4357          * @see JXG.Board#keyFocusOutListener
4358          * @see JXG.Board#keyDownListener
4359          * @see JXG.Board#keyboard
4360          *
4361          * @param  {Event} evt The browser's event object
4362          */
4363         keyFocusInListener: function (evt) {
4364             var id_node = evt.target.id,
4365                 id,
4366                 el;
4367 
4368             if (!this.attr.keyboard.enabled || id_node === '') {
4369                 return false;
4370             }
4371 
4372             // Get JSXGraph id from node id
4373             id = id_node.replace(this.containerObj.id + '_', '');
4374             el = this.select(id);
4375             if (Type.exists(el.highlight)) {
4376                 el.highlight(true);
4377                 this.focusObjects = [id];
4378                 el.triggerEventHandlers(['hit'], [evt]);
4379             }
4380             if (Type.exists(el.coords)) {
4381                 this.updateInfobox(el);
4382             }
4383         },
4384 
4385         /**
4386          * Event listener for SVG elements losing focus.
4387          * This is needed for dehighlighting when using keyboard control.
4388          * Only elements having the attribute 'tabindex' can receive focus.
4389          *
4390          * @see JXG.Board#keyFocusInListener
4391          * @see JXG.Board#keyDownListener
4392          * @see JXG.Board#keyboard
4393          *
4394          * @param  {Event} evt The browser's event object
4395          */
4396         keyFocusOutListener: function (evt) {
4397             if (!this.attr.keyboard.enabled) {
4398                 return false;
4399             }
4400             this.focusObjects = []; // This has to be before displayInfobox(false)
4401             this.dehighlightAll();
4402             this.displayInfobox(false);
4403         },
4404 
4405         /**
4406          * Update the width and height of the JSXGraph container div element.
4407          * If width and height are not supplied, read actual values with offsetWidth/Height,
4408          * and call board.resizeContainer() with this values.
4409          * <p>
4410          * If necessary, also call setBoundingBox().
4411          * @param {Number} [width=this.containerObj.offsetWidth] Width of the container element
4412          * @param {Number} [height=this.containerObj.offsetHeight] Height of the container element
4413          * @returns {JXG.Board} Reference to the board
4414          *
4415          * @see JXG.Board#startResizeObserver
4416          * @see JXG.Board#resizeListener
4417          * @see JXG.Board#resizeContainer
4418          * @see JXG.Board#setBoundingBox
4419          *
4420          */
4421         updateContainerDims: function (width, height) {
4422             var w = width,
4423                 h = height,
4424                 // bb,
4425                 css,
4426                 width_adjustment, height_adjustment;
4427 
4428             if (width === undefined) {
4429                 // Get size of the board's container div
4430                 //
4431                 // offsetWidth/Height ignores CSS transforms,
4432                 // getBoundingClientRect includes CSS transforms
4433                 //
4434                 // bb = this.containerObj.getBoundingClientRect();
4435                 // w = bb.width;
4436                 // h = bb.height;
4437                 w = this.containerObj.offsetWidth;
4438                 h = this.containerObj.offsetHeight;
4439             }
4440 
4441             if (width === undefined && window && window.getComputedStyle) {
4442                 // Subtract the border size
4443                 css = window.getComputedStyle(this.containerObj, null);
4444                 width_adjustment = parseFloat(css.getPropertyValue('border-left-width')) + parseFloat(css.getPropertyValue('border-right-width'));
4445                 if (!isNaN(width_adjustment)) {
4446                     w -= width_adjustment;
4447                 }
4448                 height_adjustment = parseFloat(css.getPropertyValue('border-top-width')) + parseFloat(css.getPropertyValue('border-bottom-width'));
4449                 if (!isNaN(height_adjustment)) {
4450                     h -= height_adjustment;
4451                 }
4452             }
4453 
4454             // If div is invisible - do nothing
4455             if (w <= 0 || h <= 0 || isNaN(w) || isNaN(h)) {
4456                 return this;
4457             }
4458 
4459             // If bounding box is not yet initialized, do it now.
4460             if (isNaN(this.getBoundingBox()[0])) {
4461                 this.setBoundingBox(this.attr.boundingbox, this.keepaspectratio, 'keep');
4462             }
4463 
4464             // Do nothing if the dimension did not change since being visible
4465             // the last time. Note that if the div had display:none in the mean time,
4466             // we did not store this._prevDim.
4467             if (Type.exists(this._prevDim) && this._prevDim.w === w && this._prevDim.h === h) {
4468                 return this;
4469             }
4470             // Set the size of the SVG or canvas element
4471             this.resizeContainer(w, h, true);
4472             this._prevDim = {
4473                 w: w,
4474                 h: h
4475             };
4476             return this;
4477         },
4478 
4479         /**
4480          * Start observer which reacts to size changes of the JSXGraph
4481          * container div element. Calls updateContainerDims().
4482          * If not available, an event listener for the window-resize event is started.
4483          * On mobile devices also scrolling might trigger resizes.
4484          * However, resize events triggered by scrolling events should be ignored.
4485          * Therefore, also a scrollListener is started.
4486          * Resize can be controlled with the board attribute resize.
4487          *
4488          * @see JXG.Board#updateContainerDims
4489          * @see JXG.Board#resizeListener
4490          * @see JXG.Board#scrollListener
4491          * @see JXG.Board#resize
4492          *
4493          */
4494         startResizeObserver: function () {
4495             var that = this;
4496 
4497             if (!Env.isBrowser || !this.attr.resize || !this.attr.resize.enabled) {
4498                 return;
4499             }
4500 
4501             this.resizeObserver = new ResizeObserver(function (entries) {
4502                 var bb;
4503                 if (!that._isResizing) {
4504                     that._isResizing = true;
4505                     bb = entries[0].contentRect;
4506                     window.setTimeout(function () {
4507                         try {
4508                             that.updateContainerDims(bb.width, bb.height);
4509                         } catch (e) {
4510                             JXG.debug(e);   // Used to log errors during board.update()
4511                             that.stopResizeObserver();
4512                         } finally {
4513                             that._isResizing = false;
4514                         }
4515                     }, that.attr.resize.throttle);
4516                 }
4517             });
4518             this.resizeObserver.observe(this.containerObj);
4519         },
4520 
4521         /**
4522          * Stops the resize observer.
4523          * @see JXG.Board#startResizeObserver
4524          *
4525          */
4526         stopResizeObserver: function () {
4527             if (!Env.isBrowser || !this.attr.resize || !this.attr.resize.enabled) {
4528                 return;
4529             }
4530 
4531             if (Type.exists(this.resizeObserver)) {
4532                 this.resizeObserver.unobserve(this.containerObj);
4533             }
4534         },
4535 
4536         /**
4537          * Fallback solutions if there is no resizeObserver available in the browser.
4538          * Reacts to resize events of the window (only). Otherwise similar to
4539          * startResizeObserver(). To handle changes of the visibility
4540          * of the JSXGraph container element, additionally an intersection observer is used.
4541          * which watches changes in the visibility of the JSXGraph container element.
4542          * This is necessary e.g. for register tabs or dia shows.
4543          *
4544          * @see JXG.Board#startResizeObserver
4545          * @see JXG.Board#startIntersectionObserver
4546          */
4547         resizeListener: function () {
4548             var that = this;
4549 
4550             if (!Env.isBrowser || !this.attr.resize || !this.attr.resize.enabled) {
4551                 return;
4552             }
4553             if (!this._isScrolling && !this._isResizing) {
4554                 this._isResizing = true;
4555                 window.setTimeout(function () {
4556                     that.updateContainerDims();
4557                     that._isResizing = false;
4558                 }, this.attr.resize.throttle);
4559             }
4560         },
4561 
4562         /**
4563          * Listener to watch for scroll events. Sets board._isScrolling = true
4564          * @param  {Event} evt The browser's event object
4565          *
4566          * @see JXG.Board#startResizeObserver
4567          * @see JXG.Board#resizeListener
4568          *
4569          */
4570         scrollListener: function (evt) {
4571             var that = this;
4572 
4573             if (!Env.isBrowser) {
4574                 return;
4575             }
4576             if (!this._isScrolling) {
4577                 this._isScrolling = true;
4578                 window.setTimeout(function () {
4579                     that._isScrolling = false;
4580                 }, 66);
4581             }
4582         },
4583 
4584         /**
4585          * Watch for changes of the visibility of the JSXGraph container element.
4586          *
4587          * @see JXG.Board#startResizeObserver
4588          * @see JXG.Board#resizeListener
4589          *
4590          */
4591         startIntersectionObserver: function () {
4592             var that = this,
4593                 options = {
4594                     root: null,
4595                     rootMargin: '0px',
4596                     threshold: 0.8
4597                 };
4598 
4599             try {
4600                 this.intersectionObserver = new IntersectionObserver(function (entries) {
4601                     // If bounding box is not yet initialized, do it now.
4602                     if (isNaN(that.getBoundingBox()[0])) {
4603                         that.updateContainerDims();
4604                     }
4605                 }, options);
4606                 this.intersectionObserver.observe(that.containerObj);
4607             } catch (err) {
4608                 JXG.debug('JSXGraph: IntersectionObserver not available in this browser.');
4609             }
4610         },
4611 
4612         /**
4613          * Stop the intersection observer
4614          *
4615          * @see JXG.Board#startIntersectionObserver
4616          *
4617          */
4618         stopIntersectionObserver: function () {
4619             if (Type.exists(this.intersectionObserver)) {
4620                 this.intersectionObserver.unobserve(this.containerObj);
4621             }
4622         },
4623 
4624         /**
4625          * Update the container before and after printing.
4626          * @param {Event} [evt]
4627          */
4628         printListener: function(evt) {
4629             this.updateContainerDims();
4630         },
4631 
4632         /**
4633          * Wrapper for printListener to be used in mediaQuery matches.
4634          * @param {MediaQueryList} mql
4635          */
4636         printListenerMatch: function (mql) {
4637             if (mql.matches) {
4638                 this.printListener();
4639             }
4640         },
4641 
4642         /**********************************************************
4643          *
4644          * End of Event Handlers
4645          *
4646          **********************************************************/
4647 
4648         /**
4649          * Initialize the info box object which is used to display
4650          * the coordinates of points near the mouse pointer,
4651          * @returns {JXG.Board} Reference to the board
4652          */
4653         initInfobox: function (attributes) {
4654             var attr = Type.copyAttributes(attributes, this.options, 'infobox');
4655 
4656             attr.id = this.id + '_infobox';
4657 
4658             /**
4659              * Infobox close to points in which the points' coordinates are displayed.
4660              * This is simply a JXG.Text element. Access through board.infobox.
4661              * Uses CSS class .JXGinfobox.
4662              *
4663              * @namespace
4664              * @name JXG.Board.infobox
4665              * @type JXG.Text
4666              *
4667              * @example
4668              * const board = JXG.JSXGraph.initBoard(BOARDID, {
4669              *     boundingbox: [-0.5, 0.5, 0.5, -0.5],
4670              *     intl: {
4671              *         enabled: false,
4672              *         locale: 'de-DE'
4673              *     },
4674              *     keepaspectratio: true,
4675              *     axis: true,
4676              *     infobox: {
4677              *         distanceY: 40,
4678              *         intl: {
4679              *             enabled: true,
4680              *             options: {
4681              *                 minimumFractionDigits: 1,
4682              *                 maximumFractionDigits: 2
4683              *             }
4684              *         }
4685              *     }
4686              * });
4687              * var p = board.create('point', [0.1, 0.1], {});
4688              *
4689              * </pre><div id="JXG822161af-fe77-4769-850f-cdf69935eab0" class="jxgbox" style="width: 300px; height: 300px;"></div>
4690              * <script type="text/javascript">
4691              *     (function() {
4692              *     const board = JXG.JSXGraph.initBoard('JXG822161af-fe77-4769-850f-cdf69935eab0', {
4693              *         boundingbox: [-0.5, 0.5, 0.5, -0.5], showcopyright: false, shownavigation: false,
4694              *         intl: {
4695              *             enabled: false,
4696              *             locale: 'de-DE'
4697              *         },
4698              *         keepaspectratio: true,
4699              *         axis: true,
4700              *         infobox: {
4701              *             distanceY: 40,
4702              *             intl: {
4703              *                 enabled: true,
4704              *                 options: {
4705              *                     minimumFractionDigits: 1,
4706              *                     maximumFractionDigits: 2
4707              *                 }
4708              *             }
4709              *         }
4710              *     });
4711              *     var p = board.create('point', [0.1, 0.1], {});
4712              *     })();
4713              *
4714              * </script><pre>
4715              *
4716              */
4717             this.infobox = this.create('text', [0, 0, '0,0'], attr);
4718             // this.infobox.needsUpdateSize = false;  // That is not true, but it speeds drawing up.
4719             this.infobox.dump = false;
4720 
4721             this.displayInfobox(false);
4722             return this;
4723         },
4724 
4725         /**
4726          * Updates and displays a little info box to show coordinates of current selected points.
4727          * @param {JXG.GeometryElement} el A GeometryElement
4728          * @returns {JXG.Board} Reference to the board
4729          * @see JXG.Board#displayInfobox
4730          * @see JXG.Board#showInfobox
4731          * @see Point#showInfobox
4732          *
4733          */
4734         updateInfobox: function (el) {
4735             var x, y, xc, yc,
4736                 vpinfoboxdigits,
4737                 distX, distY,
4738                 vpsi = el.evalVisProp('showinfobox');
4739 
4740             if ((!Type.evaluate(this.attr.showinfobox) && vpsi === 'inherit') || !vpsi) {
4741                 return this;
4742             }
4743 
4744             if (Type.isPoint(el)) {
4745                 xc = el.coords.usrCoords[1];
4746                 yc = el.coords.usrCoords[2];
4747                 distX = this.infobox.evalVisProp('distancex');
4748                 distY = this.infobox.evalVisProp('distancey');
4749 
4750                 this.infobox.setCoords(
4751                     xc + distX / this.unitX,
4752                     yc + distY / this.unitY
4753                 );
4754 
4755                 vpinfoboxdigits = el.evalVisProp('infoboxdigits');
4756                 if (typeof el.infoboxText !== 'string') {
4757                     if (vpinfoboxdigits === 'auto') {
4758                         if (this.infobox.useLocale()) {
4759                             x = this.infobox.formatNumberLocale(xc);
4760                             y = this.infobox.formatNumberLocale(yc);
4761                         } else {
4762                             x = Type.autoDigits(xc);
4763                             y = Type.autoDigits(yc);
4764                         }
4765                     } else if (Type.isNumber(vpinfoboxdigits)) {
4766                         if (this.infobox.useLocale()) {
4767                             x = this.infobox.formatNumberLocale(xc, vpinfoboxdigits);
4768                             y = this.infobox.formatNumberLocale(yc, vpinfoboxdigits);
4769                         } else {
4770                             x = Type.toFixed(xc, vpinfoboxdigits);
4771                             y = Type.toFixed(yc, vpinfoboxdigits);
4772                         }
4773 
4774                     } else {
4775                         x = xc;
4776                         y = yc;
4777                     }
4778 
4779                     this.highlightInfobox(x, y, el);
4780                 } else {
4781                     this.highlightCustomInfobox(el.infoboxText, el);
4782                 }
4783 
4784                 this.displayInfobox(true);
4785             }
4786             return this;
4787         },
4788 
4789         /**
4790          * Set infobox visible / invisible.
4791          *
4792          * It uses its property hiddenByParent to memorize its status.
4793          * In this way, many DOM access can be avoided.
4794          *
4795          * @param  {Boolean} val true for visible, false for invisible
4796          * @returns {JXG.Board} Reference to the board.
4797          * @see JXG.Board#updateInfobox
4798          *
4799          */
4800         displayInfobox: function (val) {
4801             if (!val && this.focusObjects.length > 0 &&
4802                 this.select(this.focusObjects[0]).elementClass === Const.OBJECT_CLASS_POINT) {
4803                 // If an element has focus we do not hide its infobox
4804                 return this;
4805             }
4806             if (this.infobox.hiddenByParent === val) {
4807                 this.infobox.hiddenByParent = !val;
4808                 this.infobox.prepareUpdate().updateVisibility(val).updateRenderer();
4809             }
4810             return this;
4811         },
4812 
4813         // Alias for displayInfobox to be backwards compatible.
4814         // The method showInfobox clashes with the board attribute showInfobox
4815         showInfobox: function (val) {
4816             return this.displayInfobox(val);
4817         },
4818 
4819         /**
4820          * Changes the text of the info box to show the given coordinates.
4821          * @param {Number} x
4822          * @param {Number} y
4823          * @param {JXG.GeometryElement} [el] The element the mouse is pointing at
4824          * @returns {JXG.Board} Reference to the board.
4825          */
4826         highlightInfobox: function (x, y, el) {
4827             this.highlightCustomInfobox('(' + x + ', ' + y + ')', el);
4828             return this;
4829         },
4830 
4831         /**
4832          * Changes the text of the info box to what is provided via text.
4833          * @param {String} text
4834          * @param {JXG.GeometryElement} [el]
4835          * @returns {JXG.Board} Reference to the board.
4836          */
4837         highlightCustomInfobox: function (text, el) {
4838             this.infobox.setText(text);
4839             return this;
4840         },
4841 
4842         /**
4843          * Remove highlighting of all elements.
4844          * @returns {JXG.Board} Reference to the board.
4845          */
4846         dehighlightAll: function () {
4847             var el,
4848                 pEl,
4849                 stillHighlighted = {},
4850                 needsDeHighlight = false;
4851 
4852             for (el in this.highlightedObjects) {
4853                 if (this.highlightedObjects.hasOwnProperty(el)) {
4854 
4855                     pEl = this.highlightedObjects[el];
4856                     if (this.focusObjects.indexOf(el) < 0) { // Element does not have focus
4857                         if (this.hasMouseHandlers || this.hasPointerHandlers) {
4858                             pEl.noHighlight();
4859                         }
4860                         needsDeHighlight = true;
4861                     } else {
4862                         stillHighlighted[el] = pEl;
4863                     }
4864                     // In highlightedObjects should only be objects which fulfill all these conditions
4865                     // And in case of complex elements, like a turtle based fractal, it should be faster to
4866                     // just de-highlight the element instead of checking hasPoint...
4867                     // if ((!Type.exists(pEl.hasPoint)) || !pEl.hasPoint(x, y) || !pEl.visPropCalc.visible)
4868                 }
4869             }
4870 
4871             this.highlightedObjects = stillHighlighted;
4872 
4873             // We do not need to redraw during dehighlighting in CanvasRenderer
4874             // because we are redrawing anyhow
4875             //  -- We do need to redraw during dehighlighting. Otherwise objects won't be dehighlighted until
4876             // another object is highlighted.
4877             if (this.renderer.type === 'canvas' && needsDeHighlight) {
4878                 this.prepareUpdate();
4879                 this.renderer.suspendRedraw(this);
4880                 this.updateRenderer();
4881                 this.renderer.unsuspendRedraw();
4882             }
4883 
4884             return this;
4885         },
4886 
4887         /**
4888          * Returns the input parameters in an array. This method looks pointless and it really is, but it had a purpose
4889          * once.
4890          * @private
4891          * @param {Number} x X coordinate in screen coordinates
4892          * @param {Number} y Y coordinate in screen coordinates
4893          * @returns {Array} Coordinates [x, y] of the mouse in screen coordinates.
4894          * @see JXG.Board#getUsrCoordsOfMouse
4895          */
4896         getScrCoordsOfMouse: function (x, y) {
4897             return [x, y];
4898         },
4899 
4900         /**
4901          * This method calculates the user coords of the current mouse coordinates.
4902          * @param {Event} evt Event object containing the mouse coordinates.
4903          * @returns {Array} Coordinates [x, y] of the mouse in user coordinates.
4904          * @example
4905          * board.on('up', function (evt) {
4906          *         var a = board.getUsrCoordsOfMouse(evt),
4907          *             x = a[0],
4908          *             y = a[1],
4909          *             somePoint = board.create('point', [x,y], {name:'SomePoint',size:4});
4910          *             // Shorter version:
4911          *             //somePoint = board.create('point', a, {name:'SomePoint',size:4});
4912          *         });
4913          *
4914          * </pre><div id='JXG48d5066b-16ba-4920-b8ea-a4f8eff6b746' class='jxgbox' style='width: 300px; height: 300px;'></div>
4915          * <script type='text/javascript'>
4916          *     (function() {
4917          *         var board = JXG.JSXGraph.initBoard('JXG48d5066b-16ba-4920-b8ea-a4f8eff6b746',
4918          *             {boundingbox: [-8, 8, 8,-8], axis: true, showcopyright: false, shownavigation: false});
4919          *     board.on('up', function (evt) {
4920          *             var a = board.getUsrCoordsOfMouse(evt),
4921          *                 x = a[0],
4922          *                 y = a[1],
4923          *                 somePoint = board.create('point', [x,y], {name:'SomePoint',size:4});
4924          *                 // Shorter version:
4925          *                 //somePoint = board.create('point', a, {name:'SomePoint',size:4});
4926          *             });
4927          *
4928          *     })();
4929          *
4930          * </script><pre>
4931          *
4932          * @see JXG.Board#getScrCoordsOfMouse
4933          * @see JXG.Board#getAllUnderMouse
4934          */
4935         getUsrCoordsOfMouse: function (evt) {
4936             var cPos = this.getCoordsTopLeftCorner(),
4937                 absPos = Env.getPosition(evt, null, this.document),
4938                 x = absPos[0] - cPos[0],
4939                 y = absPos[1] - cPos[1],
4940                 newCoords = new Coords(Const.COORDS_BY_SCREEN, [x, y], this);
4941 
4942             return newCoords.usrCoords.slice(1);
4943         },
4944 
4945         /**
4946          * Collects all elements under current mouse position plus current user coordinates of mouse cursor.
4947          * @param {Event} evt Event object containing the mouse coordinates.
4948          * @returns {Array} Array of elements at the current mouse position plus current user coordinates of mouse.
4949          * @see JXG.Board#getUsrCoordsOfMouse
4950          * @see JXG.Board#getAllObjectsUnderMouse
4951          */
4952         getAllUnderMouse: function (evt) {
4953             var elList = this.getAllObjectsUnderMouse(evt);
4954             elList.push(this.getUsrCoordsOfMouse(evt));
4955 
4956             return elList;
4957         },
4958 
4959         /**
4960          * Collects all elements under current mouse position.
4961          * @param {Event} evt Event object containing the mouse coordinates.
4962          * @returns {Array} Array of elements at the current mouse position.
4963          * @see JXG.Board#getAllUnderMouse
4964          */
4965         getAllObjectsUnderMouse: function (evt) {
4966             var cPos = this.getCoordsTopLeftCorner(),
4967                 absPos = Env.getPosition(evt, null, this.document),
4968                 dx = absPos[0] - cPos[0],
4969                 dy = absPos[1] - cPos[1],
4970                 elList = [],
4971                 el,
4972                 pEl,
4973                 len = this.objectsList.length;
4974 
4975             for (el = 0; el < len; el++) {
4976                 pEl = this.objectsList[el];
4977                 if (pEl.visPropCalc.visible && pEl.hasPoint && pEl.hasPoint(dx, dy)) {
4978                     elList[elList.length] = pEl;
4979                 }
4980             }
4981 
4982             return elList;
4983         },
4984 
4985         /**
4986          * Update the coords object of all elements which possess this
4987          * property. This is necessary after changing the viewport.
4988          * @returns {JXG.Board} Reference to this board.
4989          **/
4990         updateCoords: function () {
4991             var el, ob,
4992                 len = this.objectsList.length;
4993 
4994             for (ob = 0; ob < len; ob++) {
4995                 el = this.objectsList[ob];
4996 
4997                 if (Type.exists(el.coords)) {
4998                     if (el.evalVisProp('frozen') === true) {
4999                         if (el.is3D) {
5000                             el.element2D.coords.screen2usr();
5001                         } else {
5002                             el.coords.screen2usr();
5003                         }
5004                     } else {
5005                         if (el.is3D) {
5006                             el.element2D.coords.usr2screen();
5007                         } else {
5008                             el.coords.usr2screen();
5009                             if (Type.exists(el.actualCoords)) {
5010                                 el.actualCoords.usr2screen();
5011                             }
5012                         }
5013                     }
5014                 }
5015             }
5016             return this;
5017         },
5018 
5019         /**
5020          * Moves the origin and initializes an update of all elements.
5021          * @param {Number} x
5022          * @param {Number} y
5023          * @param {Boolean} [diff=false]
5024          * @returns {JXG.Board} Reference to this board.
5025          */
5026         moveOrigin: function (x, y, diff) {
5027             var ox, oy, ul, lr;
5028             if (Type.exists(x) && Type.exists(y)) {
5029                 ox = this.origin.scrCoords[1];
5030                 oy = this.origin.scrCoords[2];
5031 
5032                 this.origin.scrCoords[1] = x;
5033                 this.origin.scrCoords[2] = y;
5034 
5035                 if (diff) {
5036                     this.origin.scrCoords[1] -= this.drag_dx;
5037                     this.origin.scrCoords[2] -= this.drag_dy;
5038                 }
5039 
5040                 ul = new Coords(Const.COORDS_BY_SCREEN, [0, 0], this).usrCoords;
5041                 lr = new Coords(
5042                     Const.COORDS_BY_SCREEN,
5043                     [this.canvasWidth, this.canvasHeight],
5044                     this
5045                 ).usrCoords;
5046                 if (
5047                     ul[1] < this.maxboundingbox[0] - Mat.eps ||
5048                     ul[2] > this.maxboundingbox[1] + Mat.eps ||
5049                     lr[1] > this.maxboundingbox[2] + Mat.eps ||
5050                     lr[2] < this.maxboundingbox[3] - Mat.eps
5051                 ) {
5052                     this.origin.scrCoords[1] = ox;
5053                     this.origin.scrCoords[2] = oy;
5054                 }
5055             }
5056 
5057             this.updateCoords().clearTraces().fullUpdate();
5058             this.triggerEventHandlers(['boundingbox']);
5059 
5060             return this;
5061         },
5062 
5063         /**
5064          * Add conditional updates to the elements.
5065          * @param {String} str String containing conditional update in geonext syntax
5066          */
5067         addConditions: function (str) {
5068             var term,
5069                 m,
5070                 left,
5071                 right,
5072                 name,
5073                 el,
5074                 property,
5075                 functions = [],
5076                 // plaintext = 'var el, x, y, c, rgbo;\n',
5077                 i = str.indexOf('<data>'),
5078                 j = str.indexOf('<' + '/data>'),
5079                 xyFun = function (board, el, f, what) {
5080                     return function () {
5081                         var e, t;
5082 
5083                         e = board.select(el.id);
5084                         t = e.coords.usrCoords[what];
5085 
5086                         if (what === 2) {
5087                             e.setPositionDirectly(Const.COORDS_BY_USER, [f(), t]);
5088                         } else {
5089                             e.setPositionDirectly(Const.COORDS_BY_USER, [t, f()]);
5090                         }
5091                         e.prepareUpdate().update();
5092                     };
5093                 },
5094                 visFun = function (board, el, f) {
5095                     return function () {
5096                         var e, v;
5097 
5098                         e = board.select(el.id);
5099                         v = f();
5100 
5101                         e.setAttribute({ visible: v });
5102                     };
5103                 },
5104                 colFun = function (board, el, f, what) {
5105                     return function () {
5106                         var e, v;
5107 
5108                         e = board.select(el.id);
5109                         v = f();
5110 
5111                         if (what === 'strokewidth') {
5112                             e.visProp.strokewidth = v;
5113                         } else {
5114                             v = Color.rgba2rgbo(v);
5115                             e.visProp[what + 'color'] = v[0];
5116                             e.visProp[what + 'opacity'] = v[1];
5117                         }
5118                     };
5119                 },
5120                 posFun = function (board, el, f) {
5121                     return function () {
5122                         var e = board.select(el.id);
5123 
5124                         e.position = f();
5125                     };
5126                 },
5127                 styleFun = function (board, el, f) {
5128                     return function () {
5129                         var e = board.select(el.id);
5130 
5131                         e.setStyle(f());
5132                     };
5133                 };
5134 
5135             if (i < 0) {
5136                 return;
5137             }
5138 
5139             while (i >= 0) {
5140                 term = str.slice(i + 6, j); // throw away <data>
5141                 m = term.indexOf('=');
5142                 left = term.slice(0, m);
5143                 right = term.slice(m + 1);
5144                 m = left.indexOf('.');   // Resulting variable names must not contain dots, e.g. ' Steuern akt.'
5145                 name = left.slice(0, m); //.replace(/\s+$/,''); // do NOT cut out name (with whitespace)
5146                 el = this.elementsByName[Type.unescapeHTML(name)];
5147 
5148                 property = left
5149                     .slice(m + 1)
5150                     .replace(/\s+/g, '')
5151                     .toLowerCase(); // remove whitespace in property
5152                 right = Type.createFunction(right, this, '', true);
5153 
5154                 // Debug
5155                 if (!Type.exists(this.elementsByName[name])) {
5156                     JXG.debug('debug conditions: |' + name + '| undefined');
5157                 } else {
5158                     // plaintext += 'el = this.objects[\'' + el.id + '\'];\n';
5159 
5160                     switch (property) {
5161                         case 'x':
5162                             functions.push(xyFun(this, el, right, 2));
5163                             break;
5164                         case 'y':
5165                             functions.push(xyFun(this, el, right, 1));
5166                             break;
5167                         case 'visible':
5168                             functions.push(visFun(this, el, right));
5169                             break;
5170                         case 'position':
5171                             functions.push(posFun(this, el, right));
5172                             break;
5173                         case 'stroke':
5174                             functions.push(colFun(this, el, right, 'stroke'));
5175                             break;
5176                         case 'style':
5177                             functions.push(styleFun(this, el, right));
5178                             break;
5179                         case 'strokewidth':
5180                             functions.push(colFun(this, el, right, 'strokewidth'));
5181                             break;
5182                         case 'fill':
5183                             functions.push(colFun(this, el, right, 'fill'));
5184                             break;
5185                         case 'label':
5186                             break;
5187                         default:
5188                             JXG.debug(
5189                                 'property "' +
5190                                 property +
5191                                 '" in conditions not yet implemented:' +
5192                                 right
5193                             );
5194                             break;
5195                     }
5196                 }
5197                 str = str.slice(j + 7); // cut off '</data>'
5198                 i = str.indexOf('<data>');
5199                 j = str.indexOf('<' + '/data>');
5200             }
5201 
5202             this.updateConditions = function () {
5203                 var i;
5204 
5205                 for (i = 0; i < functions.length; i++) {
5206                     functions[i]();
5207                 }
5208 
5209                 this.prepareUpdate().updateElements();
5210                 return true;
5211             };
5212             this.updateConditions();
5213         },
5214 
5215         /**
5216          * Computes the commands in the conditions-section of the gxt file.
5217          * It is evaluated after an update, before the unsuspendRedraw.
5218          * The function is generated in
5219          * @see JXG.Board#addConditions
5220          * @private
5221          */
5222         updateConditions: function () {
5223             return false;
5224         },
5225 
5226         /**
5227          * Calculates adequate snap sizes.
5228          * @returns {JXG.Board} Reference to the board.
5229          */
5230         calculateSnapSizes: function () {
5231             var p1, p2,
5232                 bbox = this.getBoundingBox(),
5233                 gridStep = Type.evaluate(this.options.grid.majorStep),
5234                 gridX = Type.evaluate(this.options.grid.gridX),
5235                 gridY = Type.evaluate(this.options.grid.gridY),
5236                 x, y;
5237 
5238             if (!Type.isArray(gridStep)) {
5239                 gridStep = [gridStep, gridStep];
5240             }
5241             if (gridStep.length < 2) {
5242                 gridStep = [gridStep[0], gridStep[0]];
5243             }
5244             if (Type.exists(gridX)) {
5245                 gridStep[0] = gridX;
5246             }
5247             if (Type.exists(gridY)) {
5248                 gridStep[1] = gridY;
5249             }
5250 
5251             if (gridStep[0] === 'auto') {
5252                 gridStep[0] = 1;
5253             } else {
5254                 gridStep[0] = Type.parseNumber(gridStep[0], Math.abs(bbox[1] - bbox[3]), 1 / this.unitX);
5255             }
5256             if (gridStep[1] === 'auto') {
5257                 gridStep[1] = 1;
5258             } else {
5259                 gridStep[1] = Type.parseNumber(gridStep[1], Math.abs(bbox[0] - bbox[2]), 1 / this.unitY);
5260             }
5261 
5262             p1 = new Coords(Const.COORDS_BY_USER, [0, 0], this);
5263             p2 = new Coords(
5264                 Const.COORDS_BY_USER,
5265                 [gridStep[0], gridStep[1]],
5266                 this
5267             );
5268             x = p1.scrCoords[1] - p2.scrCoords[1];
5269             y = p1.scrCoords[2] - p2.scrCoords[2];
5270 
5271             this.options.grid.snapSizeX = gridStep[0];
5272             while (Math.abs(x) > 25) {
5273                 this.options.grid.snapSizeX *= 2;
5274                 x /= 2;
5275             }
5276 
5277             this.options.grid.snapSizeY = gridStep[1];
5278             while (Math.abs(y) > 25) {
5279                 this.options.grid.snapSizeY *= 2;
5280                 y /= 2;
5281             }
5282 
5283             return this;
5284         },
5285 
5286         /**
5287          * Apply update on all objects with the new zoom-factors. Clears all traces.
5288          * @returns {JXG.Board} Reference to the board.
5289          */
5290         applyZoom: function () {
5291             this.updateCoords().calculateSnapSizes().clearTraces().fullUpdate();
5292 
5293             return this;
5294         },
5295 
5296         /**
5297          * Zooms into the board by the factors board.attr.zoom.factorX and board.attr.zoom.factorY and applies the zoom.
5298          * The zoom operation is centered at x, y.
5299          * @param {Number} [x]
5300          * @param {Number} [y]
5301          * @returns {JXG.Board} Reference to the board
5302          */
5303         zoomIn: function (x, y) {
5304             var bb = this.getBoundingBox(),
5305                 zX = Type.evaluate(this.attr.zoom.factorx),
5306                 zY =  Type.evaluate(this.attr.zoom.factory),
5307                 dX = (bb[2] - bb[0]) * (1.0 - 1.0 / zX),
5308                 dY = (bb[1] - bb[3]) * (1.0 - 1.0 / zY),
5309                 lr = 0.5,
5310                 tr = 0.5,
5311                 ma = Type.evaluate(this.attr.zoom.max),
5312                 mi =  Type.evaluate(this.attr.zoom.eps) || Type.evaluate(this.attr.zoom.min) || 0.001; // this.attr.zoom.eps is deprecated
5313 
5314             if (
5315                 (this.zoomX > ma && zX > 1.0) ||
5316                 (this.zoomY > ma && zY > 1.0) ||
5317                 (this.zoomX < mi && zX < 1.0) || // zoomIn is used for all zooms on touch devices
5318                 (this.zoomY < mi && zY < 1.0)
5319             ) {
5320                 return this;
5321             }
5322 
5323             if (Type.isNumber(x) && Type.isNumber(y)) {
5324                 lr = (x - bb[0]) / (bb[2] - bb[0]);
5325                 tr = (bb[1] - y) / (bb[1] - bb[3]);
5326             }
5327 
5328             this.setBoundingBox(
5329                 [
5330                     bb[0] + dX * lr,
5331                     bb[1] - dY * tr,
5332                     bb[2] - dX * (1 - lr),
5333                     bb[3] + dY * (1 - tr)
5334                 ],
5335                 this.keepaspectratio,
5336                 'update'
5337             );
5338             return this.applyZoom();
5339         },
5340 
5341         /**
5342          * Zooms out of the board by the factors board.attr.zoom.factorX and board.attr.zoom.factorY and applies the zoom.
5343          * The zoom operation is centered at x, y.
5344          *
5345          * @param {Number} [x]
5346          * @param {Number} [y]
5347          * @returns {JXG.Board} Reference to the board
5348          */
5349         zoomOut: function (x, y) {
5350             var bb = this.getBoundingBox(),
5351                 zX = Type.evaluate(this.attr.zoom.factorx),
5352                 zY = Type.evaluate(this.attr.zoom.factory),
5353                 dX = (bb[2] - bb[0]) * (1.0 - zX),
5354                 dY = (bb[1] - bb[3]) * (1.0 - zY),
5355                 lr = 0.5,
5356                 tr = 0.5,
5357                 mi = Type.evaluate(this.attr.zoom.eps) || Type.evaluate(this.attr.zoom.min) || 0.001; // this.attr.zoom.eps is deprecated
5358 
5359             if (this.zoomX < mi || this.zoomY < mi) {
5360                 return this;
5361             }
5362 
5363             if (Type.isNumber(x) && Type.isNumber(y)) {
5364                 lr = (x - bb[0]) / (bb[2] - bb[0]);
5365                 tr = (bb[1] - y) / (bb[1] - bb[3]);
5366             }
5367 
5368             this.setBoundingBox(
5369                 [
5370                     bb[0] + dX * lr,
5371                     bb[1] - dY * tr,
5372                     bb[2] - dX * (1 - lr),
5373                     bb[3] + dY * (1 - tr)
5374                 ],
5375                 this.keepaspectratio,
5376                 'update'
5377             );
5378 
5379             return this.applyZoom();
5380         },
5381 
5382         /**
5383          * Reset the zoom level to the original zoom level from initBoard();
5384          * Additionally, if the board as been initialized with a boundingBox (which is the default),
5385          * restore the viewport to the original viewport during initialization. Otherwise,
5386          * (i.e. if the board as been initialized with unitX/Y and originX/Y),
5387          * just set the zoom level to 100%.
5388          *
5389          * @returns {JXG.Board} Reference to the board
5390          */
5391         zoom100: function () {
5392             var bb, dX, dY;
5393 
5394             if (Type.exists(this.attr.boundingbox)) {
5395                 this.setBoundingBox(this.attr.boundingbox, this.keepaspectratio, 'reset');
5396             } else {
5397                 // Board has been set up with unitX/Y and originX/Y
5398                 bb = this.getBoundingBox();
5399                 dX = (bb[2] - bb[0]) * (1.0 - this.zoomX) * 0.5;
5400                 dY = (bb[1] - bb[3]) * (1.0 - this.zoomY) * 0.5;
5401                 this.setBoundingBox(
5402                     [bb[0] + dX, bb[1] - dY, bb[2] - dX, bb[3] + dY],
5403                     this.keepaspectratio,
5404                     'reset'
5405                 );
5406             }
5407             return this.applyZoom();
5408         },
5409 
5410         /**
5411          * Zooms the board so every visible point is shown. Keeps aspect ratio.
5412          * @returns {JXG.Board} Reference to the board
5413          */
5414         zoomAllPoints: function () {
5415             var el,
5416                 border,
5417                 borderX,
5418                 borderY,
5419                 pEl,
5420                 minX = 0,
5421                 maxX = 0,
5422                 minY = 0,
5423                 maxY = 0,
5424                 len = this.objectsList.length;
5425 
5426             for (el = 0; el < len; el++) {
5427                 pEl = this.objectsList[el];
5428 
5429                 if (Type.isPoint(pEl) && pEl.visPropCalc.visible) {
5430                     if (pEl.coords.usrCoords[1] < minX) {
5431                         minX = pEl.coords.usrCoords[1];
5432                     } else if (pEl.coords.usrCoords[1] > maxX) {
5433                         maxX = pEl.coords.usrCoords[1];
5434                     }
5435                     if (pEl.coords.usrCoords[2] > maxY) {
5436                         maxY = pEl.coords.usrCoords[2];
5437                     } else if (pEl.coords.usrCoords[2] < minY) {
5438                         minY = pEl.coords.usrCoords[2];
5439                     }
5440                 }
5441             }
5442 
5443             border = 50;
5444             borderX = border / this.unitX;
5445             borderY = border / this.unitY;
5446 
5447             this.setBoundingBox(
5448                 [minX - borderX, maxY + borderY, maxX + borderX, minY - borderY],
5449                 this.keepaspectratio,
5450                 'update'
5451             );
5452 
5453             return this.applyZoom();
5454         },
5455 
5456         /**
5457          * Reset the bounding box and the zoom level to 100% such that a given set of elements is
5458          * within the board's viewport.
5459          * @param {Array} elements A set of elements given by id, reference, or name.
5460          * @returns {JXG.Board} Reference to the board.
5461          */
5462         zoomElements: function (elements) {
5463             var i, e,
5464                 box,
5465                 newBBox = [Infinity, -Infinity, -Infinity, Infinity],
5466                 cx, cy,
5467                 dx, dy,
5468                 d;
5469 
5470             if (!Type.isArray(elements) || elements.length === 0) {
5471                 return this;
5472             }
5473 
5474             for (i = 0; i < elements.length; i++) {
5475                 e = this.select(elements[i]);
5476 
5477                 box = e.bounds();
5478                 if (Type.isArray(box)) {
5479                     if (box[0] < newBBox[0]) {
5480                         newBBox[0] = box[0];
5481                     }
5482                     if (box[1] > newBBox[1]) {
5483                         newBBox[1] = box[1];
5484                     }
5485                     if (box[2] > newBBox[2]) {
5486                         newBBox[2] = box[2];
5487                     }
5488                     if (box[3] < newBBox[3]) {
5489                         newBBox[3] = box[3];
5490                     }
5491                 }
5492             }
5493 
5494             if (Type.isArray(newBBox)) {
5495                 cx = 0.5 * (newBBox[0] + newBBox[2]);
5496                 cy = 0.5 * (newBBox[1] + newBBox[3]);
5497                 dx = 1.5 * (newBBox[2] - newBBox[0]) * 0.5;
5498                 dy = 1.5 * (newBBox[1] - newBBox[3]) * 0.5;
5499                 d = Math.max(dx, dy);
5500                 this.setBoundingBox(
5501                     [cx - d, cy + d, cx + d, cy - d],
5502                     this.keepaspectratio,
5503                     'update'
5504                 );
5505             }
5506 
5507             return this;
5508         },
5509 
5510         /**
5511          * Sets the zoom level to <tt>fX</tt> resp <tt>fY</tt>.
5512          * @param {Number} fX
5513          * @param {Number} fY
5514          * @returns {JXG.Board} Reference to the board.
5515          */
5516         setZoom: function (fX, fY) {
5517             var oX = this.attr.zoom.factorx,
5518                 oY = this.attr.zoom.factory;
5519 
5520             this.attr.zoom.factorx = fX / this.zoomX;
5521             this.attr.zoom.factory = fY / this.zoomY;
5522 
5523             this.zoomIn();
5524 
5525             this.attr.zoom.factorx = oX;
5526             this.attr.zoom.factory = oY;
5527 
5528             return this;
5529         },
5530 
5531         /**
5532          * Inner, recursive method of removeObject.
5533          *
5534          * @param {JXG.GeometryElement|Array} object The object to remove or array of objects to be removed.
5535          * The element(s) is/are given by name, id or a reference.
5536          * @param {Boolean} [saveMethod=false] If saveMethod=true, the algorithm runs through all elements
5537          * and tests if the element to be deleted is a child element. If this is the case, it will be
5538          * removed from the list of child elements. If saveMethod=false (default), the element
5539          * is removed from the lists of child elements of all its ancestors.
5540          * The latter should be much faster.
5541          * @returns {JXG.Board} Reference to the board
5542          * @private
5543          */
5544         _removeObj: function (object, saveMethod) {
5545             var el, o, i;
5546 
5547             if (Type.isArray(object)) {
5548                 for (i = 0; i < object.length; i++) {
5549                     this._removeObj(object[i], saveMethod);
5550                 }
5551 
5552                 return this;
5553             }
5554 
5555             object = this.select(object);
5556 
5557             // If the object which is about to be removed is unknown or a string, do nothing.
5558             // it is a string if a string was given and could not be resolved to an element.
5559             if (!Type.exists(object) || Type.isString(object)) {
5560                 return this;
5561             }
5562 
5563             try {
5564                 // remove all children.
5565                 for (el in object.childElements) {
5566                     if (object.childElements.hasOwnProperty(el)) {
5567                         object.childElements[el].board._removeObj(object.childElements[el]);
5568                     }
5569                 }
5570 
5571                 // Remove all children in elements like turtle
5572                 for (el in object.objects) {
5573                     if (object.objects.hasOwnProperty(el)) {
5574                         object.objects[el].board._removeObj(object.objects[el]);
5575                     }
5576                 }
5577 
5578                 // Remove the element from the childElement list and the descendant list of all elements.
5579                 if (saveMethod) {
5580                     // Running through all objects has quadratic complexity if many objects are deleted.
5581                     for (el in this.objects) {
5582                         if (this.objects.hasOwnProperty(el)) {
5583                             if (
5584                                 Type.exists(this.objects[el].childElements) &&
5585                                 Type.exists(
5586                                     this.objects[el].childElements.hasOwnProperty(object.id)
5587                                 )
5588                             ) {
5589                                 delete this.objects[el].childElements[object.id];
5590                                 delete this.objects[el].descendants[object.id];
5591                             }
5592                         }
5593                     }
5594                 } else if (Type.exists(object.ancestors)) {
5595                     // Running through the ancestors should be much more efficient.
5596                     for (el in object.ancestors) {
5597                         if (object.ancestors.hasOwnProperty(el)) {
5598                             if (
5599                                 Type.exists(object.ancestors[el].childElements) &&
5600                                 Type.exists(
5601                                     object.ancestors[el].childElements.hasOwnProperty(object.id)
5602                                 )
5603                             ) {
5604                                 delete object.ancestors[el].childElements[object.id];
5605                                 delete object.ancestors[el].descendants[object.id];
5606                             }
5607                         }
5608                     }
5609                 }
5610 
5611                 // remove the object itself from our control structures
5612                 if (object._pos > -1) {
5613                     this.objectsList.splice(object._pos, 1);
5614                     // Quadratic complexity for reindexing the positions:
5615                     for (i = object._pos; i < this.objectsList.length; i++) {
5616                         o = this.objectsList[i];
5617                         if (o._pos > -1) {
5618                             o._pos--;
5619                         }
5620                     }
5621                 } else if (object.type !== Const.OBJECT_TYPE_TURTLE) {
5622                     JXG.debug(
5623                         'Board.removeObject: object ' + object.id + ' not found in list.'
5624                     );
5625                 }
5626 
5627                 delete this.objects[object.id];
5628                 delete this.elementsByName[object.name];
5629 
5630                 if (object.visProp && object.evalVisProp('trace')) {
5631                     object.clearTrace();
5632                 }
5633 
5634                 // the object deletion itself is handled by the object.
5635                 if (Type.exists(object.remove)) {
5636                     object.remove();
5637                 }
5638             } catch (e) {
5639                 JXG.debug(object.id + ': Could not be removed: ' + e);
5640             }
5641 
5642             return this;
5643         },
5644 
5645         /**
5646          * Removes object from board and from the renderer object.
5647          * <p>
5648          * <b>Performance hints:</b> It is recommended to use the JSXGraph object's id.
5649          * If many elements are removed, it is best to either
5650          * <ul>
5651          *   <li> remove the whole array if the elements are contained in an array instead
5652          *    of looping through the array OR
5653          *   <li> call <tt>board.suspendUpdate()</tt>
5654          * before looping through the elements to be removed and call
5655          * <tt>board.unsuspendUpdate()</tt> after the loop. Further, it is advisable to loop
5656          * in reverse order, i.e. remove the object in reverse order of their creation time.
5657          * </ul>
5658          * @param {JXG.GeometryElement|Array} object The object to remove or array of objects to be removed.
5659          * The element(s) is/are given by name, id or a reference.
5660          * @param {Boolean} saveMethod If true, the algorithm runs through all elements
5661          * and tests if the element to be deleted is a child element. If yes, it will be
5662          * removed from the list of child elements. If false (default), the element
5663          * is removed from the lists of child elements of all its ancestors.
5664          * This should be much faster.
5665          * @returns {JXG.Board} Reference to the board
5666          */
5667         removeObject: function (object, saveMethod) {
5668             var i;
5669 
5670             this.renderer.suspendRedraw(this);
5671             if (Type.isArray(object)) {
5672                 for (i = 0; i < object.length; i++) {
5673                     this._removeObj(object[i], saveMethod);
5674                 }
5675             } else {
5676                 this._removeObj(object, saveMethod);
5677             }
5678             this.renderer.unsuspendRedraw();
5679 
5680             this.update();
5681             return this;
5682         },
5683 
5684         /**
5685          * Removes the ancestors of an object an the object itself from board and renderer.
5686          * @param {JXG.GeometryElement} object The object to remove.
5687          * @returns {JXG.Board} Reference to the board
5688          */
5689         removeAncestors: function (object) {
5690             var anc;
5691 
5692             for (anc in object.ancestors) {
5693                 if (object.ancestors.hasOwnProperty(anc)) {
5694                     this.removeAncestors(object.ancestors[anc]);
5695                 }
5696             }
5697 
5698             this.removeObject(object);
5699 
5700             return this;
5701         },
5702 
5703         /**
5704          * Initialize some objects which are contained in every GEONExT construction by default,
5705          * but are not contained in the gxt files.
5706          * @returns {JXG.Board} Reference to the board
5707          */
5708         initGeonextBoard: function () {
5709             var p1, p2, p3;
5710 
5711             p1 = this.create('point', [0, 0], {
5712                 id: this.id + 'g00e0',
5713                 name: 'Ursprung',
5714                 withLabel: false,
5715                 visible: false,
5716                 fixed: true
5717             });
5718 
5719             p2 = this.create('point', [1, 0], {
5720                 id: this.id + 'gX0e0',
5721                 name: 'Punkt_1_0',
5722                 withLabel: false,
5723                 visible: false,
5724                 fixed: true
5725             });
5726 
5727             p3 = this.create('point', [0, 1], {
5728                 id: this.id + 'gY0e0',
5729                 name: 'Punkt_0_1',
5730                 withLabel: false,
5731                 visible: false,
5732                 fixed: true
5733             });
5734 
5735             this.create('line', [p1, p2], {
5736                 id: this.id + 'gXLe0',
5737                 name: 'X-Achse',
5738                 withLabel: false,
5739                 visible: false
5740             });
5741 
5742             this.create('line', [p1, p3], {
5743                 id: this.id + 'gYLe0',
5744                 name: 'Y-Achse',
5745                 withLabel: false,
5746                 visible: false
5747             });
5748 
5749             return this;
5750         },
5751 
5752         /**
5753          * Change the height and width of the board's container.
5754          * After doing so, {@link JXG.JSXGraph.setBoundingBox} is called using
5755          * the actual size of the bounding box and the actual value of keepaspectratio.
5756          * If setBoundingbox() should not be called automatically,
5757          * call resizeContainer with dontSetBoundingBox == true.
5758          * @param {Number} canvasWidth New width of the container.
5759          * @param {Number} canvasHeight New height of the container.
5760          * @param {Boolean} [dontset=false] If true do not set the CSS width and height of the DOM element.
5761          * @param {Boolean} [dontSetBoundingBox=false] If true do not call setBoundingBox(), but keep view centered around original visible center.
5762          * @returns {JXG.Board} Reference to the board
5763          */
5764         resizeContainer: function (canvasWidth, canvasHeight, dontset, dontSetBoundingBox) {
5765             var box,
5766                 oldWidth, oldHeight,
5767                 oX, oY;
5768 
5769             oldWidth = this.canvasWidth;
5770             oldHeight = this.canvasHeight;
5771 
5772             if (!dontSetBoundingBox) {
5773                 box = this.getBoundingBox();    // This is the actual bounding box.
5774             }
5775 
5776             // this.canvasWidth = Math.max(parseFloat(canvasWidth), Mat.eps);
5777             // this.canvasHeight = Math.max(parseFloat(canvasHeight), Mat.eps);
5778             this.canvasWidth = parseFloat(canvasWidth);
5779             this.canvasHeight = parseFloat(canvasHeight);
5780 
5781             if (!dontset) {
5782                 this.containerObj.style.width = this.canvasWidth + 'px';
5783                 this.containerObj.style.height = this.canvasHeight + 'px';
5784             }
5785             this.renderer.resize(this.canvasWidth, this.canvasHeight);
5786 
5787             if (!dontSetBoundingBox) {
5788                 this.setBoundingBox(box, this.keepaspectratio, 'keep');
5789             } else {
5790                 oX = (this.canvasWidth - oldWidth) * 0.5;
5791                 oY = (this.canvasHeight - oldHeight) * 0.5;
5792 
5793                 this.moveOrigin(
5794                     this.origin.scrCoords[1] + oX,
5795                     this.origin.scrCoords[2] + oY
5796                 );
5797             }
5798 
5799             return this;
5800         },
5801 
5802         /**
5803          * Lists the dependencies graph in a new HTML-window.
5804          * @returns {JXG.Board} Reference to the board
5805          */
5806         showDependencies: function () {
5807             var el, t, c, f, i;
5808 
5809             t = '<p>\n';
5810             for (el in this.objects) {
5811                 if (this.objects.hasOwnProperty(el)) {
5812                     i = 0;
5813                     for (c in this.objects[el].childElements) {
5814                         if (this.objects[el].childElements.hasOwnProperty(c)) {
5815                             i += 1;
5816                         }
5817                     }
5818                     if (i >= 0) {
5819                         t += '<strong>' + this.objects[el].id + ':<' + '/strong> ';
5820                     }
5821 
5822                     for (c in this.objects[el].childElements) {
5823                         if (this.objects[el].childElements.hasOwnProperty(c)) {
5824                             t +=
5825                                 this.objects[el].childElements[c].id +
5826                                 '(' +
5827                                 this.objects[el].childElements[c].name +
5828                                 ')' +
5829                                 ', ';
5830                         }
5831                     }
5832                     t += '<p>\n';
5833                 }
5834             }
5835             t += '<' + '/p>\n';
5836             f = window.open();
5837             f.document.open();
5838             f.document.write(t);
5839             f.document.close();
5840             return this;
5841         },
5842 
5843         /**
5844          * Lists the XML code of the construction in a new HTML-window.
5845          * @returns {JXG.Board} Reference to the board
5846          */
5847         showXML: function () {
5848             var f = window.open('');
5849             f.document.open();
5850             f.document.write('<pre>' + Type.escapeHTML(this.xmlString) + '<' + '/pre>');
5851             f.document.close();
5852             return this;
5853         },
5854 
5855         /**
5856          * Sets for all objects the needsUpdate flag to 'true'.
5857          * @param{JXG.GeometryElement} [drag=undefined] Optional element that is dragged.
5858          * @returns {JXG.Board} Reference to the board
5859          */
5860         prepareUpdate: function (drag) {
5861             var el, i,
5862                 pEl,
5863                 len = this.objectsList.length;
5864 
5865             /*
5866             if (this.attr.updatetype === 'hierarchical') {
5867                 return this;
5868             }
5869             */
5870 
5871             for (el = 0; el < len; el++) {
5872                 pEl = this.objectsList[el];
5873                 if (this._change3DView ||
5874                     (Type.exists(drag) && drag.elType === 'view3d_slider')
5875                 ) {
5876                     // The 3D view has changed. No elements are recomputed,
5877                     // only 3D elements are projected to the new view.
5878                     pEl.needsUpdate =
5879                         pEl.visProp.element3d ||
5880                         pEl.elType === 'view3d' ||
5881                         pEl.elType === 'view3d_slider' ||
5882                         this.needsFullUpdate;
5883 
5884                     // Special case sphere3d in central projection:
5885                     // We have to update the defining points of the ellipse
5886                     if (pEl.visProp.element3d &&
5887                         pEl.visProp.element3d.type === Const.OBJECT_TYPE_SPHERE3D
5888                         ) {
5889                         for (i = 0; i < pEl.parents.length; i++) {
5890                             this.objects[pEl.parents[i]].needsUpdate = true;
5891                         }
5892                     }
5893                 } else {
5894                     pEl.needsUpdate = pEl.needsRegularUpdate || this.needsFullUpdate;
5895                 }
5896             }
5897 
5898             for (el in this.groups) {
5899                 if (this.groups.hasOwnProperty(el)) {
5900                     pEl = this.groups[el];
5901                     pEl.needsUpdate = pEl.needsRegularUpdate || this.needsFullUpdate;
5902                 }
5903             }
5904 
5905             return this;
5906         },
5907 
5908         /**
5909          * Runs through all elements and calls their update() method.
5910          * @param {JXG.GeometryElement} drag Element that caused the update.
5911          * @returns {JXG.Board} Reference to the board
5912          */
5913         updateElements: function (drag) {
5914             var el, pEl;
5915             //var childId, i = 0;
5916 
5917             drag = this.select(drag);
5918 
5919             /*
5920             if (Type.exists(drag)) {
5921                 for (el = 0; el < this.objectsList.length; el++) {
5922                     pEl = this.objectsList[el];
5923                     if (pEl.id === drag.id) {
5924                         i = el;
5925                         break;
5926                     }
5927                 }
5928             }
5929             */
5930             for (el = 0; el < this.objectsList.length; el++) {
5931                 pEl = this.objectsList[el];
5932                 if (this.needsFullUpdate && pEl.elementClass === Const.OBJECT_CLASS_TEXT) {
5933                     pEl.updateSize();
5934                 }
5935 
5936                 // For updates of an element we distinguish if the dragged element is updated or
5937                 // other elements are updated.
5938                 // The difference lies in the treatment of gliders and points based on transformations.
5939                 pEl.update(!Type.exists(drag) || pEl.id !== drag.id).updateVisibility();
5940             }
5941 
5942             // update groups last
5943             for (el in this.groups) {
5944                 if (this.groups.hasOwnProperty(el)) {
5945                     this.groups[el].update(drag);
5946                 }
5947             }
5948 
5949             return this;
5950         },
5951 
5952         /**
5953          * Runs through all elements and calls their update() method.
5954          * @returns {JXG.Board} Reference to the board
5955          */
5956         updateRenderer: function () {
5957             var el,
5958                 len = this.objectsList.length,
5959                 autoPositionLabelList = [],
5960                 currentIndex, randomIndex;
5961 
5962             if (!this.renderer) {
5963                 return;
5964             }
5965 
5966             /*
5967             objs = this.objectsList.slice(0);
5968             objs.sort(function (a, b) {
5969                 if (a.visProp.layer < b.visProp.layer) {
5970                     return -1;
5971                 } else if (a.visProp.layer === b.visProp.layer) {
5972                     return b.lastDragTime.getTime() - a.lastDragTime.getTime();
5973                 } else {
5974                     return 1;
5975                 }
5976             });
5977             */
5978 
5979             if (this.renderer.type === 'canvas') {
5980                 this.updateRendererCanvas();
5981             } else {
5982                 for (el = 0; el < len; el++) {
5983                     if (this.objectsList[el].visProp.islabel && this.objectsList[el].visProp.autoposition) {
5984                         autoPositionLabelList.push(el);
5985                     } else {
5986                         this.objectsList[el].updateRenderer();
5987                     }
5988                 }
5989 
5990                 currentIndex = autoPositionLabelList.length;
5991 
5992                 // Randomize the order of the labels
5993                 while (currentIndex !== 0) {
5994                     randomIndex = Math.floor(Math.random() * currentIndex);
5995                     currentIndex--;
5996                     [autoPositionLabelList[currentIndex], autoPositionLabelList[randomIndex]] = [autoPositionLabelList[randomIndex], autoPositionLabelList[currentIndex]];
5997                 }
5998 
5999                 for (el = 0; el < autoPositionLabelList.length; el++) {
6000                     this.objectsList[autoPositionLabelList[el]].updateRenderer();
6001                 }
6002                 /*
6003                 for (el = autoPositionLabelList.length - 1; el >= 0; el--) {
6004                     this.objectsList[autoPositionLabelList[el]].updateRenderer();
6005                 }
6006                 */
6007             }
6008             return this;
6009         },
6010 
6011         /**
6012          * Runs through all elements and calls their update() method.
6013          * This is a special version for the CanvasRenderer.
6014          * Here, we have to do our own layer handling.
6015          * @returns {JXG.Board} Reference to the board
6016          */
6017         updateRendererCanvas: function () {
6018             var el, pEl,
6019                 olen = this.objectsList.length,
6020                 // i, minim, lay,
6021                 // layers = this.options.layer,
6022                 // len = this.options.layer.numlayers,
6023                 // last = Number.NEGATIVE_INFINITY.toExponential,
6024                 depth_order_layers = [],
6025                 objects_sorted,
6026 
6027                 /**
6028                  * Function to sort elements for depth ordering in canvas renderer.
6029                  * Only relevant for elements having a zIndex.
6030                  * Sort the elements for the canvas rendering according to
6031                  * their layer, _pos, depthOrder (with this priority).
6032                  * @param {JXG.GeometryObject} a
6033                  * @param {JXG.GeometryObject} b
6034                  * @returns Number
6035                  * @private
6036                  */
6037                 _compareDepth = function(a, b) {
6038                     if (a.visProp.layer !== b.visProp.layer) {
6039                         // For elements in different layers, the element in the
6040                         // higher layer is in front.
6041                         return a.visProp.layer - b.visProp.layer;
6042                     }
6043 
6044                     // From here on, both objects are in the same layer.
6045 
6046                     if (depth_order_layers.indexOf(a.visProp.layer) === -1) {
6047                         // The layer is not depth ordered.
6048                         return a._pos - b._pos;
6049                     }
6050 
6051                     // From here on, both objects are in the same layer
6052                     // and the layer is depth ordered.
6053 
6054                     // The objects are in the same layer and the layer is depth ordered
6055                     // But neither element is the 2D element of a 3D element.
6056                     if (!a.visProp.element3d && !b.visProp.element3d) {
6057                         return a._pos - b._pos;
6058                     }
6059 
6060                     if (a.visProp.element3d && !b.visProp.element3d) {
6061                         return -1;
6062                     }
6063 
6064                     if (!a.visProp.element3d && b.visProp.element3d) {
6065                         return 1;
6066                     }
6067 
6068                     // Finqally, both elements are 2D elements of a 3D element.
6069                     return a.visProp.element3d.zIndex - b.visProp.element3d.zIndex;
6070                 };
6071 
6072             // Only one view3d element is supported. Get the depth order layers and
6073             // update the zIndices of the 3D elements.
6074             for (el = 0; el < olen; el++) {
6075                 pEl = this.objectsList[el];
6076                 if (pEl.elType === 'view3d' &&
6077                     pEl.evalVisProp('depthorder.enabled')
6078                 ) {
6079                     depth_order_layers = pEl.evalVisProp('depthorder.layers');
6080                     pEl.updateRenderer();
6081                     break;
6082                 }
6083             }
6084 
6085             // objects_sorted = this.objectsList.toSorted(_compareDepth);
6086 
6087             // 3D elements are not rendered, but their subelements element2D
6088             objects_sorted = this.objectsList.filter(function(e) { return !e.is3D; }).toSorted(_compareDepth);
6089             olen = objects_sorted.length;
6090             for (el = 0; el < olen; el++) {
6091                 if (
6092                     objects_sorted[el].visPropCalc.visible &&
6093                     objects_sorted[el].type !== Const.OBJECT_TYPE_FACE3D // For these, updateRenderer is triggered in polyhedron3d.updateRenderer
6094                 ) {
6095                     objects_sorted[el].prepareUpdate().updateRenderer();
6096                 }
6097             }
6098 
6099             return this;
6100         },
6101 
6102         /**
6103          * Please use {@link JXG.Board.on} instead.
6104          * @param {Function} hook A function to be called by the board after an update occurred.
6105          * @param {String} [m='update'] When the hook is to be called. Possible values are <i>mouseup</i>, <i>mousedown</i> and <i>update</i>.
6106          * @param {Object} [context=board] Determines the execution context the hook is called. This parameter is optional, default is the
6107          * board object the hook is attached to.
6108          * @returns {Number} Id of the hook, required to remove the hook from the board.
6109          * @deprecated
6110          */
6111         addHook: function (hook, m, context) {
6112             JXG.deprecated('Board.addHook()', 'Board.on()');
6113             m = Type.def(m, 'update');
6114 
6115             context = Type.def(context, this);
6116 
6117             this.hooks.push([m, hook]);
6118             this.on(m, hook, context);
6119 
6120             return this.hooks.length - 1;
6121         },
6122 
6123         /**
6124          * Alias of {@link JXG.Board.on}.
6125          */
6126         addEvent: JXG.shortcut(JXG.Board.prototype, 'on'),
6127 
6128         /**
6129          * Please use {@link JXG.Board.off} instead.
6130          * @param {Number|function} id The number you got when you added the hook or a reference to the event handler.
6131          * @returns {JXG.Board} Reference to the board
6132          * @deprecated
6133          */
6134         removeHook: function (id) {
6135             JXG.deprecated('Board.removeHook()', 'Board.off()');
6136             if (this.hooks[id]) {
6137                 this.off(this.hooks[id][0], this.hooks[id][1]);
6138                 this.hooks[id] = null;
6139             }
6140 
6141             return this;
6142         },
6143 
6144         /**
6145          * Alias of {@link JXG.Board.off}.
6146          */
6147         removeEvent: JXG.shortcut(JXG.Board.prototype, 'off'),
6148 
6149         /**
6150          * Runs through all hooked functions and calls them.
6151          * @returns {JXG.Board} Reference to the board
6152          * @deprecated
6153          */
6154         updateHooks: function (m) {
6155             var arg = Array.prototype.slice.call(arguments, 0);
6156 
6157             JXG.deprecated('Board.updateHooks()', 'Board.triggerEventHandlers()');
6158 
6159             arg[0] = Type.def(arg[0], 'update');
6160             this.triggerEventHandlers([arg[0]], arguments);
6161 
6162             return this;
6163         },
6164 
6165         /**
6166          * Adds a dependent board to this board.
6167          * @param {JXG.Board} board A reference to board which will be updated after an update of this board occurred.
6168          * @returns {JXG.Board} Reference to the board
6169          */
6170         addChild: function (board) {
6171             if (Type.exists(board) && Type.exists(board.containerObj)) {
6172                 this.dependentBoards.push(board);
6173                 this.update();
6174             }
6175             return this;
6176         },
6177 
6178         /**
6179          * Deletes a board from the list of dependent boards.
6180          * @param {JXG.Board} board Reference to the board which will be removed.
6181          * @returns {JXG.Board} Reference to the board
6182          */
6183         removeChild: function (board) {
6184             var i;
6185 
6186             for (i = this.dependentBoards.length - 1; i >= 0; i--) {
6187                 if (this.dependentBoards[i] === board) {
6188                     this.dependentBoards.splice(i, 1);
6189                 }
6190             }
6191             return this;
6192         },
6193 
6194         /**
6195          * Runs through most elements and calls their update() method and update the conditions.
6196          * @param {JXG.GeometryElement} [drag] Element that caused the update.
6197          * @returns {JXG.Board} Reference to the board
6198          */
6199         update: function (drag) {
6200             var i, len, b, insert, storeActiveEl;
6201 
6202             if (this.inUpdate || this.isSuspendedUpdate) {
6203                 return this;
6204             }
6205             this.inUpdate = true;
6206 
6207             if (
6208                 this.attr.minimizereflow === 'all' &&
6209                 this.containerObj &&
6210                 this.renderer.type !== 'vml'
6211             ) {
6212                 storeActiveEl = this.document.activeElement; // Store focus element
6213                 insert = this.renderer.removeToInsertLater(this.containerObj);
6214             }
6215 
6216             if (this.attr.minimizereflow === 'svg' && this.renderer.type === 'svg') {
6217                 storeActiveEl = this.document.activeElement;
6218                 insert = this.renderer.removeToInsertLater(this.renderer.svgRoot);
6219             }
6220 
6221             this.prepareUpdate(drag).updateElements(drag).updateConditions();
6222 
6223             this.renderer.suspendRedraw(this);
6224             this.updateRenderer();
6225             this.renderer.unsuspendRedraw();
6226             this.triggerEventHandlers(['update'], []);
6227 
6228             if (insert) {
6229                 insert();
6230                 storeActiveEl.focus(); // Restore focus element
6231             }
6232 
6233             // To resolve dependencies between boards
6234             // for (var board in JXG.boards) {
6235             len = this.dependentBoards.length;
6236             for (i = 0; i < len; i++) {
6237                 b = this.dependentBoards[i];
6238                 if (Type.exists(b) && b !== this) {
6239                     b.updateQuality = this.updateQuality;
6240                     b.prepareUpdate().updateElements().updateConditions();
6241                     b.renderer.suspendRedraw(this);
6242                     b.updateRenderer();
6243                     b.renderer.unsuspendRedraw();
6244                     b.triggerEventHandlers(['update'], []);
6245                 }
6246             }
6247 
6248             this.inUpdate = false;
6249             return this;
6250         },
6251 
6252         /**
6253          * Runs through all elements and calls their update() method and update the conditions.
6254          * This is necessary after zooming and changing the bounding box.
6255          * @returns {JXG.Board} Reference to the board
6256          */
6257         fullUpdate: function () {
6258             this.needsFullUpdate = true;
6259             this.update();
6260             this.needsFullUpdate = false;
6261             return this;
6262         },
6263 
6264         /**
6265          * Adds a grid to the board according to the settings given in board.options.
6266          * @returns {JXG.Board} Reference to the board.
6267          */
6268         addGrid: function () {
6269             this.create('grid', []);
6270 
6271             return this;
6272         },
6273 
6274         /**
6275          * Removes all grids assigned to this board. Warning: This method also removes all objects depending on one or
6276          * more of the grids.
6277          * @returns {JXG.Board} Reference to the board object.
6278          */
6279         removeGrids: function () {
6280             var i;
6281 
6282             for (i = 0; i < this.grids.length; i++) {
6283                 this.removeObject(this.grids[i]);
6284             }
6285 
6286             this.grids.length = 0;
6287             this.update(); // required for canvas renderer
6288 
6289             return this;
6290         },
6291 
6292         /**
6293          * Creates a new geometric element of type elementType.
6294          * @param {String} elementType Type of the element to be constructed given as a string e.g. 'point' or 'circle'.
6295          * @param {Array} parents Array of parent elements needed to construct the element e.g. coordinates for a point or two
6296          * points to construct a line. This highly depends on the elementType that is constructed. See the corresponding JXG.create*
6297          * methods for a list of possible parameters.
6298          * @param {Object} [attributes] An object containing the attributes to be set. This also depends on the elementType.
6299          * Common attributes are name, visible, strokeColor.
6300          * @returns {Object} Reference to the created element. This is usually a GeometryElement, but can be an array containing
6301          * two or more elements.
6302          */
6303         create: function (elementType, parents, attributes) {
6304             var el, i;
6305 
6306             elementType = elementType.toLowerCase();
6307 
6308             if (!Type.exists(parents)) {
6309                 parents = [];
6310             }
6311 
6312             if (!Type.exists(attributes)) {
6313                 attributes = {};
6314             }
6315 
6316             for (i = 0; i < parents.length; i++) {
6317                 if (
6318                     Type.isString(parents[i]) &&
6319                     !(elementType === 'text' && i === 2) &&
6320                     !(elementType === 'solidofrevolution3d' && i === 2) &&
6321                     !(elementType === 'text3d' && (i === 2 || i === 4)) &&
6322                     !(
6323                         (elementType === 'input' ||
6324                             elementType === 'checkbox' ||
6325                             elementType === 'button') &&
6326                         (i === 2 || i === 3)
6327                     ) &&
6328                     !(elementType === 'curve' /*&& i > 0*/) && // Allow curve plots with jessiecode, parents[0] is the
6329                                                                // variable name
6330                     !(elementType === 'functiongraph') && // Prevent problems with function terms like 'x', 'y'
6331                     !(elementType === 'implicitcurve')
6332                 ) {
6333                     if (i > 0 && parents[0].elType === 'view3d') {
6334                         // 3D elements are based on 3D elements, only
6335                         parents[i] = parents[0].select(parents[i]);
6336                     } else {
6337                         parents[i] = this.select(parents[i]);
6338                     }
6339                 }
6340             }
6341 
6342             if (Type.isFunction(JXG.elements[elementType])) {
6343                 el = JXG.elements[elementType](this, parents, attributes);
6344             } else {
6345                 throw new Error('JSXGraph: create: Unknown element type given: ' + elementType);
6346             }
6347 
6348             if (!Type.exists(el)) {
6349                 JXG.debug('JSXGraph: create: failure creating ' + elementType);
6350                 return el;
6351             }
6352 
6353             if (el.prepareUpdate && el.update && el.updateRenderer) {
6354                 el.fullUpdate();
6355             }
6356             return el;
6357         },
6358 
6359         /**
6360          * Deprecated name for {@link JXG.Board.create}.
6361          * @deprecated
6362          */
6363         createElement: function () {
6364             JXG.deprecated('Board.createElement()', 'Board.create()');
6365             return this.create.apply(this, arguments);
6366         },
6367 
6368         /**
6369          * Delete the elements drawn as part of a trace of an element.
6370          * @returns {JXG.Board} Reference to the board
6371          */
6372         clearTraces: function () {
6373             var el;
6374 
6375             for (el = 0; el < this.objectsList.length; el++) {
6376                 this.objectsList[el].clearTrace();
6377             }
6378 
6379             this.numTraces = 0;
6380             return this;
6381         },
6382 
6383         /**
6384          * Stop updates of the board.
6385          * @returns {JXG.Board} Reference to the board
6386          */
6387         suspendUpdate: function () {
6388             if (!this.inUpdate) {
6389                 this.isSuspendedUpdate = true;
6390             }
6391             return this;
6392         },
6393 
6394         /**
6395          * Enable updates of the board.
6396          * @returns {JXG.Board} Reference to the board
6397          */
6398         unsuspendUpdate: function () {
6399             if (this.isSuspendedUpdate) {
6400                 this.isSuspendedUpdate = false;
6401                 this.fullUpdate();
6402             }
6403             return this;
6404         },
6405 
6406         /**
6407          * Set the bounding box of the board.
6408          * @param {Array} bbox New bounding box [x1,y1,x2,y2]
6409          * @param {Boolean} [keepaspectratio=false] If set to true, the aspect ratio will be 1:1, but
6410          * the resulting viewport may be larger.
6411          * @param {String} [setZoom='reset'] Reset, keep or update the zoom level of the board. 'reset'
6412          * sets {@link JXG.Board#zoomX} and {@link JXG.Board#zoomY} to the start values (or 1.0).
6413          * 'update' adapts these values accoring to the new bounding box and 'keep' does nothing.
6414          * @returns {JXG.Board} Reference to the board
6415          */
6416         setBoundingBox: function (bbox, keepaspectratio, setZoom) {
6417             var h, w, ux, uy,
6418                 offX = 0,
6419                 offY = 0,
6420                 zoom_ratio = 1,
6421                 ratio, dx, dy, prev_w, prev_h,
6422                 dim = Env.getDimensions(this.containerObj, this.document);
6423 
6424             if (!Type.isArray(bbox)) {
6425                 return this;
6426             }
6427 
6428             if (
6429                 bbox[0] < this.maxboundingbox[0] - Mat.eps ||
6430                 bbox[1] > this.maxboundingbox[1] + Mat.eps ||
6431                 bbox[2] > this.maxboundingbox[2] + Mat.eps ||
6432                 bbox[3] < this.maxboundingbox[3] - Mat.eps
6433             ) {
6434                 return this;
6435             }
6436 
6437             if (!Type.exists(setZoom)) {
6438                 setZoom = 'reset';
6439             }
6440 
6441             ux = this.unitX;
6442             uy = this.unitY;
6443             this.canvasWidth = parseFloat(dim.width);   // parseInt(dim.width, 10);
6444             this.canvasHeight = parseFloat(dim.height); // parseInt(dim.height, 10);
6445             w = this.canvasWidth;
6446             h = this.canvasHeight;
6447             if (keepaspectratio) {
6448                 if (this.keepaspectratio) {
6449                     ratio = ux / uy;        // Keep this ratio if keepaspectratio was true
6450                     if (isNaN(ratio)) {
6451                         ratio = 1.0;
6452                     }
6453                 } else {
6454                     ratio = 1.0;
6455                 }
6456                 if (setZoom === 'keep') {
6457                     zoom_ratio = this.zoomX / this.zoomY;
6458                 }
6459                 dx = bbox[2] - bbox[0];
6460                 dy = bbox[1] - bbox[3];
6461                 prev_w = ux * dx;
6462                 prev_h = uy * dy;
6463                 if (w >= h) {
6464                     if (prev_w >= prev_h) {
6465                         this.unitY = h / dy;
6466                         this.unitX = this.unitY * ratio;
6467                     } else {
6468                         // Switch dominating interval
6469                         this.unitY = h / Math.abs(dx) * Mat.sign(dy) / zoom_ratio;
6470                         this.unitX = this.unitY * ratio;
6471                     }
6472                 } else {
6473                     if (prev_h > prev_w) {
6474                         this.unitX = w / dx;
6475                         this.unitY = this.unitX / ratio;
6476                     } else {
6477                         // Switch dominating interval
6478                         this.unitX = w / Math.abs(dy) * Mat.sign(dx) * zoom_ratio;
6479                         this.unitY = this.unitX / ratio;
6480                     }
6481                 }
6482                 // Add the additional units in equal portions left and right
6483                 offX = (w / this.unitX - dx) * 0.5;
6484                 // Add the additional units in equal portions above and below
6485                 offY = (h / this.unitY - dy) * 0.5;
6486                 this.keepaspectratio = true;
6487             } else {
6488                 this.unitX = w / (bbox[2] - bbox[0]);
6489                 this.unitY = h / (bbox[1] - bbox[3]);
6490                 this.keepaspectratio = false;
6491             }
6492 
6493             this.moveOrigin(-this.unitX * (bbox[0] - offX), this.unitY * (bbox[1] + offY));
6494 
6495             if (setZoom === 'update') {
6496                 this.zoomX *= this.unitX / ux;
6497                 this.zoomY *= this.unitY / uy;
6498             } else if (setZoom === 'reset') {
6499                 this.zoomX = Type.exists(this.attr.zoomx) ? this.attr.zoomx : 1.0;
6500                 this.zoomY = Type.exists(this.attr.zoomy) ? this.attr.zoomy : 1.0;
6501             }
6502 
6503             return this;
6504         },
6505 
6506         /**
6507          * Get the bounding box of the board.
6508          * @returns {Array} bounding box [x1,y1,x2,y2] upper left corner, lower right corner
6509          */
6510         getBoundingBox: function () {
6511             var ul = new Coords(Const.COORDS_BY_SCREEN, [0, 0], this).usrCoords,
6512                 lr = new Coords(
6513                     Const.COORDS_BY_SCREEN,
6514                     [this.canvasWidth, this.canvasHeight],
6515                     this
6516                 ).usrCoords;
6517             return [ul[1], ul[2], lr[1], lr[2]];
6518         },
6519 
6520         /**
6521          * Sets the value of attribute <tt>key</tt> to <tt>value</tt>.
6522          * @param {String} key The attribute's name.
6523          * @param value The new value
6524          * @private
6525          */
6526         _set: function (key, value) {
6527             key = key.toLocaleLowerCase();
6528 
6529             if (
6530                 value !== null &&
6531                 Type.isObject(value) &&
6532                 !Type.exists(value.id) &&
6533                 !Type.exists(value.name)
6534             ) {
6535                 // value is of type {prop: val, prop: val,...}
6536                 // Convert these attributes to lowercase, too
6537                 // this.attr[key] = {};
6538                 // for (el in value) {
6539                 //     if (value.hasOwnProperty(el)) {
6540                 //         this.attr[key][el.toLocaleLowerCase()] = value[el];
6541                 //     }
6542                 // }
6543                 Type.mergeAttr(this.attr[key], value);
6544             } else {
6545                 this.attr[key] = value;
6546             }
6547         },
6548 
6549         /**
6550          * Sets an arbitrary number of attributes. This method has one or more
6551          * parameters of the following types:
6552          * <ul>
6553          * <li> object: {key1:value1,key2:value2,...}
6554          * <li> string: 'key:value'
6555          * <li> array: ['key', value]
6556          * </ul>
6557          * Some board attributes are immutable, like e.g. the renderer type.
6558          *
6559          * @param {Object} attributes An object with attributes
6560          * @param {Boolean} [force=false] if true the attributes are set regardless of the previous setting was identical.
6561          * @returns {JXG.Board} Reference to the board
6562          *
6563          * @example
6564          * const board = JXG.JSXGraph.initBoard('jxgbox', {
6565          *     boundingbox: [-5, 5, 5, -5],
6566          *     keepAspectRatio: false,
6567          *     axis:true,
6568          *     showFullscreen: true,
6569          *     showScreenshot: true,
6570          *     showCopyright: false
6571          * });
6572          *
6573          * board.setAttribute({
6574          *     animationDelay: 10,
6575          *     boundingbox: [-10, 5, 10, -5],
6576          *     defaultAxes: {
6577          *         x: { strokeColor: 'blue', ticks: { strokeColor: 'blue'}}
6578          *     },
6579          *     description: 'test',
6580          *     fullscreen: {
6581          *         scale: 0.5
6582          *     },
6583          *     intl: {
6584          *         enabled: true,
6585          *         locale: 'de-DE'
6586          *     }
6587          * });
6588          *
6589          * board.setAttribute({
6590          *     selection: {
6591          *         enabled: true,
6592          *         fillColor: 'blue'
6593          *     },
6594          *     showInfobox: false,
6595          *     zoomX: 0.5,
6596          *     zoomY: 2,
6597          *     fullscreen: { symbol: 'x' },
6598          *     screenshot: { symbol: 'y' },
6599          *     showCopyright: true,
6600          *     showFullscreen: false,
6601          *     showScreenshot: false,
6602          *     showZoom: false,
6603          *     showNavigation: false
6604          * });
6605          * board.setAttribute('showCopyright:false');
6606          *
6607          * var p = board.create('point', [1, 1], {size: 10,
6608          *     label: {
6609          *         fontSize: 24,
6610          *         highlightStrokeOpacity: 0.1,
6611          *         offset: [5, 0]
6612          *     }
6613          * });
6614          *
6615          *
6616          * </pre><div id="JXGea7b8e09-beac-4d95-9a0c-5fc1c761ffbc" class="jxgbox" style="width: 300px; height: 300px;"></div>
6617          * <script type="text/javascript">
6618          *     (function() {
6619          *     const board = JXG.JSXGraph.initBoard('JXGea7b8e09-beac-4d95-9a0c-5fc1c761ffbc', {
6620          *         boundingbox: [-5, 5, 5, -5],
6621          *         keepAspectRatio: false,
6622          *         axis:true,
6623          *         showFullscreen: true,
6624          *         showScreenshot: true,
6625          *         showCopyright: false
6626          *     });
6627          *
6628          *     board.setAttribute({
6629          *         animationDelay: 10,
6630          *         boundingbox: [-10, 5, 10, -5],
6631          *         defaultAxes: {
6632          *             x: { strokeColor: 'blue', ticks: { strokeColor: 'blue'}}
6633          *         },
6634          *         description: 'test',
6635          *         fullscreen: {
6636          *             scale: 0.5
6637          *         },
6638          *         intl: {
6639          *             enabled: true,
6640          *             locale: 'de-DE'
6641          *         }
6642          *     });
6643          *
6644          *     board.setAttribute({
6645          *         selection: {
6646          *             enabled: true,
6647          *             fillColor: 'blue'
6648          *         },
6649          *         showInfobox: false,
6650          *         zoomX: 0.5,
6651          *         zoomY: 2,
6652          *         fullscreen: { symbol: 'x' },
6653          *         screenshot: { symbol: 'y' },
6654          *         showCopyright: true,
6655          *         showFullscreen: false,
6656          *         showScreenshot: false,
6657          *         showZoom: false,
6658          *         showNavigation: false
6659          *     });
6660          *
6661          *     board.setAttribute('showCopyright:false');
6662          *
6663          *     var p = board.create('point', [1, 1], {size: 10,
6664          *         label: {
6665          *             fontSize: 24,
6666          *             highlightStrokeOpacity: 0.1,
6667          *             offset: [5, 0]
6668          *         }
6669          *     });
6670          *
6671          *
6672          *     })();
6673          *
6674          * </script><pre>
6675          *
6676          *
6677          */
6678         setAttribute: function (attr, force) {
6679             var i, arg, pair,
6680                 key, value, oldvalue,// j, le,
6681                 node, lst, e,
6682                 attributes = {};
6683 
6684             // Normalize the user input
6685             for (i = 0; i < arguments.length; i++) {
6686                 arg = arguments[i];
6687                 if (Type.isString(arg)) {
6688                     // pairRaw is string of the form 'key:value'
6689                     pair = arg.split(":");
6690                     attributes[Type.trim(pair[0])] = Type.trim(pair[1]);
6691                 } else if (!Type.isArray(arg)) {
6692                     // pairRaw consists of objects of the form {key1:value1,key2:value2,...}
6693                     JXG.extend(attributes, arg);
6694                 } else {
6695                     // pairRaw consists of array [key,value]
6696                     attributes[arg[0]] = arg[1];
6697                 }
6698             }
6699 
6700             for (i in attributes) {
6701                 if (attributes.hasOwnProperty(i)) {
6702                     key = i.replace(/\s+/g, "").toLowerCase();
6703                     value = attributes[i];
6704                 }
6705                 value = (value.toLowerCase && value.toLowerCase() === 'false')
6706                     ? false
6707                     : value;
6708                 oldvalue = this.attr[key];
6709                 if (!force && oldvalue === value) {
6710                     continue;
6711                 }
6712                 switch (key) {
6713                     case 'axis':
6714                         if (Type.exists(this.defaultAxes)) {
6715                             this.defaultAxes.x.setAttribute({ visible: value });
6716                             this.defaultAxes.y.setAttribute({ visible: value });
6717                             this.attr[key] = value;
6718                         } else {
6719                             if (value === true) {
6720                                 // create the default axis
6721                                 this.defaultAxes = {};
6722                                 this.defaultAxes.x = this.create("axis", [[0, 0], [1, 0]]);
6723                                 this.defaultAxes.y = this.create("axis", [[0, 0], [0, 1]]);
6724                                 this.attr[key] = true;
6725                             }
6726                         }
6727                         break;
6728                     case 'cssstyle':
6729                         lst = Type.css2js(value);
6730                         node = this.containerObj;
6731                         // node = this.renderer.svgRoot;
6732                         for (e in lst) if (lst.hasOwnProperty(e)) {
6733                             pair = lst[e];
6734                             node.style[pair.key] = pair.val;
6735                         }
6736 
6737                         this._set(key, value);
6738                         break;
6739                     case 'boundingbox':
6740                         this.setBoundingBox(value, this.keepaspectratio);
6741                         this._set(key, value);
6742                         break;
6743                     case 'defaultaxes':
6744                         if (Type.exists(this.defaultAxes.x) && Type.exists(value.x)) {
6745                             this.defaultAxes.x.setAttribute(value.x);
6746                         }
6747                         if (Type.exists(this.defaultAxes.y) && Type.exists(value.y)) {
6748                             this.defaultAxes.y.setAttribute(value.y);
6749                         }
6750                         break;
6751                     case 'title':
6752                         this.document.getElementById(this.container + '_ARIAlabel')
6753                             .innerText = value;
6754                         this._set(key, value);
6755                         break;
6756                     case 'keepaspectratio':
6757                         this._set(key, value);
6758                         this.setBoundingBox(this.getBoundingBox(), value, 'keep');
6759                         break;
6760 
6761                     // /* eslint-disable no-fallthrough */
6762                     case 'document':
6763                     case 'maxboundingbox':
6764                         this[key] = value;
6765                         this._set(key, value);
6766                         break;
6767 
6768                     case 'zoomx':
6769                     case 'zoomy':
6770                         this[key] = value;
6771                         this._set(key, value);
6772                         this.setZoom(this.attr.zoomx, this.attr.zoomy);
6773                         break;
6774 
6775                     case 'registerevents':
6776                     case 'renderer':
6777                         // immutable, i.e. ignored
6778                         break;
6779 
6780                     case 'fullscreen':
6781                     case 'screenshot':
6782                         node = this.containerObj.ownerDocument.getElementById(
6783                             this.container + '_navigation_' + key);
6784                         if (node && Type.exists(value.symbol)) {
6785                             node.innerText = Type.evaluate(value.symbol);
6786                         }
6787                         this._set(key, value);
6788                         break;
6789 
6790                     case 'selection':
6791                         value.visible = false;
6792                         value.withLines = false;
6793                         value.vertices = { visible: false };
6794                         this._set(key, value);
6795                         break;
6796 
6797                     case 'showcopyright':
6798                         if (this.renderer.type === 'svg') {
6799                             node = this.containerObj.ownerDocument.getElementById(
6800                                 this.renderer.uniqName('licenseText')
6801                             );
6802                             if (node) {
6803                                 node.style.display = ((Type.evaluate(value)) ? 'inline' : 'none');
6804                             } else if (Type.evaluate(value)) {
6805                                 this.renderer.displayCopyright(Const.licenseText, parseInt(this.options.text.fontSize, 10));
6806                             }
6807                         }
6808                         this._set(key, value);
6809                         break;
6810 
6811                     case 'showlogo':
6812                         if (this.renderer.type === 'svg') {
6813                             node = this.containerObj.ownerDocument.getElementById(
6814                                 this.renderer.uniqName('licenseLogo')
6815                             );
6816                             if (node) {
6817                                 node.style.display = ((Type.evaluate(value)) ? 'inline' : 'none');
6818                             } else if (Type.evaluate(value)) {
6819                                 this.renderer.displayLogo(Const.licenseLogo, parseInt(this.options.text.fontSize, 10));
6820                             }
6821                         }
6822                         this._set(key, value);
6823                         break;
6824 
6825                     default:
6826                         if (Type.exists(this.attr[key])) {
6827                             this._set(key, value);
6828                         }
6829                         break;
6830                     // /* eslint-enable no-fallthrough */
6831                 }
6832             }
6833 
6834             // Redraw navbar to handle the remaining show* attributes
6835             node = this.containerObj.ownerDocument.getElementById(this.container + "_navigationbar");
6836             if (Type.exists(node)) {
6837                 node.remove();
6838                 this.renderer.drawNavigationBar(this, this.attr.navbar);
6839             }
6840 
6841             this.triggerEventHandlers(["attribute"], [attributes, this]);
6842             this.fullUpdate();
6843 
6844             return this;
6845         },
6846 
6847         /**
6848          * Adds an animation. Animations are controlled by the boards, so the boards need to be aware of the
6849          * animated elements. This function tells the board about new elements to animate.
6850          * @param {JXG.GeometryElement} element The element which is to be animated.
6851          * @returns {JXG.Board} Reference to the board
6852          */
6853         addAnimation: function (element) {
6854             var that = this;
6855 
6856             this.animationObjects[element.id] = element;
6857 
6858             if (!this.animationIntervalCode) {
6859                 this.animationIntervalCode = window.setInterval(function () {
6860                     that.animate();
6861                 }, element.board.attr.animationdelay);
6862             }
6863 
6864             return this;
6865         },
6866 
6867         /**
6868          * Cancels all running animations.
6869          * @returns {JXG.Board} Reference to the board
6870          */
6871         stopAllAnimation: function () {
6872             var el;
6873 
6874             for (el in this.animationObjects) {
6875                 if (
6876                     this.animationObjects.hasOwnProperty(el) &&
6877                     Type.exists(this.animationObjects[el])
6878                 ) {
6879                     this.animationObjects[el] = null;
6880                     delete this.animationObjects[el];
6881                 }
6882             }
6883 
6884             window.clearInterval(this.animationIntervalCode);
6885             delete this.animationIntervalCode;
6886 
6887             return this;
6888         },
6889 
6890         /**
6891          * General purpose animation function. This currently only supports moving points from one place to another. This
6892          * is faster than managing the animation per point, especially if there is more than one animated point at the same time.
6893          * @returns {JXG.Board} Reference to the board
6894          */
6895         animate: function () {
6896             var props,
6897                 el,
6898                 o,
6899                 newCoords,
6900                 r,
6901                 p,
6902                 c,
6903                 cbtmp,
6904                 count = 0,
6905                 obj = null;
6906 
6907             for (el in this.animationObjects) {
6908                 if (
6909                     this.animationObjects.hasOwnProperty(el) &&
6910                     Type.exists(this.animationObjects[el])
6911                 ) {
6912                     count += 1;
6913                     o = this.animationObjects[el];
6914 
6915                     if (o.animationPath) {
6916                         if (Type.isFunction(o.animationPath)) {
6917                             newCoords = o.animationPath(
6918                                 new Date().getTime() - o.animationStart
6919                             );
6920                         } else {
6921                             newCoords = o.animationPath.pop();
6922                         }
6923 
6924                         if (
6925                             !Type.exists(newCoords) ||
6926                             (!Type.isArray(newCoords) && isNaN(newCoords))
6927                         ) {
6928                             delete o.animationPath;
6929                         } else {
6930                             o.setPositionDirectly(Const.COORDS_BY_USER, newCoords);
6931                             o.fullUpdate();
6932                             obj = o;
6933                         }
6934                     }
6935                     if (o.animationData) {
6936                         c = 0;
6937 
6938                         for (r in o.animationData) {
6939                             if (o.animationData.hasOwnProperty(r)) {
6940                                 p = o.animationData[r].pop();
6941 
6942                                 if (!Type.exists(p)) {
6943                                     delete o.animationData[p];
6944                                 } else {
6945                                     c += 1;
6946                                     props = {};
6947                                     props[r] = p;
6948                                     o.setAttribute(props);
6949                                 }
6950                             }
6951                         }
6952 
6953                         if (c === 0) {
6954                             delete o.animationData;
6955                         }
6956                     }
6957 
6958                     if (!Type.exists(o.animationData) && !Type.exists(o.animationPath)) {
6959                         this.animationObjects[el] = null;
6960                         delete this.animationObjects[el];
6961 
6962                         if (Type.exists(o.animationCallback)) {
6963                             cbtmp = o.animationCallback;
6964                             o.animationCallback = null;
6965                             cbtmp();
6966                         }
6967                     }
6968                 }
6969             }
6970 
6971             if (count === 0) {
6972                 window.clearInterval(this.animationIntervalCode);
6973                 delete this.animationIntervalCode;
6974             } else {
6975                 this.update(obj);
6976             }
6977 
6978             return this;
6979         },
6980 
6981         /**
6982          * Migrate the dependency properties of the point src
6983          * to the point dest and delete the point src.
6984          * For example, a circle around the point src
6985          * receives the new center dest. The old center src
6986          * will be deleted.
6987          * @param {JXG.Point} src Original point which will be deleted
6988          * @param {JXG.Point} dest New point with the dependencies of src.
6989          * @param {Boolean} copyName Flag which decides if the name of the src element is copied to the
6990          *  dest element.
6991          * @returns {JXG.Board} Reference to the board
6992          */
6993         migratePoint: function (src, dest, copyName) {
6994             var child,
6995                 childId,
6996                 prop,
6997                 found,
6998                 i,
6999                 srcLabelId,
7000                 srcHasLabel = false;
7001 
7002             src = this.select(src);
7003             dest = this.select(dest);
7004 
7005             if (Type.exists(src.label)) {
7006                 srcLabelId = src.label.id;
7007                 srcHasLabel = true;
7008                 this.removeObject(src.label);
7009             }
7010 
7011             for (childId in src.childElements) {
7012                 if (src.childElements.hasOwnProperty(childId)) {
7013                     child = src.childElements[childId];
7014                     found = false;
7015 
7016                     for (prop in child) {
7017                         if (child.hasOwnProperty(prop)) {
7018                             if (child[prop] === src) {
7019                                 child[prop] = dest;
7020                                 found = true;
7021                             }
7022                         }
7023                     }
7024 
7025                     if (found) {
7026                         delete src.childElements[childId];
7027                     }
7028 
7029                     for (i = 0; i < child.parents.length; i++) {
7030                         if (child.parents[i] === src.id) {
7031                             child.parents[i] = dest.id;
7032                         }
7033                     }
7034 
7035                     dest.addChild(child);
7036                 }
7037             }
7038 
7039             // The destination object should receive the name
7040             // and the label of the originating (src) object
7041             if (copyName) {
7042                 if (srcHasLabel) {
7043                     delete dest.childElements[srcLabelId];
7044                     delete dest.descendants[srcLabelId];
7045                 }
7046 
7047                 if (dest.label) {
7048                     this.removeObject(dest.label);
7049                 }
7050 
7051                 delete this.elementsByName[dest.name];
7052                 dest.name = src.name;
7053                 if (srcHasLabel) {
7054                     dest.createLabel();
7055                 }
7056             }
7057 
7058             this.removeObject(src);
7059 
7060             if (Type.exists(dest.name) && dest.name !== '') {
7061                 this.elementsByName[dest.name] = dest;
7062             }
7063 
7064             this.fullUpdate();
7065 
7066             return this;
7067         },
7068 
7069         /**
7070          * Initializes color blindness simulation.
7071          * @param {String} deficiency Describes the color blindness deficiency which is simulated. Accepted values are 'protanopia', 'deuteranopia', and 'tritanopia'.
7072          * @returns {JXG.Board} Reference to the board
7073          */
7074         emulateColorblindness: function (deficiency) {
7075             var e, o;
7076 
7077             if (!Type.exists(deficiency)) {
7078                 deficiency = 'none';
7079             }
7080 
7081             if (this.currentCBDef === deficiency) {
7082                 return this;
7083             }
7084 
7085             for (e in this.objects) {
7086                 if (this.objects.hasOwnProperty(e)) {
7087                     o = this.objects[e];
7088 
7089                     if (deficiency !== 'none') {
7090                         if (this.currentCBDef === 'none') {
7091                             // this could be accomplished by JXG.extend, too. But do not use
7092                             // JXG.deepCopy as this could result in an infinite loop because in
7093                             // visProp there could be geometry elements which contain the board which
7094                             // contains all objects which contain board etc.
7095                             o.visPropOriginal = {
7096                                 strokecolor: o.visProp.strokecolor,
7097                                 fillcolor: o.visProp.fillcolor,
7098                                 highlightstrokecolor: o.visProp.highlightstrokecolor,
7099                                 highlightfillcolor: o.visProp.highlightfillcolor
7100                             };
7101                         }
7102                         o.setAttribute({
7103                             strokecolor: Color.rgb2cb(
7104                                 o.eval(o.visPropOriginal.strokecolor),
7105                                 deficiency
7106                             ),
7107                             fillcolor: Color.rgb2cb(
7108                                 o.eval(o.visPropOriginal.fillcolor),
7109                                 deficiency
7110                             ),
7111                             highlightstrokecolor: Color.rgb2cb(
7112                                 o.eval(o.visPropOriginal.highlightstrokecolor),
7113                                 deficiency
7114                             ),
7115                             highlightfillcolor: Color.rgb2cb(
7116                                 o.eval(o.visPropOriginal.highlightfillcolor),
7117                                 deficiency
7118                             )
7119                         });
7120                     } else if (Type.exists(o.visPropOriginal)) {
7121                         JXG.extend(o.visProp, o.visPropOriginal);
7122                     }
7123                 }
7124             }
7125             this.currentCBDef = deficiency;
7126             this.update();
7127 
7128             return this;
7129         },
7130 
7131         /**
7132          * Select a single or multiple elements at once.
7133          * @param {String|Object|function} str The name, id or a reference to a JSXGraph element on this board. An object will
7134          * be used as a filter to return multiple elements at once filtered by the properties of the object.
7135          * @param {Boolean} onlyByIdOrName If true (default:false) elements are only filtered by their id, name or groupId.
7136          * The advanced filters consisting of objects or functions are ignored.
7137          * @returns {JXG.GeometryElement|JXG.Composition}
7138          * @example
7139          * // select the element with name A
7140          * board.select('A');
7141          *
7142          * // select all elements with strokecolor set to 'red' (but not '#ff0000')
7143          * board.select({
7144          *   strokeColor: 'red'
7145          * });
7146          *
7147          * // select all points on or below the x axis and make them black.
7148          * board.select({
7149          *   elementClass: JXG.OBJECT_CLASS_POINT,
7150          *   Y: function (v) {
7151          *     return v <= 0;
7152          *   }
7153          * }).setAttribute({color: 'black'});
7154          *
7155          * // select all elements
7156          * board.select(function (el) {
7157          *   return true;
7158          * });
7159          */
7160         select: function (str, onlyByIdOrName) {
7161             var flist,
7162                 olist,
7163                 i,
7164                 l,
7165                 s = str;
7166 
7167             if (s === null) {
7168                 return s;
7169             }
7170 
7171             // It's a string, most likely an id or a name.
7172             if (Type.isString(s) && s !== '') {
7173                 // Search by ID
7174                 if (Type.exists(this.objects[s])) {
7175                     s = this.objects[s];
7176                     // Search by name
7177                 } else if (Type.exists(this.elementsByName[s])) {
7178                     s = this.elementsByName[s];
7179                     // Search by group ID
7180                 } else if (Type.exists(this.groups[s])) {
7181                     s = this.groups[s];
7182                 }
7183 
7184                 // It's a function or an object, but not an element
7185             } else if (
7186                 !onlyByIdOrName &&
7187                 (Type.isFunction(s) || (Type.isObject(s) && !Type.isFunction(s.setAttribute)))
7188             ) {
7189                 flist = Type.filterElements(this.objectsList, s);
7190 
7191                 olist = {};
7192                 l = flist.length;
7193                 for (i = 0; i < l; i++) {
7194                     olist[flist[i].id] = flist[i];
7195                 }
7196                 s = new Composition(olist);
7197 
7198                 // It's an element which has been deleted (and still hangs around, e.g. in an attractor list
7199             } else if (
7200                 Type.isObject(s) &&
7201                 Type.exists(s.id) &&
7202                 !Type.exists(this.objects[s.id])
7203             ) {
7204                 s = null;
7205             }
7206 
7207             return s;
7208         },
7209 
7210         /**
7211          * Checks if the given point is inside the boundingbox.
7212          * @param {Number|JXG.Coords} x User coordinate or {@link JXG.Coords} object.
7213          * @param {Number} [y] User coordinate. May be omitted in case <tt>x</tt> is a {@link JXG.Coords} object.
7214          * @returns {Boolean}
7215          */
7216         hasPoint: function (x, y) {
7217             var px = x,
7218                 py = y,
7219                 bbox = this.getBoundingBox();
7220 
7221             if (Type.exists(x) && Type.isArray(x.usrCoords)) {
7222                 px = x.usrCoords[1];
7223                 py = x.usrCoords[2];
7224             }
7225 
7226             return !!(
7227                 Type.isNumber(px) &&
7228                 Type.isNumber(py) &&
7229                 bbox[0] < px &&
7230                 px < bbox[2] &&
7231                 bbox[1] > py &&
7232                 py > bbox[3]
7233             );
7234         },
7235 
7236         /**
7237          * Update CSS transformations of type scaling. It is used to correct the mouse position
7238          * in {@link JXG.Board.getMousePosition}.
7239          * The inverse transformation matrix is updated on each mouseDown and touchStart event.
7240          *
7241          * It is up to the user to call this method after an update of the CSS transformation
7242          * in the DOM.
7243          */
7244         updateCSSTransforms: function () {
7245             var obj = this.containerObj,
7246                 o = obj,
7247                 o2 = obj;
7248 
7249             this.cssTransMat = Env.getCSSTransformMatrix(o);
7250 
7251             // Newer variant of walking up the tree.
7252             // We walk up all parent nodes and collect possible CSS transforms.
7253             // Works also for ShadowDOM
7254             if (Type.exists(o.getRootNode)) {
7255                 o = o.parentNode === o.getRootNode() ? o.parentNode.host : o.parentNode;
7256                 while (o) {
7257                     this.cssTransMat = Mat.matMatMult(Env.getCSSTransformMatrix(o), this.cssTransMat);
7258                     o = o.parentNode === o.getRootNode() ? o.parentNode.host : o.parentNode;
7259                 }
7260                 this.cssTransMat = Mat.inverse(this.cssTransMat);
7261             } else {
7262                 /*
7263                  * This is necessary for IE11
7264                  */
7265                 o = o.offsetParent;
7266                 while (o) {
7267                     this.cssTransMat = Mat.matMatMult(Env.getCSSTransformMatrix(o), this.cssTransMat);
7268 
7269                     o2 = o2.parentNode;
7270                     while (o2 !== o) {
7271                         this.cssTransMat = Mat.matMatMult(Env.getCSSTransformMatrix(o), this.cssTransMat);
7272                         o2 = o2.parentNode;
7273                     }
7274                     o = o.offsetParent;
7275                 }
7276                 this.cssTransMat = Mat.inverse(this.cssTransMat);
7277             }
7278             return this;
7279         },
7280 
7281         /**
7282          * Start selection mode. This function can either be triggered from outside or by
7283          * a down event together with correct key pressing. The default keys are
7284          * shift+ctrl. But this can be changed in the options.
7285          *
7286          * Starting from out side can be realized for example with a button like this:
7287          * <pre>
7288          * 	<button onclick='board.startSelectionMode()'>Start</button>
7289          * </pre>
7290          * @example
7291          * //
7292          * // Set a new bounding box from the selection rectangle
7293          * //
7294          * var board = JXG.JSXGraph.initBoard('jxgbox', {
7295          *         boundingBox:[-3,2,3,-2],
7296          *         keepAspectRatio: false,
7297          *         axis:true,
7298          *         selection: {
7299          *             enabled: true,
7300          *             needShift: false,
7301          *             needCtrl: true,
7302          *             withLines: false,
7303          *             vertices: {
7304          *                 visible: false
7305          *             },
7306          *             fillColor: '#ffff00',
7307          *         }
7308          *      });
7309          *
7310          * var f = function f(x) { return Math.cos(x); },
7311          *     curve = board.create('functiongraph', [f]);
7312          *
7313          * board.on('stopselecting', function(){
7314          *     var box = board.stopSelectionMode(),
7315          *
7316          *         // bbox has the coordinates of the selection rectangle.
7317          *         // Attention: box[i].usrCoords have the form [1, x, y], i.e.
7318          *         // are homogeneous coordinates.
7319          *         bbox = box[0].usrCoords.slice(1).concat(box[1].usrCoords.slice(1));
7320          *
7321          *         // Set a new bounding box
7322          *         board.setBoundingBox(bbox, false);
7323          *  });
7324          *
7325          *
7326          * </pre><div class='jxgbox' id='JXG11eff3a6-8c50-11e5-b01d-901b0e1b8723' style='width: 300px; height: 300px;'></div>
7327          * <script type='text/javascript'>
7328          *     (function() {
7329          *     //
7330          *     // Set a new bounding box from the selection rectangle
7331          *     //
7332          *     var board = JXG.JSXGraph.initBoard('JXG11eff3a6-8c50-11e5-b01d-901b0e1b8723', {
7333          *             boundingBox:[-3,2,3,-2],
7334          *             keepAspectRatio: false,
7335          *             axis:true,
7336          *             selection: {
7337          *                 enabled: true,
7338          *                 needShift: false,
7339          *                 needCtrl: true,
7340          *                 withLines: false,
7341          *                 vertices: {
7342          *                     visible: false
7343          *                 },
7344          *                 fillColor: '#ffff00',
7345          *             }
7346          *        });
7347          *
7348          *     var f = function f(x) { return Math.cos(x); },
7349          *         curve = board.create('functiongraph', [f]);
7350          *
7351          *     board.on('stopselecting', function(){
7352          *         var box = board.stopSelectionMode(),
7353          *
7354          *             // bbox has the coordinates of the selection rectangle.
7355          *             // Attention: box[i].usrCoords have the form [1, x, y], i.e.
7356          *             // are homogeneous coordinates.
7357          *             bbox = box[0].usrCoords.slice(1).concat(box[1].usrCoords.slice(1));
7358          *
7359          *             // Set a new bounding box
7360          *             board.setBoundingBox(bbox, false);
7361          *      });
7362          *     })();
7363          *
7364          * </script><pre>
7365          *
7366          */
7367         startSelectionMode: function () {
7368             this.selectingMode = true;
7369             this.selectionPolygon.setAttribute({ visible: true });
7370             this.selectingBox = [
7371                 [0, 0],
7372                 [0, 0]
7373             ];
7374             this._setSelectionPolygonFromBox();
7375             this.selectionPolygon.fullUpdate();
7376         },
7377 
7378         /**
7379          * Finalize the selection: disable selection mode and return the coordinates
7380          * of the selection rectangle.
7381          * @returns {Array} Coordinates of the selection rectangle. The array
7382          * contains two {@link JXG.Coords} objects. One the upper left corner and
7383          * the second for the lower right corner.
7384          */
7385         stopSelectionMode: function () {
7386             this.selectingMode = false;
7387             this.selectionPolygon.setAttribute({ visible: false });
7388             return [
7389                 this.selectionPolygon.vertices[0].coords,
7390                 this.selectionPolygon.vertices[2].coords
7391             ];
7392         },
7393 
7394         /**
7395          * Start the selection of a region.
7396          * @private
7397          * @param  {Array} pos Screen coordiates of the upper left corner of the
7398          * selection rectangle.
7399          */
7400         _startSelecting: function (pos) {
7401             this.isSelecting = true;
7402             this.selectingBox = [
7403                 [pos[0], pos[1]],
7404                 [pos[0], pos[1]]
7405             ];
7406             this._setSelectionPolygonFromBox();
7407         },
7408 
7409         /**
7410          * Update the selection rectangle during a move event.
7411          * @private
7412          * @param  {Array} pos Screen coordiates of the move event
7413          */
7414         _moveSelecting: function (pos) {
7415             if (this.isSelecting) {
7416                 this.selectingBox[1] = [pos[0], pos[1]];
7417                 this._setSelectionPolygonFromBox();
7418                 this.selectionPolygon.fullUpdate();
7419             }
7420         },
7421 
7422         /**
7423          * Update the selection rectangle during an up event. Stop selection.
7424          * @private
7425          * @param  {Object} evt Event object
7426          */
7427         _stopSelecting: function (evt) {
7428             var pos = this.getMousePosition(evt);
7429 
7430             this.isSelecting = false;
7431             this.selectingBox[1] = [pos[0], pos[1]];
7432             this._setSelectionPolygonFromBox();
7433         },
7434 
7435         /**
7436          * Update the Selection rectangle.
7437          * @private
7438          */
7439         _setSelectionPolygonFromBox: function () {
7440             var A = this.selectingBox[0],
7441                 B = this.selectingBox[1];
7442 
7443             this.selectionPolygon.vertices[0].setPositionDirectly(JXG.COORDS_BY_SCREEN, [
7444                 A[0],
7445                 A[1]
7446             ]);
7447             this.selectionPolygon.vertices[1].setPositionDirectly(JXG.COORDS_BY_SCREEN, [
7448                 A[0],
7449                 B[1]
7450             ]);
7451             this.selectionPolygon.vertices[2].setPositionDirectly(JXG.COORDS_BY_SCREEN, [
7452                 B[0],
7453                 B[1]
7454             ]);
7455             this.selectionPolygon.vertices[3].setPositionDirectly(JXG.COORDS_BY_SCREEN, [
7456                 B[0],
7457                 A[1]
7458             ]);
7459         },
7460 
7461         /**
7462          * Test if a down event should start a selection. Test if the
7463          * required keys are pressed. If yes, {@link JXG.Board.startSelectionMode} is called.
7464          * @param  {Object} evt Event object
7465          */
7466         _testForSelection: function (evt) {
7467             if (this._isRequiredKeyPressed(evt, 'selection')) {
7468                 if (!Type.exists(this.selectionPolygon)) {
7469                     this._createSelectionPolygon(this.attr);
7470                 }
7471                 this.startSelectionMode();
7472             }
7473         },
7474 
7475         /**
7476          * Create the internal selection polygon, which will be available as board.selectionPolygon.
7477          * @private
7478          * @param  {Object} attr board attributes, e.g. the subobject board.attr.
7479          * @returns {Object} pointer to the board to enable chaining.
7480          */
7481         _createSelectionPolygon: function (attr) {
7482             var selectionattr;
7483 
7484             if (!Type.exists(this.selectionPolygon)) {
7485                 selectionattr = Type.copyAttributes(attr, Options, 'board', 'selection');
7486                 if (selectionattr.enabled === true) {
7487                     this.selectionPolygon = this.create(
7488                         'polygon',
7489                         [
7490                             [0, 0],
7491                             [0, 0],
7492                             [0, 0],
7493                             [0, 0]
7494                         ],
7495                         selectionattr
7496                     );
7497                 }
7498             }
7499 
7500             return this;
7501         },
7502 
7503         /**
7504          * Reset the sketchcurves in board.sketches[] to length 0 and add the position
7505          * of the event as first point of the sketch curve. Called at down events.
7506          * <p>
7507          * Sets board.isSketching[i] = true where i depends on the finger (1st or 2nd).
7508          *
7509          * @private
7510          * @param {Object} evt Event object
7511          * @see JXG.Board#addToSketchCurve
7512          * @see JXG.Board#finalizeSketchCurve
7513          */
7514         initSketchCurve: function(evt) {
7515             var i, c;
7516             // Init sketchcurves
7517             if (this.mode !== this.BOARD_MODE_MOVE_ORIGIN) {
7518                 // Add coords to sketch curves
7519                 // Only first and second finger are stored
7520                 c = this.getUsrCoordsOfMouse(evt);
7521                 i = (evt.isPrimary) ? 0 : 1;
7522                 if (Type.exists(this.sketches[i])) {
7523                     this.sketches[i].dataX = [c[0]];
7524                     this.sketches[i].dataY = [c[1]];
7525                     this.isSketching[i] = true;
7526                 }
7527             }
7528         },
7529 
7530         /**
7531          * Add the position of the event to the sketchcurve i in board.sketches[].
7532          * Called at move events.
7533          * Point is only added if board.isSketching[i] = true.
7534          *
7535          * @private
7536          * @param {Object} evt Event object
7537          * @see JXG.Board#initSketchCurve
7538          * @see JXG.Board#finalizeSketchCurve
7539          */
7540         addToSketchCurve: function(evt) {
7541             var i, c, len;
7542 
7543             // Add coords to sketchcurves
7544             // Only first and second finger are stored
7545             i = (evt.isPrimary) ? 0 : 1;
7546             if (this.attr.sketches.enabled && this.isSketching[i] === true) {
7547                 if (Type.exists(this.sketches[i])) {
7548                     c = this.getUsrCoordsOfMouse(evt);
7549                     this.sketches[i].dataX.push(c[0]);
7550                     this.sketches[i].dataY.push(c[1]);
7551 
7552                     len = this.sketches[i].evalVisProp('maxlength');
7553                     if (len !== null && this.sketches[i].dataX.length > len) {
7554                         this.sketches[i].dataX = this.sketches[i].dataX.slice(-len);
7555                         this.sketches[i].dataY = this.sketches[i].dataY.slice(-len);
7556                     }
7557                     if (this.sketches[i].evalVisProp('visible')) {
7558                         this.update();
7559                     }
7560                 }
7561             }
7562         },
7563 
7564         /**
7565          * Ends adding points to the sketchcurve i in board.sketches[].
7566          * Called at up events.
7567          * Sets board.isSketching[i] = false.
7568          * Empties the curve if deleteOnUp==true;
7569          *
7570          * @private
7571          * @param {Object} evt Event object
7572          * @see JXG.Board#initSketchCurve
7573          * @see JXG.Board#addToSketchCurve
7574          */
7575         finalizeSketchCurve: function(evt) {
7576             var i;
7577 
7578             // Stop sketching into this.sketches
7579             i = (evt.isPrimary) ? 0 : 1;
7580             if (this.attr.sketches.enabled) {
7581                 if (Type.exists(this.sketches[i])) {
7582                     this.isSketching[i] = false;
7583                     if (this.sketches[i].evalVisProp('deleteOnUp')) {
7584                         this.sketches[i].dataX = [];
7585                         this.sketches[i].dataY = [];
7586                     }
7587                 }
7588             }
7589         },
7590 
7591         /* **************************
7592          *     EVENT DEFINITION
7593          * for documentation purposes
7594          * ************************** */
7595 
7596         //region Event handler documentation
7597 
7598         /**
7599          * @event
7600          * @description Whenever the {@link JXG.Board#setAttribute} is called.
7601          * @name JXG.Board#attribute
7602          * @param {Event} e The browser's event object.
7603          */
7604         __evt__attribute: function (e) { },
7605 
7606         /**
7607          * @event
7608          * @description Whenever the user starts to touch or click the board.
7609          * @name JXG.Board#down
7610          * @param {Event} e The browser's event object.
7611          */
7612         __evt__down: function (e) { },
7613 
7614         /**
7615          * @event
7616          * @description Whenever the user starts to click on the board.
7617          * @name JXG.Board#mousedown
7618          * @param {Event} e The browser's event object.
7619          */
7620         __evt__mousedown: function (e) { },
7621 
7622         /**
7623          * @event
7624          * @description Whenever the user taps the pen on the board.
7625          * @name JXG.Board#pendown
7626          * @param {Event} e The browser's event object.
7627          */
7628         __evt__pendown: function (e) { },
7629 
7630         /**
7631          * @event
7632          * @description Whenever the user starts to click on the board with a
7633          * device sending pointer events.
7634          * @name JXG.Board#pointerdown
7635          * @param {Event} e The browser's event object.
7636          */
7637         __evt__pointerdown: function (e) { },
7638 
7639         /**
7640          * @event
7641          * @description Whenever the user starts to touch the board.
7642          * @name JXG.Board#touchstart
7643          * @param {Event} e The browser's event object.
7644          */
7645         __evt__touchstart: function (e) { },
7646 
7647         /**
7648          * @event
7649          * @description Whenever the user stops to touch or click the board.
7650          * @name JXG.Board#up
7651          * @param {Event} e The browser's event object.
7652          */
7653         __evt__up: function (e) { },
7654 
7655         /**
7656          * @event
7657          * @description Whenever the user releases the mousebutton over the board.
7658          * @name JXG.Board#mouseup
7659          * @param {Event} e The browser's event object.
7660          */
7661         __evt__mouseup: function (e) { },
7662 
7663         /**
7664          * @event
7665          * @description Whenever the user releases the mousebutton over the board with a
7666          * device sending pointer events.
7667          * @name JXG.Board#pointerup
7668          * @param {Event} e The browser's event object.
7669          */
7670         __evt__pointerup: function (e) { },
7671 
7672         /**
7673          * @event
7674          * @description Whenever the user stops touching the board.
7675          * @name JXG.Board#touchend
7676          * @param {Event} e The browser's event object.
7677          */
7678         __evt__touchend: function (e) { },
7679 
7680         /**
7681          * @event
7682          * @description Whenever the user clicks on the board.
7683          * @name JXG.Board#click
7684          * @see JXG.Board#clickDelay
7685          * @param {Event} e The browser's event object.
7686          */
7687         __evt__click: function (e) { },
7688 
7689         /**
7690          * @event
7691          * @description Whenever the user double clicks on the board.
7692          * This event works on desktop browser, but is undefined
7693          * on mobile browsers.
7694          * @name JXG.Board#dblclick
7695          * @see JXG.Board#clickDelay
7696          * @see JXG.Board#dblClickSuppressClick
7697          * @param {Event} e The browser's event object.
7698          */
7699         __evt__dblclick: function (e) { },
7700 
7701         /**
7702          * @event
7703          * @description Whenever the user clicks on the board with a mouse device.
7704          * @name JXG.Board#mouseclick
7705          * @param {Event} e The browser's event object.
7706          */
7707         __evt__mouseclick: function (e) { },
7708 
7709         /**
7710          * @event
7711          * @description Whenever the user double clicks on the board with a mouse device.
7712          * @name JXG.Board#mousedblclick
7713          * @see JXG.Board#clickDelay
7714          * @param {Event} e The browser's event object.
7715          */
7716         __evt__mousedblclick: function (e) { },
7717 
7718         /**
7719          * @event
7720          * @description Whenever the user clicks on the board with a pointer device.
7721          * @name JXG.Board#pointerclick
7722          * @param {Event} e The browser's event object.
7723          */
7724         __evt__pointerclick: function (e) { },
7725 
7726         /**
7727          * @event
7728          * @description Whenever the user double clicks on the board with a pointer device.
7729          * This event works on desktop browser, but is undefined
7730          * on mobile browsers.
7731          * @name JXG.Board#pointerdblclick
7732          * @see JXG.Board#clickDelay
7733          * @param {Event} e The browser's event object.
7734          */
7735         __evt__pointerdblclick: function (e) { },
7736 
7737         /**
7738          * @event
7739          * @description This event is fired whenever the user is moving the finger or mouse pointer over the board.
7740          * @name JXG.Board#move
7741          * @param {Event} e The browser's event object.
7742          * @param {Number} mode The mode the board currently is in
7743          * @see JXG.Board#mode
7744          */
7745         __evt__move: function (e, mode) { },
7746 
7747         /**
7748          * @event
7749          * @description This event is fired whenever the user is moving the mouse over the board.
7750          * @name JXG.Board#mousemove
7751          * @param {Event} e The browser's event object.
7752          * @param {Number} mode The mode the board currently is in
7753          * @see JXG.Board#mode
7754          */
7755         __evt__mousemove: function (e, mode) { },
7756 
7757         /**
7758          * @event
7759          * @description This event is fired whenever the user is moving the pen over the board.
7760          * @name JXG.Board#penmove
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__penmove: function (e, mode) { },
7766 
7767         /**
7768          * @event
7769          * @description This event is fired whenever the user is moving the mouse over the board with a
7770          * device sending pointer events.
7771          * @name JXG.Board#pointermove
7772          * @param {Event} e The browser's event object.
7773          * @param {Number} mode The mode the board currently is in
7774          * @see JXG.Board#mode
7775          */
7776         __evt__pointermove: function (e, mode) { },
7777 
7778         /**
7779          * @event
7780          * @description This event is fired whenever the user is moving the finger over the board.
7781          * @name JXG.Board#touchmove
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__touchmove: function (e, mode) { },
7787 
7788         /**
7789          * @event
7790          * @description This event is fired whenever the user is moving an element over the board by
7791          * pressing arrow keys on a keyboard.
7792          * @name JXG.Board#keymove
7793          * @param {Event} e The browser's event object.
7794          * @param {Number} mode The mode the board currently is in
7795          * @see JXG.Board#mode
7796          */
7797         __evt__keymove: function (e, mode) { },
7798 
7799         /**
7800          * @event
7801          * @description Whenever an element is highlighted this event is fired.
7802          * @name JXG.Board#hit
7803          * @param {Event} e The browser's event object.
7804          * @param {JXG.GeometryElement} el The hit element.
7805          * @param target
7806          *
7807          * @example
7808          * var c = board.create('circle', [[1, 1], 2]);
7809          * board.on('hit', function(evt, el) {
7810          *     console.log('JSXGraph example: Hit element', el);
7811          * });
7812          *
7813          * </pre><div id='JXG19eb31ac-88e6-11e8-bcb5-901b0e1b8723' class='jxgbox' style='width: 300px; height: 300px;'></div>
7814          * <script type='text/javascript'>
7815          *     (function() {
7816          *         var board = JXG.JSXGraph.initBoard('JXG19eb31ac-88e6-11e8-bcb5-901b0e1b8723',
7817          *             {boundingbox: [-8, 8, 8,-8], axis: true, showcopyright: false, shownavigation: false});
7818          *     var c = board.create('circle', [[1, 1], 2]);
7819          *     board.on('hit', function(evt, el) {
7820          *         console.log('JSXGraph example: Hit element', el);
7821          *     });
7822          *
7823          *     })();
7824          *
7825          * </script><pre>
7826          */
7827         __evt__hit: function (e, el, target) { },
7828 
7829         /**
7830          * @event
7831          * @description Whenever an element is highlighted this event is fired.
7832          * @name JXG.Board#mousehit
7833          * @see JXG.Board#hit
7834          * @param {Event} e The browser's event object.
7835          * @param {JXG.GeometryElement} el The hit element.
7836          * @param target
7837          */
7838         __evt__mousehit: function (e, el, target) { },
7839 
7840         /**
7841          * @event
7842          * @description This board is updated.
7843          * @name JXG.Board#update
7844          */
7845         __evt__update: function () { },
7846 
7847         /**
7848          * @event
7849          * @description The bounding box of the board has changed.
7850          * @name JXG.Board#boundingbox
7851          */
7852         __evt__boundingbox: function () { },
7853 
7854         /**
7855          * @event
7856          * @description Select a region is started during a down event or by calling
7857          * {@link JXG.Board.startSelectionMode}
7858          * @name JXG.Board#startselecting
7859          */
7860         __evt__startselecting: function () { },
7861 
7862         /**
7863          * @event
7864          * @description Select a region is started during a down event
7865          * from a device sending mouse events or by calling
7866          * {@link JXG.Board.startSelectionMode}.
7867          * @name JXG.Board#mousestartselecting
7868          */
7869         __evt__mousestartselecting: function () { },
7870 
7871         /**
7872          * @event
7873          * @description Select a region is started during a down event
7874          * from a device sending pointer events or by calling
7875          * {@link JXG.Board.startSelectionMode}.
7876          * @name JXG.Board#pointerstartselecting
7877          */
7878         __evt__pointerstartselecting: function () { },
7879 
7880         /**
7881          * @event
7882          * @description Select a region is started during a down event
7883          * from a device sending touch events or by calling
7884          * {@link JXG.Board.startSelectionMode}.
7885          * @name JXG.Board#touchstartselecting
7886          */
7887         __evt__touchstartselecting: function () { },
7888 
7889         /**
7890          * @event
7891          * @description Selection of a region is stopped during an up event.
7892          * @name JXG.Board#stopselecting
7893          */
7894         __evt__stopselecting: function () { },
7895 
7896         /**
7897          * @event
7898          * @description Selection of a region is stopped during an up event
7899          * from a device sending mouse events.
7900          * @name JXG.Board#mousestopselecting
7901          */
7902         __evt__mousestopselecting: function () { },
7903 
7904         /**
7905          * @event
7906          * @description Selection of a region is stopped during an up event
7907          * from a device sending pointer events.
7908          * @name JXG.Board#pointerstopselecting
7909          */
7910         __evt__pointerstopselecting: function () { },
7911 
7912         /**
7913          * @event
7914          * @description Selection of a region is stopped during an up event
7915          * from a device sending touch events.
7916          * @name JXG.Board#touchstopselecting
7917          */
7918         __evt__touchstopselecting: function () { },
7919 
7920         /**
7921          * @event
7922          * @description A move event while selecting of a region is active.
7923          * @name JXG.Board#moveselecting
7924          */
7925         __evt__moveselecting: function () { },
7926 
7927         /**
7928          * @event
7929          * @description A move event while selecting of a region is active
7930          * from a device sending mouse events.
7931          * @name JXG.Board#mousemoveselecting
7932          */
7933         __evt__mousemoveselecting: function () { },
7934 
7935         /**
7936          * @event
7937          * @description Select a region is started during a down event
7938          * from a device sending mouse events.
7939          * @name JXG.Board#pointermoveselecting
7940          */
7941         __evt__pointermoveselecting: function () { },
7942 
7943         /**
7944          * @event
7945          * @description Select a region is started during a down event
7946          * from a device sending touch events.
7947          * @name JXG.Board#touchmoveselecting
7948          */
7949         __evt__touchmoveselecting: function () { },
7950 
7951         /**
7952          * @ignore
7953          */
7954         __evt: function () { },
7955 
7956         //endregion
7957 
7958         /**
7959          * Expand the JSXGraph construction to fullscreen.
7960          * In order to preserve the proportions of the JSXGraph element,
7961          * a wrapper div is created which is set to fullscreen.
7962          * This function is called when fullscreen mode is triggered
7963          * <b>and</b> when it is closed.
7964          * <p>
7965          * The wrapping div has the CSS class 'jxgbox_wrap_private' which is
7966          * defined in the file 'jsxgraph.css'
7967          * <p>
7968          * This feature is not available on iPhones (as of December 2021).
7969          *
7970          * @param {String} id (Optional) id of the div element which is brought to fullscreen.
7971          * If not provided, this defaults to the JSXGraph div. However, it may be necessary for the aspect ratio trick
7972          * which using padding-bottom/top and an out div element. Then, the id of the outer div has to be supplied.
7973          *
7974          * @return {JXG.Board} Reference to the board
7975          *
7976          * @example
7977          * <div id='jxgbox' class='jxgbox' style='width:500px; height:200px;'></div>
7978          * <button onClick='board.toFullscreen()'>Fullscreen</button>
7979          *
7980          * <script language='Javascript' type='text/javascript'>
7981          * var board = JXG.JSXGraph.initBoard('jxgbox', {axis:true, boundingbox:[-5,5,5,-5]});
7982          * var p = board.create('point', [0, 1]);
7983          * </script>
7984          *
7985          * </pre><div id='JXGd5bab8b6-fd40-11e8-ab14-901b0e1b8723' class='jxgbox' style='width: 300px; height: 300px;'></div>
7986          * <script type='text/javascript'>
7987          *      var board_d5bab8b6;
7988          *     (function() {
7989          *         var board = JXG.JSXGraph.initBoard('JXGd5bab8b6-fd40-11e8-ab14-901b0e1b8723',
7990          *             {boundingbox:[-5,5,5,-5], axis: true, showcopyright: false, shownavigation: false});
7991          *         var p = board.create('point', [0, 1]);
7992          *         board_d5bab8b6 = board;
7993          *     })();
7994          * </script>
7995          * <button onClick='board_d5bab8b6.toFullscreen()'>Fullscreen</button>
7996          * <pre>
7997          *
7998          * @example
7999          * <div id='outer' style='max-width: 500px; margin: 0 auto;'>
8000          * <div id='jxgbox' class='jxgbox' style='height: 0; padding-bottom: 100%'></div>
8001          * </div>
8002          * <button onClick='board.toFullscreen('outer')'>Fullscreen</button>
8003          *
8004          * <script language='Javascript' type='text/javascript'>
8005          * var board = JXG.JSXGraph.initBoard('jxgbox', {
8006          *     axis:true,
8007          *     boundingbox:[-5,5,5,-5],
8008          *     fullscreen: { id: 'outer' },
8009          *     showFullscreen: true
8010          * });
8011          * var p = board.create('point', [-2, 3], {});
8012          * </script>
8013          *
8014          * </pre><div id='JXG7103f6b_outer' style='max-width: 500px; margin: 0 auto;'>
8015          * <div id='JXG7103f6be-6993-4ff8-8133-c78e50a8afac' class='jxgbox' style='height: 0; padding-bottom: 100%;'></div>
8016          * </div>
8017          * <button onClick='board_JXG7103f6be.toFullscreen('JXG7103f6b_outer')'>Fullscreen</button>
8018          * <script type='text/javascript'>
8019          *     var board_JXG7103f6be;
8020          *     (function() {
8021          *         var board = JXG.JSXGraph.initBoard('JXG7103f6be-6993-4ff8-8133-c78e50a8afac',
8022          *             {boundingbox: [-8, 8, 8,-8], axis: true, fullscreen: { id: 'JXG7103f6b_outer' }, showFullscreen: true,
8023          *              showcopyright: false, shownavigation: false});
8024          *     var p = board.create('point', [-2, 3], {});
8025          *     board_JXG7103f6be = board;
8026          *     })();
8027          *
8028          * </script><pre>
8029          *
8030          *
8031          */
8032         toFullscreen: function (id) {
8033             var wrap_id,
8034                 wrap_node,
8035                 inner_node,
8036                 dim,
8037                 doc = this.document,
8038                 fullscreenElement;
8039 
8040             id = id || this.container;
8041             this._fullscreen_inner_id = id;
8042             inner_node = doc.getElementById(id);
8043             wrap_id = 'fullscreenwrap_' + id;
8044 
8045             if (!Type.exists(inner_node._cssFullscreenStore)) {
8046                 // Store the actual, absolute size of the div
8047                 // This is used in scaleJSXGraphDiv
8048                 dim = this.containerObj.getBoundingClientRect();
8049                 inner_node._cssFullscreenStore = {
8050                     w: dim.width,
8051                     h: dim.height
8052                 };
8053             }
8054 
8055             // Wrap a div around the JSXGraph div.
8056             // It is removed when fullscreen mode is closed.
8057             if (doc.getElementById(wrap_id)) {
8058                 wrap_node = doc.getElementById(wrap_id);
8059             } else {
8060                 wrap_node = document.createElement('div');
8061                 wrap_node.classList.add('JXG_wrap_private');
8062                 wrap_node.setAttribute('id', wrap_id);
8063                 inner_node.parentNode.insertBefore(wrap_node, inner_node);
8064                 wrap_node.appendChild(inner_node);
8065             }
8066 
8067             // Trigger fullscreen mode
8068             wrap_node.requestFullscreen =
8069                 wrap_node.requestFullscreen ||
8070                 wrap_node.webkitRequestFullscreen ||
8071                 wrap_node.mozRequestFullScreen ||
8072                 wrap_node.msRequestFullscreen;
8073 
8074             if (doc.fullscreenElement !== undefined) {
8075                 fullscreenElement = doc.fullscreenElement;
8076             } else if (doc.webkitFullscreenElement !== undefined) {
8077                 fullscreenElement = doc.webkitFullscreenElement;
8078             } else {
8079                 fullscreenElement = doc.msFullscreenElement;
8080             }
8081 
8082             if (fullscreenElement === null) {
8083                 // Start fullscreen mode
8084                 if (wrap_node.requestFullscreen) {
8085                     wrap_node.requestFullscreen();
8086                     this.startFullscreenResizeObserver(wrap_node);
8087                 }
8088             } else {
8089                 this.stopFullscreenResizeObserver(wrap_node);
8090                 if (Type.exists(document.exitFullscreen)) {
8091                     document.exitFullscreen();
8092                 } else if (Type.exists(document.webkitExitFullscreen)) {
8093                     document.webkitExitFullscreen();
8094                 }
8095             }
8096 
8097             return this;
8098         },
8099 
8100         /**
8101          * If fullscreen mode is toggled, the possible CSS transformations
8102          * which are applied to the JSXGraph canvas have to be reread.
8103          * Otherwise the position of upper left corner is wrongly interpreted.
8104          *
8105          * @param  {Object} evt fullscreen event object (unused)
8106          */
8107         fullscreenListener: function (evt) {
8108             var inner_id,
8109                 inner_node,
8110                 fullscreenElement,
8111                 doc = this.document;
8112 
8113             inner_id = this._fullscreen_inner_id;
8114             if (!Type.exists(inner_id)) {
8115                 return;
8116             }
8117 
8118             if (doc.fullscreenElement !== undefined) {
8119                 fullscreenElement = doc.fullscreenElement;
8120             } else if (doc.webkitFullscreenElement !== undefined) {
8121                 fullscreenElement = doc.webkitFullscreenElement;
8122             } else {
8123                 fullscreenElement = doc.msFullscreenElement;
8124             }
8125 
8126             inner_node = doc.getElementById(inner_id);
8127             // If full screen mode is started we have to remove CSS margin around the JSXGraph div.
8128             // Otherwise, the positioning of the fullscreen div will be false.
8129             // When leaving the fullscreen mode, the margin is put back in.
8130             if (fullscreenElement) {
8131                 // Just entered fullscreen mode
8132 
8133                 // Store the original data.
8134                 // Further, the CSS margin has to be removed when in fullscreen mode,
8135                 // and must be restored later.
8136                 //
8137                 // Obsolete:
8138                 // It is used in AbstractRenderer.updateText to restore the scaling matrix
8139                 // which is removed by MathJax.
8140                 inner_node._cssFullscreenStore.id = fullscreenElement.id;
8141                 inner_node._cssFullscreenStore.isFullscreen = true;
8142                 inner_node._cssFullscreenStore.margin = inner_node.style.margin;
8143                 inner_node._cssFullscreenStore.width = inner_node.style.width;
8144                 inner_node._cssFullscreenStore.height = inner_node.style.height;
8145                 inner_node._cssFullscreenStore.transform = inner_node.style.transform;
8146                 // Be sure to replace relative width / height units by absolute units
8147                 inner_node.style.width = inner_node._cssFullscreenStore.w + 'px';
8148                 inner_node.style.height = inner_node._cssFullscreenStore.h + 'px';
8149                 inner_node.style.margin = '';
8150 
8151                 // Do the shifting and scaling via CSS properties
8152                 // We do this after fullscreen mode has been established to get the correct size
8153                 // of the JSXGraph div.
8154                 Env.scaleJSXGraphDiv(fullscreenElement.id, inner_id, doc,
8155                     Type.evaluate(this.attr.fullscreen.scale));
8156 
8157                 // Clear this.doc.fullscreenElement, because Safari doesn't to it and
8158                 // when leaving full screen mode it is still set.
8159                 fullscreenElement = null;
8160             } else if (Type.exists(inner_node._cssFullscreenStore)) {
8161                 // Just left the fullscreen mode
8162 
8163                 inner_node._cssFullscreenStore.isFullscreen = false;
8164                 inner_node.style.margin = inner_node._cssFullscreenStore.margin;
8165                 inner_node.style.width = inner_node._cssFullscreenStore.width;
8166                 inner_node.style.height = inner_node._cssFullscreenStore.height;
8167                 inner_node.style.transform = inner_node._cssFullscreenStore.transform;
8168                 inner_node._cssFullscreenStore = null;
8169 
8170                 // Remove the wrapper div
8171                 inner_node.parentElement.replaceWith(inner_node);
8172             }
8173 
8174             this.updateCSSTransforms();
8175         },
8176 
8177         /**
8178          * Start resize observer to handle
8179          * orientation changes in fullscreen mode.
8180          *
8181          * @param {Object} node DOM object which is in fullscreen mode. It is the wrapper element
8182          * around the JSXGraph div.
8183          * @returns {JXG.Board} Reference to the board
8184          * @private
8185          * @see JXG.Board#toFullscreen
8186          *
8187          */
8188         startFullscreenResizeObserver: function(node) {
8189             var that = this;
8190 
8191             if (!Env.isBrowser || !this.attr.resize || !this.attr.resize.enabled) {
8192                 return this;
8193             }
8194 
8195             this.resizeObserver = new ResizeObserver(function (entries) {
8196                 var inner_id,
8197                     fullscreenElement,
8198                     doc = that.document;
8199 
8200                 if (!that._isResizing) {
8201                     that._isResizing = true;
8202                     window.setTimeout(function () {
8203                         try {
8204                             inner_id = that._fullscreen_inner_id;
8205                             if (doc.fullscreenElement !== undefined) {
8206                                 fullscreenElement = doc.fullscreenElement;
8207                             } else if (doc.webkitFullscreenElement !== undefined) {
8208                                 fullscreenElement = doc.webkitFullscreenElement;
8209                             } else {
8210                                 fullscreenElement = doc.msFullscreenElement;
8211                             }
8212                             if (fullscreenElement !== null) {
8213                                 Env.scaleJSXGraphDiv(fullscreenElement.id, inner_id, doc,
8214                                     Type.evaluate(that.attr.fullscreen.scale));
8215                             }
8216                         } catch (err) {
8217                             that.stopFullscreenResizeObserver(node);
8218                         } finally {
8219                             that._isResizing = false;
8220                         }
8221                     }, that.attr.resize.throttle);
8222                 }
8223             });
8224             this.resizeObserver.observe(node);
8225             return this;
8226         },
8227 
8228         /**
8229          * Remove resize observer to handle orientation changes in fullscreen mode.
8230          * @param {Object} node DOM object which is in fullscreen mode. It is the wrapper element
8231          * around the JSXGraph div.
8232          * @returns {JXG.Board} Reference to the board
8233          * @private
8234          * @see JXG.Board#toFullscreen
8235          */
8236         stopFullscreenResizeObserver: function(node) {
8237             if (!Env.isBrowser || !this.attr.resize || !this.attr.resize.enabled) {
8238                 return this;
8239             }
8240 
8241             if (Type.exists(this.resizeObserver)) {
8242                 this.resizeObserver.unobserve(node);
8243             }
8244             return this;
8245         },
8246 
8247         /**
8248          * Add user activity to the array 'board.userLog'.
8249          *
8250          * @param {String} type Event type, e.g. 'drag'
8251          * @param {Object} obj JSXGraph element object
8252          *
8253          * @see JXG.Board#userLog
8254          * @return {JXG.Board} Reference to the board
8255          */
8256         addLogEntry: function (type, obj, pos) {
8257             var t, id,
8258                 last = this.userLog.length - 1;
8259 
8260             if (Type.exists(obj.elementClass)) {
8261                 id = obj.id;
8262             }
8263             if (Type.evaluate(this.attr.logging.enabled)) {
8264                 t = (new Date()).getTime();
8265                 if (last >= 0 &&
8266                     this.userLog[last].type === type &&
8267                     this.userLog[last].id === id &&
8268                     // Distinguish consecutive drag events of
8269                     // the same element
8270                     t - this.userLog[last].end < 500) {
8271 
8272                     this.userLog[last].end = t;
8273                     this.userLog[last].endpos = pos;
8274                 } else {
8275                     this.userLog.push({
8276                         type: type,
8277                         id: id,
8278                         start: t,
8279                         startpos: pos,
8280                         end: t,
8281                         endpos: pos,
8282                         bbox: this.getBoundingBox(),
8283                         canvas: [this.canvasWidth, this.canvasHeight],
8284                         zoom: [this.zoomX, this.zoomY]
8285                     });
8286                 }
8287             }
8288             return this;
8289         },
8290 
8291         /**
8292          * Function to animate a curve rolling on another curve.
8293          * @param {Curve} c1 JSXGraph curve building the floor where c2 rolls
8294          * @param {Curve} c2 JSXGraph curve which rolls on c1.
8295          * @param {number} start_c1 The parameter t such that c1(t) touches c2. This is the start position of the
8296          *                          rolling process
8297          * @param {Number} stepsize Increase in t in each step for the curve c1
8298          * @param {Number} direction
8299          * @param {Number} time Delay time for setInterval()
8300          * @param {Array} pointlist Array of points which are rolled in each step. This list should contain
8301          *      all points which define c2 and gliders on c2.
8302          *
8303          * @example
8304          *
8305          * // Line which will be the floor to roll upon.
8306          * var line = board.create('curve', [function (t) { return t;}, function (t){ return 1;}], {strokeWidth:6});
8307          * // Center of the rolling circle
8308          * var C = board.create('point',[0,2],{name:'C'});
8309          * // Starting point of the rolling circle
8310          * var P = board.create('point',[0,1],{name:'P', trace:true});
8311          * // Circle defined as a curve. The circle 'starts' at P, i.e. circle(0) = P
8312          * var circle = board.create('curve',[
8313          *           function (t){var d = P.Dist(C),
8314          *                           beta = JXG.Math.Geometry.rad([C.X()+1,C.Y()],C,P);
8315          *                       t += beta;
8316          *                       return C.X()+d*Math.cos(t);
8317          *           },
8318          *           function (t){var d = P.Dist(C),
8319          *                           beta = JXG.Math.Geometry.rad([C.X()+1,C.Y()],C,P);
8320          *                       t += beta;
8321          *                       return C.Y()+d*Math.sin(t);
8322          *           },
8323          *           0,2*Math.PI],
8324          *           {strokeWidth:6, strokeColor:'green'});
8325          *
8326          * // Point on circle
8327          * var B = board.create('glider',[0,2,circle],{name:'B', color:'blue',trace:false});
8328          * var roll = board.createRoulette(line, circle, 0, Math.PI/20, 1, 100, [C,P,B]);
8329          * roll.start() // Start the rolling, to be stopped by roll.stop()
8330          *
8331          * </pre><div class='jxgbox' id='JXGe5e1b53c-a036-4a46-9e35-190d196beca5' style='width: 300px; height: 300px;'></div>
8332          * <script type='text/javascript'>
8333          * var brd = JXG.JSXGraph.initBoard('JXGe5e1b53c-a036-4a46-9e35-190d196beca5', {boundingbox: [-5, 5, 5, -5], axis: true, showcopyright:false, shownavigation: false});
8334          * // Line which will be the floor to roll upon.
8335          * var line = brd.create('curve', [function (t) { return t;}, function (t){ return 1;}], {strokeWidth:6});
8336          * // Center of the rolling circle
8337          * var C = brd.create('point',[0,2],{name:'C'});
8338          * // Starting point of the rolling circle
8339          * var P = brd.create('point',[0,1],{name:'P', trace:true});
8340          * // Circle defined as a curve. The circle 'starts' at P, i.e. circle(0) = P
8341          * var circle = brd.create('curve',[
8342          *           function (t){var d = P.Dist(C),
8343          *                           beta = JXG.Math.Geometry.rad([C.X()+1,C.Y()],C,P);
8344          *                       t += beta;
8345          *                       return C.X()+d*Math.cos(t);
8346          *           },
8347          *           function (t){var d = P.Dist(C),
8348          *                           beta = JXG.Math.Geometry.rad([C.X()+1,C.Y()],C,P);
8349          *                       t += beta;
8350          *                       return C.Y()+d*Math.sin(t);
8351          *           },
8352          *           0,2*Math.PI],
8353          *           {strokeWidth:6, strokeColor:'green'});
8354          *
8355          * // Point on circle
8356          * var B = brd.create('glider',[0,2,circle],{name:'B', color:'blue',trace:false});
8357          * var roll = brd.createRoulette(line, circle, 0, Math.PI/20, 1, 100, [C,P,B]);
8358          * roll.start() // Start the rolling, to be stopped by roll.stop()
8359          * </script><pre>
8360          */
8361         createRoulette: function (c1, c2, start_c1, stepsize, direction, time, pointlist) {
8362             var brd = this,
8363                 Roulette = function () {
8364                     var alpha = 0,
8365                         Tx = 0,
8366                         Ty = 0,
8367                         t1 = start_c1,
8368                         t2 = Numerics.root(
8369                             function (t) {
8370                                 var c1x = c1.X(t1),
8371                                     c1y = c1.Y(t1),
8372                                     c2x = c2.X(t),
8373                                     c2y = c2.Y(t);
8374 
8375                                 return (c1x - c2x) * (c1x - c2x) + (c1y - c2y) * (c1y - c2y);
8376                             },
8377                             [0, Math.PI * 2]
8378                         ),
8379                         t1_new = 0.0,
8380                         t2_new = 0.0,
8381                         c1dist,
8382                         rotation = brd.create(
8383                             'transform',
8384                             [
8385                                 function () {
8386                                     return alpha;
8387                                 }
8388                             ],
8389                             { type: 'rotate' }
8390                         ),
8391                         rotationLocal = brd.create(
8392                             'transform',
8393                             [
8394                                 function () {
8395                                     return alpha;
8396                                 },
8397                                 function () {
8398                                     return c1.X(t1);
8399                                 },
8400                                 function () {
8401                                     return c1.Y(t1);
8402                                 }
8403                             ],
8404                             { type: 'rotate' }
8405                         ),
8406                         translate = brd.create(
8407                             'transform',
8408                             [
8409                                 function () {
8410                                     return Tx;
8411                                 },
8412                                 function () {
8413                                     return Ty;
8414                                 }
8415                             ],
8416                             { type: 'translate' }
8417                         ),
8418                         // arc length via Simpson's rule.
8419                         arclen = function (c, a, b) {
8420                             var cpxa = Numerics.D(c.X)(a),
8421                                 cpya = Numerics.D(c.Y)(a),
8422                                 cpxb = Numerics.D(c.X)(b),
8423                                 cpyb = Numerics.D(c.Y)(b),
8424                                 cpxab = Numerics.D(c.X)((a + b) * 0.5),
8425                                 cpyab = Numerics.D(c.Y)((a + b) * 0.5),
8426                                 fa = Mat.hypot(cpxa, cpya),
8427                                 fb = Mat.hypot(cpxb, cpyb),
8428                                 fab = Mat.hypot(cpxab, cpyab);
8429 
8430                             return ((fa + 4 * fab + fb) * (b - a)) / 6;
8431                         },
8432                         exactDist = function (t) {
8433                             return c1dist - arclen(c2, t2, t);
8434                         },
8435                         beta = Math.PI / 18,
8436                         beta9 = beta * 9,
8437                         interval = null;
8438 
8439                     this.rolling = function () {
8440                         var h, g, hp, gp, z;
8441 
8442                         t1_new = t1 + direction * stepsize;
8443 
8444                         // arc length between c1(t1) and c1(t1_new)
8445                         c1dist = arclen(c1, t1, t1_new);
8446 
8447                         // find t2_new such that arc length between c2(t2) and c1(t2_new) equals c1dist.
8448                         t2_new = Numerics.root(exactDist, t2);
8449 
8450                         // c1(t) as complex number
8451                         h = new Complex(c1.X(t1_new), c1.Y(t1_new));
8452 
8453                         // c2(t) as complex number
8454                         g = new Complex(c2.X(t2_new), c2.Y(t2_new));
8455 
8456                         hp = new Complex(Numerics.D(c1.X)(t1_new), Numerics.D(c1.Y)(t1_new));
8457                         gp = new Complex(Numerics.D(c2.X)(t2_new), Numerics.D(c2.Y)(t2_new));
8458 
8459                         // z is angle between the tangents of c1 at t1_new, and c2 at t2_new
8460                         z = Complex.C.div(hp, gp);
8461 
8462                         alpha = Math.atan2(z.imaginary, z.real);
8463                         // Normalizing the quotient
8464                         z.div(Complex.C.abs(z));
8465                         z.mult(g);
8466                         Tx = h.real - z.real;
8467 
8468                         // T = h(t1_new)-g(t2_new)*h'(t1_new)/g'(t2_new);
8469                         Ty = h.imaginary - z.imaginary;
8470 
8471                         // -(10-90) degrees: make corners roll smoothly
8472                         if (alpha < -beta && alpha > -beta9) {
8473                             alpha = -beta;
8474                             rotationLocal.applyOnce(pointlist);
8475                         } else if (alpha > beta && alpha < beta9) {
8476                             alpha = beta;
8477                             rotationLocal.applyOnce(pointlist);
8478                         } else {
8479                             rotation.applyOnce(pointlist);
8480                             translate.applyOnce(pointlist);
8481                             t1 = t1_new;
8482                             t2 = t2_new;
8483                         }
8484                         brd.update();
8485                     };
8486 
8487                     this.start = function () {
8488                         if (time > 0) {
8489                             interval = window.setInterval(this.rolling, time);
8490                         }
8491                         return this;
8492                     };
8493 
8494                     this.stop = function () {
8495                         window.clearInterval(interval);
8496                         return this;
8497                     };
8498                     return this;
8499                 };
8500             return new Roulette();
8501         }
8502     }
8503 );
8504 
8505 export default JXG.Board;
8506