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