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

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


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