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