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