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