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