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