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