1 /*
  2     Copyright 2008-2023
  3         Matthias Ehmann,
  4         Michael Gerhaeuser,
  5         Carsten Miller,
  6         Bianca Valentin,
  7         Alfred Wassermann,
  8         Peter Wilfahrt
  9 
 10     This file is part of JSXGraph.
 11 
 12     JSXGraph is free software dual licensed under the GNU LGPL or MIT License.
 13 
 14     You can redistribute it and/or modify it under the terms of the
 15 
 16       * GNU Lesser General Public License as published by
 17         the Free Software Foundation, either version 3 of the License, or
 18         (at your option) any later version
 19       OR
 20       * MIT License: https://github.com/jsxgraph/jsxgraph/blob/master/LICENSE.MIT
 21 
 22     JSXGraph is distributed in the hope that it will be useful,
 23     but WITHOUT ANY WARRANTY; without even the implied warranty of
 24     MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 25     GNU Lesser General Public License for more details.
 26 
 27     You should have received a copy of the GNU Lesser General Public License and
 28     the MIT License along with JSXGraph. If not, see <https://www.gnu.org/licenses/>
 29     and <https://opensource.org/licenses/MIT/>.
 30  */
 31 

 33 
 34 /*jslint nomen: true, plusplus: true*/
 35 
 36 /**
 37  * @fileoverview The JXG.Board class is defined in this file. JXG.Board controls all properties and methods
 38  * used to manage a geonext board like managing geometric elements, managing mouse and touch events, etc.
 39  */
 40 
 41 import JXG from '../jxg';
 42 import Const from './constants';
 43 import Coords from './coords';
 44 import Options from '../options';
 45 import Numerics from '../math/numerics';
 46 import Mat from '../math/math';
 47 import Geometry from '../math/geometry';
 48 import Complex from '../math/complex';
 49 import Statistics from '../math/statistics';
 50 import JessieCode from '../parser/jessiecode';
 51 import Color from '../utils/color';
 52 import Type from '../utils/type';
 53 import EventEmitter from '../utils/event';
 54 import Env from '../utils/env';
 55 import Composition from './composition';
 56 
 57 /**
 58  * Constructs a new Board object.
 59  * @class JXG.Board controls all properties and methods used to manage a geonext board like managing geometric
 60  * elements, managing mouse and touch events, etc. You probably don't want to use this constructor directly.
 61  * Please use {@link JXG.JSXGraph.initBoard} to initialize a board.
 62  * @constructor
 63  * @param {String} container The id or reference of the HTML DOM element the board is drawn in. This is usually a HTML div.
 64  * @param {JXG.AbstractRenderer} renderer The reference of a renderer.
 65  * @param {String} id Unique identifier for the board, may be an empty string or null or even undefined.
 66  * @param {JXG.Coords} origin The coordinates where the origin is placed, in user coordinates.
 67  * @param {Number} zoomX Zoom factor in x-axis direction
 68  * @param {Number} zoomY Zoom factor in y-axis direction
 69  * @param {Number} unitX Units in x-axis direction
 70  * @param {Number} unitY Units in y-axis direction
 71  * @param {Number} canvasWidth  The width of canvas
 72  * @param {Number} canvasHeight The height of canvas
 73  * @param {Object} attributes The attributes object given to {@link JXG.JSXGraph.initBoard}
 74  * @borrows JXG.EventEmitter#on as this.on
 75  * @borrows JXG.EventEmitter#off as this.off
 76  * @borrows JXG.EventEmitter#triggerEventHandlers as this.triggerEventHandlers
 77  * @borrows JXG.EventEmitter#eventHandlers as this.eventHandlers
 78  */
 79 JXG.Board = function (container, renderer, id,
 80     origin, zoomX, zoomY, unitX, unitY,
 81     canvasWidth, canvasHeight, attributes) {
 82     /**
 83      * Board is in no special mode, objects are highlighted on mouse over and objects may be
 84      * clicked to start drag&drop.
 85      * @type Number
 86      * @constant
 87      */
 88     this.BOARD_MODE_NONE = 0x0000;
 89 
 90     /**
 91      * Board is in drag mode, objects aren't highlighted on mouse over and the object referenced in
 92      * {@link JXG.Board#mouse} is updated on mouse movement.
 93      * @type Number
 94      * @constant
 95      */
 96     this.BOARD_MODE_DRAG = 0x0001;
 97 
 98     /**
 99      * In this mode a mouse move changes the origin's screen coordinates.
100      * @type Number
101      * @constant
102      */
103     this.BOARD_MODE_MOVE_ORIGIN = 0x0002;
104 
105     /**
106      * Update is made with high quality, e.g. graphs are evaluated at much more points.
107      * @type Number
108      * @constant
109      * @see JXG.Board#updateQuality
110      */
111     this.BOARD_MODE_ZOOM = 0x0011;
112 
113     /**
114      * Update is made with low quality, e.g. graphs are evaluated at a lesser amount of points.
115      * @type Number
116      * @constant
117      * @see JXG.Board#updateQuality
118      */
119     this.BOARD_QUALITY_LOW = 0x1;
120 
121     /**
122      * Update is made with high quality, e.g. graphs are evaluated at much more points.
123      * @type Number
124      * @constant
125      * @see JXG.Board#updateQuality
126      */
127     this.BOARD_QUALITY_HIGH = 0x2;
128 
129     /**
130      * Pointer to the document element containing the board.
131      * @type Object
132      */
133     if (Type.exists(attributes.document) && attributes.document !== false) {
134         this.document = attributes.document;
135     } else if (Env.isBrowser) {
136         this.document = document;
137     }
138 
139     /**
140      * The html-id of the html element containing the board.
141      * @type String
142      */
143     this.container = container;
144 
145     /**
146      * Pointer to the html element containing the board.
147      * @type Object
148      */
149     this.containerObj = (Env.isBrowser ? this.document.getElementById(this.container) : null);
150 
151     if (Env.isBrowser && renderer.type !== 'no' && this.containerObj === null) {
152         throw new Error('\nJSXGraph: HTML container element "' + container + '" not found.');
153     }
154 
155     /**
156      * A reference to this boards renderer.
157      * @type JXG.AbstractRenderer
158      * @name JXG.Board#renderer
159      * @private
160      * @ignore
161      */
162     this.renderer = renderer;
163 
164     /**
165      * Grids keeps track of all grids attached to this board.
166      * @type Array
167      * @private
168      */
169     this.grids = [];
170 
171     /**
172      * Some standard options
173      * @type JXG.Options
174      */
175     this.options = Type.deepCopy(Options);
176     this.attr = attributes;
177 
178     /**
179      * Dimension of the board.
180      * @default 2
181      * @type Number
182      */
183     this.dimension = 2;
184 
185     this.jc = new JessieCode();
186     this.jc.use(this);
187 
188     /**
189      * Coordinates of the boards origin. This a object with the two properties
190      * usrCoords and scrCoords. usrCoords always equals [1, 0, 0] and scrCoords
191      * stores the boards origin in homogeneous screen coordinates.
192      * @type Object
193      * @private
194      */
195     this.origin = {};
196     this.origin.usrCoords = [1, 0, 0];
197     this.origin.scrCoords = [1, origin[0], origin[1]];
198 
199     /**
200      * Zoom factor in X direction. It only stores the zoom factor to be able
201      * to get back to 100% in zoom100().
202      * @name JXG.Board.zoomX
203      * @type Number
204      * @private
205      * @ignore
206      */
207     this.zoomX = zoomX;
208 
209     /**
210      * Zoom factor in Y direction. It only stores the zoom factor to be able
211      * to get back to 100% in zoom100().
212      * @name JXG.Board.zoomY
213      * @type Number
214      * @private
215      * @ignore
216      */
217     this.zoomY = zoomY;
218 
219     /**
220      * The number of pixels which represent one unit in user-coordinates in x direction.
221      * @type Number
222      * @private
223      */
224     this.unitX = unitX * this.zoomX;
225 
226     /**
227      * The number of pixels which represent one unit in user-coordinates in y direction.
228      * @type Number
229      * @private
230      */
231     this.unitY = unitY * this.zoomY;
232 
233     /**
234      * Keep aspect ratio if bounding box is set and the width/height ratio differs from the
235      * width/height ratio of the canvas.
236      * @type Boolean
237      * @private
238      */
239     this.keepaspectratio = false;
240 
241     /**
242      * Canvas width.
243      * @type Number
244      * @private
245      */
246     this.canvasWidth = canvasWidth;
247 
248     /**
249      * Canvas Height
250      * @type Number
251      * @private
252      */
253     this.canvasHeight = canvasHeight;
254 
255     // If the given id is not valid, generate an unique id
256     if (Type.exists(id) && id !== '' && Env.isBrowser && !Type.exists(this.document.getElementById(id))) {
257         this.id = id;
258     } else {
259         this.id = this.generateId();
260     }
261 
262     EventEmitter.eventify(this);
263 
264     this.hooks = [];
265 
266     /**
267      * An array containing all other boards that are updated after this board has been updated.
268      * @type Array
269      * @see JXG.Board#addChild
270      * @see JXG.Board#removeChild
271      */
272     this.dependentBoards = [];
273 
274     /**
275      * During the update process this is set to false to prevent an endless loop.
276      * @default false
277      * @type Boolean
278      */
279     this.inUpdate = false;
280 
281     /**
282      * 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.
283      * @type Object
284      */
285     this.objects = {};
286 
287     /**
288      * An array containing all geometric objects on the board in the order of construction.
289      * @type Array
290      */
291     this.objectsList = [];
292 
293     /**
294      * 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.
295      * @type Object
296      */
297     this.groups = {};
298 
299     /**
300      * Stores all the objects that are currently running an animation.
301      * @type Object
302      */
303     this.animationObjects = {};
304 
305     /**
306      * An associative array containing all highlighted elements belonging to the board.
307      * @type Object
308      */
309     this.highlightedObjects = {};
310 
311     /**
312      * Number of objects ever created on this board. This includes every object, even invisible and deleted ones.
313      * @type Number
314      */
315     this.numObjects = 0;
316 
317     /**
318      * 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.
319      * @type Object
320      */
321     this.elementsByName = {};
322 
323     /**
324      * The board mode the board is currently in. Possible values are
325      * <ul>
326      * <li>JXG.Board.BOARD_MODE_NONE</li>
327      * <li>JXG.Board.BOARD_MODE_DRAG</li>
328      * <li>JXG.Board.BOARD_MODE_MOVE_ORIGIN</li>
329      * </ul>
330      * @type Number
331      */
332     this.mode = this.BOARD_MODE_NONE;
333 
334     /**
335      * The update quality of the board. In most cases this is set to {@link JXG.Board#BOARD_QUALITY_HIGH}.
336      * If {@link JXG.Board#mode} equals {@link JXG.Board#BOARD_MODE_DRAG} this is set to
337      * {@link JXG.Board#BOARD_QUALITY_LOW} to speed up the update process by e.g. reducing the number of
338      * evaluation points when plotting functions. Possible values are
339      * <ul>
340      * <li>BOARD_QUALITY_LOW</li>
341      * <li>BOARD_QUALITY_HIGH</li>
342      * </ul>
343      * @type Number
344      * @see JXG.Board#mode
345      */
346     this.updateQuality = this.BOARD_QUALITY_HIGH;
347 
348     /**
349      * If true updates are skipped.
350      * @type Boolean
351      */
352     this.isSuspendedRedraw = false;
353 
354     this.calculateSnapSizes();
355 
356     /**
357      * The distance from the mouse to the dragged object in x direction when the user clicked the mouse button.
358      * @type Number
359      * @see JXG.Board#drag_dy
360      */
361     this.drag_dx = 0;
362 
363     /**
364      * The distance from the mouse to the dragged object in y direction when the user clicked the mouse button.
365      * @type Number
366      * @see JXG.Board#drag_dx
367      */
368     this.drag_dy = 0;
369 
370     /**
371      * The last position where a drag event has been fired.
372      * @type Array
373      * @see JXG.Board#moveObject
374      */
375     this.drag_position = [0, 0];
376 
377     /**
378      * References to the object that is dragged with the mouse on the board.
379      * @type JXG.GeometryElement
380      * @see JXG.Board#touches
381      */
382     this.mouse = {};
383 
384     /**
385      * Keeps track on touched elements, like {@link JXG.Board#mouse} does for mouse events.
386      * @type Array
387      * @see JXG.Board#mouse
388      */
389     this.touches = [];
390 
391     /**
392      * A string containing the XML text of the construction.
393      * This is set in {@link JXG.FileReader.parseString}.
394      * Only useful if a construction is read from a GEONExT-, Intergeo-, Geogebra-, or Cinderella-File.
395      * @type String
396      */
397     this.xmlString = '';
398 
399     /**
400      * Cached result of getCoordsTopLeftCorner for touch/mouseMove-Events to save some DOM operations.
401      * @type Array
402      */
403     this.cPos = [];
404 
405     /**
406      * Contains the last time (epoch, msec) since the last touchMove event which was not thrown away or since
407      * touchStart because Android's Webkit browser fires too much of them.
408      * @type Number
409      */
410     this.touchMoveLast = 0;
411 
412     /**
413      * Contains the pointerId of the last touchMove event which was not thrown away or since
414      * touchStart because Android's Webkit browser fires too much of them.
415      * @type Number
416      */
417     this.touchMoveLastId = Infinity;
418 
419     /**
420      * Contains the last time (epoch, msec) since the last getCoordsTopLeftCorner call which was not thrown away.
421      * @type Number
422      */
423     this.positionAccessLast = 0;
424 
425     /**
426      * Collects all elements that triggered a mouse down event.
427      * @type Array
428      */
429     this.downObjects = [];
430 
431     /**
432      * Collects all elements that have keyboard focus. Should be either one or no element.
433      * Elements are stored with their id.
434      * @type Array
435      */
436     this.focusObjects = [];
437 
438     if (this.attr.showcopyright) {
439         this.renderer.displayCopyright(Const.licenseText, parseInt(this.options.text.fontSize, 10));
440     }
441 
442     /**
443      * Full updates are needed after zoom and axis translates. This saves some time during an update.
444      * @default false
445      * @type Boolean
446      */
447     this.needsFullUpdate = false;
448 
449     /**
450      * If reducedUpdate is set to true then only the dragged element and few (e.g. 2) following
451      * elements are updated during mouse move. On mouse up the whole construction is
452      * updated. This enables us to be fast even on very slow devices.
453      * @type Boolean
454      * @default false
455      */
456     this.reducedUpdate = false;
457 
458     /**
459      * The current color blindness deficiency is stored in this property. If color blindness is not emulated
460      * at the moment, it's value is 'none'.
461      */
462     this.currentCBDef = 'none';
463 
464     /**
465      * If GEONExT constructions are displayed, then this property should be set to true.
466      * At the moment there should be no difference. But this may change.
467      * This is set in {@link JXG.GeonextReader#readGeonext}.
468      * @type Boolean
469      * @default false
470      * @see JXG.GeonextReader#readGeonext
471      */
472     this.geonextCompatibilityMode = false;
473 
474     if (this.options.text.useASCIIMathML && translateASCIIMath) {
475         init();
476     } else {
477         this.options.text.useASCIIMathML = false;
478     }
479 
480     /**
481      * A flag which tells if the board registers mouse events.
482      * @type Boolean
483      * @default false
484      */
485     this.hasMouseHandlers = false;
486 
487     /**
488      * A flag which tells if the board registers touch events.
489      * @type Boolean
490      * @default false
491      */
492     this.hasTouchHandlers = false;
493 
494     /**
495      * A flag which stores if the board registered pointer events.
496      * @type Boolean
497      * @default false
498      */
499     this.hasPointerHandlers = false;
500 
501     /**
502      * A flag which tells if the board the JXG.Board#mouseUpListener is currently registered.
503      * @type Boolean
504      * @default false
505      */
506     this.hasMouseUp = false;
507 
508     /**
509      * A flag which tells if the board the JXG.Board#touchEndListener is currently registered.
510      * @type Boolean
511      * @default false
512      */
513     this.hasTouchEnd = false;
514 
515     /**
516      * A flag which tells us if the board has a pointerUp event registered at the moment.
517      * @type Boolean
518      * @default false
519      */
520     this.hasPointerUp = false;
521 
522     /**
523      * Offset for large coords elements like images
524      * @type Array
525      * @private
526      * @default [0, 0]
527      */
528     this._drag_offset = [0, 0];
529 
530     /**
531      * Stores the input device used in the last down or move event.
532      * @type String
533      * @private
534      * @default 'mouse'
535      */
536     this._inputDevice = 'mouse';
537 
538     /**
539      * Keeps a list of pointer devices which are currently touching the screen.
540      * @type Array
541      * @private
542      */
543     this._board_touches = [];
544 
545     /**
546      * A flag which tells us if the board is in the selecting mode
547      * @type Boolean
548      * @default false
549      */
550     this.selectingMode = false;
551 
552     /**
553      * A flag which tells us if the user is selecting
554      * @type Boolean
555      * @default false
556      */
557     this.isSelecting = false;
558 
559     /**
560      * A flag which tells us if the user is scrolling the viewport
561      * @type Boolean
562      * @private
563      * @default false
564      * @see JXG.Board#scrollListener
565      */
566     this._isScrolling = false;
567 
568     /**
569      * A flag which tells us if a resize is in process
570      * @type Boolean
571      * @private
572      * @default false
573      * @see JXG.Board#resizeListener
574      */
575     this._isResizing = false;
576 
577     /**
578      * A bounding box for the selection
579      * @type Array
580      * @default [ [0,0], [0,0] ]
581      */
582     this.selectingBox = [[0, 0], [0, 0]];
583 
584     /**
585      * Array to log user activity.
586      * Entries are objects of the form '{type, id, start, end}' notifying
587      * the start time as well as the last time of a single event of type 'type'
588      * on a JSXGraph element of id 'id'.
589      * <p> 'start' and 'end' contain the amount of milliseconds elapsed between 1 January 1970 00:00:00 UTC
590      * and the time the event happened.
591      * <p>
592      * For the time being (i.e. v1.5.0) the only supported type is 'drag'.
593      * @type Array
594      */
595     this.userLog = [];
596 
597     this.mathLib = Math;        // Math or JXG.Math.IntervalArithmetic
598     this.mathLibJXG = JXG.Math; // JXG.Math or JXG.Math.IntervalArithmetic
599 
600     if (this.attr.registerevents) {
601         this.addEventHandlers();
602     }
603     if (this.attr.registerresizeevent) {
604         this.addResizeEventHandlers();
605     }
606     if (this.attr.registerfullscreenevent) {
607         this.addFullscreenEventHandlers();
608     }
609 
610     this.methodMap = {
611         update: 'update',
612         fullUpdate: 'fullUpdate',
613         on: 'on',
614         off: 'off',
615         trigger: 'trigger',
616         setView: 'setBoundingBox',
617         setBoundingBox: 'setBoundingBox',
618         migratePoint: 'migratePoint',
619         colorblind: 'emulateColorblindness',
620         suspendUpdate: 'suspendUpdate',
621         unsuspendUpdate: 'unsuspendUpdate',
622         clearTraces: 'clearTraces',
623         left: 'clickLeftArrow',
624         right: 'clickRightArrow',
625         up: 'clickUpArrow',
626         down: 'clickDownArrow',
627         zoomIn: 'zoomIn',
628         zoomOut: 'zoomOut',
629         zoom100: 'zoom100',
630         zoomElements: 'zoomElements',
631         remove: 'removeObject',
632         removeObject: 'removeObject'
633     };
634 };
635 
636 JXG.extend(
637     JXG.Board.prototype,
638     /** @lends JXG.Board.prototype */ {
639         /**
640          * Generates an unique name for the given object. The result depends on the objects type, if the
641          * object is a {@link JXG.Point}, capital characters are used, if it is of type {@link JXG.Line}
642          * only lower case characters are used. If object is of type {@link JXG.Polygon}, a bunch of lower
643          * case characters prefixed with P_ are used. If object is of type {@link JXG.Circle} the name is
644          * generated using lower case characters. prefixed with k_ is used. In any other case, lower case
645          * chars prefixed with s_ is used.
646          * @param {Object} object Reference of an JXG.GeometryElement that is to be named.
647          * @returns {String} Unique name for the object.
648          */
649         generateName: function (object) {
650             var possibleNames, i,
651                 maxNameLength = this.attr.maxnamelength,
652                 pre = '',
653                 post = '',
654                 indices = [],
655                 name = '';
656 
657             if (object.type === Const.OBJECT_TYPE_TICKS) {
658                 return '';
659             }
660 
661             if (Type.isPoint(object) || Type.isPoint3D(object)) {
662                 // points have capital letters
663                 possibleNames = [
664                     '', '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'
665                 ];
666             } else if (object.type === Const.OBJECT_TYPE_ANGLE) {
667                 possibleNames = [
668                     '', 'α', 'β', 'γ', 'δ', 'ε', 'ζ', 'η', 'θ', 'ι', 'κ', 'λ',
669                     'μ', 'ν', 'ξ', 'ο', 'π', 'ρ', 'σ', 'τ', 'υ', 'φ', 'χ', 'ψ', 'ω'
670                 ];
671             } else {
672                 // all other elements get lowercase labels
673                 possibleNames = [
674                     '', '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'
675                 ];
676             }
677 
678             if (
679                 !Type.isPoint(object) &&
680                 object.elementClass !== Const.OBJECT_CLASS_LINE &&
681                 object.type !== Const.OBJECT_TYPE_ANGLE
682             ) {
683                 if (object.type === Const.OBJECT_TYPE_POLYGON) {
684                     pre = 'P_{';
685                 } else if (object.elementClass === Const.OBJECT_CLASS_CIRCLE) {
686                     pre = 'k_{';
687                 } else if (object.elementClass === Const.OBJECT_CLASS_TEXT) {
688                     pre = 't_{';
689                 } else {
690                     pre = 's_{';
691                 }
692                 post = '}';
693             }
694 
695             for (i = 0; i < maxNameLength; i++) {
696                 indices[i] = 0;
697             }
698 
699             while (indices[maxNameLength - 1] < possibleNames.length) {
700                 for (indices[0] = 1; indices[0] < possibleNames.length; indices[0]++) {
701                     name = pre;
702 
703                     for (i = maxNameLength; i > 0; i--) {
704                         name += possibleNames[indices[i - 1]];
705                     }
706 
707                     if (!Type.exists(this.elementsByName[name + post])) {
708                         return name + post;
709                     }
710                 }
711                 indices[0] = possibleNames.length;
712 
713                 for (i = 1; i < maxNameLength; i++) {
714                     if (indices[i - 1] === possibleNames.length) {
715                         indices[i - 1] = 1;
716                         indices[i] += 1;
717                     }
718                 }
719             }
720 
721             return '';
722         },
723 
724         /**
725          * Generates unique id for a board. The result is randomly generated and prefixed with 'jxgBoard'.
726          * @returns {String} Unique id for a board.
727          */
728         generateId: function () {
729             var r = 1;
730 
731             // as long as we don't have a unique id generate a new one
732             while (Type.exists(JXG.boards['jxgBoard' + r])) {
733                 r = Math.round(Math.random() * 65535);
734             }
735 
736             return 'jxgBoard' + r;
737         },
738 
739         /**
740          * Composes an id for an element. If the ID is empty ('' or null) a new ID is generated, depending on the
741          * object type. As a side effect {@link JXG.Board#numObjects}
742          * is updated.
743          * @param {Object} obj Reference of an geometry object that needs an id.
744          * @param {Number} type Type of the object.
745          * @returns {String} Unique id for an element.
746          */
747         setId: function (obj, type) {
748             var randomNumber,
749                 num = this.numObjects,
750                 elId = obj.id;
751 
752             this.numObjects += 1;
753 
754             // If no id is provided or id is empty string, a new one is chosen
755             if (elId === '' || !Type.exists(elId)) {
756                 elId = this.id + type + num;
757                 while (Type.exists(this.objects[elId])) {
758                     randomNumber = Math.round(Math.random() * 65535);
759                     elId = this.id + type + num + '-' + randomNumber;
760                 }
761             }
762 
763             obj.id = elId;
764             this.objects[elId] = obj;
765             obj._pos = this.objectsList.length;
766             this.objectsList[this.objectsList.length] = obj;
767 
768             return elId;
769         },
770 
771         /**
772          * After construction of the object the visibility is set
773          * and the label is constructed if necessary.
774          * @param {Object} obj The object to add.
775          */
776         finalizeAdding: function (obj) {
777             if (Type.evaluate(obj.visProp.visible) === false) {
778                 this.renderer.display(obj, false);
779             }
780         },
781 
782         finalizeLabel: function (obj) {
783             if (
784                 obj.hasLabel &&
785                 !Type.evaluate(obj.label.visProp.islabel) &&
786                 Type.evaluate(obj.label.visProp.visible) === false
787             ) {
788                 this.renderer.display(obj.label, false);
789             }
790         },
791 
792         /**********************************************************
793          *
794          * Event Handler helpers
795          *
796          **********************************************************/
797 
798         /**
799          * Returns false if the event has been triggered faster than the maximum frame rate.
800          *
801          * @param {Event} evt Event object given by the browser (unused)
802          * @returns {Boolean} If the event has been triggered faster than the maximum frame rate, false is returned.
803          * @private
804          * @see JXG.Board#pointerMoveListener
805          * @see JXG.Board#touchMoveListener
806          * @see JXG.Board#mouseMoveListener
807          */
808         checkFrameRate: function (evt) {
809             var handleEvt = false,
810                 time = new Date().getTime();
811 
812             if (Type.exists(evt.pointerId) && this.touchMoveLastId !== evt.pointerId) {
813                 handleEvt = true;
814                 this.touchMoveLastId = evt.pointerId;
815             }
816             if (!handleEvt && (time - this.touchMoveLast) * this.attr.maxframerate >= 1000) {
817                 handleEvt = true;
818             }
819             if (handleEvt) {
820                 this.touchMoveLast = time;
821             }
822             return handleEvt;
823         },
824 
825         /**
826          * Calculates mouse coordinates relative to the boards container.
827          * @returns {Array} Array of coordinates relative the boards container top left corner.
828          */
829         getCoordsTopLeftCorner: function () {
830             var cPos,
831                 doc,
832                 crect,
833                 // In ownerDoc we need the 'real' document object.
834                 // The first version is used in the case of shadowDom,
835                 // the second case in the 'normal' case.
836                 ownerDoc = this.document.ownerDocument || this.document,
837                 docElement = ownerDoc.documentElement || this.document.body.parentNode,
838                 docBody = ownerDoc.body,
839                 container = this.containerObj,
840                 // viewport, content,
841                 zoom,
842                 o;
843 
844             /**
845              * During drags and origin moves the container element is usually not changed.
846              * Check the position of the upper left corner at most every 1000 msecs
847              */
848             if (
849                 this.cPos.length > 0 &&
850                 (this.mode === this.BOARD_MODE_DRAG ||
851                     this.mode === this.BOARD_MODE_MOVE_ORIGIN ||
852                     new Date().getTime() - this.positionAccessLast < 1000)
853             ) {
854                 return this.cPos;
855             }
856             this.positionAccessLast = new Date().getTime();
857 
858             // Check if getBoundingClientRect exists. If so, use this as this covers *everything*
859             // even CSS3D transformations etc.
860             // Supported by all browsers but IE 6, 7.
861 
862             if (container.getBoundingClientRect) {
863                 crect = container.getBoundingClientRect();
864 
865                 zoom = 1.0;
866                 // Recursively search for zoom style entries.
867                 // This is necessary for reveal.js on webkit.
868                 // It fails if the user does zooming
869                 o = container;
870                 while (o && Type.exists(o.parentNode)) {
871                     if (
872                         Type.exists(o.style) &&
873                         Type.exists(o.style.zoom) &&
874                         o.style.zoom !== ''
875                     ) {
876                         zoom *= parseFloat(o.style.zoom);
877                     }
878                     o = o.parentNode;
879                 }
880                 cPos = [crect.left * zoom, crect.top * zoom];
881 
882                 // add border width
883                 cPos[0] += Env.getProp(container, 'border-left-width');
884                 cPos[1] += Env.getProp(container, 'border-top-width');
885 
886                 // vml seems to ignore paddings
887                 if (this.renderer.type !== 'vml') {
888                     // add padding
889                     cPos[0] += Env.getProp(container, 'padding-left');
890                     cPos[1] += Env.getProp(container, 'padding-top');
891                 }
892 
893                 this.cPos = cPos.slice();
894                 return this.cPos;
895             }
896 
897             //
898             //  OLD CODE
899             //  IE 6-7 only:
900             //
901             cPos = Env.getOffset(container);
902             doc = this.document.documentElement.ownerDocument;
903 
904             if (!this.containerObj.currentStyle && doc.defaultView) {
905                 // Non IE
906                 // this is for hacks like this one used in wordpress for the admin bar:
907                 // html { margin-top: 28px }
908                 // seems like it doesn't work in IE
909 
910                 cPos[0] += Env.getProp(docElement, 'margin-left');
911                 cPos[1] += Env.getProp(docElement, 'margin-top');
912 
913                 cPos[0] += Env.getProp(docElement, 'border-left-width');
914                 cPos[1] += Env.getProp(docElement, 'border-top-width');
915 
916                 cPos[0] += Env.getProp(docElement, 'padding-left');
917                 cPos[1] += Env.getProp(docElement, 'padding-top');
918             }
919 
920             if (docBody) {
921                 cPos[0] += Env.getProp(docBody, 'left');
922                 cPos[1] += Env.getProp(docBody, 'top');
923             }
924 
925             // Google Translate offers widgets for web authors. These widgets apparently tamper with the clientX
926             // and clientY coordinates of the mouse events. The minified sources seem to be the only publicly
927             // available version so we're doing it the hacky way: Add a fixed offset.


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