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