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