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