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