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