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