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