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