1 /*
  2     Copyright 2008-2023
  3         Matthias Ehmann,
  4         Michael Gerhaeuser,
  5         Carsten Miller,
  6         Bianca Valentin,
  7         Alfred Wassermann,
  8         Peter Wilfahrt
  9 
 10     This file is part of JSXGraph.
 11 
 12     JSXGraph is free software dual licensed under the GNU LGPL or MIT License.
 13 
 14     You can redistribute it and/or modify it under the terms of the
 15 
 16       * GNU Lesser General Public License as published by
 17         the Free Software Foundation, either version 3 of the License, or
 18         (at your option) any later version
 19       OR
 20       * MIT License: https://github.com/jsxgraph/jsxgraph/blob/master/LICENSE.MIT
 21 
 22     JSXGraph is distributed in the hope that it will be useful,
 23     but WITHOUT ANY WARRANTY; without even the implied warranty of
 24     MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 25     GNU Lesser General Public License for more details.
 26 
 27     You should have received a copy of the GNU Lesser General Public License and
 28     the MIT License along with JSXGraph. If not, see <https://www.gnu.org/licenses/>
 29     and <https://opensource.org/licenses/MIT/>.
 30  */
 31 
 32 /*global JXG: true, define: true, AMprocessNode: true, MathJax: true, window: true, document: true, init: true, translateASCIIMath: true, google: true*/
 33 
 34 /*jslint nomen: true, plusplus: true*/
 35 
 36 /**
 37  * @fileoverview The JXG.Board class is defined in this file. JXG.Board controls all properties and methods
 38  * used to manage a geonext board like managing geometric elements, managing mouse and touch events, etc.
 39  */
 40 
 41 import JXG from '../jxg';
 42 import Const from './constants';
 43 import Coords from './coords';
 44 import Options from '../options';
 45 import Numerics from '../math/numerics';
 46 import Mat from '../math/math';
 47 import Geometry from '../math/geometry';
 48 import Complex from '../math/complex';
 49 import Statistics from '../math/statistics';
 50 import JessieCode from '../parser/jessiecode';
 51 import Color from '../utils/color';
 52 import Type from '../utils/type';
 53 import EventEmitter from '../utils/event';
 54 import Env from '../utils/env';
 55 import Composition from './composition';
 56 
 57 /**
 58  * Constructs a new Board object.
 59  * @class JXG.Board controls all properties and methods used to manage a geonext board like managing geometric
 60  * elements, managing mouse and touch events, etc. You probably don't want to use this constructor directly.
 61  * Please use {@link JXG.JSXGraph.initBoard} to initialize a board.
 62  * @constructor
 63  * @param {String} container The id or reference of the HTML DOM element the board is drawn in. This is usually a HTML div.
 64  * @param {JXG.AbstractRenderer} renderer The reference of a renderer.
 65  * @param {String} id Unique identifier for the board, may be an empty string or null or even undefined.
 66  * @param {JXG.Coords} origin The coordinates where the origin is placed, in user coordinates.
 67  * @param {Number} zoomX Zoom factor in x-axis direction
 68  * @param {Number} zoomY Zoom factor in y-axis direction
 69  * @param {Number} unitX Units in x-axis direction
 70  * @param {Number} unitY Units in y-axis direction
 71  * @param {Number} canvasWidth  The width of canvas
 72  * @param {Number} canvasHeight The height of canvas
 73  * @param {Object} attributes The attributes object given to {@link JXG.JSXGraph.initBoard}
 74  * @borrows JXG.EventEmitter#on as this.on
 75  * @borrows JXG.EventEmitter#off as this.off
 76  * @borrows JXG.EventEmitter#triggerEventHandlers as this.triggerEventHandlers
 77  * @borrows JXG.EventEmitter#eventHandlers as this.eventHandlers
 78  */
 79 JXG.Board = function (container, renderer, id,
 80     origin, zoomX, zoomY, unitX, unitY,
 81     canvasWidth, canvasHeight, attributes) {
 82     /**
 83      * Board is in no special mode, objects are highlighted on mouse over and objects may be
 84      * clicked to start drag&drop.
 85      * @type Number
 86      * @constant
 87      */
 88     this.BOARD_MODE_NONE = 0x0000;
 89 
 90     /**
 91      * Board is in drag mode, objects aren't highlighted on mouse over and the object referenced in
 92      * {@link JXG.Board#mouse} is updated on mouse movement.
 93      * @type Number
 94      * @constant
 95      */
 96     this.BOARD_MODE_DRAG = 0x0001;
 97 
 98     /**
 99      * In this mode a mouse move changes the origin's screen coordinates.
100      * @type Number
101      * @constant
102      */
103     this.BOARD_MODE_MOVE_ORIGIN = 0x0002;
104 
105     /**
106      * Update is made with high quality, e.g. graphs are evaluated at much more points.
107      * @type Number
108      * @constant
109      * @see JXG.Board#updateQuality
110      */
111     this.BOARD_MODE_ZOOM = 0x0011;
112 
113     /**
114      * Update is made with low quality, e.g. graphs are evaluated at a lesser amount of points.
115      * @type Number
116      * @constant
117      * @see JXG.Board#updateQuality
118      */
119     this.BOARD_QUALITY_LOW = 0x1;
120 
121     /**
122      * Update is made with high quality, e.g. graphs are evaluated at much more points.
123      * @type Number
124      * @constant
125      * @see JXG.Board#updateQuality
126      */
127     this.BOARD_QUALITY_HIGH = 0x2;
128 
129     /**
130      * Pointer to the document element containing the board.
131      * @type Object
132      */
133     if (Type.exists(attributes.document) && attributes.document !== false) {
134         this.document = attributes.document;
135     } else if (Env.isBrowser) {
136         this.document = document;
137     }
138 
139     /**
140      * The html-id of the html element containing the board.
141      * @type String
142      */
143     this.container = container;
144 
145     /**
146      * Pointer to the html element containing the board.
147      * @type Object
148      */
149     this.containerObj = (Env.isBrowser ? this.document.getElementById(this.container) : null);
150 
151     if (Env.isBrowser && renderer.type !== 'no' && this.containerObj === null) {
152         throw new Error('\nJSXGraph: HTML container element "' + container + '" not found.');
153     }
154 
155     /**
156      * A reference to this boards renderer.
157      * @type JXG.AbstractRenderer
158      * @name JXG.Board#renderer
159      * @private
160      * @ignore
161      */
162     this.renderer = renderer;
163 
164     /**
165      * Grids keeps track of all grids attached to this board.
166      * @type Array
167      * @private
168      */
169     this.grids = [];
170 
171     /**
172      * Some standard options
173      * @type JXG.Options
174      */
175     this.options = Type.deepCopy(Options);
176     this.attr = attributes;
177 
178     /**
179      * Dimension of the board.
180      * @default 2
181      * @type Number
182      */
183     this.dimension = 2;
184 
185     this.jc = new JessieCode();
186     this.jc.use(this);
187 
188     /**
189      * Coordinates of the boards origin. This a object with the two properties
190      * usrCoords and scrCoords. usrCoords always equals [1, 0, 0] and scrCoords
191      * stores the boards origin in homogeneous screen coordinates.
192      * @type Object
193      * @private
194      */
195     this.origin = {};
196     this.origin.usrCoords = [1, 0, 0];
197     this.origin.scrCoords = [1, origin[0], origin[1]];
198 
199     /**
200      * Zoom factor in X direction. It only stores the zoom factor to be able
201      * to get back to 100% in zoom100().
202      * @name JXG.Board.zoomX
203      * @type Number
204      * @private
205      * @ignore
206      */
207     this.zoomX = zoomX;
208 
209     /**
210      * Zoom factor in Y direction. It only stores the zoom factor to be able
211      * to get back to 100% in zoom100().
212      * @name JXG.Board.zoomY
213      * @type Number
214      * @private
215      * @ignore
216      */
217     this.zoomY = zoomY;
218 
219     /**
220      * The number of pixels which represent one unit in user-coordinates in x direction.
221      * @type Number
222      * @private
223      */
224     this.unitX = unitX * this.zoomX;
225 
226     /**
227      * The number of pixels which represent one unit in user-coordinates in y direction.
228      * @type Number
229      * @private
230      */
231     this.unitY = unitY * this.zoomY;
232 
233     /**
234      * Keep aspect ratio if bounding box is set and the width/height ratio differs from the
235      * width/height ratio of the canvas.
236      * @type Boolean
237      * @private
238      */
239     this.keepaspectratio = false;
240 
241     /**
242      * Canvas width.
243      * @type Number
244      * @private
245      */
246     this.canvasWidth = canvasWidth;
247 
248     /**
249      * Canvas Height
250      * @type Number
251      * @private
252      */
253     this.canvasHeight = canvasHeight;
254 
255     // If the given id is not valid, generate an unique id
256     if (Type.exists(id) && id !== '' && Env.isBrowser && !Type.exists(this.document.getElementById(id))) {
257         this.id = id;
258     } else {
259         this.id = this.generateId();
260     }
261 
262     EventEmitter.eventify(this);
263 
264     this.hooks = [];
265 
266     /**
267      * An array containing all other boards that are updated after this board has been updated.
268      * @type Array
269      * @see JXG.Board#addChild
270      * @see JXG.Board#removeChild
271      */
272     this.dependentBoards = [];
273 
274     /**
275      * During the update process this is set to false to prevent an endless loop.
276      * @default false
277      * @type Boolean
278      */
279     this.inUpdate = false;
280 
281     /**
282      * An associative array containing all geometric objects belonging to the board. Key is the id of the object and value is a reference to the object.
283      * @type Object
284      */
285     this.objects = {};
286 
287     /**
288      * An array containing all geometric objects on the board in the order of construction.
289      * @type Array
290      */
291     this.objectsList = [];
292 
293     /**
294      * An associative array containing all groups belonging to the board. Key is the id of the group and value is a reference to the object.
295      * @type Object
296      */
297     this.groups = {};
298 
299     /**
300      * Stores all the objects that are currently running an animation.
301      * @type Object
302      */
303     this.animationObjects = {};
304 
305     /**
306      * An associative array containing all highlighted elements belonging to the board.
307      * @type Object
308      */
309     this.highlightedObjects = {};
310 
311     /**
312      * Number of objects ever created on this board. This includes every object, even invisible and deleted ones.
313      * @type Number
314      */
315     this.numObjects = 0;
316 
317     /**
318      * An associative array / dictionary to store the objects of the board by name. The name of the object is the key and value is a reference to the object.
319      * @type Object
320      */
321     this.elementsByName = {};
322 
323     /**
324      * The board mode the board is currently in. Possible values are
325      * <ul>
326      * <li>JXG.Board.BOARD_MODE_NONE</li>
327      * <li>JXG.Board.BOARD_MODE_DRAG</li>
328      * <li>JXG.Board.BOARD_MODE_MOVE_ORIGIN</li>
329      * </ul>
330      * @type Number
331      */
332     this.mode = this.BOARD_MODE_NONE;
333 
334     /**
335      * The update quality of the board. In most cases this is set to {@link JXG.Board#BOARD_QUALITY_HIGH}.
336      * If {@link JXG.Board#mode} equals {@link JXG.Board#BOARD_MODE_DRAG} this is set to
337      * {@link JXG.Board#BOARD_QUALITY_LOW} to speed up the update process by e.g. reducing the number of
338      * evaluation points when plotting functions. Possible values are
339      * <ul>
340      * <li>BOARD_QUALITY_LOW</li>
341      * <li>BOARD_QUALITY_HIGH</li>
342      * </ul>
343      * @type Number
344      * @see JXG.Board#mode
345      */
346     this.updateQuality = this.BOARD_QUALITY_HIGH;
347 
348     /**
349      * If true updates are skipped.
350      * @type Boolean
351      */
352     this.isSuspendedRedraw = false;
353 
354     this.calculateSnapSizes();
355 
356     /**
357      * The distance from the mouse to the dragged object in x direction when the user clicked the mouse button.
358      * @type Number
359      * @see JXG.Board#drag_dy
360      */
361     this.drag_dx = 0;
362 
363     /**
364      * The distance from the mouse to the dragged object in y direction when the user clicked the mouse button.
365      * @type Number
366      * @see JXG.Board#drag_dx
367      */
368     this.drag_dy = 0;
369 
370     /**
371      * The last position where a drag event has been fired.
372      * @type Array
373      * @see JXG.Board#moveObject
374      */
375     this.drag_position = [0, 0];
376 
377     /**
378      * References to the object that is dragged with the mouse on the board.
379      * @type JXG.GeometryElement
380      * @see JXG.Board#touches
381      */
382     this.mouse = {};
383 
384     /**
385      * Keeps track on touched elements, like {@link JXG.Board#mouse} does for mouse events.
386      * @type Array
387      * @see JXG.Board#mouse
388      */
389     this.touches = [];
390 
391     /**
392      * A string containing the XML text of the construction.
393      * This is set in {@link JXG.FileReader.parseString}.
394      * Only useful if a construction is read from a GEONExT-, Intergeo-, Geogebra-, or Cinderella-File.
395      * @type String
396      */
397     this.xmlString = '';
398 
399     /**
400      * Cached result of getCoordsTopLeftCorner for touch/mouseMove-Events to save some DOM operations.
401      * @type Array
402      */
403     this.cPos = [];
404 
405     /**
406      * Contains the last time (epoch, msec) since the last touchMove event which was not thrown away or since
407      * touchStart because Android's Webkit browser fires too much of them.
408      * @type Number
409      */
410     this.touchMoveLast = 0;
411 
412     /**
413      * Contains the pointerId of the last touchMove event which was not thrown away or since
414      * touchStart because Android's Webkit browser fires too much of them.
415      * @type Number
416      */
417     this.touchMoveLastId = Infinity;
418 
419     /**
420      * Contains the last time (epoch, msec) since the last getCoordsTopLeftCorner call which was not thrown away.
421      * @type Number
422      */
423     this.positionAccessLast = 0;
424 
425     /**
426      * Collects all elements that triggered a mouse down event.
427      * @type Array
428      */
429     this.downObjects = [];
430 
431     /**
432      * Collects all elements that have keyboard focus. Should be either one or no element.
433      * Elements are stored with their id.
434      * @type Array
435      */
436     this.focusObjects = [];
437 
438     if (this.attr.showcopyright) {
439         this.renderer.displayCopyright(Const.licenseText, parseInt(this.options.text.fontSize, 10));
440     }
441 
442     /**
443      * Full updates are needed after zoom and axis translates. This saves some time during an update.
444      * @default false
445      * @type Boolean
446      */
447     this.needsFullUpdate = false;
448 
449     /**
450      * If reducedUpdate is set to true then only the dragged element and few (e.g. 2) following
451      * elements are updated during mouse move. On mouse up the whole construction is
452      * updated. This enables us to be fast even on very slow devices.
453      * @type Boolean
454      * @default false
455      */
456     this.reducedUpdate = false;
457 
458     /**
459      * The current color blindness deficiency is stored in this property. If color blindness is not emulated
460      * at the moment, it's value is 'none'.
461      */
462     this.currentCBDef = 'none';
463 
464     /**
465      * If GEONExT constructions are displayed, then this property should be set to true.
466      * At the moment there should be no difference. But this may change.
467      * This is set in {@link JXG.GeonextReader#readGeonext}.
468      * @type Boolean
469      * @default false
470      * @see JXG.GeonextReader#readGeonext
471      */
472     this.geonextCompatibilityMode = false;
473 
474     if (this.options.text.useASCIIMathML && translateASCIIMath) {
475         init();
476     } else {
477         this.options.text.useASCIIMathML = false;
478     }
479 
480     /**
481      * A flag which tells if the board registers mouse events.
482      * @type Boolean
483      * @default false
484      */
485     this.hasMouseHandlers = false;
486 
487     /**
488      * A flag which tells if the board registers touch events.
489      * @type Boolean
490      * @default false
491      */
492     this.hasTouchHandlers = false;
493 
494     /**
495      * A flag which stores if the board registered pointer events.
496      * @type Boolean
497      * @default false
498      */
499     this.hasPointerHandlers = false;
500 
501     /**
502      * A flag which tells if the board the JXG.Board#mouseUpListener is currently registered.
503      * @type Boolean
504      * @default false
505      */
506     this.hasMouseUp = false;
507 
508     /**
509      * A flag which tells if the board the JXG.Board#touchEndListener is currently registered.
510      * @type Boolean
511      * @default false
512      */
513     this.hasTouchEnd = false;
514 
515     /**
516      * A flag which tells us if the board has a pointerUp event registered at the moment.
517      * @type Boolean
518      * @default false
519      */
520     this.hasPointerUp = false;
521 
522     /**
523      * Offset for large coords elements like images
524      * @type Array
525      * @private
526      * @default [0, 0]
527      */
528     this._drag_offset = [0, 0];
529 
530     /**
531      * Stores the input device used in the last down or move event.
532      * @type String
533      * @private
534      * @default 'mouse'
535      */
536     this._inputDevice = 'mouse';
537 
538     /**
539      * Keeps a list of pointer devices which are currently touching the screen.
540      * @type Array
541      * @private
542      */
543     this._board_touches = [];
544 
545     /**
546      * A flag which tells us if the board is in the selecting mode
547      * @type Boolean
548      * @default false
549      */
550     this.selectingMode = false;
551 
552     /**
553      * A flag which tells us if the user is selecting
554      * @type Boolean
555      * @default false
556      */
557     this.isSelecting = false;
558 
559     /**
560      * A flag which tells us if the user is scrolling the viewport
561      * @type Boolean
562      * @private
563      * @default false
564      * @see JXG.Board#scrollListener
565      */
566     this._isScrolling = false;
567 
568     /**
569      * A flag which tells us if a resize is in process
570      * @type Boolean
571      * @private
572      * @default false
573      * @see JXG.Board#resizeListener
574      */
575     this._isResizing = false;
576 
577     /**
578      * A bounding box for the selection
579      * @type Array
580      * @default [ [0,0], [0,0] ]
581      */
582     this.selectingBox = [[0, 0], [0, 0]];
583 
584     /**
585      * Array to log user activity.
586      * Entries are objects of the form '{type, id, start, end}' notifying
587      * the start time as well as the last time of a single event of type 'type'
588      * on a JSXGraph element of id 'id'.
589      * <p> 'start' and 'end' contain the amount of milliseconds elapsed between 1 January 1970 00:00:00 UTC
590      * and the time the event happened.
591      * <p>
592      * For the time being (i.e. v1.5.0) the only supported type is 'drag'.
593      * @type Array
594      */
595     this.userLog = [];
596 
597     this.mathLib = Math;        // Math or JXG.Math.IntervalArithmetic
598     this.mathLibJXG = JXG.Math; // JXG.Math or JXG.Math.IntervalArithmetic
599 
600     if (this.attr.registerevents) {
601         this.addEventHandlers();
602     }
603     if (this.attr.registerresizeevent) {
604         this.addResizeEventHandlers();
605     }
606     if (this.attr.registerfullscreenevent) {
607         this.addFullscreenEventHandlers();
608     }
609 
610     this.methodMap = {
611         update: 'update',
612         fullUpdate: 'fullUpdate',
613         on: 'on',
614         off: 'off',
615         trigger: 'trigger',
616         setView: 'setBoundingBox',
617         setBoundingBox: 'setBoundingBox',
618         migratePoint: 'migratePoint',
619         colorblind: 'emulateColorblindness',
620         suspendUpdate: 'suspendUpdate',
621         unsuspendUpdate: 'unsuspendUpdate',
622         clearTraces: 'clearTraces',
623         left: 'clickLeftArrow',
624         right: 'clickRightArrow',
625         up: 'clickUpArrow',
626         down: 'clickDownArrow',
627         zoomIn: 'zoomIn',
628         zoomOut: 'zoomOut',
629         zoom100: 'zoom100',
630         zoomElements: 'zoomElements',
631         remove: 'removeObject',
632         removeObject: 'removeObject'
633     };
634 };
635 
636 JXG.extend(
637     JXG.Board.prototype,
638     /** @lends JXG.Board.prototype */ {
639         /**
640          * Generates an unique name for the given object. The result depends on the objects type, if the
641          * object is a {@link JXG.Point}, capital characters are used, if it is of type {@link JXG.Line}
642          * only lower case characters are used. If object is of type {@link JXG.Polygon}, a bunch of lower
643          * case characters prefixed with P_ are used. If object is of type {@link JXG.Circle} the name is
644          * generated using lower case characters. prefixed with k_ is used. In any other case, lower case
645          * chars prefixed with s_ is used.
646          * @param {Object} object Reference of an JXG.GeometryElement that is to be named.
647          * @returns {String} Unique name for the object.
648          */
649         generateName: function (object) {
650             var possibleNames, i,
651                 maxNameLength = this.attr.maxnamelength,
652                 pre = '',
653                 post = '',
654                 indices = [],
655                 name = '';
656 
657             if (object.type === Const.OBJECT_TYPE_TICKS) {
658                 return '';
659             }
660 
661             if (Type.isPoint(object) || Type.isPoint3D(object)) {
662                 // points have capital letters
663                 possibleNames = [
664                     '', 'A', 'B', 'C', 'D', 'E', 'F', 'G', 'H', 'I', 'J', 'K', 'L', 'M', 'N', 'O', 'P', 'Q', 'R', 'S', 'T', 'U', 'V', 'W', 'X', 'Y', 'Z'
665                 ];
666             } else if (object.type === Const.OBJECT_TYPE_ANGLE) {
667                 possibleNames = [
668                     '', 'α', 'β', 'γ', 'δ', 'ε', 'ζ', 'η', 'θ', 'ι', 'κ', 'λ',
669                     'μ', 'ν', 'ξ', 'ο', 'π', 'ρ', 'σ', 'τ', 'υ', 'φ', 'χ', 'ψ', 'ω'
670                 ];
671             } else {
672                 // all other elements get lowercase labels
673                 possibleNames = [
674                     '', 'a', 'b', 'c', 'd', 'e', 'f', 'g', 'h', 'i', 'j', 'k', 'l', 'm', 'n', 'o', 'p', 'q', 'r', 's', 't', 'u', 'v', 'w', 'x', 'y', 'z'
675                 ];
676             }
677 
678             if (
679                 !Type.isPoint(object) &&
680                 object.elementClass !== Const.OBJECT_CLASS_LINE &&
681                 object.type !== Const.OBJECT_TYPE_ANGLE
682             ) {
683                 if (object.type === Const.OBJECT_TYPE_POLYGON) {
684                     pre = 'P_{';
685                 } else if (object.elementClass === Const.OBJECT_CLASS_CIRCLE) {
686                     pre = 'k_{';
687                 } else if (object.elementClass === Const.OBJECT_CLASS_TEXT) {
688                     pre = 't_{';
689                 } else {
690                     pre = 's_{';
691                 }
692                 post = '}';
693             }
694 
695             for (i = 0; i < maxNameLength; i++) {
696                 indices[i] = 0;
697             }
698 
699             while (indices[maxNameLength - 1] < possibleNames.length) {
700                 for (indices[0] = 1; indices[0] < possibleNames.length; indices[0]++) {
701                     name = pre;
702 
703                     for (i = maxNameLength; i > 0; i--) {
704                         name += possibleNames[indices[i - 1]];
705                     }
706 
707                     if (!Type.exists(this.elementsByName[name + post])) {
708                         return name + post;
709                     }
710                 }
711                 indices[0] = possibleNames.length;
712 
713                 for (i = 1; i < maxNameLength; i++) {
714                     if (indices[i - 1] === possibleNames.length) {
715                         indices[i - 1] = 1;
716                         indices[i] += 1;
717                     }
718                 }
719             }
720 
721             return '';
722         },
723 
724         /**
725          * Generates unique id for a board. The result is randomly generated and prefixed with 'jxgBoard'.
726          * @returns {String} Unique id for a board.
727          */
728         generateId: function () {
729             var r = 1;
730 
731             // as long as we don't have a unique id generate a new one
732             while (Type.exists(JXG.boards['jxgBoard' + r])) {
733                 r = Math.round(Math.random() * 65535);
734             }
735 
736             return 'jxgBoard' + r;
737         },
738 
739         /**
740          * Composes an id for an element. If the ID is empty ('' or null) a new ID is generated, depending on the
741          * object type. As a side effect {@link JXG.Board#numObjects}
742          * is updated.
743          * @param {Object} obj Reference of an geometry object that needs an id.
744          * @param {Number} type Type of the object.
745          * @returns {String} Unique id for an element.
746          */
747         setId: function (obj, type) {
748             var randomNumber,
749                 num = this.numObjects,
750                 elId = obj.id;
751 
752             this.numObjects += 1;
753 
754             // If no id is provided or id is empty string, a new one is chosen
755             if (elId === '' || !Type.exists(elId)) {
756                 elId = this.id + type + num;
757                 while (Type.exists(this.objects[elId])) {
758                     randomNumber = Math.round(Math.random() * 65535);
759                     elId = this.id + type + num + '-' + randomNumber;
760                 }
761             }
762 
763             obj.id = elId;
764             this.objects[elId] = obj;
765             obj._pos = this.objectsList.length;
766             this.objectsList[this.objectsList.length] = obj;
767 
768             return elId;
769         },
770 
771         /**
772          * After construction of the object the visibility is set
773          * and the label is constructed if necessary.
774          * @param {Object} obj The object to add.
775          */
776         finalizeAdding: function (obj) {
777             if (Type.evaluate(obj.visProp.visible) === false) {
778                 this.renderer.display(obj, false);
779             }
780         },
781 
782         finalizeLabel: function (obj) {
783             if (
784                 obj.hasLabel &&
785                 !Type.evaluate(obj.label.visProp.islabel) &&
786                 Type.evaluate(obj.label.visProp.visible) === false
787             ) {
788                 this.renderer.display(obj.label, false);
789             }
790         },
791 
792         /**********************************************************
793          *
794          * Event Handler helpers
795          *
796          **********************************************************/
797 
798         /**
799          * Returns false if the event has been triggered faster than the maximum frame rate.
800          *
801          * @param {Event} evt Event object given by the browser (unused)
802          * @returns {Boolean} If the event has been triggered faster than the maximum frame rate, false is returned.
803          * @private
804          * @see JXG.Board#pointerMoveListener
805          * @see JXG.Board#touchMoveListener
806          * @see JXG.Board#mouseMoveListener
807          */
808         checkFrameRate: function (evt) {
809             var handleEvt = false,
810                 time = new Date().getTime();
811 
812             if (Type.exists(evt.pointerId) && this.touchMoveLastId !== evt.pointerId) {
813                 handleEvt = true;
814                 this.touchMoveLastId = evt.pointerId;
815             }
816             if (!handleEvt && (time - this.touchMoveLast) * this.attr.maxframerate >= 1000) {
817                 handleEvt = true;
818             }
819             if (handleEvt) {
820                 this.touchMoveLast = time;
821             }
822             return handleEvt;
823         },
824 
825         /**
826          * Calculates mouse coordinates relative to the boards container.
827          * @returns {Array} Array of coordinates relative the boards container top left corner.
828          */
829         getCoordsTopLeftCorner: function () {
830             var cPos,
831                 doc,
832                 crect,
833                 // In ownerDoc we need the 'real' document object.
834                 // The first version is used in the case of shadowDom,
835                 // the second case in the 'normal' case.
836                 ownerDoc = this.document.ownerDocument || this.document,
837                 docElement = ownerDoc.documentElement || this.document.body.parentNode,
838                 docBody = ownerDoc.body,
839                 container = this.containerObj,
840                 // viewport, content,
841                 zoom,
842                 o;
843 
844             /**
845              * During drags and origin moves the container element is usually not changed.
846              * Check the position of the upper left corner at most every 1000 msecs
847              */
848             if (
849                 this.cPos.length > 0 &&
850                 (this.mode === this.BOARD_MODE_DRAG ||
851                     this.mode === this.BOARD_MODE_MOVE_ORIGIN ||
852                     new Date().getTime() - this.positionAccessLast < 1000)
853             ) {
854                 return this.cPos;
855             }
856             this.positionAccessLast = new Date().getTime();
857 
858             // Check if getBoundingClientRect exists. If so, use this as this covers *everything*
859             // even CSS3D transformations etc.
860             // Supported by all browsers but IE 6, 7.
861 
862             if (container.getBoundingClientRect) {
863                 crect = container.getBoundingClientRect();
864 
865                 zoom = 1.0;
866                 // Recursively search for zoom style entries.
867                 // This is necessary for reveal.js on webkit.
868                 // It fails if the user does zooming
869                 o = container;
870                 while (o && Type.exists(o.parentNode)) {
871                     if (
872                         Type.exists(o.style) &&
873                         Type.exists(o.style.zoom) &&
874                         o.style.zoom !== ''
875                     ) {
876                         zoom *= parseFloat(o.style.zoom);
877                     }
878                     o = o.parentNode;
879                 }
880                 cPos = [crect.left * zoom, crect.top * zoom];
881 
882                 // add border width
883                 cPos[0] += Env.getProp(container, 'border-left-width');
884                 cPos[1] += Env.getProp(container, 'border-top-width');
885 
886                 // vml seems to ignore paddings
887                 if (this.renderer.type !== 'vml') {
888                     // add padding
889                     cPos[0] += Env.getProp(container, 'padding-left');
890                     cPos[1] += Env.getProp(container, 'padding-top');
891                 }
892 
893                 this.cPos = cPos.slice();
894                 return this.cPos;
895             }
896 
897             //
898             //  OLD CODE
899             //  IE 6-7 only:
900             //
901             cPos = Env.getOffset(container);
902             doc = this.document.documentElement.ownerDocument;
903 
904             if (!this.containerObj.currentStyle && doc.defaultView) {
905                 // Non IE
906                 // this is for hacks like this one used in wordpress for the admin bar:
907                 // html { margin-top: 28px }
908                 // seems like it doesn't work in IE
909 
910                 cPos[0] += Env.getProp(docElement, 'margin-left');
911                 cPos[1] += Env.getProp(docElement, 'margin-top');
912 
913                 cPos[0] += Env.getProp(docElement, 'border-left-width');
914                 cPos[1] += Env.getProp(docElement, 'border-top-width');
915 
916                 cPos[0] += Env.getProp(docElement, 'padding-left');
917                 cPos[1] += Env.getProp(docElement, 'padding-top');
918             }
919 
920             if (docBody) {
921                 cPos[0] += Env.getProp(docBody, 'left');
922                 cPos[1] += Env.getProp(docBody, 'top');
923             }
924 
925             // Google Translate offers widgets for web authors. These widgets apparently tamper with the clientX
926             // and clientY coordinates of the mouse events. The minified sources seem to be the only publicly
927             // available version so we're doing it the hacky way: Add a fixed offset.
928             // see https://groups.google.com/d/msg/google-translate-general/H2zj0TNjjpY/jw6irtPlCw8J
929             if (typeof google === 'object' && google.translate) {
930                 cPos[0] += 10;
931                 cPos[1] += 25;
932             }
933 
934             // add border width
935             cPos[0] += Env.getProp(container, 'border-left-width');
936             cPos[1] += Env.getProp(container, 'border-top-width');
937 
938             // vml seems to ignore paddings
939             if (this.renderer.type !== 'vml') {
940                 // add padding
941                 cPos[0] += Env.getProp(container, 'padding-left');
942                 cPos[1] += Env.getProp(container, 'padding-top');
943             }
944 
945             cPos[0] += this.attr.offsetx;
946             cPos[1] += this.attr.offsety;
947 
948             this.cPos = cPos.slice();
949             return this.cPos;
950         },
951 
952         /**
953          * Get the position of the pointing device in screen coordinates, relative to the upper left corner
954          * of the host tag.
955          * @param {Event} e Event object given by the browser.
956          * @param {Number} [i] Only use in case of touch events. This determines which finger to use and should not be set
957          * for mouseevents.
958          * @returns {Array} Contains the mouse coordinates in screen coordinates, ready for {@link JXG.Coords}
959          */
960         getMousePosition: function (e, i) {
961             var cPos = this.getCoordsTopLeftCorner(),
962                 absPos,
963                 v;
964 
965             // Position of cursor using clientX/Y
966             absPos = Env.getPosition(e, i, this.document);
967 
968             /**
969              * In case there has been no down event before.
970              */
971             if (!Type.exists(this.cssTransMat)) {
972                 this.updateCSSTransforms();
973             }
974             // Position relative to the top left corner
975             v = [1, absPos[0] - cPos[0], absPos[1] - cPos[1]];
976             v = Mat.matVecMult(this.cssTransMat, v);
977             v[1] /= v[0];
978             v[2] /= v[0];
979             return [v[1], v[2]];
980 
981             // Method without CSS transformation
982             /*
983              return [absPos[0] - cPos[0], absPos[1] - cPos[1]];
984              */
985         },
986 
987         /**
988          * Initiate moving the origin. This is used in mouseDown and touchStart listeners.
989          * @param {Number} x Current mouse/touch coordinates
990          * @param {Number} y Current mouse/touch coordinates
991          */
992         initMoveOrigin: function (x, y) {
993             this.drag_dx = x - this.origin.scrCoords[1];
994             this.drag_dy = y - this.origin.scrCoords[2];
995 
996             this.mode = this.BOARD_MODE_MOVE_ORIGIN;
997             this.updateQuality = this.BOARD_QUALITY_LOW;
998         },
999 
1000         /**
1001          * Collects all elements below the current mouse pointer and fulfilling the following constraints:
1002          * <ul><li>isDraggable</li><li>visible</li><li>not fixed</li><li>not frozen</li></ul>
1003          * @param {Number} x Current mouse/touch coordinates
1004          * @param {Number} y current mouse/touch coordinates
1005          * @param {Object} evt An event object
1006          * @param {String} type What type of event? 'touch', 'mouse' or 'pen'.
1007          * @returns {Array} A list of geometric elements.
1008          */
1009         initMoveObject: function (x, y, evt, type) {
1010             var pEl,
1011                 el,
1012                 collect = [],
1013                 offset = [],
1014                 haspoint,
1015                 len = this.objectsList.length,
1016                 dragEl = { visProp: { layer: -10000 } };
1017 
1018             // Store status of key presses for 3D movement
1019             this._shiftKey = evt.shiftKey;
1020             this._ctrlKey = evt.ctrlKey;
1021 
1022             //for (el in this.objects) {
1023             for (el = 0; el < len; el++) {
1024                 pEl = this.objectsList[el];
1025                 haspoint = pEl.hasPoint && pEl.hasPoint(x, y);
1026 
1027                 if (pEl.visPropCalc.visible && haspoint) {
1028                     pEl.triggerEventHandlers([type + 'down', 'down'], [evt]);
1029                     this.downObjects.push(pEl);
1030                 }
1031 
1032                 if (haspoint &&
1033                     pEl.isDraggable &&
1034                     pEl.visPropCalc.visible &&
1035                     ((this.geonextCompatibilityMode &&
1036                         (Type.isPoint(pEl) || pEl.elementClass === Const.OBJECT_CLASS_TEXT)) ||
1037                         !this.geonextCompatibilityMode) &&
1038                     !Type.evaluate(pEl.visProp.fixed)
1039                     /*(!pEl.visProp.frozen) &&*/
1040                 ) {
1041                     // Elements in the highest layer get priority.
1042                     if (
1043                         pEl.visProp.layer > dragEl.visProp.layer ||
1044                         (pEl.visProp.layer === dragEl.visProp.layer &&
1045                             pEl.lastDragTime.getTime() >= dragEl.lastDragTime.getTime())
1046                     ) {
1047                         // If an element and its label have the focus
1048                         // simultaneously, the element is taken.
1049                         // This only works if we assume that every browser runs
1050                         // through this.objects in the right order, i.e. an element A
1051                         // added before element B turns up here before B does.
1052                         if (
1053                             !this.attr.ignorelabels ||
1054                             !Type.exists(dragEl.label) ||
1055                             pEl !== dragEl.label
1056                         ) {
1057                             dragEl = pEl;
1058                             collect.push(dragEl);
1059                             // Save offset for large coords elements.
1060                             if (Type.exists(dragEl.coords)) {
1061                                 offset.push(
1062                                     Statistics.subtract(dragEl.coords.scrCoords.slice(1), [
1063                                         x,
1064                                         y
1065                                     ])
1066                                 );
1067                             } else {
1068                                 offset.push([0, 0]);
1069                             }
1070 
1071                             // we can't drop out of this loop because of the event handling system
1072                             //if (this.attr.takefirst) {
1073                             //    return collect;
1074                             //}
1075                         }
1076                     }
1077                 }
1078             }
1079 
1080             if (this.attr.drag.enabled && collect.length > 0) {
1081                 this.mode = this.BOARD_MODE_DRAG;
1082             }
1083 
1084             // A one-element array is returned.
1085             if (this.attr.takefirst) {
1086                 collect.length = 1;
1087                 this._drag_offset = offset[0];
1088             } else {
1089                 collect = collect.slice(-1);
1090                 this._drag_offset = offset[offset.length - 1];
1091             }
1092 
1093             if (!this._drag_offset) {
1094                 this._drag_offset = [0, 0];
1095             }
1096 
1097             // Move drag element to the top of the layer
1098             if (this.renderer.type === 'svg' &&
1099                 Type.exists(collect[0]) &&
1100                 Type.evaluate(collect[0].visProp.dragtotopoflayer) &&
1101                 collect.length === 1 &&
1102                 Type.exists(collect[0].rendNode)
1103             ) {
1104                 collect[0].rendNode.parentNode.appendChild(collect[0].rendNode);
1105             }
1106 
1107             // // Init rotation angle and scale factor for two finger movements
1108             // this.previousRotation = 0.0;
1109             // this.previousScale = 1.0;
1110 
1111             if (collect.length >= 1) {
1112                 collect[0].highlight(true);
1113                 this.triggerEventHandlers(['mousehit', 'hit'], [evt, collect[0]]);
1114             }
1115 
1116             return collect;
1117         },
1118 
1119         /**
1120          * Moves an object.
1121          * @param {Number} x Coordinate
1122          * @param {Number} y Coordinate
1123          * @param {Object} o The touch object that is dragged: {JXG.Board#mouse} or {JXG.Board#touches}.
1124          * @param {Object} evt The event object.
1125          * @param {String} type Mouse or touch event?
1126          */
1127         moveObject: function (x, y, o, evt, type) {
1128             var newPos = new Coords(
1129                 Const.COORDS_BY_SCREEN,
1130                 this.getScrCoordsOfMouse(x, y),
1131                 this
1132             ),
1133                 drag,
1134                 dragScrCoords,
1135                 newDragScrCoords;
1136 
1137             if (!(o && o.obj)) {
1138                 return;
1139             }
1140             drag = o.obj;
1141 
1142             // Avoid updates for very small movements of coordsElements, see below
1143             if (drag.coords) {
1144                 dragScrCoords = drag.coords.scrCoords.slice();
1145             }
1146 
1147             this.addLogEntry('drag', drag, newPos.usrCoords.slice(1));
1148 
1149             // Store the position.
1150             this.drag_position = [newPos.scrCoords[1], newPos.scrCoords[2]];
1151             this.drag_position = Statistics.add(this.drag_position, this._drag_offset);
1152 
1153             // Store status of key presses for 3D movement
1154             this._shiftKey = evt.shiftKey;
1155             this._ctrlKey = evt.ctrlKey;
1156 
1157             //
1158             // We have to distinguish between CoordsElements and other elements like lines.
1159             // The latter need the difference between two move events.
1160             if (Type.exists(drag.coords)) {
1161                 drag.setPositionDirectly(Const.COORDS_BY_SCREEN, this.drag_position);
1162             } else {
1163                 this.displayInfobox(false);
1164                 // Hide infobox in case the user has touched an intersection point
1165                 // and drags the underlying line now.
1166 
1167                 if (!isNaN(o.targets[0].Xprev + o.targets[0].Yprev)) {
1168                     drag.setPositionDirectly(
1169                         Const.COORDS_BY_SCREEN,
1170                         [newPos.scrCoords[1], newPos.scrCoords[2]],
1171                         [o.targets[0].Xprev, o.targets[0].Yprev]
1172                     );
1173                 }
1174                 // Remember the actual position for the next move event. Then we are able to
1175                 // compute the difference vector.
1176                 o.targets[0].Xprev = newPos.scrCoords[1];
1177                 o.targets[0].Yprev = newPos.scrCoords[2];
1178             }
1179             // This may be necessary for some gliders and labels
1180             if (Type.exists(drag.coords)) {
1181                 drag.prepareUpdate().update(false).updateRenderer();
1182                 this.updateInfobox(drag);
1183                 drag.prepareUpdate().update(true).updateRenderer();
1184             }
1185 
1186             if (drag.coords) {
1187                 newDragScrCoords = drag.coords.scrCoords;
1188             }
1189             // No updates for very small movements of coordsElements
1190             if (
1191                 !drag.coords ||
1192                 dragScrCoords[1] !== newDragScrCoords[1] ||
1193                 dragScrCoords[2] !== newDragScrCoords[2]
1194             ) {
1195                 drag.triggerEventHandlers([type + 'drag', 'drag'], [evt]);
1196 
1197                 this.update();
1198             }
1199             drag.highlight(true);
1200             this.triggerEventHandlers(['mousehit', 'hit'], [evt, drag]);
1201 
1202             drag.lastDragTime = new Date();
1203         },
1204 
1205         /**
1206          * Moves elements in multitouch mode.
1207          * @param {Array} p1 x,y coordinates of first touch
1208          * @param {Array} p2 x,y coordinates of second touch
1209          * @param {Object} o The touch object that is dragged: {JXG.Board#touches}.
1210          * @param {Object} evt The event object that lead to this movement.
1211          */
1212         twoFingerMove: function (o, id, evt) {
1213             var drag;
1214 
1215             if (Type.exists(o) && Type.exists(o.obj)) {
1216                 drag = o.obj;
1217             } else {
1218                 return;
1219             }
1220 
1221             if (
1222                 drag.elementClass === Const.OBJECT_CLASS_LINE ||
1223                 drag.type === Const.OBJECT_TYPE_POLYGON
1224             ) {
1225                 this.twoFingerTouchObject(o.targets, drag, id);
1226             } else if (drag.elementClass === Const.OBJECT_CLASS_CIRCLE) {
1227                 this.twoFingerTouchCircle(o.targets, drag, id);
1228             }
1229 
1230             if (evt) {
1231                 drag.triggerEventHandlers(['touchdrag', 'drag'], [evt]);
1232             }
1233         },
1234 
1235         /**
1236          * Compute the transformation matrix to move an element according to the
1237          * previous and actual positions of finger 1 and finger 2.
1238          * See also https://math.stackexchange.com/questions/4010538/solve-for-2d-translation-rotation-and-scale-given-two-touch-point-movements
1239          *
1240          * @param {Object} finger1 Actual and previous position of finger 1
1241          * @param {Object} finger1 Actual and previous position of finger 1
1242          * @param {Boolean} scalable Flag if element may be scaled
1243          * @param {Boolean} rotatable Flag if element may be rotated
1244          * @returns
1245          */
1246         getTwoFingerTransform(finger1, finger2, scalable, rotatable) {
1247             var crd,
1248                 x1, y1, x2, y2,
1249                 dx, dy,
1250                 xx1, yy1, xx2, yy2,
1251                 dxx, dyy,
1252                 C, S, LL, tx, ty, lbda;
1253 
1254             crd = new Coords(Const.COORDS_BY_SCREEN, [finger1.Xprev, finger1.Yprev], this).usrCoords;
1255             x1 = crd[1];
1256             y1 = crd[2];
1257             crd = new Coords(Const.COORDS_BY_SCREEN, [finger2.Xprev, finger2.Yprev], this).usrCoords;
1258             x2 = crd[1];
1259             y2 = crd[2];
1260 
1261             crd = new Coords(Const.COORDS_BY_SCREEN, [finger1.X, finger1.Y], this).usrCoords;
1262             xx1 = crd[1];
1263             yy1 = crd[2];
1264             crd = new Coords(Const.COORDS_BY_SCREEN, [finger2.X, finger2.Y], this).usrCoords;
1265             xx2 = crd[1];
1266             yy2 = crd[2];
1267 
1268             dx = x2 - x1;
1269             dy = y2 - y1;
1270             dxx = xx2 - xx1;
1271             dyy = yy2 - yy1;
1272 
1273             LL = dx * dx + dy * dy;
1274             C = (dxx * dx + dyy * dy) / LL;
1275             S = (dyy * dx - dxx * dy) / LL;
1276             if (!scalable) {
1277                 lbda = Math.sqrt(C * C + S * S);
1278                 C /= lbda;
1279                 S /= lbda;
1280             }
1281             if (!rotatable) {
1282                 S = 0;
1283             }
1284             tx = 0.5 * (xx1 + xx2 - C * (x1 + x2) + S * (y1 + y2));
1285             ty = 0.5 * (yy1 + yy2 - S * (x1 + x2) - C * (y1 + y2));
1286 
1287             return [1, 0, 0,
1288                 tx, C, -S,
1289                 ty, S, C];
1290         },
1291 
1292         /**
1293          * Moves, rotates and scales a line or polygon with two fingers.
1294          * <p>
1295          * If one vertex of the polygon snaps to the grid or to points or is not draggable,
1296          * two-finger-movement is cancelled.
1297          *
1298          * @param {Array} tar Array containing touch event objects: {JXG.Board#touches.targets}.
1299          * @param {object} drag The object that is dragged:
1300          * @param {Number} id pointerId of the event. In case of old touch event this is emulated.
1301          */
1302         twoFingerTouchObject: function (tar, drag, id) {
1303             var t, T,
1304                 ar, i, len, vp,
1305                 snap = false;
1306 
1307             if (
1308                 Type.exists(tar[0]) &&
1309                 Type.exists(tar[1]) &&
1310                 !isNaN(tar[0].Xprev + tar[0].Yprev + tar[1].Xprev + tar[1].Yprev)
1311             ) {
1312 
1313                 T = this.getTwoFingerTransform(
1314                     tar[0], tar[1],
1315                     Type.evaluate(drag.visProp.scalable),
1316                     Type.evaluate(drag.visProp.rotatable));
1317                 t = this.create('transform', T, { type: 'generic' });
1318                 t.update();
1319 
1320                 if (drag.elementClass === Const.OBJECT_CLASS_LINE) {
1321                     ar = [];
1322                     if (drag.point1.draggable()) {
1323                         ar.push(drag.point1);
1324                     }
1325                     if (drag.point2.draggable()) {
1326                         ar.push(drag.point2);
1327                     }
1328                     t.applyOnce(ar);
1329                 } else if (drag.type === Const.OBJECT_TYPE_POLYGON) {
1330                     len = drag.vertices.length - 1;
1331                     vp = drag.visProp;
1332                     snap = Type.evaluate(vp.snaptogrid) || Type.evaluate(vp.snaptopoints);
1333                     for (i = 0; i < len && !snap; ++i) {
1334                         vp = drag.vertices[i].visProp;
1335                         snap = snap || Type.evaluate(vp.snaptogrid) || Type.evaluate(vp.snaptopoints);
1336                         snap = snap || (!drag.vertices[i].draggable())
1337                     }
1338                     if (!snap) {
1339                         ar = [];
1340                         for (i = 0; i < len; ++i) {
1341                             if (drag.vertices[i].draggable()) {
1342                                 ar.push(drag.vertices[i]);
1343                             }
1344                         }
1345                         t.applyOnce(ar);
1346                     }
1347                 }
1348 
1349                 this.update();
1350                 drag.highlight(true);
1351             }
1352         },
1353 
1354         /*
1355          * Moves, rotates and scales a circle with two fingers.
1356          * @param {Array} tar Array conatining touch event objects: {JXG.Board#touches.targets}.
1357          * @param {object} drag The object that is dragged:
1358          * @param {Number} id pointerId of the event. In case of old touch event this is emulated.
1359          */
1360         twoFingerTouchCircle: function (tar, drag, id) {
1361             var fixEl, moveEl, np, op, fix, d, alpha, t1, t2, t3, t4;
1362 
1363             if (drag.method === 'pointCircle' || drag.method === 'pointLine') {
1364                 return;
1365             }
1366 
1367             if (
1368                 Type.exists(tar[0]) &&
1369                 Type.exists(tar[1]) &&
1370                 !isNaN(tar[0].Xprev + tar[0].Yprev + tar[1].Xprev + tar[1].Yprev)
1371             ) {
1372                 if (id === tar[0].num) {
1373                     fixEl = tar[1];
1374                     moveEl = tar[0];
1375                 } else {
1376                     fixEl = tar[0];
1377                     moveEl = tar[1];
1378                 }
1379 
1380                 fix = new Coords(Const.COORDS_BY_SCREEN, [fixEl.Xprev, fixEl.Yprev], this)
1381                     .usrCoords;
1382                 // Previous finger position
1383                 op = new Coords(Const.COORDS_BY_SCREEN, [moveEl.Xprev, moveEl.Yprev], this)
1384                     .usrCoords;
1385                 // New finger position
1386                 np = new Coords(Const.COORDS_BY_SCREEN, [moveEl.X, moveEl.Y], this).usrCoords;
1387 
1388                 alpha = Geometry.rad(op.slice(1), fix.slice(1), np.slice(1));
1389 
1390                 // Rotate and scale by the movement of the second finger
1391                 t1 = this.create('transform', [-fix[1], -fix[2]], {
1392                     type: 'translate'
1393                 });
1394                 t2 = this.create('transform', [alpha], { type: 'rotate' });
1395                 t1.melt(t2);
1396                 if (Type.evaluate(drag.visProp.scalable)) {
1397                     d = Geometry.distance(fix, np) / Geometry.distance(fix, op);
1398                     t3 = this.create('transform', [d, d], { type: 'scale' });
1399                     t1.melt(t3);
1400                 }
1401                 t4 = this.create('transform', [fix[1], fix[2]], {
1402                     type: 'translate'
1403                 });
1404                 t1.melt(t4);
1405 
1406                 if (drag.center.draggable()) {
1407                     t1.applyOnce([drag.center]);
1408                 }
1409 
1410                 if (drag.method === 'twoPoints') {
1411                     if (drag.point2.draggable()) {
1412                         t1.applyOnce([drag.point2]);
1413                     }
1414                 } else if (drag.method === 'pointRadius') {
1415                     if (Type.isNumber(drag.updateRadius.origin)) {
1416                         drag.setRadius(drag.radius * d);
1417                     }
1418                 }
1419 
1420                 this.update(drag.center);
1421                 drag.highlight(true);
1422             }
1423         },
1424 
1425         highlightElements: function (x, y, evt, target) {
1426             var el,
1427                 pEl,
1428                 pId,
1429                 overObjects = {},
1430                 len = this.objectsList.length;
1431 
1432             // Elements  below the mouse pointer which are not highlighted yet will be highlighted.
1433             for (el = 0; el < len; el++) {
1434                 pEl = this.objectsList[el];
1435                 pId = pEl.id;
1436                 if (
1437                     Type.exists(pEl.hasPoint) &&
1438                     pEl.visPropCalc.visible &&
1439                     pEl.hasPoint(x, y)
1440                 ) {
1441                     // this is required in any case because otherwise the box won't be shown until the point is dragged
1442                     this.updateInfobox(pEl);
1443 
1444                     if (!Type.exists(this.highlightedObjects[pId])) {
1445                         // highlight only if not highlighted
1446                         overObjects[pId] = pEl;
1447                         pEl.highlight();
1448                         // triggers board event.
1449                         this.triggerEventHandlers(['mousehit', 'hit'], [evt, pEl, target]);
1450                     }
1451 
1452                     if (pEl.mouseover) {
1453                         pEl.triggerEventHandlers(['mousemove', 'move'], [evt]);
1454                     } else {
1455                         pEl.triggerEventHandlers(['mouseover', 'over'], [evt]);
1456                         pEl.mouseover = true;
1457                     }
1458                 }
1459             }
1460 
1461             for (el = 0; el < len; el++) {
1462                 pEl = this.objectsList[el];
1463                 pId = pEl.id;
1464                 if (pEl.mouseover) {
1465                     if (!overObjects[pId]) {
1466                         pEl.triggerEventHandlers(['mouseout', 'out'], [evt]);
1467                         pEl.mouseover = false;
1468                     }
1469                 }
1470             }
1471         },
1472 
1473         /**
1474          * Helper function which returns a reasonable starting point for the object being dragged.
1475          * Formerly known as initXYstart().
1476          * @private
1477          * @param {JXG.GeometryElement} obj The object to be dragged
1478          * @param {Array} targets Array of targets. It is changed by this function.
1479          */
1480         saveStartPos: function (obj, targets) {
1481             var xy = [],
1482                 i,
1483                 len;
1484 
1485             if (obj.type === Const.OBJECT_TYPE_TICKS) {
1486                 xy.push([1, NaN, NaN]);
1487             } else if (obj.elementClass === Const.OBJECT_CLASS_LINE) {
1488                 xy.push(obj.point1.coords.usrCoords);
1489                 xy.push(obj.point2.coords.usrCoords);
1490             } else if (obj.elementClass === Const.OBJECT_CLASS_CIRCLE) {
1491                 xy.push(obj.center.coords.usrCoords);
1492                 if (obj.method === 'twoPoints') {
1493                     xy.push(obj.point2.coords.usrCoords);
1494                 }
1495             } else if (obj.type === Const.OBJECT_TYPE_POLYGON) {
1496                 len = obj.vertices.length - 1;
1497                 for (i = 0; i < len; i++) {
1498                     xy.push(obj.vertices[i].coords.usrCoords);
1499                 }
1500             } else if (obj.type === Const.OBJECT_TYPE_SECTOR) {
1501                 xy.push(obj.point1.coords.usrCoords);
1502                 xy.push(obj.point2.coords.usrCoords);
1503                 xy.push(obj.point3.coords.usrCoords);
1504             } else if (Type.isPoint(obj) || obj.type === Const.OBJECT_TYPE_GLIDER) {
1505                 xy.push(obj.coords.usrCoords);
1506             } else if (obj.elementClass === Const.OBJECT_CLASS_CURVE) {
1507                 // if (Type.exists(obj.parents)) {
1508                 //     len = obj.parents.length;
1509                 //     if (len > 0) {
1510                 //         for (i = 0; i < len; i++) {
1511                 //             xy.push(this.select(obj.parents[i]).coords.usrCoords);
1512                 //         }
1513                 //     } else
1514                 // }
1515                 if (obj.points.length > 0) {
1516                     xy.push(obj.points[0].usrCoords);
1517                 }
1518             } else {
1519                 try {
1520                     xy.push(obj.coords.usrCoords);
1521                 } catch (e) {
1522                     JXG.debug(
1523                         'JSXGraph+ saveStartPos: obj.coords.usrCoords not available: ' + e
1524                     );
1525                 }
1526             }
1527 
1528             len = xy.length;
1529             for (i = 0; i < len; i++) {
1530                 targets.Zstart.push(xy[i][0]);
1531                 targets.Xstart.push(xy[i][1]);
1532                 targets.Ystart.push(xy[i][2]);
1533             }
1534         },
1535 
1536         mouseOriginMoveStart: function (evt) {
1537             var r, pos;
1538 
1539             r = this._isRequiredKeyPressed(evt, 'pan');
1540             if (r) {
1541                 pos = this.getMousePosition(evt);
1542                 this.initMoveOrigin(pos[0], pos[1]);
1543             }
1544 
1545             return r;
1546         },
1547 
1548         mouseOriginMove: function (evt) {
1549             var r = this.mode === this.BOARD_MODE_MOVE_ORIGIN,
1550                 pos;
1551 
1552             if (r) {
1553                 pos = this.getMousePosition(evt);
1554                 this.moveOrigin(pos[0], pos[1], true);
1555             }
1556 
1557             return r;
1558         },
1559 
1560         /**
1561          * Start moving the origin with one finger.
1562          * @private
1563          * @param  {Object} evt Event from touchStartListener
1564          * @return {Boolean}   returns if the origin is moved.
1565          */
1566         touchStartMoveOriginOneFinger: function (evt) {
1567             var touches = evt[JXG.touchProperty],
1568                 conditions,
1569                 pos;
1570 
1571             conditions =
1572                 this.attr.pan.enabled && !this.attr.pan.needtwofingers && touches.length === 1;
1573 
1574             if (conditions) {
1575                 pos = this.getMousePosition(evt, 0);
1576                 this.initMoveOrigin(pos[0], pos[1]);
1577             }
1578 
1579             return conditions;
1580         },
1581 
1582         /**
1583          * Move the origin with one finger
1584          * @private
1585          * @param  {Object} evt Event from touchMoveListener
1586          * @return {Boolean}     returns if the origin is moved.
1587          */
1588         touchOriginMove: function (evt) {
1589             var r = this.mode === this.BOARD_MODE_MOVE_ORIGIN,
1590                 pos;
1591 
1592             if (r) {
1593                 pos = this.getMousePosition(evt, 0);
1594                 this.moveOrigin(pos[0], pos[1], true);
1595             }
1596 
1597             return r;
1598         },
1599 
1600         /**
1601          * Stop moving the origin with one finger
1602          * @return {null} null
1603          * @private
1604          */
1605         originMoveEnd: function () {
1606             this.updateQuality = this.BOARD_QUALITY_HIGH;
1607             this.mode = this.BOARD_MODE_NONE;
1608         },
1609 
1610         /**********************************************************
1611          *
1612          * Event Handler
1613          *
1614          **********************************************************/
1615 
1616         /**
1617          * Add all possible event handlers to the board object
1618          * which move objects, i.e. mouse, pointer and touch events.
1619          */
1620         addEventHandlers: function () {
1621             if (Env.supportsPointerEvents()) {
1622                 this.addPointerEventHandlers();
1623             } else {
1624                 this.addMouseEventHandlers();
1625                 this.addTouchEventHandlers();
1626             }
1627 
1628             // This one produces errors on IE
1629             // // Env.addEvent(this.containerObj, 'contextmenu', function (e) { e.preventDefault(); return false;}, this);
1630             // This one works on IE, Firefox and Chromium with default configurations. On some Safari
1631             // or Opera versions the user must explicitly allow the deactivation of the context menu.
1632             if (this.containerObj !== null) {
1633                 this.containerObj.oncontextmenu = function (e) {
1634                     if (Type.exists(e)) {
1635                         e.preventDefault();
1636                     }
1637                     return false;
1638                 };
1639             }
1640 
1641             this.addKeyboardEventHandlers();
1642         },
1643 
1644         /**
1645          * Add resize event handlers
1646          *
1647          */
1648         addResizeEventHandlers: function () {
1649             if (Env.isBrowser) {
1650                 try {
1651                     // Supported by all new browsers
1652                     // resizeObserver: triggered if size of the JSXGraph div changes.
1653                     this.startResizeObserver();
1654                 } catch (err) {
1655                     // Certain Safari and edge version do not support
1656                     // resizeObserver, but intersectionObserver.
1657                     // resize event: triggered if size of window changes
1658                     Env.addEvent(window, 'resize', this.resizeListener, this);
1659                     // intersectionObserver: triggered if JSXGraph becomes visible.
1660                     this.startIntersectionObserver();
1661                 }
1662                 // Scroll event: needs to be captured since on mobile devices
1663                 // sometimes a header bar is displayed / hidden, which triggers a
1664                 // resize event.
1665                 Env.addEvent(window, 'scroll', this.scrollListener, this);
1666             }
1667         },
1668 
1669         /**
1670          * Remove all event handlers from the board object
1671          */
1672         removeEventHandlers: function () {
1673             this.removeMouseEventHandlers();
1674             this.removeTouchEventHandlers();
1675             this.removePointerEventHandlers();
1676 
1677             this.removeFullscreenEventHandlers();
1678             this.removeKeyboardEventHandlers();
1679 
1680             if (Env.isBrowser) {
1681                 if (Type.exists(this.resizeObserver)) {
1682                     this.stopResizeObserver();
1683                 } else {
1684                     Env.removeEvent(window, 'resize', this.resizeListener, this);
1685                     this.stopIntersectionObserver();
1686                 }
1687                 Env.removeEvent(window, 'scroll', this.scrollListener, this);
1688             }
1689         },
1690 
1691         /**
1692          * Registers pointer event handlers.
1693          */
1694         addPointerEventHandlers: function () {
1695             if (!this.hasPointerHandlers && Env.isBrowser) {
1696                 var moveTarget = this.attr.movetarget || this.containerObj;
1697 
1698                 if (window.navigator.msPointerEnabled) {
1699                     // IE10-
1700                     Env.addEvent(this.containerObj, 'MSPointerDown', this.pointerDownListener, this);
1701                     Env.addEvent(moveTarget, 'MSPointerMove', this.pointerMoveListener, this);
1702                 } else {
1703                     Env.addEvent(this.containerObj, 'pointerdown', this.pointerDownListener, this);
1704                     Env.addEvent(moveTarget, 'pointermove', this.pointerMoveListener, this);
1705                     Env.addEvent(moveTarget, 'pointerleave', this.pointerLeaveListener, this);
1706                 }
1707                 Env.addEvent(this.containerObj, 'mousewheel', this.mouseWheelListener, this);
1708                 Env.addEvent(this.containerObj, 'DOMMouseScroll', this.mouseWheelListener, this);
1709 
1710                 if (this.containerObj !== null) {
1711                     // This is needed for capturing touch events.
1712                     // It is in jsxgraph.css, for ms-touch-action...
1713                     this.containerObj.style.touchAction = 'none';
1714                 }
1715 
1716                 this.hasPointerHandlers = true;
1717             }
1718         },
1719 
1720         /**
1721          * Registers mouse move, down and wheel event handlers.
1722          */
1723         addMouseEventHandlers: function () {
1724             if (!this.hasMouseHandlers && Env.isBrowser) {
1725                 var moveTarget = this.attr.movetarget || this.containerObj;
1726 
1727                 Env.addEvent(this.containerObj, 'mousedown', this.mouseDownListener, this);
1728                 Env.addEvent(moveTarget, 'mousemove', this.mouseMoveListener, this);
1729 
1730                 Env.addEvent(this.containerObj, 'mousewheel', this.mouseWheelListener, this);
1731                 Env.addEvent(this.containerObj, 'DOMMouseScroll', this.mouseWheelListener, this);
1732 
1733                 this.hasMouseHandlers = true;
1734             }
1735         },
1736 
1737         /**
1738          * Register touch start and move and gesture start and change event handlers.
1739          * @param {Boolean} appleGestures If set to false the gesturestart and gesturechange event handlers
1740          * will not be registered.
1741          *
1742          * Since iOS 13, touch events were abandoned in favour of pointer events
1743          */
1744         addTouchEventHandlers: function (appleGestures) {
1745             if (!this.hasTouchHandlers && Env.isBrowser) {
1746                 var moveTarget = this.attr.movetarget || this.containerObj;
1747 
1748                 Env.addEvent(this.containerObj, 'touchstart', this.touchStartListener, this);
1749                 Env.addEvent(moveTarget, 'touchmove', this.touchMoveListener, this);
1750 
1751                 /*
1752                 if (!Type.exists(appleGestures) || appleGestures) {
1753                     // Gesture listener are called in touchStart and touchMove.
1754                     //Env.addEvent(this.containerObj, 'gesturestart', this.gestureStartListener, this);
1755                     //Env.addEvent(this.containerObj, 'gesturechange', this.gestureChangeListener, this);
1756                 }
1757                 */
1758 
1759                 this.hasTouchHandlers = true;
1760             }
1761         },
1762 
1763         /**
1764          * Add fullscreen events which update the CSS transformation matrix to correct
1765          * the mouse/touch/pointer positions in case of CSS transformations.
1766          */
1767         addFullscreenEventHandlers: function () {
1768             var i,
1769                 // standard/Edge, firefox, chrome/safari, IE11
1770                 events = [
1771                     'fullscreenchange',
1772                     'mozfullscreenchange',
1773                     'webkitfullscreenchange',
1774                     'msfullscreenchange'
1775                 ],
1776                 le = events.length;
1777 
1778             if (!this.hasFullscreenEventHandlers && Env.isBrowser) {
1779                 for (i = 0; i < le; i++) {
1780                     Env.addEvent(this.document, events[i], this.fullscreenListener, this);
1781                 }
1782                 this.hasFullscreenEventHandlers = true;
1783             }
1784         },
1785 
1786         addKeyboardEventHandlers: function () {
1787             if (this.attr.keyboard.enabled && !this.hasKeyboardHandlers && Env.isBrowser) {
1788                 Env.addEvent(this.containerObj, 'keydown', this.keyDownListener, this);
1789                 Env.addEvent(this.containerObj, 'focusin', this.keyFocusInListener, this);
1790                 Env.addEvent(this.containerObj, 'focusout', this.keyFocusOutListener, this);
1791                 this.hasKeyboardHandlers = true;
1792             }
1793         },
1794 
1795         /**
1796          * Remove all registered touch event handlers.
1797          */
1798         removeKeyboardEventHandlers: function () {
1799             if (this.hasKeyboardHandlers && Env.isBrowser) {
1800                 Env.removeEvent(this.containerObj, 'keydown', this.keyDownListener, this);
1801                 Env.removeEvent(this.containerObj, 'focusin', this.keyFocusInListener, this);
1802                 Env.removeEvent(this.containerObj, 'focusout', this.keyFocusOutListener, this);
1803                 this.hasKeyboardHandlers = false;
1804             }
1805         },
1806 
1807         /**
1808          * Remove all registered event handlers regarding fullscreen mode.
1809          */
1810         removeFullscreenEventHandlers: function () {
1811             var i,
1812                 // standard/Edge, firefox, chrome/safari, IE11
1813                 events = [
1814                     'fullscreenchange',
1815                     'mozfullscreenchange',
1816                     'webkitfullscreenchange',
1817                     'msfullscreenchange'
1818                 ],
1819                 le = events.length;
1820 
1821             if (this.hasFullscreenEventHandlers && Env.isBrowser) {
1822                 for (i = 0; i < le; i++) {
1823                     Env.removeEvent(this.document, events[i], this.fullscreenListener, this);
1824                 }
1825                 this.hasFullscreenEventHandlers = false;
1826             }
1827         },
1828 
1829         /**
1830          * Remove MSPointer* Event handlers.
1831          */
1832         removePointerEventHandlers: function () {
1833             if (this.hasPointerHandlers && Env.isBrowser) {
1834                 var moveTarget = this.attr.movetarget || this.containerObj;
1835 
1836                 if (window.navigator.msPointerEnabled) {
1837                     // IE10-
1838                     Env.removeEvent(this.containerObj, 'MSPointerDown', this.pointerDownListener, this);
1839                     Env.removeEvent(moveTarget, 'MSPointerMove', this.pointerMoveListener, this);
1840                 } else {
1841                     Env.removeEvent(this.containerObj, 'pointerdown', this.pointerDownListener, this);
1842                     Env.removeEvent(moveTarget, 'pointermove', this.pointerMoveListener, this);
1843                     Env.removeEvent(moveTarget, 'pointerleave', this.pointerLeaveListener, this);
1844                 }
1845 
1846                 Env.removeEvent(this.containerObj, 'mousewheel', this.mouseWheelListener, this);
1847                 Env.removeEvent(this.containerObj, 'DOMMouseScroll', this.mouseWheelListener, this);
1848 
1849                 if (this.hasPointerUp) {
1850                     if (window.navigator.msPointerEnabled) {
1851                         // IE10-
1852                         Env.removeEvent(this.document, 'MSPointerUp', this.pointerUpListener, this);
1853                     } else {
1854                         Env.removeEvent(this.document, 'pointerup', this.pointerUpListener, this);
1855                         Env.removeEvent(this.document, 'pointercancel', this.pointerUpListener, this);
1856                     }
1857                     this.hasPointerUp = false;
1858                 }
1859 
1860                 this.hasPointerHandlers = false;
1861             }
1862         },
1863 
1864         /**
1865          * De-register mouse event handlers.
1866          */
1867         removeMouseEventHandlers: function () {
1868             if (this.hasMouseHandlers && Env.isBrowser) {
1869                 var moveTarget = this.attr.movetarget || this.containerObj;
1870 
1871                 Env.removeEvent(this.containerObj, 'mousedown', this.mouseDownListener, this);
1872                 Env.removeEvent(moveTarget, 'mousemove', this.mouseMoveListener, this);
1873 
1874                 if (this.hasMouseUp) {
1875                     Env.removeEvent(this.document, 'mouseup', this.mouseUpListener, this);
1876                     this.hasMouseUp = false;
1877                 }
1878 
1879                 Env.removeEvent(this.containerObj, 'mousewheel', this.mouseWheelListener, this);
1880                 Env.removeEvent(
1881                     this.containerObj,
1882                     'DOMMouseScroll',
1883                     this.mouseWheelListener,
1884                     this
1885                 );
1886 
1887                 this.hasMouseHandlers = false;
1888             }
1889         },
1890 
1891         /**
1892          * Remove all registered touch event handlers.
1893          */
1894         removeTouchEventHandlers: function () {
1895             if (this.hasTouchHandlers && Env.isBrowser) {
1896                 var moveTarget = this.attr.movetarget || this.containerObj;
1897 
1898                 Env.removeEvent(this.containerObj, 'touchstart', this.touchStartListener, this);
1899                 Env.removeEvent(moveTarget, 'touchmove', this.touchMoveListener, this);
1900 
1901                 if (this.hasTouchEnd) {
1902                     Env.removeEvent(this.document, 'touchend', this.touchEndListener, this);
1903                     this.hasTouchEnd = false;
1904                 }
1905 
1906                 this.hasTouchHandlers = false;
1907             }
1908         },
1909 
1910         /**
1911          * Handler for click on left arrow in the navigation bar
1912          * @returns {JXG.Board} Reference to the board
1913          */
1914         clickLeftArrow: function () {
1915             this.moveOrigin(
1916                 this.origin.scrCoords[1] + this.canvasWidth * 0.1,
1917                 this.origin.scrCoords[2]
1918             );
1919             return this;
1920         },
1921 
1922         /**
1923          * Handler for click on right arrow in the navigation bar
1924          * @returns {JXG.Board} Reference to the board
1925          */
1926         clickRightArrow: function () {
1927             this.moveOrigin(
1928                 this.origin.scrCoords[1] - this.canvasWidth * 0.1,
1929                 this.origin.scrCoords[2]
1930             );
1931             return this;
1932         },
1933 
1934         /**
1935          * Handler for click on up arrow in the navigation bar
1936          * @returns {JXG.Board} Reference to the board
1937          */
1938         clickUpArrow: function () {
1939             this.moveOrigin(
1940                 this.origin.scrCoords[1],
1941                 this.origin.scrCoords[2] - this.canvasHeight * 0.1
1942             );
1943             return this;
1944         },
1945 
1946         /**
1947          * Handler for click on down arrow in the navigation bar
1948          * @returns {JXG.Board} Reference to the board
1949          */
1950         clickDownArrow: function () {
1951             this.moveOrigin(
1952                 this.origin.scrCoords[1],
1953                 this.origin.scrCoords[2] + this.canvasHeight * 0.1
1954             );
1955             return this;
1956         },
1957 
1958         /**
1959          * Triggered on iOS/Safari while the user inputs a gesture (e.g. pinch) and is used to zoom into the board.
1960          * Works on iOS/Safari and Android.
1961          * @param {Event} evt Browser event object
1962          * @returns {Boolean}
1963          */
1964         gestureChangeListener: function (evt) {
1965             var c,
1966                 dir1 = [],
1967                 dir2 = [],
1968                 angle,
1969                 mi = 10,
1970                 isPinch = false,
1971                 // Save zoomFactors
1972                 zx = this.attr.zoom.factorx,
1973                 zy = this.attr.zoom.factory,
1974                 factor, dist, theta, bound,
1975                 dx, dy, cx, cy;
1976 
1977             if (this.mode !== this.BOARD_MODE_ZOOM) {
1978                 return true;
1979             }
1980             evt.preventDefault();
1981 
1982             dist = Geometry.distance(
1983                 [evt.touches[0].clientX, evt.touches[0].clientY],
1984                 [evt.touches[1].clientX, evt.touches[1].clientY],
1985                 2
1986             );
1987 
1988             // Android pinch to zoom
1989             // evt.scale was available in iOS touch events (pre iOS 13)
1990             // evt.scale is undefined in Android
1991             if (evt.scale === undefined) {
1992                 evt.scale = dist / this.prevDist;
1993             }
1994 
1995             if (!Type.exists(this.prevCoords)) {
1996                 return false;
1997             }
1998             // Compute the angle of the two finger directions
1999             dir1 = [
2000                 evt.touches[0].clientX - this.prevCoords[0][0],
2001                 evt.touches[0].clientY - this.prevCoords[0][1]
2002             ];
2003             dir2 = [
2004                 evt.touches[1].clientX - this.prevCoords[1][0],
2005                 evt.touches[1].clientY - this.prevCoords[1][1]
2006             ];
2007 
2008             if (
2009                 dir1[0] * dir1[0] + dir1[1] * dir1[1] < mi * mi &&
2010                 dir2[0] * dir2[0] + dir2[1] * dir2[1] < mi * mi
2011             ) {
2012                 return false;
2013             }
2014 
2015             angle = Geometry.rad(dir1, [0, 0], dir2);
2016             if (
2017                 this.isPreviousGesture !== 'pan' &&
2018                 Math.abs(angle) > Math.PI * 0.2 &&
2019                 Math.abs(angle) < Math.PI * 1.8
2020             ) {
2021                 isPinch = true;
2022             }
2023 
2024             if (this.isPreviousGesture !== 'pan' && !isPinch) {
2025                 if (Math.abs(evt.scale) < 0.77 || Math.abs(evt.scale) > 1.3) {
2026                     isPinch = true;
2027                 }
2028             }
2029 
2030             factor = evt.scale / this.prevScale;
2031             this.prevScale = evt.scale;
2032             this.prevCoords = [
2033                 [evt.touches[0].clientX, evt.touches[0].clientY],
2034                 [evt.touches[1].clientX, evt.touches[1].clientY]
2035             ];
2036 
2037             c = new Coords(Const.COORDS_BY_SCREEN, this.getMousePosition(evt, 0), this);
2038 
2039             if (this.attr.pan.enabled && this.attr.pan.needtwofingers && !isPinch) {
2040                 // Pan detected
2041                 this.isPreviousGesture = 'pan';
2042                 this.moveOrigin(c.scrCoords[1], c.scrCoords[2], true);
2043 
2044             } else if (this.attr.zoom.enabled && Math.abs(factor - 1.0) < 0.5) {
2045                 // Pinch detected
2046                 if (this.attr.zoom.pinchhorizontal || this.attr.zoom.pinchvertical) {
2047                     dx = Math.abs(evt.touches[0].clientX - evt.touches[1].clientX);
2048                     dy = Math.abs(evt.touches[0].clientY - evt.touches[1].clientY);
2049                     theta = Math.abs(Math.atan2(dy, dx));
2050                     bound = (Math.PI * this.attr.zoom.pinchsensitivity) / 90.0;
2051                 }
2052 
2053                 if (this.attr.zoom.pinchhorizontal && theta < bound) {
2054                     this.attr.zoom.factorx = factor;
2055                     this.attr.zoom.factory = 1.0;
2056                     cx = 0;
2057                     cy = 0;
2058                 } else if (
2059                     this.attr.zoom.pinchvertical &&
2060                     Math.abs(theta - Math.PI * 0.5) < bound
2061                 ) {
2062                     this.attr.zoom.factorx = 1.0;
2063                     this.attr.zoom.factory = factor;
2064                     cx = 0;
2065                     cy = 0;
2066                 } else {
2067                     this.attr.zoom.factorx = factor;
2068                     this.attr.zoom.factory = factor;
2069                     cx = c.usrCoords[1];
2070                     cy = c.usrCoords[2];
2071                 }
2072 
2073                 this.zoomIn(cx, cy);
2074 
2075                 // Restore zoomFactors
2076                 this.attr.zoom.factorx = zx;
2077                 this.attr.zoom.factory = zy;
2078             }
2079 
2080             return false;
2081         },
2082 
2083         /**
2084          * Called by iOS/Safari as soon as the user starts a gesture. Works natively on iOS/Safari,
2085          * on Android we emulate it.
2086          * @param {Event} evt
2087          * @returns {Boolean}
2088          */
2089         gestureStartListener: function (evt) {
2090             var pos;
2091 
2092             evt.preventDefault();
2093             this.prevScale = 1.0;
2094             // Android pinch to zoom
2095             this.prevDist = Geometry.distance(
2096                 [evt.touches[0].clientX, evt.touches[0].clientY],
2097                 [evt.touches[1].clientX, evt.touches[1].clientY],
2098                 2
2099             );
2100             this.prevCoords = [
2101                 [evt.touches[0].clientX, evt.touches[0].clientY],
2102                 [evt.touches[1].clientX, evt.touches[1].clientY]
2103             ];
2104             this.isPreviousGesture = 'none';
2105 
2106             // If pinch-to-zoom is interpreted as panning
2107             // we have to prepare move origin
2108             pos = this.getMousePosition(evt, 0);
2109             this.initMoveOrigin(pos[0], pos[1]);
2110 
2111             this.mode = this.BOARD_MODE_ZOOM;
2112             return false;
2113         },
2114 
2115         /**
2116          * Test if the required key combination is pressed for wheel zoom, move origin and
2117          * selection
2118          * @private
2119          * @param  {Object}  evt    Mouse or pen event
2120          * @param  {String}  action String containing the action: 'zoom', 'pan', 'selection'.
2121          * Corresponds to the attribute subobject.
2122          * @return {Boolean}        true or false.
2123          */
2124         _isRequiredKeyPressed: function (evt, action) {
2125             var obj = this.attr[action];
2126             if (!obj.enabled) {
2127                 return false;
2128             }
2129 
2130             if (
2131                 ((obj.needshift && evt.shiftKey) || (!obj.needshift && !evt.shiftKey)) &&
2132                 ((obj.needctrl && evt.ctrlKey) || (!obj.needctrl && !evt.ctrlKey))
2133             ) {
2134                 return true;
2135             }
2136 
2137             return false;
2138         },
2139 
2140         /*
2141          * Pointer events
2142          */
2143 
2144         /**
2145          *
2146          * Check if pointer event is already registered in {@link JXG.Board#_board_touches}.
2147          *
2148          * @param  {Object} evt Event object
2149          * @return {Boolean} true if down event has already been sent.
2150          * @private
2151          */
2152         _isPointerRegistered: function (evt) {
2153             var i,
2154                 len = this._board_touches.length;
2155 
2156             for (i = 0; i < len; i++) {
2157                 if (this._board_touches[i].pointerId === evt.pointerId) {
2158                     return true;
2159                 }
2160             }
2161             return false;
2162         },
2163 
2164         /**
2165          *
2166          * Store the position of a pointer event.
2167          * If not yet done, registers a pointer event in {@link JXG.Board#_board_touches}.
2168          * Allows to follow the path of that finger on the screen.
2169          * Only two simultaneous touches are supported.
2170          *
2171          * @param {Object} evt Event object
2172          * @returns {JXG.Board} Reference to the board
2173          * @private
2174          */
2175         _pointerStorePosition: function (evt) {
2176             var i, found;
2177 
2178             for (i = 0, found = false; i < this._board_touches.length; i++) {
2179                 if (this._board_touches[i].pointerId === evt.pointerId) {
2180                     this._board_touches[i].clientX = evt.clientX;
2181                     this._board_touches[i].clientY = evt.clientY;
2182                     found = true;
2183                     break;
2184                 }
2185             }
2186 
2187             // Restrict the number of simultaneous touches to 2
2188             if (!found && this._board_touches.length < 2) {
2189                 this._board_touches.push({
2190                     pointerId: evt.pointerId,
2191                     clientX: evt.clientX,
2192                     clientY: evt.clientY
2193                 });
2194             }
2195 
2196             return this;
2197         },
2198 
2199         /**
2200          * Deregisters a pointer event in {@link JXG.Board#_board_touches}.
2201          * It happens if a finger has been lifted from the screen.
2202          *
2203          * @param {Object} evt Event object
2204          * @returns {JXG.Board} Reference to the board
2205          * @private
2206          */
2207         _pointerRemoveTouches: function (evt) {
2208             var i;
2209             for (i = 0; i < this._board_touches.length; i++) {
2210                 if (this._board_touches[i].pointerId === evt.pointerId) {
2211                     this._board_touches.splice(i, 1);
2212                     break;
2213                 }
2214             }
2215 
2216             return this;
2217         },
2218 
2219         /**
2220          * Remove all registered fingers from {@link JXG.Board#_board_touches}.
2221          * This might be necessary if too many fingers have been registered.
2222          * @returns {JXG.Board} Reference to the board
2223          * @private
2224          */
2225         _pointerClearTouches: function (pId) {
2226             // var i;
2227             // if (pId) {
2228             //     for (i = 0; i < this._board_touches.length; i++) {
2229             //         if (pId === this._board_touches[i].pointerId) {
2230             //             this._board_touches.splice(i, i);
2231             //             break;
2232             //         }
2233             //     }
2234             // } else {
2235             // }
2236             if (this._board_touches.length > 0) {
2237                 this.dehighlightAll();
2238             }
2239             this.updateQuality = this.BOARD_QUALITY_HIGH;
2240             this.mode = this.BOARD_MODE_NONE;
2241             this._board_touches = [];
2242             this.touches = [];
2243         },
2244 
2245         /**
2246          * Determine which input device is used for this action.
2247          * Possible devices are 'touch', 'pen' and 'mouse'.
2248          * This affects the precision and certain events.
2249          * In case of no browser, 'mouse' is used.
2250          *
2251          * @see JXG.Board#pointerDownListener
2252          * @see JXG.Board#pointerMoveListener
2253          * @see JXG.Board#initMoveObject
2254          * @see JXG.Board#moveObject
2255          *
2256          * @param {Event} evt The browsers event object.
2257          * @returns {String} 'mouse', 'pen', or 'touch'
2258          * @private
2259          */
2260         _getPointerInputDevice: function (evt) {
2261             if (Env.isBrowser) {
2262                 if (
2263                     evt.pointerType === 'touch' || // New
2264                     (window.navigator.msMaxTouchPoints && // Old
2265                         window.navigator.msMaxTouchPoints > 1)
2266                 ) {
2267                     return 'touch';
2268                 }
2269                 if (evt.pointerType === 'mouse') {
2270                     return 'mouse';
2271                 }
2272                 if (evt.pointerType === 'pen') {
2273                     return 'pen';
2274                 }
2275             }
2276             return 'mouse';
2277         },
2278 
2279         /**
2280          * This method is called by the browser when a pointing device is pressed on the screen.
2281          * @param {Event} evt The browsers event object.
2282          * @param {Object} object If the object to be dragged is already known, it can be submitted via this parameter
2283          * @param {Boolean} [allowDefaultEventHandling=false] If true event is not canceled, i.e. prevent call of evt.preventDefault()
2284          * @returns {Boolean} false if the the first finger event is sent twice, or not a browser, or
2285          *  or in selection mode. Otherwise returns true.
2286          */
2287         pointerDownListener: function (evt, object, allowDefaultEventHandling) {
2288             var i, j, k, pos,
2289                 elements, sel, target_obj,
2290                 type = 'mouse', // Used in case of no browser
2291                 found, target, ta;
2292 
2293             // Fix for Firefox browser: When using a second finger, the
2294             // touch event for the first finger is sent again.
2295             if (!object && this._isPointerRegistered(evt)) {
2296                 return false;
2297             }
2298 
2299             if (!object && evt.isPrimary) {
2300                 // First finger down. To be on the safe side this._board_touches is cleared.
2301                 // this._pointerClearTouches();
2302             }
2303 
2304             if (!this.hasPointerUp) {
2305                 if (window.navigator.msPointerEnabled) {
2306                     // IE10-
2307                     Env.addEvent(this.document, 'MSPointerUp', this.pointerUpListener, this);
2308                 } else {
2309                     // 'pointercancel' is fired e.g. if the finger leaves the browser and drags down the system menu on Android
2310                     Env.addEvent(this.document, 'pointerup', this.pointerUpListener, this);
2311                     Env.addEvent(this.document, 'pointercancel', this.pointerUpListener, this);
2312                 }
2313                 this.hasPointerUp = true;
2314             }
2315 
2316             if (this.hasMouseHandlers) {
2317                 this.removeMouseEventHandlers();
2318             }
2319 
2320             if (this.hasTouchHandlers) {
2321                 this.removeTouchEventHandlers();
2322             }
2323 
2324             // Prevent accidental selection of text
2325             if (this.document.selection && Type.isFunction(this.document.selection.empty)) {
2326                 this.document.selection.empty();
2327             } else if (window.getSelection) {
2328                 sel = window.getSelection();
2329                 if (sel.removeAllRanges) {
2330                     try {
2331                         sel.removeAllRanges();
2332                     } catch (e) { }
2333                 }
2334             }
2335 
2336             // Mouse, touch or pen device
2337             this._inputDevice = this._getPointerInputDevice(evt);
2338             type = this._inputDevice;
2339             this.options.precision.hasPoint = this.options.precision[type];
2340 
2341             // Handling of multi touch with pointer events should be easier than with touch events.
2342             // Every pointer device has its own pointerId, e.g. the mouse
2343             // always has id 1 or 0, fingers and pens get unique ids every time a pointerDown event is fired and they will
2344             // keep this id until a pointerUp event is fired. What we have to do here is:
2345             //  1. collect all elements under the current pointer
2346             //  2. run through the touches control structure
2347             //    a. look for the object collected in step 1.
2348             //    b. if an object is found, check the number of pointers. If appropriate, add the pointer.
2349             pos = this.getMousePosition(evt);
2350 
2351             // selection
2352             this._testForSelection(evt);
2353             if (this.selectingMode) {
2354                 this._startSelecting(pos);
2355                 this.triggerEventHandlers(
2356                     ['touchstartselecting', 'pointerstartselecting', 'startselecting'],
2357                     [evt]
2358                 );
2359                 return; // don't continue as a normal click
2360             }
2361 
2362             if (this.attr.drag.enabled && object) {
2363                 elements = [object];
2364                 this.mode = this.BOARD_MODE_DRAG;
2365             } else {
2366                 elements = this.initMoveObject(pos[0], pos[1], evt, type);
2367             }
2368 
2369             target_obj = {
2370                 num: evt.pointerId,
2371                 X: pos[0],
2372                 Y: pos[1],
2373                 Xprev: NaN,
2374                 Yprev: NaN,
2375                 Xstart: [],
2376                 Ystart: [],
2377                 Zstart: []
2378             };
2379 
2380             // If no draggable object can be found, get out here immediately
2381             if (elements.length > 0) {
2382                 // check touches structure
2383                 target = elements[elements.length - 1];
2384                 found = false;
2385 
2386                 // Reminder: this.touches is the list of elements which
2387                 // currently 'possess' a pointer (mouse, pen, finger)
2388                 for (i = 0; i < this.touches.length; i++) {
2389                     // An element receives a further touch, i.e.
2390                     // the target is already in our touches array, add the pointer to the existing touch
2391                     if (this.touches[i].obj === target) {
2392                         j = i;
2393                         k = this.touches[i].targets.push(target_obj) - 1;
2394                         found = true;
2395                         break;
2396                     }
2397                 }
2398                 if (!found) {
2399                     // An new element hae been touched.
2400                     k = 0;
2401                     j =
2402                         this.touches.push({
2403                             obj: target,
2404                             targets: [target_obj]
2405                         }) - 1;
2406                 }
2407 
2408                 this.dehighlightAll();
2409                 target.highlight(true);
2410 
2411                 this.saveStartPos(target, this.touches[j].targets[k]);
2412 
2413                 // Prevent accidental text selection
2414                 // this could get us new trouble: input fields, links and drop down boxes placed as text
2415                 // on the board don't work anymore.
2416                 if (evt && evt.preventDefault && !allowDefaultEventHandling) {
2417                     evt.preventDefault();
2418                     // All browser supporting pointer events know preventDefault()
2419                     // } else if (window.event) {
2420                     //     window.event.returnValue = false;
2421                 }
2422             }
2423 
2424             if (this.touches.length > 0 && !allowDefaultEventHandling) {
2425                 evt.preventDefault();
2426                 evt.stopPropagation();
2427             }
2428 
2429             if (!Env.isBrowser) {
2430                 return false;
2431             }
2432             if (this._getPointerInputDevice(evt) !== 'touch') {
2433                 if (this.mode === this.BOARD_MODE_NONE) {
2434                     this.mouseOriginMoveStart(evt);
2435                 }
2436             } else {
2437                 this._pointerStorePosition(evt);
2438                 evt.touches = this._board_touches;
2439 
2440                 // Touch events on empty areas of the board are handled here, see also touchStartListener
2441                 // 1. case: one finger. If allowed, this triggers pan with one finger
2442                 if (
2443                     evt.touches.length === 1 &&
2444                     this.mode === this.BOARD_MODE_NONE &&
2445                     this.touchStartMoveOriginOneFinger(evt)
2446                 ) {
2447                     // Empty by purpose
2448                 } else if (
2449                     evt.touches.length === 2 &&
2450                     (this.mode === this.BOARD_MODE_NONE ||
2451                         this.mode === this.BOARD_MODE_MOVE_ORIGIN)
2452                 ) {
2453                     // 2. case: two fingers: pinch to zoom or pan with two fingers needed.
2454                     // This happens when the second finger hits the device. First, the
2455                     // 'one finger pan mode' has to be cancelled.
2456                     if (this.mode === this.BOARD_MODE_MOVE_ORIGIN) {
2457                         this.originMoveEnd();
2458                     }
2459 
2460                     this.gestureStartListener(evt);
2461                 }
2462             }
2463 
2464             // Allow browser scrolling
2465             // For this: pan by one finger has to be disabled
2466             ta = 'none';             // JSXGraph catches all user touch events
2467             if (this.mode === this.BOARD_MODE_NONE &&
2468                 Type.evaluate(this.attr.browserpan) &&
2469                 !(Type.evaluate(this.attr.pan.enabled) && !Type.evaluate(this.attr.pan.needtwofingers))
2470             ) {
2471                 ta = 'pan-x pan-y';  // JSXGraph allows browser scrolling
2472             }
2473             this.containerObj.style.touchAction = ta;
2474 
2475             this.triggerEventHandlers(['touchstart', 'down', 'pointerdown', 'MSPointerDown'], [evt]);
2476 
2477             return true;
2478         },
2479 
2480         // /**
2481         //  * Called if pointer leaves an HTML tag. It is called by the inner-most tag.
2482         //  * That means, if a JSXGraph text, i.e. an HTML div, is placed close
2483         //  * to the border of the board, this pointerout event will be ignored.
2484         //  * @param  {Event} evt
2485         //  * @return {Boolean}
2486         //  */
2487         // pointerOutListener: function (evt) {
2488         //     if (evt.target === this.containerObj ||
2489         //         (this.renderer.type === 'svg' && evt.target === this.renderer.foreignObjLayer)) {
2490         //         this.pointerUpListener(evt);
2491         //     }
2492         //     return this.mode === this.BOARD_MODE_NONE;
2493         // },
2494 
2495         /**
2496          * Called periodically by the browser while the user moves a pointing device across the screen.
2497          * @param {Event} evt
2498          * @returns {Boolean}
2499          */
2500         pointerMoveListener: function (evt) {
2501             var i,
2502                 j,
2503                 pos,
2504                 touchTargets,
2505                 type = 'mouse'; // in case of no browser
2506 
2507             if (
2508                 this._getPointerInputDevice(evt) === 'touch' &&
2509                 !this._isPointerRegistered(evt)
2510             ) {
2511                 // Test, if there was a previous down event of this _getPointerId
2512                 // (in case it is a touch event).
2513                 // Otherwise this move event is ignored. This is necessary e.g. for sketchometry.
2514                 return this.BOARD_MODE_NONE;
2515             }
2516 
2517             if (!this.checkFrameRate(evt)) {
2518                 return false;
2519             }
2520 
2521             if (this.mode !== this.BOARD_MODE_DRAG) {
2522                 this.dehighlightAll();
2523                 this.displayInfobox(false);
2524             }
2525 
2526             if (this.mode !== this.BOARD_MODE_NONE) {
2527                 evt.preventDefault();
2528                 evt.stopPropagation();
2529             }
2530 
2531             this.updateQuality = this.BOARD_QUALITY_LOW;
2532             // Mouse, touch or pen device
2533             this._inputDevice = this._getPointerInputDevice(evt);
2534             type = this._inputDevice;
2535             this.options.precision.hasPoint = this.options.precision[type];
2536 
2537             // selection
2538             if (this.selectingMode) {
2539                 pos = this.getMousePosition(evt);
2540                 this._moveSelecting(pos);
2541                 this.triggerEventHandlers(
2542                     ['touchmoveselecting', 'moveselecting', 'pointermoveselecting'],
2543                     [evt, this.mode]
2544                 );
2545             } else if (!this.mouseOriginMove(evt)) {
2546                 if (this.mode === this.BOARD_MODE_DRAG) {
2547                     pos = this.getMousePosition(evt);
2548                     // Run through all jsxgraph elements which are touched by at least one finger.
2549                     for (i = 0; i < this.touches.length; i++) {
2550                         touchTargets = this.touches[i].targets;
2551                         // Run through all touch events which have been started on this jsxgraph element.
2552                         for (j = 0; j < touchTargets.length; j++) {
2553                             if (touchTargets[j].num === evt.pointerId) {
2554                                 touchTargets[j].X = pos[0];
2555                                 touchTargets[j].Y = pos[1];
2556 
2557                                 if (touchTargets.length === 1) {
2558                                     // Touch by one finger: this is possible for all elements that can be dragged
2559                                     this.moveObject(pos[0], pos[1], this.touches[i], evt, type);
2560                                 } else if (touchTargets.length === 2) {
2561                                     // Touch by two fingers: e.g. moving lines
2562                                     this.twoFingerMove(this.touches[i], evt.pointerId, evt);
2563 
2564                                     touchTargets[j].Xprev = pos[0];
2565                                     touchTargets[j].Yprev = pos[1];
2566                                 }
2567 
2568                                 // There is only one pointer in the evt object, so there's no point in looking further
2569                                 break;
2570                             }
2571                         }
2572                     }
2573                 } else {
2574                     if (this._getPointerInputDevice(evt) === 'touch') {
2575                         this._pointerStorePosition(evt);
2576 
2577                         if (this._board_touches.length === 2) {
2578                             evt.touches = this._board_touches;
2579                             this.gestureChangeListener(evt);
2580                         }
2581                     }
2582 
2583                     // Move event without dragging an element
2584                     pos = this.getMousePosition(evt);
2585                     this.highlightElements(pos[0], pos[1], evt, -1);
2586                 }
2587             }
2588 
2589             // Hiding the infobox is commented out, since it prevents showing the infobox
2590             // on IE 11+ on 'over'
2591             //if (this.mode !== this.BOARD_MODE_DRAG) {
2592             //this.displayInfobox(false);
2593             //}
2594             this.triggerEventHandlers(['pointermove', 'MSPointerMove', 'move'], [evt, this.mode]);
2595             this.updateQuality = this.BOARD_QUALITY_HIGH;
2596 
2597             return this.mode === this.BOARD_MODE_NONE;
2598         },
2599 
2600         /**
2601          * Triggered as soon as the user stops touching the device with at least one finger.
2602          *
2603          * @param {Event} evt
2604          * @returns {Boolean}
2605          */
2606         pointerUpListener: function (evt) {
2607             var i, j, found,
2608                 touchTargets,
2609                 updateNeeded = false;
2610 
2611             this.triggerEventHandlers(['touchend', 'up', 'pointerup', 'MSPointerUp'], [evt]);
2612             this.displayInfobox(false);
2613 
2614             if (evt) {
2615                 for (i = 0; i < this.touches.length; i++) {
2616                     touchTargets = this.touches[i].targets;
2617                     for (j = 0; j < touchTargets.length; j++) {
2618                         if (touchTargets[j].num === evt.pointerId) {
2619                             touchTargets.splice(j, 1);
2620                             if (touchTargets.length === 0) {
2621                                 this.touches.splice(i, 1);
2622                             }
2623                             break;
2624                         }
2625                     }
2626                 }
2627             }
2628 
2629             this.originMoveEnd();
2630             this.update();
2631 
2632             // selection
2633             if (this.selectingMode) {
2634                 this._stopSelecting(evt);
2635                 this.triggerEventHandlers(
2636                     ['touchstopselecting', 'pointerstopselecting', 'stopselecting'],
2637                     [evt]
2638                 );
2639                 this.stopSelectionMode();
2640             } else {
2641                 for (i = this.downObjects.length - 1; i > -1; i--) {
2642                     found = false;
2643                     for (j = 0; j < this.touches.length; j++) {
2644                         if (this.touches[j].obj.id === this.downObjects[i].id) {
2645                             found = true;
2646                         }
2647                     }
2648                     if (!found) {
2649                         this.downObjects[i].triggerEventHandlers(
2650                             ['touchend', 'up', 'pointerup', 'MSPointerUp'],
2651                             [evt]
2652                         );
2653                         if (!Type.exists(this.downObjects[i].coords)) {
2654                             // snapTo methods have to be called e.g. for line elements here.
2655                             // For coordsElements there might be a conflict with
2656                             // attractors, see commit from 2022.04.08, 11:12:18.
2657                             this.downObjects[i].snapToGrid();
2658                             this.downObjects[i].snapToPoints();
2659                             updateNeeded = true;
2660                         }
2661                         this.downObjects.splice(i, 1);
2662                     }
2663                 }
2664             }
2665 
2666             if (this.hasPointerUp) {
2667                 if (window.navigator.msPointerEnabled) {
2668                     // IE10-
2669                     Env.removeEvent(this.document, 'MSPointerUp', this.pointerUpListener, this);
2670                 } else {
2671                     Env.removeEvent(this.document, 'pointerup', this.pointerUpListener, this);
2672                     Env.removeEvent(
2673                         this.document,
2674                         'pointercancel',
2675                         this.pointerUpListener,
2676                         this
2677                     );
2678                 }
2679                 this.hasPointerUp = false;
2680             }
2681 
2682             // After one finger leaves the screen the gesture is stopped.
2683             this._pointerClearTouches(evt.pointerId);
2684             if (this._getPointerInputDevice(evt) !== 'touch') {
2685                 this.dehighlightAll();
2686             }
2687 
2688             if (updateNeeded) {
2689                 this.update();
2690             }
2691 
2692             return true;
2693         },
2694 
2695         /**
2696          * Triggered by the pointerleave event. This is needed in addition to
2697          * {@link JXG.Board#pointerUpListener} in the situation that a pen is used
2698          * and after an up event the pen leaves the hover range vertically. Here, it happens that
2699          * after the pointerup event further pointermove events are fired and elements get highlighted.
2700          * This highlighting has to be cancelled.
2701          *
2702          * @param {Event} evt
2703          * @returns {Boolean}
2704          */
2705         pointerLeaveListener: function (evt) {
2706             this.displayInfobox(false);
2707             this.dehighlightAll();
2708 
2709             return true;
2710         },
2711 
2712         /**
2713          * Touch-Events
2714          */
2715 
2716         /**
2717          * This method is called by the browser when a finger touches the surface of the touch-device.
2718          * @param {Event} evt The browsers event object.
2719          * @returns {Boolean} ...
2720          */
2721         touchStartListener: function (evt) {
2722             var i,
2723                 pos,
2724                 elements,
2725                 j,
2726                 k,
2727                 eps = this.options.precision.touch,
2728                 obj,
2729                 found,
2730                 targets,
2731                 evtTouches = evt[JXG.touchProperty],
2732                 target,
2733                 touchTargets;
2734 
2735             if (!this.hasTouchEnd) {
2736                 Env.addEvent(this.document, 'touchend', this.touchEndListener, this);
2737                 this.hasTouchEnd = true;
2738             }
2739 
2740             // Do not remove mouseHandlers, since Chrome on win tablets sends mouseevents if used with pen.
2741             //if (this.hasMouseHandlers) { this.removeMouseEventHandlers(); }
2742 
2743             // prevent accidental selection of text
2744             if (this.document.selection && Type.isFunction(this.document.selection.empty)) {
2745                 this.document.selection.empty();
2746             } else if (window.getSelection) {
2747                 window.getSelection().removeAllRanges();
2748             }
2749 
2750             // multitouch
2751             this._inputDevice = 'touch';
2752             this.options.precision.hasPoint = this.options.precision.touch;
2753 
2754             // This is the most critical part. first we should run through the existing touches and collect all targettouches that don't belong to our
2755             // previous touches. once this is done we run through the existing touches again and watch out for free touches that can be attached to our existing
2756             // touches, e.g. we translate (parallel translation) a line with one finger, now a second finger is over this line. this should change the operation to
2757             // a rotational translation. or one finger moves a circle, a second finger can be attached to the circle: this now changes the operation from translation to
2758             // stretching. as a last step we're going through the rest of the targettouches and initiate new move operations:
2759             //  * points have higher priority over other elements.
2760             //  * if we find a targettouch over an element that could be transformed with more than one finger, we search the rest of the targettouches, if they are over
2761             //    this element and add them.
2762             // ADDENDUM 11/10/11:
2763             //  (1) run through the touches control object,
2764             //  (2) try to find the targetTouches for every touch. on touchstart only new touches are added, hence we can find a targettouch
2765             //      for every target in our touches objects
2766             //  (3) if one of the targettouches was bound to a touches targets array, mark it
2767             //  (4) run through the targettouches. if the targettouch is marked, continue. otherwise check for elements below the targettouch:
2768             //      (a) if no element could be found: mark the target touches and continue
2769             //      --- in the following cases, 'init' means:
2770             //           (i) check if the element is already used in another touches element, if so, mark the targettouch and continue
2771             //          (ii) if not, init a new touches element, add the targettouch to the touches property and mark it
2772             //      (b) if the element is a point, init
2773             //      (c) if the element is a line, init and try to find a second targettouch on that line. if a second one is found, add and mark it
2774             //      (d) if the element is a circle, init and try to find TWO other targettouches on that circle. if only one is found, mark it and continue. otherwise
2775             //          add both to the touches array and mark them.
2776             for (i = 0; i < evtTouches.length; i++) {
2777                 evtTouches[i].jxg_isused = false;
2778             }
2779 
2780             for (i = 0; i < this.touches.length; i++) {
2781                 touchTargets = this.touches[i].targets;
2782                 for (j = 0; j < touchTargets.length; j++) {
2783                     touchTargets[j].num = -1;
2784                     eps = this.options.precision.touch;
2785 
2786                     do {
2787                         for (k = 0; k < evtTouches.length; k++) {
2788                             // find the new targettouches
2789                             if (
2790                                 Math.abs(
2791                                     Math.pow(evtTouches[k].screenX - touchTargets[j].X, 2) +
2792                                     Math.pow(evtTouches[k].screenY - touchTargets[j].Y, 2)
2793                                 ) <
2794                                 eps * eps
2795                             ) {
2796                                 touchTargets[j].num = k;
2797                                 touchTargets[j].X = evtTouches[k].screenX;
2798                                 touchTargets[j].Y = evtTouches[k].screenY;
2799                                 evtTouches[k].jxg_isused = true;
2800                                 break;
2801                             }
2802                         }
2803 
2804                         eps *= 2;
2805                     } while (
2806                         touchTargets[j].num === -1 &&
2807                         eps < this.options.precision.touchMax
2808                     );
2809 
2810                     if (touchTargets[j].num === -1) {
2811                         JXG.debug(
2812                             "i couldn't find a targettouches for target no " +
2813                             j +
2814                             ' on ' +
2815                             this.touches[i].obj.name +
2816                             ' (' +
2817                             this.touches[i].obj.id +
2818                             '). Removed the target.'
2819                         );
2820                         JXG.debug(
2821                             'eps = ' + eps + ', touchMax = ' + Options.precision.touchMax
2822                         );
2823                         touchTargets.splice(i, 1);
2824                     }
2825                 }
2826             }
2827 
2828             // we just re-mapped the targettouches to our existing touches list.
2829             // now we have to initialize some touches from additional targettouches
2830             for (i = 0; i < evtTouches.length; i++) {
2831                 if (!evtTouches[i].jxg_isused) {
2832                     pos = this.getMousePosition(evt, i);
2833                     // selection
2834                     // this._testForSelection(evt); // we do not have shift or ctrl keys yet.
2835                     if (this.selectingMode) {
2836                         this._startSelecting(pos);
2837                         this.triggerEventHandlers(
2838                             ['touchstartselecting', 'startselecting'],
2839                             [evt]
2840                         );
2841                         evt.preventDefault();
2842                         evt.stopPropagation();
2843                         this.options.precision.hasPoint = this.options.precision.mouse;
2844                         return this.touches.length > 0; // don't continue as a normal click
2845                     }
2846 
2847                     elements = this.initMoveObject(pos[0], pos[1], evt, 'touch');
2848                     if (elements.length !== 0) {
2849                         obj = elements[elements.length - 1];
2850                         target = {
2851                             num: i,
2852                             X: evtTouches[i].screenX,
2853                             Y: evtTouches[i].screenY,
2854                             Xprev: NaN,
2855                             Yprev: NaN,
2856                             Xstart: [],
2857                             Ystart: [],
2858                             Zstart: []
2859                         };
2860 
2861                         if (
2862                             Type.isPoint(obj) ||
2863                             obj.elementClass === Const.OBJECT_CLASS_TEXT ||
2864                             obj.type === Const.OBJECT_TYPE_TICKS ||
2865                             obj.type === Const.OBJECT_TYPE_IMAGE
2866                         ) {
2867                             // It's a point, so it's single touch, so we just push it to our touches
2868                             targets = [target];
2869 
2870                             // For the UNDO/REDO of object moves
2871                             this.saveStartPos(obj, targets[0]);
2872 
2873                             this.touches.push({ obj: obj, targets: targets });
2874                             obj.highlight(true);
2875                         } else if (
2876                             obj.elementClass === Const.OBJECT_CLASS_LINE ||
2877                             obj.elementClass === Const.OBJECT_CLASS_CIRCLE ||
2878                             obj.elementClass === Const.OBJECT_CLASS_CURVE ||
2879                             obj.type === Const.OBJECT_TYPE_POLYGON
2880                         ) {
2881                             found = false;
2882 
2883                             // first check if this geometric object is already captured in this.touches
2884                             for (j = 0; j < this.touches.length; j++) {
2885                                 if (obj.id === this.touches[j].obj.id) {
2886                                     found = true;
2887                                     // only add it, if we don't have two targets in there already
2888                                     if (this.touches[j].targets.length === 1) {
2889                                         // For the UNDO/REDO of object moves
2890                                         this.saveStartPos(obj, target);
2891                                         this.touches[j].targets.push(target);
2892                                     }
2893 
2894                                     evtTouches[i].jxg_isused = true;
2895                                 }
2896                             }
2897 
2898                             // we couldn't find it in touches, so we just init a new touches
2899                             // IF there is a second touch targetting this line, we will find it later on, and then add it to
2900                             // the touches control object.
2901                             if (!found) {
2902                                 targets = [target];
2903 
2904                                 // For the UNDO/REDO of object moves
2905                                 this.saveStartPos(obj, targets[0]);
2906                                 this.touches.push({ obj: obj, targets: targets });
2907                                 obj.highlight(true);
2908                             }
2909                         }
2910                     }
2911 
2912                     evtTouches[i].jxg_isused = true;
2913                 }
2914             }
2915 
2916             if (this.touches.length > 0) {
2917                 evt.preventDefault();
2918                 evt.stopPropagation();
2919             }
2920 
2921             // Touch events on empty areas of the board are handled here:
2922             // 1. case: one finger. If allowed, this triggers pan with one finger
2923             if (
2924                 evtTouches.length === 1 &&
2925                 this.mode === this.BOARD_MODE_NONE &&
2926                 this.touchStartMoveOriginOneFinger(evt)
2927             ) {
2928             } else if (
2929                 evtTouches.length === 2 &&
2930                 (this.mode === this.BOARD_MODE_NONE ||
2931                     this.mode === this.BOARD_MODE_MOVE_ORIGIN)
2932             ) {
2933                 // 2. case: two fingers: pinch to zoom or pan with two fingers needed.
2934                 // This happens when the second finger hits the device. First, the
2935                 // 'one finger pan mode' has to be cancelled.
2936                 if (this.mode === this.BOARD_MODE_MOVE_ORIGIN) {
2937                     this.originMoveEnd();
2938                 }
2939                 this.gestureStartListener(evt);
2940             }
2941 
2942             this.options.precision.hasPoint = this.options.precision.mouse;
2943             this.triggerEventHandlers(['touchstart', 'down'], [evt]);
2944 
2945             return false;
2946             //return this.touches.length > 0;
2947         },
2948 
2949         /**
2950          * Called periodically by the browser while the user moves his fingers across the device.
2951          * @param {Event} evt
2952          * @returns {Boolean}
2953          */
2954         touchMoveListener: function (evt) {
2955             var i,
2956                 pos1,
2957                 pos2,
2958                 touchTargets,
2959                 evtTouches = evt[JXG.touchProperty];
2960 
2961             if (!this.checkFrameRate(evt)) {
2962                 return false;
2963             }
2964 
2965             if (this.mode !== this.BOARD_MODE_NONE) {
2966                 evt.preventDefault();
2967                 evt.stopPropagation();
2968             }
2969 
2970             if (this.mode !== this.BOARD_MODE_DRAG) {
2971                 this.dehighlightAll();
2972                 this.displayInfobox(false);
2973             }
2974 
2975             this._inputDevice = 'touch';
2976             this.options.precision.hasPoint = this.options.precision.touch;
2977             this.updateQuality = this.BOARD_QUALITY_LOW;
2978 
2979             // selection
2980             if (this.selectingMode) {
2981                 for (i = 0; i < evtTouches.length; i++) {
2982                     if (!evtTouches[i].jxg_isused) {
2983                         pos1 = this.getMousePosition(evt, i);
2984                         this._moveSelecting(pos1);
2985                         this.triggerEventHandlers(
2986                             ['touchmoves', 'moveselecting'],
2987                             [evt, this.mode]
2988                         );
2989                         break;
2990                     }
2991                 }
2992             } else {
2993                 if (!this.touchOriginMove(evt)) {
2994                     if (this.mode === this.BOARD_MODE_DRAG) {
2995                         // Runs over through all elements which are touched
2996                         // by at least one finger.
2997                         for (i = 0; i < this.touches.length; i++) {
2998                             touchTargets = this.touches[i].targets;
2999                             if (touchTargets.length === 1) {
3000                                 // Touch by one finger:  this is possible for all elements that can be dragged
3001                                 if (evtTouches[touchTargets[0].num]) {
3002                                     pos1 = this.getMousePosition(evt, touchTargets[0].num);
3003                                     if (
3004                                         pos1[0] < 0 ||
3005                                         pos1[0] > this.canvasWidth ||
3006                                         pos1[1] < 0 ||
3007                                         pos1[1] > this.canvasHeight
3008                                     ) {
3009                                         return;
3010                                     }
3011                                     touchTargets[0].X = pos1[0];
3012                                     touchTargets[0].Y = pos1[1];
3013                                     this.moveObject(
3014                                         pos1[0],
3015                                         pos1[1],
3016                                         this.touches[i],
3017                                         evt,
3018                                         'touch'
3019                                     );
3020                                 }
3021                             } else if (
3022                                 touchTargets.length === 2 &&
3023                                 touchTargets[0].num > -1 &&
3024                                 touchTargets[1].num > -1
3025                             ) {
3026                                 // Touch by two fingers: moving lines, ...
3027                                 if (
3028                                     evtTouches[touchTargets[0].num] &&
3029                                     evtTouches[touchTargets[1].num]
3030                                 ) {
3031                                     // Get coordinates of the two touches
3032                                     pos1 = this.getMousePosition(evt, touchTargets[0].num);
3033                                     pos2 = this.getMousePosition(evt, touchTargets[1].num);
3034                                     if (
3035                                         pos1[0] < 0 ||
3036                                         pos1[0] > this.canvasWidth ||
3037                                         pos1[1] < 0 ||
3038                                         pos1[1] > this.canvasHeight ||
3039                                         pos2[0] < 0 ||
3040                                         pos2[0] > this.canvasWidth ||
3041                                         pos2[1] < 0 ||
3042                                         pos2[1] > this.canvasHeight
3043                                     ) {
3044                                         return;
3045                                     }
3046 
3047                                     touchTargets[0].X = pos1[0];
3048                                     touchTargets[0].Y = pos1[1];
3049                                     touchTargets[1].X = pos2[0];
3050                                     touchTargets[1].Y = pos2[1];
3051 
3052                                     this.twoFingerMove(
3053                                         this.touches[i],
3054                                         touchTargets[0].num,
3055                                         evt
3056                                     );
3057 
3058                                     touchTargets[0].Xprev = pos1[0];
3059                                     touchTargets[0].Yprev = pos1[1];
3060                                     touchTargets[1].Xprev = pos2[0];
3061                                     touchTargets[1].Yprev = pos2[1];
3062                                 }
3063                             }
3064                         }
3065                     } else {
3066                         if (evtTouches.length === 2) {
3067                             this.gestureChangeListener(evt);
3068                         }
3069                         // Move event without dragging an element
3070                         pos1 = this.getMousePosition(evt, 0);
3071                         this.highlightElements(pos1[0], pos1[1], evt, -1);
3072                     }
3073                 }
3074             }
3075 
3076             if (this.mode !== this.BOARD_MODE_DRAG) {
3077                 this.displayInfobox(false);
3078             }
3079 
3080             this.triggerEventHandlers(['touchmove', 'move'], [evt, this.mode]);
3081             this.options.precision.hasPoint = this.options.precision.mouse;
3082             this.updateQuality = this.BOARD_QUALITY_HIGH;
3083 
3084             return this.mode === this.BOARD_MODE_NONE;
3085         },
3086 
3087         /**
3088          * Triggered as soon as the user stops touching the device with at least one finger.
3089          * @param {Event} evt
3090          * @returns {Boolean}
3091          */
3092         touchEndListener: function (evt) {
3093             var i,
3094                 j,
3095                 k,
3096                 eps = this.options.precision.touch,
3097                 tmpTouches = [],
3098                 found,
3099                 foundNumber,
3100                 evtTouches = evt && evt[JXG.touchProperty],
3101                 touchTargets,
3102                 updateNeeded = false;
3103 
3104             this.triggerEventHandlers(['touchend', 'up'], [evt]);
3105             this.displayInfobox(false);
3106 
3107             // selection
3108             if (this.selectingMode) {
3109                 this._stopSelecting(evt);
3110                 this.triggerEventHandlers(['touchstopselecting', 'stopselecting'], [evt]);
3111                 this.stopSelectionMode();
3112             } else if (evtTouches && evtTouches.length > 0) {
3113                 for (i = 0; i < this.touches.length; i++) {
3114                     tmpTouches[i] = this.touches[i];
3115                 }
3116                 this.touches.length = 0;
3117 
3118                 // try to convert the operation, e.g. if a lines is rotated and translated with two fingers and one finger is lifted,
3119                 // convert the operation to a simple one-finger-translation.
3120                 // ADDENDUM 11/10/11:
3121                 // see addendum to touchStartListener from 11/10/11
3122                 // (1) run through the tmptouches
3123                 // (2) check the touches.obj, if it is a
3124                 //     (a) point, try to find the targettouch, if found keep it and mark the targettouch, else drop the touch.
3125                 //     (b) line with
3126                 //          (i) one target: try to find it, if found keep it mark the targettouch, else drop the touch.
3127                 //         (ii) two targets: if none can be found, drop the touch. if one can be found, remove the other target. mark all found targettouches
3128                 //     (c) circle with [proceed like in line]
3129 
3130                 // init the targettouches marker
3131                 for (i = 0; i < evtTouches.length; i++) {
3132                     evtTouches[i].jxg_isused = false;
3133                 }
3134 
3135                 for (i = 0; i < tmpTouches.length; i++) {
3136                     // could all targets of the current this.touches.obj be assigned to targettouches?
3137                     found = false;
3138                     foundNumber = 0;
3139                     touchTargets = tmpTouches[i].targets;
3140 
3141                     for (j = 0; j < touchTargets.length; j++) {
3142                         touchTargets[j].found = false;
3143                         for (k = 0; k < evtTouches.length; k++) {
3144                             if (
3145                                 Math.abs(
3146                                     Math.pow(evtTouches[k].screenX - touchTargets[j].X, 2) +
3147                                     Math.pow(evtTouches[k].screenY - touchTargets[j].Y, 2)
3148                                 ) <
3149                                 eps * eps
3150                             ) {
3151                                 touchTargets[j].found = true;
3152                                 touchTargets[j].num = k;
3153                                 touchTargets[j].X = evtTouches[k].screenX;
3154                                 touchTargets[j].Y = evtTouches[k].screenY;
3155                                 foundNumber += 1;
3156                                 break;
3157                             }
3158                         }
3159                     }
3160 
3161                     if (Type.isPoint(tmpTouches[i].obj)) {
3162                         found = touchTargets[0] && touchTargets[0].found;
3163                     } else if (tmpTouches[i].obj.elementClass === Const.OBJECT_CLASS_LINE) {
3164                         found =
3165                             (touchTargets[0] && touchTargets[0].found) ||
3166                             (touchTargets[1] && touchTargets[1].found);
3167                     } else if (tmpTouches[i].obj.elementClass === Const.OBJECT_CLASS_CIRCLE) {
3168                         found = foundNumber === 1 || foundNumber === 3;
3169                     }
3170 
3171                     // if we found this object to be still dragged by the user, add it back to this.touches
3172                     if (found) {
3173                         this.touches.push({
3174                             obj: tmpTouches[i].obj,
3175                             targets: []
3176                         });
3177 
3178                         for (j = 0; j < touchTargets.length; j++) {
3179                             if (touchTargets[j].found) {
3180                                 this.touches[this.touches.length - 1].targets.push({
3181                                     num: touchTargets[j].num,
3182                                     X: touchTargets[j].screenX,
3183                                     Y: touchTargets[j].screenY,
3184                                     Xprev: NaN,
3185                                     Yprev: NaN,
3186                                     Xstart: touchTargets[j].Xstart,
3187                                     Ystart: touchTargets[j].Ystart,
3188                                     Zstart: touchTargets[j].Zstart
3189                                 });
3190                             }
3191                         }
3192                     } else {
3193                         tmpTouches[i].obj.noHighlight();
3194                     }
3195                 }
3196             } else {
3197                 this.touches.length = 0;
3198             }
3199 
3200             for (i = this.downObjects.length - 1; i > -1; i--) {
3201                 found = false;
3202                 for (j = 0; j < this.touches.length; j++) {
3203                     if (this.touches[j].obj.id === this.downObjects[i].id) {
3204                         found = true;
3205                     }
3206                 }
3207                 if (!found) {
3208                     this.downObjects[i].triggerEventHandlers(['touchup', 'up'], [evt]);
3209                     if (!Type.exists(this.downObjects[i].coords)) {
3210                         // snapTo methods have to be called e.g. for line elements here.
3211                         // For coordsElements there might be a conflict with
3212                         // attractors, see commit from 2022.04.08, 11:12:18.
3213                         this.downObjects[i].snapToGrid();
3214                         this.downObjects[i].snapToPoints();
3215                         updateNeeded = true;
3216                     }
3217                     this.downObjects.splice(i, 1);
3218                 }
3219             }
3220 
3221             if (!evtTouches || evtTouches.length === 0) {
3222                 if (this.hasTouchEnd) {
3223                     Env.removeEvent(this.document, 'touchend', this.touchEndListener, this);
3224                     this.hasTouchEnd = false;
3225                 }
3226 
3227                 this.dehighlightAll();
3228                 this.updateQuality = this.BOARD_QUALITY_HIGH;
3229 
3230                 this.originMoveEnd();
3231                 if (updateNeeded) {
3232                     this.update();
3233                 }
3234             }
3235 
3236             return true;
3237         },
3238 
3239         /**
3240          * This method is called by the browser when the mouse button is clicked.
3241          * @param {Event} evt The browsers event object.
3242          * @returns {Boolean} True if no element is found under the current mouse pointer, false otherwise.
3243          */
3244         mouseDownListener: function (evt) {
3245             var pos, elements, result;
3246 
3247             // prevent accidental selection of text
3248             if (this.document.selection && Type.isFunction(this.document.selection.empty)) {
3249                 this.document.selection.empty();
3250             } else if (window.getSelection) {
3251                 window.getSelection().removeAllRanges();
3252             }
3253 
3254             if (!this.hasMouseUp) {
3255                 Env.addEvent(this.document, 'mouseup', this.mouseUpListener, this);
3256                 this.hasMouseUp = true;
3257             } else {
3258                 // In case this.hasMouseUp==true, it may be that there was a
3259                 // mousedown event before which was not followed by an mouseup event.
3260                 // This seems to happen with interactive whiteboard pens sometimes.
3261                 return;
3262             }
3263 
3264             this._inputDevice = 'mouse';
3265             this.options.precision.hasPoint = this.options.precision.mouse;
3266             pos = this.getMousePosition(evt);
3267 
3268             // selection
3269             this._testForSelection(evt);
3270             if (this.selectingMode) {
3271                 this._startSelecting(pos);
3272                 this.triggerEventHandlers(['mousestartselecting', 'startselecting'], [evt]);
3273                 return; // don't continue as a normal click
3274             }
3275 
3276             elements = this.initMoveObject(pos[0], pos[1], evt, 'mouse');
3277 
3278             // if no draggable object can be found, get out here immediately
3279             if (elements.length === 0) {
3280                 this.mode = this.BOARD_MODE_NONE;
3281                 result = true;
3282             } else {
3283                 /** @ignore */
3284                 this.mouse = {
3285                     obj: null,
3286                     targets: [
3287                         {
3288                             X: pos[0],
3289                             Y: pos[1],
3290                             Xprev: NaN,
3291                             Yprev: NaN
3292                         }
3293                     ]
3294                 };
3295                 this.mouse.obj = elements[elements.length - 1];
3296 
3297                 this.dehighlightAll();
3298                 this.mouse.obj.highlight(true);
3299 
3300                 this.mouse.targets[0].Xstart = [];
3301                 this.mouse.targets[0].Ystart = [];
3302                 this.mouse.targets[0].Zstart = [];
3303 
3304                 this.saveStartPos(this.mouse.obj, this.mouse.targets[0]);
3305 
3306                 // prevent accidental text selection
3307                 // this could get us new trouble: input fields, links and drop down boxes placed as text
3308                 // on the board don't work anymore.
3309                 if (evt && evt.preventDefault) {
3310                     evt.preventDefault();
3311                 } else if (window.event) {
3312                     window.event.returnValue = false;
3313                 }
3314             }
3315 
3316             if (this.mode === this.BOARD_MODE_NONE) {
3317                 result = this.mouseOriginMoveStart(evt);
3318             }
3319 
3320             this.triggerEventHandlers(['mousedown', 'down'], [evt]);
3321 
3322             return result;
3323         },
3324 
3325         /**
3326          * This method is called by the browser when the mouse is moved.
3327          * @param {Event} evt The browsers event object.
3328          */
3329         mouseMoveListener: function (evt) {
3330             var pos;
3331 
3332             if (!this.checkFrameRate(evt)) {
3333                 return false;
3334             }
3335 
3336             pos = this.getMousePosition(evt);
3337 
3338             this.updateQuality = this.BOARD_QUALITY_LOW;
3339 
3340             if (this.mode !== this.BOARD_MODE_DRAG) {
3341                 this.dehighlightAll();
3342                 this.displayInfobox(false);
3343             }
3344 
3345             // we have to check for four cases:
3346             //   * user moves origin
3347             //   * user drags an object
3348             //   * user just moves the mouse, here highlight all elements at
3349             //     the current mouse position
3350             //   * the user is selecting
3351 
3352             // selection
3353             if (this.selectingMode) {
3354                 this._moveSelecting(pos);
3355                 this.triggerEventHandlers(
3356                     ['mousemoveselecting', 'moveselecting'],
3357                     [evt, this.mode]
3358                 );
3359             } else if (!this.mouseOriginMove(evt)) {
3360                 if (this.mode === this.BOARD_MODE_DRAG) {
3361                     this.moveObject(pos[0], pos[1], this.mouse, evt, 'mouse');
3362                 } else {
3363                     // BOARD_MODE_NONE
3364                     // Move event without dragging an element
3365                     this.highlightElements(pos[0], pos[1], evt, -1);
3366                 }
3367                 this.triggerEventHandlers(['mousemove', 'move'], [evt, this.mode]);
3368             }
3369             this.updateQuality = this.BOARD_QUALITY_HIGH;
3370         },
3371 
3372         /**
3373          * This method is called by the browser when the mouse button is released.
3374          * @param {Event} evt
3375          */
3376         mouseUpListener: function (evt) {
3377             var i;
3378 
3379             if (this.selectingMode === false) {
3380                 this.triggerEventHandlers(['mouseup', 'up'], [evt]);
3381             }
3382 
3383             // redraw with high precision
3384             this.updateQuality = this.BOARD_QUALITY_HIGH;
3385 
3386             if (this.mouse && this.mouse.obj) {
3387                 if (!Type.exists(this.mouse.obj.coords)) {
3388                     // snapTo methods have to be called e.g. for line elements here.
3389                     // For coordsElements there might be a conflict with
3390                     // attractors, see commit from 2022.04.08, 11:12:18.
3391                     // The parameter is needed for lines with snapToGrid enabled
3392                     this.mouse.obj.snapToGrid(this.mouse.targets[0]);
3393                     this.mouse.obj.snapToPoints();
3394                 }
3395             }
3396 
3397             this.originMoveEnd();
3398             this.dehighlightAll();
3399             this.update();
3400 
3401             // selection
3402             if (this.selectingMode) {
3403                 this._stopSelecting(evt);
3404                 this.triggerEventHandlers(['mousestopselecting', 'stopselecting'], [evt]);
3405                 this.stopSelectionMode();
3406             } else {
3407                 for (i = 0; i < this.downObjects.length; i++) {
3408                     this.downObjects[i].triggerEventHandlers(['mouseup', 'up'], [evt]);
3409                 }
3410             }
3411 
3412             this.downObjects.length = 0;
3413 
3414             if (this.hasMouseUp) {
3415                 Env.removeEvent(this.document, 'mouseup', this.mouseUpListener, this);
3416                 this.hasMouseUp = false;
3417             }
3418 
3419             // release dragged mouse object
3420             /** @ignore */
3421             this.mouse = null;
3422         },
3423 
3424         /**
3425          * Handler for mouse wheel events. Used to zoom in and out of the board.
3426          * @param {Event} evt
3427          * @returns {Boolean}
3428          */
3429         mouseWheelListener: function (evt) {
3430             if (!this.attr.zoom.wheel || !this._isRequiredKeyPressed(evt, 'zoom')) {
3431                 return true;
3432             }
3433 
3434             evt = evt || window.event;
3435             var wd = evt.detail ? -evt.detail : evt.wheelDelta / 40,
3436                 pos = new Coords(Const.COORDS_BY_SCREEN, this.getMousePosition(evt), this);
3437 
3438             if (wd > 0) {
3439                 this.zoomIn(pos.usrCoords[1], pos.usrCoords[2]);
3440             } else {
3441                 this.zoomOut(pos.usrCoords[1], pos.usrCoords[2]);
3442             }
3443 
3444             this.triggerEventHandlers(['mousewheel'], [evt]);
3445 
3446             evt.preventDefault();
3447             return false;
3448         },
3449 
3450         /**
3451          * Allow moving of JSXGraph elements with arrow keys.
3452          * The selection of the element is done with the tab key. For this,
3453          * the attribute 'tabindex' of the element has to be set to some number (default=0).
3454          * tabindex corresponds to the HTML attribute of the same name.
3455          * <p>
3456          * Panning of the construction is done with arrow keys
3457          * if the pan key (shift or ctrl - depending on the board attributes) is pressed.
3458          * <p>
3459          * Zooming is triggered with the keys +, o, -, if
3460          * the pan key (shift or ctrl - depending on the board attributes) is pressed.
3461          * <p>
3462          * Keyboard control (move, pan, and zoom) is disabled if an HTML element of type input or textarea has received focus.
3463          *
3464          * @param  {Event} evt The browser's event object
3465          *
3466          * @see JXG.Board#keyboard
3467          * @see JXG.Board#keyFocusInListener
3468          * @see JXG.Board#keyFocusOutListener
3469          *
3470          */
3471         keyDownListener: function (evt) {
3472             var id_node = evt.target.id,
3473                 id, el, res, doc,
3474                 sX = 0,
3475                 sY = 0,
3476                 // dx, dy are provided in screen units and
3477                 // are converted to user coordinates
3478                 dx = Type.evaluate(this.attr.keyboard.dx) / this.unitX,
3479                 dy = Type.evaluate(this.attr.keyboard.dy) / this.unitY,
3480                 // u = 100,
3481                 doZoom = false,
3482                 done = true,
3483                 dir,
3484                 actPos;
3485 
3486             if (!this.attr.keyboard.enabled || id_node === '') {
3487                 return false;
3488             }
3489 
3490             // dx = Math.round(dx * u) / u;
3491             // dy = Math.round(dy * u) / u;
3492 
3493             // An element of type input or textarea has foxus, get out of here.
3494             doc = this.containerObj.shadowRoot || document;
3495             if (doc.activeElement) {
3496                 el = doc.activeElement;
3497                 if (el.tagName === 'INPUT' || el.tagName === 'textarea') {
3498                     return false;
3499                 }
3500             }
3501 
3502             // Get the JSXGraph id from the id of the SVG node.
3503             id = id_node.replace(this.containerObj.id + '_', '');
3504             el = this.select(id);
3505 
3506             if (Type.exists(el.coords)) {
3507                 actPos = el.coords.usrCoords.slice(1);
3508             }
3509 
3510             if (
3511                 (Type.evaluate(this.attr.keyboard.panshift) && evt.shiftKey) ||
3512                 (Type.evaluate(this.attr.keyboard.panctrl) && evt.ctrlKey)
3513             ) {
3514                 // Pan key has been pressed
3515 
3516                 if (Type.evaluate(this.attr.zoom.enabled) === true) {
3517                     doZoom = true;
3518                 }
3519 
3520                 // Arrow keys
3521                 if (evt.keyCode === 38) {
3522                     // up
3523                     this.clickUpArrow();
3524                 } else if (evt.keyCode === 40) {
3525                     // down
3526                     this.clickDownArrow();
3527                 } else if (evt.keyCode === 37) {
3528                     // left
3529                     this.clickLeftArrow();
3530                 } else if (evt.keyCode === 39) {
3531                     // right
3532                     this.clickRightArrow();
3533 
3534                     // Zoom keys
3535                 } else if (doZoom && evt.keyCode === 171) {
3536                     // +
3537                     this.zoomIn();
3538                 } else if (doZoom && evt.keyCode === 173) {
3539                     // -
3540                     this.zoomOut();
3541                 } else if (doZoom && evt.keyCode === 79) {
3542                     // o
3543                     this.zoom100();
3544                 } else {
3545                     done = false;
3546                 }
3547             } else {
3548                 // Adapt dx, dy to snapToGrid and attractToGrid
3549                 // snapToGrid has priority.
3550                 if (Type.exists(el.visProp)) {
3551                     if (
3552                         Type.exists(el.visProp.snaptogrid) &&
3553                         el.visProp.snaptogrid &&
3554                         Type.evaluate(el.visProp.snapsizex) &&
3555                         Type.evaluate(el.visProp.snapsizey)
3556                     ) {
3557                         // Adapt dx, dy such that snapToGrid is possible
3558                         res = el.getSnapSizes();
3559                         sX = res[0];
3560                         sY = res[1];
3561                         dx = Math.max(sX, dx);
3562                         dy = Math.max(sY, dy);
3563                     } else if (
3564                         Type.exists(el.visProp.attracttogrid) &&
3565                         el.visProp.attracttogrid &&
3566                         Type.evaluate(el.visProp.attractordistance) &&
3567                         Type.evaluate(el.visProp.attractorunit)
3568                     ) {
3569                         // Adapt dx, dy such that attractToGrid is possible
3570                         sX = 1.1 * Type.evaluate(el.visProp.attractordistance);
3571                         sY = sX;
3572 
3573                         if (Type.evaluate(el.visProp.attractorunit) === 'screen') {
3574                             sX /= this.unitX;
3575                             sY /= this.unitX;
3576                         }
3577                         dx = Math.max(sX, dx);
3578                         dy = Math.max(sY, dy);
3579                     }
3580                 }
3581 
3582                 if (evt.keyCode === 38) {
3583                     // up
3584                     dir = [0, dy];
3585                 } else if (evt.keyCode === 40) {
3586                     // down
3587                     dir = [0, -dy];
3588                 } else if (evt.keyCode === 37) {
3589                     // left
3590                     dir = [-dx, 0];
3591                 } else if (evt.keyCode === 39) {
3592                     // right
3593                     dir = [dx, 0];
3594                 } else {
3595                     done = false;
3596                 }
3597 
3598                 if (dir && el.isDraggable &&
3599                     el.visPropCalc.visible &&
3600                     ((this.geonextCompatibilityMode &&
3601                         (Type.isPoint(el) ||
3602                             el.elementClass === Const.OBJECT_CLASS_TEXT)
3603                     ) || !this.geonextCompatibilityMode) &&
3604                     !Type.evaluate(el.visProp.fixed)
3605                 ) {
3606 
3607 
3608                     this.mode = this.BOARD_MODE_DRAG;
3609                     if (Type.exists(el.coords)) {
3610                         dir[0] += actPos[0];
3611                         dir[1] += actPos[1];
3612                     }
3613                     // For coordsElement setPosition has to call setPositionDirectly.
3614                     // Otherwise the position is set by a translation.
3615                     if (Type.exists(el.coords)) {
3616                         el.setPosition(JXG.COORDS_BY_USER, dir);
3617                         this.updateInfobox(el);
3618                     } else {
3619                         this.displayInfobox(false);
3620                         el.setPositionDirectly(
3621                             Const.COORDS_BY_USER,
3622                             dir,
3623                             [0, 0]
3624                         );
3625                     }
3626 
3627                     this.triggerEventHandlers(['keymove', 'move'], [evt, this.mode]);
3628                     el.triggerEventHandlers(['keydrag', 'drag'], [evt]);
3629                     this.mode = this.BOARD_MODE_NONE;
3630                 }
3631             }
3632 
3633             this.update();
3634 
3635             if (done && Type.exists(evt.preventDefault)) {
3636                 evt.preventDefault();
3637             }
3638             return done;
3639         },
3640 
3641         /**
3642          * Event listener for SVG elements getting focus.
3643          * This is needed for highlighting when using keyboard control.
3644          * Only elements having the attribute 'tabindex' can receive focus.
3645          *
3646          * @see JXG.Board#keyFocusOutListener
3647          * @see JXG.Board#keyDownListener
3648          * @see JXG.Board#keyboard
3649          *
3650          * @param  {Event} evt The browser's event object
3651          */
3652         keyFocusInListener: function (evt) {
3653             var id_node = evt.target.id,
3654                 id,
3655                 el;
3656 
3657             if (!this.attr.keyboard.enabled || id_node === '') {
3658                 return false;
3659             }
3660 
3661             id = id_node.replace(this.containerObj.id + '_', '');
3662             el = this.select(id);
3663             if (Type.exists(el.highlight)) {
3664                 el.highlight(true);
3665                 this.focusObjects = [id];
3666                 el.triggerEventHandlers(['hit'], [evt]);
3667             }
3668             if (Type.exists(el.coords)) {
3669                 this.updateInfobox(el);
3670             }
3671         },
3672 
3673         /**
3674          * Event listener for SVG elements losing focus.
3675          * This is needed for dehighlighting when using keyboard control.
3676          * Only elements having the attribute 'tabindex' can receive focus.
3677          *
3678          * @see JXG.Board#keyFocusInListener
3679          * @see JXG.Board#keyDownListener
3680          * @see JXG.Board#keyboard
3681          *
3682          * @param  {Event} evt The browser's event object
3683          */
3684         keyFocusOutListener: function (evt) {
3685             if (!this.attr.keyboard.enabled) {
3686                 return false;
3687             }
3688             this.focusObjects = []; // This has to be before displayInfobox(false)
3689             this.dehighlightAll();
3690             this.displayInfobox(false);
3691         },
3692 
3693         /**
3694          * Update the width and height of the JSXGraph container div element.
3695          * Read actual values with getBoundingClientRect(),
3696          * and call board.resizeContainer() with this values.
3697          * <p>
3698          * If necessary, also call setBoundingBox().
3699          *
3700          * @see JXG.Board#startResizeObserver
3701          * @see JXG.Board#resizeListener
3702          * @see JXG.Board#resizeContainer
3703          * @see JXG.Board#setBoundingBox
3704          *
3705          */
3706         updateContainerDims: function () {
3707             var w, h,
3708                 bb, css,
3709                 width_adjustment, height_adjustment;
3710 
3711             // Get size of the board's container div
3712             bb = this.containerObj.getBoundingClientRect();
3713             w = bb.width;
3714             h = bb.height;
3715 
3716             // Subtract the border size
3717             if (window && window.getComputedStyle) {
3718                 css = window.getComputedStyle(this.containerObj, null);
3719                 width_adjustment = parseFloat(css.getPropertyValue('border-left-width')) + parseFloat(css.getPropertyValue('border-right-width'));
3720                 if (!isNaN(width_adjustment)) {
3721                     w -= width_adjustment;
3722                 }
3723                 height_adjustment = parseFloat(css.getPropertyValue('border-top-width')) + parseFloat(css.getPropertyValue('border-bottom-width'));
3724                 if (!isNaN(height_adjustment)) {
3725                     h -= height_adjustment;
3726                 }
3727             }
3728 
3729             // If div is invisible - do nothing
3730             if (w <= 0 || h <= 0 || isNaN(w) || isNaN(h)) {
3731                 return;
3732             }
3733 
3734             // If bounding box is not yet initialized, do it now.
3735             if (isNaN(this.getBoundingBox()[0])) {
3736                 this.setBoundingBox(this.attr.boundingbox, this.keepaspectratio, 'keep');
3737             }
3738 
3739             // Do nothing if the dimension did not change since being visible
3740             // the last time. Note that if the div had display:none in the mean time,
3741             // we did not store this._prevDim.
3742             if (Type.exists(this._prevDim) && this._prevDim.w === w && this._prevDim.h === h) {
3743                 return;
3744             }
3745 
3746             // Set the size of the SVG or canvas element
3747             this.resizeContainer(w, h, true);
3748             this._prevDim = {
3749                 w: w,
3750                 h: h
3751             };
3752         },
3753 
3754         /**
3755          * Start observer which reacts to size changes of the JSXGraph
3756          * container div element. Calls updateContainerDims().
3757          * If not available, an event listener for the window-resize event is started.
3758          * On mobile devices also scrolling might trigger resizes.
3759          * However, resize events triggered by scrolling events should be ignored.
3760          * Therefore, also a scrollListener is started.
3761          * Resize can be controlled with the board attribute resize.
3762          *
3763          * @see JXG.Board#updateContainerDims
3764          * @see JXG.Board#resizeListener
3765          * @see JXG.Board#scrollListener
3766          * @see JXG.Board#resize
3767          *
3768          */
3769         startResizeObserver: function () {
3770             var that = this;
3771 
3772             if (!Env.isBrowser || !this.attr.resize || !this.attr.resize.enabled) {
3773                 return;
3774             }
3775 
3776             this.resizeObserver = new ResizeObserver(function (entries) {
3777                 if (!that._isResizing) {
3778                     that._isResizing = true;
3779                     window.setTimeout(function () {
3780                         try {
3781                             that.updateContainerDims();
3782                         } catch (err) {
3783                             that.stopResizeObserver();
3784                         } finally {
3785                             that._isResizing = false;
3786                         }
3787                     }, that.attr.resize.throttle);
3788                 }
3789             });
3790             this.resizeObserver.observe(this.containerObj);
3791         },
3792 
3793         /**
3794          * Stops the resize observer.
3795          * @see JXG.Board#startResizeObserver
3796          *
3797          */
3798         stopResizeObserver: function () {
3799             if (!Env.isBrowser || !this.attr.resize || !this.attr.resize.enabled) {
3800                 return;
3801             }
3802 
3803             if (Type.exists(this.resizeObserver)) {
3804                 this.resizeObserver.unobserve(this.containerObj);
3805             }
3806         },
3807 
3808         /**
3809          * Fallback solutions if there is no resizeObserver available in the browser.
3810          * Reacts to resize events of the window (only). Otherwise similar to
3811          * startResizeObserver(). To handle changes of the visibility
3812          * of the JSXGraph container element, additionally an intersection observer is used.
3813          * which watches changes in the visibility of the JSXGraph container element.
3814          * This is necessary e.g. for register tabs or dia shows.
3815          *
3816          * @see JXG.Board#startResizeObserver
3817          * @see JXG.Board#startIntersectionObserver
3818          */
3819         resizeListener: function () {
3820             var that = this;
3821 
3822             if (!Env.isBrowser || !this.attr.resize || !this.attr.resize.enabled) {
3823                 return;
3824             }
3825             if (!this._isScrolling && !this._isResizing) {
3826                 this._isResizing = true;
3827                 window.setTimeout(function () {
3828                     that.updateContainerDims();
3829                     that._isResizing = false;
3830                 }, this.attr.resize.throttle);
3831             }
3832         },
3833 
3834         /**
3835          * Listener to watch for scroll events. Sets board._isScrolling = true
3836          * @param  {Event} evt The browser's event object
3837          *
3838          * @see JXG.Board#startResizeObserver
3839          * @see JXG.Board#resizeListener
3840          *
3841          */
3842         scrollListener: function (evt) {
3843             var that = this;
3844 
3845             if (!Env.isBrowser) {
3846                 return;
3847             }
3848             if (!this._isScrolling) {
3849                 this._isScrolling = true;
3850                 window.setTimeout(function () {
3851                     that._isScrolling = false;
3852                 }, 66);
3853             }
3854         },
3855 
3856         /**
3857          * Watch for changes of the visibility of the JSXGraph container element.
3858          *
3859          * @see JXG.Board#startResizeObserver
3860          * @see JXG.Board#resizeListener
3861          *
3862          */
3863         startIntersectionObserver: function () {
3864             var that = this,
3865                 options = {
3866                     root: null,
3867                     rootMargin: '0px',
3868                     threshold: 0.8
3869                 };
3870 
3871             try {
3872                 this.intersectionObserver = new IntersectionObserver(function (entries) {
3873                     // If bounding box is not yet initialized, do it now.
3874                     if (isNaN(that.getBoundingBox()[0])) {
3875                         that.updateContainerDims();
3876                     }
3877                 }, options);
3878                 this.intersectionObserver.observe(that.containerObj);
3879             } catch (err) {
3880                 console.log('JSXGraph: IntersectionObserver not available in this browser.');
3881             }
3882         },
3883 
3884         /**
3885          * Stop the intersection observer
3886          *
3887          * @see JXG.Board#startIntersectionObserver
3888          *
3889          */
3890         stopIntersectionObserver: function () {
3891             if (Type.exists(this.intersectionObserver)) {
3892                 this.intersectionObserver.unobserve(this.containerObj);
3893             }
3894         },
3895 
3896         /**********************************************************
3897          *
3898          * End of Event Handlers
3899          *
3900          **********************************************************/
3901 
3902         /**
3903          * Initialize the info box object which is used to display
3904          * the coordinates of points near the mouse pointer,
3905          * @returns {JXG.Board} Reference to the board
3906          */
3907         initInfobox: function (attributes) {
3908             var attr = Type.copyAttributes(attributes, this.options, 'infobox');
3909 
3910             attr.id = this.id + '_infobox';
3911 
3912             /**
3913              * Infobox close to points in which the points' coordinates are displayed.
3914              * This is simply a JXG.Text element. Access through board.infobox.
3915              * Uses CSS class .JXGinfobox.
3916              *
3917              * @namespace
3918              * @name JXG.Board.infobox
3919              * @type JXG.Text
3920              *
3921              * @example
3922              * const board = JXG.JSXGraph.initBoard(BOARDID, {
3923              *     boundingbox: [-0.5, 0.5, 0.5, -0.5],
3924              *     intl: {
3925              *         enabled: false,
3926              *         locale: 'de-DE'
3927              *     },
3928              *     keepaspectratio: true,
3929              *     axis: true,
3930              *     infobox: {
3931              *         distanceY: 40,
3932              *         intl: {
3933              *             enabled: true,
3934              *             options: {
3935              *                 minimumFractionDigits: 1,
3936              *                 maximumFractionDigits: 2
3937              *             }
3938              *         }
3939              *     }
3940              * });
3941              * var p = board.create('point', [0.1, 0.1], {});
3942              *
3943              * </pre><div id="JXG822161af-fe77-4769-850f-cdf69935eab0" class="jxgbox" style="width: 300px; height: 300px;"></div>
3944              * <script type="text/javascript">
3945              *     (function() {
3946              *     const board = JXG.JSXGraph.initBoard('JXG822161af-fe77-4769-850f-cdf69935eab0', {
3947              *         boundingbox: [-0.5, 0.5, 0.5, -0.5], showcopyright: false, shownavigation: false,
3948              *         intl: {
3949              *             enabled: false,
3950              *             locale: 'de-DE'
3951              *         },
3952              *         keepaspectratio: true,
3953              *         axis: true,
3954              *         infobox: {
3955              *             distanceY: 40,
3956              *             intl: {
3957              *                 enabled: true,
3958              *                 options: {
3959              *                     minimumFractionDigits: 1,
3960              *                     maximumFractionDigits: 2
3961              *                 }
3962              *             }
3963              *         }
3964              *     });
3965              *     var p = board.create('point', [0.1, 0.1], {});
3966              *     })();
3967              *
3968              * </script><pre>
3969              *
3970              */
3971             this.infobox = this.create('text', [0, 0, '0,0'], attr);
3972             // this.infobox.needsUpdateSize = false;  // That is not true, but it speeds drawing up.
3973             this.infobox.dump = false;
3974 
3975             this.displayInfobox(false);
3976             return this;
3977         },
3978 
3979         /**
3980          * Updates and displays a little info box to show coordinates of current selected points.
3981          * @param {JXG.GeometryElement} el A GeometryElement
3982          * @returns {JXG.Board} Reference to the board
3983          * @see JXG.Board#displayInfobox
3984          * @see JXG.Board#showInfobox
3985          * @see Point#showInfobox
3986          *
3987          */
3988         updateInfobox: function (el) {
3989             var x, y, xc, yc,
3990                 vpinfoboxdigits,
3991                 distX, distY,
3992                 vpsi = Type.evaluate(el.visProp.showinfobox);
3993 
3994             if ((!Type.evaluate(this.attr.showinfobox) && vpsi === 'inherit') || !vpsi) {
3995                 return this;
3996             }
3997 
3998             if (Type.isPoint(el)) {
3999                 xc = el.coords.usrCoords[1];
4000                 yc = el.coords.usrCoords[2];
4001                 distX = Type.evaluate(this.infobox.visProp.distancex);
4002                 distY = Type.evaluate(this.infobox.visProp.distancey);
4003 
4004                 this.infobox.setCoords(
4005                     xc + distX / this.unitX,
4006                     yc + distY / this.unitY
4007                 );
4008 
4009                 vpinfoboxdigits = Type.evaluate(el.visProp.infoboxdigits);
4010                 if (typeof el.infoboxText !== 'string') {
4011                     if (vpinfoboxdigits === 'auto') {
4012                         if (this.infobox.useLocale()) {
4013                             x = this.infobox.formatNumberLocale(xc);
4014                             y = this.infobox.formatNumberLocale(yc);
4015                         } else {
4016                             x = Type.autoDigits(xc);
4017                             y = Type.autoDigits(yc);
4018                         }
4019                     } else if (Type.isNumber(vpinfoboxdigits)) {
4020                         if (this.infobox.useLocale()) {
4021                             x = this.infobox.formatNumberLocale(xc, vpinfoboxdigits);
4022                             y = this.infobox.formatNumberLocale(yc, vpinfoboxdigits);
4023                         } else {
4024                             x = Type.toFixed(xc, vpinfoboxdigits);
4025                             y = Type.toFixed(yc, vpinfoboxdigits);
4026                         }
4027 
4028                     } else {
4029                         x = xc;
4030                         y = yc;
4031                     }
4032 
4033                     this.highlightInfobox(x, y, el);
4034                 } else {
4035                     this.highlightCustomInfobox(el.infoboxText, el);
4036                 }
4037 
4038                 this.displayInfobox(true);
4039             }
4040             return this;
4041         },
4042 
4043         /**
4044          * Set infobox visible / invisible.
4045          *
4046          * It uses its property hiddenByParent to memorize its status.
4047          * In this way, many DOM access can be avoided.
4048          *
4049          * @param  {Boolean} val true for visible, false for invisible
4050          * @returns {JXG.Board} Reference to the board.
4051          * @see JXG.Board#updateInfobox
4052          *
4053          */
4054         displayInfobox: function (val) {
4055             if (!val && this.focusObjects.length > 0 &&
4056                 this.select(this.focusObjects[0]).elementClass === Const.OBJECT_CLASS_POINT) {
4057                 // If an element has focus we do not hide its infobox
4058                 return this;
4059             }
4060             if (this.infobox.hiddenByParent === val) {
4061                 this.infobox.hiddenByParent = !val;
4062                 this.infobox.prepareUpdate().updateVisibility(val).updateRenderer();
4063             }
4064             return this;
4065         },
4066 
4067         // Alias for displayInfobox to be backwards compatible.
4068         // The method showInfobox clashes with the board attribute showInfobox
4069         showInfobox: function (val) {
4070             return this.displayInfobox(val);
4071         },
4072 
4073         /**
4074          * Changes the text of the info box to show the given coordinates.
4075          * @param {Number} x
4076          * @param {Number} y
4077          * @param {JXG.GeometryElement} [el] The element the mouse is pointing at
4078          * @returns {JXG.Board} Reference to the board.
4079          */
4080         highlightInfobox: function (x, y, el) {
4081             this.highlightCustomInfobox('(' + x + ', ' + y + ')', el);
4082             return this;
4083         },
4084 
4085         /**
4086          * Changes the text of the info box to what is provided via text.
4087          * @param {String} text
4088          * @param {JXG.GeometryElement} [el]
4089          * @returns {JXG.Board} Reference to the board.
4090          */
4091         highlightCustomInfobox: function (text, el) {
4092             this.infobox.setText(text);
4093             return this;
4094         },
4095 
4096         /**
4097          * Remove highlighting of all elements.
4098          * @returns {JXG.Board} Reference to the board.
4099          */
4100         dehighlightAll: function () {
4101             var el,
4102                 pEl,
4103                 stillHighlighted = {},
4104                 needsDeHighlight = false;
4105 
4106             for (el in this.highlightedObjects) {
4107                 if (this.highlightedObjects.hasOwnProperty(el)) {
4108 
4109                     pEl = this.highlightedObjects[el];
4110                     if (this.focusObjects.indexOf(el) < 0) { // Element does not have focus
4111                         if (this.hasMouseHandlers || this.hasPointerHandlers) {
4112                             pEl.noHighlight();
4113                         }
4114                         needsDeHighlight = true;
4115                     } else {
4116                         stillHighlighted[el] = pEl;
4117                     }
4118                     // In highlightedObjects should only be objects which fulfill all these conditions
4119                     // And in case of complex elements, like a turtle based fractal, it should be faster to
4120                     // just de-highlight the element instead of checking hasPoint...
4121                     // if ((!Type.exists(pEl.hasPoint)) || !pEl.hasPoint(x, y) || !pEl.visPropCalc.visible)
4122                 }
4123             }
4124 
4125             this.highlightedObjects = stillHighlighted;
4126 
4127             // We do not need to redraw during dehighlighting in CanvasRenderer
4128             // because we are redrawing anyhow
4129             //  -- We do need to redraw during dehighlighting. Otherwise objects won't be dehighlighted until
4130             // another object is highlighted.
4131             if (this.renderer.type === 'canvas' && needsDeHighlight) {
4132                 this.prepareUpdate();
4133                 this.renderer.suspendRedraw(this);
4134                 this.updateRenderer();
4135                 this.renderer.unsuspendRedraw();
4136             }
4137 
4138             return this;
4139         },
4140 
4141         /**
4142          * Returns the input parameters in an array. This method looks pointless and it really is, but it had a purpose
4143          * once.
4144          * @private
4145          * @param {Number} x X coordinate in screen coordinates
4146          * @param {Number} y Y coordinate in screen coordinates
4147          * @returns {Array} Coordinates [x, y] of the mouse in screen coordinates.
4148          * @see JXG.Board#getUsrCoordsOfMouse
4149          */
4150         getScrCoordsOfMouse: function (x, y) {
4151             return [x, y];
4152         },
4153 
4154         /**
4155          * This method calculates the user coords of the current mouse coordinates.
4156          * @param {Event} evt Event object containing the mouse coordinates.
4157          * @returns {Array} Coordinates [x, y] of the mouse in user coordinates.
4158          * @example
4159          * board.on('up', function (evt) {
4160          *         var a = board.getUsrCoordsOfMouse(evt),
4161          *             x = a[0],
4162          *             y = a[1],
4163          *             somePoint = board.create('point', [x,y], {name:'SomePoint',size:4});
4164          *             // Shorter version:
4165          *             //somePoint = board.create('point', a, {name:'SomePoint',size:4});
4166          *         });
4167          *
4168          * </pre><div id='JXG48d5066b-16ba-4920-b8ea-a4f8eff6b746' class='jxgbox' style='width: 300px; height: 300px;'></div>
4169          * <script type='text/javascript'>
4170          *     (function() {
4171          *         var board = JXG.JSXGraph.initBoard('JXG48d5066b-16ba-4920-b8ea-a4f8eff6b746',
4172          *             {boundingbox: [-8, 8, 8,-8], axis: true, showcopyright: false, shownavigation: false});
4173          *     board.on('up', function (evt) {
4174          *             var a = board.getUsrCoordsOfMouse(evt),
4175          *                 x = a[0],
4176          *                 y = a[1],
4177          *                 somePoint = board.create('point', [x,y], {name:'SomePoint',size:4});
4178          *                 // Shorter version:
4179          *                 //somePoint = board.create('point', a, {name:'SomePoint',size:4});
4180          *             });
4181          *
4182          *     })();
4183          *
4184          * </script><pre>
4185          *
4186          * @see JXG.Board#getScrCoordsOfMouse
4187          * @see JXG.Board#getAllUnderMouse
4188          */
4189         getUsrCoordsOfMouse: function (evt) {
4190             var cPos = this.getCoordsTopLeftCorner(),
4191                 absPos = Env.getPosition(evt, null, this.document),
4192                 x = absPos[0] - cPos[0],
4193                 y = absPos[1] - cPos[1],
4194                 newCoords = new Coords(Const.COORDS_BY_SCREEN, [x, y], this);
4195 
4196             return newCoords.usrCoords.slice(1);
4197         },
4198 
4199         /**
4200          * Collects all elements under current mouse position plus current user coordinates of mouse cursor.
4201          * @param {Event} evt Event object containing the mouse coordinates.
4202          * @returns {Array} Array of elements at the current mouse position plus current user coordinates of mouse.
4203          * @see JXG.Board#getUsrCoordsOfMouse
4204          * @see JXG.Board#getAllObjectsUnderMouse
4205          */
4206         getAllUnderMouse: function (evt) {
4207             var elList = this.getAllObjectsUnderMouse(evt);
4208             elList.push(this.getUsrCoordsOfMouse(evt));
4209 
4210             return elList;
4211         },
4212 
4213         /**
4214          * Collects all elements under current mouse position.
4215          * @param {Event} evt Event object containing the mouse coordinates.
4216          * @returns {Array} Array of elements at the current mouse position.
4217          * @see JXG.Board#getAllUnderMouse
4218          */
4219         getAllObjectsUnderMouse: function (evt) {
4220             var cPos = this.getCoordsTopLeftCorner(),
4221                 absPos = Env.getPosition(evt, null, this.document),
4222                 dx = absPos[0] - cPos[0],
4223                 dy = absPos[1] - cPos[1],
4224                 elList = [],
4225                 el,
4226                 pEl,
4227                 len = this.objectsList.length;
4228 
4229             for (el = 0; el < len; el++) {
4230                 pEl = this.objectsList[el];
4231                 if (pEl.visPropCalc.visible && pEl.hasPoint && pEl.hasPoint(dx, dy)) {
4232                     elList[elList.length] = pEl;
4233                 }
4234             }
4235 
4236             return elList;
4237         },
4238 
4239         /**
4240          * Update the coords object of all elements which possess this
4241          * property. This is necessary after changing the viewport.
4242          * @returns {JXG.Board} Reference to this board.
4243          **/
4244         updateCoords: function () {
4245             var el,
4246                 ob,
4247                 len = this.objectsList.length;
4248 
4249             for (ob = 0; ob < len; ob++) {
4250                 el = this.objectsList[ob];
4251 
4252                 if (Type.exists(el.coords)) {
4253                     if (Type.evaluate(el.visProp.frozen)) {
4254                         if (el.is3D) {
4255                             el.element2D.coords.screen2usr();
4256                         } else {
4257                             el.coords.screen2usr();
4258                         }
4259                     } else {
4260                         if (el.is3D) {
4261                             el.element2D.coords.usr2screen();
4262                         } else {
4263                             el.coords.usr2screen();
4264                         }
4265                     }
4266                 }
4267             }
4268             return this;
4269         },
4270 
4271         /**
4272          * Moves the origin and initializes an update of all elements.
4273          * @param {Number} x
4274          * @param {Number} y
4275          * @param {Boolean} [diff=false]
4276          * @returns {JXG.Board} Reference to this board.
4277          */
4278         moveOrigin: function (x, y, diff) {
4279             var ox, oy, ul, lr;
4280             if (Type.exists(x) && Type.exists(y)) {
4281                 ox = this.origin.scrCoords[1];
4282                 oy = this.origin.scrCoords[2];
4283 
4284                 this.origin.scrCoords[1] = x;
4285                 this.origin.scrCoords[2] = y;
4286 
4287                 if (diff) {
4288                     this.origin.scrCoords[1] -= this.drag_dx;
4289                     this.origin.scrCoords[2] -= this.drag_dy;
4290                 }
4291 
4292                 ul = new Coords(Const.COORDS_BY_SCREEN, [0, 0], this).usrCoords;
4293                 lr = new Coords(
4294                     Const.COORDS_BY_SCREEN,
4295                     [this.canvasWidth, this.canvasHeight],
4296                     this
4297                 ).usrCoords;
4298                 if (
4299                     ul[1] < this.maxboundingbox[0] ||
4300                     ul[2] > this.maxboundingbox[1] ||
4301                     lr[1] > this.maxboundingbox[2] ||
4302                     lr[2] < this.maxboundingbox[3]
4303                 ) {
4304                     this.origin.scrCoords[1] = ox;
4305                     this.origin.scrCoords[2] = oy;
4306                 }
4307             }
4308 
4309             this.updateCoords().clearTraces().fullUpdate();
4310             this.triggerEventHandlers(['boundingbox']);
4311 
4312             return this;
4313         },
4314 
4315         /**
4316          * Add conditional updates to the elements.
4317          * @param {String} str String containing coniditional update in geonext syntax
4318          */
4319         addConditions: function (str) {
4320             var term,
4321                 m,
4322                 left,
4323                 right,
4324                 name,
4325                 el,
4326                 property,
4327                 functions = [],
4328                 // plaintext = 'var el, x, y, c, rgbo;\n',
4329                 i = str.indexOf('<data>'),
4330                 j = str.indexOf('<' + '/data>'),
4331                 xyFun = function (board, el, f, what) {
4332                     return function () {
4333                         var e, t;
4334 
4335                         e = board.select(el.id);
4336                         t = e.coords.usrCoords[what];
4337 
4338                         if (what === 2) {
4339                             e.setPositionDirectly(Const.COORDS_BY_USER, [f(), t]);
4340                         } else {
4341                             e.setPositionDirectly(Const.COORDS_BY_USER, [t, f()]);
4342                         }
4343                         e.prepareUpdate().update();
4344                     };
4345                 },
4346                 visFun = function (board, el, f) {
4347                     return function () {
4348                         var e, v;
4349 
4350                         e = board.select(el.id);
4351                         v = f();
4352 
4353                         e.setAttribute({ visible: v });
4354                     };
4355                 },
4356                 colFun = function (board, el, f, what) {
4357                     return function () {
4358                         var e, v;
4359 
4360                         e = board.select(el.id);
4361                         v = f();
4362 
4363                         if (what === 'strokewidth') {
4364                             e.visProp.strokewidth = v;
4365                         } else {
4366                             v = Color.rgba2rgbo(v);
4367                             e.visProp[what + 'color'] = v[0];
4368                             e.visProp[what + 'opacity'] = v[1];
4369                         }
4370                     };
4371                 },
4372                 posFun = function (board, el, f) {
4373                     return function () {
4374                         var e = board.select(el.id);
4375 
4376                         e.position = f();
4377                     };
4378                 },
4379                 styleFun = function (board, el, f) {
4380                     return function () {
4381                         var e = board.select(el.id);
4382 
4383                         e.setStyle(f());
4384                     };
4385                 };
4386 
4387             if (i < 0) {
4388                 return;
4389             }
4390 
4391             while (i >= 0) {
4392                 term = str.slice(i + 6, j); // throw away <data>
4393                 m = term.indexOf('=');
4394                 left = term.slice(0, m);
4395                 right = term.slice(m + 1);
4396                 m = left.indexOf('.'); // Dies erzeugt Probleme bei Variablennamen der Form ' Steuern akt.'
4397                 name = left.slice(0, m); //.replace(/\s+$/,''); // do NOT cut out name (with whitespace)
4398                 el = this.elementsByName[Type.unescapeHTML(name)];
4399 
4400                 property = left
4401                     .slice(m + 1)
4402                     .replace(/\s+/g, '')
4403                     .toLowerCase(); // remove whitespace in property
4404                 right = Type.createFunction(right, this, '', true);
4405 
4406                 // Debug
4407                 if (!Type.exists(this.elementsByName[name])) {
4408                     JXG.debug('debug conditions: |' + name + '| undefined');
4409                 } else {
4410                     // plaintext += 'el = this.objects[\'' + el.id + '\'];\n';
4411 
4412                     switch (property) {
4413                         case 'x':
4414                             functions.push(xyFun(this, el, right, 2));
4415                             break;
4416                         case 'y':
4417                             functions.push(xyFun(this, el, right, 1));
4418                             break;
4419                         case 'visible':
4420                             functions.push(visFun(this, el, right));
4421                             break;
4422                         case 'position':
4423                             functions.push(posFun(this, el, right));
4424                             break;
4425                         case 'stroke':
4426                             functions.push(colFun(this, el, right, 'stroke'));
4427                             break;
4428                         case 'style':
4429                             functions.push(styleFun(this, el, right));
4430                             break;
4431                         case 'strokewidth':
4432                             functions.push(colFun(this, el, right, 'strokewidth'));
4433                             break;
4434                         case 'fill':
4435                             functions.push(colFun(this, el, right, 'fill'));
4436                             break;
4437                         case 'label':
4438                             break;
4439                         default:
4440                             JXG.debug(
4441                                 'property "' +
4442                                 property +
4443                                 '" in conditions not yet implemented:' +
4444                                 right
4445                             );
4446                             break;
4447                     }
4448                 }
4449                 str = str.slice(j + 7); // cut off '</data>'
4450                 i = str.indexOf('<data>');
4451                 j = str.indexOf('<' + '/data>');
4452             }
4453 
4454             this.updateConditions = function () {
4455                 var i;
4456 
4457                 for (i = 0; i < functions.length; i++) {
4458                     functions[i]();
4459                 }
4460 
4461                 this.prepareUpdate().updateElements();
4462                 return true;
4463             };
4464             this.updateConditions();
4465         },
4466 
4467         /**
4468          * Computes the commands in the conditions-section of the gxt file.
4469          * It is evaluated after an update, before the unsuspendRedraw.
4470          * The function is generated in
4471          * @see JXG.Board#addConditions
4472          * @private
4473          */
4474         updateConditions: function () {
4475             return false;
4476         },
4477 
4478         /**
4479          * Calculates adequate snap sizes.
4480          * @returns {JXG.Board} Reference to the board.
4481          */
4482         calculateSnapSizes: function () {
4483             var p1 = new Coords(Const.COORDS_BY_USER, [0, 0], this),
4484                 p2 = new Coords(
4485                     Const.COORDS_BY_USER,
4486                     [this.options.grid.gridX, this.options.grid.gridY],
4487                     this
4488                 ),
4489                 x = p1.scrCoords[1] - p2.scrCoords[1],
4490                 y = p1.scrCoords[2] - p2.scrCoords[2];
4491 
4492             this.options.grid.snapSizeX = this.options.grid.gridX;
4493             while (Math.abs(x) > 25) {
4494                 this.options.grid.snapSizeX *= 2;
4495                 x /= 2;
4496             }
4497 
4498             this.options.grid.snapSizeY = this.options.grid.gridY;
4499             while (Math.abs(y) > 25) {
4500                 this.options.grid.snapSizeY *= 2;
4501                 y /= 2;
4502             }
4503 
4504             return this;
4505         },
4506 
4507         /**
4508          * Apply update on all objects with the new zoom-factors. Clears all traces.
4509          * @returns {JXG.Board} Reference to the board.
4510          */
4511         applyZoom: function () {
4512             this.updateCoords().calculateSnapSizes().clearTraces().fullUpdate();
4513 
4514             return this;
4515         },
4516 
4517         /**
4518          * Zooms into the board by the factors board.attr.zoom.factorX and board.attr.zoom.factorY and applies the zoom.
4519          * The zoom operation is centered at x, y.
4520          * @param {Number} [x]
4521          * @param {Number} [y]
4522          * @returns {JXG.Board} Reference to the board
4523          */
4524         zoomIn: function (x, y) {
4525             var bb = this.getBoundingBox(),
4526                 zX = this.attr.zoom.factorx,
4527                 zY = this.attr.zoom.factory,
4528                 dX = (bb[2] - bb[0]) * (1.0 - 1.0 / zX),
4529                 dY = (bb[1] - bb[3]) * (1.0 - 1.0 / zY),
4530                 lr = 0.5,
4531                 tr = 0.5,
4532                 mi = this.attr.zoom.eps || this.attr.zoom.min || 0.001; // this.attr.zoom.eps is deprecated
4533 
4534             if (
4535                 (this.zoomX > this.attr.zoom.max && zX > 1.0) ||
4536                 (this.zoomY > this.attr.zoom.max && zY > 1.0) ||
4537                 (this.zoomX < mi && zX < 1.0) || // zoomIn is used for all zooms on touch devices
4538                 (this.zoomY < mi && zY < 1.0)
4539             ) {
4540                 return this;
4541             }
4542 
4543             if (Type.isNumber(x) && Type.isNumber(y)) {
4544                 lr = (x - bb[0]) / (bb[2] - bb[0]);
4545                 tr = (bb[1] - y) / (bb[1] - bb[3]);
4546             }
4547 
4548             this.setBoundingBox(
4549                 [
4550                     bb[0] + dX * lr,
4551                     bb[1] - dY * tr,
4552                     bb[2] - dX * (1 - lr),
4553                     bb[3] + dY * (1 - tr)
4554                 ],
4555                 this.keepaspectratio,
4556                 'update'
4557             );
4558             return this.applyZoom();
4559         },
4560 
4561         /**
4562          * Zooms out of the board by the factors board.attr.zoom.factorX and board.attr.zoom.factorY and applies the zoom.
4563          * The zoom operation is centered at x, y.
4564          *
4565          * @param {Number} [x]
4566          * @param {Number} [y]
4567          * @returns {JXG.Board} Reference to the board
4568          */
4569         zoomOut: function (x, y) {
4570             var bb = this.getBoundingBox(),
4571                 zX = this.attr.zoom.factorx,
4572                 zY = this.attr.zoom.factory,
4573                 dX = (bb[2] - bb[0]) * (1.0 - zX),
4574                 dY = (bb[1] - bb[3]) * (1.0 - zY),
4575                 lr = 0.5,
4576                 tr = 0.5,
4577                 mi = this.attr.zoom.eps || this.attr.zoom.min || 0.001; // this.attr.zoom.eps is deprecated
4578 
4579             if (this.zoomX < mi || this.zoomY < mi) {
4580                 return this;
4581             }
4582 
4583             if (Type.isNumber(x) && Type.isNumber(y)) {
4584                 lr = (x - bb[0]) / (bb[2] - bb[0]);
4585                 tr = (bb[1] - y) / (bb[1] - bb[3]);
4586             }
4587 
4588             this.setBoundingBox(
4589                 [
4590                     bb[0] + dX * lr,
4591                     bb[1] - dY * tr,
4592                     bb[2] - dX * (1 - lr),
4593                     bb[3] + dY * (1 - tr)
4594                 ],
4595                 this.keepaspectratio,
4596                 'update'
4597             );
4598 
4599             return this.applyZoom();
4600         },
4601 
4602         /**
4603          * Reset the zoom level to the original zoom level from initBoard();
4604          * Additionally, if the board as been initialized with a boundingBox (which is the default),
4605          * restore the viewport to the original viewport during initialization. Otherwise,
4606          * (i.e. if the board as been initialized with unitX/Y and originX/Y),
4607          * just set the zoom level to 100%.
4608          *
4609          * @returns {JXG.Board} Reference to the board
4610          */
4611         zoom100: function () {
4612             var bb, dX, dY;
4613 
4614             if (Type.exists(this.attr.boundingbox)) {
4615                 this.setBoundingBox(this.attr.boundingbox, this.keepaspectratio, 'reset');
4616             } else {
4617                 // Board has been set up with unitX/Y and originX/Y
4618                 bb = this.getBoundingBox();
4619                 dX = (bb[2] - bb[0]) * (1.0 - this.zoomX) * 0.5;
4620                 dY = (bb[1] - bb[3]) * (1.0 - this.zoomY) * 0.5;
4621                 this.setBoundingBox(
4622                     [bb[0] + dX, bb[1] - dY, bb[2] - dX, bb[3] + dY],
4623                     this.keepaspectratio,
4624                     'reset'
4625                 );
4626             }
4627             return this.applyZoom();
4628         },
4629 
4630         /**
4631          * Zooms the board so every visible point is shown. Keeps aspect ratio.
4632          * @returns {JXG.Board} Reference to the board
4633          */
4634         zoomAllPoints: function () {
4635             var el,
4636                 border,
4637                 borderX,
4638                 borderY,
4639                 pEl,
4640                 minX = 0,
4641                 maxX = 0,
4642                 minY = 0,
4643                 maxY = 0,
4644                 len = this.objectsList.length;
4645 
4646             for (el = 0; el < len; el++) {
4647                 pEl = this.objectsList[el];
4648 
4649                 if (Type.isPoint(pEl) && pEl.visPropCalc.visible) {
4650                     if (pEl.coords.usrCoords[1] < minX) {
4651                         minX = pEl.coords.usrCoords[1];
4652                     } else if (pEl.coords.usrCoords[1] > maxX) {
4653                         maxX = pEl.coords.usrCoords[1];
4654                     }
4655                     if (pEl.coords.usrCoords[2] > maxY) {
4656                         maxY = pEl.coords.usrCoords[2];
4657                     } else if (pEl.coords.usrCoords[2] < minY) {
4658                         minY = pEl.coords.usrCoords[2];
4659                     }
4660                 }
4661             }
4662 
4663             border = 50;
4664             borderX = border / this.unitX;
4665             borderY = border / this.unitY;
4666 
4667             this.setBoundingBox(
4668                 [minX - borderX, maxY + borderY, maxX + borderX, minY - borderY],
4669                 this.keepaspectratio,
4670                 'update'
4671             );
4672 
4673             return this.applyZoom();
4674         },
4675 
4676         /**
4677          * Reset the bounding box and the zoom level to 100% such that a given set of elements is
4678          * within the board's viewport.
4679          * @param {Array} elements A set of elements given by id, reference, or name.
4680          * @returns {JXG.Board} Reference to the board.
4681          */
4682         zoomElements: function (elements) {
4683             var i,
4684                 e,
4685                 box,
4686                 newBBox = [Infinity, -Infinity, -Infinity, Infinity],
4687                 cx,
4688                 cy,
4689                 dx,
4690                 dy,
4691                 d;
4692 
4693             if (!Type.isArray(elements) || elements.length === 0) {
4694                 return this;
4695             }
4696 
4697             for (i = 0; i <