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


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