      1 /*
      2     Copyright 2008-2022
      3         Matthias Ehmann,
      4         Michael Gerhaeuser,
      5         Carsten Miller,
      6         Bianca Valentin,
      7         Alfred Wassermann,
      8         Peter Wilfahrt
      9
     10     This file is part of JSXGraph.
     11
     12     JSXGraph is free software dual licensed under the GNU LGPL or MIT License.
     13
     14     You can redistribute it and/or modify it under the terms of the
     15
     16       * GNU Lesser General Public License as published by
     17         the Free Software Foundation, either version 3 of the License, or
     18         (at your option) any later version
     19       OR
     20       * MIT License: https://github.com/jsxgraph/jsxgraph/blob/master/LICENSE.MIT
     21
     22     JSXGraph is distributed in the hope that it will be useful,
     23     but WITHOUT ANY WARRANTY; without even the implied warranty of
     24     MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
     25     GNU Lesser General Public License for more details.
     26
     27     You should have received a copy of the GNU Lesser General Public License and
     28     the MIT License along with JSXGraph. If not, see <http://www.gnu.org/licenses/>
     29     and <http://opensource.org/licenses/MIT/>.
     30  */
     31
     32

     34
     35 /*jslint nomen: true, plusplus: true*/
     36
     37 /* depends:
     38  jxg
     39  base/constants
     40  base/coords
     41  options
     42  math/numerics
     43  math/math
     44  math/geometry
     45  math/complex
     46  parser/jessiecode
     47  parser/geonext
     48  utils/color
     49  utils/type
     50  utils/event
     51  utils/env
     52   elements:
     53    transform
     54    point
     55    line
     56    text
     57    grid
     58  */
     59
     60 /**
     61  * @fileoverview The JXG.Board class is defined in this file. JXG.Board controls all properties and methods
     62  * used to manage a geonext board like managing geometric elements, managing mouse and touch events, etc.
     63  */
     64
     65 define([
     66     'jxg', 'base/constants', 'base/coords', 'options', 'math/numerics', 'math/math', 'math/geometry', 'math/complex',
     67     'math/statistics',
     68     'parser/jessiecode', 'utils/color', 'utils/type', 'utils/event', 'utils/env',
     69     'base/composition'
     70 ], function (JXG, Const, Coords, Options, Numerics, Mat, Geometry, Complex, Statistics, JessieCode, Color, Type,
     71                 EventEmitter, Env, Composition) {
     72
     73     'use strict';
     74
     75     /**
     76      * Constructs a new Board object.
     77      * @class JXG.Board controls all properties and methods used to manage a geonext board like managing geometric
     78      * elements, managing mouse and touch events, etc. You probably don't want to use this constructor directly.
     79      * Please use {@link JXG.JSXGraph.initBoard} to initialize a board.
     80      * @constructor
     81      * @param {String} container The id or reference of the HTML DOM element the board is drawn in. This is usually a HTML div.
     82      * @param {JXG.AbstractRenderer} renderer The reference of a renderer.
     83      * @param {String} id Unique identifier for the board, may be an empty string or null or even undefined.
     84      * @param {JXG.Coords} origin The coordinates where the origin is placed, in user coordinates.
     85      * @param {Number} zoomX Zoom factor in x-axis direction
     86      * @param {Number} zoomY Zoom factor in y-axis direction
     87      * @param {Number} unitX Units in x-axis direction
     88      * @param {Number} unitY Units in y-axis direction
     89      * @param {Number} canvasWidth  The width of canvas
     90      * @param {Number} canvasHeight The height of canvas
     91      * @param {Object} attributes The attributes object given to {@link JXG.JSXGraph.initBoard}
     92      * @borrows JXG.EventEmitter#on as this.on
     93      * @borrows JXG.EventEmitter#off as this.off
     94      * @borrows JXG.EventEmitter#triggerEventHandlers as this.triggerEventHandlers
     95      * @borrows JXG.EventEmitter#eventHandlers as this.eventHandlers
     96      */
     97     JXG.Board = function (container, renderer, id, origin, zoomX, zoomY, unitX, unitY, canvasWidth, canvasHeight, attributes) {
     98         /**
     99          * Board is in no special mode, objects are highlighted on mouse over and objects may be
    100          * clicked to start drag&drop.
    101          * @type Number
    102          * @constant
    103          */
    104         this.BOARD_MODE_NONE = 0x0000;
    105
    106         /**
    107          * Board is in drag mode, objects aren't highlighted on mouse over and the object referenced in
    108          * {@link JXG.Board#mouse} is updated on mouse movement.
    109          * @type Number
    110          * @constant
    111          * @see JXG.Board#drag_obj
    112          */
    113         this.BOARD_MODE_DRAG = 0x0001;
    114
    115         /**
    116          * In this mode a mouse move changes the origin's screen coordinates.
    117          * @type Number
    118          * @constant
    119          */
    120         this.BOARD_MODE_MOVE_ORIGIN = 0x0002;
    121
    122         /**
    123          * Update is made with high quality, e.g. graphs are evaluated at much more points.
    124          * @type Number
    125          * @constant
    126          * @see JXG.Board#updateQuality
    127          */
    128         this.BOARD_MODE_ZOOM = 0x0011;
    129
    130         /**
    131          * Update is made with low quality, e.g. graphs are evaluated at a lesser amount of points.
    132          * @type Number
    133          * @constant
    134          * @see JXG.Board#updateQuality
    135          */
    136         this.BOARD_QUALITY_LOW = 0x1;
    137
    138         /**
    139          * Update is made with high quality, e.g. graphs are evaluated at much more points.
    140          * @type Number
    141          * @constant
    142          * @see JXG.Board#updateQuality
    143          */
    144         this.BOARD_QUALITY_HIGH = 0x2;
    145
    146         /**
    147          * Pointer to the document element containing the board.
    148          * @type Object
    149          */
    150         // Former version:
    151         // this.document = attributes.document || document;
    152         if (Type.exists(attributes.document) && attributes.document !== false) {
    153             this.document = attributes.document;
    154         } else if (document !== undefined && Type.isObject(document)) {
    155             this.document = document;
    156         }
    157
    158         /**
    159          * The html-id of the html element containing the board.
    160          * @type String
    161          */
    162         this.container = container;
    163
    164         /**
    165          * Pointer to the html element containing the board.
    166          * @type Object
    167          */
    168         this.containerObj = (Env.isBrowser ? this.document.getElementById(this.container) : null);
    169
    170         if (Env.isBrowser && renderer.type !== 'no' && this.containerObj === null) {
    171             throw new Error("\nJSXGraph: HTML container element '" + container + "' not found.");
    172         }
    173
    174         /**
    175          * A reference to this boards renderer.
    176          * @type JXG.AbstractRenderer
    177          * @name JXG.Board#renderer
    178          * @private
    179          * @ignore
    180          */
    181         this.renderer = renderer;
    182
    183         /**
    184          * Grids keeps track of all grids attached to this board.
    185          * @type Array
    186          * @private
    187          */
    188         this.grids = [];
    189
    190         /**
    191          * Some standard options
    192          * @type JXG.Options
    193          */
    194         this.options = Type.deepCopy(Options);
    195         this.attr = attributes;
    196
    197         /**
    198          * Dimension of the board.
    199          * @default 2
    200          * @type Number
    201          */
    202         this.dimension = 2;
    203
    204         this.jc = new JessieCode();
    205         this.jc.use(this);
    206
    207         /**
    208          * Coordinates of the boards origin. This a object with the two properties
    209          * usrCoords and scrCoords. usrCoords always equals [1, 0, 0] and scrCoords
    210          * stores the boards origin in homogeneous screen coordinates.
    211          * @type Object
    212          * @private
    213          */
    214         this.origin = {};
    215         this.origin.usrCoords = [1, 0, 0];
    216         this.origin.scrCoords = [1, origin[0], origin[1]];
    217
    218         /**
    219          * Zoom factor in X direction. It only stores the zoom factor to be able
    220          * to get back to 100% in zoom100().
    221          * @name JXG.Board.zoomX
    222          * @type Number
    223          * @private
    224          * @ignore
    225          */
    226         this.zoomX = zoomX;
    227
    228         /**
    229          * Zoom factor in Y direction. It only stores the zoom factor to be able
    230          * to get back to 100% in zoom100().
    231          * @name JXG.Board.zoomY
    232          * @type Number
    233          * @private
    234          * @ignore
    235          */
    236         this.zoomY = zoomY;
    237
    238         /**
    239          * The number of pixels which represent one unit in user-coordinates in x direction.
    240          * @type Number
    241          * @private
    242          */
    243         this.unitX = unitX * this.zoomX;
    244
    245         /**
    246          * The number of pixels which represent one unit in user-coordinates in y direction.
    247          * @type Number
    248          * @private
    249          */
    250         this.unitY = unitY * this.zoomY;
    251
    252         /**
    253          * Keep aspect ratio if bounding box is set and the width/height ratio differs from the
    254          * width/height ratio of the canvas.
    255          * @type Boolean
    256          * @private
    257          */
    258         this.keepaspectratio = false;
    259
    260         /**
    261          * Canvas width.
    262          * @type Number
    263          * @private
    264          */
    265         this.canvasWidth = canvasWidth;
    266
    267         /**
    268          * Canvas Height
    269          * @type Number
    270          * @private
    271          */
    272         this.canvasHeight = canvasHeight;
    273
    274         // If the given id is not valid, generate an unique id
    275         if (Type.exists(id) && id !== '' && Env.isBrowser && !Type.exists(this.document.getElementById(id))) {
    276             this.id = id;
    277         } else {
    278             this.id = this.generateId();
    279         }
    280
    281         EventEmitter.eventify(this);
    282
    283         this.hooks = [];
    284
    285         /**
    286          * An array containing all other boards that are updated after this board has been updated.
    287          * @type Array
    288          * @see JXG.Board#addChild
    289          * @see JXG.Board#removeChild
    290          */
    291         this.dependentBoards = [];
    292
    293         /**
    294          * During the update process this is set to false to prevent an endless loop.
    295          * @default false
    296          * @type Boolean
    297          */
    298         this.inUpdate = false;
    299
    300         /**
    301          * An associative array containing all geometric objects belonging to the board. Key is the id of the object and value is a reference to the object.
    302          * @type Object
    303          */
    304         this.objects = {};
    305
    306         /**
    307          * An array containing all geometric objects on the board in the order of construction.
    308          * @type Array
    309          */
    310         this.objectsList = [];
    311
    312         /**
    313          * An associative array containing all groups belonging to the board. Key is the id of the group and value is a reference to the object.
    314          * @type Object
    315          */
    316         this.groups = {};
    317
    318         /**
    319          * Stores all the objects that are currently running an animation.
    320          * @type Object
    321          */
    322         this.animationObjects = {};
    323
    324         /**
    325          * An associative array containing all highlighted elements belonging to the board.
    326          * @type Object
    327          */
    328         this.highlightedObjects = {};
    329
    330         /**
    331          * Number of objects ever created on this board. This includes every object, even invisible and deleted ones.
    332          * @type Number
    333          */
    334         this.numObjects = 0;
    335
    336         /**
    337          * An associative array to store the objects of the board by name. the name of the object is the key and value is a reference to the object.
    338          * @type Object
    339          */
    340         this.elementsByName = {};
    341
    342         /**
    343          * The board mode the board is currently in. Possible values are
    344          * <ul>
    345          * <li>JXG.Board.BOARD_MODE_NONE</li>
    346          * <li>JXG.Board.BOARD_MODE_DRAG</li>
    347          * <li>JXG.Board.BOARD_MODE_MOVE_ORIGIN</li>
    348          * </ul>
    349          * @type Number
    350          */
    351         this.mode = this.BOARD_MODE_NONE;
    352
    353         /**
    354          * The update quality of the board. In most cases this is set to {@link JXG.Board#BOARD_QUALITY_HIGH}.
    355          * If {@link JXG.Board#mode} equals {@link JXG.Board#BOARD_MODE_DRAG} this is set to
    356          * {@link JXG.Board#BOARD_QUALITY_LOW} to speed up the update process by e.g. reducing the number of
    357          * evaluation points when plotting functions. Possible values are
    358          * <ul>
    359          * <li>BOARD_QUALITY_LOW</li>
    360          * <li>BOARD_QUALITY_HIGH</li>
    361          * </ul>
    362          * @type Number
    363          * @see JXG.Board#mode
    364          */
    365         this.updateQuality = this.BOARD_QUALITY_HIGH;
    366
    367         /**
    368          * If true updates are skipped.
    369          * @type Boolean
    370          */
    371         this.isSuspendedRedraw = false;
    372
    373         this.calculateSnapSizes();
    374
    375         /**
    376          * The distance from the mouse to the dragged object in x direction when the user clicked the mouse button.
    377          * @type Number
    378          * @see JXG.Board#drag_dy
    379          * @see JXG.Board#drag_obj
    380          */
    381         this.drag_dx = 0;
    382
    383         /**
    384          * The distance from the mouse to the dragged object in y direction when the user clicked the mouse button.
    385          * @type Number
    386          * @see JXG.Board#drag_dx
    387          * @see JXG.Board#drag_obj
    388          */
    389         this.drag_dy = 0;
    390
    391         /**
    392          * The last position where a drag event has been fired.
    393          * @type Array
    394          * @see JXG.Board#moveObject
    395          */
    396         this.drag_position = [0, 0];
    397
    398         /**
    399          * References to the object that is dragged with the mouse on the board.
    400          * @type JXG.GeometryElement
    401          * @see JXG.Board#touches
    402          */
    403         this.mouse = {};
    404
    405         /**
    406          * Keeps track on touched elements, like {@link JXG.Board#mouse} does for mouse events.
    407          * @type Array
    408          * @see JXG.Board#mouse
    409          */
    410         this.touches = [];
    411
    412         /**
    413          * A string containing the XML text of the construction.
    414          * This is set in {@link JXG.FileReader.parseString}.
    415          * Only useful if a construction is read from a GEONExT-, Intergeo-, Geogebra-, or Cinderella-File.
    416          * @type String
    417          */
    418         this.xmlString = '';
    419
    420         /**
    421          * Cached result of getCoordsTopLeftCorner for touch/mouseMove-Events to save some DOM operations.
    422          * @type Array
    423          */
    424         this.cPos = [];
    425
    426         /**
    427          * Contains the last time (epoch, msec) since the last touchMove event which was not thrown away or since
    428          * touchStart because Android's Webkit browser fires too much of them.
    429          * @type Number
    430          */
    431         this.touchMoveLast = 0;
    432
    433         /**
    434          * Contains the pointerId of the last touchMove event which was not thrown away or since
    435          * touchStart because Android's Webkit browser fires too much of them.
    436          * @type Number
    437          */
    438          this.touchMoveLastId = Infinity;
    439
    440         /**
    441          * Contains the last time (epoch, msec) since the last getCoordsTopLeftCorner call which was not thrown away.
    442          * @type Number
    443          */
    444         this.positionAccessLast = 0;
    445
    446         /**
    447          * Collects all elements that triggered a mouse down event.
    448          * @type Array
    449          */
    450         this.downObjects = [];
    451
    452         if (this.attr.showcopyright) {
    453             this.renderer.displayCopyright(Const.licenseText, parseInt(this.options.text.fontSize, 10));
    454         }
    455
    456         /**
    457          * Full updates are needed after zoom and axis translates. This saves some time during an update.
    458          * @default false
    459          * @type Boolean
    460          */
    461         this.needsFullUpdate = false;
    462
    463         /**
    464          * If reducedUpdate is set to true then only the dragged element and few (e.g. 2) following
    465          * elements are updated during mouse move. On mouse up the whole construction is
    466          * updated. This enables us to be fast even on very slow devices.
    467          * @type Boolean
    468          * @default false
    469          */
    470         this.reducedUpdate = false;
    471
    472         /**
    473          * The current color blindness deficiency is stored in this property. If color blindness is not emulated
    474          * at the moment, it's value is 'none'.
    475          */
    476         this.currentCBDef = 'none';
    477
    478         /**
    479          * If GEONExT constructions are displayed, then this property should be set to true.
    480          * At the moment there should be no difference. But this may change.
    481          * This is set in {@link JXG.GeonextReader.readGeonext}.
    482          * @type Boolean
    483          * @default false
    484          * @see JXG.GeonextReader.readGeonext
    485          */
    486         this.geonextCompatibilityMode = false;
    487
    488         if (this.options.text.useASCIIMathML && translateASCIIMath) {
    489             init();
    490         } else {
    491             this.options.text.useASCIIMathML = false;
    492         }
    493
    494         /**
    495          * A flag which tells if the board registers mouse events.
    496          * @type Boolean
    497          * @default false
    498          */
    499         this.hasMouseHandlers = false;
    500
    501         /**
    502          * A flag which tells if the board registers touch events.
    503          * @type Boolean
    504          * @default false
    505          */
    506         this.hasTouchHandlers = false;
    507
    508         /**
    509          * A flag which stores if the board registered pointer events.
    510          * @type Boolean
    511          * @default false
    512          */
    513         this.hasPointerHandlers = false;
    514
    515         /**
    516          * A flag which tells if the board the JXG.Board#mouseUpListener is currently registered.
    517          * @type Boolean
    518          * @default false
    519          */
    520         this.hasMouseUp = false;
    521
    522         /**
    523          * A flag which tells if the board the JXG.Board#touchEndListener is currently registered.
    524          * @type Boolean
    525          * @default false
    526          */
    527         this.hasTouchEnd = false;
    528
    529         /**
    530          * A flag which tells us if the board has a pointerUp event registered at the moment.
    531          * @type Boolean
    532          * @default false
    533          */
    534         this.hasPointerUp = false;
    535
    536         /**
    537          * Offset for large coords elements like images
    538          * @type Array
    539          * @private
    540          * @default [0, 0]
    541          */
    542         this._drag_offset = [0, 0];
    543
    544         /**
    545          * Stores the input device used in the last down or move event.
    546          * @type String
    547          * @private
    548          * @default 'mouse'
    549          */
    550         this._inputDevice = 'mouse';
    551
    552         /**
    553          * Keeps a list of pointer devices which are currently touching the screen.
    554          * @type Array
    555          * @private
    556          */
    557         this._board_touches = [];
    558
    559         /**
    560          * A flag which tells us if the board is in the selecting mode
    561          * @type Boolean
    562          * @default false
    563          */
    564         this.selectingMode = false;
    565
    566         /**
    567          * A flag which tells us if the user is selecting
    568          * @type Boolean
    569          * @default false
    570          */
    571         this.isSelecting = false;
    572
    573         /**
    574          * A flag which tells us if the user is scrolling the viewport
    575          * @type Boolean
    576          * @private
    577          * @default false
    578          * @see JXG.Board#scrollListener
    579          */
    580         this._isScrolling = false;
    581
    582         /**
    583          * A flag which tells us if a resize is in process
    584          * @type Boolean
    585          * @private
    586          * @default false
    587          * @see JXG.Board#resizeListener
    588          */
    589         this._isResizing = false;
    590
    591         /**
    592          * A bounding box for the selection
    593          * @type Array
    594          * @default [ [0,0], [0,0] ]
    595          */
    596         this.selectingBox = [[0, 0], [0, 0]];
    597
    598         this.mathLib = Math;        // Math or JXG.Math.IntervalArithmetic
    599         this.mathLibJXG = JXG.Math; // JXG.Math or JXG.Math.IntervalArithmetic
    600
    601         if (this.attr.registerevents) {
    602             this.addEventHandlers();
    603         }
    604
    605         this.methodMap = {
    606             update: 'update',
    607             fullUpdate: 'fullUpdate',
    608             on: 'on',
    609             off: 'off',
    610             trigger: 'trigger',
    611             setView: 'setBoundingBox',
    612             setBoundingBox: 'setBoundingBox',
    613             migratePoint: 'migratePoint',
    614             colorblind: 'emulateColorblindness',
    615             suspendUpdate: 'suspendUpdate',
    616             unsuspendUpdate: 'unsuspendUpdate',
    617             clearTraces: 'clearTraces',
    618             left: 'clickLeftArrow',
    619             right: 'clickRightArrow',
    620             up: 'clickUpArrow',
    621             down: 'clickDownArrow',
    622             zoomIn: 'zoomIn',
    623             zoomOut: 'zoomOut',
    624             zoom100: 'zoom100',
    625             zoomElements: 'zoomElements',
    626             remove: 'removeObject',
    627             removeObject: 'removeObject'
    628         };
    629     };
    630
    631     JXG.extend(JXG.Board.prototype, /** @lends JXG.Board.prototype */ {
    632
    633         /**
    634          * Generates an unique name for the given object. The result depends on the objects type, if the
    635          * object is a {@link JXG.Point}, capital characters are used, if it is of type {@link JXG.Line}
    636          * only lower case characters are used. If object is of type {@link JXG.Polygon}, a bunch of lower
    637          * case characters prefixed with P_ are used. If object is of type {@link JXG.Circle} the name is
    638          * generated using lower case characters. prefixed with k_ is used. In any other case, lower case
    639          * chars prefixed with s_ is used.
    640          * @param {Object} object Reference of an JXG.GeometryElement that is to be named.
    641          * @returns {String} Unique name for the object.
    642          */
    643         generateName: function (object) {
    644             var possibleNames, i,
    645                 maxNameLength = this.attr.maxnamelength,
    646                 pre = '',
    647                 post = '',
    648                 indices = [],
    649                 name = '';
    650
    651             if (object.type === Const.OBJECT_TYPE_TICKS) {
    652                 return '';
    653             }
    654
    655             if (Type.isPoint(object)) {
    656                 // points have capital letters
    657                 possibleNames = ['', 'A', 'B', 'C', 'D', 'E', 'F', 'G', 'H', 'I', 'J', 'K', 'L', 'M', 'N', 'O',
    658                     'P', 'Q', 'R', 'S', 'T', 'U', 'V', 'W', 'X', 'Y', 'Z'];
    659             } else if (object.type === Const.OBJECT_TYPE_ANGLE) {
    660                 possibleNames = ['', 'α', 'β', 'γ', 'δ', 'ε', 'ζ', 'η', 'θ',
    661                     'ι', 'κ', 'λ', 'μ', 'ν', 'ξ', 'ο', 'π', 'ρ',
    662                     'σ', 'τ', 'υ', 'φ', 'χ', 'ψ', 'ω'];
    663             } else {
    664                 // all other elements get lowercase labels
    665                 possibleNames = ['', 'a', 'b', 'c', 'd', 'e', 'f', 'g', 'h', 'i', 'j', 'k', 'l', 'm', 'n', 'o',
    666                     'p', 'q', 'r', 's', 't', 'u', 'v', 'w', 'x', 'y', 'z'];
    667             }
    668
    669             if (!Type.isPoint(object) &&
    670                     object.elementClass !== Const.OBJECT_CLASS_LINE &&
    671                     object.type !== Const.OBJECT_TYPE_ANGLE) {
    672                 if (object.type === Const.OBJECT_TYPE_POLYGON) {
    673                     pre = 'P_{';
    674                 } else if (object.elementClass === Const.OBJECT_CLASS_CIRCLE) {
    675                     pre = 'k_{';
    676                 } else if (object.elementClass === Const.OBJECT_CLASS_TEXT) {
    677                     pre = 't_{';
    678                 } else {
    679                     pre = 's_{';
    680                 }
    681                 post = '}';
    682             }
    683
    684             for (i = 0; i < maxNameLength; i++) {
    685                 indices[i] = 0;
    686             }
    687
    688             while (indices[maxNameLength - 1] < possibleNames.length) {
    689                 for (indices[0] = 1; indices[0] < possibleNames.length; indices[0]++) {
    690                     name = pre;
    691
    692                     for (i = maxNameLength; i > 0; i--) {
    693                         name += possibleNames[indices[i - 1]];
    694                     }
    695
    696                     if (!Type.exists(this.elementsByName[name + post])) {
    697                         return name + post;
    698                     }
    699
    700                 }
    701                 indices[0] = possibleNames.length;
    702
    703                 for (i = 1; i < maxNameLength; i++) {
    704                     if (indices[i - 1] === possibleNames.length) {
    705                         indices[i - 1] = 1;
    706                         indices[i] += 1;
    707                     }
    708                 }
    709             }
    710
    711             return '';
    712         },
    713
    714         /**
    715          * Generates unique id for a board. The result is randomly generated and prefixed with 'jxgBoard'.
    716          * @returns {String} Unique id for a board.
    717          */
    718         generateId: function () {
    719             var r = 1;
    720
    721             // as long as we don't have a unique id generate a new one
    722             while (Type.exists(JXG.boards['jxgBoard' + r])) {
    723                 r = Math.round(Math.random() * 65535);
    724             }
    725
    726             return ('jxgBoard' + r);
    727         },
    728
    729         /**
    730          * Composes an id for an element. If the ID is empty ('' or null) a new ID is generated, depending on the
    731          * object type. As a side effect {@link JXG.Board#numObjects}
    732          * is updated.
    733          * @param {Object} obj Reference of an geometry object that needs an id.
    734          * @param {Number} type Type of the object.
    735          * @returns {String} Unique id for an element.
    736          */
    737         setId: function (obj, type) {
    738             var randomNumber,
    739                 num = this.numObjects,
    740                 elId = obj.id;
    741
    742             this.numObjects += 1;
    743
    744             // If no id is provided or id is empty string, a new one is chosen
    745             if (elId === '' || !Type.exists(elId)) {
    746                 elId = this.id + type + num;
    747                 while (Type.exists(this.objects[elId])) {
    748                     randomNumber = Math.round(Math.random() * 65535);
    749                     elId = this.id + type + num + '-' + randomNumber;
    750                 }
    751             }
    752
    753             obj.id = elId;
    754             this.objects[elId] = obj;
    755             obj._pos = this.objectsList.length;
    756             this.objectsList[this.objectsList.length] = obj;
    757
    758             return elId;
    759         },
    760
    761         /**
    762          * After construction of the object the visibility is set
    763          * and the label is constructed if necessary.
    764          * @param {Object} obj The object to add.
    765          */
    766         finalizeAdding: function (obj) {
    767             if (Type.evaluate(obj.visProp.visible) === false) {
    768                 this.renderer.display(obj, false);
    769             }
    770         },
    771
    772         finalizeLabel: function (obj) {
    773             if (obj.hasLabel &&
    774                 !Type.evaluate(obj.label.visProp.islabel) &&
    775                 Type.evaluate(obj.label.visProp.visible) === false) {
    776                 this.renderer.display(obj.label, false);
    777             }
    778         },
    779
    780         /**********************************************************
    781          *
    782          * Event Handler helpers
    783          *
    784          **********************************************************/
    785
    786         /**
    787          * Returns false if the event has been triggered faster than the maximum frame rate.
    788          *
    789          * @param {Event} evt Event object given by the browser (unused)
    790          * @returns {Boolean} If the event has been triggered faster than the maximum frame rate, false is returned.
    791          * @private
    792          * @see JXG.Board#pointerMoveListener
    793          * @see JXG.Board#touchMoveListener
    794          * @see JXG.Board#mouseMoveListener
    795          */
    796         checkFrameRate: function(evt) {
    797             var handleEvt = false,
    798                 time = new Date().getTime();
    799
    800             if (Type.exists(evt.pointerId) && this.touchMoveLastId !== evt.pointerId) {
    801                 handleEvt = true;
    802                 this.touchMoveLastId = evt.pointerId;
    803             }
    804             if (!handleEvt && (time - this.touchMoveLast) * this.attr.maxframerate >= 1000) {
    805                 handleEvt = true;
    806             }
    807             if (handleEvt) {
    808                 this.touchMoveLast = time;
    809             }
    810             return handleEvt;
    811         },
    812
    813         /**
    814          * Calculates mouse coordinates relative to the boards container.
    815          * @returns {Array} Array of coordinates relative the boards container top left corner.
    816          */
    817         getCoordsTopLeftCorner: function () {
    818             var cPos, doc, crect,
    819                 docElement = this.document.documentElement || this.document.body.parentNode,
    820                 docBody = this.document.body,
    821                 container = this.containerObj,
    822                 // viewport, content,
    823                 zoom, o;
    824
    825             /**
    826              * During drags and origin moves the container element is usually not changed.
    827              * Check the position of the upper left corner at most every 1000 msecs
    828              */
    829             if (this.cPos.length > 0 &&
    830                     (this.mode === this.BOARD_MODE_DRAG || this.mode === this.BOARD_MODE_MOVE_ORIGIN ||
    831                     (new Date()).getTime() - this.positionAccessLast < 1000)) {
    832                 return this.cPos;
    833             }
    834             this.positionAccessLast = (new Date()).getTime();
    835
    836             // Check if getBoundingClientRect exists. If so, use this as this covers *everything*
    837             // even CSS3D transformations etc.
    838             // Supported by all browsers but IE 6, 7.
    839
    840             if (container.getBoundingClientRect) {
    841                 crect = container.getBoundingClientRect();
    842
    843
    844                 zoom = 1.0;
    845                 // Recursively search for zoom style entries.
    846                 // This is necessary for reveal.js on webkit.
    847                 // It fails if the user does zooming
    848                 o = container;
    849                 while (o && Type.exists(o.parentNode)) {
    850                     if (Type.exists(o.style) && Type.exists(o.style.zoom) && o.style.zoom !== '') {
    851                         zoom *= parseFloat(o.style.zoom);
    852                     }
    853                     o = o.parentNode;
    854                 }
    855                 cPos = [crect.left * zoom, crect.top * zoom];
    856
    857                 // add border width
    858                 cPos[0] += Env.getProp(container, 'border-left-width');
    859                 cPos[1] += Env.getProp(container, 'border-top-width');
    860
    861                 // vml seems to ignore paddings
    862                 if (this.renderer.type !== 'vml') {
    863                     // add padding
    864                     cPos[0] += Env.getProp(container, 'padding-left');
    865                     cPos[1] += Env.getProp(container, 'padding-top');
    866                 }
    867
    868                 this.cPos = cPos.slice();
    869                 return this.cPos;
    870             }
    871
    872             //
    873             //  OLD CODE
    874             //  IE 6-7 only:
    875             //
    876             cPos = Env.getOffset(container);
    877             doc = this.document.documentElement.ownerDocument;
    878
    879             if (!this.containerObj.currentStyle && doc.defaultView) {     // Non IE
    880                 // this is for hacks like this one used in wordpress for the admin bar:
    881                 // html { margin-top: 28px }
    882                 // seems like it doesn't work in IE
    883
    884                 cPos[0] += Env.getProp(docElement, 'margin-left');
    885                 cPos[1] += Env.getProp(docElement, 'margin-top');
    886
    887                 cPos[0] += Env.getProp(docElement, 'border-left-width');
    888                 cPos[1] += Env.getProp(docElement, 'border-top-width');
    889
    890                 cPos[0] += Env.getProp(docElement, 'padding-left');
    891                 cPos[1] += Env.getProp(docElement, 'padding-top');
    892             }
    893
    894             if (docBody) {
    895                 cPos[0] += Env.getProp(docBody, 'left');
    896                 cPos[1] += Env.getProp(docBody, 'top');
    897             }
    898
    899             // Google Translate offers widgets for web authors. These widgets apparently tamper with the clientX
    900             // and clientY coordinates of the mouse events. The minified sources seem to be the only publicly
    901             // available version so we're doing it the hacky way: Add a fixed offset.

    904                 cPos[0] += 10;
    905                 cPos[1] += 25;
    906             }
    907
    908             // add border width
    909             cPos[0] += Env.getProp(container, 'border-left-width');
    910             cPos[1] += Env.getProp(container, 'border-top-width');
    911
    912             // vml seems to ignore paddings
    913             if (this.renderer.type !== 'vml') {
    914                 // add padding
    915                 cPos[0] += Env.getProp(container, 'padding-left');
    916                 cPos[1] += Env.getProp(container, 'padding-top');
    917             }
    918
    919             cPos[0] += this.attr.offsetx;
    920             cPos[1] += this.attr.offsety;
    921
    922             this.cPos = cPos.slice();
    923             return this.cPos;
    924         },
    925
    926         /**
    927          * Get the position of the mouse in screen coordinates, relative to the upper left corner
    928          * of the host tag.
    929          * @param {Event} e Event object given by the browser.
    930          * @param {Number} [i] Only use in case of touch events. This determines which finger to use and should not be set
    931          * for mouseevents.
    932          * @returns {Array} Contains the mouse coordinates in screen coordinates, ready for {@link JXG.Coords}
    933          */
    934         getMousePosition: function (e, i) {
    935             var cPos = this.getCoordsTopLeftCorner(),
    936                 absPos,
    937                 v;
    938
    939             // Position of cursor using clientX/Y
    940             absPos = Env.getPosition(e, i, this.document);
    941
    942             /**
    943              * In case there has been no down event before.
    944              */
    945             if (!Type.exists(this.cssTransMat)) {
    946                 this.updateCSSTransforms();
    947             }
    948             // Position relative to the top left corner
    949             v = [1, absPos[0] - cPos[0], absPos[1] - cPos[1]];
    950             v = Mat.matVecMult(this.cssTransMat, v);
    951             v[1] /= v[0];
    952             v[2] /= v[0];
    953             return [v[1], v[2]];
    954
    955             // Method without CSS transformation
    956             /*
    957              return [absPos[0] - cPos[0], absPos[1] - cPos[1]];
    958              */
    959         },
    960
    961         /**
    962          * Initiate moving the origin. This is used in mouseDown and touchStart listeners.
    963          * @param {Number} x Current mouse/touch coordinates
    964          * @param {Number} y Current mouse/touch coordinates
    965          */
    966         initMoveOrigin: function (x, y) {
    967             this.drag_dx = x - this.origin.scrCoords[1];
    968             this.drag_dy = y - this.origin.scrCoords[2];
    969
    970             this.mode = this.BOARD_MODE_MOVE_ORIGIN;
    971             this.updateQuality = this.BOARD_QUALITY_LOW;
    972         },
    973
    974         /**
    975          * Collects all elements below the current mouse pointer and fulfilling the following constraints:
    976          * <ul><li>isDraggable</li><li>visible</li><li>not fixed</li><li>not frozen</li></ul>
    977          * @param {Number} x Current mouse/touch coordinates
    978          * @param {Number} y current mouse/touch coordinates
    979          * @param {Object} evt An event object
    980          * @param {String} type What type of event? 'touch', 'mouse' or 'pen'.
    981          * @returns {Array} A list of geometric elements.
    982          */
    983         initMoveObject: function (x, y, evt, type) {
    984             var pEl,
    985                 el,
    986                 collect = [],
    987                 offset = [],
    988                 haspoint,
    989                 len = this.objectsList.length,
    990                 dragEl = {visProp: {layer: -10000}};
    991
    992             //for (el in this.objects) {
    993             for (el = 0; el < len; el++) {
    994                 pEl = this.objectsList[el];
    995                 haspoint = pEl.hasPoint && pEl.hasPoint(x, y);
    996
    997                 if (pEl.visPropCalc.visible && haspoint) {
    998                     pEl.triggerEventHandlers([type + 'down', 'down'], [evt]);
    999                     this.downObjects.push(pEl);
    1000                 }
    1001
    1002                 if (haspoint &&
    1003                     pEl.isDraggable &&
    1004                     pEl.visPropCalc.visible &&
    1005                     ((this.geonextCompatibilityMode &&
    1006                         (Type.isPoint(pEl) ||
    1007                          pEl.elementClass === Const.OBJECT_CLASS_TEXT)
    1008                      ) ||
    1009                      !this.geonextCompatibilityMode
    1010                     ) &&
    1011                     !Type.evaluate(pEl.visProp.fixed)
    1012                     /*(!pEl.visProp.frozen) &&*/
    1013                     ) {
    1014
    1015                     // Elements in the highest layer get priority.
    1016                     if (pEl.visProp.layer > dragEl.visProp.layer ||
    1017                             (pEl.visProp.layer === dragEl.visProp.layer &&
    1018                              pEl.lastDragTime.getTime() >= dragEl.lastDragTime.getTime()
    1019                             )) {
    1020                         // If an element and its label have the focus
    1021                         // simultaneously, the element is taken.
    1022                         // This only works if we assume that every browser runs
    1023                         // through this.objects in the right order, i.e. an element A
    1024                         // added before element B turns up here before B does.
    1025                         if (!this.attr.ignorelabels ||
    1026                             (!Type.exists(dragEl.label) || pEl !== dragEl.label)) {
    1027                             dragEl = pEl;
    1028                             collect.push(dragEl);
    1029
    1030                             // Save offset for large coords elements.
    1031                             if (Type.exists(dragEl.coords)) {
    1032                                 offset.push(Statistics.subtract(dragEl.coords.scrCoords.slice(1), [x, y]));
    1033                             } else {
    1034                                 offset.push([0, 0]);
    1035                             }
    1036
    1037                             // we can't drop out of this loop because of the event handling system
    1038                             //if (this.attr.takefirst) {
    1039                             //    return collect;
    1040                             //}
    1041                         }
    1042                     }
    1043                 }
    1044             }
    1045
    1046             if (this.attr.drag.enabled && collect.length > 0) {
    1047                 this.mode = this.BOARD_MODE_DRAG;
    1048             }
    1049
    1050             // A one-element array is returned.
    1051             if (this.attr.takefirst) {
    1052                 collect.length = 1;
    1053                 this._drag_offset = offset[0];
    1054             } else {
    1055                 collect = collect.slice(-1);
    1056                 this._drag_offset = offset[offset.length - 1];
    1057             }
    1058
    1059             if (!this._drag_offset) {
    1060                 this._drag_offset = [0, 0];
    1061             }
    1062
    1063             // Move drag element to the top of the layer
    1064             if (this.renderer.type === 'svg' &&
    1065                 Type.exists(collect[0]) &&
    1066                 Type.evaluate(collect[0].visProp.dragtotopoflayer) &&
    1067                 collect.length === 1 &&
    1068                 Type.exists(collect[0].rendNode)) {
    1069
    1070                 collect[0].rendNode.parentNode.appendChild(collect[0].rendNode);
    1071             }
    1072
    1073             // Init rotation angle and scale factor for two finger movements
    1074             this.previousRotation = 0.0;
    1075             this.previousScale = 1.0;
    1076
    1077             if (collect.length >= 1) {
    1078                 collect[0].highlight(true);
    1079                 this.triggerEventHandlers(['mousehit', 'hit'], [evt, collect[0]]);
    1080             }
    1081
    1082             return collect;
    1083         },
    1084
    1085         /**
    1086          * Moves an object.
    1087          * @param {Number} x Coordinate
    1088          * @param {Number} y Coordinate
    1089          * @param {Object} o The touch object that is dragged: {JXG.Board#mouse} or {JXG.Board#touches}.
    1090          * @param {Object} evt The event object.
    1091          * @param {String} type Mouse or touch event?
    1092          */
    1093         moveObject: function (x, y, o, evt, type) {
    1094             var newPos = new Coords(Const.COORDS_BY_SCREEN, this.getScrCoordsOfMouse(x, y), this),
    1095                 drag,
    1096                 dragScrCoords, newDragScrCoords;
    1097
    1098             if (!(o && o.obj)) {
    1099                 return;
    1100             }
    1101             drag = o.obj;
    1102
    1103             // Save updates for very small movements of coordsElements, see below
    1104             if (drag.coords) {
    1105                 dragScrCoords = drag.coords.scrCoords.slice();
    1106             }
    1107
    1108             /*
    1109              * Save the position.
    1110              */
    1111             this.drag_position = [newPos.scrCoords[1], newPos.scrCoords[2]];
    1112             this.drag_position = Statistics.add(this.drag_position, this._drag_offset);
    1113             //
    1114             // We have to distinguish between CoordsElements and other elements like lines.
    1115             // The latter need the difference between two move events.
    1116             if (Type.exists(drag.coords)) {
    1117                 drag.setPositionDirectly(Const.COORDS_BY_SCREEN, this.drag_position);
    1118             } else {
    1119                 this.displayInfobox(false);
    1120                                     // Hide infobox in case the user has touched an intersection point
    1121                                     // and drags the underlying line now.
    1122
    1123                 if (!isNaN(o.targets[0].Xprev + o.targets[0].Yprev)) {
    1124                     drag.setPositionDirectly(Const.COORDS_BY_SCREEN,
    1125                         [newPos.scrCoords[1], newPos.scrCoords[2]],
    1126                         [o.targets[0].Xprev, o.targets[0].Yprev]
    1127                         );
    1128                 }
    1129                 // Remember the actual position for the next move event. Then we are able to
    1130                 // compute the difference vector.
    1131                 o.targets[0].Xprev = newPos.scrCoords[1];
    1132                 o.targets[0].Yprev = newPos.scrCoords[2];
    1133             }
    1134             // This may be necessary for some gliders and labels
    1135             if (Type.exists(drag.coords)) {
    1136                 drag.prepareUpdate().update(false).updateRenderer();
    1137                 this.updateInfobox(drag);
    1138                 drag.prepareUpdate().update(true).updateRenderer();
    1139             }
    1140
    1141             if (drag.coords) {
    1142                 newDragScrCoords = drag.coords.scrCoords;
    1143             }
    1144             // No updates for very small movements of coordsElements
    1145             if (!drag.coords ||
    1146                 dragScrCoords[1] !== newDragScrCoords[1] ||
    1147                 dragScrCoords[2] !== newDragScrCoords[2]) {
    1148
    1149                 drag.triggerEventHandlers([type + 'drag', 'drag'], [evt]);
    1150
    1151                 this.update();
    1152             }
    1153             drag.highlight(true);
    1154             this.triggerEventHandlers(['mousehit', 'hit'], [evt, drag]);
    1155
    1156             drag.lastDragTime = new Date();
    1157         },
    1158
    1159         /**
    1160          * Moves elements in multitouch mode.
    1161          * @param {Array} p1 x,y coordinates of first touch
    1162          * @param {Array} p2 x,y coordinates of second touch
    1163          * @param {Object} o The touch object that is dragged: {JXG.Board#touches}.
    1164          * @param {Object} evt The event object that lead to this movement.
    1165          */
    1166         twoFingerMove: function (o, id, evt) {
    1167             var drag;
    1168
    1169             if (Type.exists(o) && Type.exists(o.obj)) {
    1170                 drag = o.obj;
    1171             } else {
    1172                 return;
    1173             }
    1174
    1175             if (drag.elementClass === Const.OBJECT_CLASS_LINE ||
    1176                 drag.type === Const.OBJECT_TYPE_POLYGON) {
    1177                 this.twoFingerTouchObject(o.targets, drag, id);
    1178             } else if (drag.elementClass === Const.OBJECT_CLASS_CIRCLE) {
    1179                 this.twoFingerTouchCircle(o.targets, drag, id);
    1180             }
    1181
    1182             if (evt) {
    1183                 drag.triggerEventHandlers(['touchdrag', 'drag'], [evt]);
    1184             }
    1185         },
    1186
    1187         /**
    1188          * Moves, rotates and scales a line or polygon with two fingers.
    1189          * @param {Array} tar Array conatining touch event objects: {JXG.Board#touches.targets}.
    1190          * @param {object} drag The object that is dragged:
    1191          * @param {Number} id pointerId of the event. In case of old touch event this is emulated.
    1192          */
    1193         twoFingerTouchObject: function (tar, drag, id) {
    1194             var np, op, nd, od,
    1195                 d, alpha,
    1196                 S, t1, t3, t4, t5,
    1197                 ar, i, len,
    1198                 fixEl, moveEl, fix;
    1199
    1200             if (Type.exists(tar[0]) && Type.exists(tar[1]) &&
    1201                 !isNaN(tar[0].Xprev + tar[0].Yprev + tar[1].Xprev + tar[1].Yprev)) {
    1202
    1203                 if (id === tar[0].num) {
    1204                     fixEl  = tar[1];
    1205                     moveEl = tar[0];
    1206                 } else {
    1207                     fixEl  = tar[0];
    1208                     moveEl = tar[1];
    1209                 }
    1210
    1211                 fix = (new Coords(Const.COORDS_BY_SCREEN, [fixEl.Xprev, fixEl.Yprev], this)).usrCoords;
    1212                 // Previous finger position
    1213                 op = (new Coords(Const.COORDS_BY_SCREEN, [moveEl.Xprev, moveEl.Yprev], this)).usrCoords;
    1214                 // New finger position
    1215                 np = (new Coords(Const.COORDS_BY_SCREEN, [moveEl.X, moveEl.Y], this)).usrCoords;
    1216
    1217                 // Old and new directions
    1218                 od = Mat.crossProduct(fix, op);
    1219                 nd = Mat.crossProduct(fix, np);
    1220
    1221                 // Intersection between the two directions
    1222                 S = Mat.crossProduct(od, nd);
    1223
    1224                 // If parallel translate, otherwise rotate
    1225                 if (Math.abs(S[0]) < Mat.eps) {
    1226                     return;
    1227                 }
    1228
    1229                 alpha = Geometry.rad(op.slice(1), fix.slice(1), np.slice(1));
    1230
    1231                 t1 = this.create('transform', [alpha, [fix[1], fix[2]]], {type: 'rotate'});
    1232                 t1.update();
    1233
    1234                 if (Type.evaluate(drag.visProp.scalable)) {
    1235                     // Scale
    1236                     d = Geometry.distance(np, fix) / Geometry.distance(op, fix);
    1237
    1238                     t3 = this.create('transform', [-fix[1], -fix[2]], {type: 'translate'});
    1239                     t4 = this.create('transform', [d, d], {type: 'scale'});
    1240                     t5 = this.create('transform', [fix[1], fix[2]], {type: 'translate'});
    1241                     t1.melt(t3).melt(t4).melt(t5);
    1242                 }
    1243
    1244                 if (drag.elementClass === Const.OBJECT_CLASS_LINE) {
    1245                     ar = [];
    1246                     if (drag.point1.draggable()) {
    1247                         ar.push(drag.point1);
    1248                     }
    1249                     if (drag.point2.draggable()) {
    1250                         ar.push(drag.point2);
    1251                     }
    1252                     t1.applyOnce(ar);
    1253                 } else if (drag.type === Const.OBJECT_TYPE_POLYGON) {
    1254                     ar = [];
    1255                     len = drag.vertices.length - 1;
    1256                     for (i = 0; i < len; ++i) {
    1257                         if (drag.vertices[i].draggable()) {
    1258                             ar.push(drag.vertices[i]);
    1259                         }
    1260                     }
    1261                     t1.applyOnce(ar);
    1262                 }
    1263
    1264                 this.update();
    1265                 drag.highlight(true);
    1266             }
    1267         },
    1268
    1269         /*
    1270          * Moves, rotates and scales a circle with two fingers.
    1271          * @param {Array} tar Array conatining touch event objects: {JXG.Board#touches.targets}.
    1272          * @param {object} drag The object that is dragged:
    1273          * @param {Number} id pointerId of the event. In case of old touch event this is emulated.
    1274          */
    1275         twoFingerTouchCircle: function (tar, drag, id) {
    1276             var fixEl, moveEl, np, op, fix,
    1277                 d, alpha, t1, t2, t3, t4;
    1278
    1279             if (drag.method === 'pointCircle' || drag.method === 'pointLine') {
    1280                 return;
    1281             }
    1282
    1283             if (Type.exists(tar[0]) && Type.exists(tar[1]) &&
    1284                 !isNaN(tar[0].Xprev + tar[0].Yprev + tar[1].Xprev + tar[1].Yprev)) {
    1285
    1286                 if (id === tar[0].num) {
    1287                     fixEl  = tar[1];
    1288                     moveEl = tar[0];
    1289                 } else {
    1290                     fixEl  = tar[0];
    1291                     moveEl = tar[1];
    1292                 }
    1293
    1294                 fix = (new Coords(Const.COORDS_BY_SCREEN, [fixEl.Xprev, fixEl.Yprev], this)).usrCoords;
    1295                 // Previous finger position
    1296                 op = (new Coords(Const.COORDS_BY_SCREEN, [moveEl.Xprev, moveEl.Yprev], this)).usrCoords;
    1297                 // New finger position
    1298                 np = (new Coords(Const.COORDS_BY_SCREEN, [moveEl.X, moveEl.Y], this)).usrCoords;
    1299
    1300                 alpha = Geometry.rad(op.slice(1), fix.slice(1), np.slice(1));
    1301
    1302                 // Rotate and scale by the movement of the second finger
    1303                 t1 = this.create('transform', [-fix[1], -fix[2]], {type: 'translate'});
    1304                 t2 = this.create('transform', [alpha], {type: 'rotate'});
    1305                 t1.melt(t2);
    1306                 if (Type.evaluate(drag.visProp.scalable)) {
    1307                     d = Geometry.distance(fix, np) / Geometry.distance(fix, op);
    1308                     t3 = this.create('transform', [d, d], {type: 'scale'});
    1309                     t1.melt(t3);
    1310                 }
    1311                 t4 = this.create('transform', [fix[1], fix[2]], {type: 'translate'});
    1312                 t1.melt(t4);
    1313
    1314                 if (drag.center.draggable()) {
    1315                     t1.applyOnce([drag.center]);
    1316                 }
    1317
    1318                 if (drag.method === 'twoPoints') {
    1319                     if (drag.point2.draggable()) {
    1320                         t1.applyOnce([drag.point2]);
    1321                     }
    1322                 } else if (drag.method === 'pointRadius') {
    1323                     if (Type.isNumber(drag.updateRadius.origin)) {
    1324                         drag.setRadius(drag.radius * d);
    1325                     }
    1326                 }
    1327
    1328                 this.update(drag.center);
    1329                 drag.highlight(true);
    1330             }
    1331         },
    1332
    1333         highlightElements: function (x, y, evt, target) {
    1334             var el, pEl, pId,
    1335                 overObjects = {},
    1336                 len = this.objectsList.length;
    1337
    1338             // Elements  below the mouse pointer which are not highlighted yet will be highlighted.
    1339             for (el = 0; el < len; el++) {
    1340                 pEl = this.objectsList[el];
    1341                 pId = pEl.id;
    1342                 if (Type.exists(pEl.hasPoint) && pEl.visPropCalc.visible && pEl.hasPoint(x, y)) {
    1343                     // this is required in any case because otherwise the box won't be shown until the point is dragged
    1344                     this.updateInfobox(pEl);
    1345
    1346                     if (!Type.exists(this.highlightedObjects[pId])) { // highlight only if not highlighted
    1347                         overObjects[pId] = pEl;
    1348                         pEl.highlight();
    1349                         // triggers board event.
    1350                         this.triggerEventHandlers(['mousehit', 'hit'], [evt, pEl, target]);
    1351                     }
    1352
    1353                     if (pEl.mouseover) {
    1354                         pEl.triggerEventHandlers(['mousemove', 'move'], [evt]);
    1355                     } else {
    1356                         pEl.triggerEventHandlers(['mouseover', 'over'], [evt]);
    1357                         pEl.mouseover = true;
    1358                     }
    1359                 }
    1360             }
    1361
    1362             for (el = 0; el < len; el++) {
    1363                 pEl = this.objectsList[el];
    1364                 pId = pEl.id;
    1365                 if (pEl.mouseover) {
    1366                     if (!overObjects[pId]) {
    1367                         pEl.triggerEventHandlers(['mouseout', 'out'], [evt]);
    1368                         pEl.mouseover = false;
    1369                     }
    1370                 }
    1371             }
    1372         },
    1373
    1374         /**
    1375          * Helper function which returns a reasonable starting point for the object being dragged.
    1376          * Formerly known as initXYstart().
    1377          * @private
    1378          * @param {JXG.GeometryElement} obj The object to be dragged
    1379          * @param {Array} targets Array of targets. It is changed by this function.
    1380          */
    1381         saveStartPos: function (obj, targets) {
    1382             var xy = [], i, len;
    1383
    1384             if (obj.type === Const.OBJECT_TYPE_TICKS) {
    1385                 xy.push([1, NaN, NaN]);
    1386             } else if (obj.elementClass === Const.OBJECT_CLASS_LINE) {
    1387                 xy.push(obj.point1.coords.usrCoords);
    1388                 xy.push(obj.point2.coords.usrCoords);
    1389             } else if (obj.elementClass === Const.OBJECT_CLASS_CIRCLE) {
    1390                 xy.push(obj.center.coords.usrCoords);
    1391                 if (obj.method === 'twoPoints') {
    1392                     xy.push(obj.point2.coords.usrCoords);
    1393                 }
    1394             } else if (obj.type === Const.OBJECT_TYPE_POLYGON) {
    1395                 len = obj.vertices.length - 1;
    1396                 for (i = 0; i < len; i++) {
    1397                     xy.push(obj.vertices[i].coords.usrCoords);
    1398                 }
    1399             } else if (obj.type === Const.OBJECT_TYPE_SECTOR) {
    1400                 xy.push(obj.point1.coords.usrCoords);
    1401                 xy.push(obj.point2.coords.usrCoords);
    1402                 xy.push(obj.point3.coords.usrCoords);
    1403             } else if (Type.isPoint(obj) || obj.type === Const.OBJECT_TYPE_GLIDER) {
    1404                 xy.push(obj.coords.usrCoords);
    1405             } else if (obj.elementClass === Const.OBJECT_CLASS_CURVE) {
    1406                 // if (Type.exists(obj.parents)) {
    1407                 //     len = obj.parents.length;
    1408                 //     if (len > 0) {
    1409                 //         for (i = 0; i < len; i++) {
    1410                 //             xy.push(this.select(obj.parents[i]).coords.usrCoords);
    1411                 //         }
    1412                 //     } else
    1413                 // }
    1414                 if (obj.points.length > 0) {
    1415                     xy.push(obj.points[0].usrCoords);
    1416                 }
    1417             } else {
    1418                 try {
    1419                     xy.push(obj.coords.usrCoords);
    1420                 } catch (e) {
    1421                     JXG.debug('JSXGraph+ saveStartPos: obj.coords.usrCoords not available: ' + e);
    1422                 }
    1423             }
    1424
    1425             len = xy.length;
    1426             for (i = 0; i < len; i++) {
    1427                 targets.Zstart.push(xy[i][0]);
    1428                 targets.Xstart.push(xy[i][1]);
    1429                 targets.Ystart.push(xy[i][2]);
    1430             }
    1431         },
    1432
    1433         mouseOriginMoveStart: function (evt) {
    1434             var r, pos;
    1435
    1436             r = this._isRequiredKeyPressed(evt, 'pan');
    1437             if (r) {
    1438                 pos = this.getMousePosition(evt);
    1439                 this.initMoveOrigin(pos[0], pos[1]);
    1440             }
    1441
    1442             return r;
    1443         },
    1444
    1445         mouseOriginMove: function (evt) {
    1446             var r = (this.mode === this.BOARD_MODE_MOVE_ORIGIN),
    1447                 pos;
    1448
    1449             if (r) {
    1450                 pos = this.getMousePosition(evt);
    1451                 this.moveOrigin(pos[0], pos[1], true);
    1452             }
    1453
    1454             return r;
    1455         },
    1456
    1457         /**
    1458          * Start moving the origin with one finger.
    1459          * @private
    1460          * @param  {Object} evt Event from touchStartListener
    1461          * @return {Boolean}   returns if the origin is moved.
    1462          */
    1463         touchStartMoveOriginOneFinger: function (evt) {
    1464             var touches = evt[JXG.touchProperty],
    1465                 conditions, pos;
    1466
    1467             conditions = this.attr.pan.enabled &&
    1468                 !this.attr.pan.needtwofingers &&
    1469                 touches.length === 1;
    1470
    1471             if (conditions) {
    1472                 pos = this.getMousePosition(evt, 0);
    1473                 this.initMoveOrigin(pos[0], pos[1]);
    1474             }
    1475
    1476             return conditions;
    1477         },
    1478
    1479         /**
    1480          * Move the origin with one finger
    1481          * @private
    1482          * @param  {Object} evt Event from touchMoveListener
    1483          * @return {Boolean}     returns if the origin is moved.
    1484          */
    1485         touchOriginMove: function (evt) {
    1486             var r = (this.mode === this.BOARD_MODE_MOVE_ORIGIN),
    1487                 pos;
    1488
    1489             if (r) {
    1490                 pos = this.getMousePosition(evt, 0);
    1491                 this.moveOrigin(pos[0], pos[1], true);
    1492             }
    1493
    1494             return r;
    1495         },
    1496
    1497         /**
    1498          * Stop moving the origin with one finger
    1499          * @return {null} null
    1500          * @private
    1501          */
    1502         originMoveEnd: function () {
    1503             this.updateQuality = this.BOARD_QUALITY_HIGH;
    1504             this.mode = this.BOARD_MODE_NONE;
    1505         },
    1506
    1507         /**********************************************************
    1508          *
    1509          * Event Handler
    1510          *
    1511          **********************************************************/
    1512
    1513         /**
    1514          *  Add all possible event handlers to the board object
    1515          */
    1516         addEventHandlers: function () {
    1517             if (Env.supportsPointerEvents()) {
    1518                 this.addPointerEventHandlers();
    1519             } else {
    1520                 this.addMouseEventHandlers();
    1521                 this.addTouchEventHandlers();
    1522             }
    1523
    1524             // This one produces errors on IE
    1525             //Env.addEvent(this.containerObj, 'contextmenu', function (e) { e.preventDefault(); return false;}, this);
    1526             // This one works on IE, Firefox and Chromium with default configurations. On some Safari
    1527             // or Opera versions the user must explicitly allow the deactivation of the context menu.
    1528             if (this.containerObj !== null) {
    1529                 this.containerObj.oncontextmenu = function (e) {
    1530                     if (Type.exists(e)) {
    1531                         e.preventDefault();
    1532                     }
    1533                     return false;
    1534                 };
    1535             }
    1536
    1537             this.addFullscreenEventHandlers();
    1538             this.addKeyboardEventHandlers();
    1539
    1540             if (Env.isBrowser) {
    1541                 try {
    1542                     // resizeObserver: triggered if size of the JSXGraph div changes.
    1543                     this.startResizeObserver();
    1544                 } catch (err) {
    1545                     // resize event: triggered if size of window changes
    1546                     Env.addEvent(window, 'resize', this.resizeListener, this);
    1547                     // intersectionObserver: triggered if JSXGraph becomes visible.
    1548                     this.startIntersectionObserver();
    1549                 }
    1550                 // Scroll event: needs to be captured since on mobile devices
    1551                 // sometimes a header bar is displayed / hidden, which triggers a
    1552                 // resize event.
    1553                 Env.addEvent(window, 'scroll', this.scrollListener, this);
    1554             }
    1555         },
    1556
    1557         /**
    1558          * Remove all event handlers from the board object
    1559          */
    1560         removeEventHandlers: function () {
    1561             this.removeMouseEventHandlers();
    1562             this.removeTouchEventHandlers();
    1563             this.removePointerEventHandlers();
    1564
    1565             this.removeFullscreenEventHandlers();
    1566             this.removeKeyboardEventHandlers();
    1567             if (Env.isBrowser) {
    1568                 if (Type.exists(this.resizeObserver)) {
    1569                     this.stopResizeObserver();
    1570                 } else {
    1571                     Env.removeEvent(window, 'resize', this.resizeListener, this);
    1572                     this.stopIntersectionObserver();
    1573                 }
    1574                 Env.removeEvent(window, 'scroll', this.scrollListener, this);
    1575             }
    1576         },
    1577
    1578         /**
    1579          * Registers the MSPointer* event handlers.
    1580          */
    1581         addPointerEventHandlers: function () {
    1582             if (!this.hasPointerHandlers && Env.isBrowser) {
    1583                 var moveTarget = this.attr.movetarget || this.containerObj;
    1584
    1585                 if (window.navigator.msPointerEnabled) {  // IE10-
    1586                     Env.addEvent(this.containerObj, 'MSPointerDown', this.pointerDownListener, this);
    1587                     Env.addEvent(moveTarget, 'MSPointerMove', this.pointerMoveListener, this);
    1588                 } else {
    1589                     Env.addEvent(this.containerObj, 'pointerdown', this.pointerDownListener, this);
    1590                     Env.addEvent(moveTarget, 'pointermove', this.pointerMoveListener, this);
    1591                 }
    1592                 Env.addEvent(this.containerObj, 'mousewheel', this.mouseWheelListener, this);
    1593                 Env.addEvent(this.containerObj, 'DOMMouseScroll', this.mouseWheelListener, this);
    1594
    1595                 if (this.containerObj !== null) {
    1596                     // This is needed for capturing touch events.
    1597                     // It is also in jsxgraph.css, but one never knows...
    1598                     this.containerObj.style.touchAction = 'none';
    1599                 }
    1600
    1601                 this.hasPointerHandlers = true;
    1602             }
    1603         },
    1604
    1605         /**
    1606          * Registers mouse move, down and wheel event handlers.
    1607          */
    1608         addMouseEventHandlers: function () {
    1609             if (!this.hasMouseHandlers && Env.isBrowser) {
    1610                 var moveTarget = this.attr.movetarget || this.containerObj;
    1611
    1612                 Env.addEvent(this.containerObj, 'mousedown', this.mouseDownListener, this);
    1613                 Env.addEvent(moveTarget, 'mousemove', this.mouseMoveListener, this);
    1614
    1615                 Env.addEvent(this.containerObj, 'mousewheel', this.mouseWheelListener, this);
    1616                 Env.addEvent(this.containerObj, 'DOMMouseScroll', this.mouseWheelListener, this);
    1617
    1618                 this.hasMouseHandlers = true;
    1619             }
    1620         },
    1621
    1622         /**
    1623          * Register touch start and move and gesture start and change event handlers.
    1624          * @param {Boolean} appleGestures If set to false the gesturestart and gesturechange event handlers
    1625          * will not be registered.
    1626          *
    1627          * Since iOS 13, touch events were abandoned in favour of pointer events
    1628          */
    1629         addTouchEventHandlers: function (appleGestures) {
    1630             if (!this.hasTouchHandlers && Env.isBrowser) {
    1631                 var moveTarget = this.attr.movetarget || this.containerObj;
    1632
    1633                 Env.addEvent(this.containerObj, 'touchstart', this.touchStartListener, this);
    1634                 Env.addEvent(moveTarget, 'touchmove', this.touchMoveListener, this);
    1635
    1636                 /*
    1637                 if (!Type.exists(appleGestures) || appleGestures) {
    1638                     // Gesture listener are called in touchStart and touchMove.
    1639                     //Env.addEvent(this.containerObj, 'gesturestart', this.gestureStartListener, this);
    1640                     //Env.addEvent(this.containerObj, 'gesturechange', this.gestureChangeListener, this);
    1641                 }
    1642                 */
    1643
    1644                 this.hasTouchHandlers = true;
    1645             }
    1646         },
    1647
    1648         /**
    1649          * Add fullscreen events which update the CSS transformation matrix to correct
    1650          * the mouse/touch/pointer positions in case of CSS transformations.
    1651          */
    1652         addFullscreenEventHandlers: function() {
    1653             var i,
    1654                 // standard/Edge, firefox, chrome/safari, IE11
    1655                 events = ['fullscreenchange', 'mozfullscreenchange', 'webkitfullscreenchange', 'msfullscreenchange'],
    1656                 le = events.length;
    1657
    1658             if (!this.hasFullsceenEventHandlers && Env.isBrowser) {
    1659                 for (i = 0; i < le; i++) {
    1660                     Env.addEvent(this.document, events[i], this.fullscreenListener, this);
    1661                 }
    1662                 this.hasFullsceenEventHandlers = true;
    1663             }
    1664         },
    1665
    1666         addKeyboardEventHandlers: function() {
    1667             if (this.attr.keyboard.enabled && !this.hasKeyboardHandlers && Env.isBrowser) {
    1668                 Env.addEvent(this.containerObj, 'keydown', this.keyDownListener, this);
    1669                 Env.addEvent(this.containerObj, 'focusin', this.keyFocusInListener, this);
    1670                 Env.addEvent(this.containerObj, 'focusout', this.keyFocusOutListener, this);
    1671                 this.hasKeyboardHandlers = true;
    1672             }
    1673         },
    1674
    1675         /**
    1676          * Remove all registered touch event handlers.
    1677          */
    1678         removeKeyboardEventHandlers: function () {
    1679             if (this.hasKeyboardHandlers && Env.isBrowser) {
    1680                 Env.removeEvent(this.containerObj, 'keydown', this.keyDownListener, this);
    1681                 Env.removeEvent(this.containerObj, 'focusin', this.keyFocusInListener, this);
    1682                 Env.removeEvent(this.containerObj, 'focusout', this.keyFocusOutListener, this);
    1683                 this.hasKeyboardHandlers = false;
    1684             }
    1685         },
    1686
    1687         /**
    1688          * Remove all registered event handlers regarding fullscreen mode.
    1689          */
    1690         removeFullscreenEventHandlers: function() {
    1691             var i,
    1692                 // standard/Edge, firefox, chrome/safari, IE11
    1693                 events = ['fullscreenchange', 'mozfullscreenchange', 'webkitfullscreenchange', 'msfullscreenchange'],
    1694                 le = events.length;
    1695
    1696             if (this.hasFullsceenEventHandlers && Env.isBrowser) {
    1697                 for (i = 0; i < le; i++) {
    1698                     Env.removeEvent(this.document, events[i], this.fullscreenListener, this);
    1699                 }
    1700                 this.hasFullsceenEventHandlers = false;
    1701             }
    1702         },
    1703
    1704         /**
    1705          * Remove MSPointer* Event handlers.
    1706          */
    1707         removePointerEventHandlers: function () {
    1708             if (this.hasPointerHandlers && Env.isBrowser) {
    1709                 var moveTarget = this.attr.movetarget || this.containerObj;
    1710
    1711                 if (window.navigator.msPointerEnabled) {  // IE10-
    1712                     Env.removeEvent(this.containerObj, 'MSPointerDown', this.pointerDownListener, this);
    1713                     Env.removeEvent(moveTarget, 'MSPointerMove', this.pointerMoveListener, this);
    1714                 } else {
    1715                     Env.removeEvent(this.containerObj, 'pointerdown', this.pointerDownListener, this);
    1716                     Env.removeEvent(moveTarget, 'pointermove', this.pointerMoveListener, this);
    1717                 }
    1718
    1719                 Env.removeEvent(this.containerObj, 'mousewheel', this.mouseWheelListener, this);
    1720                 Env.removeEvent(this.containerObj, 'DOMMouseScroll', this.mouseWheelListener, this);
    1721
    1722                 if (this.hasPointerUp) {
    1723                     if (window.navigator.msPointerEnabled) {  // IE10-
    1724                         Env.removeEvent(this.document, 'MSPointerUp',   this.pointerUpListener, this);
    1725                     } else {
    1726                         Env.removeEvent(this.document, 'pointerup',     this.pointerUpListener, this);
    1727                         Env.removeEvent(this.document, 'pointercancel', this.pointerUpListener, this);
    1728                     }
    1729                     this.hasPointerUp = false;
    1730                 }
    1731
    1732                 this.hasPointerHandlers = false;
    1733             }
    1734         },
    1735
    1736         /**
    1737          * De-register mouse event handlers.
    1738          */
    1739         removeMouseEventHandlers: function () {
    1740             if (this.hasMouseHandlers && Env.isBrowser) {
    1741                 var moveTarget = this.attr.movetarget || this.containerObj;
    1742
    1743                 Env.removeEvent(this.containerObj, 'mousedown', this.mouseDownListener, this);
    1744                 Env.removeEvent(moveTarget, 'mousemove', this.mouseMoveListener, this);
    1745
    1746                 if (this.hasMouseUp) {
    1747                     Env.removeEvent(this.document, 'mouseup', this.mouseUpListener, this);
    1748                     this.hasMouseUp = false;
    1749                 }
    1750
    1751                 Env.removeEvent(this.containerObj, 'mousewheel', this.mouseWheelListener, this);
    1752                 Env.removeEvent(this.containerObj, 'DOMMouseScroll', this.mouseWheelListener, this);
    1753
    1754                 this.hasMouseHandlers = false;
    1755             }
    1756         },
    1757
    1758         /**
    1759          * Remove all registered touch event handlers.
    1760          */
    1761         removeTouchEventHandlers: function () {
    1762             if (this.hasTouchHandlers && Env.isBrowser) {
    1763                 var moveTarget = this.attr.movetarget || this.containerObj;
    1764
    1765                 Env.removeEvent(this.containerObj, 'touchstart', this.touchStartListener, this);
    1766                 Env.removeEvent(moveTarget, 'touchmove', this.touchMoveListener, this);
    1767
    1768                 if (this.hasTouchEnd) {
    1769                     Env.removeEvent(this.document, 'touchend', this.touchEndListener, this);
    1770                     this.hasTouchEnd = false;
    1771                 }
    1772
    1773                 this.hasTouchHandlers = false;
    1774             }
    1775         },
    1776
    1777         /**
    1778          * Handler for click on left arrow in the navigation bar
    1779          * @returns {JXG.Board} Reference to the board
    1780          */
    1781         clickLeftArrow: function () {
    1782             this.moveOrigin(this.origin.scrCoords[1] + this.canvasWidth * 0.1, this.origin.scrCoords[2]);
    1783             return this;
    1784         },
    1785
    1786         /**
    1787          * Handler for click on right arrow in the navigation bar
    1788          * @returns {JXG.Board} Reference to the board
    1789          */
    1790         clickRightArrow: function () {
    1791             this.moveOrigin(this.origin.scrCoords[1] - this.canvasWidth * 0.1, this.origin.scrCoords[2]);
    1792             return this;
    1793         },
    1794
    1795         /**
    1796          * Handler for click on up arrow in the navigation bar
    1797          * @returns {JXG.Board} Reference to the board
    1798          */
    1799         clickUpArrow: function () {
    1800             this.moveOrigin(this.origin.scrCoords[1], this.origin.scrCoords[2] - this.canvasHeight * 0.1);
    1801             return this;
    1802         },
    1803
    1804         /**
    1805          * Handler for click on down arrow in the navigation bar
    1806          * @returns {JXG.Board} Reference to the board
    1807          */
    1808         clickDownArrow: function () {
    1809             this.moveOrigin(this.origin.scrCoords[1], this.origin.scrCoords[2] + this.canvasHeight * 0.1);
    1810             return this;
    1811         },
    1812
    1813         /**
    1814          * Triggered on iOS/Safari while the user inputs a gesture (e.g. pinch) and is used to zoom into the board.
    1815          * Works on iOS/Safari and Android.
    1816          * @param {Event} evt Browser event object
    1817          * @returns {Boolean}
    1818          */
    1819         gestureChangeListener: function (evt) {
    1820             var c,
    1821                 dir1 = [],
    1822                 dir2 = [],
    1823                 angle,
    1824                 mi = 10,
    1825                 isPinch = false,
    1826                 // Save zoomFactors
    1827                 zx = this.attr.zoom.factorx,
    1828                 zy = this.attr.zoom.factory,
    1829                 factor,
    1830                 dist,
    1831                 dx, dy, theta, cx, cy, bound;
    1832
    1833             if (this.mode !== this.BOARD_MODE_ZOOM) {
    1834                 return true;
    1835             }
    1836             evt.preventDefault();
    1837
    1838             dist = Geometry.distance([evt.touches[0].clientX, evt.touches[0].clientY],
    1839                 [evt.touches[1].clientX, evt.touches[1].clientY], 2);
    1840
    1841             // Android pinch to zoom
    1842             // evt.scale was available in iOS touch events (pre iOS 13)
    1843             // evt.scale is undefined in Android
    1844             if (evt.scale === undefined) {
    1845                 evt.scale = dist / this.prevDist;
    1846             }
    1847
    1848             if (!Type.exists(this.prevCoords)) {
    1849                 return false;
    1850             }
    1851             // Compute the angle of the two finger directions
    1852             dir1 = [evt.touches[0].clientX - this.prevCoords[0][0],
    1853                     evt.touches[0].clientY - this.prevCoords[0][1]];
    1854             dir2 = [evt.touches[1].clientX - this.prevCoords[1][0],
    1855                     evt.touches[1].clientY - this.prevCoords[1][1]];
    1856
    1857             if ((dir1[0] * dir1[0] + dir1[1] * dir1[1] < mi * mi) &&
    1858                 (dir2[0] * dir2[0] + dir2[1] * dir2[1] < mi * mi)) {
    1859                     return false;
    1860             }
    1861
    1862             angle = Geometry.rad(dir1, [0,0], dir2);
    1863             if (this.isPreviousGesture !== 'pan' &&
    1864                 Math.abs(angle) > Math.PI * 0.2 &&
    1865                 Math.abs(angle) < Math.PI * 1.8) {
    1866                 isPinch = true;
    1867             }
    1868
    1869             if (this.isPreviousGesture !== 'pan' && !isPinch) {
    1870                 if (Math.abs(evt.scale) < 0.77 || Math.abs(evt.scale) > 1.3) {
    1871                     isPinch = true;
    1872                 }
    1873             }
    1874
    1875             factor = evt.scale / this.prevScale;
    1876             this.prevScale = evt.scale;
    1877             this.prevCoords = [[evt.touches[0].clientX, evt.touches[0].clientY],
    1878                                [evt.touches[1].clientX, evt.touches[1].clientY]];
    1879
    1880             c = new Coords(Const.COORDS_BY_SCREEN, this.getMousePosition(evt, 0), this);
    1881
    1882             if (this.attr.pan.enabled &&
    1883                 this.attr.pan.needtwofingers &&
    1884                 !isPinch) {
    1885                 // Pan detected
    1886
    1887                 this.isPreviousGesture = 'pan';
    1888
    1889                 this.moveOrigin(c.scrCoords[1], c.scrCoords[2], true);
    1890             } else if (this.attr.zoom.enabled &&
    1891                         Math.abs(factor - 1.0) < 0.5) {
    1892                 // Pinch detected
    1893
    1894                 if (this.attr.zoom.pinchhorizontal || this.attr.zoom.pinchvertical) {
    1895                     dx = Math.abs(evt.touches[0].clientX - evt.touches[1].clientX);
    1896                     dy = Math.abs(evt.touches[0].clientY - evt.touches[1].clientY);
    1897                     theta = Math.abs(Math.atan2(dy, dx));
    1898                     bound = Math.PI * this.attr.zoom.pinchsensitivity / 90.0;
    1899                 }
    1900
    1901                 if (this.attr.zoom.pinchhorizontal && theta < bound) {
    1902                     this.attr.zoom.factorx = factor;
    1903                     this.attr.zoom.factory = 1.0;
    1904                     cx = 0;
    1905                     cy = 0;
    1906                 } else if (this.attr.zoom.pinchvertical && Math.abs(theta - Math.PI * 0.5) < bound) {
    1907                     this.attr.zoom.factorx = 1.0;
    1908                     this.attr.zoom.factory = factor;
    1909                     cx = 0;
    1910                     cy = 0;
    1911                 } else {
    1912                     this.attr.zoom.factorx = factor;
    1913                     this.attr.zoom.factory = factor;
    1914                     cx = c.usrCoords[1];
    1915                     cy = c.usrCoords[2];
    1916                 }
    1917
    1918                 this.zoomIn(cx, cy);
    1919
    1920                 // Restore zoomFactors
    1921                 this.attr.zoom.factorx = zx;
    1922                 this.attr.zoom.factory = zy;
    1923             }
    1924
    1925             return false;
    1926         },
    1927
    1928         /**
    1929          * Called by iOS/Safari as soon as the user starts a gesture. Works natively on iOS/Safari,
    1930          * on Android we emulate it.
    1931          * @param {Event} evt
    1932          * @returns {Boolean}
    1933          */
    1934         gestureStartListener: function (evt) {
    1935             var pos;
    1936
    1937             evt.preventDefault();
    1938             this.prevScale = 1.0;
    1939             // Android pinch to zoom
    1940             this.prevDist = Geometry.distance([evt.touches[0].clientX, evt.touches[0].clientY],
    1941                             [evt.touches[1].clientX, evt.touches[1].clientY], 2);
    1942             this.prevCoords = [[evt.touches[0].clientX, evt.touches[0].clientY],
    1943                                [evt.touches[1].clientX, evt.touches[1].clientY]];
    1944             this.isPreviousGesture = 'none';
    1945
    1946             // If pinch-to-zoom is interpreted as panning
    1947             // we have to prepare move origin
    1948             pos = this.getMousePosition(evt, 0);
    1949             this.initMoveOrigin(pos[0], pos[1]);
    1950
    1951             this.mode = this.BOARD_MODE_ZOOM;
    1952             return false;
    1953         },
    1954
    1955         /**
    1956          * Test if the required key combination is pressed for wheel zoom, move origin and
    1957          * selection
    1958          * @private
    1959          * @param  {Object}  evt    Mouse or pen event
    1960          * @param  {String}  action String containing the action: 'zoom', 'pan', 'selection'.
    1961          * Corresponds to the attribute subobject.
    1962          * @return {Boolean}        true or false.
    1963          */
    1964         _isRequiredKeyPressed: function (evt, action) {
    1965             var obj = this.attr[action];
    1966             if (!obj.enabled) {
    1967                 return false;
    1968             }
    1969
    1970             if (((obj.needshift && evt.shiftKey) || (!obj.needshift && !evt.shiftKey)) &&
    1971                 ((obj.needctrl && evt.ctrlKey) || (!obj.needctrl && !evt.ctrlKey))
    1972             )  {
    1973                 return true;
    1974             }
    1975
    1976             return false;
    1977         },
    1978
    1979         /*
    1980          * Pointer events
    1981          */
    1982
    1983         /**
    1984          *
    1985          * Check if pointer event is already registered in {@link JXG.Board#_board_touches}.
    1986          *
    1987          * @param  {Object} evt Event object
    1988          * @return {Boolean} true if down event has already been sent.
    1989          * @private
    1990          */
    1991          _isPointerRegistered: function(evt) {
    1992             var i, len = this._board_touches.length;
    1993
    1994             for (i = 0; i < len; i++) {
    1995                 if (this._board_touches[i].pointerId === evt.pointerId) {
    1996                     return true;
    1997                 }
    1998             }
    1999             return false;
    2000         },
    2001
    2002         /**
    2003          *
    2004          * Store the position of a pointer event.
    2005          * If not yet done, registers a pointer event in {@link JXG.Board#_board_touches}.
    2006          * Allows to follow the path of that finger on the screen.
    2007          * Only two simultaneous touches are supported.
    2008          *
    2009          * @param {Object} evt Event object
    2010          * @returns {JXG.Board} Reference to the board
    2011          * @private
    2012          */
    2013          _pointerStorePosition: function (evt) {
    2014             var i, found;
    2015
    2016             for (i = 0, found = false; i < this._board_touches.length; i++) {
    2017                 if (this._board_touches[i].pointerId === evt.pointerId) {
    2018                     this._board_touches[i].clientX = evt.clientX;
    2019                     this._board_touches[i].clientY = evt.clientY;
    2020                     found = true;
    2021                     break;
    2022                 }
    2023             }
    2024
    2025             // Restrict the number of simultaneous touches to 2
    2026             if (!found && this._board_touches.length < 2) {
    2027                 this._board_touches.push({
    2028                     pointerId: evt.pointerId,
    2029                     clientX: evt.clientX,
    2030                     clientY: evt.clientY
    2031                 });
    2032             }
    2033
    2034             return this;
    2035         },
    2036
    2037         /**
    2038          * Deregisters a pointer event in {@link JXG.Board#_board_touches}.
    2039          * It happens if a finger has been lifted from the screen.
    2040          *
    2041          * @param {Object} evt Event object
    2042          * @returns {JXG.Board} Reference to the board
    2043          * @private
    2044          */
    2045         _pointerRemoveTouches: function (evt) {
    2046             var i;
    2047             for (i = 0; i < this._board_touches.length; i++) {
    2048                 if (this._board_touches[i].pointerId === evt.pointerId) {
    2049                     this._board_touches.splice(i, 1);
    2050                     break;
    2051                 }
    2052             }
    2053
    2054             return this;
    2055         },
    2056
    2057         /**
    2058          * Remove all registered fingers from {@link JXG.Board#_board_touches}.
    2059          * This might be necessary if too many fingers have been registered.
    2060          * @returns {JXG.Board} Reference to the board
    2061          * @private
    2062          */
    2063         _pointerClearTouches: function() {
    2064             if (this._board_touches.length > 0) {
    2065                 this.dehighlightAll();
    2066             }
    2067             this.updateQuality = this.BOARD_QUALITY_HIGH;
    2068             this.mode = this.BOARD_MODE_NONE;
    2069             this._board_touches = [];
    2070             this.touches = [];
    2071         },
    2072
    2073         /**
    2074          * Determine which input device is used for this action.
    2075          * Possible devices are 'touch', 'pen' and 'mouse'.
    2076          * This affects the precision and certain events.
    2077          * In case of no browser, 'mouse' is used.
    2078          *
    2079          * @see JXG.Board#pointerDownListener
    2080          * @see JXG.Board#pointerMoveListener
    2081          * @see JXG.Board#initMoveObject
    2082          * @see JXG.Board#moveObject
    2083          *
    2084          * @param {Event} evt The browsers event object.
    2085          * @returns {String} 'mouse', 'pen', or 'touch'
    2086          * @private
    2087          */
    2088         _getPointerInputDevice: function(evt) {
    2089             if (Env.isBrowser) {
    2090                 if (evt.pointerType === 'touch' ||        // New
    2091                     (window.navigator.msMaxTouchPoints && // Old
    2092                         window.navigator.msMaxTouchPoints > 1)) {
    2093                     return 'touch';
    2094                 }
    2095                 if (evt.pointerType === 'mouse') {
    2096                     return 'mouse';
    2097                 }
    2098                 if (evt.pointerType === 'pen') {
    2099                     return 'pen';
    2100                 }
    2101             }
    2102             return 'mouse';
    2103         },
    2104
    2105         /**
    2106          * This method is called by the browser when a pointing device is pressed on the screen.
    2107          * @param {Event} evt The browsers event object.
    2108          * @param {Object} object If the object to be dragged is already known, it can be submitted via this parameter
    2109          * @returns {Boolean} ...
    2110          */
    2111         pointerDownListener: function (evt, object) {
    2112             var i, j, k, pos, elements, sel,
    2113                 target_obj,
    2114                 type = 'mouse', // Used in case of no browser
    2115                 found, target;
    2116
    2117             // Fix for Firefox browser: When using a second finger, the
    2118             // touch event for the first finger is sent again.
    2119             if (!object && this._isPointerRegistered(evt)) {
    2120                 return false;
    2121             }
    2122
    2123             if (!object && evt.isPrimary) {
    2124                 // First finger down. To be on the safe side this._board_touches is cleared.
    2125                 this._pointerClearTouches();
    2126             }
    2127
    2128             if (!this.hasPointerUp) {
    2129                 if (window.navigator.msPointerEnabled) {  // IE10-
    2130                     Env.addEvent(this.document, 'MSPointerUp',   this.pointerUpListener, this);
    2131                 } else {
    2132                     // 'pointercancel' is fired e.g. if the finger leaves the browser and drags down the system menu on Android
    2133                     Env.addEvent(this.document, 'pointerup',     this.pointerUpListener, this);
    2134                     Env.addEvent(this.document, 'pointercancel', this.pointerUpListener, this);
    2135                 }
    2136                 this.hasPointerUp = true;
    2137             }
    2138
    2139             if (this.hasMouseHandlers) {
    2140                 this.removeMouseEventHandlers();
    2141             }
    2142
    2143             if (this.hasTouchHandlers) {
    2144                 this.removeTouchEventHandlers();
    2145             }
    2146
    2147             // Prevent accidental selection of text
    2148             if (this.document.selection && Type.isFunction(this.document.selection.empty)) {
    2149                 this.document.selection.empty();
    2150             } else if (window.getSelection) {
    2151                 sel = window.getSelection();
    2152                 if (sel.removeAllRanges) {
    2153                     try {
    2154                         sel.removeAllRanges();
    2155                     } catch (e) {}
    2156                 }
    2157             }
    2158
    2159             // Mouse, touch or pen device
    2160             this._inputDevice = this._getPointerInputDevice(evt);
    2161             type = this._inputDevice;
    2162             this.options.precision.hasPoint = this.options.precision[type];
    2163
    2164             // Handling of multi touch with pointer events should be easier than the touch events.
    2165             // Every pointer device has its own pointerId, e.g. the mouse
    2166             // always has id 1 or 0, fingers and pens get unique ids every time a pointerDown event is fired and they will
    2167             // keep this id until a pointerUp event is fired. What we have to do here is:
    2168             //  1. collect all elements under the current pointer
    2169             //  2. run through the touches control structure
    2170             //    a. look for the object collected in step 1.
    2171             //    b. if an object is found, check the number of pointers. If appropriate, add the pointer.
    2172             pos = this.getMousePosition(evt);
    2173
    2174             // selection
    2175             this._testForSelection(evt);
    2176             if (this.selectingMode) {
    2177                 this._startSelecting(pos);
    2178                 this.triggerEventHandlers(['touchstartselecting', 'pointerstartselecting', 'startselecting'], [evt]);
    2179                 return;     // don't continue as a normal click
    2180             }
    2181
    2182             if (this.attr.drag.enabled && object) {
    2183                 elements = [ object ];
    2184                 this.mode = this.BOARD_MODE_DRAG;
    2185             } else {
    2186                 elements = this.initMoveObject(pos[0], pos[1], evt, type);
    2187             }
    2188
    2189             target_obj = {
    2190                 num: evt.pointerId,
    2191                 X: pos[0],
    2192                 Y: pos[1],
    2193                 Xprev: NaN,
    2194                 Yprev: NaN,
    2195                 Xstart: [],
    2196                 Ystart: [],
    2197                 Zstart: []
    2198             };
    2199
    2200             // If no draggable object can be found, get out here immediately
    2201             if (elements.length > 0) {
    2202                 // check touches structure
    2203                 target = elements[elements.length - 1];
    2204                 found = false;
    2205
    2206                 // Reminder: this.touches is the list of elements which
    2207                 // currently "possess" a pointer (mouse, pen, finger)
    2208                 for (i = 0; i < this.touches.length; i++) {
    2209                     // An element receives a further touch, i.e.
    2210                     // the target is already in our touches array, add the pointer to the existing touch
    2211                     if (this.touches[i].obj === target) {
    2212                         j = i;
    2213                         k = this.touches[i].targets.push(target_obj) - 1;
    2214                         found = true;
    2215                         break;
    2216                     }
    2217                 }
    2218                 if (!found) {
    2219                     // An new element hae been touched.
    2220                     k = 0;
    2221                     j = this.touches.push({
    2222                         obj: target,
    2223                         targets: [target_obj]
    2224                     }) - 1;
    2225                 }
    2226
    2227                 this.dehighlightAll();
    2228                 target.highlight(true);
    2229
    2230                 this.saveStartPos(target, this.touches[j].targets[k]);
    2231
    2232                 // Prevent accidental text selection
    2233                 // this could get us new trouble: input fields, links and drop down boxes placed as text
    2234                 // on the board don't work anymore.
    2235                 if (evt && evt.preventDefault) {
    2236                     evt.preventDefault();
    2237                 } else if (window.event) {
    2238                     window.event.returnValue = false;
    2239                 }
    2240             }
    2241
    2242             if (this.touches.length > 0) {
    2243                 evt.preventDefault();
    2244                 evt.stopPropagation();
    2245             }
    2246
    2247             if (!Env.isBrowser) {
    2248                 return false;
    2249             }
    2250             if (this._getPointerInputDevice(evt) !== 'touch') {
    2251                 if (this.mode === this.BOARD_MODE_NONE) {
    2252                     this.mouseOriginMoveStart(evt);
    2253                 }
    2254             } else {
    2255                 this._pointerStorePosition(evt);
    2256                 evt.touches = this._board_touches;
    2257
    2258                 // Touch events on empty areas of the board are handled here, see also touchStartListener
    2259                 // 1. case: one finger. If allowed, this triggers pan with one finger
    2260                 if (evt.touches.length === 1 &&
    2261                     this.mode === this.BOARD_MODE_NONE &&
    2262                     this.touchStartMoveOriginOneFinger(evt)) {
    2263                         // Empty by purpose
    2264                 } else if (evt.touches.length === 2 &&
    2265                             (this.mode === this.BOARD_MODE_NONE || this.mode === this.BOARD_MODE_MOVE_ORIGIN)
    2266                         ) {
    2267                     // 2. case: two fingers: pinch to zoom or pan with two fingers needed.
    2268                     // This happens when the second finger hits the device. First, the
    2269                     // "one finger pan mode" has to be cancelled.
    2270                     if (this.mode === this.BOARD_MODE_MOVE_ORIGIN) {
    2271                         this.originMoveEnd();
    2272                     }
    2273
    2274                     this.gestureStartListener(evt);
    2275                 }
    2276             }
    2277
    2278             this.triggerEventHandlers(['touchstart', 'down', 'pointerdown', 'MSPointerDown'], [evt]);
    2279             return false;
    2280         },
    2281
    2282         // /**
    2283         //  * Called if pointer leaves an HTML tag. It is called by the inner-most tag.
    2284         //  * That means, if a JSXGraph text, i.e. an HTML div, is placed close
    2285         //  * to the border of the board, this pointerout event will be ignored.
    2286         //  * @param  {Event} evt
    2287         //  * @return {Boolean}
    2288         //  */
    2289         // pointerOutListener: function (evt) {
    2290         //     if (evt.target === this.containerObj ||
    2291         //         (this.renderer.type === 'svg' && evt.target === this.renderer.foreignObjLayer)) {
    2292         //         this.pointerUpListener(evt);
    2293         //     }
    2294         //     return this.mode === this.BOARD_MODE_NONE;
    2295         // },
    2296
    2297         /**
    2298          * Called periodically by the browser while the user moves a pointing device across the screen.
    2299          * @param {Event} evt
    2300          * @returns {Boolean}
    2301          */
    2302         pointerMoveListener: function (evt) {
    2303             var i, j, pos, touchTargets,
    2304                 type = 'mouse'; // in case of no browser
    2305
    2306             if (this._getPointerInputDevice(evt) === 'touch' && !this._isPointerRegistered(evt)) {
    2307                 // Test, if there was a previous down event of this _getPointerId
    2308                 // (in case it is a touch event).
    2309                 // Otherwise this move event is ignored. This is necessary e.g. for sketchometry.
    2310                 return this.BOARD_MODE_NONE;
    2311             }
    2312
    2313             if (!this.checkFrameRate(evt)) {
    2314                 return false;
    2315             }
    2316
    2317             if (this.mode !== this.BOARD_MODE_DRAG) {
    2318                 this.dehighlightAll();
    2319                 this.displayInfobox(false);
    2320             }
    2321
    2322             if (this.mode !== this.BOARD_MODE_NONE) {
    2323                 evt.preventDefault();
    2324                 evt.stopPropagation();
    2325             }
    2326
    2327             this.updateQuality = this.BOARD_QUALITY_LOW;
    2328             // Mouse, touch or pen device
    2329             this._inputDevice = this._getPointerInputDevice(evt);
    2330             type = this._inputDevice;
    2331             this.options.precision.hasPoint = this.options.precision[type];
    2332
    2333             // selection
    2334             if (this.selectingMode) {
    2335                 pos = this.getMousePosition(evt);
    2336                 this._moveSelecting(pos);
    2337                 this.triggerEventHandlers(['touchmoveselecting', 'moveselecting', 'pointermoveselecting'], [evt, this.mode]);
    2338             } else if (!this.mouseOriginMove(evt)) {
    2339                 if (this.mode === this.BOARD_MODE_DRAG) {
    2340                     // Run through all jsxgraph elements which are touched by at least one finger.
    2341                     for (i = 0; i < this.touches.length; i++) {
    2342                         touchTargets = this.touches[i].targets;
    2343                         // Run through all touch events which have been started on this jsxgraph element.
    2344                         for (j = 0; j < touchTargets.length; j++) {
    2345                             if (touchTargets[j].num === evt.pointerId) {
    2346
    2347                                 pos = this.getMousePosition(evt);
    2348                                 touchTargets[j].X = pos[0];
    2349                                 touchTargets[j].Y = pos[1];
    2350
    2351                                 if (touchTargets.length === 1) {
    2352                                     // Touch by one finger: this is possible for all elements that can be dragged
    2353                                     this.moveObject(pos[0], pos[1], this.touches[i], evt, type);
    2354                                 } else if (touchTargets.length === 2) {
    2355                                     // Touch by two fingers: e.g. moving lines
    2356                                     this.twoFingerMove(this.touches[i], evt.pointerId, evt);
    2357
    2358                                     touchTargets[j].Xprev = pos[0];
    2359                                     touchTargets[j].Yprev = pos[1];
    2360                                 }
    2361
    2362                                 // There is only one pointer in the evt object, so there's no point in looking further
    2363                                 break;
    2364                             }
    2365                         }
    2366                     }
    2367                 } else {
    2368                     if (this._getPointerInputDevice(evt) === 'touch') {
    2369                         this._pointerStorePosition(evt);
    2370
    2371                         if (this._board_touches.length === 2) {
    2372                             evt.touches = this._board_touches;
    2373                             this.gestureChangeListener(evt);
    2374                         }
    2375                     }
    2376
    2377                     // Move event without dragging an element
    2378                     pos = this.getMousePosition(evt);
    2379                     this.highlightElements(pos[0], pos[1], evt, -1);
    2380                 }
    2381             }
    2382
    2383             // Hiding the infobox is commented out, since it prevents showing the infobox
    2384             // on IE 11+ on 'over'
    2385             //if (this.mode !== this.BOARD_MODE_DRAG) {
    2386                 //this.displayInfobox(false);
    2387             //}
    2388             this.triggerEventHandlers(['touchmove', 'move', 'pointermove', 'MSPointerMove'], [evt, this.mode]);
    2389             this.updateQuality = this.BOARD_QUALITY_HIGH;
    2390
    2391             return this.mode === this.BOARD_MODE_NONE;
    2392         },
    2393
    2394         /**
    2395          * Triggered as soon as the user stops touching the device with at least one finger.
    2396          * @param {Event} evt
    2397          * @returns {Boolean}
    2398          */
    2399         pointerUpListener: function (evt) {
    2400             var i, j, found, touchTargets;
    2401
    2402             this.triggerEventHandlers(['touchend', 'up', 'pointerup', 'MSPointerUp'], [evt]);
    2403             this.displayInfobox(false);
    2404
    2405             if (evt) {
    2406                 for (i = 0; i < this.touches.length; i++) {
    2407                     touchTargets = this.touches[i].targets;
    2408                     for (j = 0; j < touchTargets.length; j++) {
    2409                         if (touchTargets[j].num === evt.pointerId) {
    2410                             touchTargets.splice(j, 1);
    2411                             if (touchTargets.length === 0) {
    2412                                 this.touches.splice(i, 1);
    2413                             }
    2414                             break;
    2415                         }
    2416                     }
    2417                 }
    2418             }
    2419
    2420             // selection
    2421             if (this.selectingMode) {
    2422                 this._stopSelecting(evt);
    2423                 this.triggerEventHandlers(['touchstopselecting', 'pointerstopselecting', 'stopselecting'], [evt]);
    2424             } else {
    2425                 for (i = this.downObjects.length - 1; i > -1; i--) {
    2426                     found = false;
    2427                     for (j = 0; j < this.touches.length; j++) {
    2428                         if (this.touches[j].obj.id === this.downObjects[i].id) {
    2429                             found = true;
    2430                         }
    2431                     }
    2432                     if (!found) {
    2433                         this.downObjects[i].triggerEventHandlers(['touchend', 'up', 'pointerup', 'MSPointerUp'], [evt]);
    2434                         this.downObjects[i].snapToGrid();
    2435                         this.downObjects[i].snapToPoints();
    2436                         this.downObjects.splice(i, 1);
    2437                     }
    2438                 }
    2439             }
    2440
    2441             // this._pointerRemoveTouches(evt);
    2442             // if (this._board_touches.length === 0) {
    2443                 if (this.hasPointerUp) {
    2444                     if (window.navigator.msPointerEnabled) {  // IE10-
    2445                         Env.removeEvent(this.document, 'MSPointerUp',   this.pointerUpListener, this);
    2446                     } else {
    2447                         Env.removeEvent(this.document, 'pointerup',     this.pointerUpListener, this);
    2448                         Env.removeEvent(this.document, 'pointercancel', this.pointerUpListener, this);
    2449                     }
    2450                     this.hasPointerUp = false;
    2451                 }
    2452
    2453                 // this.dehighlightAll();
    2454                 // this.updateQuality = this.BOARD_QUALITY_HIGH;
    2455                 // this.mode = this.BOARD_MODE_NONE;
    2456
    2457                 this.originMoveEnd();
    2458                 this.update();
    2459             // }
    2460             // After one finger leaves the screen the gesture is stopped.
    2461             this._pointerClearTouches();
    2462             return true;
    2463         },
    2464
    2465         /**
    2466          * Touch-Events
    2467          */
    2468
    2469         /**
    2470          * This method is called by the browser when a finger touches the surface of the touch-device.
    2471          * @param {Event} evt The browsers event object.
    2472          * @returns {Boolean} ...
    2473          */
    2474         touchStartListener: function (evt) {
    2475             var i, pos, elements, j, k,
    2476                 eps = this.options.precision.touch,
    2477                 obj, found, targets,
    2478                 evtTouches = evt[JXG.touchProperty],
    2479                 target, touchTargets;
    2480
    2481             if (!this.hasTouchEnd) {
    2482                 Env.addEvent(this.document, 'touchend', this.touchEndListener, this);
    2483                 this.hasTouchEnd = true;
    2484             }
    2485
    2486             // Do not remove mouseHandlers, since Chrome on win tablets sends mouseevents if used with pen.
    2487             //if (this.hasMouseHandlers) { this.removeMouseEventHandlers(); }
    2488
    2489             // prevent accidental selection of text
    2490             if (this.document.selection && Type.isFunction(this.document.selection.empty)) {
    2491                 this.document.selection.empty();
    2492             } else if (window.getSelection) {
    2493                 window.getSelection().removeAllRanges();
    2494             }
    2495
    2496             // multitouch
    2497             this._inputDevice = 'touch';
    2498             this.options.precision.hasPoint = this.options.precision.touch;
    2499
    2500             // This is the most critical part. first we should run through the existing touches and collect all targettouches that don't belong to our
    2501             // 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
    2502             // 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
    2503             // 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
    2504             // stretching. as a last step we're going through the rest of the targettouches and initiate new move operations:
    2505             //  * points have higher priority over other elements.
    2506             //  * 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
    2507             //    this element and add them.
    2508             // ADDENDUM 11/10/11:
    2509             //  (1) run through the touches control object,
    2510             //  (2) try to find the targetTouches for every touch. on touchstart only new touches are added, hence we can find a targettouch
    2511             //      for every target in our touches objects
    2512             //  (3) if one of the targettouches was bound to a touches targets array, mark it
    2513             //  (4) run through the targettouches. if the targettouch is marked, continue. otherwise check for elements below the targettouch:
    2514             //      (a) if no element could be found: mark the target touches and continue
    2515             //      --- in the following cases, "init" means:
    2516             //           (i) check if the element is already used in another touches element, if so, mark the targettouch and continue
    2517             //          (ii) if not, init a new touches element, add the targettouch to the touches property and mark it
    2518             //      (b) if the element is a point, init
    2519             //      (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
    2520             //      (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
    2521             //          add both to the touches array and mark them.
    2522             for (i = 0; i < evtTouches.length; i++) {
    2523                 evtTouches[i].jxg_isused = false;
    2524             }
    2525
    2526             for (i = 0; i < this.touches.length; i++) {
    2527                 touchTargets = this.touches[i].targets;
    2528                 for (j = 0; j < touchTargets.length; j++) {
    2529                     touchTargets[j].num = -1;
    2530                     eps = this.options.precision.touch;
    2531
    2532                     do {
    2533                         for (k = 0; k < evtTouches.length; k++) {
    2534                             // find the new targettouches
    2535                             if (Math.abs(Math.pow(evtTouches[k].screenX - touchTargets[j].X, 2) +
    2536                                     Math.pow(evtTouches[k].screenY - touchTargets[j].Y, 2)) < eps * eps) {
    2537                                 touchTargets[j].num = k;
    2538                                 touchTargets[j].X = evtTouches[k].screenX;
    2539                                 touchTargets[j].Y = evtTouches[k].screenY;
    2540                                 evtTouches[k].jxg_isused = true;
    2541                                 break;
    2542                             }
    2543                         }
    2544
    2545                         eps *= 2;
    2546
    2547                     } while (touchTargets[j].num === -1 &&
    2548                              eps < this.options.precision.touchMax);
    2549
    2550                     if (touchTargets[j].num === -1) {
    2551                         JXG.debug('i couldn\'t find a targettouches for target no ' + j + ' on ' + this.touches[i].obj.name + ' (' + this.touches[i].obj.id + '). Removed the target.');
    2552                         JXG.debug('eps = ' + eps + ', touchMax = ' + Options.precision.touchMax);
    2553                         touchTargets.splice(i, 1);
    2554                     }
    2555
    2556                 }
    2557             }
    2558
    2559             // we just re-mapped the targettouches to our existing touches list.
    2560             // now we have to initialize some touches from additional targettouches
    2561             for (i = 0; i < evtTouches.length; i++) {
    2562                 if (!evtTouches[i].jxg_isused) {
    2563
    2564                     pos = this.getMousePosition(evt, i);
    2565                     // selection
    2566                     // this._testForSelection(evt); // we do not have shift or ctrl keys yet.
    2567                     if (this.selectingMode) {
    2568                         this._startSelecting(pos);
    2569                         this.triggerEventHandlers(['touchstartselecting', 'startselecting'], [evt]);
    2570                         evt.preventDefault();
    2571                         evt.stopPropagation();
    2572                         this.options.precision.hasPoint = this.options.precision.mouse;
    2573                         return this.touches.length > 0; // don't continue as a normal click
    2574                     }
    2575
    2576                     elements = this.initMoveObject(pos[0], pos[1], evt, 'touch');
    2577                     if (elements.length !== 0) {
    2578                         obj = elements[elements.length - 1];
    2579                         target = {num: i,
    2580                             X: evtTouches[i].screenX,
    2581                             Y: evtTouches[i].screenY,
    2582                             Xprev: NaN,
    2583                             Yprev: NaN,
    2584                             Xstart: [],
    2585                             Ystart: [],
    2586                             Zstart: []
    2587                         };
    2588
    2589                         if (Type.isPoint(obj) ||
    2590                                 obj.elementClass === Const.OBJECT_CLASS_TEXT ||
    2591                                 obj.type === Const.OBJECT_TYPE_TICKS ||
    2592                                 obj.type === Const.OBJECT_TYPE_IMAGE) {
    2593                             // It's a point, so it's single touch, so we just push it to our touches
    2594                             targets = [target];
    2595
    2596                             // For the UNDO/REDO of object moves
    2597                             this.saveStartPos(obj, targets[0]);
    2598
    2599                             this.touches.push({ obj: obj, targets: targets });
    2600                             obj.highlight(true);
    2601
    2602                         } else if (obj.elementClass === Const.OBJECT_CLASS_LINE ||
    2603                                 obj.elementClass === Const.OBJECT_CLASS_CIRCLE ||
    2604                                 obj.elementClass === Const.OBJECT_CLASS_CURVE ||
    2605                                 obj.type === Const.OBJECT_TYPE_POLYGON) {
    2606                             found = false;
    2607
    2608                             // first check if this geometric object is already captured in this.touches
    2609                             for (j = 0; j < this.touches.length; j++) {
    2610                                 if (obj.id === this.touches[j].obj.id) {
    2611                                     found = true;
    2612                                     // only add it, if we don't have two targets in there already
    2613                                     if (this.touches[j].targets.length === 1) {
    2614                                         // For the UNDO/REDO of object moves
    2615                                         this.saveStartPos(obj, target);
    2616                                         this.touches[j].targets.push(target);
    2617                                     }
    2618
    2619                                     evtTouches[i].jxg_isused = true;
    2620                                 }
    2621                             }
    2622
    2623                             // we couldn't find it in touches, so we just init a new touches
    2624                             // IF there is a second touch targetting this line, we will find it later on, and then add it to
    2625                             // the touches control object.
    2626                             if (!found) {
    2627                                 targets = [target];
    2628
    2629                                 // For the UNDO/REDO of object moves
    2630                                 this.saveStartPos(obj, targets[0]);
    2631                                 this.touches.push({ obj: obj, targets: targets });
    2632                                 obj.highlight(true);
    2633                             }
    2634                         }
    2635                     }
    2636
    2637                     evtTouches[i].jxg_isused = true;
    2638                 }
    2639             }
    2640
    2641             if (this.touches.length > 0) {
    2642                 evt.preventDefault();
    2643                 evt.stopPropagation();
    2644             }
    2645
    2646             // Touch events on empty areas of the board are handled here:
    2647             // 1. case: one finger. If allowed, this triggers pan with one finger
    2648             if (evtTouches.length === 1 && this.mode === this.BOARD_MODE_NONE && this.touchStartMoveOriginOneFinger(evt)) {
    2649             } else if (evtTouches.length === 2 &&
    2650                         (this.mode === this.BOARD_MODE_NONE || this.mode === this.BOARD_MODE_MOVE_ORIGIN)
    2651                     ) {
    2652                 // 2. case: two fingers: pinch to zoom or pan with two fingers needed.
    2653                 // This happens when the second finger hits the device. First, the
    2654                 // "one finger pan mode" has to be cancelled.
    2655                 if (this.mode === this.BOARD_MODE_MOVE_ORIGIN) {
    2656                     this.originMoveEnd();
    2657                 }
    2658                 this.gestureStartListener(evt);
    2659             }
    2660
    2661             this.options.precision.hasPoint = this.options.precision.mouse;
    2662             this.triggerEventHandlers(['touchstart', 'down'], [evt]);
    2663
    2664             return false;
    2665             //return this.touches.length > 0;
    2666         },
    2667
    2668         /**
    2669          * Called periodically by the browser while the user moves his fingers across the device.
    2670          * @param {Event} evt
    2671          * @returns {Boolean}
    2672          */
    2673         touchMoveListener: function (evt) {
    2674             var i, pos1, pos2,
    2675                 touchTargets,
    2676                 evtTouches = evt[JXG.touchProperty];
    2677
    2678             if (!this.checkFrameRate(evt)) {
    2679                 return false;
    2680             }
    2681
    2682             if (this.mode !== this.BOARD_MODE_NONE) {
    2683                 evt.preventDefault();
    2684                 evt.stopPropagation();
    2685             }
    2686
    2687             if (this.mode !== this.BOARD_MODE_DRAG) {
    2688                 this.dehighlightAll();
    2689                 this.displayInfobox(false);
    2690             }
    2691
    2692             this._inputDevice = 'touch';
    2693             this.options.precision.hasPoint = this.options.precision.touch;
    2694             this.updateQuality = this.BOARD_QUALITY_LOW;
    2695
    2696             // selection
    2697             if (this.selectingMode) {
    2698                 for (i = 0; i < evtTouches.length; i++) {
    2699                     if (!evtTouches[i].jxg_isused) {
    2700                         pos1 = this.getMousePosition(evt, i);
    2701                         this._moveSelecting(pos1);
    2702                         this.triggerEventHandlers(['touchmoves', 'moveselecting'], [evt, this.mode]);
    2703                         break;
    2704                     }
    2705                 }
    2706             } else {
    2707                 if (!this.touchOriginMove(evt)) {
    2708                     if (this.mode === this.BOARD_MODE_DRAG) {
    2709                         // Runs over through all elements which are touched
    2710                         // by at least one finger.
    2711                         for (i = 0; i < this.touches.length; i++) {
    2712                             touchTargets = this.touches[i].targets;
    2713                             if (touchTargets.length === 1) {
    2714
    2715
    2716                                 // Touch by one finger:  this is possible for all elements that can be dragged
    2717                                 if (evtTouches[touchTargets[0].num]) {
    2718                                     pos1 = this.getMousePosition(evt, touchTargets[0].num);
    2719                                     if (pos1[0] < 0 || pos1[0] > this.canvasWidth ||
    2720                                         pos1[1] < 0 || pos1[1] > this.canvasHeight) {
    2721                                         return;
    2722                                     }
    2723                                     touchTargets[0].X = pos1[0];
    2724                                     touchTargets[0].Y = pos1[1];
    2725                                     this.moveObject(pos1[0], pos1[1], this.touches[i], evt, 'touch');
    2726                                 }
    2727
    2728                             } else if (touchTargets.length === 2 &&
    2729                                 touchTargets[0].num > -1 &&
    2730                                 touchTargets[1].num > -1) {
    2731
    2732                                 // Touch by two fingers: moving lines, ...
    2733                                 if (evtTouches[touchTargets[0].num] &&
    2734                                     evtTouches[touchTargets[1].num]) {
    2735
    2736                                     // Get coordinates of the two touches
    2737                                     pos1 = this.getMousePosition(evt, touchTargets[0].num);
    2738                                     pos2 = this.getMousePosition(evt, touchTargets[1].num);
    2739                                     if (pos1[0] < 0 || pos1[0] > this.canvasWidth ||
    2740                                         pos1[1] < 0 || pos1[1] > this.canvasHeight ||
    2741                                         pos2[0] < 0 || pos2[0] > this.canvasWidth ||
    2742                                         pos2[1] < 0 || pos2[1] > this.canvasHeight) {
    2743                                         return;
    2744                                     }
    2745
    2746                                     touchTargets[0].X = pos1[0];
    2747                                     touchTargets[0].Y = pos1[1];
    2748                                     touchTargets[1].X = pos2[0];
    2749                                     touchTargets[1].Y = pos2[1];
    2750
    2751                                     this.twoFingerMove(this.touches[i], touchTargets[0].num, evt);
    2752                                     this.twoFingerMove(this.touches[i], touchTargets[1].num);
    2753
    2754                                     touchTargets[0].Xprev = pos1[0];
    2755                                     touchTargets[0].Yprev = pos1[1];
    2756                                     touchTargets[1].Xprev = pos2[0];
    2757                                     touchTargets[1].Yprev = pos2[1];
    2758                                 }
    2759                             }
    2760                         }
    2761                     } else {
    2762                         if (evtTouches.length === 2) {
    2763                             this.gestureChangeListener(evt);
    2764                         }
    2765                         // Move event without dragging an element
    2766                         pos1 = this.getMousePosition(evt, 0);
    2767                         this.highlightElements(pos1[0], pos1[1], evt, -1);
    2768                     }
    2769                 }
    2770             }
    2771
    2772             if (this.mode !== this.BOARD_MODE_DRAG) {
    2773                 this.displayInfobox(false);
    2774             }
    2775
    2776             this.triggerEventHandlers(['touchmove', 'move'], [evt, this.mode]);
    2777             this.options.precision.hasPoint = this.options.precision.mouse;
    2778             this.updateQuality = this.BOARD_QUALITY_HIGH;
    2779
    2780             return this.mode === this.BOARD_MODE_NONE;
    2781         },
    2782
    2783         /**
    2784          * Triggered as soon as the user stops touching the device with at least one finger.
    2785          * @param {Event} evt
    2786          * @returns {Boolean}
    2787          */
    2788         touchEndListener: function (evt) {
    2789             var i, j, k,
    2790                 eps = this.options.precision.touch,
    2791                 tmpTouches = [], found, foundNumber,
    2792                 evtTouches = evt && evt[JXG.touchProperty],
    2793                 touchTargets;
    2794
    2795             this.triggerEventHandlers(['touchend', 'up'], [evt]);
    2796             this.displayInfobox(false);
    2797
    2798             // selection
    2799             if (this.selectingMode) {
    2800                 this._stopSelecting(evt);
    2801                 this.triggerEventHandlers(['touchstopselecting', 'stopselecting'], [evt]);
    2802             } else if (evtTouches && evtTouches.length > 0) {
    2803                 for (i = 0; i < this.touches.length; i++) {
    2804                     tmpTouches[i] = this.touches[i];
    2805                 }
    2806                 this.touches.length = 0;
    2807
    2808                 // try to convert the operation, e.g. if a lines is rotated and translated with two fingers and one finger is lifted,
    2809                 // convert the operation to a simple one-finger-translation.
    2810                 // ADDENDUM 11/10/11:
    2811                 // see addendum to touchStartListener from 11/10/11
    2812                 // (1) run through the tmptouches
    2813                 // (2) check the touches.obj, if it is a
    2814                 //     (a) point, try to find the targettouch, if found keep it and mark the targettouch, else drop the touch.
    2815                 //     (b) line with
    2816                 //          (i) one target: try to find it, if found keep it mark the targettouch, else drop the touch.
    2817                 //         (ii) two targets: if none can be found, drop the touch. if one can be found, remove the other target. mark all found targettouches
    2818                 //     (c) circle with [proceed like in line]
    2819
    2820                 // init the targettouches marker
    2821                 for (i = 0; i < evtTouches.length; i++) {
    2822                     evtTouches[i].jxg_isused = false;
    2823                 }
    2824
    2825                 for (i = 0; i < tmpTouches.length; i++) {
    2826                     // could all targets of the current this.touches.obj be assigned to targettouches?
    2827                     found = false;
    2828                     foundNumber = 0;
    2829                     touchTargets = tmpTouches[i].targets;
    2830
    2831                     for (j = 0; j < touchTargets.length; j++) {
    2832                         touchTargets[j].found = false;
    2833                         for (k = 0; k < evtTouches.length; k++) {
    2834                             if (Math.abs(Math.pow(evtTouches[k].screenX - touchTargets[j].X, 2) + Math.pow(evtTouches[k].screenY - touchTargets[j].Y, 2)) < eps * eps) {
    2835                                 touchTargets[j].found = true;
    2836                                 touchTargets[j].num = k;
    2837                                 touchTargets[j].X = evtTouches[k].screenX;
    2838                                 touchTargets[j].Y = evtTouches[k].screenY;
    2839                                 foundNumber += 1;
    2840                                 break;
    2841                             }
    2842                         }
    2843                     }
    2844
    2845                     if (Type.isPoint(tmpTouches[i].obj)) {
    2846                         found = (touchTargets[0] && touchTargets[0].found);
    2847                     } else if (tmpTouches[i].obj.elementClass === Const.OBJECT_CLASS_LINE) {
    2848                         found = (touchTargets[0] && touchTargets[0].found) || (touchTargets[1] && touchTargets[1].found);
    2849                     } else if (tmpTouches[i].obj.elementClass === Const.OBJECT_CLASS_CIRCLE) {
    2850                         found = foundNumber === 1 || foundNumber === 3;
    2851                     }
    2852
    2853                     // if we found this object to be still dragged by the user, add it back to this.touches
    2854                     if (found) {
    2855                         this.touches.push({
    2856                             obj: tmpTouches[i].obj,
    2857                             targets: []
    2858                         });
    2859
    2860                         for (j = 0; j < touchTargets.length; j++) {
    2861                             if (touchTargets[j].found) {
    2862                                 this.touches[this.touches.length - 1].targets.push({
    2863                                     num: touchTargets[j].num,
    2864                                     X: touchTargets[j].screenX,
    2865                                     Y: touchTargets[j].screenY,
    2866                                     Xprev: NaN,
    2867                                     Yprev: NaN,
    2868                                     Xstart: touchTargets[j].Xstart,
    2869                                     Ystart: touchTargets[j].Ystart,
    2870                                     Zstart: touchTargets[j].Zstart
    2871                                 });
    2872                             }
    2873                         }
    2874
    2875                     } else {
    2876                         tmpTouches[i].obj.noHighlight();
    2877                     }
    2878                 }
    2879
    2880             } else {
    2881                 this.touches.length = 0;
    2882             }
    2883
    2884             for (i = this.downObjects.length - 1; i > -1; i--) {
    2885                 found = false;
    2886                 for (j = 0; j < this.touches.length; j++) {
    2887                     if (this.touches[j].obj.id === this.downObjects[i].id) {
    2888                         found = true;
    2889                     }
    2890                 }
    2891                 if (!found) {
    2892                     this.downObjects[i].triggerEventHandlers(['touchup', 'up'], [evt]);
    2893                     this.downObjects[i].snapToGrid();
    2894                     this.downObjects[i].snapToPoints();
    2895                     this.downObjects.splice(i, 1);
    2896                 }
    2897             }
    2898
    2899             if (!evtTouches || evtTouches.length === 0) {
    2900
    2901                 if (this.hasTouchEnd) {
    2902                     Env.removeEvent(this.document, 'touchend', this.touchEndListener, this);
    2903                     this.hasTouchEnd = false;
    2904                 }
    2905
    2906                 this.dehighlightAll();
    2907                 this.updateQuality = this.BOARD_QUALITY_HIGH;
    2908
    2909                 this.originMoveEnd();
    2910                 this.update();
    2911             }
    2912
    2913             return true;
    2914         },
    2915
    2916         /**
    2917          * This method is called by the browser when the mouse button is clicked.
    2918          * @param {Event} evt The browsers event object.
    2919          * @returns {Boolean} True if no element is found under the current mouse pointer, false otherwise.
    2920          */
    2921         mouseDownListener: function (evt) {
    2922             var pos, elements, result;
    2923
    2924             // prevent accidental selection of text
    2925             if (this.document.selection && Type.isFunction(this.document.selection.empty)) {
    2926                 this.document.selection.empty();
    2927             } else if (window.getSelection) {
    2928                 window.getSelection().removeAllRanges();
    2929             }
    2930
    2931             if (!this.hasMouseUp) {
    2932                 Env.addEvent(this.document, 'mouseup', this.mouseUpListener, this);
    2933                 this.hasMouseUp = true;
    2934             } else {
    2935                 // In case this.hasMouseUp==true, it may be that there was a
    2936                 // mousedown event before which was not followed by an mouseup event.
    2937                 // This seems to happen with interactive whiteboard pens sometimes.
    2938                 return;
    2939             }
    2940
    2941             this._inputDevice = 'mouse';
    2942             this.options.precision.hasPoint = this.options.precision.mouse;
    2943             pos = this.getMousePosition(evt);
    2944
    2945             // selection
    2946             this._testForSelection(evt);
    2947             if (this.selectingMode) {
    2948                 this._startSelecting(pos);
    2949                 this.triggerEventHandlers(['mousestartselecting', 'startselecting'], [evt]);
    2950                 return;     // don't continue as a normal click
    2951             }
    2952
    2953             elements = this.initMoveObject(pos[0], pos[1], evt, 'mouse');
    2954
    2955             // if no draggable object can be found, get out here immediately
    2956             if (elements.length === 0) {
    2957                 this.mode = this.BOARD_MODE_NONE;
    2958                 result = true;
    2959             } else {
    2960                 /** @ignore */
    2961                 this.mouse = {
    2962                     obj: null,
    2963                     targets: [{
    2964                         X: pos[0],
    2965                         Y: pos[1],
    2966                         Xprev: NaN,
    2967                         Yprev: NaN
    2968                     }]
    2969                 };
    2970                 this.mouse.obj = elements[elements.length - 1];
    2971
    2972                 this.dehighlightAll();
    2973                 this.mouse.obj.highlight(true);
    2974
    2975                 this.mouse.targets[0].Xstart = [];
    2976                 this.mouse.targets[0].Ystart = [];
    2977                 this.mouse.targets[0].Zstart = [];
    2978
    2979                 this.saveStartPos(this.mouse.obj, this.mouse.targets[0]);
    2980
    2981                 // prevent accidental text selection
    2982                 // this could get us new trouble: input fields, links and drop down boxes placed as text
    2983                 // on the board don't work anymore.
    2984                 if (evt && evt.preventDefault) {
    2985                     evt.preventDefault();
    2986                 } else if (window.event) {
    2987                     window.event.returnValue = false;
    2988                 }
    2989             }
    2990
    2991             if (this.mode === this.BOARD_MODE_NONE) {
    2992                 result = this.mouseOriginMoveStart(evt);
    2993             }
    2994
    2995             this.triggerEventHandlers(['mousedown', 'down'], [evt]);
    2996
    2997             return result;
    2998         },
    2999
    3000         /**
    3001          * This method is called by the browser when the mouse is moved.
    3002          * @param {Event} evt The browsers event object.
    3003          */
    3004         mouseMoveListener: function (evt) {
    3005             var pos;
    3006
    3007             if (!this.checkFrameRate(evt)) {
    3008                 return false;
    3009             }
    3010
    3011             pos = this.getMousePosition(evt);
    3012
    3013             this.updateQuality = this.BOARD_QUALITY_LOW;
    3014
    3015             if (this.mode !== this.BOARD_MODE_DRAG) {
    3016                 this.dehighlightAll();
    3017                 this.displayInfobox(false);
    3018             }
    3019
    3020             // we have to check for four cases:
    3021             //   * user moves origin
    3022             //   * user drags an object
    3023             //   * user just moves the mouse, here highlight all elements at
    3024             //     the current mouse position
    3025             //   * the user is selecting
    3026
    3027             // selection
    3028             if (this.selectingMode) {
    3029                 this._moveSelecting(pos);
    3030                 this.triggerEventHandlers(['mousemoveselecting', 'moveselecting'], [evt, this.mode]);
    3031             } else if (!this.mouseOriginMove(evt)) {
    3032                 if (this.mode === this.BOARD_MODE_DRAG) {
    3033                     this.moveObject(pos[0], pos[1], this.mouse, evt, 'mouse');
    3034                 } else { // BOARD_MODE_NONE
    3035                     // Move event without dragging an element
    3036                     this.highlightElements(pos[0], pos[1], evt, -1);
    3037                 }
    3038                 this.triggerEventHandlers(['mousemove', 'move'], [evt, this.mode]);
    3039             }
    3040             this.updateQuality = this.BOARD_QUALITY_HIGH;
    3041         },
    3042
    3043         /**
    3044          * This method is called by the browser when the mouse button is released.
    3045          * @param {Event} evt
    3046          */
    3047         mouseUpListener: function (evt) {
    3048             var i;
    3049
    3050             if (this.selectingMode === false) {
    3051                 this.triggerEventHandlers(['mouseup', 'up'], [evt]);
    3052             }
    3053
    3054             // redraw with high precision
    3055             this.updateQuality = this.BOARD_QUALITY_HIGH;
    3056
    3057             if (this.mouse && this.mouse.obj) {
    3058                 // The parameter is needed for lines with snapToGrid enabled
    3059                 this.mouse.obj.snapToGrid(this.mouse.targets[0]);
    3060                 this.mouse.obj.snapToPoints();
    3061             }
    3062
    3063             this.originMoveEnd();
    3064             this.dehighlightAll();
    3065             this.update();
    3066
    3067             // selection
    3068             if (this.selectingMode) {
    3069                 this._stopSelecting(evt);
    3070                 this.triggerEventHandlers(['mousestopselecting', 'stopselecting'], [evt]);
    3071             } else {
    3072                 for (i = 0; i < this.downObjects.length; i++) {
    3073                     this.downObjects[i].triggerEventHandlers(['mouseup', 'up'], [evt]);
    3074                 }
    3075             }
    3076
    3077             this.downObjects.length = 0;
    3078
    3079             if (this.hasMouseUp) {
    3080                 Env.removeEvent(this.document, 'mouseup', this.mouseUpListener, this);
    3081                 this.hasMouseUp = false;
    3082             }
    3083
    3084             // release dragged mouse object
    3085             /** @ignore */
    3086             this.mouse = null;
    3087         },
    3088
    3089         /**
    3090          * Handler for mouse wheel events. Used to zoom in and out of the board.
    3091          * @param {Event} evt
    3092          * @returns {Boolean}
    3093          */
    3094         mouseWheelListener: function (evt) {
    3095             if (!this.attr.zoom.wheel || !this._isRequiredKeyPressed(evt, 'zoom')) {
    3096                 return true;
    3097             }
    3098
    3099             evt = evt || window.event;
    3100             var wd = evt.detail ? -evt.detail : evt.wheelDelta / 40,
    3101                 pos = new Coords(Const.COORDS_BY_SCREEN, this.getMousePosition(evt), this);
    3102
    3103             if (wd > 0) {
    3104                 this.zoomIn(pos.usrCoords[1], pos.usrCoords[2]);
    3105             } else {
    3106                 this.zoomOut(pos.usrCoords[1], pos.usrCoords[2]);
    3107             }
    3108
    3109             this.triggerEventHandlers(['mousewheel'], [evt]);
    3110
    3111             evt.preventDefault();
    3112             return false;
    3113         },
    3114
    3115         /**
    3116          * Allow moving of JSXGraph elements with arrow keys
    3117          * and zooming of the construction with + / -.
    3118          * Panning of the construction is done with arrow keys
    3119          * if the pan key (shift or ctrl) is pressed.
    3120          * The selection of the element is done with the tab key.
    3121          *
    3122          * @param  {Event} evt The browser's event object
    3123          *
    3124          * @see JXG.Board#keyboard
    3125          * @see JXG.Board#keyFocusInListener
    3126          * @see JXG.Board#keyFocusOutListener
    3127          *
    3128          */
    3129         keyDownListener: function (evt) {
    3130             var id_node = evt.target.id,
    3131                 id, el, res,
    3132                 sX = 0,
    3133                 sY = 0,
    3134                 // dx, dy are provided in screen units and
    3135                 // are converted to user coordinates
    3136                 dx = Type.evaluate(this.attr.keyboard.dx) / this.unitX,
    3137                 dy = Type.evaluate(this.attr.keyboard.dy) / this.unitY,
    3138                 doZoom = false,
    3139                 done = true,
    3140                 dir, actPos;
    3141
    3142             if (!this.attr.keyboard.enabled || id_node === '') {
    3143                 return false;
    3144             }
    3145
    3146             // Get the JSXGraph id from the id of the SVG node.
    3147             id = id_node.replace(this.containerObj.id + '_', '');
    3148             el = this.select(id);
    3149
    3150             if (Type.exists(el.coords)) {
    3151                 actPos = el.coords.usrCoords.slice(1);
    3152             }
    3153
    3154             if (Type.evaluate(this.attr.keyboard.panshift) || Type.evaluate(this.attr.keyboard.panctrl)) {
    3155                 doZoom = true;
    3156             }
    3157
    3158             if ((Type.evaluate(this.attr.keyboard.panshift) && evt.shiftKey) ||
    3159                 (Type.evaluate(this.attr.keyboard.panctrl) && evt.ctrlKey)) {
    3160                 if (evt.keyCode === 38) {           // up
    3161                     this.clickUpArrow();
    3162                 } else if (evt.keyCode === 40) {    // down
    3163                     this.clickDownArrow();
    3164                 } else if (evt.keyCode === 37) {    // left
    3165                     this.clickLeftArrow();
    3166                 } else if (evt.keyCode === 39) {    // right
    3167                     this.clickRightArrow();
    3168                 } else {
    3169                     done = false;
    3170                 }
    3171             } else {
    3172                 // Adapt dx, dy to snapToGrid and attractToGrid
    3173                 // snapToGrid has priority.
    3174                 if (Type.exists(el.visProp)) {
    3175                     if (Type.exists(el.visProp.snaptogrid) &&
    3176                         el.visProp.snaptogrid &&
    3177                         Type.evaluate(el.visProp.snapsizex) &&
    3178                         Type.evaluate(el.visProp.snapsizey)) {
    3179
    3180                         // Adapt dx, dy such that snapToGrid is possible
    3181                         res = el.getSnapSizes();
    3182                         sX = res[0];
    3183                         sY = res[1];
    3184                         dx = Math.max(sX, dx);
    3185                         dy = Math.max(sY, dy);
    3186
    3187                     } else if (Type.exists(el.visProp.attracttogrid) &&
    3188                         el.visProp.attracttogrid &&
    3189                         Type.evaluate(el.visProp.attractordistance) &&
    3190                         Type.evaluate(el.visProp.attractorunit)) {
    3191
    3192                         // Adapt dx, dy such that attractToGrid is possible
    3193                         sX = 1.1 * Type.evaluate(el.visProp.attractordistance);
    3194                         sY = sX;
    3195
    3196                         if (Type.evaluate(el.visProp.attractorunit) === 'screen') {
    3197                             sX /= this.unitX;
    3198                             sY /= this.unitX;
    3199                         }
    3200                         dx = Math.max(sX, dx);
    3201                         dy = Math.max(sY, dy);
    3202                     }
    3203
    3204                 }
    3205
    3206                 if (evt.keyCode === 38) {           // up
    3207                     dir = [0, dy];
    3208                 } else if (evt.keyCode === 40) {    // down
    3209                     dir = [0, -dy];
    3210                 } else if (evt.keyCode === 37) {    // left
    3211                     dir = [-dx, 0];
    3212                 } else if (evt.keyCode === 39) {    // right
    3213                     dir = [dx, 0];
    3214                 // } else if (evt.keyCode === 9) {  // tab
    3215
    3216                 } else if (doZoom && evt.key === '+') {   // +
    3217                     this.zoomIn();
    3218                 } else if (doZoom && evt.key === '-') {   // -
    3219                     this.zoomOut();
    3220                 } else if (doZoom && evt.key === 'o') {   // o
    3221                     this.zoom100();
    3222                 } else {
    3223                     done = false;
    3224                 }
    3225
    3226                 if (dir && el.isDraggable &&
    3227                         el.visPropCalc.visible &&
    3228                         ((this.geonextCompatibilityMode &&
    3229                             (Type.isPoint(el) ||
    3230                             el.elementClass === Const.OBJECT_CLASS_TEXT)
    3231                         ) || !this.geonextCompatibilityMode) &&
    3232                         !Type.evaluate(el.visProp.fixed)
    3233                     ) {
    3234
    3235                     if (Type.exists(el.coords)) {
    3236                         dir[0] += actPos[0];
    3237                         dir[1] += actPos[1];
    3238                     }
    3239                     // For coordsElement setPosition has to call setPositionDirectly.
    3240                     // Otherwise the position is set by a translation.
    3241                     el.setPosition(JXG.COORDS_BY_USER, dir);
    3242                     if (Type.exists(el.coords)) {
    3243                         this.updateInfobox(el);
    3244                     }
    3245                     this.triggerEventHandlers(['hit'], [evt, el]);
    3246                 }
    3247             }
    3248
    3249             this.update();
    3250
    3251             if (done) {
    3252                 evt.preventDefault();
    3253             }
    3254             return true;
    3255         },
    3256
    3257         /**
    3258          * Event listener for SVG elements getting focus.
    3259          * This is needed for highlighting when using keyboard control.
    3260          *
    3261          * @see JXG.Board#keyFocusOutListener
    3262          * @see JXG.Board#keyDownListener
    3263          * @see JXG.Board#keyboard
    3264          *
    3265          * @param  {Event} evt The browser's event object
    3266          */
    3267         keyFocusInListener: function (evt) {
    3268             var id_node = evt.target.id,
    3269                 id, el;
    3270
    3271             if (!this.attr.keyboard.enabled || id_node === '') {
    3272                 return false;
    3273             }
    3274
    3275             id = id_node.replace(this.containerObj.id + '_', '');
    3276             el = this.select(id);
    3277             if (Type.exists(el.highlight)) {
    3278                 el.highlight(true);
    3279             }
    3280             if (Type.exists(el.coords)) {
    3281                 this.updateInfobox(el);
    3282             }
    3283             this.triggerEventHandlers(['hit'], [evt, el]);
    3284         },
    3285
    3286         /**
    3287          * Event listener for SVG elements losing focus.
    3288          * This is needed for dehighlighting when using keyboard control.
    3289          *
    3290          * @see JXG.Board#keyFocusInListener
    3291          * @see JXG.Board#keyDownListener
    3292          * @see JXG.Board#keyboard
    3293          *
    3294          * @param  {Event} evt The browser's event object
    3295          */
    3296         keyFocusOutListener: function (evt) {
    3297             if (!this.attr.keyboard.enabled) {
    3298                 return false;
    3299             }
    3300             // var id_node = evt.target.id,
    3301             //     id, el;
    3302
    3303             // id = id_node.replace(this.containerObj.id + '_', '');
    3304             // el = this.select(id);
    3305             this.dehighlightAll();
    3306             this.displayInfobox(false);
    3307         },
    3308
    3309         /**
    3310          * Update the width and height of the JSXGraph container div element.
    3311          * Read actual values with getBoundingClientRect(),
    3312          * and call board.resizeContainer() with this values.
    3313          * <p>
    3314          * If necessary, also call setBoundingBox().
    3315          *
    3316          * @see JXG.Board#startResizeObserver
    3317          * @see JXG.Board#resizeListener
    3318          * @see JXG.Board#resizeContainer
    3319          * @see JXG.Board#setBoundingBox
    3320          *
    3321          */
    3322         updateContainerDims: function() {
    3323             var w, h,
    3324                 bb, css;
    3325
    3326             // Get size of the board's container div
    3327             bb = this.containerObj.getBoundingClientRect();
    3328             w = bb.width;
    3329             h = bb.height;
    3330
    3331             // Subtract the border size
    3332             if (window && window.getComputedStyle) {
    3333                 css = window.getComputedStyle(this.containerObj, null);
    3334                 w -= parseFloat(css.getPropertyValue('border-left-width')) + parseFloat(css.getPropertyValue('border-right-width'));
    3335                 h -= parseFloat(css.getPropertyValue('border-top-width'))  + parseFloat(css.getPropertyValue('border-bottom-width'));
    3336             }
    3337
    3338             // If div is invisible - do nothing
    3339             if (w <= 0 || h <= 0) {
    3340                 return;
    3341             }
    3342
    3343             // If bounding box is not yet initialized, do it now.
    3344             if (isNaN(this.getBoundingBox()[0])) {
    3345                 this.setBoundingBox(this.attr.boundingbox, this.keepaspectratio, 'keep');
    3346             }
    3347
    3348             // Do nothing if the dimension did not change since being visible
    3349             // the last time. Note that if the div had display:none in the mean time,
    3350             // we did not store this._prevDim.
    3351             if (Type.exists(this._prevDim) &&
    3352                 this._prevDim.w === w && this._prevDim.h === h) {
    3353                     return;
    3354             }
    3355
    3356             // Set the size of the SVG or canvas element
    3357             this.resizeContainer(w, h, true);
    3358             this._prevDim = {
    3359                 w: w,
    3360                 h: h
    3361             };
    3362         },
    3363
    3364         /**
    3365          * Start observer which reacts to size changes of the JSXGraph
    3366          * container div element. Calls updateContainerDims().
    3367          * If not available, an event listener for the window-resize event is started.
    3368          * On mobile devices also scrolling might trigger resizes.
    3369          * However, resize events triggered by scrolling events should be ignored.
    3370          * Therefore, also a scrollListener is started.
    3371          * Resize can be controlled with the board attribute resize.
    3372          *
    3373          * @see JXG.Board#updateContainerDims
    3374          * @see JXG.Board#resizeListener
    3375          * @see JXG.Board#scrollListener
    3376          * @see JXG.Board#resize
    3377          *
    3378          */
    3379         startResizeObserver: function() {
    3380             var that = this;
    3381
    3382             if (!Env.isBrowser || !this.attr.resize || !this.attr.resize.enabled) {
    3383                 return;
    3384             }
    3385
    3386             this.resizeObserver = new ResizeObserver(function(entries) {
    3387                 if (!that._isResizing) {
    3388                     that._isResizing = true;
    3389                     window.setTimeout(function() {
    3390                         try {
    3391                             that.updateContainerDims();
    3392                         } catch (err) {
    3393                             that.stopResizeObserver();
    3394                         } finally {
    3395                             that._isResizing = false;
    3396                         }
    3397                     }, that.attr.resize.throttle);
    3398                 }
    3399             });
    3400             this.resizeObserver.observe(this.containerObj);
    3401         },
    3402
    3403         /**
    3404          * Stops the resize observer.
    3405          * @see JXG.Board#startResizeObserver
    3406          *
    3407          */
    3408         stopResizeObserver: function() {
    3409             if (!Env.isBrowser || !this.attr.resize || !this.attr.resize.enabled) {
    3410                 return;
    3411             }
    3412
    3413             if (Type.exists(this.resizeObserver)) {
    3414                 this.resizeObserver.unobserve(this.containerObj);
    3415             }
    3416         },
    3417
    3418         /**
    3419          * Fallback solutions if there is no resizeObserver available in the browser.
    3420          * Reacts to resize events of the window (only). Otherwise similar to
    3421          * startResizeObserver(). To handle changes of the visibility
    3422          * of the JSXGraph container element, additionally an intersection observer is used.
    3423          * which watches changes in the visibility of the JSXGraph container element.
    3424          * This is necessary e.g. for register tabs or dia shows.
    3425          *
    3426          * @see JXG.Board#startResizeObserver
    3427          * @see JXG.Board#startIntersectionObserver
    3428          */
    3429         resizeListener: function() {
    3430             var that = this;
    3431
    3432             if (!Env.isBrowser || !this.attr.resize || !this.attr.resize.enabled) {
    3433                 return;
    3434             }
    3435             if (!this._isScrolling && !this._isResizing) {
    3436                 this._isResizing = true;
    3437                 window.setTimeout(function() {
    3438                     that.updateContainerDims();
    3439                     that._isResizing = false;
    3440                 }, this.attr.resize.throttle);
    3441             }
    3442         },
    3443
    3444         /**
    3445          * Listener to watch for scroll events. Sets board._isScrolling = true
    3446          * @param  {Event} evt The browser's event object
    3447          *
    3448          * @see JXG.Board#startResizeObserver
    3449          * @see JXG.Board#resizeListener
    3450          *
    3451          */
    3452         scrollListener: function(evt) {
    3453             var that = this;
    3454
    3455             if (!Env.isBrowser) {
    3456                 return;
    3457             }
    3458             if (!this._isScrolling) {
    3459                 this._isScrolling = true;
    3460                 window.setTimeout(function() {
    3461                     that._isScrolling = false;
    3462                 }, 66);
    3463             }
    3464         },
    3465
    3466         /**
    3467          * Watch for changes of the visibility of the JSXGraph container element.
    3468          *
    3469          * @see JXG.Board#startResizeObserver
    3470          * @see JXG.Board#resizeListener
    3471          *
    3472          */
    3473         startIntersectionObserver: function() {
    3474             var that = this,
    3475                 options = {
    3476                     root: null,
    3477                     rootMargin: '0px',
    3478                     threshold: 0.8
    3479                 };
    3480
    3481             try {
    3482                 this.intersectionObserver = new IntersectionObserver(function(entries) {
    3483                     // If bounding box is not yet initialized, do it now.
    3484                     if (isNaN(that.getBoundingBox()[0])) {
    3485                         that.updateContainerDims();
    3486                     }
    3487                 }, options);
    3488                 this.intersectionObserver.observe(that.containerObj);
    3489             } catch (err) {
    3490                 console.log('JSXGraph: IntersectionObserver not available in this browser.');
    3491             }
    3492         },
    3493
    3494         /**
    3495          * Stop the intersection observer
    3496          *
    3497          * @see JXG.Board#startIntersectionObserver
    3498          *
    3499          */
    3500         stopIntersectionObserver: function() {
    3501             if (Type.exists(this.intersectionObserver)) {
    3502                 this.intersectionObserver.unobserve(this.containerObj);
    3503             }
    3504         },
    3505
    3506         /**********************************************************
    3507          *
    3508          * End of Event Handlers
    3509          *
    3510          **********************************************************/
    3511
    3512         /**
    3513          * Initialize the info box object which is used to display
    3514          * the coordinates of points near the mouse pointer,
    3515          * @returns {JXG.Board} Reference to the board
    3516         */
    3517         initInfobox: function () {
    3518             var  attr = Type.copyAttributes({}, this.options, 'infobox');
    3519
    3520             attr.id = this.id + '_infobox';
    3521             /**
    3522              * Infobox close to points in which the points' coordinates are displayed.
    3523              * This is simply a JXG.Text element. Access through board.infobox.
    3524              * Uses CSS class .JXGinfobox.
    3525              * @type JXG.Text
    3526              *
    3527              */
    3528             this.infobox = this.create('text', [0, 0, '0,0'], attr);
    3529
    3530             this.infobox.distanceX = -20;
    3531             this.infobox.distanceY = 25;
    3532             // this.infobox.needsUpdateSize = false;  // That is not true, but it speeds drawing up.
    3533
    3534             this.infobox.dump = false;
    3535
    3536             this.displayInfobox(false);
    3537             return this;
    3538         },
    3539
    3540         /**
    3541          * Updates and displays a little info box to show coordinates of current selected points.
    3542          * @param {JXG.GeometryElement} el A GeometryElement
    3543          * @returns {JXG.Board} Reference to the board
    3544          * @see JXG.Board#displayInfobox
    3545          * @see JXG.Board#showInfobox
    3546          * @see Point#showInfobox
    3547          *
    3548          */
    3549         updateInfobox: function (el) {
    3550             var x, y, xc, yc,
    3551             vpinfoboxdigits,
    3552             vpsi = Type.evaluate(el.visProp.showinfobox);
    3553
    3554             if ((!Type.evaluate(this.attr.showinfobox) &&  vpsi === 'inherit') ||
    3555                 !vpsi) {
    3556                 return this;
    3557             }
    3558
    3559             if (Type.isPoint(el)) {
    3560                 xc = el.coords.usrCoords[1];
    3561                 yc = el.coords.usrCoords[2];
    3562
    3563                 vpinfoboxdigits = Type.evaluate(el.visProp.infoboxdigits);
    3564                 this.infobox.setCoords(xc + this.infobox.distanceX / this.unitX,
    3565                                        yc + this.infobox.distanceY / this.unitY);
    3566
    3567                 if (typeof el.infoboxText !== 'string') {
    3568                     if (vpinfoboxdigits === 'auto') {
    3569                         x = Type.autoDigits(xc);
    3570                         y = Type.autoDigits(yc);
    3571                     } else if (Type.isNumber(vpinfoboxdigits)) {
    3572                         x = Type.toFixed(xc, vpinfoboxdigits);
    3573                         y = Type.toFixed(yc, vpinfoboxdigits);
    3574                     } else {
    3575                         x = xc;
    3576                         y = yc;
    3577                     }
    3578
    3579                     this.highlightInfobox(x, y, el);
    3580                 } else {
    3581                     this.highlightCustomInfobox(el.infoboxText, el);
    3582                 }
    3583
    3584                 this.displayInfobox(true);
    3585             }
    3586             return this;
    3587         },
    3588
    3589         /**
    3590          * Set infobox visible / invisible.
    3591          *
    3592          * It uses its property hiddenByParent to memorize its status.
    3593          * In this way, many DOM access can be avoided.
    3594          *
    3595          * @param  {Boolean} val true for visible, false for invisible
    3596          * @returns {JXG.Board} Reference to the board.
    3597          * @see JXG.Board#updateInfobox
    3598          *
    3599          */
    3600         displayInfobox: function(val) {
    3601             if (this.infobox.hiddenByParent === val) {
    3602                 this.infobox.hiddenByParent = !val;
    3603                 this.infobox.prepareUpdate().updateVisibility(val).updateRenderer();
    3604             }
    3605             return this;
    3606         },
    3607
    3608         // Alias for displayInfobox to be backwards compatible.
    3609         // The method showInfobox clashes with the board attribute showInfobox
    3610         showInfobox: function(val) {
    3611             return this.displayInfobox(val);
    3612         },
    3613
    3614         /**
    3615          * Changes the text of the info box to show the given coordinates.
    3616          * @param {Number} x
    3617          * @param {Number} y
    3618          * @param {JXG.GeometryElement} [el] The element the mouse is pointing at
    3619          * @returns {JXG.Board} Reference to the board.
    3620          */
    3621         highlightInfobox: function (x, y, el) {
    3622             this.highlightCustomInfobox('(' + x + ', ' + y + ')', el);
    3623             return this;
    3624         },
    3625
    3626         /**
    3627          * Changes the text of the info box to what is provided via text.
    3628          * @param {String} text
    3629          * @param {JXG.GeometryElement} [el]
    3630          * @returns {JXG.Board} Reference to the board.
    3631          */
    3632         highlightCustomInfobox: function (text, el) {
    3633             this.infobox.setText(text);
    3634             return this;
    3635         },
    3636
    3637         /**
    3638          * Remove highlighting of all elements.
    3639          * @returns {JXG.Board} Reference to the board.
    3640          */
    3641         dehighlightAll: function () {
    3642             var el, pEl, needsDehighlight = false;
    3643
    3644             for (el in this.highlightedObjects) {
    3645                 if (this.highlightedObjects.hasOwnProperty(el)) {
    3646                     pEl = this.highlightedObjects[el];
    3647
    3648                     if (this.hasMouseHandlers || this.hasPointerHandlers) {
    3649                         pEl.noHighlight();
    3650                     }
    3651
    3652                     needsDehighlight = true;
    3653
    3654                     // In highlightedObjects should only be objects which fulfill all these conditions
    3655                     // And in case of complex elements, like a turtle based fractal, it should be faster to
    3656                     // just de-highlight the element instead of checking hasPoint...
    3657                     // if ((!Type.exists(pEl.hasPoint)) || !pEl.hasPoint(x, y) || !pEl.visPropCalc.visible)
    3658                 }
    3659             }
    3660
    3661             this.highlightedObjects = {};
    3662
    3663             // We do not need to redraw during dehighlighting in CanvasRenderer
    3664             // because we are redrawing anyhow
    3665             //  -- We do need to redraw during dehighlighting. Otherwise objects won't be dehighlighted until
    3666             // another object is highlighted.
    3667             if (this.renderer.type === 'canvas' && needsDehighlight) {
    3668                 this.prepareUpdate();
    3669                 this.renderer.suspendRedraw(this);
    3670                 this.updateRenderer();
    3671                 this.renderer.unsuspendRedraw();
    3672             }
    3673
    3674             return this;
    3675         },
    3676
    3677         /**
    3678          * Returns the input parameters in an array. This method looks pointless and it really is, but it had a purpose
    3679          * once.
    3680          * @private
    3681          * @param {Number} x X coordinate in screen coordinates
    3682          * @param {Number} y Y coordinate in screen coordinates
    3683          * @returns {Array} Coordinates [x, y] of the mouse in screen coordinates.
    3684          * @see JXG.Board#getUsrCoordsOfMouse
    3685          */
    3686         getScrCoordsOfMouse: function (x, y) {
    3687             return [x, y];
    3688         },
    3689
    3690         /**
    3691          * This method calculates the user coords of the current mouse coordinates.
    3692          * @param {Event} evt Event object containing the mouse coordinates.
    3693          * @returns {Array} Coordinates [x, y] of the mouse in user coordinates.
    3694          * @example
    3695          * board.on('up', function (evt) {
    3696          *         var a = board.getUsrCoordsOfMouse(evt),
    3697          *             x = a[0],
    3698          *             y = a[1],
    3699          *             somePoint = board.create('point', [x,y], {name:'SomePoint',size:4});
    3700          *             // Shorter version:
    3701          *             //somePoint = board.create('point', a, {name:'SomePoint',size:4});
    3702          *         });
    3703          *
    3704          * </pre><div id="JXG48d5066b-16ba-4920-b8ea-a4f8eff6b746" class="jxgbox" style="width: 300px; height: 300px;"></div>
    3705          * <script type="text/javascript">
    3706          *     (function() {
    3707          *         var board = JXG.JSXGraph.initBoard('JXG48d5066b-16ba-4920-b8ea-a4f8eff6b746',
    3708          *             {boundingbox: [-8, 8, 8,-8], axis: true, showcopyright: false, shownavigation: false});
    3709          *     board.on('up', function (evt) {
    3710          *             var a = board.getUsrCoordsOfMouse(evt),
    3711          *                 x = a[0],
    3712          *                 y = a[1],
    3713          *                 somePoint = board.create('point', [x,y], {name:'SomePoint',size:4});
    3714          *                 // Shorter version:
    3715          *                 //somePoint = board.create('point', a, {name:'SomePoint',size:4});
    3716          *             });
    3717          *
    3718          *     })();
    3719          *
    3720          * </script><pre>
    3721          *
    3722          * @see JXG.Board#getScrCoordsOfMouse
    3723          * @see JXG.Board#getAllUnderMouse
    3724          */
    3725         getUsrCoordsOfMouse: function (evt) {
    3726             var cPos = this.getCoordsTopLeftCorner(),
    3727                 absPos = Env.getPosition(evt, null, this.document),
    3728                 x = absPos[0] - cPos[0],
    3729                 y = absPos[1] - cPos[1],
    3730                 newCoords = new Coords(Const.COORDS_BY_SCREEN, [x, y], this);
    3731
    3732             return newCoords.usrCoords.slice(1);
    3733         },
    3734
    3735         /**
    3736          * Collects all elements under current mouse position plus current user coordinates of mouse cursor.
    3737          * @param {Event} evt Event object containing the mouse coordinates.
    3738          * @returns {Array} Array of elements at the current mouse position plus current user coordinates of mouse.
    3739          * @see JXG.Board#getUsrCoordsOfMouse
    3740          * @see JXG.Board#getAllObjectsUnderMouse
    3741          */
    3742         getAllUnderMouse: function (evt) {
    3743             var elList = this.getAllObjectsUnderMouse(evt);
    3744             elList.push(this.getUsrCoordsOfMouse(evt));
    3745
    3746             return elList;
    3747         },
    3748
    3749         /**
    3750          * Collects all elements under current mouse position.
    3751          * @param {Event} evt Event object containing the mouse coordinates.
    3752          * @returns {Array} Array of elements at the current mouse position.
    3753          * @see JXG.Board#getAllUnderMouse
    3754          */
    3755         getAllObjectsUnderMouse: function (evt) {
    3756             var cPos = this.getCoordsTopLeftCorner(),
    3757                 absPos = Env.getPosition(evt, null, this.document),
    3758                 dx = absPos[0] - cPos[0],
    3759                 dy = absPos[1] - cPos[1],
    3760                 elList = [],
    3761                 el,
    3762                 pEl,
    3763                 len = this.objectsList.length;
    3764
    3765             for (el = 0; el < len; el++) {
    3766                 pEl = this.objectsList[el];
    3767                 if (pEl.visPropCalc.visible && pEl.hasPoint && pEl.hasPoint(dx, dy)) {
    3768                     elList[elList.length] = pEl;
    3769                 }
    3770             }
    3771
    3772             return elList;
    3773         },
    3774
    3775         /**
    3776          * Update the coords object of all elements which possess this
    3777          * property. This is necessary after changing the viewport.
    3778          * @returns {JXG.Board} Reference to this board.
    3779          **/
    3780         updateCoords: function () {
    3781             var el, ob, len = this.objectsList.length;
    3782
    3783             for (ob = 0; ob < len; ob++) {
    3784                 el = this.objectsList[ob];
    3785
    3786                 if (Type.exists(el.coords)) {
    3787                     if (Type.evaluate(el.visProp.frozen)) {
    3788                         el.coords.screen2usr();
    3789                     } else {
    3790                         el.coords.usr2screen();
    3791                     }
    3792                 }
    3793             }
    3794             return this;
    3795         },
    3796
    3797         /**
    3798          * Moves the origin and initializes an update of all elements.
    3799          * @param {Number} x
    3800          * @param {Number} y
    3801          * @param {Boolean} [diff=false]
    3802          * @returns {JXG.Board} Reference to this board.
    3803          */
    3804         moveOrigin: function (x, y, diff) {
    3805             var ox, oy, ul, lr;
    3806             if (Type.exists(x) && Type.exists(y)) {
    3807                 ox = this.origin.scrCoords[1];
    3808                 oy = this.origin.scrCoords[2];
    3809
    3810                 this.origin.scrCoords[1] = x;
    3811                 this.origin.scrCoords[2] = y;
    3812
    3813                 if (diff) {
    3814                     this.origin.scrCoords[1] -= this.drag_dx;
    3815                     this.origin.scrCoords[2] -= this.drag_dy;
    3816                 }
    3817
    3818                 ul = (new Coords(Const.COORDS_BY_SCREEN, [0, 0], this)).usrCoords;
    3819                 lr = (new Coords(Const.COORDS_BY_SCREEN, [this.canvasWidth, this.canvasHeight], this)).usrCoords;
    3820                 if (ul[1] < this.maxboundingbox[0] ||
    3821                     ul[2] > this.maxboundingbox[1] ||
    3822                     lr[1] > this.maxboundingbox[2] ||
    3823                     lr[2] < this.maxboundingbox[3]) {
    3824
    3825                     this.origin.scrCoords[1] = ox;
    3826                     this.origin.scrCoords[2] = oy;
    3827                 }
    3828             }
    3829
    3830             this.updateCoords().clearTraces().fullUpdate();
    3831             this.triggerEventHandlers(['boundingbox']);
    3832
    3833             return this;
    3834         },
    3835
    3836         /**
    3837          * Add conditional updates to the elements.
    3838          * @param {String} str String containing coniditional update in geonext syntax
    3839          */
    3840         addConditions: function (str) {
    3841             var term, m, left, right, name, el, property,
    3842                 functions = [],
    3843                 // plaintext = 'var el, x, y, c, rgbo;\n',
    3844                 i = str.indexOf('<data>'),
    3845                 j = str.indexOf('<' + '/data>'),
    3846
    3847                 xyFun = function (board, el, f, what) {
    3848                     return function () {
    3849                         var e, t;
    3850
    3851                         e = board.select(el.id);
    3852                         t = e.coords.usrCoords[what];
    3853
    3854                         if (what === 2) {
    3855                             e.setPositionDirectly(Const.COORDS_BY_USER, [f(), t]);
    3856                         } else {
    3857                             e.setPositionDirectly(Const.COORDS_BY_USER, [t, f()]);
    3858                         }
    3859                         e.prepareUpdate().update();
    3860                     };
    3861                 },
    3862
    3863                 visFun = function (board, el, f) {
    3864                     return function () {
    3865                         var e, v;
    3866
    3867                         e = board.select(el.id);
    3868                         v = f();
    3869
    3870                         e.setAttribute({visible: v});
    3871                     };
    3872                 },
    3873
    3874                 colFun = function (board, el, f, what) {
    3875                     return function () {
    3876                         var e, v;
    3877
    3878                         e = board.select(el.id);
    3879                         v = f();
    3880
    3881                         if (what === 'strokewidth') {
    3882                             e.visProp.strokewidth = v;
    3883                         } else {
    3884                             v = Color.rgba2rgbo(v);
    3885                             e.visProp[what + 'color'] = v[0];
    3886                             e.visProp[what + 'opacity'] = v[1];
    3887                         }
    3888                     };
    3889                 },
    3890
    3891                 posFun = function (board, el, f) {
    3892                     return function () {
    3893                         var e = board.select(el.id);
    3894
    3895                         e.position = f();
    3896                     };
    3897                 },
    3898
    3899                 styleFun = function (board, el, f) {
    3900                     return function () {
    3901                         var e = board.select(el.id);
    3902
    3903                         e.setStyle(f());
    3904                     };
    3905                 };
    3906
    3907             if (i < 0) {
    3908                 return;
    3909             }
    3910
    3911             while (i >= 0) {
    3912                 term = str.slice(i + 6, j);   // throw away <data>
    3913                 m = term.indexOf('=');
    3914                 left = term.slice(0, m);
    3915                 right = term.slice(m + 1);
    3916                 m = left.indexOf('.');     // Dies erzeugt Probleme bei Variablennamen der Form " Steuern akt."
    3917                 name = left.slice(0, m);    //.replace(/\s+$/,''); // do NOT cut out name (with whitespace)
    3918                 el = this.elementsByName[Type.unescapeHTML(name)];
    3919
    3920                 property = left.slice(m + 1).replace(/\s+/g, '').toLowerCase(); // remove whitespace in property
    3921                 right = Type.createFunction (right, this, '', true);
    3922
    3923                 // Debug
    3924                 if (!Type.exists(this.elementsByName[name])) {
    3925                     JXG.debug("debug conditions: |" + name + "| undefined");
    3926                 } else {
    3927                     // plaintext += "el = this.objects[\"" + el.id + "\"];\n";
    3928
    3929                     switch (property) {
    3930                     case 'x':
    3931                         functions.push(xyFun(this, el, right, 2));
    3932                         break;
    3933                     case 'y':
    3934                         functions.push(xyFun(this, el, right, 1));
    3935                         break;
    3936                     case 'visible':
    3937                         functions.push(visFun(this, el, right));
    3938                         break;
    3939                     case 'position':
    3940                         functions.push(posFun(this, el, right));
    3941                         break;
    3942                     case 'stroke':
    3943                         functions.push(colFun(this, el, right, 'stroke'));
    3944                         break;
    3945                     case 'style':
    3946                         functions.push(styleFun(this, el, right));
    3947                         break;
    3948                     case 'strokewidth':
    3949                         functions.push(colFun(this, el, right, 'strokewidth'));
    3950                         break;
    3951                     case 'fill':
    3952                         functions.push(colFun(this, el, right, 'fill'));
    3953                         break;
    3954                     case 'label':
    3955                         break;
    3956                     default:
    3957                         JXG.debug("property '" + property + "' in conditions not yet implemented:" + right);
    3958                         break;
    3959                     }
    3960                 }
    3961                 str = str.slice(j + 7); // cut off "</data>"
    3962                 i = str.indexOf('<data>');
    3963                 j = str.indexOf('<' + '/data>');
    3964             }
    3965
    3966             this.updateConditions = function () {
    3967                 var i;
    3968
    3969                 for (i = 0; i < functions.length; i++) {
    3970                     functions[i]();
    3971                 }
    3972
    3973                 this.prepareUpdate().updateElements();
    3974                 return true;
    3975             };
    3976             this.updateConditions();
    3977         },
    3978
    3979         /**
    3980          * Computes the commands in the conditions-section of the gxt file.
    3981          * It is evaluated after an update, before the unsuspendRedraw.
    3982          * The function is generated in
    3983          * @see JXG.Board#addConditions
    3984          * @private
    3985          */
    3986         updateConditions: function () {
    3987             return false;
    3988         },
    3989
    3990         /**
    3991          * Calculates adequate snap sizes.
    3992          * @returns {JXG.Board} Reference to the board.
    3993          */
    3994         calculateSnapSizes: function () {
    3995             var p1 = new Coords(Const.COORDS_BY_USER, [0, 0], this),
    3996                 p2 = new Coords(Const.COORDS_BY_USER, [this.options.grid.gridX, this.options.grid.gridY], this),
    3997                 x = p1.scrCoords[1] - p2.scrCoords[1],
    3998                 y = p1.scrCoords[2] - p2.scrCoords[2];
    3999
    4000             this.options.grid.snapSizeX = this.options.grid.gridX;
    4001             while (Math.abs(x) > 25) {
    4002                 this.options.grid.snapSizeX *= 2;
    4003                 x /= 2;
    4004             }
    4005
    4006             this.options.grid.snapSizeY = this.options.grid.gridY;
    4007             while (Math.abs(y) > 25) {
    4008                 this.options.grid.snapSizeY *= 2;
    4009                 y /= 2;
    4010             }
    4011
    4012             return this;
    4013         },
    4014
    4015         /**
    4016          * Apply update on all objects with the new zoom-factors. Clears all traces.
    4017          * @returns {JXG.Board} Reference to the board.
    4018          */
    4019         applyZoom: function () {
    4020             this.updateCoords().calculateSnapSizes().clearTraces().fullUpdate();
    4021
    4022             return this;
    4023         },
    4024
    4025         /**
    4026          * Zooms into the board by the factors board.attr.zoom.factorX and board.attr.zoom.factorY and applies the zoom.
    4027          * The zoom operation is centered at x, y.
    4028          * @param {Number} [x]
    4029          * @param {Number} [y]
    4030          * @returns {JXG.Board} Reference to the board
    4031          */
    4032         zoomIn: function (x, y) {
    4033             var bb = this.getBoundingBox(),
    4034                 zX = this.attr.zoom.factorx,
    4035                 zY = this.attr.zoom.factory,
    4036                 dX = (bb[2] - bb[0]) * (1.0 - 1.0 / zX),
    4037                 dY = (bb[1] - bb[3]) * (1.0 - 1.0 / zY),
    4038                 lr = 0.5,
    4039                 tr = 0.5,
    4040                 mi = this.attr.zoom.eps || this.attr.zoom.min || 0.001;  // this.attr.zoom.eps is deprecated
    4041
    4042             if ((this.zoomX > this.attr.zoom.max && zX > 1.0) ||
    4043                 (this.zoomY > this.attr.zoom.max && zY > 1.0) ||
    4044                 (this.zoomX < mi && zX < 1.0) ||  // zoomIn is used for all zooms on touch devices
    4045                 (this.zoomY < mi && zY < 1.0)) {
    4046                 return this;
    4047             }
    4048
    4049             if (Type.isNumber(x) && Type.isNumber(y)) {
    4050                 lr = (x - bb[0]) / (bb[2] - bb[0]);
    4051                 tr = (bb[1] - y) / (bb[1] - bb[3]);
    4052             }
    4053
    4054             this.setBoundingBox([bb[0] + dX * lr, bb[1] - dY * tr, bb[2] - dX * (1 - lr), bb[3] + dY * (1 - tr)], this.keepaspectratio, 'update');
    4055             return this.applyZoom();
    4056         },
    4057
    4058         /**
    4059          * Zooms out of the board by the factors board.attr.zoom.factorX and board.attr.zoom.factorY and applies the zoom.
    4060          * The zoom operation is centered at x, y.
    4061          *
    4062          * @param {Number} [x]
    4063          * @param {Number} [y]
    4064          * @returns {JXG.Board} Reference to the board
    4065          */
    4066         zoomOut: function (x, y) {
    4067             var bb = this.getBoundingBox(),
    4068                 zX = this.attr.zoom.factorx,
    4069                 zY = this.attr.zoom.factory,
    4070                 dX = (bb[2] - bb[0]) * (1.0 - zX),
    4071                 dY = (bb[1] - bb[3]) * (1.0 - zY),
    4072                 lr = 0.5,
    4073                 tr = 0.5,
    4074                 mi = this.attr.zoom.eps || this.attr.zoom.min || 0.001;  // this.attr.zoom.eps is deprecated
    4075
    4076             if (this.zoomX < mi || this.zoomY < mi) {
    4077                 return this;
    4078             }
    4079
    4080             if (Type.isNumber(x) && Type.isNumber(y)) {
    4081                 lr = (x - bb[0]) / (bb[2] - bb[0]);
    4082                 tr = (bb[1] - y) / (bb[1] - bb[3]);
    4083             }
    4084
    4085             this.setBoundingBox([bb[0] + dX * lr, bb[1] - dY * tr, bb[2] - dX * (1 - lr), bb[3] + dY * (1 - tr)], this.keepaspectratio, 'update');
    4086
    4087             return this.applyZoom();
    4088         },
    4089
    4090         /**
    4091          * Reset the zoom level to the original zoom level from initBoard();
    4092          * Additionally, if the board as been initialized with a boundingBox (which is the default),
    4093          * restore the viewport to the original viewport during initialization. Otherwise,
    4094          * (i.e. if the board as been initialized with unitX/Y and originX/Y),
    4095          * just set the zoom level to 100%.
    4096          *
    4097          * @returns {JXG.Board} Reference to the board
    4098          */
    4099         zoom100: function () {
    4100             var bb, dX, dY;
    4101
    4102             if (Type.exists(this.attr.boundingbox)) {
    4103                 this.setBoundingBox(this.attr.boundingbox, this.keepaspectratio, 'reset');
    4104             } else {
    4105                 // Board has been set up with unitX/Y and originX/Y
    4106                 bb = this.getBoundingBox();
    4107                 dX = (bb[2] - bb[0]) * (1.0 - this.zoomX) * 0.5;
    4108                 dY = (bb[1] - bb[3]) * (1.0 - this.zoomY) * 0.5;
    4109                 this.setBoundingBox([bb[0] + dX, bb[1] - dY, bb[2] - dX, bb[3] + dY], this.keepaspectratio, 'reset');
    4110             }
    4111             return this.applyZoom();
    4112         },
    4113
    4114         /**
    4115          * Zooms the board so every visible point is shown. Keeps aspect ratio.
    4116          * @returns {JXG.Board} Reference to the board
    4117          */
    4118         zoomAllPoints: function () {
    4119             var el, border, borderX, borderY, pEl,
    4120                 minX = 0,
    4121                 maxX = 0,
    4122                 minY = 0,
    4123                 maxY = 0,
    4124                 len = this.objectsList.length;
    4125
    4126             for (el = 0; el < len; el++) {
    4127                 pEl = this.objectsList[el];
    4128
    4129                 if (Type.isPoint(pEl) && pEl.visPropCalc.visible) {
    4130                     if (pEl.coords.usrCoords[1] < minX) {
    4131                         minX = pEl.coords.usrCoords[1];
    4132                     } else if (pEl.coords.usrCoords[1] > maxX) {
    4133                         maxX = pEl.coords.usrCoords[1];
    4134                     }
    4135                     if (pEl.coords.usrCoords[2] > maxY) {
    4136                         maxY = pEl.coords.usrCoords[2];
    4137                     } else if (pEl.coords.usrCoords[2] < minY) {
    4138                         minY = pEl.coords.usrCoords[2];
    4139                     }
    4140                 }
    4141             }
    4142
    4143             border = 50;
    4144             borderX = border / this.unitX;
    4145             borderY = border / this.unitY;
    4146
    4147             this.setBoundingBox([minX - borderX, maxY + borderY, maxX + borderX, minY - borderY], this.keepaspectratio, 'update');
    4148
    4149             return this.applyZoom();
    4150         },
    4151
    4152         /**
    4153          * Reset the bounding box and the zoom level to 100% such that a given set of elements is
    4154          * within the board's viewport.
    4155          * @param {Array} elements A set of elements given by id, reference, or name.
    4156          * @returns {JXG.Board} Reference to the board.
    4157          */
    4158         zoomElements: function (elements) {
    4159             var i, e, box,
    4160                 newBBox = [Infinity, -Infinity, -Infinity, Infinity],
    4161                 cx, cy, dx, dy, d;
    4162
    4163             if (!Type.isArray(elements) || elements.length === 0) {
    4164                 return this;
    4165             }
    4166
    4167             for (i = 0; i < elements.length; i++) {
    4168                 e = this.select(elements[i]);
    4169
    4170                 box = e.bounds();
    4171                 if (Type.isArray(box)) {
    4172                     if (box[0] < newBBox[0]) { newBBox[0] = box[0]; }
    4173                     if (box[1] > newBBox[1]) { newBBox[1] = box[1]; }
    4174                     if (box[2] > newBBox[2]) { newBBox[2] = box[2]; }
    4175                     if (box[3] < newBBox[3]) { newBBox[3] = box[3]; }
    4176                 }
    4177             }
    4178
    4179             if (Type.isArray(newBBox)) {
    4180                 cx = 0.5 * (newBBox[0] + newBBox[2]);
    4181                 cy = 0.5 * (newBBox[1] + newBBox[3]);
    4182                 dx = 1.5 * (newBBox[2] - newBBox[0]) * 0.5;
    4183                 dy = 1.5 * (newBBox[1] - newBBox[3]) * 0.5;
    4184                 d = Math.max(dx, dy);
    4185                 this.setBoundingBox([cx - d, cy + d, cx + d, cy - d], this.keepaspectratio, 'update');
    4186             }
    4187
    4188             return this;
    4189         },
    4190
    4191         /**
    4192          * Sets the zoom level to <tt>fX</tt> resp <tt>fY</tt>.
    4193          * @param {Number} fX
    4194          * @param {Number} fY
    4195          * @returns {JXG.Board} Reference to the board.
    4196          */
    4197         setZoom: function (fX, fY) {
    4198             var oX = this.attr.zoom.factorx,
    4199                 oY = this.attr.zoom.factory;
    4200
    4201             this.attr.zoom.factorx = fX / this.zoomX;
    4202             this.attr.zoom.factory = fY / this.zoomY;
    4203
    4204             this.zoomIn();
    4205
    4206             this.attr.zoom.factorx = oX;
    4207             this.attr.zoom.factory = oY;
    4208
    4209             return this;
    4210         },
    4211
    4212         /**
    4213          * Removes object from board and renderer.
    4214          * <p>
    4215          * <b>Performance hints:</b> It is recommended to use the object's id.
    4216          * If many elements are removed, it is best to call <tt>board.suspendUpdate()</tt>
    4217          * before looping through the elements to be removed and call
    4218          * <tt>board.unsuspendUpdate()</tt> after the loop. Further, it is advisable to loop
    4219          * in reverse order, i.e. remove the object in reverse order of their creation time.
    4220          *
    4221          * @param {JXG.GeometryElement|Array} object The object to remove or array of objects to be removed.
    4222          * The element(s) is/are given by name, id or a reference.
    4223          * @param {Boolean} saveMethod If true, the algorithm runs through all elements
    4224          * and tests if the element to be deleted is a child element. If yes, it will be
    4225          * removed from the list of child elements. If false (default), the element
    4226          * is removed from the lists of child elements of all its ancestors.
    4227          * This should be much faster.
    4228          * @returns {JXG.Board} Reference to the board
    4229          */
    4230         removeObject: function (object, saveMethod) {
    4231             var el, i;
    4232
    4233             if (Type.isArray(object)) {
    4234                 for (i = 0; i < object.length; i++) {
    4235                     this.removeObject(object[i]);
    4236                 }
    4237
    4238                 return this;
    4239             }
    4240
    4241             object = this.select(object);
    4242
    4243             // If the object which is about to be removed unknown or a string, do nothing.
    4244             // it is a string if a string was given and could not be resolved to an element.
    4245             if (!Type.exists(object) || Type.isString(object)) {
    4246                 return this;
    4247             }
    4248
    4249             try {
    4250                 // remove all children.
    4251                 for (el in object.childElements) {
    4252                     if (object.childElements.hasOwnProperty(el)) {
    4253                         object.childElements[el].board.removeObject(object.childElements[el]);
    4254                     }
    4255                 }
    4256
    4257                 // Remove all children in elements like turtle
    4258                 for (el in object.objects) {
    4259                     if (object.objects.hasOwnProperty(el)) {
    4260                         object.objects[el].board.removeObject(object.objects[el]);
    4261                     }
    4262                 }
    4263
    4264                 // Remove the element from the childElement list and the descendant list of all elements.
    4265                 if (saveMethod) {
    4266                     // Running through all objects has quadratic complexity if many objects are deleted.
    4267                     for (el in this.objects) {
    4268                         if (this.objects.hasOwnProperty(el)) {
    4269                             if (Type.exists(this.objects[el].childElements) &&
    4270                                 Type.exists(this.objects[el].childElements.hasOwnProperty(object.id))
    4271                             ) {
    4272                                 delete this.objects[el].childElements[object.id];
    4273                                 delete this.objects[el].descendants[object.id];
    4274                             }
    4275                         }
    4276                     }
    4277                 } else if (Type.exists(object.ancestors)) {
    4278                     // Running through the ancestors should be much more efficient.
    4279                     for (el in object.ancestors) {
    4280                         if (object.ancestors.hasOwnProperty(el)) {
    4281                             if (Type.exists(object.ancestors[el].childElements) &&
    4282                                 Type.exists(object.ancestors[el].childElements.hasOwnProperty(object.id))
    4283                             ) {
    4284                                 delete object.ancestors[el].childElements[object.id];
    4285                                 delete object.ancestors[el].descendants[object.id];
    4286                             }
    4287                         }
    4288                     }
    4289                 }
    4290
    4291                 // remove the object itself from our control structures
    4292                 if (object._pos > -1) {
    4293                     this.objectsList.splice(object._pos, 1);
    4294                     for (el = object._pos; el < this.objectsList.length; el++) {
    4295                         this.objectsList[el]._pos--;
    4296                     }
    4297                 } else if (object.type !== Const.OBJECT_TYPE_TURTLE) {
    4298                     JXG.debug('Board.removeObject: object ' + object.id + ' not found in list.');
    4299                 }
    4300
    4301                 delete this.objects[object.id];
    4302                 delete this.elementsByName[object.name];
    4303
    4304                 if (object.visProp && Type.evaluate(object.visProp.trace)) {
    4305                     object.clearTrace();
    4306                 }
    4307
    4308                 // the object deletion itself is handled by the object.
    4309                 if (Type.exists(object.remove)) {
    4310                     object.remove();
    4311                 }
    4312             } catch (e) {
    4313                 JXG.debug(object.id + ': Could not be removed: ' + e);
    4314             }
    4315
    4316             this.update();
    4317
    4318             return this;
    4319         },
    4320
    4321         /**
    4322          * Removes the ancestors of an object an the object itself from board and renderer.
    4323          * @param {JXG.GeometryElement} object The object to remove.
    4324          * @returns {JXG.Board} Reference to the board
    4325          */
    4326         removeAncestors: function (object) {
    4327             var anc;
    4328
    4329             for (anc in object.ancestors) {
    4330                 if (object.ancestors.hasOwnProperty(anc)) {
    4331                     this.removeAncestors(object.ancestors[anc]);
    4332                 }
    4333             }
    4334
    4335             this.removeObject(object);
    4336
    4337             return this;
    4338         },
    4339
    4340         /**
    4341          * Initialize some objects which are contained in every GEONExT construction by default,
    4342          * but are not contained in the gxt files.
    4343          * @returns {JXG.Board} Reference to the board
    4344          */
    4345         initGeonextBoard: function () {
    4346             var p1, p2, p3;
    4347
    4348             p1 = this.create('point', [0, 0], {
    4349                 id: this.id + 'g00e0',
    4350                 name: 'Ursprung',
    4351                 withLabel: false,
    4352                 visible: false,
    4353                 fixed: true
    4354             });
    4355
    4356             p2 = this.create('point', [1, 0], {
    4357                 id: this.id + 'gX0e0',
    4358                 name: 'Punkt_1_0',
    4359                 withLabel: false,
    4360                 visible: false,
    4361                 fixed: true
    4362             });
    4363
    4364             p3 = this.create('point', [0, 1], {
    4365                 id: this.id + 'gY0e0',
    4366                 name: 'Punkt_0_1',
    4367                 withLabel: false,
    4368                 visible: false,
    4369                 fixed: true
    4370             });
    4371
    4372             this.create('line', [p1, p2], {
    4373                 id: this.id + 'gXLe0',
    4374                 name: 'X-Achse',
    4375                 withLabel: false,
    4376                 visible: false
    4377             });
    4378
    4379             this.create('line', [p1, p3], {
    4380                 id: this.id + 'gYLe0',
    4381                 name: 'Y-Achse',
    4382                 withLabel: false,
    4383                 visible: false
    4384             });
    4385
    4386             return this;
    4387         },
    4388
    4389         /**
    4390          * Change the height and width of the board's container.
    4391          * After doing so, {@link JXG.JSXGraph.setBoundingBox} is called using
    4392          * the actual size of the bounding box and the actual value of keepaspectratio.
    4393          * If setBoundingbox() should not be called automatically,
    4394          * call resizeContainer with dontSetBoundingBox == true.
    4395          * @param {Number} canvasWidth New width of the container.
    4396          * @param {Number} canvasHeight New height of the container.
    4397          * @param {Boolean} [dontset=false] If true do not set the CSS width and height of the DOM element.
    4398          * @param {Boolean} [dontSetBoundingBox=false] If true do not call setBoundingBox().
    4399          * @returns {JXG.Board} Reference to the board
    4400          */
    4401         resizeContainer: function (canvasWidth, canvasHeight, dontset, dontSetBoundingBox) {
    4402             var box;
    4403                 // w, h, cx, cy;
    4404                 // box_act,
    4405                 // shift_x = 0,
    4406                 // shift_y = 0;
    4407
    4408             if (!dontSetBoundingBox) {
    4409                 // box_act = this.getBoundingBox();    // This is the actual bounding box.
    4410                 box = this.getBoundingBox();    // This is the actual bounding box.
    4411             }
    4412
    4413             this.canvasWidth = parseFloat(canvasWidth);
    4414             this.canvasHeight = parseFloat(canvasHeight);
    4415
    4416             // if (!dontSetBoundingBox) {
    4417             //     box     = this.attr.boundingbox;    // This is the intended bounding box.
    4418
    4419             //     // The shift values compensate the follow-up correction
    4420             //     // in setBoundingBox in case of "this.keepaspectratio==true"
    4421             //     // Otherwise, shift_x and shift_y will be zero.
    4422             //     // Obsolet since setBoundingBox centers in case of "this.keepaspectratio==true".
    4423             //     // shift_x = box_act[0] - box[0] / this.zoomX;
    4424             //     // shift_y = box_act[1] - box[1] / this.zoomY;
    4425
    4426             //     cx = (box[2] + box[0]) * 0.5; // + shift_x;
    4427             //     cy = (box[3] + box[1]) * 0.5; // + shift_y;
    4428
    4429             //     w = (box[2] - box[0]) * 0.5 / this.zoomX;
    4430             //     h = (box[1] - box[3]) * 0.5 / this.zoomY;
    4431
    4432             //     box = [cx - w, cy + h, cx + w, cy - h];
    4433             // }
    4434
    4435             if (!dontset) {
    4436                 this.containerObj.style.width = (this.canvasWidth) + 'px';
    4437                 this.containerObj.style.height = (this.canvasHeight) + 'px';
    4438             }
    4439             this.renderer.resize(this.canvasWidth, this.canvasHeight);
    4440
    4441             if (!dontSetBoundingBox) {
    4442                 this.setBoundingBox(box, this.keepaspectratio, 'keep');
    4443             }
    4444
    4445             return this;
    4446         },
    4447
    4448         /**
    4449          * Lists the dependencies graph in a new HTML-window.
    4450          * @returns {JXG.Board} Reference to the board
    4451          */
    4452         showDependencies: function () {
    4453             var el, t, c, f, i;
    4454
    4455             t = '<p>\n';
    4456             for (el in this.objects) {
    4457                 if (this.objects.hasOwnProperty(el)) {
    4458                     i = 0;
    4459                     for (c in this.objects[el].childElements) {
    4460                         if (this.objects[el].childElements.hasOwnProperty(c)) {
    4461                             i += 1;
    4462                         }
    4463                     }
    4464                     if (i >= 0) {
    4465                         t += '<strong>' + this.objects[el].id + ':<' + '/strong> ';
    4466                     }
    4467
    4468                     for (c in this.objects[el].childElements) {
    4469                         if (this.objects[el].childElements.hasOwnProperty(c)) {
    4470                             t += this.objects[el].childElements[c].id + '(' + this.objects[el].childElements[c].name + ')' + ', ';
    4471                         }
    4472                     }
    4473                     t += '<p>\n';
    4474                 }
    4475             }
    4476             t += '<' + '/p>\n';
    4477             f = window.open();
    4478             f.document.open();
    4479             f.document.write(t);
    4480             f.document.close();
    4481             return this;
    4482         },
    4483
    4484         /**
    4485          * Lists the XML code of the construction in a new HTML-window.
    4486          * @returns {JXG.Board} Reference to the board
    4487          */
    4488         showXML: function () {
    4489             var f = window.open('');
    4490             f.document.open();
    4491             f.document.write('<pre>' + Type.escapeHTML(this.xmlString) + '<' + '/pre>');
    4492             f.document.close();
    4493             return this;
    4494         },
    4495
    4496         /**
    4497          * Sets for all objects the needsUpdate flag to "true".
    4498          * @returns {JXG.Board} Reference to the board
    4499          */
    4500         prepareUpdate: function () {
    4501             var el, pEl, len = this.objectsList.length;
    4502
    4503             /*
    4504             if (this.attr.updatetype === 'hierarchical') {
    4505                 return this;
    4506             }
    4507             */
    4508
    4509             for (el = 0; el < len; el++) {
    4510                 pEl = this.objectsList[el];
    4511                 pEl.needsUpdate = pEl.needsRegularUpdate || this.needsFullUpdate;
    4512             }
    4513
    4514             for (el in this.groups) {
    4515                 if (this.groups.hasOwnProperty(el)) {
    4516                     pEl = this.groups[el];
    4517                     pEl.needsUpdate = pEl.needsRegularUpdate || this.needsFullUpdate;
    4518                 }
    4519             }
    4520
    4521             return this;
    4522         },
    4523
    4524         /**
    4525          * Runs through all elements and calls their update() method.
    4526          * @param {JXG.GeometryElement} drag Element that caused the update.
    4527          * @returns {JXG.Board} Reference to the board
    4528          */
    4529         updateElements: function (drag) {
    4530             var el, pEl;
    4531             //var childId, i = 0;
    4532
    4533             drag = this.select(drag);
    4534
    4535             /*
    4536             if (Type.exists(drag)) {
    4537                 for (el = 0; el < this.objectsList.length; el++) {
    4538                     pEl = this.objectsList[el];
    4539                     if (pEl.id === drag.id) {
    4540                         i = el;
    4541                         break;
    4542                     }
    4543                 }
    4544             }
    4545             */
    4546
    4547             for (el = 0; el < this.objectsList.length; el++) {
    4548                 pEl = this.objectsList[el];
    4549                 if (this.needsFullUpdate && pEl.elementClass === Const.OBJECT_CLASS_TEXT) {
    4550                     pEl.updateSize();
    4551                 }
    4552
    4553                 // For updates of an element we distinguish if the dragged element is updated or
    4554                 // other elements are updated.
    4555                 // The difference lies in the treatment of gliders and points based on transformations.
    4556                 pEl.update(!Type.exists(drag) || pEl.id !== drag.id)
    4557                    .updateVisibility();
    4558             }
    4559
    4560             // update groups last
    4561             for (el in this.groups) {
    4562                 if (this.groups.hasOwnProperty(el)) {
    4563                     this.groups[el].update(drag);
    4564                 }
    4565             }
    4566
    4567             return this;
    4568         },
    4569
    4570         /**
    4571          * Runs through all elements and calls their update() method.
    4572          * @returns {JXG.Board} Reference to the board
    4573          */
    4574         updateRenderer: function () {
    4575             var el,
    4576                 len = this.objectsList.length;
    4577
    4578             /*
    4579             objs = this.objectsList.slice(0);
    4580             objs.sort(function (a, b) {
    4581                 if (a.visProp.layer < b.visProp.layer) {
    4582                     return -1;
    4583                 } else if (a.visProp.layer === b.visProp.layer) {
    4584                     return b.lastDragTime.getTime() - a.lastDragTime.getTime();
    4585                 } else {
    4586                     return 1;
    4587                 }
    4588             });
    4589             */
    4590
    4591             if (this.renderer.type === 'canvas') {
    4592                 this.updateRendererCanvas();
    4593             } else {
    4594                 for (el = 0; el < len; el++) {
    4595                     this.objectsList[el].updateRenderer();
    4596                 }
    4597             }
    4598             return this;
    4599         },
    4600
    4601         /**
    4602          * Runs through all elements and calls their update() method.
    4603          * This is a special version for the CanvasRenderer.
    4604          * Here, we have to do our own layer handling.
    4605          * @returns {JXG.Board} Reference to the board
    4606          */
    4607         updateRendererCanvas: function () {
    4608             var el, pEl, i, mini, la,
    4609                 olen = this.objectsList.length,
    4610                 layers = this.options.layer,
    4611                 len = this.options.layer.numlayers,
    4612                 last = Number.NEGATIVE_INFINITY;
    4613
    4614             for (i = 0; i < len; i++) {
    4615                 mini = Number.POSITIVE_INFINITY;
    4616
    4617                 for (la in layers) {
    4618                     if (layers.hasOwnProperty(la)) {
    4619                         if (layers[la] > last && layers[la] < mini) {
    4620                             mini = layers[la];
    4621                         }
    4622                     }
    4623                 }
    4624
    4625                 last = mini;
    4626
    4627                 for (el = 0; el < olen; el++) {
    4628                     pEl = this.objectsList[el];
    4629
    4630                     if (pEl.visProp.layer === mini) {
    4631                         pEl.prepareUpdate().updateRenderer();
    4632                     }
    4633                 }
    4634             }
    4635             return this;
    4636         },
    4637
    4638         /**
    4639          * Please use {@link JXG.Board.on} instead.
    4640          * @param {Function} hook A function to be called by the board after an update occurred.
    4641          * @param {String} [m='update'] When the hook is to be called. Possible values are <i>mouseup</i>, <i>mousedown</i> and <i>update</i>.
    4642          * @param {Object} [context=board] Determines the execution context the hook is called. This parameter is optional, default is the
    4643          * board object the hook is attached to.
    4644          * @returns {Number} Id of the hook, required to remove the hook from the board.
    4645          * @deprecated
    4646          */
    4647         addHook: function (hook, m, context) {
    4648             JXG.deprecated('Board.addHook()', 'Board.on()');
    4649             m = Type.def(m, 'update');
    4650
    4651             context = Type.def(context, this);
    4652
    4653             this.hooks.push([m, hook]);
    4654             this.on(m, hook, context);
    4655
    4656             return this.hooks.length - 1;
    4657         },
    4658
    4659         /**
    4660          * Alias of {@link JXG.Board.on}.
    4661          */
    4662         addEvent: JXG.shortcut(JXG.Board.prototype, 'on'),
    4663
    4664         /**
    4665          * Please use {@link JXG.Board.off} instead.
    4666          * @param {Number|function} id The number you got when you added the hook or a reference to the event handler.
    4667          * @returns {JXG.Board} Reference to the board
    4668          * @deprecated
    4669          */
    4670         removeHook: function (id) {
    4671             JXG.deprecated('Board.removeHook()', 'Board.off()');
    4672             if (this.hooks[id]) {
    4673                 this.off(this.hooks[id][0], this.hooks[id][1]);
    4674                 this.hooks[id] = null;
    4675             }
    4676
    4677             return this;
    4678         },
    4679
    4680         /**
    4681          * Alias of {@link JXG.Board.off}.
    4682          */
    4683         removeEvent: JXG.shortcut(JXG.Board.prototype, 'off'),
    4684
    4685         /**
    4686          * Runs through all hooked functions and calls them.
    4687          * @returns {JXG.Board} Reference to the board
    4688          * @deprecated
    4689          */
    4690         updateHooks: function (m) {
    4691             var arg = Array.prototype.slice.call(arguments, 0);
    4692
    4693             JXG.deprecated('Board.updateHooks()', 'Board.triggerEventHandlers()');
    4694
    4695             arg[0] = Type.def(arg[0], 'update');
    4696             this.triggerEventHandlers([arg[0]], arguments);
    4697
    4698             return this;
    4699         },
    4700
    4701         /**
    4702          * Adds a dependent board to this board.
    4703          * @param {JXG.Board} board A reference to board which will be updated after an update of this board occurred.
    4704          * @returns {JXG.Board} Reference to the board
    4705          */
    4706         addChild: function (board) {
    4707             if (Type.exists(board) && Type.exists(board.containerObj)) {
    4708                 this.dependentBoards.push(board);
    4709                 this.update();
    4710             }
    4711             return this;
    4712         },
    4713
    4714         /**
    4715          * Deletes a board from the list of dependent boards.
    4716          * @param {JXG.Board} board Reference to the board which will be removed.
    4717          * @returns {JXG.Board} Reference to the board
    4718          */
    4719         removeChild: function (board) {
    4720             var i;
    4721
    4722             for (i = this.dependentBoards.length - 1; i >= 0; i--) {
    4723                 if (this.dependentBoards[i] === board) {
    4724                     this.dependentBoards.splice(i, 1);
    4725                 }
    4726             }
    4727             return this;
    4728         },
    4729
    4730         /**
    4731          * Runs through most elements and calls their update() method and update the conditions.
    4732          * @param {JXG.GeometryElement} [drag] Element that caused the update.
    4733          * @returns {JXG.Board} Reference to the board
    4734          */
    4735         update: function (drag) {
    4736             var i, len, b, insert,
    4737                 storeActiveEl;
    4738
    4739             if (this.inUpdate || this.isSuspendedUpdate) {
    4740                 return this;
    4741             }
    4742             this.inUpdate = true;
    4743
    4744             if (this.attr.minimizereflow === 'all' && this.containerObj && this.renderer.type !== 'vml') {
    4745                 storeActiveEl = document.activeElement; // Store focus element
    4746                 insert = this.renderer.removeToInsertLater(this.containerObj);
    4747             }
    4748
    4749             if (this.attr.minimizereflow === 'svg' && this.renderer.type === 'svg') {
    4750                 storeActiveEl = document.activeElement;
    4751                 insert = this.renderer.removeToInsertLater(this.renderer.svgRoot);
    4752             }
    4753
    4754             this.prepareUpdate().updateElements(drag).updateConditions();
    4755             this.renderer.suspendRedraw(this);
    4756             this.updateRenderer();
    4757             this.renderer.unsuspendRedraw();
    4758             this.triggerEventHandlers(['update'], []);
    4759
    4760             if (insert) {
    4761                 insert();
    4762                 storeActiveEl.focus();     // Restore focus element
    4763             }
    4764
    4765             // To resolve dependencies between boards
    4766             // for (var board in JXG.boards) {
    4767             len = this.dependentBoards.length;
    4768             for (i = 0; i < len; i++) {
    4769                 b = this.dependentBoards[i];
    4770                 if (Type.exists(b) && b !== this) {
    4771                     b.updateQuality = this.updateQuality;
    4772                     b.prepareUpdate().updateElements().updateConditions();
    4773                     b.renderer.suspendRedraw();
    4774                     b.updateRenderer();
    4775                     b.renderer.unsuspendRedraw();
    4776                     b.triggerEventHandlers(['update'], []);
    4777                 }
    4778
    4779             }
    4780
    4781             this.inUpdate = false;
    4782             return this;
    4783         },
    4784
    4785         /**
    4786          * Runs through all elements and calls their update() method and update the conditions.
    4787          * This is necessary after zooming and changing the bounding box.
    4788          * @returns {JXG.Board} Reference to the board
    4789          */
    4790         fullUpdate: function () {
    4791             this.needsFullUpdate = true;
    4792             this.update();
    4793             this.needsFullUpdate = false;
    4794             return this;
    4795         },
    4796
    4797         /**
    4798          * Adds a grid to the board according to the settings given in board.options.
    4799          * @returns {JXG.Board} Reference to the board.
    4800          */
    4801         addGrid: function () {
    4802             this.create('grid', []);
    4803
    4804             return this;
    4805         },
    4806
    4807         /**
    4808          * Removes all grids assigned to this board. Warning: This method also removes all objects depending on one or
    4809          * more of the grids.
    4810          * @returns {JXG.Board} Reference to the board object.
    4811          */
    4812         removeGrids: function () {
    4813             var i;
    4814
    4815             for (i = 0; i < this.grids.length; i++) {
    4816                 this.removeObject(this.grids[i]);
    4817             }
    4818
    4819             this.grids.length = 0;
    4820             this.update(); // required for canvas renderer
    4821
    4822             return this;
    4823         },
    4824
    4825         /**
    4826          * Creates a new geometric element of type elementType.
    4827          * @param {String} elementType Type of the element to be constructed given as a string e.g. 'point' or 'circle'.
    4828          * @param {Array} parents Array of parent elements needed to construct the element e.g. coordinates for a point or two
    4829          * points to construct a line. This highly depends on the elementType that is constructed. See the corresponding JXG.create*
    4830          * methods for a list of possible parameters.
    4831          * @param {Object} [attributes] An object containing the attributes to be set. This also depends on the elementType.
    4832          * Common attributes are name, visible, strokeColor.
    4833          * @returns {Object} Reference to the created element. This is usually a GeometryElement, but can be an array containing
    4834          * two or more elements.
    4835          */
    4836         create: function (elementType, parents, attributes) {
    4837             var el, i;
    4838
    4839             elementType = elementType.toLowerCase();
    4840
    4841             if (!Type.exists(parents)) {
    4842                 parents = [];
    4843             }
    4844
    4845             if (!Type.exists(attributes)) {
    4846                 attributes = {};
    4847             }
    4848
    4849             for (i = 0; i < parents.length; i++) {
    4850                 if (Type.isString(parents[i]) &&
    4851                     !(elementType === 'text' && i === 2) &&
    4852                     !(elementType === 'solidofrevolution3d' && i === 2) &&
    4853                     !((elementType === 'input' || elementType === 'checkbox' || elementType === 'button') &&
    4854                       (i === 2 || i === 3)) &&
    4855                     !(elementType === 'curve' && i > 0) // Allow curve plots with jessiecode
    4856                 ) {
    4857                     parents[i] = this.select(parents[i]);
    4858                 }
    4859             }
    4860
    4861             if (Type.isFunction(JXG.elements[elementType])) {
    4862                 el = JXG.elements[elementType](this, parents, attributes);
    4863             } else {
    4864                 throw new Error("JSXGraph: create: Unknown element type given: " + elementType);
    4865             }
    4866
    4867             if (!Type.exists(el)) {
    4868                 JXG.debug("JSXGraph: create: failure creating " + elementType);
    4869                 return el;
    4870             }
    4871
    4872             if (el.prepareUpdate && el.update && el.updateRenderer) {
    4873                 el.fullUpdate();
    4874             }
    4875             return el;
    4876         },
    4877
    4878         /**
    4879          * Deprecated name for {@link JXG.Board.create}.
    4880          * @deprecated
    4881          */
    4882         createElement: function () {
    4883             JXG.deprecated('Board.createElement()', 'Board.create()');
    4884             return this.create.apply(this, arguments);
    4885         },
    4886
    4887         /**
    4888          * Delete the elements drawn as part of a trace of an element.
    4889          * @returns {JXG.Board} Reference to the board
    4890          */
    4891         clearTraces: function () {
    4892             var el;
    4893
    4894             for (el = 0; el < this.objectsList.length; el++) {
    4895                 this.objectsList[el].clearTrace();
    4896             }
    4897
    4898             this.numTraces = 0;
    4899             return this;
    4900         },
    4901
    4902         /**
    4903          * Stop updates of the board.
    4904          * @returns {JXG.Board} Reference to the board
    4905          */
    4906         suspendUpdate: function () {
    4907             if (!this.inUpdate) {
    4908                 this.isSuspendedUpdate = true;
    4909             }
    4910             return this;
    4911         },
    4912
    4913         /**
    4914          * Enable updates of the board.
    4915          * @returns {JXG.Board} Reference to the board
    4916          */
    4917         unsuspendUpdate: function () {
    4918             if (this.isSuspendedUpdate) {
    4919                 this.isSuspendedUpdate = false;
    4920                 this.fullUpdate();
    4921             }
    4922             return this;
    4923         },
    4924
    4925         /**
    4926          * Set the bounding box of the board.
    4927          * @param {Array} bbox New bounding box [x1,y1,x2,y2]
    4928          * @param {Boolean} [keepaspectratio=false] If set to true, the aspect ratio will be 1:1, but
    4929          * the resulting viewport may be larger.
    4930          * @param {String} [setZoom='reset'] Reset, keep or update the zoom level of the board. 'reset'
    4931          * sets {@link JXG.Board#zoomX} and {@link JXG.Board#zoomY} to the start values (or 1.0).
    4932          * 'update' adapts these values accoring to the new bounding box and 'keep' does nothing.
    4933          * @returns {JXG.Board} Reference to the board
    4934          */
    4935         setBoundingBox: function (bbox, keepaspectratio, setZoom) {
    4936             var h, w, ux, uy,
    4937                 offX = 0,
    4938                 offY = 0,
    4939                 dim = Env.getDimensions(this.container, this.document);
    4940
    4941             if (!Type.isArray(bbox)) {
    4942                 return this;
    4943             }
    4944
    4945             if (bbox[0] < this.maxboundingbox[0] ||
    4946                 bbox[1] > this.maxboundingbox[1] ||
    4947                 bbox[2] > this.maxboundingbox[2] ||
    4948                 bbox[3] < this.maxboundingbox[3]) {
    4949                 return this;
    4950             }
    4951
    4952             if (!Type.exists(setZoom)) {
    4953                 setZoom = 'reset';
    4954             }
    4955
    4956             ux = this.unitX;
    4957             uy = this.unitY;
    4958
    4959             this.canvasWidth = parseInt(dim.width, 10);
    4960             this.canvasHeight = parseInt(dim.height, 10);
    4961             w = this.canvasWidth;
    4962             h = this.canvasHeight;
    4963             if (keepaspectratio) {
    4964                 this.unitX = w / (bbox[2] - bbox[0]);
    4965                 this.unitY = h / (bbox[1] - bbox[3]);
    4966                 if (Math.abs(this.unitX) < Math.abs(this.unitY)) {
    4967                     this.unitY = Math.abs(this.unitX) * this.unitY / Math.abs(this.unitY);
    4968                     // Add the additional units in equal portions above and below
    4969                     offY = (h / this.unitY - (bbox[1] - bbox[3])) * 0.5;
    4970                 } else {
    4971                     this.unitX = Math.abs(this.unitY) * this.unitX / Math.abs(this.unitX);
    4972                     // Add the additional units in equal portions left and right
    4973                     offX = (w / this.unitX - (bbox[2] - bbox[0])) * 0.5;
    4974                 }
    4975                 this.keepaspectratio = true;
    4976             } else {
    4977                 this.unitX = w / (bbox[2] - bbox[0]);
    4978                 this.unitY = h / (bbox[1] - bbox[3]);
    4979                 this.keepaspectratio = false;
    4980             }
    4981
    4982             this.moveOrigin(-this.unitX * (bbox[0] - offX), this.unitY * (bbox[1] + offY));
    4983
    4984             if (setZoom === 'update') {
    4985                 this.zoomX *= this.unitX / ux;
    4986                 this.zoomY *= this.unitY / uy;
    4987             } else if (setZoom === 'reset') {
    4988                 this.zoomX = Type.exists(this.attr.zoomx) ? this.attr.zoomx : 1.0;
    4989                 this.zoomY = Type.exists(this.attr.zoomy) ? this.attr.zoomy : 1.0;
    4990             }
    4991
    4992             return this;
    4993         },
    4994
    4995         /**
    4996          * Get the bounding box of the board.
    4997          * @returns {Array} bounding box [x1,y1,x2,y2] upper left corner, lower right corner
    4998          */
    4999         getBoundingBox: function () {
    5000             var ul = (new Coords(Const.COORDS_BY_SCREEN, [0, 0], this)).usrCoords,
    5001                 lr = (new Coords(Const.COORDS_BY_SCREEN, [this.canvasWidth, this.canvasHeight], this)).usrCoords;
    5002
    5003             return [ul[1], ul[2], lr[1], lr[2]];
    5004         },
    5005
    5006         /**
    5007          * Adds an animation. Animations are controlled by the boards, so the boards need to be aware of the
    5008          * animated elements. This function tells the board about new elements to animate.
    5009          * @param {JXG.GeometryElement} element The element which is to be animated.
    5010          * @returns {JXG.Board} Reference to the board
    5011          */
    5012         addAnimation: function (element) {
    5013             var that = this;
    5014
    5015             this.animationObjects[element.id] = element;
    5016
    5017             if (!this.animationIntervalCode) {
    5018                 this.animationIntervalCode = window.setInterval(function () {
    5019                     that.animate();
    5020                 }, element.board.attr.animationdelay);
    5021             }
    5022
    5023             return this;
    5024         },
    5025
    5026         /**
    5027          * Cancels all running animations.
    5028          * @returns {JXG.Board} Reference to the board
    5029          */
    5030         stopAllAnimation: function () {
    5031             var el;
    5032
    5033             for (el in this.animationObjects) {
    5034                 if (this.animationObjects.hasOwnProperty(el) && Type.exists(this.animationObjects[el])) {
    5035                     this.animationObjects[el] = null;
    5036                     delete this.animationObjects[el];
    5037                 }
    5038             }
    5039
    5040             window.clearInterval(this.animationIntervalCode);
    5041             delete this.animationIntervalCode;
    5042
    5043             return this;
    5044         },
    5045
    5046         /**
    5047          * General purpose animation function. This currently only supports moving points from one place to another. This
    5048          * is faster than managing the animation per point, especially if there is more than one animated point at the same time.
    5049          * @returns {JXG.Board} Reference to the board
    5050          */
    5051         animate: function () {
    5052             var props, el, o, newCoords, r, p, c, cbtmp,
    5053                 count = 0,
    5054                 obj = null;
    5055
    5056             for (el in this.animationObjects) {
    5057                 if (this.animationObjects.hasOwnProperty(el) && Type.exists(this.animationObjects[el])) {
    5058                     count += 1;
    5059                     o = this.animationObjects[el];
    5060
    5061                     if (o.animationPath) {
    5062                         if (Type.isFunction(o.animationPath)) {
    5063                             newCoords = o.animationPath(new Date().getTime() - o.animationStart);
    5064                         } else {
    5065                             newCoords = o.animationPath.pop();
    5066                         }
    5067
    5068                         if ((!Type.exists(newCoords)) || (!Type.isArray(newCoords) && isNaN(newCoords))) {
    5069                             delete o.animationPath;
    5070                         } else {
    5071                             o.setPositionDirectly(Const.COORDS_BY_USER, newCoords);
    5072                             o.fullUpdate();
    5073                             obj = o;
    5074                         }
    5075                     }
    5076                     if (o.animationData) {
    5077                         c = 0;
    5078
    5079                         for (r in o.animationData) {
    5080                             if (o.animationData.hasOwnProperty(r)) {
    5081                                 p = o.animationData[r].pop();
    5082
    5083                                 if (!Type.exists(p)) {
    5084                                     delete o.animationData[p];
    5085                                 } else {
    5086                                     c += 1;
    5087                                     props = {};
    5088                                     props[r] = p;
    5089                                     o.setAttribute(props);
    5090                                 }
    5091                             }
    5092                         }
    5093
    5094                         if (c === 0) {
    5095                             delete o.animationData;
    5096                         }
    5097                     }
    5098
    5099                     if (!Type.exists(o.animationData) && !Type.exists(o.animationPath)) {
    5100                         this.animationObjects[el] = null;
    5101                         delete this.animationObjects[el];
    5102
    5103                         if (Type.exists(o.animationCallback)) {
    5104                             cbtmp = o.animationCallback;
    5105                             o.animationCallback = null;
    5106                             cbtmp();
    5107                         }
    5108                     }
    5109                 }
    5110             }
    5111
    5112             if (count === 0) {
    5113                 window.clearInterval(this.animationIntervalCode);
    5114                 delete this.animationIntervalCode;
    5115             } else {
    5116                 this.update(obj);
    5117             }
    5118
    5119             return this;
    5120         },
    5121
    5122         /**
    5123          * Migrate the dependency properties of the point src
    5124          * to the point dest and  delete the point src.
    5125          * For example, a circle around the point src
    5126          * receives the new center dest. The old center src
    5127          * will be deleted.
    5128          * @param {JXG.Point} src Original point which will be deleted
    5129          * @param {JXG.Point} dest New point with the dependencies of src.
    5130          * @param {Boolean} copyName Flag which decides if the name of the src element is copied to the
    5131          *  dest element.
    5132          * @returns {JXG.Board} Reference to the board
    5133          */
    5134         migratePoint: function (src, dest, copyName) {
    5135             var child, childId, prop, found, i, srcLabelId, srcHasLabel = false;
    5136
    5137             src = this.select(src);
    5138             dest = this.select(dest);
    5139
    5140             if (Type.exists(src.label)) {
    5141                 srcLabelId = src.label.id;
    5142                 srcHasLabel = true;
    5143                 this.removeObject(src.label);
    5144             }
    5145
    5146             for (childId in src.childElements) {
    5147                 if (src.childElements.hasOwnProperty(childId)) {
    5148                     child = src.childElements[childId];
    5149                     found = false;
    5150
    5151                     for (prop in child) {
    5152                         if (child.hasOwnProperty(prop)) {
    5153                             if (child[prop] ===  src) {
    5154                                 child[prop] = dest;
    5155                                 found = true;
    5156                             }
    5157                         }
    5158                     }
    5159
    5160                     if (found) {
    5161                         delete src.childElements[childId];
    5162                     }
    5163
    5164                     for (i = 0; i < child.parents.length; i++) {
    5165                         if (child.parents[i] === src.id) {
    5166                             child.parents[i] = dest.id;
    5167                         }
    5168                     }
    5169
    5170                     dest.addChild(child);
    5171                 }
    5172             }
    5173
    5174             // The destination object should receive the name
    5175             // and the label of the originating (src) object
    5176             if (copyName) {
    5177                 if (srcHasLabel) {
    5178                     delete dest.childElements[srcLabelId];
    5179                     delete dest.descendants[srcLabelId];
    5180                 }
    5181
    5182                 if (dest.label) {
    5183                     this.removeObject(dest.label);
    5184                 }
    5185
    5186                 delete this.elementsByName[dest.name];
    5187                 dest.name = src.name;
    5188                 if (srcHasLabel) {
    5189                     dest.createLabel();
    5190                 }
    5191             }
    5192
    5193             this.removeObject(src);
    5194
    5195             if (Type.exists(dest.name) && dest.name !== '') {
    5196                 this.elementsByName[dest.name] = dest;
    5197             }
    5198
    5199             this.fullUpdate();
    5200
    5201             return this;
    5202         },
    5203
    5204         /**
    5205          * Initializes color blindness simulation.
    5206          * @param {String} deficiency Describes the color blindness deficiency which is simulated. Accepted values are 'protanopia', 'deuteranopia', and 'tritanopia'.
    5207          * @returns {JXG.Board} Reference to the board
    5208          */
    5209         emulateColorblindness: function (deficiency) {
    5210             var e, o;
    5211
    5212             if (!Type.exists(deficiency)) {
    5213                 deficiency = 'none';
    5214             }
    5215
    5216             if (this.currentCBDef === deficiency) {
    5217                 return this;
    5218             }
    5219
    5220             for (e in this.objects) {
    5221                 if (this.objects.hasOwnProperty(e)) {
    5222                     o = this.objects[e];
    5223
    5224                     if (deficiency !== 'none') {
    5225                         if (this.currentCBDef === 'none') {
    5226                             // this could be accomplished by JXG.extend, too. But do not use
    5227                             // JXG.deepCopy as this could result in an infinite loop because in
    5228                             // visProp there could be geometry elements which contain the board which
    5229                             // contains all objects which contain board etc.
    5230                             o.visPropOriginal = {
    5231                                 strokecolor: o.visProp.strokecolor,
    5232                                 fillcolor: o.visProp.fillcolor,
    5233                                 highlightstrokecolor: o.visProp.highlightstrokecolor,
    5234                                 highlightfillcolor: o.visProp.highlightfillcolor
    5235                             };
    5236                         }
    5237                         o.setAttribute({
    5238                             strokecolor: Color.rgb2cb(Type.evaluate(o.visPropOriginal.strokecolor), deficiency),
    5239                             fillcolor: Color.rgb2cb(Type.evaluate(o.visPropOriginal.fillcolor), deficiency),
    5240                             highlightstrokecolor: Color.rgb2cb(Type.evaluate(o.visPropOriginal.highlightstrokecolor), deficiency),
    5241                             highlightfillcolor: Color.rgb2cb(Type.evaluate(o.visPropOriginal.highlightfillcolor), deficiency)
    5242                         });
    5243                     } else if (Type.exists(o.visPropOriginal)) {
    5244                         JXG.extend(o.visProp, o.visPropOriginal);
    5245                     }
    5246                 }
    5247             }
    5248             this.currentCBDef = deficiency;
    5249             this.update();
    5250
    5251             return this;
    5252         },
    5253
    5254         /**
    5255          * Select a single or multiple elements at once.
    5256          * @param {String|Object|function} str The name, id or a reference to a JSXGraph element on this board. An object will
    5257          * be used as a filter to return multiple elements at once filtered by the properties of the object.
    5258          * @param {Boolean} onlyByIdOrName If true (default:false) elements are only filtered by their id, name or groupId.
    5259          * The advanced filters consisting of objects or functions are ignored.
    5260          * @returns {JXG.GeometryElement|JXG.Composition}
    5261          * @example
    5262          * // select the element with name A
    5263          * board.select('A');
    5264          *
    5265          * // select all elements with strokecolor set to 'red' (but not '#ff0000')
    5266          * board.select({
    5267          *   strokeColor: 'red'
    5268          * });
    5269          *
    5270          * // select all points on or below the x axis and make them black.
    5271          * board.select({
    5272          *   elementClass: JXG.OBJECT_CLASS_POINT,
    5273          *   Y: function (v) {
    5274          *     return v <= 0;
    5275          *   }
    5276          * }).setAttribute({color: 'black'});
    5277          *
    5278          * // select all elements
    5279          * board.select(function (el) {
    5280          *   return true;
    5281          * });
    5282          */
    5283         select: function (str, onlyByIdOrName) {
    5284             var flist, olist, i, l,
    5285                 s = str;
    5286
    5287             if (s === null) {
    5288                 return s;
    5289             }
    5290
    5291             // it's a string, most likely an id or a name.
    5292             if (Type.isString(s) && s !== '') {
    5293                 // Search by ID
    5294                 if (Type.exists(this.objects[s])) {
    5295                     s = this.objects[s];
    5296                 // Search by name
    5297                 } else if (Type.exists(this.elementsByName[s])) {
    5298                     s = this.elementsByName[s];
    5299                 // Search by group ID
    5300                 } else if (Type.exists(this.groups[s])) {
    5301                     s = this.groups[s];
    5302                 }
    5303             // it's a function or an object, but not an element
    5304             } else if (!onlyByIdOrName &&
    5305                 (Type.isFunction(s) ||
    5306                  (Type.isObject(s) && !Type.isFunction(s.setAttribute))
    5307                 )) {
    5308                 flist = Type.filterElements(this.objectsList, s);
    5309
    5310                 olist = {};
    5311                 l = flist.length;
    5312                 for (i = 0; i < l; i++) {
    5313                     olist[flist[i].id] = flist[i];
    5314                 }
    5315                 s = new Composition(olist);
    5316             // it's an element which has been deleted (and still hangs around, e.g. in an attractor list
    5317             } else if (Type.isObject(s) && Type.exists(s.id) && !Type.exists(this.objects[s.id])) {
    5318                 s = null;
    5319             }
    5320
    5321             return s;
    5322         },
    5323
    5324         /**
    5325          * Checks if the given point is inside the boundingbox.
    5326          * @param {Number|JXG.Coords} x User coordinate or {@link JXG.Coords} object.
    5327          * @param {Number} [y] User coordinate. May be omitted in case <tt>x</tt> is a {@link JXG.Coords} object.
    5328          * @returns {Boolean}
    5329          */
    5330         hasPoint: function (x, y) {
    5331             var px = x,
    5332                 py = y,
    5333                 bbox = this.getBoundingBox();
    5334
    5335             if (Type.exists(x) && Type.isArray(x.usrCoords)) {
    5336                 px = x.usrCoords[1];
    5337                 py = x.usrCoords[2];
    5338             }
    5339
    5340             return !!(Type.isNumber(px) && Type.isNumber(py) &&
    5341                 bbox[0] < px && px < bbox[2] && bbox[1] > py && py > bbox[3]);
    5342         },
    5343
    5344         /**
    5345          * Update CSS transformations of type scaling. It is used to correct the mouse position
    5346          * in {@link JXG.Board.getMousePosition}.
    5347          * The inverse transformation matrix is updated on each mouseDown and touchStart event.
    5348          *
    5349          * It is up to the user to call this method after an update of the CSS transformation
    5350          * in the DOM.
    5351          */
    5352         updateCSSTransforms: function () {
    5353             var obj = this.containerObj,
    5354                 o = obj,
    5355                 o2 = obj;
    5356
    5357             this.cssTransMat = Env.getCSSTransformMatrix(o);
    5358
    5359             /*
    5360              * In Mozilla and Webkit: offsetParent seems to jump at least to the next iframe,
    5361              * if not to the body. In IE and if we are in an position:absolute environment
    5362              * offsetParent walks up the DOM hierarchy.
    5363              * In order to walk up the DOM hierarchy also in Mozilla and Webkit
    5364              * we need the parentNode steps.
    5365              */
    5366             o = o.offsetParent;
    5367             while (o) {
    5368                 this.cssTransMat = Mat.matMatMult(Env.getCSSTransformMatrix(o), this.cssTransMat);
    5369
    5370                 o2 = o2.parentNode;
    5371                 while (o2 !== o) {
    5372                     this.cssTransMat = Mat.matMatMult(Env.getCSSTransformMatrix(o), this.cssTransMat);
    5373                     o2 = o2.parentNode;
    5374                 }
    5375
    5376                 o = o.offsetParent;
    5377             }
    5378             this.cssTransMat = Mat.inverse(this.cssTransMat);
    5379
    5380             return this;
    5381         },
    5382
    5383         /**
    5384          * Start selection mode. This function can either be triggered from outside or by
    5385          * a down event together with correct key pressing. The default keys are
    5386          * shift+ctrl. But this can be changed in the options.
    5387          *
    5388          * Starting from out side can be realized for example with a button like this:
    5389          * <pre>
    5390          * 	<button onclick="board.startSelectionMode()">Start</button>
    5391          * </pre>
    5392          * @example
    5393          * //
    5394          * // Set a new bounding box from the selection rectangle
    5395          * //
    5396          * var board = JXG.JSXGraph.initBoard('jxgbox', {
    5397          *         boundingBox:[-3,2,3,-2],
    5398          *         keepAspectRatio: false,
    5399          *         axis:true,
    5400          *         selection: {
    5401          *             enabled: true,
    5402          *             needShift: false,
    5403          *             needCtrl: true,
    5404          *             withLines: false,
    5405          *             vertices: {
    5406          *                 visible: false
    5407          *             },
    5408          *             fillColor: '#ffff00',
    5409          *         }
    5410          *      });
    5411          *
    5412          * var f = function f(x) { return Math.cos(x); },
    5413          *     curve = board.create('functiongraph', [f]);
    5414          *
    5415          * board.on('stopselecting', function(){
    5416          *     var box = board.stopSelectionMode(),
    5417          *
    5418          *         // bbox has the coordinates of the selection rectangle.
    5419          *         // Attention: box[i].usrCoords have the form [1, x, y], i.e.
    5420          *         // are homogeneous coordinates.
    5421          *         bbox = box[0].usrCoords.slice(1).concat(box[1].usrCoords.slice(1));
    5422          *
    5423          *         // Set a new bounding box
    5424          *         board.setBoundingBox(bbox, false);
    5425          *  });
    5426          *
    5427          *
    5428          * </pre><div class="jxgbox" id="JXG11eff3a6-8c50-11e5-b01d-901b0e1b8723" style="width: 300px; height: 300px;"></div>
    5429          * <script type="text/javascript">
    5430          *     (function() {
    5431          *     //
    5432          *     // Set a new bounding box from the selection rectangle
    5433          *     //
    5434          *     var board = JXG.JSXGraph.initBoard('JXG11eff3a6-8c50-11e5-b01d-901b0e1b8723', {
    5435          *             boundingBox:[-3,2,3,-2],
    5436          *             keepAspectRatio: false,
    5437          *             axis:true,
    5438          *             selection: {
    5439          *                 enabled: true,
    5440          *                 needShift: false,
    5441          *                 needCtrl: true,
    5442          *                 withLines: false,
    5443          *                 vertices: {
    5444          *                     visible: false
    5445          *                 },
    5446          *                 fillColor: '#ffff00',
    5447          *             }
    5448          *        });
    5449          *
    5450          *     var f = function f(x) { return Math.cos(x); },
    5451          *         curve = board.create('functiongraph', [f]);
    5452          *
    5453          *     board.on('stopselecting', function(){
    5454          *         var box = board.stopSelectionMode(),
    5455          *
    5456          *             // bbox has the coordinates of the selection rectangle.
    5457          *             // Attention: box[i].usrCoords have the form [1, x, y], i.e.
    5458          *             // are homogeneous coordinates.
    5459          *             bbox = box[0].usrCoords.slice(1).concat(box[1].usrCoords.slice(1));
    5460          *
    5461          *             // Set a new bounding box
    5462          *             board.setBoundingBox(bbox, false);
    5463          *      });
    5464          *     })();
    5465          *
    5466          * </script><pre>
    5467          *
    5468          */
    5469         startSelectionMode: function () {
    5470             this.selectingMode = true;
    5471             this.selectionPolygon.setAttribute({visible: true});
    5472             this.selectingBox = [[0, 0], [0, 0]];
    5473             this._setSelectionPolygonFromBox();
    5474             this.selectionPolygon.fullUpdate();
    5475         },
    5476
    5477         /**
    5478          * Finalize the selection: disable selection mode and return the coordinates
    5479          * of the selection rectangle.
    5480          * @returns {Array} Coordinates of the selection rectangle. The array
    5481          * contains two {@link JXG.Coords} objects. One the upper left corner and
    5482          * the second for the lower right corner.
    5483          */
    5484         stopSelectionMode: function () {
    5485             this.selectingMode = false;
    5486             this.selectionPolygon.setAttribute({visible: false});
    5487             return [this.selectionPolygon.vertices[0].coords, this.selectionPolygon.vertices[2].coords];
    5488         },
    5489
    5490         /**
    5491          * Start the selection of a region.
    5492          * @private
    5493          * @param  {Array} pos Screen coordiates of the upper left corner of the
    5494          * selection rectangle.
    5495          */
    5496         _startSelecting: function (pos) {
    5497             this.isSelecting = true;
    5498             this.selectingBox = [ [pos[0], pos[1]], [pos[0], pos[1]] ];
    5499             this._setSelectionPolygonFromBox();
    5500         },
    5501
    5502         /**
    5503          * Update the selection rectangle during a move event.
    5504          * @private
    5505          * @param  {Array} pos Screen coordiates of the move event
    5506          */
    5507         _moveSelecting: function (pos) {
    5508             if (this.isSelecting) {
    5509                 this.selectingBox[1] = [pos[0], pos[1]];
    5510                 this._setSelectionPolygonFromBox();
    5511                 this.selectionPolygon.fullUpdate();
    5512             }
    5513         },
    5514
    5515         /**
    5516          * Update the selection rectangle during an up event. Stop selection.
    5517          * @private
    5518          * @param  {Object} evt Event object
    5519          */
    5520         _stopSelecting:  function (evt) {
    5521             var pos = this.getMousePosition(evt);
    5522
    5523             this.isSelecting = false;
    5524             this.selectingBox[1] = [pos[0], pos[1]];
    5525             this._setSelectionPolygonFromBox();
    5526         },
    5527
    5528         /**
    5529          * Update the Selection rectangle.
    5530          * @private
    5531          */
    5532         _setSelectionPolygonFromBox: function () {
    5533                var A = this.selectingBox[0],
    5534                 B = this.selectingBox[1];
    5535
    5536                this.selectionPolygon.vertices[0].setPositionDirectly(JXG.COORDS_BY_SCREEN, [A[0], A[1]]);
    5537                this.selectionPolygon.vertices[1].setPositionDirectly(JXG.COORDS_BY_SCREEN, [A[0], B[1]]);
    5538                this.selectionPolygon.vertices[2].setPositionDirectly(JXG.COORDS_BY_SCREEN, [B[0], B[1]]);
    5539                this.selectionPolygon.vertices[3].setPositionDirectly(JXG.COORDS_BY_SCREEN, [B[0], A[1]]);
    5540         },
    5541
    5542         /**
    5543          * Test if a down event should start a selection. Test if the
    5544          * required keys are pressed. If yes, {@link JXG.Board.startSelectionMode} is called.
    5545          * @param  {Object} evt Event object
    5546          */
    5547         _testForSelection: function (evt) {
    5548             if (this._isRequiredKeyPressed(evt, 'selection')) {
    5549                 if (!Type.exists(this.selectionPolygon)) {
    5550                     this._createSelectionPolygon(this.attr);
    5551                 }
    5552                 this.startSelectionMode();
    5553             }
    5554         },
    5555
    5556         /**
    5557          * Create the internal selection polygon, which will be available as board.selectionPolygon.
    5558          * @private
    5559          * @param  {Object} attr board attributes, e.g. the subobject board.attr.
    5560          * @returns {Object} pointer to the board to enable chaining.
    5561          */
    5562         _createSelectionPolygon: function(attr) {
    5563             var selectionattr;
    5564
    5565             if (!Type.exists(this.selectionPolygon)) {
    5566                 selectionattr = Type.copyAttributes(attr, Options, 'board', 'selection');
    5567                 if (selectionattr.enabled === true) {
    5568                     this.selectionPolygon = this.create('polygon', [[0, 0], [0, 0], [0, 0], [0, 0]], selectionattr);
    5569                 }
    5570             }
    5571
    5572             return this;
    5573         },
    5574
    5575         /* **************************
    5576          *     EVENT DEFINITION
    5577          * for documentation purposes
    5578          * ************************** */
    5579
    5580         //region Event handler documentation
    5581
    5582         /**
    5583          * @event
    5584          * @description Whenever the user starts to touch or click the board.
    5585          * @name JXG.Board#down
    5586          * @param {Event} e The browser's event object.
    5587          */
    5588         __evt__down: function (e) { },
    5589
    5590         /**
    5591          * @event
    5592          * @description Whenever the user starts to click on the board.
    5593          * @name JXG.Board#mousedown
    5594          * @param {Event} e The browser's event object.
    5595          */
    5596         __evt__mousedown: function (e) { },
    5597
    5598         /**
    5599          * @event
    5600          * @description Whenever the user taps the pen on the board.
    5601          * @name JXG.Board#pendown
    5602          * @param {Event} e The browser's event object.
    5603          */
    5604         __evt__pendown: function (e) { },
    5605
    5606         /**
    5607          * @event
    5608          * @description Whenever the user starts to click on the board with a
    5609          * device sending pointer events.
    5610          * @name JXG.Board#pointerdown
    5611          * @param {Event} e The browser's event object.
    5612          */
    5613         __evt__pointerdown: function (e) { },
    5614
    5615         /**
    5616          * @event
    5617          * @description Whenever the user starts to touch the board.
    5618          * @name JXG.Board#touchstart
    5619          * @param {Event} e The browser's event object.
    5620          */
    5621         __evt__touchstart: function (e) { },
    5622
    5623         /**
    5624          * @event
    5625          * @description Whenever the user stops to touch or click the board.
    5626          * @name JXG.Board#up
    5627          * @param {Event} e The browser's event object.
    5628          */
    5629         __evt__up: function (e) { },
    5630
    5631         /**
    5632          * @event
    5633          * @description Whenever the user releases the mousebutton over the board.
    5634          * @name JXG.Board#mouseup
    5635          * @param {Event} e The browser's event object.
    5636          */
    5637         __evt__mouseup: function (e) { },
    5638
    5639         /**
    5640          * @event
    5641          * @description Whenever the user releases the mousebutton over the board with a
    5642          * device sending pointer events.
    5643          * @name JXG.Board#pointerup
    5644          * @param {Event} e The browser's event object.
    5645          */
    5646         __evt__pointerup: function (e) { },
    5647
    5648         /**
    5649          * @event
    5650          * @description Whenever the user stops touching the board.
    5651          * @name JXG.Board#touchend
    5652          * @param {Event} e The browser's event object.
    5653          */
    5654         __evt__touchend: function (e) { },
    5655
    5656         /**
    5657          * @event
    5658          * @description This event is fired whenever the user is moving the finger or mouse pointer over the board.
    5659          * @name JXG.Board#move
    5660          * @param {Event} e The browser's event object.
    5661          * @param {Number} mode The mode the board currently is in
    5662          * @see JXG.Board#mode
    5663          */
    5664         __evt__move: function (e, mode) { },
    5665
    5666         /**
    5667          * @event
    5668          * @description This event is fired whenever the user is moving the mouse over the board.
    5669          * @name JXG.Board#mousemove
    5670          * @param {Event} e The browser's event object.
    5671          * @param {Number} mode The mode the board currently is in
    5672          * @see JXG.Board#mode
    5673          */
    5674         __evt__mousemove: function (e, mode) { },
    5675
    5676         /**
    5677          * @event
    5678          * @description This event is fired whenever the user is moving the pen over the board.
    5679          * @name JXG.Board#penmove
    5680          * @param {Event} e The browser's event object.
    5681          * @param {Number} mode The mode the board currently is in
    5682          * @see JXG.Board#mode
    5683          */
    5684         __evt__penmove: function (e, mode) { },
    5685
    5686         /**
    5687          * @event
    5688          * @description This event is fired whenever the user is moving the mouse over the board  with a
    5689          * device sending pointer events.
    5690          * @name JXG.Board#pointermove
    5691          * @param {Event} e The browser's event object.
    5692          * @param {Number} mode The mode the board currently is in
    5693          * @see JXG.Board#mode
    5694          */
    5695         __evt__pointermove: function (e, mode) { },
    5696
    5697         /**
    5698          * @event
    5699          * @description This event is fired whenever the user is moving the finger over the board.
    5700          * @name JXG.Board#touchmove
    5701          * @param {Event} e The browser's event object.
    5702          * @param {Number} mode The mode the board currently is in
    5703          * @see JXG.Board#mode
    5704          */
    5705         __evt__touchmove: function (e, mode) { },
    5706
    5707         /**
    5708          * @event
    5709          * @description Whenever an element is highlighted this event is fired.
    5710          * @name JXG.Board#hit
    5711          * @param {Event} e The browser's event object.
    5712          * @param {JXG.GeometryElement} el The hit element.
    5713          * @param target
    5714          *
    5715          * @example
    5716          * var c = board.create('circle', [[1, 1], 2]);
    5717          * board.on('hit', function(evt, el) {
    5718          *     console.log("Hit element", el);
    5719          * });
    5720          *
    5721          * </pre><div id="JXG19eb31ac-88e6-11e8-bcb5-901b0e1b8723" class="jxgbox" style="width: 300px; height: 300px;"></div>
    5722          * <script type="text/javascript">
    5723          *     (function() {
    5724          *         var board = JXG.JSXGraph.initBoard('JXG19eb31ac-88e6-11e8-bcb5-901b0e1b8723',
    5725          *             {boundingbox: [-8, 8, 8,-8], axis: true, showcopyright: false, shownavigation: false});
    5726          *     var c = board.create('circle', [[1, 1], 2]);
    5727          *     board.on('hit', function(evt, el) {
    5728          *         console.log("Hit element", el);
    5729          *     });
    5730          *
    5731          *     })();
    5732          *
    5733          * </script><pre>
    5734          */
    5735         __evt__hit: function (e, el, target) { },
    5736
    5737         /**
    5738          * @event
    5739          * @description Whenever an element is highlighted this event is fired.
    5740          * @name JXG.Board#mousehit
    5741          * @see JXG.Board#hit
    5742          * @param {Event} e The browser's event object.
    5743          * @param {JXG.GeometryElement} el The hit element.
    5744          * @param target
    5745          */
    5746         __evt__mousehit: function (e, el, target) { },
    5747
    5748         /**
    5749          * @event
    5750          * @description This board is updated.
    5751          * @name JXG.Board#update
    5752          */
    5753         __evt__update: function () { },
    5754
    5755         /**
    5756          * @event
    5757          * @description The bounding box of the board has changed.
    5758          * @name JXG.Board#boundingbox
    5759          */
    5760         __evt__boundingbox: function () { },
    5761
    5762         /**
    5763          * @event
    5764          * @description Select a region is started during a down event or by calling
    5765          * {@link JXG.Board.startSelectionMode}
    5766          * @name JXG.Board#startselecting
    5767          */
    5768          __evt__startselecting: function () { },
    5769
    5770          /**
    5771          * @event
    5772          * @description Select a region is started during a down event
    5773          * from a device sending mouse events or by calling
    5774          * {@link JXG.Board.startSelectionMode}.
    5775          * @name JXG.Board#mousestartselecting
    5776          */
    5777          __evt__mousestartselecting: function () { },
    5778
    5779          /**
    5780          * @event
    5781          * @description Select a region is started during a down event
    5782          * from a device sending pointer events or by calling
    5783          * {@link JXG.Board.startSelectionMode}.
    5784          * @name JXG.Board#pointerstartselecting
    5785          */
    5786          __evt__pointerstartselecting: function () { },
    5787
    5788          /**
    5789          * @event
    5790          * @description Select a region is started during a down event
    5791          * from a device sending touch events or by calling
    5792          * {@link JXG.Board.startSelectionMode}.
    5793          * @name JXG.Board#touchstartselecting
    5794          */
    5795          __evt__touchstartselecting: function () { },
    5796
    5797          /**
    5798           * @event
    5799           * @description Selection of a region is stopped during an up event.
    5800           * @name JXG.Board#stopselecting
    5801           */
    5802          __evt__stopselecting: function () { },
    5803
    5804          /**
    5805          * @event
    5806          * @description Selection of a region is stopped during an up event
    5807          * from a device sending mouse events.
    5808          * @name JXG.Board#mousestopselecting
    5809          */
    5810          __evt__mousestopselecting: function () { },
    5811
    5812          /**
    5813          * @event
    5814          * @description Selection of a region is stopped during an up event
    5815          * from a device sending pointer events.
    5816          * @name JXG.Board#pointerstopselecting
    5817          */
    5818          __evt__pointerstopselecting: function () { },
    5819
    5820          /**
    5821          * @event
    5822          * @description Selection of a region is stopped during an up event
    5823          * from a device sending touch events.
    5824          * @name JXG.Board#touchstopselecting
    5825          */
    5826          __evt__touchstopselecting: function () { },
    5827
    5828          /**
    5829          * @event
    5830          * @description A move event while selecting of a region is active.
    5831          * @name JXG.Board#moveselecting
    5832          */
    5833          __evt__moveselecting: function () { },
    5834
    5835          /**
    5836          * @event
    5837          * @description A move event while selecting of a region is active
    5838          * from a device sending mouse events.
    5839          * @name JXG.Board#mousemoveselecting
    5840          */
    5841          __evt__mousemoveselecting: function () { },
    5842
    5843          /**
    5844          * @event
    5845          * @description Select a region is started during a down event
    5846          * from a device sending mouse events.
    5847          * @name JXG.Board#pointermoveselecting
    5848          */
    5849          __evt__pointermoveselecting: function () { },
    5850
    5851          /**
    5852          * @event
    5853          * @description Select a region is started during a down event
    5854          * from a device sending touch events.
    5855          * @name JXG.Board#touchmoveselecting
    5856          */
    5857          __evt__touchmoveselecting: function () { },
    5858
    5859         /**
    5860          * @ignore
    5861          */
    5862         __evt: function () {},
    5863
    5864         //endregion
    5865
    5866         /**
    5867          * Expand the JSXGraph construction to fullscreen.
    5868          * In order to preserve the proportions of the JSXGraph element,
    5869          * a wrapper div is created which is set to fullscreen.
    5870          * <p>
    5871          * The wrapping div has the CSS class 'jxgbox_wrap_private' which is
    5872          * defined in the file 'jsxgraph.css'
    5873          * <p>
    5874          * This feature is not available on iPhones (as of December 2021).
    5875          *
    5876          * @param {String} id (Optional) id of the div element which is brought to fullscreen.
    5877          * If not provided, this defaults to the JSXGraph div. However, it may be necessary for the aspect ratio trick
    5878          * which using padding-bottom/top and an out div element. Then, the id of the outer div has to be supplied.
    5879          *
    5880          * @return {JXG.Board} Reference to the board
    5881          *
    5882          * @example
    5883          * <div id='jxgbox' class='jxgbox' style='width:500px; height:200px;'></div>
    5884          * <button onClick="board.toFullscreen()">Fullscreen</button>
    5885          *
    5886          * <script language="Javascript" type='text/javascript'>
    5887          * var board = JXG.JSXGraph.initBoard('jxgbox', {axis:true, boundingbox:[-5,5,5,-5]});
    5888          * var p = board.create('point', [0, 1]);
    5889          * </script>
    5890          *
    5891          * </pre><div id="JXGd5bab8b6-fd40-11e8-ab14-901b0e1b8723" class="jxgbox" style="width: 300px; height: 300px;"></div>
    5892          * <script type="text/javascript">
    5893          *      var board_d5bab8b6;
    5894          *     (function() {
    5895          *         var board = JXG.JSXGraph.initBoard('JXGd5bab8b6-fd40-11e8-ab14-901b0e1b8723',
    5896          *             {boundingbox:[-5,5,5,-5], axis: true, showcopyright: false, shownavigation: false});
    5897          *         var p = board.create('point', [0, 1]);
    5898          *         board_d5bab8b6 = board;
    5899          *     })();
    5900          * </script>
    5901          * <button onClick="board_d5bab8b6.toFullscreen()">Fullscreen</button>
    5902          * <pre>
    5903          *
    5904          * @example
    5905          * <div id='outer' style='max-width: 500px; margin: 0 auto;'>
    5906          * <div id='jxgbox' class='jxgbox' style='height: 0; padding-bottom: 100%'></div>
    5907          * </div>
    5908          * <button onClick="board.toFullscreen('outer')">Fullscreen</button>
    5909          *
    5910          * <script language="Javascript" type='text/javascript'>
    5911          * var board = JXG.JSXGraph.initBoard('jxgbox', {
    5912          *     axis:true,
    5913          *     boundingbox:[-5,5,5,-5],
    5914          *     fullscreen: { id: 'outer' },
    5915          *     showFullscreen: true
    5916          * });
    5917          * var p = board.create('point', [-2, 3], {});
    5918          * </script>
    5919          *
    5920          * </pre><div id="JXG7103f6b_outer" style='max-width: 500px; margin: 0 auto;'>
    5921          * <div id="JXG7103f6be-6993-4ff8-8133-c78e50a8afac" class="jxgbox" style="height: 0; padding-bottom: 100%;"></div>
    5922          * </div>
    5923          * <button onClick="board_JXG7103f6be.toFullscreen('JXG7103f6b_outer')">Fullscreen</button>
    5924          * <script type="text/javascript">
    5925          *     var board_JXG7103f6be;
    5926          *     (function() {
    5927          *         var board = JXG.JSXGraph.initBoard('JXG7103f6be-6993-4ff8-8133-c78e50a8afac',
    5928          *             {boundingbox: [-8, 8, 8,-8], axis: true, fullscreen: { id: 'JXG7103f6b_outer' }, showFullscreen: true,
    5929          *              showcopyright: false, shownavigation: false});
    5930          *     var p = board.create('point', [-2, 3], {});
    5931          *     board_JXG7103f6be = board;
    5932          *     })();
    5933          *
    5934          * </script><pre>
    5935          *
    5936          *
    5937          */
    5938         toFullscreen: function (id) {
    5939             var wrap_id, wrap_node, inner_node;
    5940
    5941             id = id || this.container;
    5942             this._fullscreen_inner_id = id;
    5943             inner_node = document.getElementById(id);
    5944             wrap_id = 'fullscreenwrap_' + id;
    5945
    5946             // Wrap a div around the JSXGraph div.
    5947             if (this.document.getElementById(wrap_id)) {
    5948                 wrap_node = this.document.getElementById(wrap_id);
    5949             } else {
    5950                 wrap_node = document.createElement('div');
    5951                 wrap_node.classList.add('JXG_wrap_private');
    5952                 wrap_node.setAttribute('id', wrap_id);
    5953                 inner_node.parentNode.insertBefore(wrap_node, inner_node);
    5954                 wrap_node.appendChild(inner_node);
    5955             }
    5956
    5957             // Get the real width and height of the JSXGraph div
    5958             // and determine the scaling and vertical shift amount
    5959             this._fullscreen_res = Env._getScaleFactors(inner_node);
    5960
    5961             // Trigger fullscreen mode
    5962             wrap_node.requestFullscreen = wrap_node.requestFullscreen ||
    5963                 wrap_node.webkitRequestFullscreen ||
    5964                 wrap_node.mozRequestFullScreen ||
    5965                 wrap_node.msRequestFullscreen;
    5966
    5967             if (wrap_node.requestFullscreen) {
    5968                 wrap_node.requestFullscreen();
    5969             }
    5970
    5971             return this;
    5972         },
    5973
    5974         /**
    5975          * If fullscreen mode is toggled, the possible CSS transformations
    5976          * which are applied to the JSXGraph canvas have to be reread.
    5977          * Otherwise the position of upper left corner is wrongly interpreted.
    5978          *
    5979          * @param  {Object} evt fullscreen event object (unused)
    5980          */
    5981         fullscreenListener: function (evt) {
    5982             var res, inner_id, inner_node;
    5983
    5984             inner_id = this._fullscreen_inner_id;
    5985             if (!Type.exists(inner_id)) {
    5986                 return;
    5987             }
    5988
    5989             document.fullscreenElement = document.fullscreenElement ||
    5990                     document.webkitFullscreenElement ||
    5991                     document.mozFullscreenElement ||
    5992                     document.msFullscreenElement;
    5993
    5994             inner_node = document.getElementById(inner_id);
    5995             // If full screen mode is started we have to remove CSS margin around the JSXGraph div.
    5996             // Otherwise, the positioning of the fullscreen div will be false.
    5997             // When leaving the fullscreen mode, the margin is put back in.
    5998             if (document.fullscreenElement) {
    5999                 // Just entered fullscreen mode
    6000
    6001                 // Get the data computed in board.toFullscreen()
    6002                 res = this._fullscreen_res;
    6003
    6004                 // Store the scaling data.
    6005                 // It is used in AbstractRenderer.updateText to restore the scaling matrix
    6006                 // which is removed by MathJax.
    6007                 // Further, the CSS margin has to be removed when in fullscreen mode,
    6008                 // and must be restored later.
    6009                 inner_node._cssFullscreenStore = {
    6010                     id: document.fullscreenElement.id,
    6011                     isFullscreen: true,
    6012                     margin: inner_node.style.margin,
    6013                     width: inner_node.style.width,
    6014                     scale: res.scale,
    6015                     vshift: res.vshift
    6016                 };
    6017
    6018                 inner_node.style.margin = '';
    6019                 inner_node.style.width = res.width + 'px';
    6020
    6021                 // Do the shifting and scaling via CSS pseudo rules
    6022                 // We do this after fullscreen mode has been established to get the correct size
    6023                 // of the JSXGraph div.
    6024                 Env.scaleJSXGraphDiv(document.fullscreenElement.id, inner_id, res.scale, res.vshift);
    6025
    6026                 // Clear document.fullscreenElement, because Safari doesn't to it and
    6027                 // when leaving full screen mode it is still set.
    6028                 document.fullscreenElement = null;
    6029
    6030             } else if (Type.exists(inner_node._cssFullscreenStore)) {
    6031                 // Just left the fullscreen mode
    6032
    6033                 // Remove the CSS rules added in Env.scaleJSXGraphDiv
    6034                 try {
    6035                     document.styleSheets[document.styleSheets.length - 1].deleteRule(0);
    6036                 } catch (err) {
    6037                     console.log('JSXGraph: Could not remove CSS rules for full screen mode');
    6038                 }
    6039
    6040                 inner_node._cssFullscreenStore.isFullscreen = false;
    6041                 inner_node.style.margin = inner_node._cssFullscreenStore.margin;
    6042                 inner_node.style.width = inner_node._cssFullscreenStore.width;
    6043
    6044             }
    6045
    6046             this.updateCSSTransforms();
    6047         },
    6048
    6049         /**
    6050          * Function to animate a curve rolling on another curve.
    6051          * @param {Curve} c1 JSXGraph curve building the floor where c2 rolls
    6052          * @param {Curve} c2 JSXGraph curve which rolls on c1.
    6053          * @param {number} start_c1 The parameter t such that c1(t) touches c2. This is the start position of the
    6054          *                          rolling process
    6055          * @param {Number} stepsize Increase in t in each step for the curve c1
    6056          * @param {Number} direction
    6057          * @param {Number} time Delay time for setInterval()
    6058          * @param {Array} pointlist Array of points which are rolled in each step. This list should contain
    6059          *      all points which define c2 and gliders on c2.
    6060          *
    6061          * @example
    6062          *
    6063          * // Line which will be the floor to roll upon.
    6064          * var line = brd.create('curve', [function (t) { return t;}, function (t){ return 1;}], {strokeWidth:6});
    6065          * // Center of the rolling circle
    6066          * var C = brd.create('point',[0,2],{name:'C'});
    6067          * // Starting point of the rolling circle
    6068          * var P = brd.create('point',[0,1],{name:'P', trace:true});
    6069          * // Circle defined as a curve. The circle "starts" at P, i.e. circle(0) = P
    6070          * var circle = brd.create('curve',[
    6071          *           function (t){var d = P.Dist(C),
    6072          *                           beta = JXG.Math.Geometry.rad([C.X()+1,C.Y()],C,P);
    6073          *                       t += beta;
    6074          *                       return C.X()+d*Math.cos(t);
    6075          *           },
    6076          *           function (t){var d = P.Dist(C),
    6077          *                           beta = JXG.Math.Geometry.rad([C.X()+1,C.Y()],C,P);
    6078          *                       t += beta;
    6079          *                       return C.Y()+d*Math.sin(t);
    6080          *           },
    6081          *           0,2*Math.PI],
    6082          *           {strokeWidth:6, strokeColor:'green'});
    6083          *
    6084          * // Point on circle
    6085          * var B = brd.create('glider',[0,2,circle],{name:'B', color:'blue',trace:false});
    6086          * var roll = brd.createRoulette(line, circle, 0, Math.PI/20, 1, 100, [C,P,B]);
    6087          * roll.start() // Start the rolling, to be stopped by roll.stop()
    6088          *
    6089          * </pre><div class="jxgbox" id="JXGe5e1b53c-a036-4a46-9e35-190d196beca5" style="width: 300px; height: 300px;"></div>
    6090          * <script type="text/javascript">
    6091          * var brd = JXG.JSXGraph.initBoard('JXGe5e1b53c-a036-4a46-9e35-190d196beca5', {boundingbox: [-5, 5, 5, -5], axis: true, showcopyright:false, shownavigation: false});
    6092          * // Line which will be the floor to roll upon.
    6093          * var line = brd.create('curve', [function (t) { return t;}, function (t){ return 1;}], {strokeWidth:6});
    6094          * // Center of the rolling circle
    6095          * var C = brd.create('point',[0,2],{name:'C'});
    6096          * // Starting point of the rolling circle
    6097          * var P = brd.create('point',[0,1],{name:'P', trace:true});
    6098          * // Circle defined as a curve. The circle "starts" at P, i.e. circle(0) = P
    6099          * var circle = brd.create('curve',[
    6100          *           function (t){var d = P.Dist(C),
    6101          *                           beta = JXG.Math.Geometry.rad([C.X()+1,C.Y()],C,P);
    6102          *                       t += beta;
    6103          *                       return C.X()+d*Math.cos(t);
    6104          *           },
    6105          *           function (t){var d = P.Dist(C),
    6106          *                           beta = JXG.Math.Geometry.rad([C.X()+1,C.Y()],C,P);
    6107          *                       t += beta;
    6108          *                       return C.Y()+d*Math.sin(t);
    6109          *           },
    6110          *           0,2*Math.PI],
    6111          *           {strokeWidth:6, strokeColor:'green'});
    6112          *
    6113          * // Point on circle
    6114          * var B = brd.create('glider',[0,2,circle],{name:'B', color:'blue',trace:false});
    6115          * var roll = brd.createRoulette(line, circle, 0, Math.PI/20, 1, 100, [C,P,B]);
    6116          * roll.start() // Start the rolling, to be stopped by roll.stop()
    6117          * </script><pre>
    6118          */
    6119         createRoulette: function (c1, c2, start_c1, stepsize, direction, time, pointlist) {
    6120             var brd = this,
    6121                 Roulette = function () {
    6122                     var alpha = 0, Tx = 0, Ty = 0,
    6123                         t1 = start_c1,
    6124                         t2 = Numerics.root(
    6125                             function (t) {
    6126                                 var c1x = c1.X(t1),
    6127                                     c1y = c1.Y(t1),
    6128                                     c2x = c2.X(t),
    6129                                     c2y = c2.Y(t);
    6130
    6131                                 return (c1x - c2x) * (c1x - c2x) + (c1y - c2y) * (c1y - c2y);
    6132                             },
    6133                             [0, Math.PI * 2]
    6134                         ),
    6135                         t1_new = 0.0, t2_new = 0.0,
    6136                         c1dist,
    6137
    6138                         rotation = brd.create('transform', [
    6139                             function () {
    6140                                 return alpha;
    6141                             }
    6142                         ], {type: 'rotate'}),
    6143
    6144                         rotationLocal = brd.create('transform', [
    6145                             function () {
    6146                                 return alpha;
    6147                             },
    6148                             function () {
    6149                                 return c1.X(t1);
    6150                             },
    6151                             function () {
    6152                                 return c1.Y(t1);
    6153                             }
    6154                         ], {type: 'rotate'}),
    6155
    6156                         translate = brd.create('transform', [
    6157                             function () {
    6158                                 return Tx;
    6159                             },
    6160                             function () {
    6161                                 return Ty;
    6162                             }
    6163                         ], {type: 'translate'}),
    6164
    6165                         // arc length via Simpson's rule.
    6166                         arclen = function (c, a, b) {
    6167                             var cpxa = Numerics.D(c.X)(a),
    6168                                 cpya = Numerics.D(c.Y)(a),
    6169                                 cpxb = Numerics.D(c.X)(b),
    6170                                 cpyb = Numerics.D(c.Y)(b),
    6171                                 cpxab = Numerics.D(c.X)((a + b) * 0.5),
    6172                                 cpyab = Numerics.D(c.Y)((a + b) * 0.5),
    6173
    6174                                 fa = Math.sqrt(cpxa * cpxa + cpya * cpya),
    6175                                 fb = Math.sqrt(cpxb * cpxb + cpyb * cpyb),
    6176                                 fab = Math.sqrt(cpxab * cpxab + cpyab * cpyab);
    6177
    6178                             return (fa + 4 * fab + fb) * (b - a) / 6;
    6179                         },
    6180
    6181                         exactDist = function (t) {
    6182                             return c1dist - arclen(c2, t2, t);
    6183                         },
    6184
    6185                         beta = Math.PI / 18,
    6186                         beta9 = beta * 9,
    6187                         interval = null;
    6188
    6189                     this.rolling = function () {
    6190                         var h, g, hp, gp, z;
    6191
    6192                         t1_new = t1 + direction * stepsize;
    6193
    6194                         // arc length between c1(t1) and c1(t1_new)
    6195                         c1dist = arclen(c1, t1, t1_new);
    6196
    6197                         // find t2_new such that arc length between c2(t2) and c1(t2_new) equals c1dist.
    6198                         t2_new = Numerics.root(exactDist, t2);
    6199
    6200                         // c1(t) as complex number
    6201                         h = new Complex(c1.X(t1_new), c1.Y(t1_new));
    6202
    6203                         // c2(t) as complex number
    6204                         g = new Complex(c2.X(t2_new), c2.Y(t2_new));
    6205
    6206                         hp = new Complex(Numerics.D(c1.X)(t1_new), Numerics.D(c1.Y)(t1_new));
    6207                         gp = new Complex(Numerics.D(c2.X)(t2_new), Numerics.D(c2.Y)(t2_new));
    6208
    6209                         // z is angle between the tangents of c1 at t1_new, and c2 at t2_new
    6210                         z = Complex.C.div(hp, gp);
    6211
    6212                         alpha = Math.atan2(z.imaginary, z.real);
    6213                         // Normalizing the quotient
    6214                         z.div(Complex.C.abs(z));
    6215                         z.mult(g);
    6216                         Tx = h.real - z.real;
    6217
    6218                         // T = h(t1_new)-g(t2_new)*h'(t1_new)/g'(t2_new);
    6219                         Ty = h.imaginary - z.imaginary;
    6220
    6221                         // -(10-90) degrees: make corners roll smoothly
    6222                         if (alpha < -beta && alpha > -beta9) {
    6223                             alpha = -beta;
    6224                             rotationLocal.applyOnce(pointlist);
    6225                         } else if (alpha > beta && alpha < beta9) {
    6226                             alpha = beta;
    6227                             rotationLocal.applyOnce(pointlist);
    6228                         } else {
    6229                             rotation.applyOnce(pointlist);
    6230                             translate.applyOnce(pointlist);
    6231                             t1 = t1_new;
    6232                             t2 = t2_new;
    6233                         }
    6234                         brd.update();
    6235                     };
    6236
    6237                     this.start = function () {
    6238                         if (time > 0) {
    6239                             interval = window.setInterval(this.rolling, time);
    6240                         }
    6241                         return this;
    6242                     };
    6243
    6244                     this.stop = function () {
    6245                         window.clearInterval(interval);
    6246                         return this;
    6247                     };
    6248                     return this;
    6249                 };
    6250             return new Roulette();
    6251         }
    6252     });
    6253
    6254     return JXG.Board;
    6255 });
    6256