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