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