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