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