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