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                 froz, e, o, f,
4869                 len = this.objectsList.length;
4870 
4871             for (ob = 0; ob < len; ob++) {
4872                 el = this.objectsList[ob];
4873 
4874                 if (Type.exists(el.coords)) {
4875                     froz = el.evalVisProp('frozen');
4876                     if (froz === 'inherit') {
4877                         // Search if a descendant of 'el' is set to 'frozen'.
4878                         // If yes, set element 'el' as frozen, too.
4879                         for (e in el.descendants/*el.childElements*/) {
4880                             if (el.descendants.hasOwnProperty(e)) {
4881                                 o = el.descendants[e];
4882                                 f = o.evalVisProp('frozen');
4883                                 if (f === true) {
4884                                     froz = true;
4885                                     break;
4886                                 }
4887                             }
4888                         }
4889                     }
4890                     if (froz === true) {
4891                         if (el.is3D) {
4892                             el.element2D.coords.screen2usr();
4893                         } else {
4894                             el.coords.screen2usr();
4895                         }
4896                     } else {
4897                         if (el.is3D) {
4898                             el.element2D.coords.usr2screen();
4899                         } else {
4900                             el.coords.usr2screen();
4901                             if (Type.exists(el.actualCoords)) {
4902                                 el.actualCoords.usr2screen();
4903 
4904                             }
4905                         }
4906                     }
4907                 }
4908             }
4909             return this;
4910         },
4911 
4912         /**
4913          * Moves the origin and initializes an update of all elements.
4914          * @param {Number} x
4915          * @param {Number} y
4916          * @param {Boolean} [diff=false]
4917          * @returns {JXG.Board} Reference to this board.
4918          */
4919         moveOrigin: function (x, y, diff) {
4920             var ox, oy, ul, lr;
4921             if (Type.exists(x) && Type.exists(y)) {
4922                 ox = this.origin.scrCoords[1];
4923                 oy = this.origin.scrCoords[2];
4924 
4925                 this.origin.scrCoords[1] = x;
4926                 this.origin.scrCoords[2] = y;
4927 
4928                 if (diff) {
4929                     this.origin.scrCoords[1] -= this.drag_dx;
4930                     this.origin.scrCoords[2] -= this.drag_dy;
4931                 }
4932 
4933                 ul = new Coords(Const.COORDS_BY_SCREEN, [0, 0], this).usrCoords;
4934                 lr = new Coords(
4935                     Const.COORDS_BY_SCREEN,
4936                     [this.canvasWidth, this.canvasHeight],
4937                     this
4938                 ).usrCoords;
4939                 if (
4940                     ul[1] < this.maxboundingbox[0] - Mat.eps ||
4941                     ul[2] > this.maxboundingbox[1] + Mat.eps ||
4942                     lr[1] > this.maxboundingbox[2] + Mat.eps ||
4943                     lr[2] < this.maxboundingbox[3] - Mat.eps
4944                 ) {
4945                     this.origin.scrCoords[1] = ox;
4946                     this.origin.scrCoords[2] = oy;
4947                 }
4948             }
4949 
4950             this.updateCoords().clearTraces().fullUpdate();
4951             this.triggerEventHandlers(['boundingbox']);
4952 
4953             return this;
4954         },
4955 
4956         /**
4957          * Add conditional updates to the elements.
4958          * @param {String} str String containing conditional update in geonext syntax
4959          */
4960         addConditions: function (str) {
4961             var term,
4962                 m,
4963                 left,
4964                 right,
4965                 name,
4966                 el,
4967                 property,
4968                 functions = [],
4969                 // plaintext = 'var el, x, y, c, rgbo;\n',
4970                 i = str.indexOf('<data>'),
4971                 j = str.indexOf('<' + '/data>'),
4972                 xyFun = function (board, el, f, what) {
4973                     return function () {
4974                         var e, t;
4975 
4976                         e = board.select(el.id);
4977                         t = e.coords.usrCoords[what];
4978 
4979                         if (what === 2) {
4980                             e.setPositionDirectly(Const.COORDS_BY_USER, [f(), t]);
4981                         } else {
4982                             e.setPositionDirectly(Const.COORDS_BY_USER, [t, f()]);
4983                         }
4984                         e.prepareUpdate().update();
4985                     };
4986                 },
4987                 visFun = function (board, el, f) {
4988                     return function () {
4989                         var e, v;
4990 
4991                         e = board.select(el.id);
4992                         v = f();
4993 
4994                         e.setAttribute({ visible: v });
4995                     };
4996                 },
4997                 colFun = function (board, el, f, what) {
4998                     return function () {
4999                         var e, v;
5000 
5001                         e = board.select(el.id);
5002                         v = f();
5003 
5004                         if (what === 'strokewidth') {
5005                             e.visProp.strokewidth = v;
5006                         } else {
5007                             v = Color.rgba2rgbo(v);
5008                             e.visProp[what + 'color'] = v[0];
5009                             e.visProp[what + 'opacity'] = v[1];
5010                         }
5011                     };
5012                 },
5013                 posFun = function (board, el, f) {
5014                     return function () {
5015                         var e = board.select(el.id);
5016 
5017                         e.position = f();
5018                     };
5019                 },
5020                 styleFun = function (board, el, f) {
5021                     return function () {
5022                         var e = board.select(el.id);
5023 
5024                         e.setStyle(f());
5025                     };
5026                 };
5027 
5028             if (i < 0) {
5029                 return;
5030             }
5031 
5032             while (i >= 0) {
5033                 term = str.slice(i + 6, j); // throw away <data>
5034                 m = term.indexOf('=');
5035                 left = term.slice(0, m);
5036                 right = term.slice(m + 1);
5037                 m = left.indexOf('.');   // Resulting variable names must not contain dots, e.g. ' Steuern akt.'
5038                 name = left.slice(0, m); //.replace(/\s+$/,''); // do NOT cut out name (with whitespace)
5039                 el = this.elementsByName[Type.unescapeHTML(name)];
5040 
5041                 property = left
5042                     .slice(m + 1)
5043                     .replace(/\s+/g, '')
5044                     .toLowerCase(); // remove whitespace in property
5045                 right = Type.createFunction(right, this, '', true);
5046 
5047                 // Debug
5048                 if (!Type.exists(this.elementsByName[name])) {
5049                     JXG.debug('debug conditions: |' + name + '| undefined');
5050                 } else {
5051                     // plaintext += 'el = this.objects[\'' + el.id + '\'];\n';
5052 
5053                     switch (property) {
5054                         case 'x':
5055                             functions.push(xyFun(this, el, right, 2));
5056                             break;
5057                         case 'y':
5058                             functions.push(xyFun(this, el, right, 1));
5059                             break;
5060                         case 'visible':
5061                             functions.push(visFun(this, el, right));
5062                             break;
5063                         case 'position':
5064                             functions.push(posFun(this, el, right));
5065                             break;
5066                         case 'stroke':
5067                             functions.push(colFun(this, el, right, 'stroke'));
5068                             break;
5069                         case 'style':
5070                             functions.push(styleFun(this, el, right));
5071                             break;
5072                         case 'strokewidth':
5073                             functions.push(colFun(this, el, right, 'strokewidth'));
5074                             break;
5075                         case 'fill':
5076                             functions.push(colFun(this, el, right, 'fill'));
5077                             break;
5078                         case 'label':
5079                             break;
5080                         default:
5081                             JXG.debug(
5082                                 'property "' +
5083                                 property +
5084                                 '" in conditions not yet implemented:' +
5085                                 right
5086                             );
5087                             break;
5088                     }
5089                 }
5090                 str = str.slice(j + 7); // cut off '</data>'
5091                 i = str.indexOf('<data>');
5092                 j = str.indexOf('<' + '/data>');
5093             }
5094 
5095             this.updateConditions = function () {
5096                 var i;
5097 
5098                 for (i = 0; i < functions.length; i++) {
5099                     functions[i]();
5100                 }
5101 
5102                 this.prepareUpdate().updateElements();
5103                 return true;
5104             };
5105             this.updateConditions();
5106         },
5107 
5108         /**
5109          * Computes the commands in the conditions-section of the gxt file.
5110          * It is evaluated after an update, before the unsuspendRedraw.
5111          * The function is generated in
5112          * @see JXG.Board#addConditions
5113          * @private
5114          */
5115         updateConditions: function () {
5116             return false;
5117         },
5118 
5119         /**
5120          * Calculates adequate snap sizes.
5121          * @returns {JXG.Board} Reference to the board.
5122          */
5123         calculateSnapSizes: function () {
5124             var p1, p2,
5125                 bbox = this.getBoundingBox(),
5126                 gridStep = Type.evaluate(this.options.grid.majorStep),
5127                 gridX = Type.evaluate(this.options.grid.gridX),
5128                 gridY = Type.evaluate(this.options.grid.gridY),
5129                 x, y;
5130 
5131             if (!Type.isArray(gridStep)) {
5132                 gridStep = [gridStep, gridStep];
5133             }
5134             if (gridStep.length < 2) {
5135                 gridStep = [gridStep[0], gridStep[0]];
5136             }
5137             if (Type.exists(gridX)) {
5138                 gridStep[0] = gridX;
5139             }
5140             if (Type.exists(gridY)) {
5141                 gridStep[1] = gridY;
5142             }
5143 
5144             if (gridStep[0] === 'auto') {
5145                 gridStep[0] = 1;
5146             } else {
5147                 gridStep[0] = Type.parseNumber(gridStep[0], Math.abs(bbox[1] - bbox[3]), 1 / this.unitX);
5148             }
5149             if (gridStep[1] === 'auto') {
5150                 gridStep[1] = 1;
5151             } else {
5152                 gridStep[1] = Type.parseNumber(gridStep[1], Math.abs(bbox[0] - bbox[2]), 1 / this.unitY);
5153             }
5154 
5155             p1 = new Coords(Const.COORDS_BY_USER, [0, 0], this);
5156             p2 = new Coords(
5157                 Const.COORDS_BY_USER,
5158                 [gridStep[0], gridStep[1]],
5159                 this
5160             );
5161             x = p1.scrCoords[1] - p2.scrCoords[1];
5162             y = p1.scrCoords[2] - p2.scrCoords[2];
5163 
5164             this.options.grid.snapSizeX = gridStep[0];
5165             while (Math.abs(x) > 25) {
5166                 this.options.grid.snapSizeX *= 2;
5167                 x /= 2;
5168             }
5169 
5170             this.options.grid.snapSizeY = gridStep[1];
5171             while (Math.abs(y) > 25) {
5172                 this.options.grid.snapSizeY *= 2;
5173                 y /= 2;
5174             }
5175 
5176             return this;
5177         },
5178 
5179         /**
5180          * Apply update on all objects with the new zoom-factors. Clears all traces.
5181          * @returns {JXG.Board} Reference to the board.
5182          */
5183         applyZoom: function () {
5184             this.updateCoords().calculateSnapSizes().clearTraces().fullUpdate();
5185 
5186             return this;
5187         },
5188 
5189         /**
5190          * Zooms into the board by the factors board.attr.zoom.factorX and board.attr.zoom.factorY and applies the zoom.
5191          * The zoom operation is centered at x, y.
5192          * @param {Number} [x]
5193          * @param {Number} [y]
5194          * @returns {JXG.Board} Reference to the board
5195          */
5196         zoomIn: function (x, y) {
5197             var bb = this.getBoundingBox(),
5198                 zX = Type.evaluate(this.attr.zoom.factorx),
5199                 zY =  Type.evaluate(this.attr.zoom.factory),
5200                 dX = (bb[2] - bb[0]) * (1.0 - 1.0 / zX),
5201                 dY = (bb[1] - bb[3]) * (1.0 - 1.0 / zY),
5202                 lr = 0.5,
5203                 tr = 0.5,
5204                 ma = Type.evaluate(this.attr.zoom.max),
5205                 mi =  Type.evaluate(this.attr.zoom.eps) || Type.evaluate(this.attr.zoom.min) || 0.001; // this.attr.zoom.eps is deprecated
5206 
5207             if (
5208                 (this.zoomX > ma && zX > 1.0) ||
5209                 (this.zoomY > ma && zY > 1.0) ||
5210                 (this.zoomX < mi && zX < 1.0) || // zoomIn is used for all zooms on touch devices
5211                 (this.zoomY < mi && zY < 1.0)
5212             ) {
5213                 return this;
5214             }
5215 
5216             if (Type.isNumber(x) && Type.isNumber(y)) {
5217                 lr = (x - bb[0]) / (bb[2] - bb[0]);
5218                 tr = (bb[1] - y) / (bb[1] - bb[3]);
5219             }
5220 
5221             this.setBoundingBox(
5222                 [
5223                     bb[0] + dX * lr,
5224                     bb[1] - dY * tr,
5225                     bb[2] - dX * (1 - lr),
5226                     bb[3] + dY * (1 - tr)
5227                 ],
5228                 this.keepaspectratio,
5229                 'update'
5230             );
5231             return this.applyZoom();
5232         },
5233 
5234         /**
5235          * Zooms out of the board by the factors board.attr.zoom.factorX and board.attr.zoom.factorY and applies the zoom.
5236          * The zoom operation is centered at x, y.
5237          *
5238          * @param {Number} [x]
5239          * @param {Number} [y]
5240          * @returns {JXG.Board} Reference to the board
5241          */
5242         zoomOut: function (x, y) {
5243             var bb = this.getBoundingBox(),
5244                 zX = Type.evaluate(this.attr.zoom.factorx),
5245                 zY = Type.evaluate(this.attr.zoom.factory),
5246                 dX = (bb[2] - bb[0]) * (1.0 - zX),
5247                 dY = (bb[1] - bb[3]) * (1.0 - zY),
5248                 lr = 0.5,
5249                 tr = 0.5,
5250                 mi = Type.evaluate(this.attr.zoom.eps) || Type.evaluate(this.attr.zoom.min) || 0.001; // this.attr.zoom.eps is deprecated
5251 
5252             if (this.zoomX < mi || this.zoomY < mi) {
5253                 return this;
5254             }
5255 
5256             if (Type.isNumber(x) && Type.isNumber(y)) {
5257                 lr = (x - bb[0]) / (bb[2] - bb[0]);
5258                 tr = (bb[1] - y) / (bb[1] - bb[3]);
5259             }
5260 
5261             this.setBoundingBox(
5262                 [
5263                     bb[0] + dX * lr,
5264                     bb[1] - dY * tr,
5265                     bb[2] - dX * (1 - lr),
5266                     bb[3] + dY * (1 - tr)
5267                 ],
5268                 this.keepaspectratio,
5269                 'update'
5270             );
5271 
5272             return this.applyZoom();
5273         },
5274 
5275         /**
5276          * Reset the zoom level to the original zoom level from initBoard();
5277          * Additionally, if the board as been initialized with a boundingBox (which is the default),
5278          * restore the viewport to the original viewport during initialization. Otherwise,
5279          * (i.e. if the board as been initialized with unitX/Y and originX/Y),
5280          * just set the zoom level to 100%.
5281          *
5282          * @returns {JXG.Board} Reference to the board
5283          */
5284         zoom100: function () {
5285             var bb, dX, dY;
5286 
5287             if (Type.exists(this.attr.boundingbox)) {
5288                 this.setBoundingBox(this.attr.boundingbox, this.keepaspectratio, 'reset');
5289             } else {
5290                 // Board has been set up with unitX/Y and originX/Y
5291                 bb = this.getBoundingBox();
5292                 dX = (bb[2] - bb[0]) * (1.0 - this.zoomX) * 0.5;
5293                 dY = (bb[1] - bb[3]) * (1.0 - this.zoomY) * 0.5;
5294                 this.setBoundingBox(
5295                     [bb[0] + dX, bb[1] - dY, bb[2] - dX, bb[3] + dY],
5296                     this.keepaspectratio,
5297                     'reset'
5298                 );
5299             }
5300             return this.applyZoom();
5301         },
5302 
5303         /**
5304          * Zooms the board so every visible point is shown. Keeps aspect ratio.
5305          * @returns {JXG.Board} Reference to the board
5306          */
5307         zoomAllPoints: function () {
5308             var el,
5309                 border,
5310                 borderX,
5311                 borderY,
5312                 pEl,
5313                 minX = 0,
5314                 maxX = 0,
5315                 minY = 0,
5316                 maxY = 0,
5317                 len = this.objectsList.length;
5318 
5319             for (el = 0; el < len; el++) {
5320                 pEl = this.objectsList[el];
5321 
5322                 if (Type.isPoint(pEl) && pEl.visPropCalc.visible) {
5323                     if (pEl.coords.usrCoords[1] < minX) {
5324                         minX = pEl.coords.usrCoords[1];
5325                     } else if (pEl.coords.usrCoords[1] > maxX) {
5326                         maxX = pEl.coords.usrCoords[1];
5327                     }
5328                     if (pEl.coords.usrCoords[2] > maxY) {
5329                         maxY = pEl.coords.usrCoords[2];
5330                     } else if (pEl.coords.usrCoords[2] < minY) {
5331                         minY = pEl.coords.usrCoords[2];
5332                     }
5333                 }
5334             }
5335 
5336             border = 50;
5337             borderX = border / this.unitX;
5338             borderY = border / this.unitY;
5339 
5340             this.setBoundingBox(
5341                 [minX - borderX, maxY + borderY, maxX + borderX, minY - borderY],
5342                 this.keepaspectratio,
5343                 'update'
5344             );
5345 
5346             return this.applyZoom();
5347         },
5348 
5349         /**
5350          * Reset the bounding box and the zoom level to 100% such that a given set of elements is
5351          * within the board's viewport.
5352          * @param {Array} elements A set of elements given by id, reference, or name.
5353          * @returns {JXG.Board} Reference to the board.
5354          */
5355         zoomElements: function (elements) {
5356             var i, e,
5357                 box,
5358                 newBBox = [Infinity, -Infinity, -Infinity, Infinity],
5359                 cx, cy,
5360                 dx, dy,
5361                 d;
5362 
5363             if (!Type.isArray(elements) || elements.length === 0) {
5364                 return this;
5365             }
5366 
5367             for (i = 0; i < elements.length; i++) {
5368                 e = this.select(elements[i]);
5369 
5370                 box = e.bounds();
5371                 if (Type.isArray(box)) {
5372                     if (box[0] < newBBox[0]) {
5373                         newBBox[0] = box[0];
5374                     }
5375                     if (box[1] > newBBox[1]) {
5376                         newBBox[1] = box[1];
5377                     }
5378                     if (box[2] > newBBox[2]) {
5379                         newBBox[2] = box[2];
5380                     }
5381                     if (box[3] < newBBox[3]) {
5382                         newBBox[3] = box[3];
5383                     }
5384                 }
5385             }
5386 
5387             if (Type.isArray(newBBox)) {
5388                 cx = 0.5 * (newBBox[0] + newBBox[2]);
5389                 cy = 0.5 * (newBBox[1] + newBBox[3]);
5390                 dx = 1.5 * (newBBox[2] - newBBox[0]) * 0.5;
5391                 dy = 1.5 * (newBBox[1] - newBBox[3]) * 0.5;
5392                 d = Math.max(dx, dy);
5393                 this.setBoundingBox(
5394                     [cx - d, cy + d, cx + d, cy - d],
5395                     this.keepaspectratio,
5396                     'update'
5397                 );
5398             }
5399 
5400             return this;
5401         },
5402 
5403         /**
5404          * Sets the zoom level to <tt>fX</tt> resp <tt>fY</tt>.
5405          * @param {Number} fX
5406          * @param {Number} fY
5407          * @returns {JXG.Board} Reference to the board.
5408          */
5409         setZoom: function (fX, fY) {
5410             var oX = this.attr.zoom.factorx,
5411                 oY = this.attr.zoom.factory;
5412 
5413             this.attr.zoom.factorx = fX / this.zoomX;
5414             this.attr.zoom.factory = fY / this.zoomY;
5415 
5416             this.zoomIn();
5417 
5418             this.attr.zoom.factorx = oX;
5419             this.attr.zoom.factory = oY;
5420 
5421             return this;
5422         },
5423 
5424         /**
5425          * Inner, recursive method of removeObject.
5426          *
5427          * @param {JXG.GeometryElement|Array} object The object to remove or array of objects to be removed.
5428          * The element(s) is/are given by name, id or a reference.
5429          * @param {Boolean} [saveMethod=false] If saveMethod=true, the algorithm runs through all elements
5430          * and tests if the element to be deleted is a child element. If this is the case, it will be
5431          * removed from the list of child elements. If saveMethod=false (default), the element
5432          * is removed from the lists of child elements of all its ancestors.
5433          * The latter should be much faster.
5434          * @returns {JXG.Board} Reference to the board
5435          * @private
5436          */
5437         _removeObj: function (object, saveMethod) {
5438             var el, i;
5439 
5440             if (Type.isArray(object)) {
5441                 for (i = 0; i < object.length; i++) {
5442                     this._removeObj(object[i], saveMethod);
5443                 }
5444 
5445                 return this;
5446             }
5447 
5448             object = this.select(object);
5449 
5450             // If the object which is about to be removed is unknown or a string, do nothing.
5451             // it is a string if a string was given and could not be resolved to an element.
5452             if (!Type.exists(object) || Type.isString(object)) {
5453                 return this;
5454             }
5455 
5456             try {
5457                 // remove all children.
5458                 for (el in object.childElements) {
5459                     if (object.childElements.hasOwnProperty(el)) {
5460                         object.childElements[el].board._removeObj(object.childElements[el]);
5461                     }
5462                 }
5463 
5464                 // Remove all children in elements like turtle
5465                 for (el in object.objects) {
5466                     if (object.objects.hasOwnProperty(el)) {
5467                         object.objects[el].board._removeObj(object.objects[el]);
5468                     }
5469                 }
5470 
5471                 // Remove the element from the childElement list and the descendant list of all elements.
5472                 if (saveMethod) {
5473                     // Running through all objects has quadratic complexity if many objects are deleted.
5474                     for (el in this.objects) {
5475                         if (this.objects.hasOwnProperty(el)) {
5476                             if (
5477                                 Type.exists(this.objects[el].childElements) &&
5478                                 Type.exists(
5479                                     this.objects[el].childElements.hasOwnProperty(object.id)
5480                                 )
5481                             ) {
5482                                 delete this.objects[el].childElements[object.id];
5483                                 delete this.objects[el].descendants[object.id];
5484                             }
5485                         }
5486                     }
5487                 } else if (Type.exists(object.ancestors)) {
5488                     // Running through the ancestors should be much more efficient.
5489                     for (el in object.ancestors) {
5490                         if (object.ancestors.hasOwnProperty(el)) {
5491                             if (
5492                                 Type.exists(object.ancestors[el].childElements) &&
5493                                 Type.exists(
5494                                     object.ancestors[el].childElements.hasOwnProperty(object.id)
5495                                 )
5496                             ) {
5497                                 delete object.ancestors[el].childElements[object.id];
5498                                 delete object.ancestors[el].descendants[object.id];
5499                             }
5500                         }
5501                     }
5502                 }
5503 
5504                 // remove the object itself from our control structures
5505                 if (object._pos > -1) {
5506                     this.objectsList.splice(object._pos, 1);
5507                     for (i = object._pos; i < this.objectsList.length; i++) {
5508                         this.objectsList[i]._pos--;
5509                     }
5510                 } else if (object.type !== Const.OBJECT_TYPE_TURTLE) {
5511                     JXG.debug(
5512                         'Board.removeObject: object ' + object.id + ' not found in list.'
5513                     );
5514                 }
5515 
5516                 delete this.objects[object.id];
5517                 delete this.elementsByName[object.name];
5518 
5519                 if (object.visProp && object.evalVisProp('trace')) {
5520                     object.clearTrace();
5521                 }
5522 
5523                 // the object deletion itself is handled by the object.
5524                 if (Type.exists(object.remove)) {
5525                     object.remove();
5526                 }
5527             } catch (e) {
5528                 JXG.debug(object.id + ': Could not be removed: ' + e);
5529             }
5530 
5531             return this;
5532         },
5533 
5534         /**
5535          * Removes object from board and renderer.
5536          * <p>
5537          * <b>Performance hints:</b> It is recommended to use the object's id.
5538          * If many elements are removed, it is best to call <tt>board.suspendUpdate()</tt>
5539          * before looping through the elements to be removed and call
5540          * <tt>board.unsuspendUpdate()</tt> after the loop. Further, it is advisable to loop
5541          * in reverse order, i.e. remove the object in reverse order of their creation time.
5542          * @param {JXG.GeometryElement|Array} object The object to remove or array of objects to be removed.
5543          * The element(s) is/are given by name, id or a reference.
5544          * @param {Boolean} saveMethod If true, the algorithm runs through all elements
5545          * and tests if the element to be deleted is a child element. If yes, it will be
5546          * removed from the list of child elements. If false (default), the element
5547          * is removed from the lists of child elements of all its ancestors.
5548          * This should be much faster.
5549          * @returns {JXG.Board} Reference to the board
5550          */
5551         removeObject: function (object, saveMethod) {
5552             var i;
5553 
5554             this.renderer.suspendRedraw(this);
5555             if (Type.isArray(object)) {
5556                 for (i = 0; i < object.length; i++) {
5557                     this._removeObj(object[i], saveMethod);
5558                 }
5559             } else {
5560                 this._removeObj(object, saveMethod);
5561             }
5562             this.renderer.unsuspendRedraw();
5563 
5564             this.update();
5565             return this;
5566         },
5567 
5568         /**
5569          * Removes the ancestors of an object an the object itself from board and renderer.
5570          * @param {JXG.GeometryElement} object The object to remove.
5571          * @returns {JXG.Board} Reference to the board
5572          */
5573         removeAncestors: function (object) {
5574             var anc;
5575 
5576             for (anc in object.ancestors) {
5577                 if (object.ancestors.hasOwnProperty(anc)) {
5578                     this.removeAncestors(object.ancestors[anc]);
5579                 }
5580             }
5581 
5582             this.removeObject(object);
5583 
5584             return this;
5585         },
5586 
5587         /**
5588          * Initialize some objects which are contained in every GEONExT construction by default,
5589          * but are not contained in the gxt files.
5590          * @returns {JXG.Board} Reference to the board
5591          */
5592         initGeonextBoard: function () {
5593             var p1, p2, p3;
5594 
5595             p1 = this.create('point', [0, 0], {
5596                 id: this.id + 'g00e0',
5597                 name: 'Ursprung',
5598                 withLabel: false,
5599                 visible: false,
5600                 fixed: true
5601             });
5602 
5603             p2 = this.create('point', [1, 0], {
5604                 id: this.id + 'gX0e0',
5605                 name: 'Punkt_1_0',
5606                 withLabel: false,
5607                 visible: false,
5608                 fixed: true
5609             });
5610 
5611             p3 = this.create('point', [0, 1], {
5612                 id: this.id + 'gY0e0',
5613                 name: 'Punkt_0_1',
5614                 withLabel: false,
5615                 visible: false,
5616                 fixed: true
5617             });
5618 
5619             this.create('line', [p1, p2], {
5620                 id: this.id + 'gXLe0',
5621                 name: 'X-Achse',
5622                 withLabel: false,
5623                 visible: false
5624             });
5625 
5626             this.create('line', [p1, p3], {
5627                 id: this.id + 'gYLe0',
5628                 name: 'Y-Achse',
5629                 withLabel: false,
5630                 visible: false
5631             });
5632 
5633             return this;
5634         },
5635 
5636         /**
5637          * Change the height and width of the board's container.
5638          * After doing so, {@link JXG.JSXGraph.setBoundingBox} is called using
5639          * the actual size of the bounding box and the actual value of keepaspectratio.
5640          * If setBoundingbox() should not be called automatically,
5641          * call resizeContainer with dontSetBoundingBox == true.
5642          * @param {Number} canvasWidth New width of the container.
5643          * @param {Number} canvasHeight New height of the container.
5644          * @param {Boolean} [dontset=false] If true do not set the CSS width and height of the DOM element.
5645          * @param {Boolean} [dontSetBoundingBox=false] If true do not call setBoundingBox(), but keep view centered around original visible center.
5646          * @returns {JXG.Board} Reference to the board
5647          */
5648         resizeContainer: function (canvasWidth, canvasHeight, dontset, dontSetBoundingBox) {
5649             var box,
5650                 oldWidth, oldHeight,
5651                 oX, oY;
5652 
5653             oldWidth = this.canvasWidth;
5654             oldHeight = this.canvasHeight;
5655 
5656             if (!dontSetBoundingBox) {
5657                 box = this.getBoundingBox();    // This is the actual bounding box.
5658             }
5659 
5660             // this.canvasWidth = Math.max(parseFloat(canvasWidth), Mat.eps);
5661             // this.canvasHeight = Math.max(parseFloat(canvasHeight), Mat.eps);
5662             this.canvasWidth = parseFloat(canvasWidth);
5663             this.canvasHeight = parseFloat(canvasHeight);
5664 
5665             if (!dontset) {
5666                 this.containerObj.style.width = this.canvasWidth + 'px';
5667                 this.containerObj.style.height = this.canvasHeight + 'px';
5668             }
5669             this.renderer.resize(this.canvasWidth, this.canvasHeight);
5670 
5671             if (!dontSetBoundingBox) {
5672                 this.setBoundingBox(box, this.keepaspectratio, 'keep');
5673             } else {
5674                 oX = (this.canvasWidth - oldWidth) * 0.5;
5675                 oY = (this.canvasHeight - oldHeight) * 0.5;
5676 
5677                 this.moveOrigin(
5678                     this.origin.scrCoords[1] + oX,
5679                     this.origin.scrCoords[2] + oY
5680                 );
5681             }
5682 
5683             return this;
5684         },
5685 
5686         /**
5687          * Lists the dependencies graph in a new HTML-window.
5688          * @returns {JXG.Board} Reference to the board
5689          */
5690         showDependencies: function () {
5691             var el, t, c, f, i;
5692 
5693             t = '<p>\n';
5694             for (el in this.objects) {
5695                 if (this.objects.hasOwnProperty(el)) {
5696                     i = 0;
5697                     for (c in this.objects[el].childElements) {
5698                         if (this.objects[el].childElements.hasOwnProperty(c)) {
5699                             i += 1;
5700                         }
5701                     }
5702                     if (i >= 0) {
5703                         t += '<strong>' + this.objects[el].id + ':<' + '/strong> ';
5704                     }
5705 
5706                     for (c in this.objects[el].childElements) {
5707                         if (this.objects[el].childElements.hasOwnProperty(c)) {
5708                             t +=
5709                                 this.objects[el].childElements[c].id +
5710                                 '(' +
5711                                 this.objects[el].childElements[c].name +
5712                                 ')' +
5713                                 ', ';
5714                         }
5715                     }
5716                     t += '<p>\n';
5717                 }
5718             }
5719             t += '<' + '/p>\n';
5720             f = window.open();
5721             f.document.open();
5722             f.document.write(t);
5723             f.document.close();
5724             return this;
5725         },
5726 
5727         /**
5728          * Lists the XML code of the construction in a new HTML-window.
5729          * @returns {JXG.Board} Reference to the board
5730          */
5731         showXML: function () {
5732             var f = window.open('');
5733             f.document.open();
5734             f.document.write('<pre>' + Type.escapeHTML(this.xmlString) + '<' + '/pre>');
5735             f.document.close();
5736             return this;
5737         },
5738 
5739         /**
5740          * Sets for all objects the needsUpdate flag to 'true'.
5741          * @param{JXG.GeometryElement} [drag=undefined] Optional element that is dragged.
5742          * @returns {JXG.Board} Reference to the board
5743          */
5744         prepareUpdate: function (drag) {
5745             var el, i,
5746                 pEl,
5747                 len = this.objectsList.length;
5748 
5749             /*
5750             if (this.attr.updatetype === 'hierarchical') {
5751                 return this;
5752             }
5753             */
5754 
5755             for (el = 0; el < len; el++) {
5756                 pEl = this.objectsList[el];
5757                 if (this._change3DView ||
5758                     (Type.exists(drag) && drag.elType === 'view3d_slider')
5759                 ) {
5760                     // The 3D view has changed. No elements are recomputed,
5761                     // only 3D elements are projected to the new view.
5762                     pEl.needsUpdate =
5763                         pEl.visProp.element3d ||
5764                         pEl.elType === 'view3d' ||
5765                         pEl.elType === 'view3d_slider' ||
5766                         this.needsFullUpdate;
5767 
5768                     // Special case sphere3d in central projection:
5769                     // We have to update the defining points of the ellipse
5770                     if (pEl.visProp.element3d &&
5771                         pEl.visProp.element3d.type === Const.OBJECT_TYPE_SPHERE3D
5772                         ) {
5773                         for (i = 0; i < pEl.parents.length; i++) {
5774                             this.objects[pEl.parents[i]].needsUpdate = true;
5775                         }
5776                     }
5777                 } else {
5778                     pEl.needsUpdate = pEl.needsRegularUpdate || this.needsFullUpdate;
5779                 }
5780             }
5781 
5782             for (el in this.groups) {
5783                 if (this.groups.hasOwnProperty(el)) {
5784                     pEl = this.groups[el];
5785                     pEl.needsUpdate = pEl.needsRegularUpdate || this.needsFullUpdate;
5786                 }
5787             }
5788 
5789             return this;
5790         },
5791 
5792         /**
5793          * Runs through all elements and calls their update() method.
5794          * @param {JXG.GeometryElement} drag Element that caused the update.
5795          * @returns {JXG.Board} Reference to the board
5796          */
5797         updateElements: function (drag) {
5798             var el, pEl;
5799             //var childId, i = 0;
5800 
5801             drag = this.select(drag);
5802 
5803             /*
5804             if (Type.exists(drag)) {
5805                 for (el = 0; el < this.objectsList.length; el++) {
5806                     pEl = this.objectsList[el];
5807                     if (pEl.id === drag.id) {
5808                         i = el;
5809                         break;
5810                     }
5811                 }
5812             }
5813             */
5814             for (el = 0; el < this.objectsList.length; el++) {
5815                 pEl = this.objectsList[el];
5816                 if (this.needsFullUpdate && pEl.elementClass === Const.OBJECT_CLASS_TEXT) {
5817                     pEl.updateSize();
5818                 }
5819 
5820                 // For updates of an element we distinguish if the dragged element is updated or
5821                 // other elements are updated.
5822                 // The difference lies in the treatment of gliders and points based on transformations.
5823                 pEl.update(!Type.exists(drag) || pEl.id !== drag.id).updateVisibility();
5824             }
5825 
5826             // update groups last
5827             for (el in this.groups) {
5828                 if (this.groups.hasOwnProperty(el)) {
5829                     this.groups[el].update(drag);
5830                 }
5831             }
5832 
5833             return this;
5834         },
5835 
5836         /**
5837          * Runs through all elements and calls their update() method.
5838          * @returns {JXG.Board} Reference to the board
5839          */
5840         updateRenderer: function () {
5841             var el,
5842                 len = this.objectsList.length,
5843                 autoPositionLabelList = [],
5844                 currentIndex, randomIndex;
5845 
5846             if (!this.renderer) {
5847                 return;
5848             }
5849 
5850             /*
5851             objs = this.objectsList.slice(0);
5852             objs.sort(function (a, b) {
5853                 if (a.visProp.layer < b.visProp.layer) {
5854                     return -1;
5855                 } else if (a.visProp.layer === b.visProp.layer) {
5856                     return b.lastDragTime.getTime() - a.lastDragTime.getTime();
5857                 } else {
5858                     return 1;
5859                 }
5860             });
5861             */
5862 
5863             if (this.renderer.type === 'canvas') {
5864                 this.updateRendererCanvas();
5865             } else {
5866                 for (el = 0; el < len; el++) {
5867                     if (this.objectsList[el].visProp.islabel && this.objectsList[el].visProp.autoposition) {
5868                         autoPositionLabelList.push(el);
5869                     } else {
5870                     this.objectsList[el].updateRenderer();
5871                 }
5872             }
5873 
5874                 currentIndex = autoPositionLabelList.length;
5875 
5876                 // Randomize the order of the labels
5877                 while (currentIndex !== 0) {
5878                     randomIndex = Math.floor(Math.random() * currentIndex);
5879                     currentIndex--;
5880                     [autoPositionLabelList[currentIndex], autoPositionLabelList[randomIndex]] = [autoPositionLabelList[randomIndex], autoPositionLabelList[currentIndex]];
5881                 }
5882 
5883                 for (el = 0; el < autoPositionLabelList.length; el++) {
5884                     this.objectsList[autoPositionLabelList[el]].updateRenderer();
5885                 }
5886                 /*
5887                 for (el = autoPositionLabelList.length - 1; el >= 0; el--) {
5888                     this.objectsList[autoPositionLabelList[el]].updateRenderer();
5889                 }
5890                 */
5891             }
5892             return this;
5893         },
5894 
5895         /**
5896          * Runs through all elements and calls their update() method.
5897          * This is a special version for the CanvasRenderer.
5898          * Here, we have to do our own layer handling.
5899          * @returns {JXG.Board} Reference to the board
5900          */
5901         updateRendererCanvas: function () {
5902             var el, pEl,
5903                 olen = this.objectsList.length,
5904                 // i, minim, lay,
5905                 // layers = this.options.layer,
5906                 // len = this.options.layer.numlayers,
5907                 // last = Number.NEGATIVE_INFINITY.toExponential,
5908                 depth_order_layers = [],
5909                 objects_sorted,
5910                 // Sort the elements for the canvas rendering according to
5911                 // their layer, _pos, depthOrder (with this priority)
5912                 // @private
5913                 _compareFn = function(a, b) {
5914                     if (a.visProp.layer !== b.visProp.layer) {
5915                         return a.visProp.layer - b.visProp.layer;
5916                     }
5917 
5918                     // The objects are in the same layer, but the layer is not depth ordered
5919                     if (depth_order_layers.indexOf(a.visProp.layer) === -1) {
5920                         return a._pos - b._pos;
5921                     }
5922 
5923                     // The objects are in the same layer and the layer is depth ordered
5924                     // We have to sort 2D elements according to the zIndices of
5925                     // their 3D parents.
5926                     if (!a.visProp.element3d && !b.visProp.element3d) {
5927                         return a._pos - b._pos;
5928                     }
5929 
5930                     if (a.visProp.element3d && !b.visProp.element3d) {
5931                         return -1;
5932                     }
5933 
5934                     if (b.visProp.element3d && !a.visProp.element3d) {
5935                         return 1;
5936                     }
5937 
5938                     return a.visProp.element3d.zIndex - b.visProp.element3d.zIndex;
5939                 };
5940 
5941             // Only one view3d element is supported. Get the depth orderer layers and
5942             // update the zIndices of the 3D elements.
5943             for (el = 0; el < olen; el++) {
5944                 pEl = this.objectsList[el];
5945                 if (pEl.elType === 'view3d' && pEl.evalVisProp('depthorder.enabled')) {
5946                     depth_order_layers = pEl.evalVisProp('depthorder.layers');
5947                     pEl.updateRenderer();
5948                     break;
5949                 }
5950             }
5951 
5952             objects_sorted = this.objectsList.toSorted(_compareFn);
5953             olen = objects_sorted.length;
5954             for (el = 0; el < olen; el++) {
5955                 objects_sorted[el].prepareUpdate().updateRenderer();
5956             }
5957 
5958             // for (i = 0; i < len; i++) {
5959             //     minim = Number.POSITIVE_INFINITY;
5960 
5961             //     for (lay in layers) {
5962             //         if (layers.hasOwnProperty(lay)) {
5963             //             if (layers[lay] > last && layers[lay] < minim) {
5964             //                 minim = layers[lay];
5965             //             }
5966             //         }
5967             //     }
5968 
5969             //     for (el = 0; el < olen; el++) {
5970             //         pEl = this.objectsList[el];
5971             //         if (pEl.visProp.layer === minim) {
5972             //             pEl.prepareUpdate().updateRenderer();
5973             //         }
5974             //     }
5975             //     last = minim;
5976             // }
5977 
5978             return this;
5979         },
5980 
5981         /**
5982          * Please use {@link JXG.Board.on} instead.
5983          * @param {Function} hook A function to be called by the board after an update occurred.
5984          * @param {String} [m='update'] When the hook is to be called. Possible values are <i>mouseup</i>, <i>mousedown</i> and <i>update</i>.
5985          * @param {Object} [context=board] Determines the execution context the hook is called. This parameter is optional, default is the
5986          * board object the hook is attached to.
5987          * @returns {Number} Id of the hook, required to remove the hook from the board.
5988          * @deprecated
5989          */
5990         addHook: function (hook, m, context) {
5991             JXG.deprecated('Board.addHook()', 'Board.on()');
5992             m = Type.def(m, 'update');
5993 
5994             context = Type.def(context, this);
5995 
5996             this.hooks.push([m, hook]);
5997             this.on(m, hook, context);
5998 
5999             return this.hooks.length - 1;
6000         },
6001 
6002         /**
6003          * Alias of {@link JXG.Board.on}.
6004          */
6005         addEvent: JXG.shortcut(JXG.Board.prototype, 'on'),
6006 
6007         /**
6008          * Please use {@link JXG.Board.off} instead.
6009          * @param {Number|function} id The number you got when you added the hook or a reference to the event handler.
6010          * @returns {JXG.Board} Reference to the board
6011          * @deprecated
6012          */
6013         removeHook: function (id) {
6014             JXG.deprecated('Board.removeHook()', 'Board.off()');
6015             if (this.hooks[id]) {
6016                 this.off(this.hooks[id][0], this.hooks[id][1]);
6017                 this.hooks[id] = null;
6018             }
6019 
6020             return this;
6021         },
6022 
6023         /**
6024          * Alias of {@link JXG.Board.off}.
6025          */
6026         removeEvent: JXG.shortcut(JXG.Board.prototype, 'off'),
6027 
6028         /**
6029          * Runs through all hooked functions and calls them.
6030          * @returns {JXG.Board} Reference to the board
6031          * @deprecated
6032          */
6033         updateHooks: function (m) {
6034             var arg = Array.prototype.slice.call(arguments, 0);
6035 
6036             JXG.deprecated('Board.updateHooks()', 'Board.triggerEventHandlers()');
6037 
6038             arg[0] = Type.def(arg[0], 'update');
6039             this.triggerEventHandlers([arg[0]], arguments);
6040 
6041             return this;
6042         },
6043 
6044         /**
6045          * Adds a dependent board to this board.
6046          * @param {JXG.Board} board A reference to board which will be updated after an update of this board occurred.
6047          * @returns {JXG.Board} Reference to the board
6048          */
6049         addChild: function (board) {
6050             if (Type.exists(board) && Type.exists(board.containerObj)) {
6051                 this.dependentBoards.push(board);
6052                 this.update();
6053             }
6054             return this;
6055         },
6056 
6057         /**
6058          * Deletes a board from the list of dependent boards.
6059          * @param {JXG.Board} board Reference to the board which will be removed.
6060          * @returns {JXG.Board} Reference to the board
6061          */
6062         removeChild: function (board) {
6063             var i;
6064 
6065             for (i = this.dependentBoards.length - 1; i >= 0; i--) {
6066                 if (this.dependentBoards[i] === board) {
6067                     this.dependentBoards.splice(i, 1);
6068                 }
6069             }
6070             return this;
6071         },
6072 
6073         /**
6074          * Runs through most elements and calls their update() method and update the conditions.
6075          * @param {JXG.GeometryElement} [drag] Element that caused the update.
6076          * @returns {JXG.Board} Reference to the board
6077          */
6078         update: function (drag) {
6079             var i, len, b, insert, storeActiveEl;
6080 
6081             if (this.inUpdate || this.isSuspendedUpdate) {
6082                 return this;
6083             }
6084             this.inUpdate = true;
6085 
6086             if (
6087                 this.attr.minimizereflow === 'all' &&
6088                 this.containerObj &&
6089                 this.renderer.type !== 'vml'
6090             ) {
6091                 storeActiveEl = this.document.activeElement; // Store focus element
6092                 insert = this.renderer.removeToInsertLater(this.containerObj);
6093             }
6094 
6095             if (this.attr.minimizereflow === 'svg' && this.renderer.type === 'svg') {
6096                 storeActiveEl = this.document.activeElement;
6097                 insert = this.renderer.removeToInsertLater(this.renderer.svgRoot);
6098             }
6099 
6100             this.prepareUpdate(drag).updateElements(drag).updateConditions();
6101 
6102             this.renderer.suspendRedraw(this);
6103             this.updateRenderer();
6104             this.renderer.unsuspendRedraw();
6105             this.triggerEventHandlers(['update'], []);
6106 
6107             if (insert) {
6108                 insert();
6109                 storeActiveEl.focus(); // Restore focus element
6110             }
6111 
6112             // To resolve dependencies between boards
6113             // for (var board in JXG.boards) {
6114             len = this.dependentBoards.length;
6115             for (i = 0; i < len; i++) {
6116                 b = this.dependentBoards[i];
6117                 if (Type.exists(b) && b !== this) {
6118                     b.updateQuality = this.updateQuality;
6119                     b.prepareUpdate().updateElements().updateConditions();
6120                     b.renderer.suspendRedraw(this);
6121                     b.updateRenderer();
6122                     b.renderer.unsuspendRedraw();
6123                     b.triggerEventHandlers(['update'], []);
6124                 }
6125             }
6126 
6127             this.inUpdate = false;
6128             return this;
6129         },
6130 
6131         /**
6132          * Runs through all elements and calls their update() method and update the conditions.
6133          * This is necessary after zooming and changing the bounding box.
6134          * @returns {JXG.Board} Reference to the board
6135          */
6136         fullUpdate: function () {
6137             this.needsFullUpdate = true;
6138             this.update();
6139             this.needsFullUpdate = false;
6140             return this;
6141         },
6142 
6143         /**
6144          * Adds a grid to the board according to the settings given in board.options.
6145          * @returns {JXG.Board} Reference to the board.
6146          */
6147         addGrid: function () {
6148             this.create('grid', []);
6149 
6150             return this;
6151         },
6152 
6153         /**
6154          * Removes all grids assigned to this board. Warning: This method also removes all objects depending on one or
6155          * more of the grids.
6156          * @returns {JXG.Board} Reference to the board object.
6157          */
6158         removeGrids: function () {
6159             var i;
6160 
6161             for (i = 0; i < this.grids.length; i++) {
6162                 this.removeObject(this.grids[i]);
6163             }
6164 
6165             this.grids.length = 0;
6166             this.update(); // required for canvas renderer
6167 
6168             return this;
6169         },
6170 
6171         /**
6172          * Creates a new geometric element of type elementType.
6173          * @param {String} elementType Type of the element to be constructed given as a string e.g. 'point' or 'circle'.
6174          * @param {Array} parents Array of parent elements needed to construct the element e.g. coordinates for a point or two
6175          * points to construct a line. This highly depends on the elementType that is constructed. See the corresponding JXG.create*
6176          * methods for a list of possible parameters.
6177          * @param {Object} [attributes] An object containing the attributes to be set. This also depends on the elementType.
6178          * Common attributes are name, visible, strokeColor.
6179          * @returns {Object} Reference to the created element. This is usually a GeometryElement, but can be an array containing
6180          * two or more elements.
6181          */
6182         create: function (elementType, parents, attributes) {
6183             var el, i;
6184 
6185             elementType = elementType.toLowerCase();
6186 
6187             if (!Type.exists(parents)) {
6188                 parents = [];
6189             }
6190 
6191             if (!Type.exists(attributes)) {
6192                 attributes = {};
6193             }
6194 
6195             for (i = 0; i < parents.length; i++) {
6196                 if (
6197                     Type.isString(parents[i]) &&
6198                     !(elementType === 'text' && i === 2) &&
6199                     !(elementType === 'solidofrevolution3d' && i === 2) &&
6200                     !(elementType === 'text3d' && (i === 2 || i === 4)) &&
6201                     !(
6202                         (elementType === 'input' ||
6203                             elementType === 'checkbox' ||
6204                             elementType === 'button') &&
6205                         (i === 2 || i === 3)
6206                     ) &&
6207                     !(elementType === 'curve' /*&& i > 0*/) && // Allow curve plots with jessiecode, parents[0] is the
6208                                                                // variable name
6209                     !(elementType === 'functiongraph') && // Prevent problems with function terms like 'x', 'y'
6210                     !(elementType === 'implicitcurve')
6211                 ) {
6212                     if (i > 0 && parents[0].elType === 'view3d') {
6213                         // 3D elements are based on 3D elements, only
6214                         parents[i] = parents[0].select(parents[i]);
6215                     } else {
6216                         parents[i] = this.select(parents[i]);
6217                     }
6218                 }
6219             }
6220 
6221             if (Type.isFunction(JXG.elements[elementType])) {
6222                 el = JXG.elements[elementType](this, parents, attributes);
6223             } else {
6224                 throw new Error('JSXGraph: create: Unknown element type given: ' + elementType);
6225             }
6226 
6227             if (!Type.exists(el)) {
6228                 JXG.debug('JSXGraph: create: failure creating ' + elementType);
6229                 return el;
6230             }
6231 
6232             if (el.prepareUpdate && el.update && el.updateRenderer) {
6233                 el.fullUpdate();
6234             }
6235             return el;
6236         },
6237 
6238         /**
6239          * Deprecated name for {@link JXG.Board.create}.
6240          * @deprecated
6241          */
6242         createElement: function () {
6243             JXG.deprecated('Board.createElement()', 'Board.create()');
6244             return this.create.apply(this, arguments);
6245         },
6246 
6247         /**
6248          * Delete the elements drawn as part of a trace of an element.
6249          * @returns {JXG.Board} Reference to the board
6250          */
6251         clearTraces: function () {
6252             var el;
6253 
6254             for (el = 0; el < this.objectsList.length; el++) {
6255                 this.objectsList[el].clearTrace();
6256             }
6257 
6258             this.numTraces = 0;
6259             return this;
6260         },
6261 
6262         /**
6263          * Stop updates of the board.
6264          * @returns {JXG.Board} Reference to the board
6265          */
6266         suspendUpdate: function () {
6267             if (!this.inUpdate) {
6268                 this.isSuspendedUpdate = true;
6269             }
6270             return this;
6271         },
6272 
6273         /**
6274          * Enable updates of the board.
6275          * @returns {JXG.Board} Reference to the board
6276          */
6277         unsuspendUpdate: function () {
6278             if (this.isSuspendedUpdate) {
6279                 this.isSuspendedUpdate = false;
6280                 this.fullUpdate();
6281             }
6282             return this;
6283         },
6284 
6285         /**
6286          * Set the bounding box of the board.
6287          * @param {Array} bbox New bounding box [x1,y1,x2,y2]
6288          * @param {Boolean} [keepaspectratio=false] If set to true, the aspect ratio will be 1:1, but
6289          * the resulting viewport may be larger.
6290          * @param {String} [setZoom='reset'] Reset, keep or update the zoom level of the board. 'reset'
6291          * sets {@link JXG.Board#zoomX} and {@link JXG.Board#zoomY} to the start values (or 1.0).
6292          * 'update' adapts these values accoring to the new bounding box and 'keep' does nothing.
6293          * @returns {JXG.Board} Reference to the board
6294          */
6295         setBoundingBox: function (bbox, keepaspectratio, setZoom) {
6296             var h, w, ux, uy,
6297                 offX = 0,
6298                 offY = 0,
6299                 zoom_ratio = 1,
6300                 ratio, dx, dy, prev_w, prev_h,
6301                 dim = Env.getDimensions(this.containerObj, this.document);
6302 
6303             if (!Type.isArray(bbox)) {
6304                 return this;
6305             }
6306 
6307             if (
6308                 bbox[0] < this.maxboundingbox[0] - Mat.eps ||
6309                 bbox[1] > this.maxboundingbox[1] + Mat.eps ||
6310                 bbox[2] > this.maxboundingbox[2] + Mat.eps ||
6311                 bbox[3] < this.maxboundingbox[3] - Mat.eps
6312             ) {
6313                 return this;
6314             }
6315 
6316             if (!Type.exists(setZoom)) {
6317                 setZoom = 'reset';
6318             }
6319 
6320             ux = this.unitX;
6321             uy = this.unitY;
6322             this.canvasWidth = parseFloat(dim.width);   // parseInt(dim.width, 10);
6323             this.canvasHeight = parseFloat(dim.height); // parseInt(dim.height, 10);
6324             w = this.canvasWidth;
6325             h = this.canvasHeight;
6326             if (keepaspectratio) {
6327                 if (this.keepaspectratio) {
6328                     ratio = ux / uy;        // Keep this ratio if keepaspectratio was true
6329                     if (isNaN(ratio)) {
6330                         ratio = 1.0;
6331                     }
6332                 } else {
6333                     ratio = 1.0;
6334                 }
6335                 if (setZoom === 'keep') {
6336                     zoom_ratio = this.zoomX / this.zoomY;
6337                 }
6338                 dx = bbox[2] - bbox[0];
6339                 dy = bbox[1] - bbox[3];
6340                 prev_w = ux * dx;
6341                 prev_h = uy * dy;
6342                 if (w >= h) {
6343                     if (prev_w >= prev_h) {
6344                         this.unitY = h / dy;
6345                         this.unitX = this.unitY * ratio;
6346                     } else {
6347                         // Switch dominating interval
6348                         this.unitY = h / Math.abs(dx) * Mat.sign(dy) / zoom_ratio;
6349                         this.unitX = this.unitY * ratio;
6350                     }
6351                 } else {
6352                     if (prev_h > prev_w) {
6353                         this.unitX = w / dx;
6354                         this.unitY = this.unitX / ratio;
6355                     } else {
6356                         // Switch dominating interval
6357                         this.unitX = w / Math.abs(dy) * Mat.sign(dx) * zoom_ratio;
6358                         this.unitY = this.unitX / ratio;
6359                     }
6360                 }
6361                 // Add the additional units in equal portions left and right
6362                 offX = (w / this.unitX - dx) * 0.5;
6363                 // Add the additional units in equal portions above and below
6364                 offY = (h / this.unitY - dy) * 0.5;
6365                 this.keepaspectratio = true;
6366             } else {
6367                 this.unitX = w / (bbox[2] - bbox[0]);
6368                 this.unitY = h / (bbox[1] - bbox[3]);
6369                 this.keepaspectratio = false;
6370             }
6371 
6372             this.moveOrigin(-this.unitX * (bbox[0] - offX), this.unitY * (bbox[1] + offY));
6373 
6374             if (setZoom === 'update') {
6375                 this.zoomX *= this.unitX / ux;
6376                 this.zoomY *= this.unitY / uy;
6377             } else if (setZoom === 'reset') {
6378                 this.zoomX = Type.exists(this.attr.zoomx) ? this.attr.zoomx : 1.0;
6379                 this.zoomY = Type.exists(this.attr.zoomy) ? this.attr.zoomy : 1.0;
6380             }
6381 
6382             return this;
6383         },
6384 
6385         /**
6386          * Get the bounding box of the board.
6387          * @returns {Array} bounding box [x1,y1,x2,y2] upper left corner, lower right corner
6388          */
6389         getBoundingBox: function () {
6390             var ul = new Coords(Const.COORDS_BY_SCREEN, [0, 0], this).usrCoords,
6391                 lr = new Coords(
6392                     Const.COORDS_BY_SCREEN,
6393                     [this.canvasWidth, this.canvasHeight],
6394                     this
6395                 ).usrCoords;
6396             return [ul[1], ul[2], lr[1], lr[2]];
6397         },
6398 
6399         /**
6400          * Sets the value of attribute <tt>key</tt> to <tt>value</tt>.
6401          * @param {String} key The attribute's name.
6402          * @param value The new value
6403          * @private
6404          */
6405         _set: function (key, value) {
6406             key = key.toLocaleLowerCase();
6407 
6408             if (
6409                 value !== null &&
6410                 Type.isObject(value) &&
6411                 !Type.exists(value.id) &&
6412                 !Type.exists(value.name)
6413             ) {
6414                 // value is of type {prop: val, prop: val,...}
6415                 // Convert these attributes to lowercase, too
6416                 // this.attr[key] = {};
6417                 // for (el in value) {
6418                 //     if (value.hasOwnProperty(el)) {
6419                 //         this.attr[key][el.toLocaleLowerCase()] = value[el];
6420                 //     }
6421                 // }
6422                 Type.mergeAttr(this.attr[key], value);
6423             } else {
6424                 this.attr[key] = value;
6425             }
6426         },
6427 
6428         /**
6429          * Sets an arbitrary number of attributes. This method has one or more
6430          * parameters of the following types:
6431          * <ul>
6432          * <li> object: {key1:value1,key2:value2,...}
6433          * <li> string: 'key:value'
6434          * <li> array: ['key', value]
6435          * </ul>
6436          * Some board attributes are immutable, like e.g. the renderer type.
6437          *
6438          * @param {Object} attributes An object with attributes.
6439          * @returns {JXG.Board} Reference to the board
6440          *
6441          * @example
6442          * const board = JXG.JSXGraph.initBoard('jxgbox', {
6443          *     boundingbox: [-5, 5, 5, -5],
6444          *     keepAspectRatio: false,
6445          *     axis:true,
6446          *     showFullscreen: true,
6447          *     showScreenshot: true,
6448          *     showCopyright: false
6449          * });
6450          *
6451          * board.setAttribute({
6452          *     animationDelay: 10,
6453          *     boundingbox: [-10, 5, 10, -5],
6454          *     defaultAxes: {
6455          *         x: { strokeColor: 'blue', ticks: { strokeColor: 'blue'}}
6456          *     },
6457          *     description: 'test',
6458          *     fullscreen: {
6459          *         scale: 0.5
6460          *     },
6461          *     intl: {
6462          *         enabled: true,
6463          *         locale: 'de-DE'
6464          *     }
6465          * });
6466          *
6467          * board.setAttribute({
6468          *     selection: {
6469          *         enabled: true,
6470          *         fillColor: 'blue'
6471          *     },
6472          *     showInfobox: false,
6473          *     zoomX: 0.5,
6474          *     zoomY: 2,
6475          *     fullscreen: { symbol: 'x' },
6476          *     screenshot: { symbol: 'y' },
6477          *     showCopyright: true,
6478          *     showFullscreen: false,
6479          *     showScreenshot: false,
6480          *     showZoom: false,
6481          *     showNavigation: false
6482          * });
6483          * board.setAttribute('showCopyright:false');
6484          *
6485          * var p = board.create('point', [1, 1], {size: 10,
6486          *     label: {
6487          *         fontSize: 24,
6488          *         highlightStrokeOpacity: 0.1,
6489          *         offset: [5, 0]
6490          *     }
6491          * });
6492          *
6493          *
6494          * </pre><div id="JXGea7b8e09-beac-4d95-9a0c-5fc1c761ffbc" class="jxgbox" style="width: 300px; height: 300px;"></div>
6495          * <script type="text/javascript">
6496          *     (function() {
6497          *     const board = JXG.JSXGraph.initBoard('JXGea7b8e09-beac-4d95-9a0c-5fc1c761ffbc', {
6498          *         boundingbox: [-5, 5, 5, -5],
6499          *         keepAspectRatio: false,
6500          *         axis:true,
6501          *         showFullscreen: true,
6502          *         showScreenshot: true,
6503          *         showCopyright: false
6504          *     });
6505          *
6506          *     board.setAttribute({
6507          *         animationDelay: 10,
6508          *         boundingbox: [-10, 5, 10, -5],
6509          *         defaultAxes: {
6510          *             x: { strokeColor: 'blue', ticks: { strokeColor: 'blue'}}
6511          *         },
6512          *         description: 'test',
6513          *         fullscreen: {
6514          *             scale: 0.5
6515          *         },
6516          *         intl: {
6517          *             enabled: true,
6518          *             locale: 'de-DE'
6519          *         }
6520          *     });
6521          *
6522          *     board.setAttribute({
6523          *         selection: {
6524          *             enabled: true,
6525          *             fillColor: 'blue'
6526          *         },
6527          *         showInfobox: false,
6528          *         zoomX: 0.5,
6529          *         zoomY: 2,
6530          *         fullscreen: { symbol: 'x' },
6531          *         screenshot: { symbol: 'y' },
6532          *         showCopyright: true,
6533          *         showFullscreen: false,
6534          *         showScreenshot: false,
6535          *         showZoom: false,
6536          *         showNavigation: false
6537          *     });
6538          *
6539          *     board.setAttribute('showCopyright:false');
6540          *
6541          *     var p = board.create('point', [1, 1], {size: 10,
6542          *         label: {
6543          *             fontSize: 24,
6544          *             highlightStrokeOpacity: 0.1,
6545          *             offset: [5, 0]
6546          *         }
6547          *     });
6548          *
6549          *
6550          *     })();
6551          *
6552          * </script><pre>
6553          *
6554          *
6555          */
6556         setAttribute: function (attr) {
6557             var i, arg, pair,
6558                 key, value, oldvalue,// j, le,
6559                 node,
6560                 attributes = {};
6561 
6562             // Normalize the user input
6563             for (i = 0; i < arguments.length; i++) {
6564                 arg = arguments[i];
6565                 if (Type.isString(arg)) {
6566                     // pairRaw is string of the form 'key:value'
6567                     pair = arg.split(":");
6568                     attributes[Type.trim(pair[0])] = Type.trim(pair[1]);
6569                 } else if (!Type.isArray(arg)) {
6570                     // pairRaw consists of objects of the form {key1:value1,key2:value2,...}
6571                     JXG.extend(attributes, arg);
6572                 } else {
6573                     // pairRaw consists of array [key,value]
6574                     attributes[arg[0]] = arg[1];
6575                 }
6576             }
6577 
6578             for (i in attributes) {
6579                 if (attributes.hasOwnProperty(i)) {
6580                     key = i.replace(/\s+/g, "").toLowerCase();
6581                     value = attributes[i];
6582                 }
6583                 value = (value.toLowerCase && value.toLowerCase() === 'false')
6584                     ? false
6585                     : value;
6586 
6587                 oldvalue = this.attr[key];
6588                 if (oldvalue === value) {
6589                     continue;
6590                 }
6591                 switch (key) {
6592                     case 'axis':
6593                         if (value === false) {
6594                             if (Type.exists(this.defaultAxes)) {
6595                                 this.defaultAxes.x.setAttribute({ visible: false });
6596                                 this.defaultAxes.y.setAttribute({ visible: false });
6597                             }
6598                         } else {
6599                             // TODO
6600                         }
6601                         break;
6602                     case 'boundingbox':
6603                         this.setBoundingBox(value, this.keepaspectratio);
6604                         this._set(key, value);
6605                         break;
6606                     case 'defaultaxes':
6607                         if (Type.exists(this.defaultAxes.x) && Type.exists(value.x)) {
6608                             this.defaultAxes.x.setAttribute(value.x);
6609                         }
6610                         if (Type.exists(this.defaultAxes.y) && Type.exists(value.y)) {
6611                             this.defaultAxes.y.setAttribute(value.y);
6612                         }
6613                         break;
6614                     case 'title':
6615                         this.document.getElementById(this.container + '_ARIAlabel')
6616                             .innerHTML = value;
6617                         this._set(key, value);
6618                         break;
6619                     case 'keepaspectratio':
6620                         this._set(key, value);
6621                         this.setBoundingBox(this.getBoundingBox(), value, 'keep');
6622                         break;
6623 
6624                     // /* eslint-disable no-fallthrough */
6625                     case 'document':
6626                     case 'maxboundingbox':
6627                         this[key] = value;
6628                         this._set(key, value);
6629                         break;
6630 
6631                     case 'zoomx':
6632                     case 'zoomy':
6633                         this[key] = value;
6634                         this._set(key, value);
6635                         this.setZoom(this.attr.zoomx, this.attr.zoomy);
6636                         break;
6637 
6638                     case 'registerevents':
6639                     case 'renderer':
6640                         // immutable, i.e. ignored
6641                         break;
6642 
6643                     case 'fullscreen':
6644                     case 'screenshot':
6645                         node = this.containerObj.ownerDocument.getElementById(
6646                             this.container + '_navigation_' + key);
6647                         if (node && Type.exists(value.symbol)) {
6648                             node.innerHTML = Type.evaluate(value.symbol);
6649                         }
6650                         this._set(key, value);
6651                         break;
6652 
6653                     case 'selection':
6654                         value.visible = false;
6655                         value.withLines = false;
6656                         value.vertices = { visible: false };
6657                         this._set(key, value);
6658                         break;
6659 
6660                     case 'showcopyright':
6661                         if (this.renderer.type === 'svg') {
6662                             node = this.containerObj.ownerDocument.getElementById(
6663                                 this.renderer.uniqName('licenseText')
6664                             );
6665                             if (node) {
6666                                 node.style.display = ((Type.evaluate(value)) ? 'inline' : 'none');
6667                             } else if (Type.evaluate(value)) {
6668                                 this.renderer.displayCopyright(Const.licenseText, parseInt(this.options.text.fontSize, 10));
6669                             }
6670                         }
6671                         this._set(key, value);
6672                         break;
6673 
6674                     case 'showlogo':
6675                         if (this.renderer.type === 'svg') {
6676                             node = this.containerObj.ownerDocument.getElementById(
6677                                 this.renderer.uniqName('licenseLogo')
6678                             );
6679                             if (node) {
6680                                 node.style.display = ((Type.evaluate(value)) ? 'inline' : 'none');
6681                             } else if (Type.evaluate(value)) {
6682                                 this.renderer.displayLogo(Const.licenseLogo, parseInt(this.options.text.fontSize, 10));
6683                             }
6684                         }
6685                         this._set(key, value);
6686                         break;
6687 
6688                     default:
6689                         if (Type.exists(this.attr[key])) {
6690                             this._set(key, value);
6691                         }
6692                         break;
6693                     // /* eslint-enable no-fallthrough */
6694                 }
6695             }
6696 
6697             // Redraw navbar to handle the remaining show* attributes
6698             this.containerObj.ownerDocument.getElementById(
6699                 this.container + "_navigationbar"
6700             ).remove();
6701             this.renderer.drawNavigationBar(this, this.attr.navbar);
6702 
6703             this.triggerEventHandlers(["attribute"], [attributes, this]);
6704             this.fullUpdate();
6705 
6706             return this;
6707         },
6708 
6709         /**
6710          * Adds an animation. Animations are controlled by the boards, so the boards need to be aware of the
6711          * animated elements. This function tells the board about new elements to animate.
6712          * @param {JXG.GeometryElement} element The element which is to be animated.
6713          * @returns {JXG.Board} Reference to the board
6714          */
6715         addAnimation: function (element) {
6716             var that = this;
6717 
6718             this.animationObjects[element.id] = element;
6719 
6720             if (!this.animationIntervalCode) {
6721                 this.animationIntervalCode = window.setInterval(function () {
6722                     that.animate();
6723                 }, element.board.attr.animationdelay);
6724             }
6725 
6726             return this;
6727         },
6728 
6729         /**
6730          * Cancels all running animations.
6731          * @returns {JXG.Board} Reference to the board
6732          */
6733         stopAllAnimation: function () {
6734             var el;
6735 
6736             for (el in this.animationObjects) {
6737                 if (
6738                     this.animationObjects.hasOwnProperty(el) &&
6739                     Type.exists(this.animationObjects[el])
6740                 ) {
6741                     this.animationObjects[el] = null;
6742                     delete this.animationObjects[el];
6743                 }
6744             }
6745 
6746             window.clearInterval(this.animationIntervalCode);
6747             delete this.animationIntervalCode;
6748 
6749             return this;
6750         },
6751 
6752         /**
6753          * General purpose animation function. This currently only supports moving points from one place to another. This
6754          * is faster than managing the animation per point, especially if there is more than one animated point at the same time.
6755          * @returns {JXG.Board} Reference to the board
6756          */
6757         animate: function () {
6758             var props,
6759                 el,
6760                 o,
6761                 newCoords,
6762                 r,
6763                 p,
6764                 c,
6765                 cbtmp,
6766                 count = 0,
6767                 obj = null;
6768 
6769             for (el in this.animationObjects) {
6770                 if (
6771                     this.animationObjects.hasOwnProperty(el) &&
6772                     Type.exists(this.animationObjects[el])
6773                 ) {
6774                     count += 1;
6775                     o = this.animationObjects[el];
6776 
6777                     if (o.animationPath) {
6778                         if (Type.isFunction(o.animationPath)) {
6779                             newCoords = o.animationPath(
6780                                 new Date().getTime() - o.animationStart
6781                             );
6782                         } else {
6783                             newCoords = o.animationPath.pop();
6784                         }
6785 
6786                         if (
6787                             !Type.exists(newCoords) ||
6788                             (!Type.isArray(newCoords) && isNaN(newCoords))
6789                         ) {
6790                             delete o.animationPath;
6791                         } else {
6792                             o.setPositionDirectly(Const.COORDS_BY_USER, newCoords);
6793                             o.fullUpdate();
6794                             obj = o;
6795                         }
6796                     }
6797                     if (o.animationData) {
6798                         c = 0;
6799 
6800                         for (r in o.animationData) {
6801                             if (o.animationData.hasOwnProperty(r)) {
6802                                 p = o.animationData[r].pop();
6803 
6804                                 if (!Type.exists(p)) {
6805                                     delete o.animationData[p];
6806                                 } else {
6807                                     c += 1;
6808                                     props = {};
6809                                     props[r] = p;
6810                                     o.setAttribute(props);
6811                                 }
6812                             }
6813                         }
6814 
6815                         if (c === 0) {
6816                             delete o.animationData;
6817                         }
6818                     }
6819 
6820                     if (!Type.exists(o.animationData) && !Type.exists(o.animationPath)) {
6821                         this.animationObjects[el] = null;
6822                         delete this.animationObjects[el];
6823 
6824                         if (Type.exists(o.animationCallback)) {
6825                             cbtmp = o.animationCallback;
6826                             o.animationCallback = null;
6827                             cbtmp();
6828                         }
6829                     }
6830                 }
6831             }
6832 
6833             if (count === 0) {
6834                 window.clearInterval(this.animationIntervalCode);
6835                 delete this.animationIntervalCode;
6836             } else {
6837                 this.update(obj);
6838             }
6839 
6840             return this;
6841         },
6842 
6843         /**
6844          * Migrate the dependency properties of the point src
6845          * to the point dest and delete the point src.
6846          * For example, a circle around the point src
6847          * receives the new center dest. The old center src
6848          * will be deleted.
6849          * @param {JXG.Point} src Original point which will be deleted
6850          * @param {JXG.Point} dest New point with the dependencies of src.
6851          * @param {Boolean} copyName Flag which decides if the name of the src element is copied to the
6852          *  dest element.
6853          * @returns {JXG.Board} Reference to the board
6854          */
6855         migratePoint: function (src, dest, copyName) {
6856             var child,
6857                 childId,
6858                 prop,
6859                 found,
6860                 i,
6861                 srcLabelId,
6862                 srcHasLabel = false;
6863 
6864             src = this.select(src);
6865             dest = this.select(dest);
6866 
6867             if (Type.exists(src.label)) {
6868                 srcLabelId = src.label.id;
6869                 srcHasLabel = true;
6870                 this.removeObject(src.label);
6871             }
6872 
6873             for (childId in src.childElements) {
6874                 if (src.childElements.hasOwnProperty(childId)) {
6875                     child = src.childElements[childId];
6876                     found = false;
6877 
6878                     for (prop in child) {
6879                         if (child.hasOwnProperty(prop)) {
6880                             if (child[prop] === src) {
6881                                 child[prop] = dest;
6882                                 found = true;
6883                             }
6884                         }
6885                     }
6886 
6887                     if (found) {
6888                         delete src.childElements[childId];
6889                     }
6890 
6891                     for (i = 0; i < child.parents.length; i++) {
6892                         if (child.parents[i] === src.id) {
6893                             child.parents[i] = dest.id;
6894                         }
6895                     }
6896 
6897                     dest.addChild(child);
6898                 }
6899             }
6900 
6901             // The destination object should receive the name
6902             // and the label of the originating (src) object
6903             if (copyName) {
6904                 if (srcHasLabel) {
6905                     delete dest.childElements[srcLabelId];
6906                     delete dest.descendants[srcLabelId];
6907                 }
6908 
6909                 if (dest.label) {
6910                     this.removeObject(dest.label);
6911                 }
6912 
6913                 delete this.elementsByName[dest.name];
6914                 dest.name = src.name;
6915                 if (srcHasLabel) {
6916                     dest.createLabel();
6917                 }
6918             }
6919 
6920             this.removeObject(src);
6921 
6922             if (Type.exists(dest.name) && dest.name !== '') {
6923                 this.elementsByName[dest.name] = dest;
6924             }
6925 
6926             this.fullUpdate();
6927 
6928             return this;
6929         },
6930 
6931         /**
6932          * Initializes color blindness simulation.
6933          * @param {String} deficiency Describes the color blindness deficiency which is simulated. Accepted values are 'protanopia', 'deuteranopia', and 'tritanopia'.
6934          * @returns {JXG.Board} Reference to the board
6935          */
6936         emulateColorblindness: function (deficiency) {
6937             var e, o;
6938 
6939             if (!Type.exists(deficiency)) {
6940                 deficiency = 'none';
6941             }
6942 
6943             if (this.currentCBDef === deficiency) {
6944                 return this;
6945             }
6946 
6947             for (e in this.objects) {
6948                 if (this.objects.hasOwnProperty(e)) {
6949                     o = this.objects[e];
6950 
6951                     if (deficiency !== 'none') {
6952                         if (this.currentCBDef === 'none') {
6953                             // this could be accomplished by JXG.extend, too. But do not use
6954                             // JXG.deepCopy as this could result in an infinite loop because in
6955                             // visProp there could be geometry elements which contain the board which
6956                             // contains all objects which contain board etc.
6957                             o.visPropOriginal = {
6958                                 strokecolor: o.visProp.strokecolor,
6959                                 fillcolor: o.visProp.fillcolor,
6960                                 highlightstrokecolor: o.visProp.highlightstrokecolor,
6961                                 highlightfillcolor: o.visProp.highlightfillcolor
6962                             };
6963                         }
6964                         o.setAttribute({
6965                             strokecolor: Color.rgb2cb(
6966                                 o.eval(o.visPropOriginal.strokecolor),
6967                                 deficiency
6968                             ),
6969                             fillcolor: Color.rgb2cb(
6970                                 o.eval(o.visPropOriginal.fillcolor),
6971                                 deficiency
6972                             ),
6973                             highlightstrokecolor: Color.rgb2cb(
6974                                 o.eval(o.visPropOriginal.highlightstrokecolor),
6975                                 deficiency
6976                             ),
6977                             highlightfillcolor: Color.rgb2cb(
6978                                 o.eval(o.visPropOriginal.highlightfillcolor),
6979                                 deficiency
6980                             )
6981                         });
6982                     } else if (Type.exists(o.visPropOriginal)) {
6983                         JXG.extend(o.visProp, o.visPropOriginal);
6984                     }
6985                 }
6986             }
6987             this.currentCBDef = deficiency;
6988             this.update();
6989 
6990             return this;
6991         },
6992 
6993         /**
6994          * Select a single or multiple elements at once.
6995          * @param {String|Object|function} str The name, id or a reference to a JSXGraph element on this board. An object will
6996          * be used as a filter to return multiple elements at once filtered by the properties of the object.
6997          * @param {Boolean} onlyByIdOrName If true (default:false) elements are only filtered by their id, name or groupId.
6998          * The advanced filters consisting of objects or functions are ignored.
6999          * @returns {JXG.GeometryElement|JXG.Composition}
7000          * @example
7001          * // select the element with name A
7002          * board.select('A');
7003          *
7004          * // select all elements with strokecolor set to 'red' (but not '#ff0000')
7005          * board.select({
7006          *   strokeColor: 'red'
7007          * });
7008          *
7009          * // select all points on or below the x axis and make them black.
7010          * board.select({
7011          *   elementClass: JXG.OBJECT_CLASS_POINT,
7012          *   Y: function (v) {
7013          *     return v <= 0;
7014          *   }
7015          * }).setAttribute({color: 'black'});
7016          *
7017          * // select all elements
7018          * board.select(function (el) {
7019          *   return true;
7020          * });
7021          */
7022         select: function (str, onlyByIdOrName) {
7023             var flist,
7024                 olist,
7025                 i,
7026                 l,
7027                 s = str;
7028 
7029             if (s === null) {
7030                 return s;
7031             }
7032 
7033             // It's a string, most likely an id or a name.
7034             if (Type.isString(s) && s !== '') {
7035                 // Search by ID
7036                 if (Type.exists(this.objects[s])) {
7037                     s = this.objects[s];
7038                     // Search by name
7039                 } else if (Type.exists(this.elementsByName[s])) {
7040                     s = this.elementsByName[s];
7041                     // Search by group ID
7042                 } else if (Type.exists(this.groups[s])) {
7043                     s = this.groups[s];
7044                 }
7045 
7046                 // It's a function or an object, but not an element
7047             } else if (
7048                 !onlyByIdOrName &&
7049                 (Type.isFunction(s) || (Type.isObject(s) && !Type.isFunction(s.setAttribute)))
7050             ) {
7051                 flist = Type.filterElements(this.objectsList, s);
7052 
7053                 olist = {};
7054                 l = flist.length;
7055                 for (i = 0; i < l; i++) {
7056                     olist[flist[i].id] = flist[i];
7057                 }
7058                 s = new Composition(olist);
7059 
7060                 // It's an element which has been deleted (and still hangs around, e.g. in an attractor list
7061             } else if (
7062                 Type.isObject(s) &&
7063                 Type.exists(s.id) &&
7064                 !Type.exists(this.objects[s.id])
7065             ) {
7066                 s = null;
7067             }
7068 
7069             return s;
7070         },
7071 
7072         /**
7073          * Checks if the given point is inside the boundingbox.
7074          * @param {Number|JXG.Coords} x User coordinate or {@link JXG.Coords} object.
7075          * @param {Number} [y] User coordinate. May be omitted in case <tt>x</tt> is a {@link JXG.Coords} object.
7076          * @returns {Boolean}
7077          */
7078         hasPoint: function (x, y) {
7079             var px = x,
7080                 py = y,
7081                 bbox = this.getBoundingBox();
7082 
7083             if (Type.exists(x) && Type.isArray(x.usrCoords)) {
7084                 px = x.usrCoords[1];
7085                 py = x.usrCoords[2];
7086             }
7087 
7088             return !!(
7089                 Type.isNumber(px) &&
7090                 Type.isNumber(py) &&
7091                 bbox[0] < px &&
7092                 px < bbox[2] &&
7093                 bbox[1] > py &&
7094                 py > bbox[3]
7095             );
7096         },
7097 
7098         /**
7099          * Update CSS transformations of type scaling. It is used to correct the mouse position
7100          * in {@link JXG.Board.getMousePosition}.
7101          * The inverse transformation matrix is updated on each mouseDown and touchStart event.
7102          *
7103          * It is up to the user to call this method after an update of the CSS transformation
7104          * in the DOM.
7105          */
7106         updateCSSTransforms: function () {
7107             var obj = this.containerObj,
7108                 o = obj,
7109                 o2 = obj;
7110 
7111             this.cssTransMat = Env.getCSSTransformMatrix(o);
7112 
7113             // Newer variant of walking up the tree.
7114             // We walk up all parent nodes and collect possible CSS transforms.
7115             // Works also for ShadowDOM
7116             if (Type.exists(o.getRootNode)) {
7117                 o = o.parentNode === o.getRootNode() ? o.parentNode.host : o.parentNode;
7118                 while (o) {
7119                     this.cssTransMat = Mat.matMatMult(Env.getCSSTransformMatrix(o), this.cssTransMat);
7120                     o = o.parentNode === o.getRootNode() ? o.parentNode.host : o.parentNode;
7121                 }
7122                 this.cssTransMat = Mat.inverse(this.cssTransMat);
7123             } else {
7124                 /*
7125                  * This is necessary for IE11
7126                  */
7127                 o = o.offsetParent;
7128                 while (o) {
7129                     this.cssTransMat = Mat.matMatMult(Env.getCSSTransformMatrix(o), this.cssTransMat);
7130 
7131                     o2 = o2.parentNode;
7132                     while (o2 !== o) {
7133                         this.cssTransMat = Mat.matMatMult(Env.getCSSTransformMatrix(o), this.cssTransMat);
7134                         o2 = o2.parentNode;
7135                     }
7136                     o = o.offsetParent;
7137                 }
7138                 this.cssTransMat = Mat.inverse(this.cssTransMat);
7139             }
7140             return this;
7141         },
7142 
7143         /**
7144          * Start selection mode. This function can either be triggered from outside or by
7145          * a down event together with correct key pressing. The default keys are
7146          * shift+ctrl. But this can be changed in the options.
7147          *
7148          * Starting from out side can be realized for example with a button like this:
7149          * <pre>
7150          * 	<button onclick='board.startSelectionMode()'>Start</button>
7151          * </pre>
7152          * @example
7153          * //
7154          * // Set a new bounding box from the selection rectangle
7155          * //
7156          * var board = JXG.JSXGraph.initBoard('jxgbox', {
7157          *         boundingBox:[-3,2,3,-2],
7158          *         keepAspectRatio: false,
7159          *         axis:true,
7160          *         selection: {
7161          *             enabled: true,
7162          *             needShift: false,
7163          *             needCtrl: true,
7164          *             withLines: false,
7165          *             vertices: {
7166          *                 visible: false
7167          *             },
7168          *             fillColor: '#ffff00',
7169          *         }
7170          *      });
7171          *
7172          * var f = function f(x) { return Math.cos(x); },
7173          *     curve = board.create('functiongraph', [f]);
7174          *
7175          * board.on('stopselecting', function(){
7176          *     var box = board.stopSelectionMode(),
7177          *
7178          *         // bbox has the coordinates of the selection rectangle.
7179          *         // Attention: box[i].usrCoords have the form [1, x, y], i.e.
7180          *         // are homogeneous coordinates.
7181          *         bbox = box[0].usrCoords.slice(1).concat(box[1].usrCoords.slice(1));
7182          *
7183          *         // Set a new bounding box
7184          *         board.setBoundingBox(bbox, false);
7185          *  });
7186          *
7187          *
7188          * </pre><div class='jxgbox' id='JXG11eff3a6-8c50-11e5-b01d-901b0e1b8723' style='width: 300px; height: 300px;'></div>
7189          * <script type='text/javascript'>
7190          *     (function() {
7191          *     //
7192          *     // Set a new bounding box from the selection rectangle
7193          *     //
7194          *     var board = JXG.JSXGraph.initBoard('JXG11eff3a6-8c50-11e5-b01d-901b0e1b8723', {
7195          *             boundingBox:[-3,2,3,-2],
7196          *             keepAspectRatio: false,
7197          *             axis:true,
7198          *             selection: {
7199          *                 enabled: true,
7200          *                 needShift: false,
7201          *                 needCtrl: true,
7202          *                 withLines: false,
7203          *                 vertices: {
7204          *                     visible: false
7205          *                 },
7206          *                 fillColor: '#ffff00',
7207          *             }
7208          *        });
7209          *
7210          *     var f = function f(x) { return Math.cos(x); },
7211          *         curve = board.create('functiongraph', [f]);
7212          *
7213          *     board.on('stopselecting', function(){
7214          *         var box = board.stopSelectionMode(),
7215          *
7216          *             // bbox has the coordinates of the selection rectangle.
7217          *             // Attention: box[i].usrCoords have the form [1, x, y], i.e.
7218          *             // are homogeneous coordinates.
7219          *             bbox = box[0].usrCoords.slice(1).concat(box[1].usrCoords.slice(1));
7220          *
7221          *             // Set a new bounding box
7222          *             board.setBoundingBox(bbox, false);
7223          *      });
7224          *     })();
7225          *
7226          * </script><pre>
7227          *
7228          */
7229         startSelectionMode: function () {
7230             this.selectingMode = true;
7231             this.selectionPolygon.setAttribute({ visible: true });
7232             this.selectingBox = [
7233                 [0, 0],
7234                 [0, 0]
7235             ];
7236             this._setSelectionPolygonFromBox();
7237             this.selectionPolygon.fullUpdate();
7238         },
7239 
7240         /**
7241          * Finalize the selection: disable selection mode and return the coordinates
7242          * of the selection rectangle.
7243          * @returns {Array} Coordinates of the selection rectangle. The array
7244          * contains two {@link JXG.Coords} objects. One the upper left corner and
7245          * the second for the lower right corner.
7246          */
7247         stopSelectionMode: function () {
7248             this.selectingMode = false;
7249             this.selectionPolygon.setAttribute({ visible: false });
7250             return [
7251                 this.selectionPolygon.vertices[0].coords,
7252                 this.selectionPolygon.vertices[2].coords
7253             ];
7254         },
7255 
7256         /**
7257          * Start the selection of a region.
7258          * @private
7259          * @param  {Array} pos Screen coordiates of the upper left corner of the
7260          * selection rectangle.
7261          */
7262         _startSelecting: function (pos) {
7263             this.isSelecting = true;
7264             this.selectingBox = [
7265                 [pos[0], pos[1]],
7266                 [pos[0], pos[1]]
7267             ];
7268             this._setSelectionPolygonFromBox();
7269         },
7270 
7271         /**
7272          * Update the selection rectangle during a move event.
7273          * @private
7274          * @param  {Array} pos Screen coordiates of the move event
7275          */
7276         _moveSelecting: function (pos) {
7277             if (this.isSelecting) {
7278                 this.selectingBox[1] = [pos[0], pos[1]];
7279                 this._setSelectionPolygonFromBox();
7280                 this.selectionPolygon.fullUpdate();
7281             }
7282         },
7283 
7284         /**
7285          * Update the selection rectangle during an up event. Stop selection.
7286          * @private
7287          * @param  {Object} evt Event object
7288          */
7289         _stopSelecting: function (evt) {
7290             var pos = this.getMousePosition(evt);
7291 
7292             this.isSelecting = false;
7293             this.selectingBox[1] = [pos[0], pos[1]];
7294             this._setSelectionPolygonFromBox();
7295         },
7296 
7297         /**
7298          * Update the Selection rectangle.
7299          * @private
7300          */
7301         _setSelectionPolygonFromBox: function () {
7302             var A = this.selectingBox[0],
7303                 B = this.selectingBox[1];
7304 
7305             this.selectionPolygon.vertices[0].setPositionDirectly(JXG.COORDS_BY_SCREEN, [
7306                 A[0],
7307                 A[1]
7308             ]);
7309             this.selectionPolygon.vertices[1].setPositionDirectly(JXG.COORDS_BY_SCREEN, [
7310                 A[0],
7311                 B[1]
7312             ]);
7313             this.selectionPolygon.vertices[2].setPositionDirectly(JXG.COORDS_BY_SCREEN, [
7314                 B[0],
7315                 B[1]
7316             ]);
7317             this.selectionPolygon.vertices[3].setPositionDirectly(JXG.COORDS_BY_SCREEN, [
7318                 B[0],
7319                 A[1]
7320             ]);
7321         },
7322 
7323         /**
7324          * Test if a down event should start a selection. Test if the
7325          * required keys are pressed. If yes, {@link JXG.Board.startSelectionMode} is called.
7326          * @param  {Object} evt Event object
7327          */
7328         _testForSelection: function (evt) {
7329             if (this._isRequiredKeyPressed(evt, 'selection')) {
7330                 if (!Type.exists(this.selectionPolygon)) {
7331                     this._createSelectionPolygon(this.attr);
7332                 }
7333                 this.startSelectionMode();
7334             }
7335         },
7336 
7337         /**
7338          * Create the internal selection polygon, which will be available as board.selectionPolygon.
7339          * @private
7340          * @param  {Object} attr board attributes, e.g. the subobject board.attr.
7341          * @returns {Object} pointer to the board to enable chaining.
7342          */
7343         _createSelectionPolygon: function (attr) {
7344             var selectionattr;
7345 
7346             if (!Type.exists(this.selectionPolygon)) {
7347                 selectionattr = Type.copyAttributes(attr, Options, 'board', 'selection');
7348                 if (selectionattr.enabled === true) {
7349                     this.selectionPolygon = this.create(
7350                         'polygon',
7351                         [
7352                             [0, 0],
7353                             [0, 0],
7354                             [0, 0],
7355                             [0, 0]
7356                         ],
7357                         selectionattr
7358                     );
7359                 }
7360             }
7361 
7362             return this;
7363         },
7364 
7365         /* **************************
7366          *     EVENT DEFINITION
7367          * for documentation purposes
7368          * ************************** */
7369 
7370         //region Event handler documentation
7371 
7372         /**
7373          * @event
7374          * @description Whenever the {@link JXG.Board#setAttribute} is called.
7375          * @name JXG.Board#attribute
7376          * @param {Event} e The browser's event object.
7377          */
7378         __evt__attribute: function (e) { },
7379 
7380         /**
7381          * @event
7382          * @description Whenever the user starts to touch or click the board.
7383          * @name JXG.Board#down
7384          * @param {Event} e The browser's event object.
7385          */
7386         __evt__down: function (e) { },
7387 
7388         /**
7389          * @event
7390          * @description Whenever the user starts to click on the board.
7391          * @name JXG.Board#mousedown
7392          * @param {Event} e The browser's event object.
7393          */
7394         __evt__mousedown: function (e) { },
7395 
7396         /**
7397          * @event
7398          * @description Whenever the user taps the pen on the board.
7399          * @name JXG.Board#pendown
7400          * @param {Event} e The browser's event object.
7401          */
7402         __evt__pendown: function (e) { },
7403 
7404         /**
7405          * @event
7406          * @description Whenever the user starts to click on the board with a
7407          * device sending pointer events.
7408          * @name JXG.Board#pointerdown
7409          * @param {Event} e The browser's event object.
7410          */
7411         __evt__pointerdown: function (e) { },
7412 
7413         /**
7414          * @event
7415          * @description Whenever the user starts to touch the board.
7416          * @name JXG.Board#touchstart
7417          * @param {Event} e The browser's event object.
7418          */
7419         __evt__touchstart: function (e) { },
7420 
7421         /**
7422          * @event
7423          * @description Whenever the user stops to touch or click the board.
7424          * @name JXG.Board#up
7425          * @param {Event} e The browser's event object.
7426          */
7427         __evt__up: function (e) { },
7428 
7429         /**
7430          * @event
7431          * @description Whenever the user releases the mousebutton over the board.
7432          * @name JXG.Board#mouseup
7433          * @param {Event} e The browser's event object.
7434          */
7435         __evt__mouseup: function (e) { },
7436 
7437         /**
7438          * @event
7439          * @description Whenever the user releases the mousebutton over the board with a
7440          * device sending pointer events.
7441          * @name JXG.Board#pointerup
7442          * @param {Event} e The browser's event object.
7443          */
7444         __evt__pointerup: function (e) { },
7445 
7446         /**
7447          * @event
7448          * @description Whenever the user stops touching the board.
7449          * @name JXG.Board#touchend
7450          * @param {Event} e The browser's event object.
7451          */
7452         __evt__touchend: function (e) { },
7453 
7454         /**
7455          * @event
7456          * @description Whenever the user clicks on the board.
7457          * @name JXG.Board#click
7458          * @see JXG.Board#clickDelay
7459          * @param {Event} e The browser's event object.
7460          */
7461         __evt__click: function (e) { },
7462 
7463         /**
7464          * @event
7465          * @description Whenever the user double clicks on the board.
7466          * This event works on desktop browser, but is undefined
7467          * on mobile browsers.
7468          * @name JXG.Board#dblclick
7469          * @see JXG.Board#clickDelay
7470          * @see JXG.Board#dblClickSuppressClick
7471          * @param {Event} e The browser's event object.
7472          */
7473         __evt__dblclick: function (e) { },
7474 
7475         /**
7476          * @event
7477          * @description Whenever the user clicks on the board with a mouse device.
7478          * @name JXG.Board#mouseclick
7479          * @param {Event} e The browser's event object.
7480          */
7481         __evt__mouseclick: function (e) { },
7482 
7483         /**
7484          * @event
7485          * @description Whenever the user double clicks on the board with a mouse device.
7486          * @name JXG.Board#mousedblclick
7487          * @see JXG.Board#clickDelay
7488          * @param {Event} e The browser's event object.
7489          */
7490         __evt__mousedblclick: function (e) { },
7491 
7492         /**
7493          * @event
7494          * @description Whenever the user clicks on the board with a pointer device.
7495          * @name JXG.Board#pointerclick
7496          * @param {Event} e The browser's event object.
7497          */
7498         __evt__pointerclick: function (e) { },
7499 
7500         /**
7501          * @event
7502          * @description Whenever the user double clicks on the board with a pointer device.
7503          * This event works on desktop browser, but is undefined
7504          * on mobile browsers.
7505          * @name JXG.Board#pointerdblclick
7506          * @see JXG.Board#clickDelay
7507          * @param {Event} e The browser's event object.
7508          */
7509         __evt__pointerdblclick: function (e) { },
7510 
7511         /**
7512          * @event
7513          * @description This event is fired whenever the user is moving the finger or mouse pointer over the board.
7514          * @name JXG.Board#move
7515          * @param {Event} e The browser's event object.
7516          * @param {Number} mode The mode the board currently is in
7517          * @see JXG.Board#mode
7518          */
7519         __evt__move: function (e, mode) { },
7520 
7521         /**
7522          * @event
7523          * @description This event is fired whenever the user is moving the mouse over the board.
7524          * @name JXG.Board#mousemove
7525          * @param {Event} e The browser's event object.
7526          * @param {Number} mode The mode the board currently is in
7527          * @see JXG.Board#mode
7528          */
7529         __evt__mousemove: function (e, mode) { },
7530 
7531         /**
7532          * @event
7533          * @description This event is fired whenever the user is moving the pen over the board.
7534          * @name JXG.Board#penmove
7535          * @param {Event} e The browser's event object.
7536          * @param {Number} mode The mode the board currently is in
7537          * @see JXG.Board#mode
7538          */
7539         __evt__penmove: function (e, mode) { },
7540 
7541         /**
7542          * @event
7543          * @description This event is fired whenever the user is moving the mouse over the board with a
7544          * device sending pointer events.
7545          * @name JXG.Board#pointermove
7546          * @param {Event} e The browser's event object.
7547          * @param {Number} mode The mode the board currently is in
7548          * @see JXG.Board#mode
7549          */
7550         __evt__pointermove: function (e, mode) { },
7551 
7552         /**
7553          * @event
7554          * @description This event is fired whenever the user is moving the finger over the board.
7555          * @name JXG.Board#touchmove
7556          * @param {Event} e The browser's event object.
7557          * @param {Number} mode The mode the board currently is in
7558          * @see JXG.Board#mode
7559          */
7560         __evt__touchmove: function (e, mode) { },
7561 
7562         /**
7563          * @event
7564          * @description This event is fired whenever the user is moving an element over the board by
7565          * pressing arrow keys on a keyboard.
7566          * @name JXG.Board#keymove
7567          * @param {Event} e The browser's event object.
7568          * @param {Number} mode The mode the board currently is in
7569          * @see JXG.Board#mode
7570          */
7571         __evt__keymove: function (e, mode) { },
7572 
7573         /**
7574          * @event
7575          * @description Whenever an element is highlighted this event is fired.
7576          * @name JXG.Board#hit
7577          * @param {Event} e The browser's event object.
7578          * @param {JXG.GeometryElement} el The hit element.
7579          * @param target
7580          *
7581          * @example
7582          * var c = board.create('circle', [[1, 1], 2]);
7583          * board.on('hit', function(evt, el) {
7584          *     console.log('Hit element', el);
7585          * });
7586          *
7587          * </pre><div id='JXG19eb31ac-88e6-11e8-bcb5-901b0e1b8723' class='jxgbox' style='width: 300px; height: 300px;'></div>
7588          * <script type='text/javascript'>
7589          *     (function() {
7590          *         var board = JXG.JSXGraph.initBoard('JXG19eb31ac-88e6-11e8-bcb5-901b0e1b8723',
7591          *             {boundingbox: [-8, 8, 8,-8], axis: true, showcopyright: false, shownavigation: false});
7592          *     var c = board.create('circle', [[1, 1], 2]);
7593          *     board.on('hit', function(evt, el) {
7594          *         console.log('Hit element', el);
7595          *     });
7596          *
7597          *     })();
7598          *
7599          * </script><pre>
7600          */
7601         __evt__hit: function (e, el, target) { },
7602 
7603         /**
7604          * @event
7605          * @description Whenever an element is highlighted this event is fired.
7606          * @name JXG.Board#mousehit
7607          * @see JXG.Board#hit
7608          * @param {Event} e The browser's event object.
7609          * @param {JXG.GeometryElement} el The hit element.
7610          * @param target
7611          */
7612         __evt__mousehit: function (e, el, target) { },
7613 
7614         /**
7615          * @event
7616          * @description This board is updated.
7617          * @name JXG.Board#update
7618          */
7619         __evt__update: function () { },
7620 
7621         /**
7622          * @event
7623          * @description The bounding box of the board has changed.
7624          * @name JXG.Board#boundingbox
7625          */
7626         __evt__boundingbox: function () { },
7627 
7628         /**
7629          * @event
7630          * @description Select a region is started during a down event or by calling
7631          * {@link JXG.Board.startSelectionMode}
7632          * @name JXG.Board#startselecting
7633          */
7634         __evt__startselecting: function () { },
7635 
7636         /**
7637          * @event
7638          * @description Select a region is started during a down event
7639          * from a device sending mouse events or by calling
7640          * {@link JXG.Board.startSelectionMode}.
7641          * @name JXG.Board#mousestartselecting
7642          */
7643         __evt__mousestartselecting: function () { },
7644 
7645         /**
7646          * @event
7647          * @description Select a region is started during a down event
7648          * from a device sending pointer events or by calling
7649          * {@link JXG.Board.startSelectionMode}.
7650          * @name JXG.Board#pointerstartselecting
7651          */
7652         __evt__pointerstartselecting: function () { },
7653 
7654         /**
7655          * @event
7656          * @description Select a region is started during a down event
7657          * from a device sending touch events or by calling
7658          * {@link JXG.Board.startSelectionMode}.
7659          * @name JXG.Board#touchstartselecting
7660          */
7661         __evt__touchstartselecting: function () { },
7662 
7663         /**
7664          * @event
7665          * @description Selection of a region is stopped during an up event.
7666          * @name JXG.Board#stopselecting
7667          */
7668         __evt__stopselecting: function () { },
7669 
7670         /**
7671          * @event
7672          * @description Selection of a region is stopped during an up event
7673          * from a device sending mouse events.
7674          * @name JXG.Board#mousestopselecting
7675          */
7676         __evt__mousestopselecting: function () { },
7677 
7678         /**
7679          * @event
7680          * @description Selection of a region is stopped during an up event
7681          * from a device sending pointer events.
7682          * @name JXG.Board#pointerstopselecting
7683          */
7684         __evt__pointerstopselecting: function () { },
7685 
7686         /**
7687          * @event
7688          * @description Selection of a region is stopped during an up event
7689          * from a device sending touch events.
7690          * @name JXG.Board#touchstopselecting
7691          */
7692         __evt__touchstopselecting: function () { },
7693 
7694         /**
7695          * @event
7696          * @description A move event while selecting of a region is active.
7697          * @name JXG.Board#moveselecting
7698          */
7699         __evt__moveselecting: function () { },
7700 
7701         /**
7702          * @event
7703          * @description A move event while selecting of a region is active
7704          * from a device sending mouse events.
7705          * @name JXG.Board#mousemoveselecting
7706          */
7707         __evt__mousemoveselecting: function () { },
7708 
7709         /**
7710          * @event
7711          * @description Select a region is started during a down event
7712          * from a device sending mouse events.
7713          * @name JXG.Board#pointermoveselecting
7714          */
7715         __evt__pointermoveselecting: function () { },
7716 
7717         /**
7718          * @event
7719          * @description Select a region is started during a down event
7720          * from a device sending touch events.
7721          * @name JXG.Board#touchmoveselecting
7722          */
7723         __evt__touchmoveselecting: function () { },
7724 
7725         /**
7726          * @ignore
7727          */
7728         __evt: function () { },
7729 
7730         //endregion
7731 
7732         /**
7733          * Expand the JSXGraph construction to fullscreen.
7734          * In order to preserve the proportions of the JSXGraph element,
7735          * a wrapper div is created which is set to fullscreen.
7736          * This function is called when fullscreen mode is triggered
7737          * <b>and</b> when it is closed.
7738          * <p>
7739          * The wrapping div has the CSS class 'jxgbox_wrap_private' which is
7740          * defined in the file 'jsxgraph.css'
7741          * <p>
7742          * This feature is not available on iPhones (as of December 2021).
7743          *
7744          * @param {String} id (Optional) id of the div element which is brought to fullscreen.
7745          * If not provided, this defaults to the JSXGraph div. However, it may be necessary for the aspect ratio trick
7746          * which using padding-bottom/top and an out div element. Then, the id of the outer div has to be supplied.
7747          *
7748          * @return {JXG.Board} Reference to the board
7749          *
7750          * @example
7751          * <div id='jxgbox' class='jxgbox' style='width:500px; height:200px;'></div>
7752          * <button onClick='board.toFullscreen()'>Fullscreen</button>
7753          *
7754          * <script language='Javascript' type='text/javascript'>
7755          * var board = JXG.JSXGraph.initBoard('jxgbox', {axis:true, boundingbox:[-5,5,5,-5]});
7756          * var p = board.create('point', [0, 1]);
7757          * </script>
7758          *
7759          * </pre><div id='JXGd5bab8b6-fd40-11e8-ab14-901b0e1b8723' class='jxgbox' style='width: 300px; height: 300px;'></div>
7760          * <script type='text/javascript'>
7761          *      var board_d5bab8b6;
7762          *     (function() {
7763          *         var board = JXG.JSXGraph.initBoard('JXGd5bab8b6-fd40-11e8-ab14-901b0e1b8723',
7764          *             {boundingbox:[-5,5,5,-5], axis: true, showcopyright: false, shownavigation: false});
7765          *         var p = board.create('point', [0, 1]);
7766          *         board_d5bab8b6 = board;
7767          *     })();
7768          * </script>
7769          * <button onClick='board_d5bab8b6.toFullscreen()'>Fullscreen</button>
7770          * <pre>
7771          *
7772          * @example
7773          * <div id='outer' style='max-width: 500px; margin: 0 auto;'>
7774          * <div id='jxgbox' class='jxgbox' style='height: 0; padding-bottom: 100%'></div>
7775          * </div>
7776          * <button onClick='board.toFullscreen('outer')'>Fullscreen</button>
7777          *
7778          * <script language='Javascript' type='text/javascript'>
7779          * var board = JXG.JSXGraph.initBoard('jxgbox', {
7780          *     axis:true,
7781          *     boundingbox:[-5,5,5,-5],
7782          *     fullscreen: { id: 'outer' },
7783          *     showFullscreen: true
7784          * });
7785          * var p = board.create('point', [-2, 3], {});
7786          * </script>
7787          *
7788          * </pre><div id='JXG7103f6b_outer' style='max-width: 500px; margin: 0 auto;'>
7789          * <div id='JXG7103f6be-6993-4ff8-8133-c78e50a8afac' class='jxgbox' style='height: 0; padding-bottom: 100%;'></div>
7790          * </div>
7791          * <button onClick='board_JXG7103f6be.toFullscreen('JXG7103f6b_outer')'>Fullscreen</button>
7792          * <script type='text/javascript'>
7793          *     var board_JXG7103f6be;
7794          *     (function() {
7795          *         var board = JXG.JSXGraph.initBoard('JXG7103f6be-6993-4ff8-8133-c78e50a8afac',
7796          *             {boundingbox: [-8, 8, 8,-8], axis: true, fullscreen: { id: 'JXG7103f6b_outer' }, showFullscreen: true,
7797          *              showcopyright: false, shownavigation: false});
7798          *     var p = board.create('point', [-2, 3], {});
7799          *     board_JXG7103f6be = board;
7800          *     })();
7801          *
7802          * </script><pre>
7803          *
7804          *
7805          */
7806         toFullscreen: function (id) {
7807             var wrap_id,
7808                 wrap_node,
7809                 inner_node,
7810                 dim,
7811                 doc = this.document,
7812                 fullscreenElement;
7813 
7814             id = id || this.container;
7815             this._fullscreen_inner_id = id;
7816             inner_node = doc.getElementById(id);
7817             wrap_id = 'fullscreenwrap_' + id;
7818 
7819             if (!Type.exists(inner_node._cssFullscreenStore)) {
7820                 // Store the actual, absolute size of the div
7821                 // This is used in scaleJSXGraphDiv
7822                 dim = this.containerObj.getBoundingClientRect();
7823                 inner_node._cssFullscreenStore = {
7824                     w: dim.width,
7825                     h: dim.height
7826                 };
7827             }
7828 
7829             // Wrap a div around the JSXGraph div.
7830             // It is removed when fullscreen mode is closed.
7831             if (doc.getElementById(wrap_id)) {
7832                 wrap_node = doc.getElementById(wrap_id);
7833             } else {
7834                 wrap_node = document.createElement('div');
7835                 wrap_node.classList.add('JXG_wrap_private');
7836                 wrap_node.setAttribute('id', wrap_id);
7837                 inner_node.parentNode.insertBefore(wrap_node, inner_node);
7838                 wrap_node.appendChild(inner_node);
7839             }
7840 
7841             // Trigger fullscreen mode
7842             wrap_node.requestFullscreen =
7843                 wrap_node.requestFullscreen ||
7844                 wrap_node.webkitRequestFullscreen ||
7845                 wrap_node.mozRequestFullScreen ||
7846                 wrap_node.msRequestFullscreen;
7847 
7848             if (doc.fullscreenElement !== undefined) {
7849                 fullscreenElement = doc.fullscreenElement;
7850             } else if (doc.webkitFullscreenElement !== undefined) {
7851                 fullscreenElement = doc.webkitFullscreenElement;
7852             } else {
7853                 fullscreenElement = doc.msFullscreenElement;
7854             }
7855 
7856             if (fullscreenElement === null) {
7857                 // Start fullscreen mode
7858                 if (wrap_node.requestFullscreen) {
7859                     wrap_node.requestFullscreen();
7860                     this.startFullscreenResizeObserver(wrap_node);
7861                 }
7862             } else {
7863                 this.stopFullscreenResizeObserver(wrap_node);
7864                 if (Type.exists(document.exitFullscreen)) {
7865                     document.exitFullscreen();
7866                 } else if (Type.exists(document.webkitExitFullscreen)) {
7867                     document.webkitExitFullscreen();
7868                 }
7869             }
7870 
7871             return this;
7872         },
7873 
7874         /**
7875          * If fullscreen mode is toggled, the possible CSS transformations
7876          * which are applied to the JSXGraph canvas have to be reread.
7877          * Otherwise the position of upper left corner is wrongly interpreted.
7878          *
7879          * @param  {Object} evt fullscreen event object (unused)
7880          */
7881         fullscreenListener: function (evt) {
7882             var inner_id,
7883                 inner_node,
7884                 fullscreenElement,
7885                 doc = this.document;
7886 
7887             inner_id = this._fullscreen_inner_id;
7888             if (!Type.exists(inner_id)) {
7889                 return;
7890             }
7891 
7892             if (doc.fullscreenElement !== undefined) {
7893                 fullscreenElement = doc.fullscreenElement;
7894             } else if (doc.webkitFullscreenElement !== undefined) {
7895                 fullscreenElement = doc.webkitFullscreenElement;
7896             } else {
7897                 fullscreenElement = doc.msFullscreenElement;
7898             }
7899 
7900             inner_node = doc.getElementById(inner_id);
7901             // If full screen mode is started we have to remove CSS margin around the JSXGraph div.
7902             // Otherwise, the positioning of the fullscreen div will be false.
7903             // When leaving the fullscreen mode, the margin is put back in.
7904             if (fullscreenElement) {
7905                 // Just entered fullscreen mode
7906 
7907                 // Store the original data.
7908                 // Further, the CSS margin has to be removed when in fullscreen mode,
7909                 // and must be restored later.
7910                 //
7911                 // Obsolete:
7912                 // It is used in AbstractRenderer.updateText to restore the scaling matrix
7913                 // which is removed by MathJax.
7914                 inner_node._cssFullscreenStore.id = fullscreenElement.id;
7915                 inner_node._cssFullscreenStore.isFullscreen = true;
7916                 inner_node._cssFullscreenStore.margin = inner_node.style.margin;
7917                 inner_node._cssFullscreenStore.width = inner_node.style.width;
7918                 inner_node._cssFullscreenStore.height = inner_node.style.height;
7919                 inner_node._cssFullscreenStore.transform = inner_node.style.transform;
7920                 // Be sure to replace relative width / height units by absolute units
7921                 inner_node.style.width = inner_node._cssFullscreenStore.w + 'px';
7922                 inner_node.style.height = inner_node._cssFullscreenStore.h + 'px';
7923                 inner_node.style.margin = '';
7924 
7925                 // Do the shifting and scaling via CSS properties
7926                 // We do this after fullscreen mode has been established to get the correct size
7927                 // of the JSXGraph div.
7928                 Env.scaleJSXGraphDiv(fullscreenElement.id, inner_id, doc,
7929                     Type.evaluate(this.attr.fullscreen.scale));
7930 
7931                 // Clear this.doc.fullscreenElement, because Safari doesn't to it and
7932                 // when leaving full screen mode it is still set.
7933                 fullscreenElement = null;
7934             } else if (Type.exists(inner_node._cssFullscreenStore)) {
7935                 // Just left the fullscreen mode
7936 
7937                 inner_node._cssFullscreenStore.isFullscreen = false;
7938                 inner_node.style.margin = inner_node._cssFullscreenStore.margin;
7939                 inner_node.style.width = inner_node._cssFullscreenStore.width;
7940                 inner_node.style.height = inner_node._cssFullscreenStore.height;
7941                 inner_node.style.transform = inner_node._cssFullscreenStore.transform;
7942                 inner_node._cssFullscreenStore = null;
7943 
7944                 // Remove the wrapper div
7945                 inner_node.parentElement.replaceWith(inner_node);
7946             }
7947 
7948             this.updateCSSTransforms();
7949         },
7950 
7951         /**
7952          * Start resize observer to handle
7953          * orientation changes in fullscreen mode.
7954          *
7955          * @param {Object} node DOM object which is in fullscreen mode. It is the wrapper element
7956          * around the JSXGraph div.
7957          * @returns {JXG.Board} Reference to the board
7958          * @private
7959          * @see JXG.Board#toFullscreen
7960          *
7961          */
7962         startFullscreenResizeObserver: function(node) {
7963             var that = this;
7964 
7965             if (!Env.isBrowser || !this.attr.resize || !this.attr.resize.enabled) {
7966                 return this;
7967             }
7968 
7969             this.resizeObserver = new ResizeObserver(function (entries) {
7970                 var inner_id,
7971                     fullscreenElement,
7972                     doc = that.document;
7973 
7974                 if (!that._isResizing) {
7975                     that._isResizing = true;
7976                     window.setTimeout(function () {
7977                         try {
7978                             inner_id = that._fullscreen_inner_id;
7979                             if (doc.fullscreenElement !== undefined) {
7980                                 fullscreenElement = doc.fullscreenElement;
7981                             } else if (doc.webkitFullscreenElement !== undefined) {
7982                                 fullscreenElement = doc.webkitFullscreenElement;
7983                             } else {
7984                                 fullscreenElement = doc.msFullscreenElement;
7985                             }
7986                             if (fullscreenElement !== null) {
7987                                 Env.scaleJSXGraphDiv(fullscreenElement.id, inner_id, doc,
7988                                     Type.evaluate(that.attr.fullscreen.scale));
7989                             }
7990                         } catch (err) {
7991                             that.stopFullscreenResizeObserver(node);
7992                         } finally {
7993                             that._isResizing = false;
7994                         }
7995                     }, that.attr.resize.throttle);
7996                 }
7997             });
7998             this.resizeObserver.observe(node);
7999             return this;
8000         },
8001 
8002         /**
8003          * Remove resize observer to handle orientation changes in fullscreen mode.
8004          * @param {Object} node DOM object which is in fullscreen mode. It is the wrapper element
8005          * around the JSXGraph div.
8006          * @returns {JXG.Board} Reference to the board
8007          * @private
8008          * @see JXG.Board#toFullscreen
8009          */
8010         stopFullscreenResizeObserver: function(node) {
8011             if (!Env.isBrowser || !this.attr.resize || !this.attr.resize.enabled) {
8012                 return this;
8013             }
8014 
8015             if (Type.exists(this.resizeObserver)) {
8016                 this.resizeObserver.unobserve(node);
8017             }
8018             return this;
8019         },
8020 
8021         /**
8022          * Add user activity to the array 'board.userLog'.
8023          *
8024          * @param {String} type Event type, e.g. 'drag'
8025          * @param {Object} obj JSXGraph element object
8026          *
8027          * @see JXG.Board#userLog
8028          * @return {JXG.Board} Reference to the board
8029          */
8030         addLogEntry: function (type, obj, pos) {
8031             var t, id,
8032                 last = this.userLog.length - 1;
8033 
8034             if (Type.exists(obj.elementClass)) {
8035                 id = obj.id;
8036             }
8037             if (Type.evaluate(this.attr.logging.enabled)) {
8038                 t = (new Date()).getTime();
8039                 if (last >= 0 &&
8040                     this.userLog[last].type === type &&
8041                     this.userLog[last].id === id &&
8042                     // Distinguish consecutive drag events of
8043                     // the same element
8044                     t - this.userLog[last].end < 500) {
8045 
8046                     this.userLog[last].end = t;
8047                     this.userLog[last].endpos = pos;
8048                 } else {
8049                     this.userLog.push({
8050                         type: type,
8051                         id: id,
8052                         start: t,
8053                         startpos: pos,
8054                         end: t,
8055                         endpos: pos,
8056                         bbox: this.getBoundingBox(),
8057                         canvas: [this.canvasWidth, this.canvasHeight],
8058                         zoom: [this.zoomX, this.zoomY]
8059                     });
8060                 }
8061             }
8062             return this;
8063         },
8064 
8065         /**
8066          * Function to animate a curve rolling on another curve.
8067          * @param {Curve} c1 JSXGraph curve building the floor where c2 rolls
8068          * @param {Curve} c2 JSXGraph curve which rolls on c1.
8069          * @param {number} start_c1 The parameter t such that c1(t) touches c2. This is the start position of the
8070          *                          rolling process
8071          * @param {Number} stepsize Increase in t in each step for the curve c1
8072          * @param {Number} direction
8073          * @param {Number} time Delay time for setInterval()
8074          * @param {Array} pointlist Array of points which are rolled in each step. This list should contain
8075          *      all points which define c2 and gliders on c2.
8076          *
8077          * @example
8078          *
8079          * // Line which will be the floor to roll upon.
8080          * var line = board.create('curve', [function (t) { return t;}, function (t){ return 1;}], {strokeWidth:6});
8081          * // Center of the rolling circle
8082          * var C = board.create('point',[0,2],{name:'C'});
8083          * // Starting point of the rolling circle
8084          * var P = board.create('point',[0,1],{name:'P', trace:true});
8085          * // Circle defined as a curve. The circle 'starts' at P, i.e. circle(0) = P
8086          * var circle = board.create('curve',[
8087          *           function (t){var d = P.Dist(C),
8088          *                           beta = JXG.Math.Geometry.rad([C.X()+1,C.Y()],C,P);
8089          *                       t += beta;
8090          *                       return C.X()+d*Math.cos(t);
8091          *           },
8092          *           function (t){var d = P.Dist(C),
8093          *                           beta = JXG.Math.Geometry.rad([C.X()+1,C.Y()],C,P);
8094          *                       t += beta;
8095          *                       return C.Y()+d*Math.sin(t);
8096          *           },
8097          *           0,2*Math.PI],
8098          *           {strokeWidth:6, strokeColor:'green'});
8099          *
8100          * // Point on circle
8101          * var B = board.create('glider',[0,2,circle],{name:'B', color:'blue',trace:false});
8102          * var roll = board.createRoulette(line, circle, 0, Math.PI/20, 1, 100, [C,P,B]);
8103          * roll.start() // Start the rolling, to be stopped by roll.stop()
8104          *
8105          * </pre><div class='jxgbox' id='JXGe5e1b53c-a036-4a46-9e35-190d196beca5' style='width: 300px; height: 300px;'></div>
8106          * <script type='text/javascript'>
8107          * var brd = JXG.JSXGraph.initBoard('JXGe5e1b53c-a036-4a46-9e35-190d196beca5', {boundingbox: [-5, 5, 5, -5], axis: true, showcopyright:false, shownavigation: false});
8108          * // Line which will be the floor to roll upon.
8109          * var line = brd.create('curve', [function (t) { return t;}, function (t){ return 1;}], {strokeWidth:6});
8110          * // Center of the rolling circle
8111          * var C = brd.create('point',[0,2],{name:'C'});
8112          * // Starting point of the rolling circle
8113          * var P = brd.create('point',[0,1],{name:'P', trace:true});
8114          * // Circle defined as a curve. The circle 'starts' at P, i.e. circle(0) = P
8115          * var circle = brd.create('curve',[
8116          *           function (t){var d = P.Dist(C),
8117          *                           beta = JXG.Math.Geometry.rad([C.X()+1,C.Y()],C,P);
8118          *                       t += beta;
8119          *                       return C.X()+d*Math.cos(t);
8120          *           },
8121          *           function (t){var d = P.Dist(C),
8122          *                           beta = JXG.Math.Geometry.rad([C.X()+1,C.Y()],C,P);
8123          *                       t += beta;
8124          *                       return C.Y()+d*Math.sin(t);
8125          *           },
8126          *           0,2*Math.PI],
8127          *           {strokeWidth:6, strokeColor:'green'});
8128          *
8129          * // Point on circle
8130          * var B = brd.create('glider',[0,2,circle],{name:'B', color:'blue',trace:false});
8131          * var roll = brd.createRoulette(line, circle, 0, Math.PI/20, 1, 100, [C,P,B]);
8132          * roll.start() // Start the rolling, to be stopped by roll.stop()
8133          * </script><pre>
8134          */
8135         createRoulette: function (c1, c2, start_c1, stepsize, direction, time, pointlist) {
8136             var brd = this,
8137                 Roulette = function () {
8138                     var alpha = 0,
8139                         Tx = 0,
8140                         Ty = 0,
8141                         t1 = start_c1,
8142                         t2 = Numerics.root(
8143                             function (t) {
8144                                 var c1x = c1.X(t1),
8145                                     c1y = c1.Y(t1),
8146                                     c2x = c2.X(t),
8147                                     c2y = c2.Y(t);
8148 
8149                                 return (c1x - c2x) * (c1x - c2x) + (c1y - c2y) * (c1y - c2y);
8150                             },
8151                             [0, Math.PI * 2]
8152                         ),
8153                         t1_new = 0.0,
8154                         t2_new = 0.0,
8155                         c1dist,
8156                         rotation = brd.create(
8157                             'transform',
8158                             [
8159                                 function () {
8160                                     return alpha;
8161                                 }
8162                             ],
8163                             { type: 'rotate' }
8164                         ),
8165                         rotationLocal = brd.create(
8166                             'transform',
8167                             [
8168                                 function () {
8169                                     return alpha;
8170                                 },
8171                                 function () {
8172                                     return c1.X(t1);
8173                                 },
8174                                 function () {
8175                                     return c1.Y(t1);
8176                                 }
8177                             ],
8178                             { type: 'rotate' }
8179                         ),
8180                         translate = brd.create(
8181                             'transform',
8182                             [
8183                                 function () {
8184                                     return Tx;
8185                                 },
8186                                 function () {
8187                                     return Ty;
8188                                 }
8189                             ],
8190                             { type: 'translate' }
8191                         ),
8192                         // arc length via Simpson's rule.
8193                         arclen = function (c, a, b) {
8194                             var cpxa = Numerics.D(c.X)(a),
8195                                 cpya = Numerics.D(c.Y)(a),
8196                                 cpxb = Numerics.D(c.X)(b),
8197                                 cpyb = Numerics.D(c.Y)(b),
8198                                 cpxab = Numerics.D(c.X)((a + b) * 0.5),
8199                                 cpyab = Numerics.D(c.Y)((a + b) * 0.5),
8200                                 fa = Mat.hypot(cpxa, cpya),
8201                                 fb = Mat.hypot(cpxb, cpyb),
8202                                 fab = Mat.hypot(cpxab, cpyab);
8203 
8204                             return ((fa + 4 * fab + fb) * (b - a)) / 6;
8205                         },
8206                         exactDist = function (t) {
8207                             return c1dist - arclen(c2, t2, t);
8208                         },
8209                         beta = Math.PI / 18,
8210                         beta9 = beta * 9,
8211                         interval = null;
8212 
8213                     this.rolling = function () {
8214                         var h, g, hp, gp, z;
8215 
8216                         t1_new = t1 + direction * stepsize;
8217 
8218                         // arc length between c1(t1) and c1(t1_new)
8219                         c1dist = arclen(c1, t1, t1_new);
8220 
8221                         // find t2_new such that arc length between c2(t2) and c1(t2_new) equals c1dist.
8222                         t2_new = Numerics.root(exactDist, t2);
8223 
8224                         // c1(t) as complex number
8225                         h = new Complex(c1.X(t1_new), c1.Y(t1_new));
8226 
8227                         // c2(t) as complex number
8228                         g = new Complex(c2.X(t2_new), c2.Y(t2_new));
8229 
8230                         hp = new Complex(Numerics.D(c1.X)(t1_new), Numerics.D(c1.Y)(t1_new));
8231                         gp = new Complex(Numerics.D(c2.X)(t2_new), Numerics.D(c2.Y)(t2_new));
8232 
8233                         // z is angle between the tangents of c1 at t1_new, and c2 at t2_new
8234                         z = Complex.C.div(hp, gp);
8235 
8236                         alpha = Math.atan2(z.imaginary, z.real);
8237                         // Normalizing the quotient
8238                         z.div(Complex.C.abs(z));
8239                         z.mult(g);
8240                         Tx = h.real - z.real;
8241 
8242                         // T = h(t1_new)-g(t2_new)*h'(t1_new)/g'(t2_new);
8243                         Ty = h.imaginary - z.imaginary;
8244 
8245                         // -(10-90) degrees: make corners roll smoothly
8246                         if (alpha < -beta && alpha > -beta9) {
8247                             alpha = -beta;
8248                             rotationLocal.applyOnce(pointlist);
8249                         } else if (alpha > beta && alpha < beta9) {
8250                             alpha = beta;
8251                             rotationLocal.applyOnce(pointlist);
8252                         } else {
8253                             rotation.applyOnce(pointlist);
8254                             translate.applyOnce(pointlist);
8255                             t1 = t1_new;
8256                             t2 = t2_new;
8257                         }
8258                         brd.update();
8259                     };
8260 
8261                     this.start = function () {
8262                         if (time > 0) {
8263                             interval = window.setInterval(this.rolling, time);
8264                         }
8265                         return this;
8266                     };
8267 
8268                     this.stop = function () {
8269                         window.clearInterval(interval);
8270                         return this;
8271                     };
8272                     return this;
8273                 };
8274             return new Roulette();
8275         }
8276     }
8277 );
8278 
8279 export default JXG.Board;
8280