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

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

    818                 cPos[0] += 10;
    819                 cPos[1] += 25;
    820             }
    821
    822             // add border width
    823             cPos[0] += Env.getProp(container, 'border-left-width');
    824             cPos[1] += Env.getProp(container, 'border-top-width');
    825
    826             // vml seems to ignore paddings
    827             if (this.renderer.type !== 'vml') {
    828                 // add padding
    829                 cPos[0] += Env.getProp(container, 'padding-left');
    830                 cPos[1] += Env.getProp(container, 'padding-top');
    831             }
    832
    833             cPos[0] += this.attr.offsetx;
    834             cPos[1] += this.attr.offsety;
    835
    836             this.cPos = cPos.slice();
    837             return this.cPos;
    838         },
    839
    840         /**
    841          * Get the position of the mouse in screen coordinates, relative to the upper left corner
    842          * of the host tag.
    843          * @param {Event} e Event object given by the browser.
    844          * @param {Number} [i] Only use in case of touch events. This determines which finger to use and should not be set
    845          * for mouseevents.
    846          * @returns {Array} Contains the mouse coordinates in user coordinates, ready  for {@link JXG.Coords}
    847          */
    848         getMousePosition: function (e, i) {
    849             var cPos = this.getCoordsTopLeftCorner(),
    850                 absPos,
    851                 v;
    852
    853             // position of mouse cursor relative to containers position of container
    854             absPos = Env.getPosition(e, i, this.document);
    855
    856             /**
    857              * In case there has been no down event before.
    858              */
    859             if (!Type.exists(this.cssTransMat)) {
    860                 this.updateCSSTransforms();
    861             }
    862             v = [1, absPos[0] - cPos[0], absPos[1] - cPos[1]];
    863             v = Mat.matVecMult(this.cssTransMat, v);
    864             v[1] /= v[0];
    865             v[2] /= v[0];
    866             return [v[1], v[2]];
    867
    868             // Method without CSS transformation
    869             /*
    870              return [absPos[0] - cPos[0], absPos[1] - cPos[1]];
    871              */
    872         },
    873
    874         /**
    875          * Initiate moving the origin. This is used in mouseDown and touchStart listeners.
    876          * @param {Number} x Current mouse/touch coordinates
    877          * @param {Number} y Current mouse/touch coordinates
    878          */
    879         initMoveOrigin: function (x, y) {
    880             this.drag_dx = x - this.origin.scrCoords[1];
    881             this.drag_dy = y - this.origin.scrCoords[2];
    882
    883             this.mode = this.BOARD_MODE_MOVE_ORIGIN;
    884             this.updateQuality = this.BOARD_QUALITY_LOW;
    885         },
    886
    887         /**
    888          * Collects all elements below the current mouse pointer and fulfilling the following constraints:
    889          * <ul><li>isDraggable</li><li>visible</li><li>not fixed</li><li>not frozen</li></ul>
    890          * @param {Number} x Current mouse/touch coordinates
    891          * @param {Number} y current mouse/touch coordinates
    892          * @param {Object} evt An event object
    893          * @param {String} type What type of event? 'touch' or 'mouse'.
    894          * @returns {Array} A list of geometric elements.
    895          */
    896         initMoveObject: function (x, y, evt, type) {
    897             var pEl,
    898                 el,
    899                 collect = [],
    900                 offset = [],
    901                 haspoint,
    902                 len = this.objectsList.length,
    903                 dragEl = {visProp: {layer: -10000}};
    904
    905             //for (el in this.objects) {
    906             for (el = 0; el < len; el++) {
    907                 pEl = this.objectsList[el];
    908                 haspoint = pEl.hasPoint && pEl.hasPoint(x, y);
    909
    910                 if (pEl.visPropCalc.visible && haspoint) {
    911                     pEl.triggerEventHandlers([type + 'down', 'down'], [evt]);
    912                     this.downObjects.push(pEl);
    913                 }
    914
    915                 if (((this.geonextCompatibilityMode &&
    916                         (Type.isPoint(pEl) ||
    917                           pEl.elementClass === Const.OBJECT_CLASS_TEXT)) ||
    918                         !this.geonextCompatibilityMode) &&
    919                         pEl.isDraggable &&
    920                         pEl.visPropCalc.visible &&
    921                         (!Type.evaluate(pEl.visProp.fixed)) && /*(!pEl.visProp.frozen) &&*/
    922                         haspoint) {
    923                     // Elements in the highest layer get priority.
    924                     if (pEl.visProp.layer > dragEl.visProp.layer ||
    925                             (pEl.visProp.layer === dragEl.visProp.layer &&
    926                              pEl.lastDragTime.getTime() >= dragEl.lastDragTime.getTime()
    927                             )) {
    928                         // If an element and its label have the focus
    929                         // simultaneously, the element is taken.
    930                         // This only works if we assume that every browser runs
    931                         // through this.objects in the right order, i.e. an element A
    932                         // added before element B turns up here before B does.
    933                         if (!this.attr.ignorelabels ||
    934                             (!Type.exists(dragEl.label) || pEl !== dragEl.label)) {
    935                             dragEl = pEl;
    936                             collect.push(dragEl);
    937
    938                             // Save offset for large coords elements.
    939                             if (Type.exists(dragEl.coords)) {
    940                                 offset.push(Statistics.subtract(dragEl.coords.scrCoords.slice(1), [x, y]));
    941                             } else {
    942                                 offset.push([0, 0]);
    943                             }
    944
    945                             // we can't drop out of this loop because of the event handling system
    946                             //if (this.attr.takefirst) {
    947                             //    return collect;
    948                             //}
    949                         }
    950                     }
    951                 }
    952             }
    953
    954             if (collect.length > 0) {
    955                 this.mode = this.BOARD_MODE_DRAG;
    956             }
    957
    958             // A one-element array is returned.
    959             if (this.attr.takefirst) {
    960                 collect.length = 1;
    961                 this._drag_offset = offset[0];
    962             } else {
    963                 collect = collect.slice(-1);
    964                 this._drag_offset = offset[offset.length - 1];
    965             }
    966
    967             if (!this._drag_offset) {
    968                 this._drag_offset = [0, 0];
    969             }
    970
    971             // Move drag element to the top of the layer
    972             if (this.renderer.type === 'svg' &&
    973                 Type.exists(collect[0]) &&
    974                 Type.evaluate(collect[0].visProp.dragtotopoflayer) &&
    975                 collect.length === 1 &&
    976                 Type.exists(collect[0].rendNode)) {
    977
    978                 collect[0].rendNode.parentNode.appendChild(collect[0].rendNode);
    979             }
    980
    981             return collect;
    982         },
    983
    984         /**
    985          * Moves an object.
    986          * @param {Number} x Coordinate
    987          * @param {Number} y Coordinate
    988          * @param {Object} o The touch object that is dragged: {JXG.Board#mouse} or {JXG.Board#touches}.
    989          * @param {Object} evt The event object.
    990          * @param {String} type Mouse or touch event?
    991          */
    992         moveObject: function (x, y, o, evt, type) {
    993             var newPos = new Coords(Const.COORDS_BY_SCREEN, this.getScrCoordsOfMouse(x, y), this),
    994                 drag,
    995                 dragScrCoords, newDragScrCoords;
    996
    997             if (!(o && o.obj)) {
    998                 return;
    999             }
    1000             drag = o.obj;
    1001
    1002             // Save updates for very small movements of coordsElements, see below
    1003             if (drag.coords) {
    1004                 dragScrCoords = drag.coords.scrCoords.slice();
    1005             }
    1006
    1007             /*
    1008              * Save the position.
    1009              */
    1010             this.drag_position = [newPos.scrCoords[1], newPos.scrCoords[2]];
    1011             this.drag_position = Statistics.add(this.drag_position, this._drag_offset);
    1012             //
    1013             // We have to distinguish between CoordsElements and other elements like lines.
    1014             // The latter need the difference between two move events.
    1015             if (Type.exists(drag.coords)) {
    1016                 drag.setPositionDirectly(Const.COORDS_BY_SCREEN, this.drag_position);
    1017             } else {
    1018                 this.showInfobox(false);
    1019                                     // Hide infobox in case the user has touched an intersection point
    1020                                     // and drags the underlying line now.
    1021
    1022                 if (!isNaN(o.targets[0].Xprev + o.targets[0].Yprev)) {
    1023                     drag.setPositionDirectly(Const.COORDS_BY_SCREEN,
    1024                         [newPos.scrCoords[1], newPos.scrCoords[2]],
    1025                         [o.targets[0].Xprev, o.targets[0].Yprev]
    1026                         );
    1027                 }
    1028                 // Remember the actual position for the next move event. Then we are able to
    1029                 // compute the difference vector.
    1030                 o.targets[0].Xprev = newPos.scrCoords[1];
    1031                 o.targets[0].Yprev = newPos.scrCoords[2];
    1032             }
    1033             // This may be necessary for some gliders
    1034             drag.prepareUpdate().update(false).updateRenderer();
    1035             this.updateInfobox(drag);
    1036             drag.prepareUpdate().update(true).updateRenderer();
    1037             if (drag.coords) {
    1038                 newDragScrCoords = drag.coords.scrCoords;
    1039             }
    1040
    1041             // No updates for very small movements of coordsElements
    1042             if (!drag.coords ||
    1043                 dragScrCoords[1] !== newDragScrCoords[1] || dragScrCoords[2] !== newDragScrCoords[2]) {
    1044
    1045                 drag.triggerEventHandlers([type + 'drag', 'drag'], [evt]);
    1046                 this.update();
    1047             }
    1048             drag.highlight(true);
    1049
    1050             drag.lastDragTime = new Date();
    1051         },
    1052
    1053         /**
    1054          * Moves elements in multitouch mode.
    1055          * @param {Array} p1 x,y coordinates of first touch
    1056          * @param {Array} p2 x,y coordinates of second touch
    1057          * @param {Object} o The touch object that is dragged: {JXG.Board#touches}.
    1058          * @param {Object} evt The event object that lead to this movement.
    1059          */
    1060         twoFingerMove: function (p1, p2, o, evt) {
    1061             var np1c, np2c, drag;
    1062             if (Type.exists(o) && Type.exists(o.obj)) {
    1063                 drag = o.obj;
    1064             } else {
    1065                 return;
    1066             }
    1067
    1068             // New finger position
    1069             np1c = new Coords(Const.COORDS_BY_SCREEN, this.getScrCoordsOfMouse(p1[0], p1[1]), this);
    1070             np2c = new Coords(Const.COORDS_BY_SCREEN, this.getScrCoordsOfMouse(p2[0], p2[1]), this);
    1071
    1072             if (drag.elementClass === Const.OBJECT_CLASS_LINE ||
    1073                     drag.type === Const.OBJECT_TYPE_POLYGON) {
    1074                 this.twoFingerTouchObject(np1c, np2c, o, drag);
    1075             } else if (drag.elementClass === Const.OBJECT_CLASS_CIRCLE) {
    1076                 this.twoFingerTouchCircle(np1c, np2c, o, drag);
    1077             }
    1078             drag.triggerEventHandlers(['touchdrag', 'drag'], [evt]);
    1079
    1080             o.targets[0].Xprev = np1c.scrCoords[1];
    1081             o.targets[0].Yprev = np1c.scrCoords[2];
    1082             o.targets[1].Xprev = np2c.scrCoords[1];
    1083             o.targets[1].Yprev = np2c.scrCoords[2];
    1084         },
    1085
    1086         /**
    1087          * Moves a line or polygon with two fingers
    1088          * @param {JXG.Coords} np1c x,y coordinates of first touch
    1089          * @param {JXG.Coords} np2c x,y coordinates of second touch
    1090          * @param {object} o The touch object that is dragged: {JXG.Board#touches}.
    1091          * @param {object} drag The object that is dragged:
    1092          */
    1093         twoFingerTouchObject: function (np1c, np2c, o, drag) {
    1094             var np1, np2, op1, op2,
    1095                 nmid, omid, nd, od,
    1096                 d,
    1097                 S, alpha, t1, t2, t3, t4, t5,
    1098                 ar, i, len;
    1099
    1100             if (Type.exists(o.targets[0]) &&
    1101                     Type.exists(o.targets[1]) &&
    1102                     !isNaN(o.targets[0].Xprev + o.targets[0].Yprev + o.targets[1].Xprev + o.targets[1].Yprev)) {
    1103
    1104                 np1 = np1c.usrCoords;
    1105                 np2 = np2c.usrCoords;
    1106                 // Previous finger position
    1107                 op1 = (new Coords(Const.COORDS_BY_SCREEN, [o.targets[0].Xprev, o.targets[0].Yprev], this)).usrCoords;
    1108                 op2 = (new Coords(Const.COORDS_BY_SCREEN, [o.targets[1].Xprev, o.targets[1].Yprev], this)).usrCoords;
    1109
    1110                 // Affine mid points of the old and new positions
    1111                 omid = [1, (op1[1] + op2[1]) * 0.5, (op1[2] + op2[2]) * 0.5];
    1112                 nmid = [1, (np1[1] + np2[1]) * 0.5, (np1[2] + np2[2]) * 0.5];
    1113
    1114                 // Old and new directions
    1115                 od = Mat.crossProduct(op1, op2);
    1116                 nd = Mat.crossProduct(np1, np2);
    1117                 S = Mat.crossProduct(od, nd);
    1118
    1119                 // If parallel, translate otherwise rotate
    1120                 if (Math.abs(S[0]) < Mat.eps) {
    1121                     return;
    1122                 }
    1123
    1124                 S[1] /= S[0];
    1125                 S[2] /= S[0];
    1126                 alpha = Geometry.rad(omid.slice(1), S.slice(1), nmid.slice(1));
    1127                 t1 = this.create('transform', [alpha, S[1], S[2]], {type: 'rotate'});
    1128
    1129                 // Old midpoint of fingers after first transformation:
    1130                 t1.update();
    1131                 omid = Mat.matVecMult(t1.matrix, omid);
    1132                 omid[1] /= omid[0];
    1133                 omid[2] /= omid[0];
    1134
    1135                 // Shift to the new mid point
    1136                 t2 = this.create('transform', [nmid[1] - omid[1], nmid[2] - omid[2]], {type: 'translate'});
    1137                 t2.update();
    1138                 //omid = Mat.matVecMult(t2.matrix, omid);
    1139
    1140                 t1.melt(t2);
    1141                 if (Type.evaluate(drag.visProp.scalable)) {
    1142                     // Scale
    1143                     d = Geometry.distance(np1, np2) / Geometry.distance(op1, op2);
    1144                     t3 = this.create('transform', [-nmid[1], -nmid[2]], {type: 'translate'});
    1145                     t4 = this.create('transform', [d, d], {type: 'scale'});
    1146                     t5 = this.create('transform', [nmid[1], nmid[2]], {type: 'translate'});
    1147                     t1.melt(t3).melt(t4).melt(t5);
    1148                 }
    1149
    1150
    1151                 if (drag.elementClass === Const.OBJECT_CLASS_LINE) {
    1152                     ar = [];
    1153                     if (drag.point1.draggable()) {
    1154                         ar.push(drag.point1);
    1155                     }
    1156                     if (drag.point2.draggable()) {
    1157                         ar.push(drag.point2);
    1158                     }
    1159                     t1.applyOnce(ar);
    1160                 } else if (drag.type === Const.OBJECT_TYPE_POLYGON) {
    1161                     ar = [];
    1162                     len = drag.vertices.length - 1;
    1163                     for (i = 0; i < len; ++i) {
    1164                         if (drag.vertices[i].draggable()) {
    1165                             ar.push(drag.vertices[i]);
    1166                         }
    1167                     }
    1168                     t1.applyOnce(ar);
    1169                 }
    1170
    1171                 this.update();
    1172                 drag.highlight(true);
    1173             }
    1174         },
    1175
    1176         /*
    1177          * Moves a circle with two fingers
    1178          * @param {JXG.Coords} np1c x,y coordinates of first touch
    1179          * @param {JXG.Coords} np2c x,y coordinates of second touch
    1180          * @param {object} o The touch object that is dragged: {JXG.Board#touches}.
    1181          * @param {object} drag The object that is dragged:
    1182          */
    1183         twoFingerTouchCircle: function (np1c, np2c, o, drag) {
    1184             var np1, np2, op1, op2,
    1185                 d, alpha, t1, t2, t3, t4, t5;
    1186
    1187             if (drag.method === 'pointCircle' ||
    1188                     drag.method === 'pointLine') {
    1189                 return;
    1190             }
    1191
    1192             if (Type.exists(o.targets[0]) &&
    1193                     Type.exists(o.targets[1]) &&
    1194                     !isNaN(o.targets[0].Xprev + o.targets[0].Yprev + o.targets[1].Xprev + o.targets[1].Yprev)) {
    1195
    1196                 np1 = np1c.usrCoords;
    1197                 np2 = np2c.usrCoords;
    1198                 // Previous finger position
    1199                 op1 = (new Coords(Const.COORDS_BY_SCREEN, [o.targets[0].Xprev, o.targets[0].Yprev], this)).usrCoords;
    1200                 op2 = (new Coords(Const.COORDS_BY_SCREEN, [o.targets[1].Xprev, o.targets[1].Yprev], this)).usrCoords;
    1201
    1202                 // Shift by the movement of the first finger
    1203                 t1 = this.create('transform', [np1[1] - op1[1], np1[2] - op1[2]], {type: 'translate'});
    1204                 alpha = Geometry.rad(op2.slice(1), np1.slice(1), np2.slice(1));
    1205
    1206                 // Rotate and scale by the movement of the second finger
    1207                 t2 = this.create('transform', [-np1[1], -np1[2]], {type: 'translate'});
    1208                 t3 = this.create('transform', [alpha], {type: 'rotate'});
    1209                 t1.melt(t2).melt(t3);
    1210
    1211                 if (Type.evaluate(drag.visProp.scalable)) {
    1212                     d = Geometry.distance(np1, np2) / Geometry.distance(op1, op2);
    1213                     t4 = this.create('transform', [d, d], {type: 'scale'});
    1214                     t1.melt(t4);
    1215                 }
    1216                 t5 = this.create('transform', [np1[1], np1[2]], {type: 'translate'});
    1217                 t1.melt(t5);
    1218
    1219                 if (drag.center.draggable()) {
    1220                     t1.applyOnce([drag.center]);
    1221                 }
    1222
    1223                 if (drag.method === 'twoPoints') {
    1224                     if (drag.point2.draggable()) {
    1225                         t1.applyOnce([drag.point2]);
    1226                     }
    1227                 } else if (drag.method === 'pointRadius') {
    1228                     if (Type.isNumber(drag.updateRadius.origin)) {
    1229                         drag.setRadius(drag.radius * d);
    1230                     }
    1231                 }
    1232                 this.update(drag.center);
    1233                 drag.highlight(true);
    1234             }
    1235         },
    1236
    1237         highlightElements: function (x, y, evt, target) {
    1238             var el, pEl, pId,
    1239                 overObjects = {},
    1240                 len = this.objectsList.length;
    1241
    1242             // Elements  below the mouse pointer which are not highlighted yet will be highlighted.
    1243             for (el = 0; el < len; el++) {
    1244                 pEl = this.objectsList[el];
    1245                 pId = pEl.id;
    1246                 if (Type.exists(pEl.hasPoint) && pEl.visPropCalc.visible && pEl.hasPoint(x, y)) {
    1247                     // this is required in any case because otherwise the box won't be shown until the point is dragged
    1248                     this.updateInfobox(pEl);
    1249
    1250                     if (!Type.exists(this.highlightedObjects[pId])) { // highlight only if not highlighted
    1251                         overObjects[pId] = pEl;
    1252                         pEl.highlight();
    1253                         this.triggerEventHandlers(['mousehit', 'hit'], [evt, pEl, target]);
    1254                     }
    1255
    1256                     if (pEl.mouseover) {
    1257                         pEl.triggerEventHandlers(['mousemove', 'move'], [evt]);
    1258                     } else {
    1259                         pEl.triggerEventHandlers(['mouseover', 'over'], [evt]);
    1260                         pEl.mouseover = true;
    1261                     }
    1262                 }
    1263             }
    1264
    1265             for (el = 0; el < len; el++) {
    1266                 pEl = this.objectsList[el];
    1267                 pId = pEl.id;
    1268                 if (pEl.mouseover) {
    1269                     if (!overObjects[pId]) {
    1270                         pEl.triggerEventHandlers(['mouseout', 'out'], [evt]);
    1271                         pEl.mouseover = false;
    1272                     }
    1273                 }
    1274             }
    1275         },
    1276
    1277         /**
    1278          * Helper function which returns a reasonable starting point for the object being dragged.
    1279          * Formerly known as initXYstart().
    1280          * @private
    1281          * @param {JXG.GeometryElement} obj The object to be dragged
    1282          * @param {Array} targets Array of targets. It is changed by this function.
    1283          */
    1284         saveStartPos: function (obj, targets) {
    1285             var xy = [], i, len;
    1286
    1287             if (obj.type === Const.OBJECT_TYPE_TICKS) {
    1288                 xy.push([1, NaN, NaN]);
    1289             } else if (obj.elementClass === Const.OBJECT_CLASS_LINE) {
    1290                 xy.push(obj.point1.coords.usrCoords);
    1291                 xy.push(obj.point2.coords.usrCoords);
    1292             } else if (obj.elementClass === Const.OBJECT_CLASS_CIRCLE) {
    1293                 xy.push(obj.center.coords.usrCoords);
    1294                 if (obj.method === 'twoPoints') {
    1295                     xy.push(obj.point2.coords.usrCoords);
    1296                 }
    1297             } else if (obj.type === Const.OBJECT_TYPE_POLYGON) {
    1298                 len = obj.vertices.length - 1;
    1299                 for (i = 0; i < len; i++) {
    1300                     xy.push(obj.vertices[i].coords.usrCoords);
    1301                 }
    1302             } else if (obj.type === Const.OBJECT_TYPE_SECTOR) {
    1303                 xy.push(obj.point1.coords.usrCoords);
    1304                 xy.push(obj.point2.coords.usrCoords);
    1305                 xy.push(obj.point3.coords.usrCoords);
    1306             } else if (Type.isPoint(obj) || obj.type === Const.OBJECT_TYPE_GLIDER) {
    1307                 xy.push(obj.coords.usrCoords);
    1308             } else if (obj.elementClass === Const.OBJECT_CLASS_CURVE) {
    1309                 // if (Type.exists(obj.parents)) {
    1310                 //     len = obj.parents.length;
    1311                 //     if (len > 0) {
    1312                 //         for (i = 0; i < len; i++) {
    1313                 //             xy.push(this.select(obj.parents[i]).coords.usrCoords);
    1314                 //         }
    1315                 //     } else
    1316                 // }
    1317                 if (obj.points.length > 0) {
    1318                     xy.push(obj.points[0].usrCoords);
    1319                 }
    1320             } else {
    1321                 try {
    1322                     xy.push(obj.coords.usrCoords);
    1323                 } catch (e) {
    1324                     JXG.debug('JSXGraph+ saveStartPos: obj.coords.usrCoords not available: ' + e);
    1325                 }
    1326             }
    1327
    1328             len = xy.length;
    1329             for (i = 0; i < len; i++) {
    1330                 targets.Zstart.push(xy[i][0]);
    1331                 targets.Xstart.push(xy[i][1]);
    1332                 targets.Ystart.push(xy[i][2]);
    1333             }
    1334         },
    1335
    1336         mouseOriginMoveStart: function (evt) {
    1337             var r, pos;
    1338
    1339             r = this._isRequiredKeyPressed(evt, 'pan');
    1340             if (r) {
    1341                 pos = this.getMousePosition(evt);
    1342                 this.initMoveOrigin(pos[0], pos[1]);
    1343             }
    1344
    1345             return r;
    1346         },
    1347
    1348         mouseOriginMove: function (evt) {
    1349             var r = (this.mode === this.BOARD_MODE_MOVE_ORIGIN),
    1350                 pos;
    1351
    1352             if (r) {
    1353                 pos = this.getMousePosition(evt);
    1354                 this.moveOrigin(pos[0], pos[1], true);
    1355             }
    1356
    1357             return r;
    1358         },
    1359
    1360         /**
    1361          * Start moving the origin with one finger.
    1362          * @private
    1363          * @param  {Object} evt Event from touchStartListener
    1364          * @return {Boolean}   returns if the origin is moved.
    1365          */
    1366         touchOriginMoveStart: function (evt) {
    1367             var touches = evt[JXG.touchProperty],
    1368                 r, pos;
    1369
    1370             r = this.attr.pan.enabled &&
    1371                 !this.attr.pan.needtwofingers &&
    1372                 touches.length == 1;
    1373
    1374             if (r) {
    1375                 pos = this.getMousePosition(evt, 0);
    1376                 this.initMoveOrigin(pos[0], pos[1]);
    1377             }
    1378
    1379             return r;
    1380         },
    1381
    1382         /**
    1383          * Move the origin with one finger
    1384          * @private
    1385          * @param  {Object} evt Event from touchMoveListener
    1386          * @return {Boolean}     returns if the origin is moved.
    1387          */
    1388         touchOriginMove: function (evt) {
    1389             var r = (this.mode === this.BOARD_MODE_MOVE_ORIGIN),
    1390                 pos;
    1391
    1392             if (r) {
    1393                 pos = this.getMousePosition(evt, 0);
    1394                 this.moveOrigin(pos[0], pos[1], true);
    1395             }
    1396
    1397             return r;
    1398         },
    1399
    1400         /**
    1401          * Stop moving the origin with one finger
    1402          * @return {null} null
    1403          * @private
    1404          */
    1405         originMoveEnd: function () {
    1406             this.updateQuality = this.BOARD_QUALITY_HIGH;
    1407             this.mode = this.BOARD_MODE_NONE;
    1408         },
    1409
    1410         /**********************************************************
    1411          *
    1412          * Event Handler
    1413          *
    1414          **********************************************************/
    1415
    1416         /**
    1417          *  Add all possible event handlers to the board object
    1418          */
    1419         addEventHandlers: function () {
    1420             if (Env.supportsPointerEvents()) {
    1421                 this.addPointerEventHandlers();
    1422             } else {
    1423                 this.addMouseEventHandlers();
    1424                 this.addTouchEventHandlers();
    1425             }
    1426             //if (Env.isBrowser) {
    1427             //Env.addEvent(window, 'resize', this.update, this);
    1428             //}
    1429
    1430             // This one produces errors on IE
    1431             //Env.addEvent(this.containerObj, 'contextmenu', function (e) { e.preventDefault(); return false;}, this);
    1432             // This one works on IE, Firefox and Chromium with default configurations. On some Safari
    1433             // or Opera versions the user must explicitly allow the deactivation of the context menu.
    1434             if (this.containerObj !== null) {
    1435                 this.containerObj.oncontextmenu = function (e) {
    1436                     if (Type.exists(e)) {
    1437                         e.preventDefault();
    1438                     }
    1439                     return false;
    1440                 };
    1441             }
    1442
    1443         },
    1444
    1445         /**
    1446          * Registers the MSPointer* event handlers.
    1447          */
    1448         addPointerEventHandlers: function () {
    1449             if (!this.hasPointerHandlers && Env.isBrowser) {
    1450                 if (window.navigator.msPointerEnabled) {  // IE10-
    1451                     Env.addEvent(this.containerObj, 'MSPointerDown', this.pointerDownListener, this);
    1452                     Env.addEvent(this.containerObj, 'MSPointerMove', this.pointerMoveListener, this);
    1453                 } else {
    1454                     Env.addEvent(this.containerObj, 'pointerdown', this.pointerDownListener, this);
    1455                     Env.addEvent(this.containerObj, 'pointermove', this.pointerMoveListener, this);
    1456                 }
    1457                 Env.addEvent(this.containerObj, 'mousewheel', this.mouseWheelListener, this);
    1458                 Env.addEvent(this.containerObj, 'DOMMouseScroll', this.mouseWheelListener, this);
    1459
    1460                 if (this.containerObj !== null) {
    1461                     // This is needed for capturing touch events.
    1462                     // It is also in jsxgraph.css, but one never knows...
    1463                     this.containerObj.style.touchAction = 'none';
    1464                 }
    1465
    1466                 this.hasPointerHandlers = true;
    1467             }
    1468         },
    1469
    1470         /**
    1471          * Registers mouse move, down and wheel event handlers.
    1472          */
    1473         addMouseEventHandlers: function () {
    1474             if (!this.hasMouseHandlers && Env.isBrowser) {
    1475                 Env.addEvent(this.containerObj, 'mousedown', this.mouseDownListener, this);
    1476                 Env.addEvent(this.containerObj, 'mousemove', this.mouseMoveListener, this);
    1477
    1478                 Env.addEvent(this.containerObj, 'mousewheel', this.mouseWheelListener, this);
    1479                 Env.addEvent(this.containerObj, 'DOMMouseScroll', this.mouseWheelListener, this);
    1480
    1481                 this.hasMouseHandlers = true;
    1482             }
    1483         },
    1484
    1485         /**
    1486          * Register touch start and move and gesture start and change event handlers.
    1487          * @param {Boolean} appleGestures If set to false the gesturestart and gesturechange event handlers
    1488          * will not be registered.
    1489          */
    1490         addTouchEventHandlers: function (appleGestures) {
    1491             if (!this.hasTouchHandlers && Env.isBrowser) {
    1492                 Env.addEvent(this.containerObj, 'touchstart', this.touchStartListener, this);
    1493                 Env.addEvent(this.containerObj, 'touchmove', this.touchMoveListener, this);
    1494
    1495                 /*
    1496                 if (!Type.exists(appleGestures) || appleGestures) {
    1497                     // Gesture listener are called in touchStart and touchMove.
    1498                     //Env.addEvent(this.containerObj, 'gesturestart', this.gestureStartListener, this);
    1499                     //Env.addEvent(this.containerObj, 'gesturechange', this.gestureChangeListener, this);
    1500                 }
    1501                 */
    1502
    1503                 this.hasTouchHandlers = true;
    1504             }
    1505         },
    1506
    1507         /**
    1508          * Remove MSPointer* Event handlers.
    1509          */
    1510         removePointerEventHandlers: function () {
    1511             if (this.hasPointerHandlers && Env.isBrowser) {
    1512                 if (window.navigator.msPointerEnabled) {  // IE10-
    1513                     Env.removeEvent(this.containerObj, 'MSPointerDown', this.pointerDownListener, this);
    1514                     Env.removeEvent(this.containerObj, 'MSPointerMove', this.pointerMoveListener, this);
    1515                 } else {
    1516                     Env.removeEvent(this.containerObj, 'pointerdown', this.pointerDownListener, this);
    1517                     Env.removeEvent(this.containerObj, 'pointermove', this.pointerMoveListener, this);
    1518                 }
    1519
    1520                 Env.removeEvent(this.containerObj, 'mousewheel', this.mouseWheelListener, this);
    1521                 Env.removeEvent(this.containerObj, 'DOMMouseScroll', this.mouseWheelListener, this);
    1522
    1523                 if (this.hasPointerUp) {
    1524                     if (window.navigator.msPointerEnabled) {  // IE10-
    1525                         Env.removeEvent(this.document, 'MSPointerUp', this.pointerUpListener, this);
    1526                     } else {
    1527                         Env.removeEvent(this.document, 'pointerup', this.pointerUpListener, this);
    1528                     }
    1529                     this.hasPointerUp = false;
    1530                 }
    1531
    1532                 this.hasPointerHandlers = false;
    1533             }
    1534         },
    1535
    1536         /**
    1537          * De-register mouse event handlers.
    1538          */
    1539         removeMouseEventHandlers: function () {
    1540             if (this.hasMouseHandlers && Env.isBrowser) {
    1541                 Env.removeEvent(this.containerObj, 'mousedown', this.mouseDownListener, this);
    1542                 Env.removeEvent(this.containerObj, 'mousemove', this.mouseMoveListener, this);
    1543
    1544                 if (this.hasMouseUp) {
    1545                     Env.removeEvent(this.document, 'mouseup', this.mouseUpListener, this);
    1546                     this.hasMouseUp = false;
    1547                 }
    1548
    1549                 Env.removeEvent(this.containerObj, 'mousewheel', this.mouseWheelListener, this);
    1550                 Env.removeEvent(this.containerObj, 'DOMMouseScroll', this.mouseWheelListener, this);
    1551
    1552                 this.hasMouseHandlers = false;
    1553             }
    1554         },
    1555
    1556         /**
    1557          * Remove all registered touch event handlers.
    1558          */
    1559         removeTouchEventHandlers: function () {
    1560             if (this.hasTouchHandlers && Env.isBrowser) {
    1561                 Env.removeEvent(this.containerObj, 'touchstart', this.touchStartListener, this);
    1562                 Env.removeEvent(this.containerObj, 'touchmove', this.touchMoveListener, this);
    1563
    1564                 if (this.hasTouchEnd) {
    1565                     Env.removeEvent(this.document, 'touchend', this.touchEndListener, this);
    1566                     this.hasTouchEnd = false;
    1567                 }
    1568
    1569                 this.hasTouchHandlers = false;
    1570             }
    1571         },
    1572
    1573         /**
    1574          * Remove all event handlers from the board object
    1575          */
    1576         removeEventHandlers: function () {
    1577             this.removeMouseEventHandlers();
    1578             this.removeTouchEventHandlers();
    1579             this.removePointerEventHandlers();
    1580         },
    1581
    1582         /**
    1583          * Handler for click on left arrow in the navigation bar
    1584          * @returns {JXG.Board} Reference to the board
    1585          */
    1586         clickLeftArrow: function () {
    1587             this.moveOrigin(this.origin.scrCoords[1] + this.canvasWidth * 0.1, this.origin.scrCoords[2]);
    1588             return this;
    1589         },
    1590
    1591         /**
    1592          * Handler for click on right arrow in the navigation bar
    1593          * @returns {JXG.Board} Reference to the board
    1594          */
    1595         clickRightArrow: function () {
    1596             this.moveOrigin(this.origin.scrCoords[1] - this.canvasWidth * 0.1, this.origin.scrCoords[2]);
    1597             return this;
    1598         },
    1599
    1600         /**
    1601          * Handler for click on up arrow in the navigation bar
    1602          * @returns {JXG.Board} Reference to the board
    1603          */
    1604         clickUpArrow: function () {
    1605             this.moveOrigin(this.origin.scrCoords[1], this.origin.scrCoords[2] - this.canvasHeight * 0.1);
    1606             return this;
    1607         },
    1608
    1609         /**
    1610          * Handler for click on down arrow in the navigation bar
    1611          * @returns {JXG.Board} Reference to the board
    1612          */
    1613         clickDownArrow: function () {
    1614             this.moveOrigin(this.origin.scrCoords[1], this.origin.scrCoords[2] + this.canvasHeight * 0.1);
    1615             return this;
    1616         },
    1617
    1618         /**
    1619          * Triggered on iOS/Safari while the user inputs a gesture (e.g. pinch) and is used to zoom into the board.
    1620          * Works on iOS/Safari and Android.
    1621          * @param {Event} evt Browser event object
    1622          * @returns {Boolean}
    1623          */
    1624         gestureChangeListener: function (evt) {
    1625             var c,
    1626                 // Save zoomFactors
    1627                 zx = this.attr.zoom.factorx,
    1628                 zy = this.attr.zoom.factory,
    1629                 factor,
    1630                 dist,
    1631                 dx, dy, theta, cx, cy, bound;
    1632
    1633             if (this.mode !== this.BOARD_MODE_ZOOM) {
    1634                 return true;
    1635             }
    1636             evt.preventDefault();
    1637
    1638             c = new Coords(Const.COORDS_BY_SCREEN, this.getMousePosition(evt, 0), this);
    1639             dist = Geometry.distance([evt.touches[0].clientX, evt.touches[0].clientY],
    1640                             [evt.touches[1].clientX, evt.touches[1].clientY], 2);
    1641
    1642             // Android pinch to zoom
    1643             if (evt.scale === undefined) {
    1644                 // evt.scale is undefined in Android
    1645                 evt.scale = dist / this.prevDist;
    1646             }
    1647
    1648             factor = evt.scale / this.prevScale;
    1649             this.prevScale = evt.scale;
    1650
    1651             // pan detected
    1652             if (this.attr.pan.enabled &&
    1653                 this.attr.pan.needtwofingers &&
    1654                 Math.abs(evt.scale - 1.0) < 0.4 &&
    1655                 this._num_pan >= 0.8 * this._num_zoom) {
    1656
    1657                 this._num_pan++;
    1658                 this.moveOrigin(c.scrCoords[1], c.scrCoords[2], true);
    1659             } else if (this.attr.zoom.enabled &&
    1660                 Math.abs(factor - 1.0) < 0.5) {
    1661
    1662                 this._num_zoom++;
    1663
    1664                 if (this.attr.zoom.pinchhorizontal || this.attr.zoom.pinchvertical) {
    1665                     dx = Math.abs(evt.touches[0].clientX - evt.touches[1].clientX);
    1666                     dy = Math.abs(evt.touches[0].clientY - evt.touches[1].clientY);
    1667                     theta = Math.abs(Math.atan2(dy, dx));
    1668                     bound = Math.PI * this.attr.zoom.pinchsensitivity / 90.0;
    1669                 }
    1670
    1671                 if (this.attr.zoom.pinchhorizontal && theta < bound) {
    1672                     this.attr.zoom.factorx = factor;
    1673                     this.attr.zoom.factory = 1.0;
    1674                     cx = 0;
    1675                     cy = 0;
    1676                 } else if (this.attr.zoom.pinchvertical && Math.abs(theta - Math.PI * 0.5) < bound) {
    1677                     this.attr.zoom.factorx = 1.0;
    1678                     this.attr.zoom.factory = factor;
    1679                     cx = 0;
    1680                     cy = 0;
    1681                 } else {
    1682                     this.attr.zoom.factorx = factor;
    1683                     this.attr.zoom.factory = factor;
    1684                     cx = c.usrCoords[1];
    1685                     cy = c.usrCoords[2];
    1686                 }
    1687
    1688                 this.zoomIn(cx, cy);
    1689
    1690                 // Restore zoomFactors
    1691                 this.attr.zoom.factorx = zx;
    1692                 this.attr.zoom.factory = zy;
    1693             }
    1694
    1695             return false;
    1696         },
    1697
    1698         /**
    1699          * Called by iOS/Safari as soon as the user starts a gesture. Works natively on iOS/Safari,
    1700          * on Android we emulate it.
    1701          * @param {Event} evt
    1702          * @returns {Boolean}
    1703          */
    1704         gestureStartListener: function (evt) {
    1705             var pos;
    1706
    1707             evt.preventDefault();
    1708             this.prevScale = 1.0;
    1709             // Android pinch to zoom
    1710             this.prevDist = Geometry.distance([evt.touches[0].clientX, evt.touches[0].clientY],
    1711                             [evt.touches[1].clientX, evt.touches[1].clientY], 2);
    1712
    1713             // If pinch-to-zoom is interpreted as panning
    1714             // we have to prepare move origin
    1715             pos = this.getMousePosition(evt, 0);
    1716             this.initMoveOrigin(pos[0], pos[1]);
    1717
    1718             this._num_zoom = this._num_pan = 0;
    1719             this.mode = this.BOARD_MODE_ZOOM;
    1720             return false;
    1721         },
    1722
    1723         /**
    1724          * Test if the required key combination is pressed for wheel zoom, move origin and
    1725          * selection
    1726          * @private
    1727          * @param  {Object}  evt    Mouse or pen event
    1728          * @param  {String}  action String containing the action: 'zoom', 'pan', 'selection'.
    1729          * Corresponds to the attribute subobject.
    1730          * @return {Boolean}        true or false.
    1731          */
    1732         _isRequiredKeyPressed: function (evt, action) {
    1733             var obj = this.attr[action];
    1734             if (!obj.enabled) {
    1735                 return false;
    1736             }
    1737
    1738             if (((obj.needshift && evt.shiftKey) || (!obj.needshift && !evt.shiftKey)) &&
    1739                 ((obj.needctrl && evt.ctrlKey) || (!obj.needctrl && !evt.ctrlKey))
    1740             )  {
    1741                 return true;
    1742             }
    1743
    1744             return false;
    1745         },
    1746
    1747         /**
    1748          * pointer-Events
    1749          */
    1750
    1751         _pointerAddBoardTouches: function (evt) {
    1752             var i, found;
    1753
    1754             for (i = 0, found = false; i < this._board_touches.length; i++) {
    1755                 if (this._board_touches[i].pointerId === evt.pointerId) {
    1756                     this._board_touches[i].clientX = evt.clientX;
    1757                     this._board_touches[i].clientY = evt.clientY;
    1758                     found = true;
    1759                     break;
    1760                 }
    1761             }
    1762
    1763             if (!found) {
    1764                 this._board_touches.push({
    1765                     pointerId: evt.pointerId,
    1766                     clientX: evt.clientX,
    1767                     clientY: evt.clientY
    1768                 });
    1769             }
    1770
    1771             return this;
    1772         },
    1773
    1774         _pointerRemoveBoardTouches: function (evt) {
    1775             var i;
    1776             for (i = 0; i < this._board_touches.length; i++) {
    1777                 if (this._board_touches[i].pointerId === evt.pointerId) {
    1778                     this._board_touches.splice(i, 1);
    1779                     break;
    1780                 }
    1781             }
    1782
    1783             return this;
    1784         },
    1785
    1786         /**
    1787          * This method is called by the browser when a pointing device is pressed on the screen.
    1788          * @param {Event} evt The browsers event object.
    1789          * @param {Object} object If the object to be dragged is already known, it can be submitted via this parameter
    1790          * @returns {Boolean} ...
    1791          */
    1792         pointerDownListener: function (evt, object) {
    1793             var i, j, k, pos, elements, sel,
    1794                 eps = this.options.precision.touch,
    1795                 found, target, result;
    1796
    1797             if (!this.hasPointerUp) {
    1798                 if (window.navigator.msPointerEnabled) {  // IE10-
    1799                     Env.addEvent(this.document, 'MSPointerUp', this.pointerUpListener, this);
    1800                 } else {
    1801                     Env.addEvent(this.document, 'pointerup', this.pointerUpListener, this);
    1802                 }
    1803                 this.hasPointerUp = true;
    1804             }
    1805
    1806             if (this.hasMouseHandlers) {
    1807                 this.removeMouseEventHandlers();
    1808             }
    1809
    1810             if (this.hasTouchHandlers) {
    1811                 this.removeTouchEventHandlers();
    1812             }
    1813
    1814             // prevent accidental selection of text
    1815             if (this.document.selection && Type.isFunction(this.document.selection.empty)) {
    1816                 this.document.selection.empty();
    1817             } else if (window.getSelection) {
    1818                 sel = window.getSelection();
    1819                 if (sel.removeAllRanges) {
    1820                     try {
    1821                         sel.removeAllRanges();
    1822                     } catch (e) {}
    1823                 }
    1824             }
    1825
    1826             // Touch or pen device
    1827             if (Env.isBrowser &&
    1828                     (evt.pointerType === 'touch' || // New
    1829                     (window.navigator.msMaxTouchPoints && window.navigator.msMaxTouchPoints > 1)) // Old
    1830                 ) {
    1831                 this.options.precision.hasPoint = eps;
    1832             }
    1833
    1834             // This should be easier than the touch events. Every pointer device gets its own pointerId, e.g. the mouse
    1835             // always has id 1, fingers and pens get unique ids every time a pointerDown event is fired and they will
    1836             // keep this id until a pointerUp event is fired. What we have to do here is:
    1837             //  1. collect all elements under the current pointer
    1838             //  2. run through the touches control structure
    1839             //    a. look for the object collected in step 1.
    1840             //    b. if an object is found, check the number of pointers. If appropriate, add the pointer.
    1841
    1842             pos = this.getMousePosition(evt);
    1843
    1844             // selection
    1845             this._testForSelection(evt);
    1846             if (this.selectingMode) {
    1847                 this._startSelecting(pos);
    1848                 this.triggerEventHandlers(['touchstartselecting', 'pointerstartselecting', 'startselecting'], [evt]);
    1849                 return;     // don't continue as a normal click
    1850             }
    1851
    1852             if (object) {
    1853                 elements = [ object ];
    1854                 this.mode = this.BOARD_MODE_DRAG;
    1855             } else {
    1856                 elements = this.initMoveObject(pos[0], pos[1], evt, 'mouse');
    1857             }
    1858
    1859             // if no draggable object can be found, get out here immediately
    1860             if (elements.length > 0) {
    1861                 // check touches structure
    1862                 target = elements[elements.length - 1];
    1863                 found = false;
    1864                 for (i = 0; i < this.touches.length; i++) {
    1865                     // the target is already in our touches array, try to add the pointer to the existing touch
    1866                     if (this.touches[i].obj === target) {
    1867                         j = i;
    1868                         k = this.touches[i].targets.push({
    1869                             num: evt.pointerId,
    1870                             X: pos[0],
    1871                             Y: pos[1],
    1872                             Xprev: NaN,
    1873                             Yprev: NaN,
    1874                             Xstart: [],
    1875                             Ystart: [],
    1876                             Zstart: []
    1877                         }) - 1;
    1878
    1879                         found = true;
    1880                         break;
    1881                     }
    1882                 }
    1883
    1884                 if (!found) {
    1885                     k = 0;
    1886                     j = this.touches.push({
    1887                         obj: target,
    1888                         targets: [{
    1889                             num: evt.pointerId,
    1890                             X: pos[0],
    1891                             Y: pos[1],
    1892                             Xprev: NaN,
    1893                             Yprev: NaN,
    1894                             Xstart: [],
    1895                             Ystart: [],
    1896                             Zstart: []
    1897                         }]
    1898                     }) - 1;
    1899                 }
    1900
    1901                 this.dehighlightAll();
    1902                 target.highlight(true);
    1903
    1904                 this.saveStartPos(target, this.touches[j].targets[k]);
    1905
    1906                 // prevent accidental text selection
    1907                 // this could get us new trouble: input fields, links and drop down boxes placed as text
    1908                 // on the board don't work anymore.
    1909                 if (evt && evt.preventDefault) {
    1910                     evt.preventDefault();
    1911                 } else if (window.event) {
    1912                     window.event.returnValue = false;
    1913                 }
    1914             }
    1915
    1916             if (this.touches.length > 0) {
    1917                 evt.preventDefault();
    1918                 evt.stopPropagation();
    1919             }
    1920
    1921             this.options.precision.hasPoint = this.options.precision.mouse;
    1922
    1923             if (Env.isBrowser && evt.pointerType !== 'touch') {
    1924                 if (this.mode === this.BOARD_MODE_NONE) {
    1925                     this.mouseOriginMoveStart(evt);
    1926                 }
    1927             } else {
    1928                 this._pointerAddBoardTouches(evt);
    1929                 evt.touches = this._board_touches;
    1930
    1931                 // See touchStartListener
    1932                 if (this.mode === this.BOARD_MODE_NONE && this.touchOriginMoveStart(evt)) {
    1933                 } else if ((this.mode === this.BOARD_MODE_NONE ||
    1934                             this.mode === this.BOARD_MODE_MOVE_ORIGIN) &&
    1935                            evt.touches.length == 2) {
    1936                     if (this.mode === this.BOARD_MODE_MOVE_ORIGIN) {
    1937                         this.originMoveEnd();
    1938                     }
    1939
    1940                     this.gestureStartListener(evt);
    1941                 }
    1942             }
    1943
    1944             this.triggerEventHandlers(['touchstart', 'down', 'pointerdown', 'MSPointerDown'], [evt]);
    1945
    1946             //return result;
    1947             return false;
    1948         },
    1949
    1950         /**
    1951          * Called periodically by the browser while the user moves a pointing device across the screen.
    1952          * @param {Event} evt
    1953          * @returns {Boolean}
    1954          */
    1955         pointerMoveListener: function (evt) {
    1956             var i, j, pos;
    1957
    1958             if (this.mode !== this.BOARD_MODE_DRAG) {
    1959                 this.dehighlightAll();
    1960                 this.showInfobox(false);
    1961             }
    1962
    1963             if (this.mode !== this.BOARD_MODE_NONE) {
    1964                 evt.preventDefault();
    1965                 evt.stopPropagation();
    1966             }
    1967
    1968             // Touch or pen device
    1969             if (Env.isBrowser &&
    1970                     (evt.pointerType === 'touch' || // New
    1971                     (window.navigator.msMaxTouchPoints && window.navigator.msMaxTouchPoints > 1)) // Old
    1972                 ) {
    1973                 this.options.precision.hasPoint = this.options.precision.touch;
    1974             }
    1975             this.updateQuality = this.BOARD_QUALITY_LOW;
    1976
    1977             // selection
    1978             if (this.selectingMode) {
    1979                 pos = this.getMousePosition(evt);
    1980                 this._moveSelecting(pos);
    1981                 this.triggerEventHandlers(['touchmoveselecting', 'moveselecting', 'pointermoveselecting'], [evt, this.mode]);
    1982             } else if (!this.mouseOriginMove(evt)) {
    1983                 if (this.mode === this.BOARD_MODE_DRAG) {
    1984                     // Runs through all elements which are touched by at least one finger.
    1985                     for (i = 0; i < this.touches.length; i++) {
    1986                         for (j = 0; j < this.touches[i].targets.length; j++) {
    1987                             if (this.touches[i].targets[j].num === evt.pointerId) {
    1988                                 // Touch by one finger:  this is possible for all elements that can be dragged
    1989                                 if (this.touches[i].targets.length === 1) {
    1990                                     this.touches[i].targets[j].X = evt.pageX;
    1991                                     this.touches[i].targets[j].Y = evt.pageY;
    1992                                     pos = this.getMousePosition(evt);
    1993                                     this.moveObject(pos[0], pos[1], this.touches[i], evt, 'touch');
    1994                                 // Touch by two fingers: moving lines
    1995                                 } else if (this.touches[i].targets.length === 2 &&
    1996                                     this.touches[i].targets[0].num > -1 && this.touches[i].targets[1].num > -1) {
    1997
    1998                                     this.touches[i].targets[j].X = evt.pageX;
    1999                                     this.touches[i].targets[j].Y = evt.pageY;
    2000
    2001                                     this.twoFingerMove(
    2002                                         this.getMousePosition({
    2003                                             clientX: this.touches[i].targets[0].X,
    2004                                             clientY: this.touches[i].targets[0].Y
    2005                                         }),
    2006                                         this.getMousePosition({
    2007                                             clientX: this.touches[i].targets[1].X,
    2008                                             clientY: this.touches[i].targets[1].Y
    2009                                         }),
    2010                                         this.touches[i],
    2011                                         evt
    2012                                     );
    2013                                 }
    2014
    2015                                 // there is only one pointer in the evt object, there's no point in looking further
    2016                                 break;
    2017                             }
    2018                         }
    2019                     }
    2020                 } else {
    2021                     if (evt.pointerType == 'touch') {
    2022                         this._pointerAddBoardTouches(evt);
    2023                         if (this._board_touches.length == 2) {
    2024                             evt.touches = this._board_touches;
    2025                             this.gestureChangeListener(evt);
    2026                         }
    2027                     } else {
    2028                         pos = this.getMousePosition(evt);
    2029                         this.highlightElements(pos[0], pos[1], evt, -1);
    2030                     }
    2031                 }
    2032             }
    2033
    2034             // Hiding the infobox is commented out, since it prevents showing the infobox
    2035             // on IE 11+ on 'over'
    2036             //if (this.mode !== this.BOARD_MODE_DRAG) {
    2037                 //this.showInfobox(false);
    2038             //}
    2039
    2040             this.options.precision.hasPoint = this.options.precision.mouse;
    2041             this.triggerEventHandlers(['touchmove', 'move', 'pointermove', 'MSPointerMove'], [evt, this.mode]);
    2042
    2043             return this.mode === this.BOARD_MODE_NONE;
    2044         },
    2045
    2046         /**
    2047          * Triggered as soon as the user stops touching the device with at least one finger.
    2048          * @param {Event} evt
    2049          * @returns {Boolean}
    2050          */
    2051         pointerUpListener: function (evt) {
    2052             var i, j, found;
    2053
    2054             this.triggerEventHandlers(['touchend', 'up', 'pointerup', 'MSPointerUp'], [evt]);
    2055             this.showInfobox(false);
    2056
    2057             if (evt) {
    2058                 for (i = 0; i < this.touches.length; i++) {
    2059                     for (j = 0; j < this.touches[i].targets.length; j++) {
    2060                         if (this.touches[i].targets[j].num === evt.pointerId) {
    2061                             this.touches[i].targets.splice(j, 1);
    2062
    2063                             if (this.touches[i].targets.length === 0) {
    2064                                 this.touches.splice(i, 1);
    2065                             }
    2066
    2067                             break;
    2068                         }
    2069                     }
    2070                 }
    2071             }
    2072
    2073             // selection
    2074             if (this.selectingMode) {
    2075                 this._stopSelecting(evt);
    2076                 this.triggerEventHandlers(['touchstopselecting', 'pointerstopselecting', 'stopselecting'], [evt]);
    2077             } else {
    2078                 for (i = this.downObjects.length - 1; i > -1; i--) {
    2079                     found = false;
    2080                     for (j = 0; j < this.touches.length; j++) {
    2081                         if (this.touches[j].obj.id === this.downObjects[i].id) {
    2082                             found = true;
    2083                         }
    2084                     }
    2085                     if (!found) {
    2086                         this.downObjects[i].triggerEventHandlers(['touchend', 'up', 'pointerup', 'MSPointerUp'], [evt]);
    2087                         this.downObjects[i].snapToGrid();
    2088                         this.downObjects[i].snapToPoints();
    2089                         this.downObjects.splice(i, 1);
    2090                     }
    2091                 }
    2092             }
    2093
    2094             this._pointerRemoveBoardTouches(evt);
    2095
    2096             // if (this.touches.length === 0) {
    2097             if (this._board_touches.length === 0) {
    2098                 if (this.hasPointerUp) {
    2099                     if (window.navigator.msPointerEnabled) {  // IE10-
    2100                         Env.removeEvent(this.document, 'MSPointerUp', this.pointerUpListener, this);
    2101                     } else {
    2102                         Env.removeEvent(this.document, 'pointerup', this.pointerUpListener, this);
    2103                     }
    2104                     this.hasPointerUp = false;
    2105                 }
    2106
    2107                 this.dehighlightAll();
    2108                 this.updateQuality = this.BOARD_QUALITY_HIGH;
    2109                 this.mode = this.BOARD_MODE_NONE;
    2110
    2111                 this.originMoveEnd();
    2112                 this.update();
    2113             }
    2114
    2115
    2116             return true;
    2117         },
    2118
    2119         /**
    2120          * Touch-Events
    2121          */
    2122
    2123         /**
    2124          * This method is called by the browser when a finger touches the surface of the touch-device.
    2125          * @param {Event} evt The browsers event object.
    2126          * @returns {Boolean} ...
    2127          */
    2128         touchStartListener: function (evt) {
    2129             var i, pos, elements, j, k, time,
    2130                 eps = this.options.precision.touch,
    2131                 obj, found, targets,
    2132                 evtTouches = evt[JXG.touchProperty],
    2133                 target;
    2134
    2135             if (!this.hasTouchEnd) {
    2136                 Env.addEvent(this.document, 'touchend', this.touchEndListener, this);
    2137                 this.hasTouchEnd = true;
    2138             }
    2139
    2140             // Do not remove mouseHandlers, since Chrome on win tablets sends mouseevents if used with pen.
    2141             //if (this.hasMouseHandlers) { this.removeMouseEventHandlers(); }
    2142
    2143             // prevent accidental selection of text
    2144             if (this.document.selection && Type.isFunction(this.document.selection.empty)) {
    2145                 this.document.selection.empty();
    2146             } else if (window.getSelection) {
    2147                 window.getSelection().removeAllRanges();
    2148             }
    2149
    2150             // multitouch
    2151             this.options.precision.hasPoint = this.options.precision.touch;
    2152
    2153             // This is the most critical part. first we should run through the existing touches and collect all targettouches that don't belong to our
    2154             // 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
    2155             // 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
    2156             // 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
    2157             // stretching. as a last step we're going through the rest of the targettouches and initiate new move operations:
    2158             //  * points have higher priority over other elements.
    2159             //  * 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
    2160             //    this element and add them.
    2161             // ADDENDUM 11/10/11:
    2162             //  (1) run through the touches control object,
    2163             //  (2) try to find the targetTouches for every touch. on touchstart only new touches are added, hence we can find a targettouch
    2164             //      for every target in our touches objects
    2165             //  (3) if one of the targettouches was bound to a touches targets array, mark it
    2166             //  (4) run through the targettouches. if the targettouch is marked, continue. otherwise check for elements below the targettouch:
    2167             //      (a) if no element could be found: mark the target touches and continue
    2168             //      --- in the following cases, "init" means:
    2169             //           (i) check if the element is already used in another touches element, if so, mark the targettouch and continue
    2170             //          (ii) if not, init a new touches element, add the targettouch to the touches property and mark it
    2171             //      (b) if the element is a point, init
    2172             //      (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
    2173             //      (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
    2174             //          add both to the touches array and mark them.
    2175             for (i = 0; i < evtTouches.length; i++) {
    2176                 evtTouches[i].jxg_isused = false;
    2177             }
    2178
    2179             for (i = 0; i < this.touches.length; i++) {
    2180                 for (j = 0; j < this.touches[i].targets.length; j++) {
    2181                     this.touches[i].targets[j].num = -1;
    2182                     eps = this.options.precision.touch;
    2183
    2184                     do {
    2185                         for (k = 0; k < evtTouches.length; k++) {
    2186                             // find the new targettouches
    2187                             if (Math.abs(Math.pow(evtTouches[k].screenX - this.touches[i].targets[j].X, 2) +
    2188                                     Math.pow(evtTouches[k].screenY - this.touches[i].targets[j].Y, 2)) < eps * eps) {
    2189                                 this.touches[i].targets[j].num = k;
    2190
    2191                                 this.touches[i].targets[j].X = evtTouches[k].screenX;
    2192                                 this.touches[i].targets[j].Y = evtTouches[k].screenY;
    2193                                 evtTouches[k].jxg_isused = true;
    2194                                 break;
    2195                             }
    2196                         }
    2197
    2198                         eps *= 2;
    2199
    2200                     } while (this.touches[i].targets[j].num === -1 && eps < this.options.precision.touchMax);
    2201
    2202                     if (this.touches[i].targets[j].num === -1) {
    2203                         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.');
    2204                         JXG.debug('eps = ' + eps + ', touchMax = ' + Options.precision.touchMax);
    2205                         this.touches[i].targets.splice(i, 1);
    2206                     }
    2207
    2208                 }
    2209             }
    2210
    2211             // we just re-mapped the targettouches to our existing touches list.
    2212             // now we have to initialize some touches from additional targettouches
    2213             for (i = 0; i < evtTouches.length; i++) {
    2214                 if (!evtTouches[i].jxg_isused) {
    2215
    2216                     pos = this.getMousePosition(evt, i);
    2217                     // selection
    2218                     // this._testForSelection(evt); // we do not have shift or ctrl keys yet.
    2219                     if (this.selectingMode) {
    2220                         this._startSelecting(pos);
    2221                         this.triggerEventHandlers(['touchstartselecting', 'startselecting'], [evt]);
    2222                         evt.preventDefault();
    2223                         evt.stopPropagation();
    2224                         this.options.precision.hasPoint = this.options.precision.mouse;
    2225                         return this.touches.length > 0; // don't continue as a normal click
    2226                     }
    2227
    2228                     elements = this.initMoveObject(pos[0], pos[1], evt, 'touch');
    2229                     if (elements.length !== 0) {
    2230                         obj = elements[elements.length - 1];
    2231
    2232                         if (Type.isPoint(obj) ||
    2233                                 obj.elementClass === Const.OBJECT_CLASS_TEXT ||
    2234                                 obj.type === Const.OBJECT_TYPE_TICKS ||
    2235                                 obj.type === Const.OBJECT_TYPE_IMAGE) {
    2236                             // it's a point, so it's single touch, so we just push it to our touches
    2237                             targets = [{ num: i, X: evtTouches[i].screenX, Y: evtTouches[i].screenY, Xprev: NaN, Yprev: NaN, Xstart: [], Ystart: [], Zstart: [] }];
    2238
    2239                             // For the UNDO/REDO of object moves
    2240                             this.saveStartPos(obj, targets[0]);
    2241
    2242                             this.touches.push({ obj: obj, targets: targets });
    2243                             obj.highlight(true);
    2244
    2245                         } else if (obj.elementClass === Const.OBJECT_CLASS_LINE ||
    2246                                 obj.elementClass === Const.OBJECT_CLASS_CIRCLE ||
    2247                                 obj.elementClass === Const.OBJECT_CLASS_CURVE ||
    2248                                 obj.type === Const.OBJECT_TYPE_POLYGON) {
    2249                             found = false;
    2250
    2251                             // first check if this geometric object is already captured in this.touches
    2252                             for (j = 0; j < this.touches.length; j++) {
    2253                                 if (obj.id === this.touches[j].obj.id) {
    2254                                     found = true;
    2255                                     // only add it, if we don't have two targets in there already
    2256                                     if (this.touches[j].targets.length === 1) {
    2257                                         target = { num: i, X: evtTouches[i].screenX, Y: evtTouches[i].screenY, Xprev: NaN, Yprev: NaN, Xstart: [], Ystart: [], Zstart: [] };
    2258
    2259                                         // For the UNDO/REDO of object moves
    2260                                         this.saveStartPos(obj, target);
    2261                                         this.touches[j].targets.push(target);
    2262                                     }
    2263
    2264                                     evtTouches[i].jxg_isused = true;
    2265                                 }
    2266                             }
    2267
    2268                             // we couldn't find it in touches, so we just init a new touches
    2269                             // IF there is a second touch targetting this line, we will find it later on, and then add it to
    2270                             // the touches control object.
    2271                             if (!found) {
    2272                                 targets = [{ num: i, X: evtTouches[i].screenX, Y: evtTouches[i].screenY, Xprev: NaN, Yprev: NaN, Xstart: [], Ystart: [], Zstart: [] }];
    2273
    2274                                 // For the UNDO/REDO of object moves
    2275                                 this.saveStartPos(obj, targets[0]);
    2276                                 this.touches.push({ obj: obj, targets: targets });
    2277                                 obj.highlight(true);
    2278                             }
    2279                         }
    2280                     }
    2281
    2282                     evtTouches[i].jxg_isused = true;
    2283                 }
    2284             }
    2285
    2286             if (this.touches.length > 0) {
    2287                 evt.preventDefault();
    2288                 evt.stopPropagation();
    2289             }
    2290
    2291             // Touch events on empty areas of the board are handled here:
    2292             // 1. case: one finger. If allowed, this triggers pan with one finger
    2293             if (this.mode === this.BOARD_MODE_NONE && this.touchOriginMoveStart(evt)) {
    2294             } else if (evtTouches.length == 2 &&
    2295                         (this.mode === this.BOARD_MODE_NONE ||
    2296                          this.mode === this.BOARD_MODE_MOVE_ORIGIN /*||
    2297                          (this.mode === this.BOARD_MODE_DRAG && this.touches.length == 1) */
    2298                         )) {
    2299                 // 2. case: two fingers: pinch to zoom or pan with two fingers needed.
    2300                 // This happens when the second finger hits the device. First, the
    2301                 // "one finger pan mode" has to be cancelled.
    2302                 if (this.mode === this.BOARD_MODE_MOVE_ORIGIN) {
    2303                     this.originMoveEnd();
    2304                 }
    2305                 this.gestureStartListener(evt);
    2306             }
    2307
    2308             this.options.precision.hasPoint = this.options.precision.mouse;
    2309             this.triggerEventHandlers(['touchstart', 'down'], [evt]);
    2310
    2311             return false;
    2312             //return this.touches.length > 0;
    2313         },
    2314
    2315         /**
    2316          * Called periodically by the browser while the user moves his fingers across the device.
    2317          * @param {Event} evt
    2318          * @returns {Boolean}
    2319          */
    2320         touchMoveListener: function (evt) {
    2321             var i, pos1, pos2, time,
    2322                 evtTouches = evt[JXG.touchProperty];
    2323
    2324             if (this.mode !== this.BOARD_MODE_NONE) {
    2325                 evt.preventDefault();
    2326                 evt.stopPropagation();
    2327             }
    2328
    2329             // Reduce update frequency for Android devices
    2330             // if (false && Env.isWebkitAndroid()) {
    2331             //     time = new Date();
    2332             //     time = time.getTime();
    2333             //
    2334             //     if (time - this.touchMoveLast < 80) {
    2335             //         this.updateQuality = this.BOARD_QUALITY_HIGH;
    2336             //         this.triggerEventHandlers(['touchmove', 'move'], [evt, this.mode]);
    2337             //
    2338             //         return false;
    2339             //     }
    2340             //
    2341             //     this.touchMoveLast = time;
    2342             // }
    2343
    2344             if (this.mode !== this.BOARD_MODE_DRAG) {
    2345                 this.showInfobox(false);
    2346             }
    2347
    2348             this.options.precision.hasPoint = this.options.precision.touch;
    2349             this.updateQuality = this.BOARD_QUALITY_LOW;
    2350
    2351             // selection
    2352             if (this.selectingMode) {
    2353                 for (i = 0; i < evtTouches.length; i++) {
    2354                     if (!evtTouches[i].jxg_isused) {
    2355                         pos1 = this.getMousePosition(evt, i);
    2356                         this._moveSelecting(pos1);
    2357                         this.triggerEventHandlers(['touchmoves', 'moveselecting'], [evt, this.mode]);
    2358                         break;
    2359                     }
    2360                 }
    2361             } else {
    2362                 if (!this.touchOriginMove(evt)) {
    2363                     if (this.mode === this.BOARD_MODE_DRAG) {
    2364                         // Runs over through all elements which are touched
    2365                         // by at least one finger.
    2366                         for (i = 0; i < this.touches.length; i++) {
    2367                             // Touch by one finger:  this is possible for all elements that can be dragged
    2368                             if (this.touches[i].targets.length === 1) {
    2369                                 if (evtTouches[this.touches[i].targets[0].num]) {
    2370                                     pos1 = this.getMousePosition(evt, this.touches[i].targets[0].num);
    2371                                     if (pos1[0] < 0 || pos1[0] > this.canvasWidth ||  pos1[1] < 0 || pos1[1] > this.canvasHeight) {
    2372                                         return;
    2373                                     }
    2374                                     this.touches[i].targets[0].X = evtTouches[this.touches[i].targets[0].num].screenX;
    2375                                     this.touches[i].targets[0].Y = evtTouches[this.touches[i].targets[0].num].screenY;
    2376                                     this.moveObject(pos1[0], pos1[1], this.touches[i], evt, 'touch');
    2377                                 }
    2378                                 // Touch by two fingers: moving lines
    2379                             } else if (this.touches[i].targets.length === 2 &&
    2380                                         this.touches[i].targets[0].num > -1 &&
    2381                                         this.touches[i].targets[1].num > -1) {
    2382                                 if (evtTouches[this.touches[i].targets[0].num] && evtTouches[this.touches[i].targets[1].num]) {
    2383                                     pos1 = this.getMousePosition(evt, this.touches[i].targets[0].num);
    2384                                     pos2 = this.getMousePosition(evt, this.touches[i].targets[1].num);
    2385                                     if (pos1[0] < 0 || pos1[0] > this.canvasWidth ||  pos1[1] < 0 || pos1[1] > this.canvasHeight ||
    2386                                             pos2[0] < 0 || pos2[0] > this.canvasWidth ||  pos2[1] < 0 || pos2[1] > this.canvasHeight) {
    2387                                         return;
    2388                                     }
    2389                                     this.touches[i].targets[0].X = evtTouches[this.touches[i].targets[0].num].screenX;
    2390                                     this.touches[i].targets[0].Y = evtTouches[this.touches[i].targets[0].num].screenY;
    2391                                     this.touches[i].targets[1].X = evtTouches[this.touches[i].targets[1].num].screenX;
    2392                                     this.touches[i].targets[1].Y = evtTouches[this.touches[i].targets[1].num].screenY;
    2393                                     this.twoFingerMove(pos1, pos2, this.touches[i], evt);
    2394                                 }
    2395                             }
    2396                         }
    2397                     } else {
    2398                         if (evtTouches.length == 2) {
    2399                             this.gestureChangeListener(evt);
    2400                         }
    2401                     }
    2402                 }
    2403             }
    2404
    2405             if (this.mode !== this.BOARD_MODE_DRAG) {
    2406                 this.showInfobox(false);
    2407             }
    2408
    2409             /*
    2410               this.updateQuality = this.BOARD_QUALITY_HIGH; is set in touchEnd
    2411             */
    2412             this.options.precision.hasPoint = this.options.precision.mouse;
    2413             this.triggerEventHandlers(['touchmove', 'move'], [evt, this.mode]);
    2414
    2415             return this.mode === this.BOARD_MODE_NONE;
    2416         },
    2417
    2418         /**
    2419          * Triggered as soon as the user stops touching the device with at least one finger.
    2420          * @param {Event} evt
    2421          * @returns {Boolean}
    2422          */
    2423         touchEndListener: function (evt) {
    2424             var i, j, k,
    2425                 eps = this.options.precision.touch,
    2426                 tmpTouches = [], found, foundNumber,
    2427                 evtTouches = evt && evt[JXG.touchProperty];
    2428
    2429             this.triggerEventHandlers(['touchend', 'up'], [evt]);
    2430             this.showInfobox(false);
    2431
    2432             // selection
    2433             if (this.selectingMode) {
    2434                 this._stopSelecting(evt);
    2435                 this.triggerEventHandlers(['touchstopselecting', 'stopselecting'], [evt]);
    2436             } else if (evtTouches && evtTouches.length > 0) {
    2437                 for (i = 0; i < this.touches.length; i++) {
    2438                     tmpTouches[i] = this.touches[i];
    2439                 }
    2440                 this.touches.length = 0;
    2441
    2442                 // try to convert the operation, e.g. if a lines is rotated and translated with two fingers and one finger is lifted,
    2443                 // convert the operation to a simple one-finger-translation.
    2444                 // ADDENDUM 11/10/11:
    2445                 // see addendum to touchStartListener from 11/10/11
    2446                 // (1) run through the tmptouches
    2447                 // (2) check the touches.obj, if it is a
    2448                 //     (a) point, try to find the targettouch, if found keep it and mark the targettouch, else drop the touch.
    2449                 //     (b) line with
    2450                 //          (i) one target: try to find it, if found keep it mark the targettouch, else drop the touch.
    2451                 //         (ii) two targets: if none can be found, drop the touch. if one can be found, remove the other target. mark all found targettouches
    2452                 //     (c) circle with [proceed like in line]
    2453
    2454                 // init the targettouches marker
    2455                 for (i = 0; i < evtTouches.length; i++) {
    2456                     evtTouches[i].jxg_isused = false;
    2457                 }
    2458
    2459                 for (i = 0; i < tmpTouches.length; i++) {
    2460                     // could all targets of the current this.touches.obj be assigned to targettouches?
    2461                     found = false;
    2462                     foundNumber = 0;
    2463
    2464                     for (j = 0; j < tmpTouches[i].targets.length; j++) {
    2465                         tmpTouches[i].targets[j].found = false;
    2466                         for (k = 0; k < evtTouches.length; k++) {
    2467                             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) {
    2468                                 tmpTouches[i].targets[j].found = true;
    2469                                 tmpTouches[i].targets[j].num = k;
    2470                                 tmpTouches[i].targets[j].X = evtTouches[k].screenX;
    2471                                 tmpTouches[i].targets[j].Y = evtTouches[k].screenY;
    2472                                 foundNumber += 1;
    2473                                 break;
    2474                             }
    2475                         }
    2476                     }
    2477
    2478                     if (Type.isPoint(tmpTouches[i].obj)) {
    2479                         found = (tmpTouches[i].targets[0] && tmpTouches[i].targets[0].found);
    2480                     } else if (tmpTouches[i].obj.elementClass === Const.OBJECT_CLASS_LINE) {
    2481                         found = (tmpTouches[i].targets[0] && tmpTouches[i].targets[0].found) || (tmpTouches[i].targets[1] && tmpTouches[i].targets[1].found);
    2482                     } else if (tmpTouches[i].obj.elementClass === Const.OBJECT_CLASS_CIRCLE) {
    2483                         found = foundNumber === 1 || foundNumber === 3;
    2484                     }
    2485
    2486                     // if we found this object to be still dragged by the user, add it back to this.touches
    2487                     if (found) {
    2488                         this.touches.push({
    2489                             obj: tmpTouches[i].obj,
    2490                             targets: []
    2491                         });
    2492
    2493                         for (j = 0; j < tmpTouches[i].targets.length; j++) {
    2494                             if (tmpTouches[i].targets[j].found) {
    2495                                 this.touches[this.touches.length - 1].targets.push({
    2496                                     num: tmpTouches[i].targets[j].num,
    2497                                     X: tmpTouches[i].targets[j].screenX,
    2498                                     Y: tmpTouches[i].targets[j].screenY,
    2499                                     Xprev: NaN,
    2500                                     Yprev: NaN,
    2501                                     Xstart: tmpTouches[i].targets[j].Xstart,
    2502                                     Ystart: tmpTouches[i].targets[j].Ystart,
    2503                                     Zstart: tmpTouches[i].targets[j].Zstart
    2504                                 });
    2505                             }
    2506                         }
    2507
    2508                     } else {
    2509                         tmpTouches[i].obj.noHighlight();
    2510                     }
    2511                 }
    2512
    2513             } else {
    2514                 this.touches.length = 0;
    2515             }
    2516
    2517             for (i = this.downObjects.length - 1; i > -1; i--) {
    2518                 found = false;
    2519                 for (j = 0; j < this.touches.length; j++) {
    2520                     if (this.touches[j].obj.id === this.downObjects[i].id) {
    2521                         found = true;
    2522                     }
    2523                 }
    2524                 if (!found) {
    2525                     this.downObjects[i].triggerEventHandlers(['touchup', 'up'], [evt]);
    2526                     this.downObjects[i].snapToGrid();
    2527                     this.downObjects[i].snapToPoints();
    2528                     this.downObjects.splice(i, 1);
    2529                 }
    2530             }
    2531
    2532             if (!evtTouches || evtTouches.length === 0) {
    2533
    2534                 if (this.hasTouchEnd) {
    2535                     Env.removeEvent(this.document, 'touchend', this.touchEndListener, this);
    2536                     this.hasTouchEnd = false;
    2537                 }
    2538
    2539                 this.dehighlightAll();
    2540                 this.updateQuality = this.BOARD_QUALITY_HIGH;
    2541
    2542                 this.originMoveEnd();
    2543                 this.update();
    2544             }
    2545
    2546             return true;
    2547         },
    2548
    2549         /**
    2550          * This method is called by the browser when the mouse button is clicked.
    2551          * @param {Event} evt The browsers event object.
    2552          * @returns {Boolean} True if no element is found under the current mouse pointer, false otherwise.
    2553          */
    2554         mouseDownListener: function (evt) {
    2555             var pos, elements, result;
    2556
    2557             // prevent accidental selection of text
    2558             if (this.document.selection && Type.isFunction(this.document.selection.empty)) {
    2559                 this.document.selection.empty();
    2560             } else if (window.getSelection) {
    2561                 window.getSelection().removeAllRanges();
    2562             }
    2563
    2564             if (!this.hasMouseUp) {
    2565                 Env.addEvent(this.document, 'mouseup', this.mouseUpListener, this);
    2566                 this.hasMouseUp = true;
    2567             } else {
    2568                 // In case this.hasMouseUp==true, it may be that there was a
    2569                 // mousedown event before which was not followed by an mouseup event.
    2570                 // This seems to happen with interactive whiteboard pens sometimes.
    2571                 return;
    2572             }
    2573
    2574             pos = this.getMousePosition(evt);
    2575
    2576             // selection
    2577             this._testForSelection(evt);
    2578             if (this.selectingMode) {
    2579                 this._startSelecting(pos);
    2580                 this.triggerEventHandlers(['mousestartselecting', 'startselecting'], [evt]);
    2581                 return;     // don't continue as a normal click
    2582             }
    2583
    2584             elements = this.initMoveObject(pos[0], pos[1], evt, 'mouse');
    2585
    2586             // if no draggable object can be found, get out here immediately
    2587             if (elements.length === 0) {
    2588                 this.mode = this.BOARD_MODE_NONE;
    2589                 result = true;
    2590             } else {
    2591                 this.mouse = {
    2592                     obj: null,
    2593                     targets: [{
    2594                         X: pos[0],
    2595                         Y: pos[1],
    2596                         Xprev: NaN,
    2597                         Yprev: NaN
    2598                     }]
    2599                 };
    2600                 this.mouse.obj = elements[elements.length - 1];
    2601
    2602                 this.dehighlightAll();
    2603                 this.mouse.obj.highlight(true);
    2604
    2605                 this.mouse.targets[0].Xstart = [];
    2606                 this.mouse.targets[0].Ystart = [];
    2607                 this.mouse.targets[0].Zstart = [];
    2608
    2609                 this.saveStartPos(this.mouse.obj, this.mouse.targets[0]);
    2610
    2611                 // prevent accidental text selection
    2612                 // this could get us new trouble: input fields, links and drop down boxes placed as text
    2613                 // on the board don't work anymore.
    2614                 if (evt && evt.preventDefault) {
    2615                     evt.preventDefault();
    2616                 } else if (window.event) {
    2617                     window.event.returnValue = false;
    2618                 }
    2619             }
    2620
    2621             if (this.mode === this.BOARD_MODE_NONE) {
    2622                 result = this.mouseOriginMoveStart(evt);
    2623             }
    2624
    2625             this.triggerEventHandlers(['mousedown', 'down'], [evt]);
    2626
    2627             return result;
    2628         },
    2629
    2630         /**
    2631          * This method is called by the browser when the mouse is moved.
    2632          * @param {Event} evt The browsers event object.
    2633          */
    2634         mouseMoveListener: function (evt) {
    2635             var pos;
    2636
    2637             pos = this.getMousePosition(evt);
    2638
    2639             this.updateQuality = this.BOARD_QUALITY_LOW;
    2640
    2641             if (this.mode !== this.BOARD_MODE_DRAG) {
    2642                 this.dehighlightAll();
    2643                 this.showInfobox(false);
    2644             }
    2645
    2646             // we have to check for four cases:
    2647             //   * user moves origin
    2648             //   * user drags an object
    2649             //   * user just moves the mouse, here highlight all elements at
    2650             //     the current mouse position
    2651             //   * the user is selecting
    2652
    2653             // selection
    2654             if (this.selectingMode) {
    2655                 this._moveSelecting(pos);
    2656                 this.triggerEventHandlers(['mousemoveselecting', 'moveselecting'], [evt, this.mode]);
    2657             } else if (!this.mouseOriginMove(evt)) {
    2658                 if (this.mode === this.BOARD_MODE_DRAG) {
    2659                     this.moveObject(pos[0], pos[1], this.mouse, evt, 'mouse');
    2660                 } else { // BOARD_MODE_NONE
    2661                     this.highlightElements(pos[0], pos[1], evt, -1);
    2662                 }
    2663                 this.triggerEventHandlers(['mousemove', 'move'], [evt, this.mode]);
    2664             }
    2665             this.updateQuality = this.BOARD_QUALITY_HIGH;
    2666         },
    2667
    2668         /**
    2669          * This method is called by the browser when the mouse button is released.
    2670          * @param {Event} evt
    2671          */
    2672         mouseUpListener: function (evt) {
    2673             var i;
    2674
    2675             if (this.selectingMode === false) {
    2676                 this.triggerEventHandlers(['mouseup', 'up'], [evt]);
    2677             }
    2678
    2679             // redraw with high precision
    2680             this.updateQuality = this.BOARD_QUALITY_HIGH;
    2681
    2682             if (this.mouse && this.mouse.obj) {
    2683                 // The parameter is needed for lines with snapToGrid enabled
    2684                 this.mouse.obj.snapToGrid(this.mouse.targets[0]);
    2685                 this.mouse.obj.snapToPoints();
    2686             }
    2687
    2688             this.originMoveEnd();
    2689             this.dehighlightAll();
    2690             this.update();
    2691
    2692             // selection
    2693             if (this.selectingMode) {
    2694                 this._stopSelecting(evt);
    2695                 this.triggerEventHandlers(['mousestopselecting', 'stopselecting'], [evt]);
    2696             } else {
    2697                 for (i = 0; i < this.downObjects.length; i++) {
    2698                     this.downObjects[i].triggerEventHandlers(['mouseup', 'up'], [evt]);
    2699                 }
    2700             }
    2701
    2702             this.downObjects.length = 0;
    2703
    2704             if (this.hasMouseUp) {
    2705                 Env.removeEvent(this.document, 'mouseup', this.mouseUpListener, this);
    2706                 this.hasMouseUp = false;
    2707             }
    2708
    2709             // release dragged mouse object
    2710             this.mouse = null;
    2711         },
    2712
    2713         /**
    2714          * Handler for mouse wheel events. Used to zoom in and out of the board.
    2715          * @param {Event} evt
    2716          * @returns {Boolean}
    2717          */
    2718         mouseWheelListener: function (evt) {
    2719             if (!this.attr.zoom.wheel || !this._isRequiredKeyPressed(evt, 'zoom')) {
    2720                 return true;
    2721             }
    2722
    2723             evt = evt || window.event;
    2724             var wd = evt.detail ? -evt.detail : evt.wheelDelta / 40,
    2725                 pos = new Coords(Const.COORDS_BY_SCREEN, this.getMousePosition(evt), this);
    2726
    2727             if (wd > 0) {
    2728                 this.zoomIn(pos.usrCoords[1], pos.usrCoords[2]);
    2729             } else {
    2730                 this.zoomOut(pos.usrCoords[1], pos.usrCoords[2]);
    2731             }
    2732
    2733             this.triggerEventHandlers(['mousewheel'], [evt]);
    2734
    2735             evt.preventDefault();
    2736             return false;
    2737         },
    2738
    2739         /**********************************************************
    2740          *
    2741          * End of Event Handlers
    2742          *
    2743          **********************************************************/
    2744
    2745         /**
    2746          * Initialize the info box object which is used to display
    2747          * the coordinates of points near the mouse pointer,
    2748          * @returns {JXG.Board} Reference to the board
    2749         */
    2750         initInfobox: function () {
    2751             var  attr = Type.copyAttributes({}, this.options, 'infobox');
    2752
    2753             attr.id = this.id + '_infobox';
    2754             this.infobox = this.create('text', [0, 0, '0,0'], attr);
    2755
    2756             this.infobox.distanceX = -20;
    2757             this.infobox.distanceY = 25;
    2758             // this.infobox.needsUpdateSize = false;  // That is not true, but it speeds drawing up.
    2759
    2760             this.infobox.dump = false;
    2761
    2762             this.showInfobox(false);
    2763             return this;
    2764         },
    2765
    2766         /**
    2767          * Updates and displays a little info box to show coordinates of current selected points.
    2768          * @param {JXG.GeometryElement} el A GeometryElement
    2769          * @returns {JXG.Board} Reference to the board
    2770          */
    2771         updateInfobox: function (el) {
    2772             var x, y, xc, yc,
    2773             vpinfoboxdigits;
    2774
    2775             if (!Type.evaluate(el.visProp.showinfobox)) {
    2776                 return this;
    2777             }
    2778
    2779             if (Type.isPoint(el)) {
    2780                 xc = el.coords.usrCoords[1];
    2781                 yc = el.coords.usrCoords[2];
    2782
    2783                 vpinfoboxdigits = Type.evaluate(el.visProp.infoboxdigits);
    2784                 this.infobox.setCoords(xc + this.infobox.distanceX / this.unitX,
    2785                                        yc + this.infobox.distanceY / this.unitY);
    2786
    2787                 if (typeof el.infoboxText !== 'string') {
    2788                     if (vpinfoboxdigits === 'auto') {
    2789                         x = Type.autoDigits(xc);
    2790                         y = Type.autoDigits(yc);
    2791                     } else if (Type.isNumber(vpinfoboxdigits)) {
    2792                         x = Type.toFixed(xc, vpinfoboxdigits);
    2793                         y = Type.toFixed(yc, vpinfoboxdigits);
    2794                     } else {
    2795                         x = xc;
    2796                         y = yc;
    2797                     }
    2798
    2799                     this.highlightInfobox(x, y, el);
    2800                 } else {
    2801                     this.highlightCustomInfobox(el.infoboxText, el);
    2802                 }
    2803
    2804                 this.showInfobox(true);
    2805             }
    2806             return this;
    2807         },
    2808
    2809         /**
    2810          * Set infobox visible / invisible.
    2811          *
    2812          * It uses its property hiddenByParent to memorize its status.
    2813          * In this way, many DOM access can be avoided.
    2814          *
    2815          * @param  {Boolean} val true for visible, false for invisible
    2816          * @return {JXG.Board} Reference to the board.
    2817          */
    2818         showInfobox: function(val) {
    2819             if (this.infobox.hiddenByParent == val) {
    2820                 this.infobox.hiddenByParent = !val;
    2821                 this.infobox.prepareUpdate().updateVisibility(val).updateRenderer();
    2822             }
    2823             return this;
    2824         },
    2825
    2826         /**
    2827          * Changes the text of the info box to show the given coordinates.
    2828          * @param {Number} x
    2829          * @param {Number} y
    2830          * @param {JXG.GeometryElement} [el] The element the mouse is pointing at
    2831          * @returns {JXG.Board} Reference to the board.
    2832          */
    2833         highlightInfobox: function (x, y, el) {
    2834             this.highlightCustomInfobox('(' + x + ', ' + y + ')', el);
    2835             return this;
    2836         },
    2837
    2838         /**
    2839          * Changes the text of the info box to what is provided via text.
    2840          * @param {String} text
    2841          * @param {JXG.GeometryElement} [el]
    2842          * @returns {JXG.Board} Reference to the board.
    2843          */
    2844         highlightCustomInfobox: function (text, el) {
    2845             this.infobox.setText(text);
    2846             return this;
    2847         },
    2848
    2849         /**
    2850          * Remove highlighting of all elements.
    2851          * @returns {JXG.Board} Reference to the board.
    2852          */
    2853         dehighlightAll: function () {
    2854             var el, pEl, needsDehighlight = false;
    2855
    2856             for (el in this.highlightedObjects) {
    2857                 if (this.highlightedObjects.hasOwnProperty(el)) {
    2858                     pEl = this.highlightedObjects[el];
    2859
    2860                     if (this.hasMouseHandlers || this.hasPointerHandlers) {
    2861                         pEl.noHighlight();
    2862                     }
    2863
    2864                     needsDehighlight = true;
    2865
    2866                     // In highlightedObjects should only be objects which fulfill all these conditions
    2867                     // And in case of complex elements, like a turtle based fractal, it should be faster to
    2868                     // just de-highlight the element instead of checking hasPoint...
    2869                     // if ((!Type.exists(pEl.hasPoint)) || !pEl.hasPoint(x, y) || !pEl.visPropCalc.visible)
    2870                 }
    2871             }
    2872
    2873             this.highlightedObjects = {};
    2874
    2875             // We do not need to redraw during dehighlighting in CanvasRenderer
    2876             // because we are redrawing anyhow
    2877             //  -- We do need to redraw during dehighlighting. Otherwise objects won't be dehighlighted until
    2878             // another object is highlighted.
    2879             if (this.renderer.type === 'canvas' && needsDehighlight) {
    2880                 this.prepareUpdate();
    2881                 this.renderer.suspendRedraw(this);
    2882                 this.updateRenderer();
    2883                 this.renderer.unsuspendRedraw();
    2884             }
    2885
    2886             return this;
    2887         },
    2888
    2889         /**
    2890          * Returns the input parameters in an array. This method looks pointless and it really is, but it had a purpose
    2891          * once.
    2892          * @param {Number} x X coordinate in screen coordinates
    2893          * @param {Number} y Y coordinate in screen coordinates
    2894          * @returns {Array} Coordinates of the mouse in screen coordinates.
    2895          */
    2896         getScrCoordsOfMouse: function (x, y) {
    2897             return [x, y];
    2898         },
    2899
    2900         /**
    2901          * This method calculates the user coords of the current mouse coordinates.
    2902          * @param {Event} evt Event object containing the mouse coordinates.
    2903          * @returns {Array} Coordinates of the mouse in screen coordinates.
    2904          */
    2905         getUsrCoordsOfMouse: function (evt) {
    2906             var cPos = this.getCoordsTopLeftCorner(),
    2907                 absPos = Env.getPosition(evt, null, this.document),
    2908                 x = absPos[0] - cPos[0],
    2909                 y = absPos[1] - cPos[1],
    2910                 newCoords = new Coords(Const.COORDS_BY_SCREEN, [x, y], this);
    2911
    2912             return newCoords.usrCoords.slice(1);
    2913         },
    2914
    2915         /**
    2916          * Collects all elements under current mouse position plus current user coordinates of mouse cursor.
    2917          * @param {Event} evt Event object containing the mouse coordinates.
    2918          * @returns {Array} Array of elements at the current mouse position plus current user coordinates of mouse.
    2919          */
    2920         getAllUnderMouse: function (evt) {
    2921             var elList = this.getAllObjectsUnderMouse(evt);
    2922             elList.push(this.getUsrCoordsOfMouse(evt));
    2923
    2924             return elList;
    2925         },
    2926
    2927         /**
    2928          * Collects all elements under current mouse position.
    2929          * @param {Event} evt Event object containing the mouse coordinates.
    2930          * @returns {Array} Array of elements at the current mouse position.
    2931          */
    2932         getAllObjectsUnderMouse: function (evt) {
    2933             var cPos = this.getCoordsTopLeftCorner(),
    2934                 absPos = Env.getPosition(evt, null, this.document),
    2935                 dx = absPos[0] - cPos[0],
    2936                 dy = absPos[1] - cPos[1],
    2937                 elList = [],
    2938                 el,
    2939                 pEl,
    2940                 len = this.objectsList.length;
    2941
    2942             for (el = 0; el < len; el++) {
    2943                 pEl = this.objectsList[el];
    2944                 if (pEl.visPropCalc.visible && pEl.hasPoint && pEl.hasPoint(dx, dy)) {
    2945                     elList[elList.length] = pEl;
    2946                 }
    2947             }
    2948
    2949             return elList;
    2950         },
    2951
    2952         /**
    2953          * Update the coords object of all elements which possess this
    2954          * property. This is necessary after changing the viewport.
    2955          * @returns {JXG.Board} Reference to this board.
    2956          **/
    2957         updateCoords: function () {
    2958             var el, ob, len = this.objectsList.length;
    2959
    2960             for (ob = 0; ob < len; ob++) {
    2961                 el = this.objectsList[ob];
    2962
    2963                 if (Type.exists(el.coords)) {
    2964                     if (Type.evaluate(el.visProp.frozen)) {
    2965                         el.coords.screen2usr();
    2966                     } else {
    2967                         el.coords.usr2screen();
    2968                     }
    2969                 }
    2970             }
    2971             return this;
    2972         },
    2973
    2974         /**
    2975          * Moves the origin and initializes an update of all elements.
    2976          * @param {Number} x
    2977          * @param {Number} y
    2978          * @param {Boolean} [diff=false]
    2979          * @returns {JXG.Board} Reference to this board.
    2980          */
    2981         moveOrigin: function (x, y, diff) {
    2982             if (Type.exists(x) && Type.exists(y)) {
    2983                 this.origin.scrCoords[1] = x;
    2984                 this.origin.scrCoords[2] = y;
    2985
    2986                 if (diff) {
    2987                     this.origin.scrCoords[1] -= this.drag_dx;
    2988                     this.origin.scrCoords[2] -= this.drag_dy;
    2989                 }
    2990             }
    2991
    2992             this.updateCoords().clearTraces().fullUpdate();
    2993             this.triggerEventHandlers(['boundingbox']);
    2994
    2995             return this;
    2996         },
    2997
    2998         /**
    2999          * Add conditional updates to the elements.
    3000          * @param {String} str String containing coniditional update in geonext syntax
    3001          */
    3002         addConditions: function (str) {
    3003             var term, m, left, right, name, el, property,
    3004                 functions = [],
    3005                 plaintext = 'var el, x, y, c, rgbo;\n',
    3006                 i = str.indexOf('<data>'),
    3007                 j = str.indexOf('<' + '/data>'),
    3008
    3009                 xyFun = function (board, el, f, what) {
    3010                     return function () {
    3011                         var e, t;
    3012
    3013                         e = board.select(el.id);
    3014                         t = e.coords.usrCoords[what];
    3015
    3016                         if (what === 2) {
    3017                             e.setPositionDirectly(Const.COORDS_BY_USER, [f(), t]);
    3018                         } else {
    3019                             e.setPositionDirectly(Const.COORDS_BY_USER, [t, f()]);
    3020                         }
    3021                         e.prepareUpdate().update();
    3022                     };
    3023                 },
    3024
    3025                 visFun = function (board, el, f) {
    3026                     return function () {
    3027                         var e, v;
    3028
    3029                         e = board.select(el.id);
    3030                         v = f();
    3031
    3032                         e.setAttribute({visible: v});
    3033                     };
    3034                 },
    3035
    3036                 colFun = function (board, el, f, what) {
    3037                     return function () {
    3038                         var e, v;
    3039
    3040                         e = board.select(el.id);
    3041                         v = f();
    3042
    3043                         if (what === 'strokewidth') {
    3044                             e.visProp.strokewidth = v;
    3045                         } else {
    3046                             v = Color.rgba2rgbo(v);
    3047                             e.visProp[what + 'color'] = v[0];
    3048                             e.visProp[what + 'opacity'] = v[1];
    3049                         }
    3050                     };
    3051                 },
    3052
    3053                 posFun = function (board, el, f) {
    3054                     return function () {
    3055                         var e = board.select(el.id);
    3056
    3057                         e.position = f();
    3058                     };
    3059                 },
    3060
    3061                 styleFun = function (board, el, f) {
    3062                     return function () {
    3063                         var e = board.select(el.id);
    3064
    3065                         e.setStyle(f());
    3066                     };
    3067                 };
    3068
    3069             if (i < 0) {
    3070                 return;
    3071             }
    3072
    3073             while (i >= 0) {
    3074                 term = str.slice(i + 6, j);   // throw away <data>
    3075                 m = term.indexOf('=');
    3076                 left = term.slice(0, m);
    3077                 right = term.slice(m + 1);
    3078                 m = left.indexOf('.');     // Dies erzeugt Probleme bei Variablennamen der Form " Steuern akt."
    3079                 name = left.slice(0, m);    //.replace(/\s+$/,''); // do NOT cut out name (with whitespace)
    3080                 el = this.elementsByName[Type.unescapeHTML(name)];
    3081
    3082                 property = left.slice(m + 1).replace(/\s+/g, '').toLowerCase(); // remove whitespace in property
    3083                 right = Type.createFunction (right, this, '', true);
    3084
    3085                 // Debug
    3086                 if (!Type.exists(this.elementsByName[name])) {
    3087                     JXG.debug("debug conditions: |" + name + "| undefined");
    3088                 } else {
    3089                     plaintext += "el = this.objects[\"" + el.id + "\"];\n";
    3090
    3091                     switch (property) {
    3092                     case 'x':
    3093                         functions.push(xyFun(this, el, right, 2));
    3094                         break;
    3095                     case 'y':
    3096                         functions.push(xyFun(this, el, right, 1));
    3097                         break;
    3098                     case 'visible':
    3099                         functions.push(visFun(this, el, right));
    3100                         break;
    3101                     case 'position':
    3102                         functions.push(posFun(this, el, right));
    3103                         break;
    3104                     case 'stroke':
    3105                         functions.push(colFun(this, el, right, 'stroke'));
    3106                         break;
    3107                     case 'style':
    3108                         functions.push(styleFun(this, el, right));
    3109                         break;
    3110                     case 'strokewidth':
    3111                         functions.push(colFun(this, el, right, 'strokewidth'));
    3112                         break;
    3113                     case 'fill':
    3114                         functions.push(colFun(this, el, right, 'fill'));
    3115                         break;
    3116                     case 'label':
    3117                         break;
    3118                     default:
    3119                         JXG.debug("property '" + property + "' in conditions not yet implemented:" + right);
    3120                         break;
    3121                     }
    3122                 }
    3123                 str = str.slice(j + 7); // cut off "</data>"
    3124                 i = str.indexOf('<data>');
    3125                 j = str.indexOf('<' + '/data>');
    3126             }
    3127
    3128             this.updateConditions = function () {
    3129                 var i;
    3130
    3131                 for (i = 0; i < functions.length; i++) {
    3132                     functions[i]();
    3133                 }
    3134
    3135                 this.prepareUpdate().updateElements();
    3136                 return true;
    3137             };
    3138             this.updateConditions();
    3139         },
    3140
    3141         /**
    3142          * Computes the commands in the conditions-section of the gxt file.
    3143          * It is evaluated after an update, before the unsuspendRedraw.
    3144          * The function is generated in
    3145          * @see JXG.Board#addConditions
    3146          * @private
    3147          */
    3148         updateConditions: function () {
    3149             return false;
    3150         },
    3151
    3152         /**
    3153          * Calculates adequate snap sizes.
    3154          * @returns {JXG.Board} Reference to the board.
    3155          */
    3156         calculateSnapSizes: function () {
    3157             var p1 = new Coords(Const.COORDS_BY_USER, [0, 0], this),
    3158                 p2 = new Coords(Const.COORDS_BY_USER, [this.options.grid.gridX, this.options.grid.gridY], this),
    3159                 x = p1.scrCoords[1] - p2.scrCoords[1],
    3160                 y = p1.scrCoords[2] - p2.scrCoords[2];
    3161
    3162             this.options.grid.snapSizeX = this.options.grid.gridX;
    3163             while (Math.abs(x) > 25) {
    3164                 this.options.grid.snapSizeX *= 2;
    3165                 x /= 2;
    3166             }
    3167
    3168             this.options.grid.snapSizeY = this.options.grid.gridY;
    3169             while (Math.abs(y) > 25) {
    3170                 this.options.grid.snapSizeY *= 2;
    3171                 y /= 2;
    3172             }
    3173
    3174             return this;
    3175         },
    3176
    3177         /**
    3178          * Apply update on all objects with the new zoom-factors. Clears all traces.
    3179          * @returns {JXG.Board} Reference to the board.
    3180          */
    3181         applyZoom: function () {
    3182             this.updateCoords().calculateSnapSizes().clearTraces().fullUpdate();
    3183
    3184             return this;
    3185         },
    3186
    3187         /**
    3188          * Zooms into the board by the factors board.attr.zoom.factorX and board.attr.zoom.factorY and applies the zoom.
    3189          * The zoom operation is centered at x, y.
    3190          * @param {Number} [x]
    3191          * @param {Number} [y]
    3192          * @returns {JXG.Board} Reference to the board
    3193          */
    3194         zoomIn: function (x, y) {
    3195             var bb = this.getBoundingBox(),
    3196                 zX = this.attr.zoom.factorx,
    3197                 zY = this.attr.zoom.factory,
    3198                 dX = (bb[2] - bb[0]) * (1.0 - 1.0 / zX),
    3199                 dY = (bb[1] - bb[3]) * (1.0 - 1.0 / zY),
    3200                 lr = 0.5,
    3201                 tr = 0.5,
    3202                 mi = this.attr.zoom.eps || this.attr.zoom.min || 0.001;  // this.attr.zoom.eps is deprecated
    3203
    3204             if ((this.zoomX > this.attr.zoom.max && zX > 1.0) ||
    3205                 (this.zoomY > this.attr.zoom.max && zY > 1.0) ||
    3206                 (this.zoomX < mi && zX < 1.0) ||  // zoomIn is used for all zooms on touch devices
    3207                 (this.zoomY < mi && zY < 1.0)) {
    3208                 return this;
    3209             }
    3210
    3211             if (Type.isNumber(x) && Type.isNumber(y)) {
    3212                 lr = (x - bb[0]) / (bb[2] - bb[0]);
    3213                 tr = (bb[1] - y) / (bb[1] - bb[3]);
    3214             }
    3215
    3216             this.setBoundingBox([bb[0] + dX * lr, bb[1] - dY * tr, bb[2] - dX * (1 - lr), bb[3] + dY * (1 - tr)], false);
    3217             this.zoomX *= zX;
    3218             this.zoomY *= zY;
    3219             return this.applyZoom();
    3220         },
    3221
    3222         /**
    3223          * Zooms out of the board by the factors board.attr.zoom.factorX and board.attr.zoom.factorY and applies the zoom.
    3224          * The zoom operation is centered at x, y.
    3225          *
    3226          * @param {Number} [x]
    3227          * @param {Number} [y]
    3228          * @returns {JXG.Board} Reference to the board
    3229          */
    3230         zoomOut: function (x, y) {
    3231             var bb = this.getBoundingBox(),
    3232                 zX = this.attr.zoom.factorx,
    3233                 zY = this.attr.zoom.factory,
    3234                 dX = (bb[2] - bb[0]) * (1.0 - zX),
    3235                 dY = (bb[1] - bb[3]) * (1.0 - zY),
    3236                 lr = 0.5,
    3237                 tr = 0.5,
    3238                 mi = this.attr.zoom.eps || this.attr.zoom.min || 0.001;  // this.attr.zoom.eps is deprecated
    3239
    3240             if (this.zoomX < mi || this.zoomY < mi) {
    3241                 return this;
    3242             }
    3243
    3244             if (Type.isNumber(x) && Type.isNumber(y)) {
    3245                 lr = (x - bb[0]) / (bb[2] - bb[0]);
    3246                 tr = (bb[1] - y) / (bb[1] - bb[3]);
    3247             }
    3248
    3249             this.setBoundingBox([bb[0] + dX * lr, bb[1] - dY * tr, bb[2] - dX * (1 - lr), bb[3] + dY * (1 - tr)], false);
    3250             this.zoomX /= zX;
    3251             this.zoomY /= zY;
    3252
    3253             return this.applyZoom();
    3254         },
    3255
    3256         /**
    3257          * Resets zoom factor to 100%.
    3258          * @returns {JXG.Board} Reference to the board
    3259          */
    3260         zoom100: function () {
    3261             var bb = this.getBoundingBox(),
    3262                 dX = (bb[2] - bb[0]) * (1.0 - this.zoomX) * 0.5,
    3263                 dY = (bb[1] - bb[3]) * (1.0 - this.zoomY) * 0.5;
    3264
    3265             this.setBoundingBox([bb[0] + dX, bb[1] - dY, bb[2] - dX, bb[3] + dY], false);
    3266             this.zoomX = 1.0;
    3267             this.zoomY = 1.0;
    3268             return this.applyZoom();
    3269         },
    3270
    3271         /**
    3272          * Zooms the board so every visible point is shown. Keeps aspect ratio.
    3273          * @returns {JXG.Board} Reference to the board
    3274          */
    3275         zoomAllPoints: function () {
    3276             var el, border, borderX, borderY, pEl,
    3277                 minX = 0,
    3278                 maxX = 0,
    3279                 minY = 0,
    3280                 maxY = 0,
    3281                 len = this.objectsList.length;
    3282
    3283             for (el = 0; el < len; el++) {
    3284                 pEl = this.objectsList[el];
    3285
    3286                 if (Type.isPoint(pEl) && pEl.visPropCalc.visible) {
    3287                     if (pEl.coords.usrCoords[1] < minX) {
    3288                         minX = pEl.coords.usrCoords[1];
    3289                     } else if (pEl.coords.usrCoords[1] > maxX) {
    3290                         maxX = pEl.coords.usrCoords[1];
    3291                     }
    3292                     if (pEl.coords.usrCoords[2] > maxY) {
    3293                         maxY = pEl.coords.usrCoords[2];
    3294                     } else if (pEl.coords.usrCoords[2] < minY) {
    3295                         minY = pEl.coords.usrCoords[2];
    3296                     }
    3297                 }
    3298             }
    3299
    3300             border = 50;
    3301             borderX = border / this.unitX;
    3302             borderY = border / this.unitY;
    3303
    3304             this.zoomX = 1.0;
    3305             this.zoomY = 1.0;
    3306
    3307             this.setBoundingBox([minX - borderX, maxY + borderY, maxX + borderX, minY - borderY], true);
    3308
    3309             return this.applyZoom();
    3310         },
    3311
    3312         /**
    3313          * Reset the bounding box and the zoom level to 100% such that a given set of elements is within the board's viewport.
    3314          * @param {Array} elements A set of elements given by id, reference, or name.
    3315          * @returns {JXG.Board} Reference to the board.
    3316          */
    3317         zoomElements: function (elements) {
    3318             var i, j, e, box,
    3319                 newBBox = [0, 0, 0, 0],
    3320                 dir = [1, -1, -1, 1];
    3321
    3322             if (!Type.isArray(elements) || elements.length === 0) {
    3323                 return this;
    3324             }
    3325
    3326             for (i = 0; i < elements.length; i++) {
    3327                 e = this.select(elements[i]);
    3328
    3329                 box = e.bounds();
    3330                 if (Type.isArray(box)) {
    3331                     if (Type.isArray(newBBox)) {
    3332                         for (j = 0; j < 4; j++) {
    3333                             if (dir[j] * box[j] < dir[j] * newBBox[j]) {
    3334                                 newBBox[j] = box[j];
    3335                             }
    3336                         }
    3337                     } else {
    3338                         newBBox = box;
    3339                     }
    3340                 }
    3341             }
    3342
    3343             if (Type.isArray(newBBox)) {
    3344                 for (j = 0; j < 4; j++) {
    3345                     newBBox[j] -= dir[j];
    3346                 }
    3347
    3348                 this.zoomX = 1.0;
    3349                 this.zoomY = 1.0;
    3350                 this.setBoundingBox(newBBox, true);
    3351             }
    3352
    3353             return this;
    3354         },
    3355
    3356         /**
    3357          * Sets the zoom level to <tt>fX</tt> resp <tt>fY</tt>.
    3358          * @param {Number} fX
    3359          * @param {Number} fY
    3360          * @returns {JXG.Board} Reference to the board.
    3361          */
    3362         setZoom: function (fX, fY) {
    3363             var oX = this.attr.zoom.factorx,
    3364                 oY = this.attr.zoom.factory;
    3365
    3366             this.attr.zoom.factorx = fX / this.zoomX;
    3367             this.attr.zoom.factory = fY / this.zoomY;
    3368
    3369             this.zoomIn();
    3370
    3371             this.attr.zoom.factorx = oX;
    3372             this.attr.zoom.factory = oY;
    3373
    3374             return this;
    3375         },
    3376
    3377         /**
    3378          * Removes object from board and renderer.
    3379          * @param {JXG.GeometryElement} object The object to remove.
    3380          * @returns {JXG.Board} Reference to the board
    3381          */
    3382         removeObject: function (object) {
    3383             var el, i;
    3384
    3385             if (Type.isArray(object)) {
    3386                 for (i = 0; i < object.length; i++) {
    3387                     this.removeObject(object[i]);
    3388                 }
    3389
    3390                 return this;
    3391             }
    3392
    3393             object = this.select(object);
    3394
    3395             // If the object which is about to be removed unknown or a string, do nothing.
    3396             // it is a string if a string was given and could not be resolved to an element.
    3397             if (!Type.exists(object) || Type.isString(object)) {
    3398                 return this;
    3399             }
    3400
    3401             try {
    3402                 // remove all children.
    3403                 for (el in object.childElements) {
    3404                     if (object.childElements.hasOwnProperty(el)) {
    3405                         object.childElements[el].board.removeObject(object.childElements[el]);
    3406                     }
    3407                 }
    3408
    3409                 // Remove all children in elements like turtle
    3410                 for (el in object.objects) {
    3411                     if (object.objects.hasOwnProperty(el)) {
    3412                         object.objects[el].board.removeObject(object.objects[el]);
    3413                     }
    3414                 }
    3415
    3416                 for (el in this.objects) {
    3417                     if (this.objects.hasOwnProperty(el) && Type.exists(this.objects[el].childElements)) {
    3418                         delete this.objects[el].childElements[object.id];
    3419                         delete this.objects[el].descendants[object.id];
    3420                     }
    3421                 }
    3422
    3423                 // remove the object itself from our control structures
    3424                 if (object._pos > -1) {
    3425                     this.objectsList.splice(object._pos, 1);
    3426                     for (el = object._pos; el < this.objectsList.length; el++) {
    3427                         this.objectsList[el]._pos--;
    3428                     }
    3429                 } else if (object.type !== Const.OBJECT_TYPE_TURTLE) {
    3430                     JXG.debug('Board.removeObject: object ' + object.id + ' not found in list.');
    3431                 }
    3432
    3433                 delete this.objects[object.id];
    3434                 delete this.elementsByName[object.name];
    3435
    3436
    3437                 if (object.visProp && Type.evaluate(object.visProp.trace)) {
    3438                     object.clearTrace();
    3439                 }
    3440
    3441                 // the object deletion itself is handled by the object.
    3442                 if (Type.exists(object.remove)) {
    3443                     object.remove();
    3444                 }
    3445             } catch (e) {
    3446                 JXG.debug(object.id + ': Could not be removed: ' + e);
    3447             }
    3448
    3449             this.update();
    3450
    3451             return this;
    3452         },
    3453
    3454         /**
    3455          * Removes the ancestors of an object an the object itself from board and renderer.
    3456          * @param {JXG.GeometryElement} object The object to remove.
    3457          * @returns {JXG.Board} Reference to the board
    3458          */
    3459         removeAncestors: function (object) {
    3460             var anc;
    3461
    3462             for (anc in object.ancestors) {
    3463                 if (object.ancestors.hasOwnProperty(anc)) {
    3464                     this.removeAncestors(object.ancestors[anc]);
    3465                 }
    3466             }
    3467
    3468             this.removeObject(object);
    3469
    3470             return this;
    3471         },
    3472
    3473         /**
    3474          * Initialize some objects which are contained in every GEONExT construction by default,
    3475          * but are not contained in the gxt files.
    3476          * @returns {JXG.Board} Reference to the board
    3477          */
    3478         initGeonextBoard: function () {
    3479             var p1, p2, p3;
    3480
    3481             p1 = this.create('point', [0, 0], {
    3482                 id: this.id + 'g00e0',
    3483                 name: 'Ursprung',
    3484                 withLabel: false,
    3485                 visible: false,
    3486                 fixed: true
    3487             });
    3488
    3489             p2 = this.create('point', [1, 0], {
    3490                 id: this.id + 'gX0e0',
    3491                 name: 'Punkt_1_0',
    3492                 withLabel: false,
    3493                 visible: false,
    3494                 fixed: true
    3495             });
    3496
    3497             p3 = this.create('point', [0, 1], {
    3498                 id: this.id + 'gY0e0',
    3499                 name: 'Punkt_0_1',
    3500                 withLabel: false,
    3501                 visible: false,
    3502                 fixed: true
    3503             });
    3504
    3505             this.create('line', [p1, p2], {
    3506                 id: this.id + 'gXLe0',
    3507                 name: 'X-Achse',
    3508                 withLabel: false,
    3509                 visible: false
    3510             });
    3511
    3512             this.create('line', [p1, p3], {
    3513                 id: this.id + 'gYLe0',
    3514                 name: 'Y-Achse',
    3515                 withLabel: false,
    3516                 visible: false
    3517             });
    3518
    3519             return this;
    3520         },
    3521
    3522         /**
    3523          * Change the height and width of the board's container.
    3524          * After doing so, {@link JXG.JSXGraph.setBoundingBox} is called using
    3525          * the actual size of the bounding box and the actual value of keepaspectratio.
    3526          * If setBoundingbox() should not be called automatically,
    3527          * call resizeContainer with dontSetBoundingBox == true.
    3528          * @param {Number} canvasWidth New width of the container.
    3529          * @param {Number} canvasHeight New height of the container.
    3530          * @param {Boolean} [dontset=false] If true do not set the height of the DOM element.
    3531          * @param {Boolean} [dontSetBoundingBox=false] If true do not call setBoundingBox().
    3532          * @returns {JXG.Board} Reference to the board
    3533          */
    3534         resizeContainer: function (canvasWidth, canvasHeight, dontset, dontSetBoundingBox) {
    3535             var box;
    3536
    3537             if (!dontSetBoundingBox) {
    3538                 box = this.getBoundingBox();
    3539             }
    3540             this.canvasWidth = parseInt(canvasWidth, 10);
    3541             this.canvasHeight = parseInt(canvasHeight, 10);
    3542
    3543             if (!dontset) {
    3544                 this.containerObj.style.width = (this.canvasWidth) + 'px';
    3545                 this.containerObj.style.height = (this.canvasHeight) + 'px';
    3546             }
    3547
    3548             this.renderer.resize(this.canvasWidth, this.canvasHeight);
    3549
    3550             if (!dontSetBoundingBox) {
    3551                 this.setBoundingBox(box, this.keepaspectratio);
    3552             }
    3553
    3554             return this;
    3555         },
    3556
    3557         /**
    3558          * Lists the dependencies graph in a new HTML-window.
    3559          * @returns {JXG.Board} Reference to the board
    3560          */
    3561         showDependencies: function () {
    3562             var el, t, c, f, i;
    3563
    3564             t = '<p>\n';
    3565             for (el in this.objects) {
    3566                 if (this.objects.hasOwnProperty(el)) {
    3567                     i = 0;
    3568                     for (c in this.objects[el].childElements) {
    3569                         if (this.objects[el].childElements.hasOwnProperty(c)) {
    3570                             i += 1;
    3571                         }
    3572                     }
    3573                     if (i >= 0) {
    3574                         t += '<strong>' + this.objects[el].id + ':<' + '/strong> ';
    3575                     }
    3576
    3577                     for (c in this.objects[el].childElements) {
    3578                         if (this.objects[el].childElements.hasOwnProperty(c)) {
    3579                             t += this.objects[el].childElements[c].id + '(' + this.objects[el].childElements[c].name + ')' + ', ';
    3580                         }
    3581                     }
    3582                     t += '<p>\n';
    3583                 }
    3584             }
    3585             t += '<' + '/p>\n';
    3586             f = window.open();
    3587             f.document.open();
    3588             f.document.write(t);
    3589             f.document.close();
    3590             return this;
    3591         },
    3592
    3593         /**
    3594          * Lists the XML code of the construction in a new HTML-window.
    3595          * @returns {JXG.Board} Reference to the board
    3596          */
    3597         showXML: function () {
    3598             var f = window.open('');
    3599             f.document.open();
    3600             f.document.write('<pre>' + Type.escapeHTML(this.xmlString) + '<' + '/pre>');
    3601             f.document.close();
    3602             return this;
    3603         },
    3604
    3605         /**
    3606          * Sets for all objects the needsUpdate flag to "true".
    3607          * @returns {JXG.Board} Reference to the board
    3608          */
    3609         prepareUpdate: function () {
    3610             var el, pEl, len = this.objectsList.length;
    3611
    3612             /*
    3613             if (this.attr.updatetype === 'hierarchical') {
    3614                 return this;
    3615             }
    3616             */
    3617
    3618             for (el = 0; el < len; el++) {
    3619                 pEl = this.objectsList[el];
    3620                 pEl.needsUpdate = pEl.needsRegularUpdate || this.needsFullUpdate;
    3621             }
    3622
    3623             for (el in this.groups) {
    3624                 if (this.groups.hasOwnProperty(el)) {
    3625                     pEl = this.groups[el];
    3626                     pEl.needsUpdate = pEl.needsRegularUpdate || this.needsFullUpdate;
    3627                 }
    3628             }
    3629
    3630             return this;
    3631         },
    3632
    3633         /**
    3634          * Runs through all elements and calls their update() method.
    3635          * @param {JXG.GeometryElement} drag Element that caused the update.
    3636          * @returns {JXG.Board} Reference to the board
    3637          */
    3638         updateElements: function (drag) {
    3639             var el, pEl;
    3640             //var childId, i = 0;
    3641
    3642             drag = this.select(drag);
    3643
    3644             /*
    3645             if (Type.exists(drag)) {
    3646                 for (el = 0; el < this.objectsList.length; el++) {
    3647                     pEl = this.objectsList[el];
    3648                     if (pEl.id === drag.id) {
    3649                         i = el;
    3650                         break;
    3651                     }
    3652                 }
    3653             }
    3654             */
    3655
    3656             for (el = 0; el < this.objectsList.length; el++) {
    3657                 pEl = this.objectsList[el];
    3658                 // For updates of an element we distinguish if the dragged element is updated or
    3659                 // other elements are updated.
    3660                 // The difference lies in the treatment of gliders.
    3661                 pEl.update(!Type.exists(drag) || pEl.id !== drag.id)
    3662                    .updateVisibility();
    3663             }
    3664
    3665             // update groups last
    3666             for (el in this.groups) {
    3667                 if (this.groups.hasOwnProperty(el)) {
    3668                     this.groups[el].update(drag);
    3669                 }
    3670             }
    3671
    3672             return this;
    3673         },
    3674
    3675         /**
    3676          * Runs through all elements and calls their update() method.
    3677          * @returns {JXG.Board} Reference to the board
    3678          */
    3679         updateRenderer: function () {
    3680             var el,
    3681                 len = this.objectsList.length;
    3682
    3683             /*
    3684             objs = this.objectsList.slice(0);
    3685             objs.sort(function (a, b) {
    3686                 if (a.visProp.layer < b.visProp.layer) {
    3687                     return -1;
    3688                 } else if (a.visProp.layer === b.visProp.layer) {
    3689                     return b.lastDragTime.getTime() - a.lastDragTime.getTime();
    3690                 } else {
    3691                     return 1;
    3692                 }
    3693             });
    3694             */
    3695
    3696             if (this.renderer.type === 'canvas') {
    3697                 this.updateRendererCanvas();
    3698             } else {
    3699                 for (el = 0; el < len; el++) {
    3700                     this.objectsList[el].updateRenderer();
    3701                 }
    3702             }
    3703             return this;
    3704         },
    3705
    3706         /**
    3707          * Runs through all elements and calls their update() method.
    3708          * This is a special version for the CanvasRenderer.
    3709          * Here, we have to do our own layer handling.
    3710          * @returns {JXG.Board} Reference to the board
    3711          */
    3712         updateRendererCanvas: function () {
    3713             var el, pEl, i, mini, la,
    3714                 olen = this.objectsList.length,
    3715                 layers = this.options.layer,
    3716                 len = this.options.layer.numlayers,
    3717                 last = Number.NEGATIVE_INFINITY;
    3718
    3719             for (i = 0; i < len; i++) {
    3720                 mini = Number.POSITIVE_INFINITY;
    3721
    3722                 for (la in layers) {
    3723                     if (layers.hasOwnProperty(la)) {
    3724                         if (layers[la] > last && layers[la] < mini) {
    3725                             mini = layers[la];
    3726                         }
    3727                     }
    3728                 }
    3729
    3730                 last = mini;
    3731
    3732                 for (el = 0; el < olen; el++) {
    3733                     pEl = this.objectsList[el];
    3734
    3735                     if (pEl.visProp.layer === mini) {
    3736                         pEl.prepareUpdate().updateRenderer();
    3737                     }
    3738                 }
    3739             }
    3740             return this;
    3741         },
    3742
    3743         /**
    3744          * Please use {@link JXG.Board.on} instead.
    3745          * @param {Function} hook A function to be called by the board after an update occurred.
    3746          * @param {String} [m='update'] When the hook is to be called. Possible values are <i>mouseup</i>, <i>mousedown</i> and <i>update</i>.
    3747          * @param {Object} [context=board] Determines the execution context the hook is called. This parameter is optional, default is the
    3748          * board object the hook is attached to.
    3749          * @returns {Number} Id of the hook, required to remove the hook from the board.
    3750          * @deprecated
    3751          */
    3752         addHook: function (hook, m, context) {
    3753             JXG.deprecated('Board.addHook()', 'Board.on()');
    3754             m = Type.def(m, 'update');
    3755
    3756             context = Type.def(context, this);
    3757
    3758             this.hooks.push([m, hook]);
    3759             this.on(m, hook, context);
    3760
    3761             return this.hooks.length - 1;
    3762         },
    3763
    3764         /**
    3765          * Alias of {@link JXG.Board.on}.
    3766          */
    3767         addEvent: JXG.shortcut(JXG.Board.prototype, 'on'),
    3768
    3769         /**
    3770          * Please use {@link JXG.Board.off} instead.
    3771          * @param {Number|function} id The number you got when you added the hook or a reference to the event handler.
    3772          * @returns {JXG.Board} Reference to the board
    3773          * @deprecated
    3774          */
    3775         removeHook: function (id) {
    3776             JXG.deprecated('Board.removeHook()', 'Board.off()');
    3777             if (this.hooks[id]) {
    3778                 this.off(this.hooks[id][0], this.hooks[id][1]);
    3779                 this.hooks[id] = null;
    3780             }
    3781
    3782             return this;
    3783         },
    3784
    3785         /**
    3786          * Alias of {@link JXG.Board.off}.
    3787          */
    3788         removeEvent: JXG.shortcut(JXG.Board.prototype, 'off'),
    3789
    3790         /**
    3791          * Runs through all hooked functions and calls them.
    3792          * @returns {JXG.Board} Reference to the board
    3793          * @deprecated
    3794          */
    3795         updateHooks: function (m) {
    3796             var arg = Array.prototype.slice.call(arguments, 0);
    3797
    3798             JXG.deprecated('Board.updateHooks()', 'Board.triggerEventHandlers()');
    3799
    3800             arg[0] = Type.def(arg[0], 'update');
    3801             this.triggerEventHandlers([arg[0]], arguments);
    3802
    3803             return this;
    3804         },
    3805
    3806         /**
    3807          * Adds a dependent board to this board.
    3808          * @param {JXG.Board} board A reference to board which will be updated after an update of this board occurred.
    3809          * @returns {JXG.Board} Reference to the board
    3810          */
    3811         addChild: function (board) {
    3812             if (Type.exists(board) && Type.exists(board.containerObj)) {
    3813                 this.dependentBoards.push(board);
    3814                 this.update();
    3815             }
    3816             return this;
    3817         },
    3818
    3819         /**
    3820          * Deletes a board from the list of dependent boards.
    3821          * @param {JXG.Board} board Reference to the board which will be removed.
    3822          * @returns {JXG.Board} Reference to the board
    3823          */
    3824         removeChild: function (board) {
    3825             var i;
    3826
    3827             for (i = this.dependentBoards.length - 1; i >= 0; i--) {
    3828                 if (this.dependentBoards[i] === board) {
    3829                     this.dependentBoards.splice(i, 1);
    3830                 }
    3831             }
    3832             return this;
    3833         },
    3834
    3835         /**
    3836          * Runs through most elements and calls their update() method and update the conditions.
    3837          * @param {JXG.GeometryElement} [drag] Element that caused the update.
    3838          * @returns {JXG.Board} Reference to the board
    3839          */
    3840         update: function (drag) {
    3841             var i, len, b, insert;
    3842
    3843             if (this.inUpdate || this.isSuspendedUpdate) {
    3844                 return this;
    3845             }
    3846             this.inUpdate = true;
    3847
    3848             if (this.attr.minimizereflow === 'all' && this.containerObj && this.renderer.type !== 'vml') {
    3849                 insert = this.renderer.removeToInsertLater(this.containerObj);
    3850             }
    3851
    3852             if (this.attr.minimizereflow === 'svg' && this.renderer.type === 'svg') {
    3853                 insert = this.renderer.removeToInsertLater(this.renderer.svgRoot);
    3854             }
    3855             this.prepareUpdate().updateElements(drag).updateConditions();
    3856
    3857             this.renderer.suspendRedraw(this);
    3858             this.updateRenderer();
    3859             this.renderer.unsuspendRedraw();
    3860             this.triggerEventHandlers(['update'], []);
    3861
    3862             if (insert) {
    3863                 insert();
    3864             }
    3865
    3866             // To resolve dependencies between boards
    3867             // for (var board in JXG.boards) {
    3868             len = this.dependentBoards.length;
    3869             for (i = 0; i < len; i++) {
    3870                 b = this.dependentBoards[i];
    3871                 if (Type.exists(b) && b !== this) {
    3872                     b.updateQuality = this.updateQuality;
    3873                     b.prepareUpdate().updateElements().updateConditions();
    3874                     b.renderer.suspendRedraw();
    3875                     b.updateRenderer();
    3876                     b.renderer.unsuspendRedraw();
    3877                     b.triggerEventHandlers(['update'], []);
    3878                 }
    3879
    3880             }
    3881
    3882             this.inUpdate = false;
    3883             return this;
    3884         },
    3885
    3886         /**
    3887          * Runs through all elements and calls their update() method and update the conditions.
    3888          * This is necessary after zooming and changing the bounding box.
    3889          * @returns {JXG.Board} Reference to the board
    3890          */
    3891         fullUpdate: function () {
    3892             this.needsFullUpdate = true;
    3893             this.update();
    3894             this.needsFullUpdate = false;
    3895             return this;
    3896         },
    3897
    3898         /**
    3899          * Adds a grid to the board according to the settings given in board.options.
    3900          * @returns {JXG.Board} Reference to the board.
    3901          */
    3902         addGrid: function () {
    3903             this.create('grid', []);
    3904
    3905             return this;
    3906         },
    3907
    3908         /**
    3909          * Removes all grids assigned to this board. Warning: This method also removes all objects depending on one or
    3910          * more of the grids.
    3911          * @returns {JXG.Board} Reference to the board object.
    3912          */
    3913         removeGrids: function () {
    3914             var i;
    3915
    3916             for (i = 0; i < this.grids.length; i++) {
    3917                 this.removeObject(this.grids[i]);
    3918             }
    3919
    3920             this.grids.length = 0;
    3921             this.update(); // required for canvas renderer
    3922
    3923             return this;
    3924         },
    3925
    3926         /**
    3927          * Creates a new geometric element of type elementType.
    3928          * @param {String} elementType Type of the element to be constructed given as a string e.g. 'point' or 'circle'.
    3929          * @param {Array} parents Array of parent elements needed to construct the element e.g. coordinates for a point or two
    3930          * points to construct a line. This highly depends on the elementType that is constructed. See the corresponding JXG.create*
    3931          * methods for a list of possible parameters.
    3932          * @param {Object} [attributes] An object containing the attributes to be set. This also depends on the elementType.
    3933          * Common attributes are name, visible, strokeColor.
    3934          * @returns {Object} Reference to the created element. This is usually a GeometryElement, but can be an array containing
    3935          * two or more elements.
    3936          */
    3937         create: function (elementType, parents, attributes) {
    3938             var el, i;
    3939
    3940             elementType = elementType.toLowerCase();
    3941
    3942             if (!Type.exists(parents)) {
    3943                 parents = [];
    3944             }
    3945
    3946             if (!Type.exists(attributes)) {
    3947                 attributes = {};
    3948             }
    3949
    3950             for (i = 0; i < parents.length; i++) {
    3951                 if (Type.isString(parents[i]) &&
    3952                     !(elementType === 'text' && i === 2) &&
    3953                     !((elementType === 'input' || elementType === 'checkbox' || elementType === 'button') &&
    3954                       (i === 2 || i === 3))
    3955                 ) {
    3956                     parents[i] = this.select(parents[i]);
    3957                 }
    3958             }
    3959
    3960             if (Type.isFunction(JXG.elements[elementType])) {
    3961                 el = JXG.elements[elementType](this, parents, attributes);
    3962             } else {
    3963                 throw new Error("JSXGraph: create: Unknown element type given: " + elementType);
    3964             }
    3965
    3966             if (!Type.exists(el)) {
    3967                 JXG.debug("JSXGraph: create: failure creating " + elementType);
    3968                 return el;
    3969             }
    3970
    3971             if (el.prepareUpdate && el.update && el.updateRenderer) {
    3972                 el.fullUpdate();
    3973             }
    3974             return el;
    3975         },
    3976
    3977         /**
    3978          * Deprecated name for {@link JXG.Board.create}.
    3979          * @deprecated
    3980          */
    3981         createElement: function () {
    3982             JXG.deprecated('Board.createElement()', 'Board.create()');
    3983             return this.create.apply(this, arguments);
    3984         },
    3985
    3986         /**
    3987          * Delete the elements drawn as part of a trace of an element.
    3988          * @returns {JXG.Board} Reference to the board
    3989          */
    3990         clearTraces: function () {
    3991             var el;
    3992
    3993             for (el = 0; el < this.objectsList.length; el++) {
    3994                 this.objectsList[el].clearTrace();
    3995             }
    3996
    3997             this.numTraces = 0;
    3998             return this;
    3999         },
    4000
    4001         /**
    4002          * Stop updates of the board.
    4003          * @returns {JXG.Board} Reference to the board
    4004          */
    4005         suspendUpdate: function () {
    4006             if (!this.inUpdate) {
    4007                 this.isSuspendedUpdate = true;
    4008             }
    4009             return this;
    4010         },
    4011
    4012         /**
    4013          * Enable updates of the board.
    4014          * @returns {JXG.Board} Reference to the board
    4015          */
    4016         unsuspendUpdate: function () {
    4017             if (this.isSuspendedUpdate) {
    4018                 this.isSuspendedUpdate = false;
    4019                 this.fullUpdate();
    4020             }
    4021             return this;
    4022         },
    4023
    4024         /**
    4025          * Set the bounding box of the board.
    4026          * @param {Array} bbox New bounding box [x1,y1,x2,y2]
    4027          * @param {Boolean} [keepaspectratio=false] If set to true, the aspect ratio will be 1:1, but
    4028          * the resulting viewport may be larger.
    4029          * @returns {JXG.Board} Reference to the board
    4030          */
    4031         setBoundingBox: function (bbox, keepaspectratio) {
    4032             var h, w,
    4033                 dim = Env.getDimensions(this.container, this.document);
    4034
    4035             if (!Type.isArray(bbox)) {
    4036                 return this;
    4037             }
    4038
    4039             this.plainBB = bbox;
    4040
    4041             this.canvasWidth = parseInt(dim.width, 10);
    4042             this.canvasHeight = parseInt(dim.height, 10);
    4043             w = this.canvasWidth;
    4044             h = this.canvasHeight;
    4045
    4046             if (keepaspectratio) {
    4047                 this.unitX = w / (bbox[2] - bbox[0]);
    4048                 this.unitY = h / (bbox[1] - bbox[3]);
    4049                 if (Math.abs(this.unitX) < Math.abs(this.unitY)) {
    4050                     this.unitY = Math.abs(this.unitX) * this.unitY / Math.abs(this.unitY);
    4051                 } else {
    4052                     this.unitX = Math.abs(this.unitY) * this.unitX / Math.abs(this.unitX);
    4053                 }
    4054                 this.keepaspectratio = true;
    4055             } else {
    4056                 this.unitX = w / (bbox[2] - bbox[0]);
    4057                 this.unitY = h / (bbox[1] - bbox[3]);
    4058                 this.keepaspectratio = false;
    4059             }
    4060
    4061             this.moveOrigin(-this.unitX * bbox[0], this.unitY * bbox[1]);
    4062
    4063             return this;
    4064         },
    4065
    4066         /**
    4067          * Get the bounding box of the board.
    4068          * @returns {Array} bounding box [x1,y1,x2,y2] upper left corner, lower right corner
    4069          */
    4070         getBoundingBox: function () {
    4071             var ul = new Coords(Const.COORDS_BY_SCREEN, [0, 0], this),
    4072                 lr = new Coords(Const.COORDS_BY_SCREEN, [this.canvasWidth, this.canvasHeight], this);
    4073
    4074             return [ul.usrCoords[1], ul.usrCoords[2], lr.usrCoords[1], lr.usrCoords[2]];
    4075         },
    4076
    4077         /**
    4078          * Adds an animation. Animations are controlled by the boards, so the boards need to be aware of the
    4079          * animated elements. This function tells the board about new elements to animate.
    4080          * @param {JXG.GeometryElement} element The element which is to be animated.
    4081          * @returns {JXG.Board} Reference to the board
    4082          */
    4083         addAnimation: function (element) {
    4084             var that = this;
    4085
    4086             this.animationObjects[element.id] = element;
    4087
    4088             if (!this.animationIntervalCode) {
    4089                 this.animationIntervalCode = window.setInterval(function () {
    4090                     that.animate();
    4091                 }, element.board.attr.animationdelay);
    4092             }
    4093
    4094             return this;
    4095         },
    4096
    4097         /**
    4098          * Cancels all running animations.
    4099          * @returns {JXG.Board} Reference to the board
    4100          */
    4101         stopAllAnimation: function () {
    4102             var el;
    4103
    4104             for (el in this.animationObjects) {
    4105                 if (this.animationObjects.hasOwnProperty(el) && Type.exists(this.animationObjects[el])) {
    4106                     this.animationObjects[el] = null;
    4107                     delete this.animationObjects[el];
    4108                 }
    4109             }
    4110
    4111             window.clearInterval(this.animationIntervalCode);
    4112             delete this.animationIntervalCode;
    4113
    4114             return this;
    4115         },
    4116
    4117         /**
    4118          * General purpose animation function. This currently only supports moving points from one place to another. This
    4119          * is faster than managing the animation per point, especially if there is more than one animated point at the same time.
    4120          * @returns {JXG.Board} Reference to the board
    4121          */
    4122         animate: function () {
    4123             var props, el, o, newCoords, r, p, c, cbtmp,
    4124                 count = 0,
    4125                 obj = null;
    4126
    4127             for (el in this.animationObjects) {
    4128                 if (this.animationObjects.hasOwnProperty(el) && Type.exists(this.animationObjects[el])) {
    4129                     count += 1;
    4130                     o = this.animationObjects[el];
    4131
    4132                     if (o.animationPath) {
    4133                         if (Type.isFunction(o.animationPath)) {
    4134                             newCoords = o.animationPath(new Date().getTime() - o.animationStart);
    4135                         } else {
    4136                             newCoords = o.animationPath.pop();
    4137                         }
    4138
    4139                         if ((!Type.exists(newCoords)) || (!Type.isArray(newCoords) && isNaN(newCoords))) {
    4140                             delete o.animationPath;
    4141                         } else {
    4142                             o.setPositionDirectly(Const.COORDS_BY_USER, newCoords);
    4143                             o.fullUpdate();
    4144                             obj = o;
    4145                         }
    4146                     }
    4147                     if (o.animationData) {
    4148                         c = 0;
    4149
    4150                         for (r in o.animationData) {
    4151                             if (o.animationData.hasOwnProperty(r)) {
    4152                                 p = o.animationData[r].pop();
    4153
    4154                                 if (!Type.exists(p)) {
    4155                                     delete o.animationData[p];
    4156                                 } else {
    4157                                     c += 1;
    4158                                     props = {};
    4159                                     props[r] = p;
    4160                                     o.setAttribute(props);
    4161                                 }
    4162                             }
    4163                         }
    4164
    4165                         if (c === 0) {
    4166                             delete o.animationData;
    4167                         }
    4168                     }
    4169
    4170                     if (!Type.exists(o.animationData) && !Type.exists(o.animationPath)) {
    4171                         this.animationObjects[el] = null;
    4172                         delete this.animationObjects[el];
    4173
    4174                         if (Type.exists(o.animationCallback)) {
    4175                             cbtmp = o.animationCallback;
    4176                             o.animationCallback = null;
    4177                             cbtmp();
    4178                         }
    4179                     }
    4180                 }
    4181             }
    4182
    4183             if (count === 0) {
    4184                 window.clearInterval(this.animationIntervalCode);
    4185                 delete this.animationIntervalCode;
    4186             } else {
    4187                 this.update(obj);
    4188             }
    4189
    4190             return this;
    4191         },
    4192
    4193         /**
    4194          * Migrate the dependency properties of the point src
    4195          * to the point dest and  delete the point src.
    4196          * For example, a circle around the point src
    4197          * receives the new center dest. The old center src
    4198          * will be deleted.
    4199          * @param {JXG.Point} src Original point which will be deleted
    4200          * @param {JXG.Point} dest New point with the dependencies of src.
    4201          * @param {Boolean} copyName Flag which decides if the name of the src element is copied to the
    4202          *  dest element.
    4203          * @returns {JXG.Board} Reference to the board
    4204          */
    4205         migratePoint: function (src, dest, copyName) {
    4206             var child, childId, prop, found, i, srcLabelId, srcHasLabel = false;
    4207
    4208             src = this.select(src);
    4209             dest = this.select(dest);
    4210
    4211             if (Type.exists(src.label)) {
    4212                 srcLabelId = src.label.id;
    4213                 srcHasLabel = true;
    4214                 this.removeObject(src.label);
    4215             }
    4216
    4217             for (childId in src.childElements) {
    4218                 if (src.childElements.hasOwnProperty(childId)) {
    4219                     child = src.childElements[childId];
    4220                     found = false;
    4221
    4222                     for (prop in child) {
    4223                         if (child.hasOwnProperty(prop)) {
    4224                             if (child[prop] ===  src) {
    4225                                 child[prop] = dest;
    4226                                 found = true;
    4227                             }
    4228                         }
    4229                     }
    4230
    4231                     if (found) {
    4232                         delete src.childElements[childId];
    4233                     }
    4234
    4235                     for (i = 0; i < child.parents.length; i++) {
    4236                         if (child.parents[i] === src.id) {
    4237                             child.parents[i] = dest.id;
    4238                         }
    4239                     }
    4240
    4241                     dest.addChild(child);
    4242                 }
    4243             }
    4244
    4245             // The destination object should receive the name
    4246             // and the label of the originating (src) object
    4247             if (copyName) {
    4248                 if (srcHasLabel) {
    4249                     delete dest.childElements[srcLabelId];
    4250                     delete dest.descendants[srcLabelId];
    4251                 }
    4252
    4253                 if (dest.label) {
    4254                     this.removeObject(dest.label);
    4255                 }
    4256
    4257                 delete this.elementsByName[dest.name];
    4258                 dest.name = src.name;
    4259                 if (srcHasLabel) {
    4260                     dest.createLabel();
    4261                 }
    4262             }
    4263
    4264             this.removeObject(src);
    4265
    4266             if (Type.exists(dest.name) && dest.name !== '') {
    4267                 this.elementsByName[dest.name] = dest;
    4268             }
    4269
    4270             this.fullUpdate();
    4271
    4272             return this;
    4273         },
    4274
    4275         /**
    4276          * Initializes color blindness simulation.
    4277          * @param {String} deficiency Describes the color blindness deficiency which is simulated. Accepted values are 'protanopia', 'deuteranopia', and 'tritanopia'.
    4278          * @returns {JXG.Board} Reference to the board
    4279          */
    4280         emulateColorblindness: function (deficiency) {
    4281             var e, o;
    4282
    4283             if (!Type.exists(deficiency)) {
    4284                 deficiency = 'none';
    4285             }
    4286
    4287             if (this.currentCBDef === deficiency) {
    4288                 return this;
    4289             }
    4290
    4291             for (e in this.objects) {
    4292                 if (this.objects.hasOwnProperty(e)) {
    4293                     o = this.objects[e];
    4294
    4295                     if (deficiency !== 'none') {
    4296                         if (this.currentCBDef === 'none') {
    4297                             // this could be accomplished by JXG.extend, too. But do not use
    4298                             // JXG.deepCopy as this could result in an infinite loop because in
    4299                             // visProp there could be geometry elements which contain the board which
    4300                             // contains all objects which contain board etc.
    4301                             o.visPropOriginal = {
    4302                                 strokecolor: o.visProp.strokecolor,
    4303                                 fillcolor: o.visProp.fillcolor,
    4304                                 highlightstrokecolor: o.visProp.highlightstrokecolor,
    4305                                 highlightfillcolor: o.visProp.highlightfillcolor
    4306                             };
    4307                         }
    4308                         o.setAttribute({
    4309                             strokecolor: Color.rgb2cb(Type.evaluate(o.visPropOriginal.strokecolor), deficiency),
    4310                             fillcolor: Color.rgb2cb(Type.evaluate(o.visPropOriginal.fillcolor), deficiency),
    4311                             highlightstrokecolor: Color.rgb2cb(Type.evaluate(o.visPropOriginal.highlightstrokecolor), deficiency),
    4312                             highlightfillcolor: Color.rgb2cb(Type.evaluate(o.visPropOriginal.highlightfillcolor), deficiency)
    4313                         });
    4314                     } else if (Type.exists(o.visPropOriginal)) {
    4315                         JXG.extend(o.visProp, o.visPropOriginal);
    4316                     }
    4317                 }
    4318             }
    4319             this.currentCBDef = deficiency;
    4320             this.update();
    4321
    4322             return this;
    4323         },
    4324
    4325         /**
    4326          * Select a single or multiple elements at once.
    4327          * @param {String|Object|function} str The name, id or a reference to a JSXGraph element on this board. An object will
    4328          * be used as a filter to return multiple elements at once filtered by the properties of the object.
    4329          * @returns {JXG.GeometryElement|JXG.Composition}
    4330          * @example
    4331          * // select the element with name A
    4332          * board.select('A');
    4333          *
    4334          * // select all elements with strokecolor set to 'red' (but not '#ff0000')
    4335          * board.select({
    4336          *   strokeColor: 'red'
    4337          * });
    4338          *
    4339          * // select all points on or below the x axis and make them black.
    4340          * board.select({
    4341          *   elementClass: JXG.OBJECT_CLASS_POINT,
    4342          *   Y: function (v) {
    4343          *     return v <= 0;
    4344          *   }
    4345          * }).setAttribute({color: 'black'});
    4346          *
    4347          * // select all elements
    4348          * board.select(function (el) {
    4349          *   return true;
    4350          * });
    4351          */
    4352         select: function (str) {
    4353             var flist, olist, i, l,
    4354                 s = str;
    4355
    4356             if (s === null) {
    4357                 return s;
    4358             }
    4359
    4360             // it's a string, most likely an id or a name.
    4361             if (Type.isString(s) && s !== '') {
    4362                 // Search by ID
    4363                 if (Type.exists(this.objects[s])) {
    4364                     s = this.objects[s];
    4365                 // Search by name
    4366                 } else if (Type.exists(this.elementsByName[s])) {
    4367                     s = this.elementsByName[s];
    4368                 // Search by group ID
    4369                 } else if (Type.exists(this.groups[s])) {
    4370                     s = this.groups[s];
    4371                 }
    4372             // it's a function or an object, but not an element
    4373         } else if (Type.isFunction(s) || (Type.isObject(s) && !Type.isFunction(s.setAttribute))) {
    4374
    4375                 flist = Type.filterElements(this.objectsList, s);
    4376
    4377                 olist = {};
    4378                 l = flist.length;
    4379                 for (i = 0; i < l; i++) {
    4380                     olist[flist[i].id] = flist[i];
    4381                 }
    4382                 s = new EComposition(olist);
    4383             // it's an element which has been deleted (and still hangs around, e.g. in an attractor list
    4384             } else if (Type.isObject(s) && Type.exists(s.id) && !Type.exists(this.objects[s.id])) {
    4385                 s = null;
    4386             }
    4387
    4388             return s;
    4389         },
    4390
    4391         /**
    4392          * Checks if the given point is inside the boundingbox.
    4393          * @param {Number|JXG.Coords} x User coordinate or {@link JXG.Coords} object.
    4394          * @param {Number} [y] User coordinate. May be omitted in case <tt>x</tt> is a {@link JXG.Coords} object.
    4395          * @returns {Boolean}
    4396          */
    4397         hasPoint: function (x, y) {
    4398             var px = x,
    4399                 py = y,
    4400                 bbox = this.getBoundingBox();
    4401
    4402             if (Type.exists(x) && Type.isArray(x.usrCoords)) {
    4403                 px = x.usrCoords[1];
    4404                 py = x.usrCoords[2];
    4405             }
    4406
    4407             return !!(Type.isNumber(px) && Type.isNumber(py) &&
    4408                 bbox[0] < px && px < bbox[2] && bbox[1] > py && py > bbox[3]);
    4409         },
    4410
    4411         /**
    4412          * Update CSS transformations of sclaing type. It is used to correct the mouse position
    4413          * in {@link JXG.Board.getMousePosition}.
    4414          * The inverse transformation matrix is updated on each mouseDown and touchStart event.
    4415          *
    4416          * It is up to the user to call this method after an update of the CSS transformation
    4417          * in the DOM.
    4418          */
    4419         updateCSSTransforms: function () {
    4420             var obj = this.containerObj,
    4421                 o = obj,
    4422                 o2 = obj;
    4423
    4424             this.cssTransMat = Env.getCSSTransformMatrix(o);
    4425
    4426             /*
    4427              * In Mozilla and Webkit: offsetParent seems to jump at least to the next iframe,
    4428              * if not to the body. In IE and if we are in an position:absolute environment
    4429              * offsetParent walks up the DOM hierarchy.
    4430              * In order to walk up the DOM hierarchy also in Mozilla and Webkit
    4431              * we need the parentNode steps.
    4432              */
    4433             o = o.offsetParent;
    4434             while (o) {
    4435                 this.cssTransMat = Mat.matMatMult(Env.getCSSTransformMatrix(o), this.cssTransMat);
    4436
    4437                 o2 = o2.parentNode;
    4438                 while (o2 !== o) {
    4439                     this.cssTransMat = Mat.matMatMult(Env.getCSSTransformMatrix(o), this.cssTransMat);
    4440                     o2 = o2.parentNode;
    4441                 }
    4442
    4443                 o = o.offsetParent;
    4444             }
    4445             this.cssTransMat = Mat.inverse(this.cssTransMat);
    4446
    4447             return this;
    4448         },
    4449
    4450         /**
    4451          * Start selection mode. This function can either be triggered from outside or by
    4452          * a down event together with correct key pressing. The default keys are
    4453          * shift+ctrl. But this can be changed in the options.
    4454          *
    4455          * Starting from out side can be realized for example with a button like this:
    4456          * <pre>
    4457          * 	<button onclick="board.startSelectionMode()">Start</button>
    4458          * </pre>
    4459          * @example
    4460          * //
    4461          * // Set a new bounding box from the selection rectangle
    4462          * //
    4463          * var board = JXG.JSXGraph.initBoard('jxgbox', {
    4464          *         boundingBox:[-3,2,3,-2],
    4465          *         keepAspectRatio: false,
    4466          *         axis:true,
    4467          *         selection: {
    4468          *             enabled: true,
    4469          *             needShift: false,
    4470          *             needCtrl: true,
    4471          *             withLines: false,
    4472          *             vertices: {
    4473          *                 visible: false
    4474          *             },
    4475          *             fillColor: '#ffff00',
    4476          *         }
    4477          *      });
    4478          *
    4479          * var f = function f(x) { return Math.cos(x); },
    4480          *     curve = board.create('functiongraph', [f]);
    4481          *
    4482          * board.on('stopselecting', function(){
    4483          *     var box = board.stopSelectionMode(),
    4484          *
    4485          *         // bbox has the coordinates of the selection rectangle.
    4486          *         // Attention: box[i].usrCoords have the form [1, x, y], i.e.
    4487          *         // are homogeneous coordinates.
    4488          *         bbox = box[0].usrCoords.slice(1).concat(box[1].usrCoords.slice(1));
    4489          *
    4490          *         // Set a new bounding box
    4491          *         board.setBoundingBox(bbox, false);
    4492          *  });
    4493          *
    4494          *
    4495          * </pre><div class="jxgbox" id="11eff3a6-8c50-11e5-b01d-901b0e1b8723" style="width: 300px; height: 300px;"></div>
    4496          * <script type="text/javascript">
    4497          *     (function() {
    4498          *         var board = JXG.JSXGraph.initBoard('11eff3a6-8c50-11e5-b01d-901b0e1b8723',
    4499          *             {boundingbox: [-8, 8, 8,-8], axis: true, showcopyright: false, shownavigation: false});
    4500          *     //
    4501          *     // Set a new bounding box from the selection rectangle
    4502          *     //
    4503          *     var board = JXG.JSXGraph.initBoard('11eff3a6-8c50-11e5-b01d-901b0e1b8723', {
    4504          *             boundingBox:[-3,2,3,-2],
    4505          *             keepAspectRatio: false,
    4506          *             axis:true,
    4507          *             selection: {
    4508          *                 enabled: true,
    4509          *                 needShift: false,
    4510          *                 needCtrl: true,
    4511          *                 withLines: false,
    4512          *                 vertices: {
    4513          *                     visible: false
    4514          *                 },
    4515          *                 fillColor: '#ffff00',
    4516          *             }
    4517          *        });
    4518          *
    4519          *     var f = function f(x) { return Math.cos(x); },
    4520          *         curve = board.create('functiongraph', [f]);
    4521          *
    4522          *     board.on('stopselecting', function(){
    4523          *         var box = board.stopSelectionMode(),
    4524          *
    4525          *             // bbox has the coordinates of the selection rectangle.
    4526          *             // Attention: box[i].usrCoords have the form [1, x, y], i.e.
    4527          *             // are homogeneous coordinates.
    4528          *             bbox = box[0].usrCoords.slice(1).concat(box[1].usrCoords.slice(1));
    4529          *
    4530          *             // Set a new bounding box
    4531          *             board.setBoundingBox(bbox, false);
    4532          *      });
    4533          *     })();
    4534          *
    4535          * </script><pre>
    4536          *
    4537          */
    4538         startSelectionMode: function () {
    4539             this.selectingMode = true;
    4540             this.selectionPolygon.setAttribute({visible: true});
    4541             this.selectingBox = [[0, 0], [0, 0]];
    4542             this._setSelectionPolygonFromBox();
    4543             this.selectionPolygon.fullUpdate();
    4544         },
    4545
    4546         /**
    4547          * Finalize the selection: disable selection mode and return the coordinates
    4548          * of the selection rectangle.
    4549          * @returns {Array} Coordinates of the selection rectangle. The array
    4550          * contains two {@link JXG.Coords} objects. One the upper left corner and
    4551          * the second for the lower right corner.
    4552          */
    4553         stopSelectionMode: function () {
    4554             this.selectingMode = false;
    4555             this.selectionPolygon.setAttribute({visible: false});
    4556             return [this.selectionPolygon.vertices[0].coords, this.selectionPolygon.vertices[2].coords];
    4557         },
    4558
    4559         /**
    4560          * Start the selection of a region.
    4561          * @private
    4562          * @param  {Array} pos Screen coordiates of the upper left corner of the
    4563          * selection rectangle.
    4564          */
    4565         _startSelecting: function (pos) {
    4566             this.isSelecting = true;
    4567             this.selectingBox = [ [pos[0], pos[1]], [pos[0], pos[1]] ];
    4568             this._setSelectionPolygonFromBox();
    4569         },
    4570
    4571         /**
    4572          * Update the selection rectangle during a move event.
    4573          * @private
    4574          * @param  {Array} pos Screen coordiates of the move event
    4575          */
    4576         _moveSelecting: function (pos) {
    4577             if (this.isSelecting) {
    4578                 this.selectingBox[1] = [pos[0], pos[1]];
    4579                 this._setSelectionPolygonFromBox();
    4580                 this.selectionPolygon.fullUpdate();
    4581             }
    4582         },
    4583
    4584         /**
    4585          * Update the selection rectangle during an up event. Stop selection.
    4586          * @private
    4587          * @param  {Object} evt Event object
    4588          */
    4589         _stopSelecting:  function (evt) {
    4590             var pos = this.getMousePosition(evt);
    4591
    4592             this.isSelecting = false;
    4593             this.selectingBox[1] = [pos[0], pos[1]];
    4594             this._setSelectionPolygonFromBox();
    4595         },
    4596
    4597         /**
    4598          * Update the Selection rectangle.
    4599          * @private
    4600          */
    4601         _setSelectionPolygonFromBox: function () {
    4602                var A = this.selectingBox[0],
    4603                 B = this.selectingBox[1];
    4604
    4605                this.selectionPolygon.vertices[0].setPositionDirectly(JXG.COORDS_BY_SCREEN, [A[0], A[1]]);
    4606                this.selectionPolygon.vertices[1].setPositionDirectly(JXG.COORDS_BY_SCREEN, [A[0], B[1]]);
    4607                this.selectionPolygon.vertices[2].setPositionDirectly(JXG.COORDS_BY_SCREEN, [B[0], B[1]]);
    4608                this.selectionPolygon.vertices[3].setPositionDirectly(JXG.COORDS_BY_SCREEN, [B[0], A[1]]);
    4609         },
    4610
    4611         /**
    4612          * Test if a down event should start a selection. Test if the
    4613          * required keys are pressed. If yes, {@link JXG.Board.startSelectionMode} is called.
    4614          * @param  {Object} evt Event object
    4615          */
    4616         _testForSelection: function (evt) {
    4617             if (this._isRequiredKeyPressed(evt, 'selection')) {
    4618                 if (!Type.exists(this.selectionPolygon)) {
    4619                     this._createSelectionPolygon(this.attr);
    4620                 }
    4621                 this.startSelectionMode();
    4622             }
    4623         },
    4624
    4625         /**
    4626          * Create the internal selection polygon, which will be available as board.selectionPolygon.
    4627          * @private
    4628          * @param  {Object} attr board attributes, e.g. the subobject board.attr.
    4629          * @returns {Object} pointer to the board to enable chaining.
    4630          */
    4631         _createSelectionPolygon: function(attr) {
    4632             var selectionattr;
    4633
    4634             if (!Type.exists(this.selectionPolygon)) {
    4635                 selectionattr = Type.copyAttributes(attr, Options, 'board', 'selection');
    4636                 if (selectionattr.enabled === true) {
    4637                     this.selectionPolygon = this.create('polygon', [[0, 0], [0, 0], [0, 0], [0, 0]], selectionattr);
    4638                 }
    4639             }
    4640
    4641             return this;
    4642         },
    4643
    4644         /* **************************
    4645          *     EVENT DEFINITION
    4646          * for documentation purposes
    4647          * ************************** */
    4648
    4649         //region Event handler documentation
    4650
    4651         /**
    4652          * @event
    4653          * @description Whenever the user starts to touch or click the board.
    4654          * @name JXG.Board#down
    4655          * @param {Event} e The browser's event object.
    4656          */
    4657         __evt__down: function (e) { },
    4658
    4659         /**
    4660          * @event
    4661          * @description Whenever the user starts to click on the board.
    4662          * @name JXG.Board#mousedown
    4663          * @param {Event} e The browser's event object.
    4664          */
    4665         __evt__mousedown: function (e) { },
    4666
    4667         /**
    4668          * @event
    4669          * @description Whenever the user starts to click on the board with a
    4670          * device sending pointer events.
    4671          * @name JXG.Board#pointerdown
    4672          * @param {Event} e The browser's event object.
    4673          */
    4674         __evt__pointerdown: function (e) { },
    4675
    4676         /**
    4677          * @event
    4678          * @description Whenever the user starts to touch the board.
    4679          * @name JXG.Board#touchstart
    4680          * @param {Event} e The browser's event object.
    4681          */
    4682         __evt__touchstart: function (e) { },
    4683
    4684         /**
    4685          * @event
    4686          * @description Whenever the user stops to touch or click the board.
    4687          * @name JXG.Board#up
    4688          * @param {Event} e The browser's event object.
    4689          */
    4690         __evt__up: function (e) { },
    4691
    4692         /**
    4693          * @event
    4694          * @description Whenever the user releases the mousebutton over the board.
    4695          * @name JXG.Board#mouseup
    4696          * @param {Event} e The browser's event object.
    4697          */
    4698         __evt__mouseup: function (e) { },
    4699
    4700         /**
    4701          * @event
    4702          * @description Whenever the user releases the mousebutton over the board with a
    4703          * device sending pointer events.
    4704          * @name JXG.Board#pointerup
    4705          * @param {Event} e The browser's event object.
    4706          */
    4707         __evt__pointerup: function (e) { },
    4708
    4709         /**
    4710          * @event
    4711          * @description Whenever the user stops touching the board.
    4712          * @name JXG.Board#touchend
    4713          * @param {Event} e The browser's event object.
    4714          */
    4715         __evt__touchend: function (e) { },
    4716
    4717         /**
    4718          * @event
    4719          * @description This event is fired whenever the user is moving the finger or mouse pointer over the board.
    4720          * @name JXG.Board#move
    4721          * @param {Event} e The browser's event object.
    4722          * @param {Number} mode The mode the board currently is in
    4723          * @see {JXG.Board#mode}
    4724          */
    4725         __evt__move: function (e, mode) { },
    4726
    4727         /**
    4728          * @event
    4729          * @description This event is fired whenever the user is moving the mouse over the board.
    4730          * @name JXG.Board#mousemove
    4731          * @param {Event} e The browser's event object.
    4732          * @param {Number} mode The mode the board currently is in
    4733          * @see {JXG.Board#mode}
    4734          */
    4735         __evt__mousemove: function (e, mode) { },
    4736
    4737         /**
    4738          * @event
    4739          * @description This event is fired whenever the user is moving the mouse over the board  with a
    4740          * device sending pointer events.
    4741          * @name JXG.Board#pointermove
    4742          * @param {Event} e The browser's event object.
    4743          * @param {Number} mode The mode the board currently is in
    4744          * @see {JXG.Board#mode}
    4745          */
    4746         __evt__pointermove: function (e, mode) { },
    4747
    4748         /**
    4749          * @event
    4750          * @description This event is fired whenever the user is moving the finger over the board.
    4751          * @name JXG.Board#touchmove
    4752          * @param {Event} e The browser's event object.
    4753          * @param {Number} mode The mode the board currently is in
    4754          * @see {JXG.Board#mode}
    4755          */
    4756         __evt__touchmove: function (e, mode) { },
    4757
    4758         /**
    4759          * @event
    4760          * @description Whenever an element is highlighted this event is fired.
    4761          * @name JXG.Board#hit
    4762          * @param {Event} e The browser's event object.
    4763          * @param {JXG.GeometryElement} el The hit element.
    4764          * @param target
    4765          */
    4766         __evt__hit: function (e, el, target) { },
    4767
    4768         /**
    4769          * @event
    4770          * @description Whenever an element is highlighted this event is fired.
    4771          * @name JXG.Board#mousehit
    4772          * @param {Event} e The browser's event object.
    4773          * @param {JXG.GeometryElement} el The hit element.
    4774          * @param target
    4775          */
    4776         __evt__mousehit: function (e, el, target) { },
    4777
    4778         /**
    4779          * @event
    4780          * @description This board is updated.
    4781          * @name JXG.Board#update
    4782          */
    4783         __evt__update: function () { },
    4784
    4785         /**
    4786          * @event
    4787          * @description The bounding box of the board has changed.
    4788          * @name JXG.Board#boundingbox
    4789          */
    4790         __evt__boundingbox: function () { },
    4791
    4792         /**
    4793          * @event
    4794          * @description Select a region is started during a down event or by calling
    4795          * {@link JXG.Board.startSelectionMode}
    4796          * @name JXG.Board#startselecting
    4797          */
    4798          __evt__startselecting: function () { },
    4799
    4800          /**
    4801          * @event
    4802          * @description Select a region is started during a down event
    4803          * from a device sending mouse events or by calling
    4804          * {@link JXG.Board.startSelectionMode}.
    4805          * @name JXG.Board#mousestartselecting
    4806          */
    4807          __evt__mousestartselecting: function () { },
    4808
    4809          /**
    4810          * @event
    4811          * @description Select a region is started during a down event
    4812          * from a device sending pointer events or by calling
    4813          * {@link JXG.Board.startSelectionMode}.
    4814          * @name JXG.Board#pointerstartselecting
    4815          */
    4816          __evt__pointerstartselecting: function () { },
    4817
    4818          /**
    4819          * @event
    4820          * @description Select a region is started during a down event
    4821          * from a device sending touch events or by calling
    4822          * {@link JXG.Board.startSelectionMode}.
    4823          * @name JXG.Board#touchstartselecting
    4824          */
    4825          __evt__touchstartselecting: function () { },
    4826
    4827          /**
    4828           * @event
    4829           * @description Selection of a region is stopped during an up event.
    4830           * @name JXG.Board#stopselecting
    4831           */
    4832          __evt__stopselecting: function () { },
    4833
    4834          /**
    4835          * @event
    4836          * @description Selection of a region is stopped during an up event
    4837          * from a device sending mouse events.
    4838          * @name JXG.Board#mousestopselecting
    4839          */
    4840          __evt__mousestopselecting: function () { },
    4841
    4842          /**
    4843          * @event
    4844          * @description Selection of a region is stopped during an up event
    4845          * from a device sending pointer events.
    4846          * @name JXG.Board#pointerstopselecting
    4847          */
    4848          __evt__pointerstopselecting: function () { },
    4849
    4850          /**
    4851          * @event
    4852          * @description Selection of a region is stopped during an up event
    4853          * from a device sending touch events.
    4854          * @name JXG.Board#touchstopselecting
    4855          */
    4856          __evt__touchstopselecting: function () { },
    4857
    4858          /**
    4859          * @event
    4860          * @description A move event while selecting of a region is active.
    4861          * @name JXG.Board#moveselecting
    4862          */
    4863          __evt__moveselecting: function () { },
    4864
    4865          /**
    4866          * @event
    4867          * @description A move event while selecting of a region is active
    4868          * from a device sending mouse events.
    4869          * @name JXG.Board#mousemoveselecting
    4870          */
    4871          __evt__mousemoveselecting: function () { },
    4872
    4873          /**
    4874          * @event
    4875          * @description Select a region is started during a down event
    4876          * from a device sending mouse events.
    4877          * @name JXG.Board#pointermoveselecting
    4878          */
    4879          __evt__pointermoveselecting: function () { },
    4880
    4881          /**
    4882          * @event
    4883          * @description Select a region is started during a down event
    4884          * from a device sending touch events.
    4885          * @name JXG.Board#touchmoveselecting
    4886          */
    4887          __evt__touchmoveselecting: function () { },
    4888
    4889         /**
    4890          * @ignore
    4891          */
    4892         __evt: function () {},
    4893
    4894         //endregion
    4895
    4896         /**
    4897          * Function to animate a curve rolling on another curve.
    4898          * @param {Curve} c1 JSXGraph curve building the floor where c2 rolls
    4899          * @param {Curve} c2 JSXGraph curve which rolls on c1.
    4900          * @param {number} start_c1 The parameter t such that c1(t) touches c2. This is the start position of the
    4901          *                          rolling process
    4902          * @param {Number} stepsize Increase in t in each step for the curve c1
    4903          * @param {Number} direction
    4904          * @param {Number} time Delay time for setInterval()
    4905          * @param {Array} pointlist Array of points which are rolled in each step. This list should contain
    4906          *      all points which define c2 and gliders on c2.
    4907          *
    4908          * @example
    4909          *
    4910          * // Line which will be the floor to roll upon.
    4911          * var line = brd.create('curve', [function (t) { return t;}, function (t){ return 1;}], {strokeWidth:6});
    4912          * // Center of the rolling circle
    4913          * var C = brd.create('point',[0,2],{name:'C'});
    4914          * // Starting point of the rolling circle
    4915          * var P = brd.create('point',[0,1],{name:'P', trace:true});
    4916          * // Circle defined as a curve. The circle "starts" at P, i.e. circle(0) = P
    4917          * var circle = brd.create('curve',[
    4918          *           function (t){var d = P.Dist(C),
    4919          *                           beta = JXG.Math.Geometry.rad([C.X()+1,C.Y()],C,P);
    4920          *                       t += beta;
    4921          *                       return C.X()+d*Math.cos(t);
    4922          *           },
    4923          *           function (t){var d = P.Dist(C),
    4924          *                           beta = JXG.Math.Geometry.rad([C.X()+1,C.Y()],C,P);
    4925          *                       t += beta;
    4926          *                       return C.Y()+d*Math.sin(t);
    4927          *           },
    4928          *           0,2*Math.PI],
    4929          *           {strokeWidth:6, strokeColor:'green'});
    4930          *
    4931          * // Point on circle
    4932          * var B = brd.create('glider',[0,2,circle],{name:'B', color:'blue',trace:false});
    4933          * var roll = brd.createRoulette(line, circle, 0, Math.PI/20, 1, 100, [C,P,B]);
    4934          * roll.start() // Start the rolling, to be stopped by roll.stop()
    4935          *
    4936          * </pre><div class="jxgbox" id="e5e1b53c-a036-4a46-9e35-190d196beca5" style="width: 300px; height: 300px;"></div>
    4937          * <script type="text/javascript">
    4938          * var brd = JXG.JSXGraph.initBoard('e5e1b53c-a036-4a46-9e35-190d196beca5', {boundingbox: [-5, 5, 5, -5], axis: true, showcopyright:false, shownavigation: false});
    4939          * // Line which will be the floor to roll upon.
    4940          * var line = brd.create('curve', [function (t) { return t;}, function (t){ return 1;}], {strokeWidth:6});
    4941          * // Center of the rolling circle
    4942          * var C = brd.create('point',[0,2],{name:'C'});
    4943          * // Starting point of the rolling circle
    4944          * var P = brd.create('point',[0,1],{name:'P', trace:true});
    4945          * // Circle defined as a curve. The circle "starts" at P, i.e. circle(0) = P
    4946          * var circle = brd.create('curve',[
    4947          *           function (t){var d = P.Dist(C),
    4948          *                           beta = JXG.Math.Geometry.rad([C.X()+1,C.Y()],C,P);
    4949          *                       t += beta;
    4950          *                       return C.X()+d*Math.cos(t);
    4951          *           },
    4952          *           function (t){var d = P.Dist(C),
    4953          *                           beta = JXG.Math.Geometry.rad([C.X()+1,C.Y()],C,P);
    4954          *                       t += beta;
    4955          *                       return C.Y()+d*Math.sin(t);
    4956          *           },
    4957          *           0,2*Math.PI],
    4958          *           {strokeWidth:6, strokeColor:'green'});
    4959          *
    4960          * // Point on circle
    4961          * var B = brd.create('glider',[0,2,circle],{name:'B', color:'blue',trace:false});
    4962          * var roll = brd.createRoulette(line, circle, 0, Math.PI/20, 1, 100, [C,P,B]);
    4963          * roll.start() // Start the rolling, to be stopped by roll.stop()
    4964          * </script><pre>
    4965          */
    4966         createRoulette: function (c1, c2, start_c1, stepsize, direction, time, pointlist) {
    4967             var brd = this,
    4968                 Roulette = function () {
    4969                     var alpha = 0, Tx = 0, Ty = 0,
    4970                         t1 = start_c1,
    4971                         t2 = Numerics.root(
    4972                             function (t) {
    4973                                 var c1x = c1.X(t1),
    4974                                     c1y = c1.Y(t1),
    4975                                     c2x = c2.X(t),
    4976                                     c2y = c2.Y(t);
    4977
    4978                                 return (c1x - c2x) * (c1x - c2x) + (c1y - c2y) * (c1y - c2y);
    4979                             },
    4980                             [0, Math.PI * 2]
    4981                         ),
    4982                         t1_new = 0.0, t2_new = 0.0,
    4983                         c1dist,
    4984
    4985                         rotation = brd.create('transform', [
    4986                             function () {
    4987                                 return alpha;
    4988                             }
    4989                         ], {type: 'rotate'}),
    4990
    4991                         rotationLocal = brd.create('transform', [
    4992                             function () {
    4993                                 return alpha;
    4994                             },
    4995                             function () {
    4996                                 return c1.X(t1);
    4997                             },
    4998                             function () {
    4999                                 return c1.Y(t1);
    5000                             }
    5001                         ], {type: 'rotate'}),
    5002
    5003                         translate = brd.create('transform', [
    5004                             function () {
    5005                                 return Tx;
    5006                             },
    5007                             function () {
    5008                                 return Ty;
    5009                             }
    5010                         ], {type: 'translate'}),
    5011
    5012                         // arc length via Simpson's rule.
    5013                         arclen = function (c, a, b) {
    5014                             var cpxa = Numerics.D(c.X)(a),
    5015                                 cpya = Numerics.D(c.Y)(a),
    5016                                 cpxb = Numerics.D(c.X)(b),
    5017                                 cpyb = Numerics.D(c.Y)(b),
    5018                                 cpxab = Numerics.D(c.X)((a + b) * 0.5),
    5019                                 cpyab = Numerics.D(c.Y)((a + b) * 0.5),
    5020
    5021                                 fa = Math.sqrt(cpxa * cpxa + cpya * cpya),
    5022                                 fb = Math.sqrt(cpxb * cpxb + cpyb * cpyb),
    5023                                 fab = Math.sqrt(cpxab * cpxab + cpyab * cpyab);
    5024
    5025                             return (fa + 4 * fab + fb) * (b - a) / 6;
    5026                         },
    5027
    5028                         exactDist = function (t) {
    5029                             return c1dist - arclen(c2, t2, t);
    5030                         },
    5031
    5032                         beta = Math.PI / 18,
    5033                         beta9 = beta * 9,
    5034                         interval = null;
    5035
    5036                     this.rolling = function () {
    5037                         var h, g, hp, gp, z;
    5038
    5039                         t1_new = t1 + direction * stepsize;
    5040
    5041                         // arc length between c1(t1) and c1(t1_new)
    5042                         c1dist = arclen(c1, t1, t1_new);
    5043
    5044                         // find t2_new such that arc length between c2(t2) and c1(t2_new) equals c1dist.
    5045                         t2_new = Numerics.root(exactDist, t2);
    5046
    5047                         // c1(t) as complex number
    5048                         h = new Complex(c1.X(t1_new), c1.Y(t1_new));
    5049
    5050                         // c2(t) as complex number
    5051                         g = new Complex(c2.X(t2_new), c2.Y(t2_new));
    5052
    5053                         hp = new Complex(Numerics.D(c1.X)(t1_new), Numerics.D(c1.Y)(t1_new));
    5054                         gp = new Complex(Numerics.D(c2.X)(t2_new), Numerics.D(c2.Y)(t2_new));
    5055
    5056                         // z is angle between the tangents of c1 at t1_new, and c2 at t2_new
    5057                         z = Complex.C.div(hp, gp);
    5058
    5059                         alpha = Math.atan2(z.imaginary, z.real);
    5060                         // Normalizing the quotient
    5061                         z.div(Complex.C.abs(z));
    5062                         z.mult(g);
    5063                         Tx = h.real - z.real;
    5064
    5065                         // T = h(t1_new)-g(t2_new)*h'(t1_new)/g'(t2_new);
    5066                         Ty = h.imaginary - z.imaginary;
    5067
    5068                         // -(10-90) degrees: make corners roll smoothly
    5069                         if (alpha < -beta && alpha > -beta9) {
    5070                             alpha = -beta;
    5071                             rotationLocal.applyOnce(pointlist);
    5072                         } else if (alpha > beta && alpha < beta9) {
    5073                             alpha = beta;
    5074                             rotationLocal.applyOnce(pointlist);
    5075                         } else {
    5076                             rotation.applyOnce(pointlist);
    5077                             translate.applyOnce(pointlist);
    5078                             t1 = t1_new;
    5079                             t2 = t2_new;
    5080                         }
    5081                         brd.update();
    5082                     };
    5083
    5084                     this.start = function () {
    5085                         if (time > 0) {
    5086                             interval = window.setInterval(this.rolling, time);
    5087                         }
    5088                         return this;
    5089                     };
    5090
    5091                     this.stop = function () {
    5092                         window.clearInterval(interval);
    5093                         return this;
    5094                     };
    5095                     return this;
    5096                 };
    5097             return new Roulette();
    5098         }
    5099     });
    5100
    5101     return JXG.Board;
    5102 });
    5103