1 /*
  2     Copyright 2008-2011
  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: you can redistribute it and/or modify
 13     it under the terms of the GNU Lesser General Public License as published by
 14     the Free Software Foundation, either version 3 of the License, or
 15     (at your option) any later version.
 16 
 17     JSXGraph is distributed in the hope that it will be useful,
 18     but WITHOUT ANY WARRANTY; without even the implied warranty of
 19     MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 20     GNU Lesser General Public License for more details.
 21 
 22     You should have received a copy of the GNU Lesser General Public License
 23     along with JSXGraph.  If not, see <http://www.gnu.org/licenses/>.
 24 */
 25 
 26 /**
 27  * @fileoverview The JXG.Board class is defined in this file. JXG.Board controls all properties and methods
 28  * used to manage a geonext board like managing geometric elements, managing mouse and touch events, etc.
 29  * @author graphjs
 30  * @version 0.1
 31  */
 32 
 33 /**
 34  * Constructs a new Board object.
 35  * @class JXG.Board controls all properties and methods used to manage a geonext board like managing geometric
 36  * elements, managing mouse and touch events, etc. You probably don't want to use this constructor directly.
 37  * Please use {@link JXG.JSXGraph#initBoard} to initialize a board.
 38  * @constructor
 39  * @param {String} container The id or reference of the HTML DOM element the board is drawn in. This is usually a HTML div.
 40  * @param {JXG.AbstractRenderer} renderer The reference of a renderer.
 41  * @param {String} id Unique identifier for the board, may be an empty string or null or even undefined.
 42  * @param {JXG.Coords} origin The coordinates where the origin is placed, in user coordinates.
 43  * @param {Number} zoomX Zoom factor in x-axis direction
 44  * @param {Number} zoomY Zoom factor in y-axis direction
 45  * @param {Number} unitX Units in x-axis direction
 46  * @param {Number} unitY Units in y-axis direction
 47  * @param {Number} canvasWidth  The width of canvas
 48  * @param {Number} canvasHeight The height of canvas
 49  * @param {Boolean} showCopyright Display the copyright text
 50  */
 51 JXG.Board = function (container, renderer, id, origin, zoomX, zoomY, unitX, unitY, canvasWidth, canvasHeight, showCopyright) {
 52     /**
 53      * Board is in no special mode, objects are highlighted on mouse over and objects may be
 54      * clicked to start drag&drop.
 55      * @type Number
 56      * @constant
 57      */
 58     this.BOARD_MODE_NONE = 0x0000;
 59 
 60     /**
 61      * Board is in drag mode, objects aren't highlighted on mouse over and the object referenced in
 62      * {JXG.Board#mouse} is updated on mouse movement.
 63      * @type Number
 64      * @constant
 65      * @see JXG.Board#drag_obj
 66      */
 67     this.BOARD_MODE_DRAG = 0x0001;
 68 
 69     /**
 70      * In this mode a mouse move changes the origin's screen coordinates.
 71      * @type Number
 72      * @constant
 73      */
 74     this.BOARD_MODE_MOVE_ORIGIN = 0x0002;
 75 
 76     /**
 77      /* Update is made with low quality, e.g. graphs are evaluated at a lesser amount of points.
 78      * @type Number
 79      * @constant
 80      * @see JXG.Board#updateQuality
 81      */
 82     this.BOARD_QUALITY_LOW = 0x1;
 83 
 84     /**
 85      * Update is made with high quality, e.g. graphs are evaluated at much more points.
 86      * @type Number
 87      * @constant
 88      * @see JXG.Board#updateQuality
 89      */
 90     this.BOARD_QUALITY_HIGH = 0x2;
 91 
 92     /**
 93      * Update is made with high quality, e.g. graphs are evaluated at much more points.
 94      * @type Number
 95      * @constant
 96      * @see JXG.Board#updateQuality
 97      */
 98     this.BOARD_MODE_ZOOM = 0x0011;
 99 
100     // TODO: Do we still need the CONSTRUCTIOIN_TYPE_* properties?!?
101     // BEGIN CONSTRUCTION_TYPE_* stuff
102 
103     /**
104      * Board is in construction mode, objects are highlighted on mouse over and the behaviour of the board
105      * is determined by the construction type stored in the field constructionType.
106      * @type Number
107      * @constant
108      */
109     this.BOARD_MODE_CONSTRUCT = 0x0010;
110 
111     /**
112      * When the board is in construction mode this construction type says we want to construct a point.
113      * @type Number
114      * @constant
115      */
116     this.CONSTRUCTION_TYPE_POINT         = 0x43545054;       // CTPT
117     /**
118      * When the board is in construction mode this construction type says we want to construct a circle.
119      * @type Number
120      * @constant
121      */
122     this.CONSTRUCTION_TYPE_CIRCLE        = 0x4354434C;       // CTCL
123     /**
124      * When the board is in construction mode this construction type says we want to construct a line.
125      * @type int
126      * @private
127      * @final
128      */
129     this.CONSTRUCTION_TYPE_LINE          = 0x43544C4E;       // CTLN
130     /**
131      * When the board is in construction mode this construction type says we want to construct a glider.
132      * @type int
133      * @private
134      * @final
135      */
136     this.CONSTRUCTION_TYPE_GLIDER        = 0x43544744;       // CTSD
137     /**
138      * When the board is in construction mode this construction type says we want to construct a midpoint.
139      * @type int
140      * @private
141      * @final
142      */
143     this.CONSTRUCTION_TYPE_MIDPOINT      = 0x43544D50;       // CTMP
144     /**
145      * When the board is in construction mode this construction type says we want to construct a perpendicular.
146      * @type int
147      * @private
148      * @final
149      */
150     this.CONSTRUCTION_TYPE_PERPENDICULAR = 0x43545044;       // CTPD
151     /**
152      * When the board is in construction mode this construction type says we want to construct a parallel.
153      * @type int
154      * @private
155      * @final
156      */
157     this.CONSTRUCTION_TYPE_PARALLEL      = 0x4354504C;       // CTPL
158     /**
159      * When the board is in construction mode this construction type says we want to construct a intersection.
160      * @type int
161      * @private
162      * @final
163      */
164     this.CONSTRUCTION_TYPE_INTERSECTION  = 0x43544953;       // CTIS
165     // END CONSTRUCTION_TYPE_* stuff
166 
167     /**
168      * The html-id of the html element containing the board.
169      * @type String
170      */
171     this.container = container;
172 
173     /**
174      * Pointer to the html element containing the board.
175      * @type Object
176      */
177     this.containerObj = document.getElementById(this.container);
178     if (this.containerObj == null) {
179         throw new Error("\nJSXGraph: HTML container element '" + (container) + "' not found.");
180     }
181 
182     /**
183      * A reference to this boards renderer.
184      * @type JXG.AbstractRenderer
185      */
186     this.renderer = renderer;
187 
188     /**
189      * Grids keeps track of all grids attached to this board.
190      */
191     this.grids = [];
192 
193     /**
194      * Some standard options
195      * @type JXG.Options
196      */
197     this.options = JXG.deepCopy(JXG.Options);
198 
199     /**
200      * Dimension of the board.
201      * @default 2
202      * @type Number
203      */
204     this.dimension = 2;
205 
206     this.jc = new JXG.JessieCode();
207     this.jc.board = this;
208 
209     /**
210      * Coordinates of the boards origin. This a object with the two properties
211      * usrCoords and scrCoords. usrCoords always equals [1, 0, 0] and scrCoords
212      * stores the boards origin in homogeneous screen coordinates.
213      * @type Object
214      */
215     this.origin = {};
216     this.origin.usrCoords = [1, 0, 0];
217     this.origin.scrCoords = [1, origin[0], origin[1]];
218 
219     /**
220      * Zoom factor in X direction. It only stores the zoom factor to be able
221      * to get back to 100% in zoom100().
222      * @type Number
223      */
224     this.zoomX = zoomX;
225 
226     /**
227      * Zoom factor in Y direction. It only stores the zoom factor to be able
228      * to get back to 100% in zoom100().
229      * @type Number
230      */
231     this.zoomY = zoomY;
232 
233     /**
234      * The number of pixels which represent one unit in user-coordinates in x direction.
235      * @type Number
236      */
237     this.unitX = unitX*this.zoomX;
238 
239     /**
240      * The number of pixels which represent one unit in user-coordinates in y direction.
241      * @type Number
242      */
243     this.unitY = unitY*this.zoomY;
244 
245     /**
246      * Canvas width.
247      * @type Number
248      */
249     this.canvasWidth = canvasWidth;
250 
251     /**
252      * Canvas Height
253      * @type Number
254      */
255     this.canvasHeight = canvasHeight;
256 
257     // If the given id is not valid, generate an unique id
258     if (JXG.exists(id) && id !== '' && !JXG.exists(document.getElementById(id))) {
259         this.id = id;
260     } else {
261         this.id = this.generateId();
262     }
263 
264     /**
265      * An array containing all hook functions.
266      * @type Array
267      * @see JXG.Board#addHook
268      * @see JXG.Board#removeHook
269      * @see JXG.Board#updateHooks
270      */
271     this.hooks = [];
272 
273     /**
274      * An array containing all other boards that are updated after this board has been updated.
275      * @type Array
276      * @see JXG.Board#addChild
277      * @see JXG.Board#removeChild
278      */
279     this.dependentBoards = [];
280 
281     /**
282      * During the update process this is set to false to prevent an endless loop.
283      * @default false
284      * @type Boolean
285      */
286     this.inUpdate = false;
287 
288     /**
289      * 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.
290      * @type Object
291      */
292     this.objects = {};
293 
294     /**
295      * An associative array containing all groups belonging to the board. Key is the id of the group and value is a reference to the object.
296      * @type Object
297      */
298     this.groups = {};
299 
300     /**
301      * Stores all the objects that are currently running an animation.
302      * @type Object
303      */
304     this.animationObjects = {};
305 
306     /**
307      * An associative array containing all highlighted elements belonging to the board.
308      * @type Object
309      */
310     this.highlightedObjects = {};
311 
312     /**
313      * Number of objects ever created on this board. This includes every object, even invisible and deleted ones.
314      * @type Number
315      */
316     this.numObjects = 0;
317 
318     /**
319      * An associative array to store the objects of the board by name. the name of the object is the key and value is a reference to the object.
320      * @type Object
321      */
322     this.elementsByName = {};
323 
324     /**
325      * The board mode the board is currently in. Possible values are
326      * <ul>
327      * <li>JXG.Board.BOARD_MODE_NONE</li>
328      * <li>JXG.Board.BOARD_MODE_DRAG</li>
329      * <li>JXG.Board.BOARD_MODE_CONSTRUCT</li>
330      * <li>JXG.Board.BOARD_MODE_MOVE_ORIGIN</li>
331      * </ul>
332      * @type Number
333      */
334     this.mode = this.BOARD_MODE_NONE;
335 
336     /**
337      * The update quality of the board. In most cases this is set to {@link JXG.Board#BOARD_QUALITY_HIGH}.
338      * If {@link JXG.Board#mode} equals {@link JXG.Board#BOARD_MODE_DRAG} this is set to
339      * {@link JXG.Board#BOARD_QUALITY_LOW} to speed up the update process by e.g. reducing the number of
340      * evaluation points when plotting functions. Possible values are
341      * <ul>
342      * <li>BOARD_QUALITY_LOW</li>
343      * <li>BOARD_QUALITY_HIGH</li>
344      * </ul>
345      * @type Number
346      * @see JXG.Board#mode
347      */
348     this.updateQuality = this.BOARD_QUALITY_HIGH;
349 
350    /**
351      * If true updates are skipped.
352      * @type Boolean
353      */
354     this.isSuspendedRedraw = false;
355 
356     this.calculateSnapSizes();
357 
358     /**
359      * The distance from the mouse to the dragged object in x direction when the user clicked the mouse button.
360      * @type Number
361      * @see JXG.Board#drag_dy
362      * @see JXG.Board#drag_obj
363      */
364     this.drag_dx = 0;
365 
366     /**
367      * The distance from the mouse to the dragged object in y direction when the user clicked the mouse button.
368      * @type Number
369      * @see JXG.Board#drag_dx
370      * @see JXG.Board#drag_obj
371      */
372     this.drag_dy = 0;
373 
374     /**
375      * References to the object that is dragged with the mouse on the board.
376      * @type {@link JXG.GeometryElement}.
377      * @see {JXG.Board#touches}
378      */
379     this.mouse = null;
380 
381     /**
382      * Keeps track on touched elements, like {@link JXG.Board#mouse} does for mouse events.
383      * @type Array
384      * @see {JXG.Board#mouse}
385      */
386     this.touches = [];
387 
388     /**
389      * A string containing the XML text of the construction. This is set in {@link JXG.FileReader#parseString}.
390      * Only useful if a construction is read from a GEONExT-, Intergeo-, Geogebra-, or Cinderella-File.
391      * @type String
392      */
393     this.xmlString = '';
394 
395     /**
396      * Cached ressult of getCoordsTopLeftCorner for touch/mouseMove-Events to save some DOM operations.
397      * @type Array
398      */
399     this.cPos = [];
400 
401     /**
402      * Contains the last time (epoch, msec) since the last touchMove event which was not thrown away or since
403      * touchStart because Android's Webkit browser fires too much of them.
404      * @type Number
405      */
406     this.touchMoveLast = 0;
407 
408     /**
409      * Collects all elements that triggered a mouse down event.
410      * @type Array
411      */
412     this.downObjects = [];
413 
414     /**
415      * Display the licence text.
416      * @see JXG.JSXGraph#licenseText
417      * @see JXG.JSXGraph#initBoard
418      */
419     this.showCopyright = false;
420     if ((showCopyright!=null && showCopyright) || (showCopyright==null && this.options.showCopyright)) {
421         this.showCopyright = true;
422         this.renderer.displayCopyright(JXG.JSXGraph.licenseText, this.options.text.fontSize);
423     }
424 
425     /**
426      * Full updates are needed after zoom and axis translates. This saves some time during an update.
427      * @default false
428      * @type Boolean
429      */
430     this.needsFullUpdate = false;
431 
432     /**
433      * If reducedUpdate is set to true then only the dragged element and few (e.g. 2) following
434      * elements are updated during mouse move. On mouse up the whole construction is
435      * updated. This enables us to be fast even on very slow devices.
436      * @type Boolean
437      * @default false
438      */
439     this.reducedUpdate = false;
440 
441     /**
442      * The current color blindness deficiency is stored in this property. If color blindness is not emulated
443      * at the moment, it's value is 'none'.
444      */
445     this.currentCBDef = 'none';
446 
447     /**
448      * If GEONExT constructions are displayed, then this property should be set to true.
449      * At the moment there should be no difference. But this may change.
450      * This is set in {@link JXG.GeonextReader#readGeonext}.
451      * @type Boolean
452      * @default false
453      * @see JXG.GeonextReader#readGeonext
454      */
455     this.geonextCompatibilityMode = false;
456 
457     if (this.options.text.useASCIIMathML && translateASCIIMath) {
458         init();
459     } else {
460         this.options.text.useASCIIMathML = false;
461     }
462 
463     /**
464      * A flag which tells if the board registers mouse events.
465      * @type Boolean
466      * @default true
467      */
468     this.hasMouseHandlers = false;
469 
470     /**
471      * A flag which tells if the board registers touch events.
472      * @type Boolean
473      * @default true
474      */
475     this.hasTouchHandlers = false;
476 
477     this.addEventHandlers();
478 };
479 
480 JXG.extend(JXG.Board.prototype, /** @lends JXG.Board.prototype */ {
481 
482     /**
483      * Generates an unique name for the given object. The result depends on the objects type, if the
484      * object is a {@link JXG.Point}, capital characters are used, if it is of type {@link JXG.Line}
485      * only lower case characters are used. If object is of type {@link JXG.Polygon}, a bunch of lower
486      * case characters prefixed with P_ are used. If object is of type {@link JXG.Circle} the name is
487      * generated using lower case characters. prefixed with k_ is used. In any other case, lower case
488      * chars prefixed with s_ is used.
489      * @param {Object} object Reference of an JXG.GeometryElement that is to be named.
490      * @returns {String} Unique name for the object.
491      */
492     generateName: function (object) {
493         if (object.type == JXG.OBJECT_TYPE_TICKS) {
494             return '';
495         }
496 
497         var possibleNames,
498             maxNameLength = 3,
499             pre = '',
500             post = '',
501             indices = [],
502             name = '',
503             i, j;
504 
505         if (object.elementClass == JXG.OBJECT_CLASS_POINT) {
506             // points have capital letters
507             possibleNames = ['', 'A', 'B', 'C', 'D', 'E', 'F', 'G', 'H', 'I', 'J', 'K', 'L', 'M', 'N', 'O',
508                 'P', 'Q', 'R', 'S', 'T', 'U', 'V', 'W', 'X', 'Y', 'Z'];
509         } else {
510             // all other elements get lowercase labels
511             possibleNames = ['', 'a', 'b', 'c', 'd', 'e', 'f', 'g', 'h', 'i', 'j', 'k', 'l', 'm', 'n', 'o',
512                 'p', 'q', 'r', 's', 't', 'u', 'v', 'w', 'x', 'y', 'z'];
513         }
514 
515         switch(object.type) {
516             case JXG.OBJECT_TYPE_POLYGON:
517                 pre = 'P_{';
518                 post = '}';
519                 break;
520             case JXG.OBJECT_TYPE_CIRCLE:
521                 pre = 'k_{';
522                 post = '}';
523                 break;
524             case JXG.OBJECT_TYPE_ANGLE:
525                 pre = 'W_{';
526                 post = '}';
527                 break;
528             default:
529                 if (object.elementClass != JXG.OBJECT_CLASS_POINT && object.elementClass != JXG.OBJECT_CLASS_LINE) {
530                     pre = 's_{';
531                     post = '}';
532                 }
533         }
534 
535         for (i=0; i<maxNameLength; i++) {
536             indices[i] = 0;
537         }
538 
539         while (indices[maxNameLength-1] < possibleNames.length) {
540             for (indices[0]=1; indices[0]<possibleNames.length; indices[0]++) {
541                 name = pre;
542 
543                 for (i=maxNameLength; i>0; i--) {
544                     name += possibleNames[indices[i-1]];
545                 }
546 
547                 if (this.elementsByName[name+post] == null) {
548                     return name+post;
549                 }
550 
551             }
552             indices[0] = possibleNames.length;
553             for (i=1; i<maxNameLength; i++) {
554                 if (indices[i-1] == possibleNames.length) {
555                     indices[i-1] = 1;
556                     indices[i]++;
557                 }
558             }
559         }
560 
561         return '';
562     },
563 
564     /**
565      * Generates unique id for a board. The result is randomly generated and prefixed with 'jxgBoard'.
566      * @returns {String} Unique id for a board.
567      */
568     generateId: function () {
569         var r = 1;
570 
571         // as long as we don't have an unique id generate a new one
572         while (JXG.JSXGraph.boards['jxgBoard' + r] != null) {
573             r = Math.round(Math.random()*65535);
574         }
575 
576         return ('jxgBoard' + r);
577     },
578 
579     /**
580      * Composes an id for an element. If the ID is empty ('' or null) a new ID is generated, depending on the
581      * object type. Additionally, the id of the label is set. As a side effect {@link JXG.Board#numObjects}
582      * is updated.
583      * @param {Object} obj Reference of an geometry object that needs an id.
584      * @param {Number} type Type of the object.
585      * @returns {String} Unique id for an element.
586      */
587     setId: function (obj, type) {
588         var num = this.numObjects++,
589             elId = obj.id;
590 
591         // Falls Id nicht vorgegeben, eine Neue generieren:
592         if (elId == '' || !JXG.exists(elId)) {
593             elId = this.id + type + num;
594         }
595 
596         obj.id = elId;
597         this.objects[elId] = obj;
598 
599         return elId;
600     },
601 
602     /**
603      * After construction of the object the visibility is set
604      * and the label is constructed if necessary.
605      * @param {Object} obj The object to add.
606      */
607     finalizeAdding: function (obj) {
608         if (!obj.visProp.visible) {
609             this.renderer.hide(obj);
610         }
611     },
612 
613     finalizeLabel: function (obj) {
614         if (obj.hasLabel && !obj.label.content.visProp.islabel && !obj.label.content.visProp.visible) {
615             this.renderer.hide(obj.label.content);
616         }
617     },
618 
619 /**********************************************************
620  *
621  * Event Handler helpers
622  *
623  **********************************************************/
624 
625     /**
626      * Calculates mouse coordinates relative to the boards container.
627      * @returns {Array} Array of coordinates relative the boards container top left corner.
628      */
629     getCoordsTopLeftCorner: function () {
630         var pCont = this.containerObj,
631             cPos = JXG.getOffset(pCont),
632             doc = document.documentElement.ownerDocument,
633             getProp = function(css) {
634                 var n = parseInt(JXG.getStyle(pCont, css));
635                 return isNaN(n) ? 0 : n;
636             };
637 
638         if (this.mode === JXG.BOARD_MODE_DRAG || this.mode === JXG.BOARD_MODE_MOVE_ORIGIN) {
639             return this.cPos;
640         }
641 
642         if (!pCont.currentStyle && doc.defaultView) {     // Non IE
643             pCont = document.documentElement;
644 
645             // this is for hacks like this one used in wordpress for the admin bar:
646             // html { margin-top: 28px }
647             // seems like it doesn't work in IE
648 
649             cPos[0] += getProp('margin-left');
650             cPos[1] += getProp('margin-top');
651 
652             cPos[0] += getProp('border-left-width');
653             cPos[1] += getProp('border-top-width');
654 
655             cPos[0] += getProp('padding-left');
656             cPos[1] += getProp('padding-top');
657 
658             pCont = this.containerObj;
659         }
660 
661         // add border width
662         cPos[0] += getProp('border-left-width');
663         cPos[1] += getProp('border-top-width');
664 
665         // vml seems to ignore paddings
666         if (this.renderer.type !== 'vml') {
667             // add padding
668             cPos[0] += getProp('padding-left');
669             cPos[1] += getProp('padding-top');
670         }
671 
672         this.cPos = cPos;
673 
674         return cPos;
675     },
676 
677     /**
678      * Get the position of the mouse in screen coordinates, relative to the upper left corner
679      * of the host tag.
680      * @param {Event} e Event object given by the browser.
681      * @param {Number} [i] Only use in case of touch events. This determines which finger to use and should not be set
682      * for mouseevents.
683      * @returns {Array} Contains the mouse coordinates in user coordinates, ready  for {@link JXG.Coords}
684      */
685     getMousePosition: function (e, i) {
686         var cPos = this.getCoordsTopLeftCorner(),
687             absPos;
688 
689         // This fixes the object-drag bug on zoomed webpages on Android powered devices with the default WebKit browser
690         // Seems to be obsolete now
691         //if (JXG.isWebkitAndroid()) {
692         //    cPos[0] -= document.body.scrollLeft;
693         //    cPos[1] -= document.body.scrollTop;
694         //}
695 
696         // position of mouse cursor relative to containers position of container
697         absPos = JXG.getPosition(e, i);
698         return [absPos[0]-cPos[0], absPos[1]-cPos[1]];
699     },
700 
701     /**
702      * Initiate moving the origin. This is used in mouseDown and touchStart listeners.
703      * @param {Number} x Current mouse/touch coordinates
704      * @param {Number} y Current mouse/touch coordinates
705      */
706     initMoveOrigin: function (x, y) {
707         this.drag_dx = x - this.origin.scrCoords[1];
708         this.drag_dy = y - this.origin.scrCoords[2];
709         this.mode = this.BOARD_MODE_MOVE_ORIGIN;
710     },
711 
712     /**
713      * Collects all elements below the current mouse pointer and fulfilling the following constraints:
714      * <ul><li>isDraggable</li><li>visible</li><li>not fixed</li><li>not frozen</li></ul>
715      * @param {Number} x Current mouse/touch coordinates
716      * @param {Number} y current mouse/touch coordinates
717      * @returns {Array} A list of geometric elements.
718      */
719     initMoveObject: function (x, y) {
720         var pEl, el, collect = [], haspoint,
721             dragEl = {visProp:{layer:-10000}};
722 
723         for (el in this.objects) {
724             pEl = this.objects[el];
725             haspoint = pEl.hasPoint && pEl.hasPoint(x, y);
726 
727             if (pEl.visProp.visible && haspoint) {
728                 pEl.triggerEventHandlers('down');
729                 this.downObjects.push(pEl);
730             }
731             if (
732                 ((this.geonextCompatibilityMode
733                   &&(pEl.elementClass==JXG.OBJECT_CLASS_POINT
734                      || pEl.type==JXG.OBJECT_TYPE_TEXT)
735                  )
736                  ||
737                  !this.geonextCompatibilityMode
738                 )
739                 && pEl.isDraggable
740                 && pEl.visProp.visible
741                 && (!pEl.visProp.fixed) && (!pEl.visProp.frozen)
742                 && haspoint
743                 ) {
744                     // Elements in the highest layer get priority.
745                     if (pEl.visProp.layer >= dragEl.visProp.layer) {
746                         // If an element and its label have the focus
747                         // simultaneously, the element is taken
748                         // this only works if we assume that every browser runs
749                         // through this.objects in the right order, i.e. an element A
750                         // added before element B turns up here before B does.
751                         if (JXG.exists(dragEl.label) && pEl==dragEl.label.content) {
752                             continue;
753                         }
754 
755                         dragEl = pEl;
756                         collect[0] = dragEl;
757 
758                         // we can't drop out of this loop because of the event handling system
759                         //if (this.options.takeFirst) {
760                         //    return collect;
761                         //}
762                     }
763             }
764         }
765 
766         if (collect.length > 0) {
767             this.mode = this.BOARD_MODE_DRAG;
768         }
769 
770         if (this.options.takeFirst) {
771             collect.length = 1;
772         }
773 
774         return collect;
775     },
776 
777     /**
778      * Moves an object.
779      * @param {Number} x Coordinate
780      * @param {Number} y Coordinate
781      * @param {object} o The touch object that is dragged: {JXG.Board#mouse} or {JXG.Board#touches}.
782      */
783     moveObject: function (x, y, o) {
784         var newPos = new JXG.Coords(JXG.COORDS_BY_SCREEN, this.getScrCoordsOfMouse(x, y), this),
785             drag = o.obj,
786             oldCoords;
787 
788         if (drag.type != JXG.OBJECT_TYPE_GLIDER) {
789             if (!isNaN(o.targets[0].Xprev+o.targets[0].Yprev)) {
790                  drag.setPositionDirectly(JXG.COORDS_BY_SCREEN, newPos.scrCoords[1], newPos.scrCoords[2], o.targets[0].Xprev, o.targets[0].Yprev);
791             }
792             // Remember the actual position for the next move event. Then we are able to
793             // compute the difference vector.
794             o.targets[0].Xprev = newPos.scrCoords[1];
795             o.targets[0].Yprev = newPos.scrCoords[2];
796             this.update(drag);
797         } else if (drag.type == JXG.OBJECT_TYPE_GLIDER) {
798             oldCoords = drag.coords;
799 
800             // First the new position of the glider is set to the new mouse position
801             drag.setPositionDirectly(JXG.COORDS_BY_USER, newPos.usrCoords[1], newPos.usrCoords[2]);
802 
803             // Then, from this position we compute the projection to the object the glider on which the glider lives.
804             if (drag.slideObject.elementClass == JXG.OBJECT_CLASS_CIRCLE) {
805                 drag.coords = JXG.Math.Geometry.projectPointToCircle(drag, drag.slideObject, this);
806             } else if (drag.slideObject.elementClass == JXG.OBJECT_CLASS_LINE) {
807                 drag.coords = JXG.Math.Geometry.projectPointToLine(drag, drag.slideObject, this);
808             }
809 
810             // Now, we have to adjust the other group elements again.
811             if (drag.group.length != 0) {
812                 drag.group[drag.group.length-1].dX = drag.coords.scrCoords[1] - oldCoords.scrCoords[1];
813                 drag.group[drag.group.length-1].dY = drag.coords.scrCoords[2] - oldCoords.scrCoords[2];
814                 drag.group[drag.group.length-1].update(this);
815             } else {
816                 this.update(drag);
817             }
818         }
819 
820         drag.triggerEventHandlers('drag');
821 
822         this.updateInfobox(drag);
823         drag.highlight();
824     },
825 
826     /**
827      * Moves a line in multitouch mode.
828      * @param {array} p1 x,y coordinates of first touch
829      * @param {array} p2 x,y coordinates of second touch
830      * @param {object} o The touch object that is dragged: {JXG.Board#touches}.
831      */
832     moveLine: function(p1, p2, o) {
833         var np1c, np2c, np1, np2, op1, op2,
834             nmid, omid, nd, od,
835             d, drag,
836             S, alpha, t1, t2, t3, t4, t5;
837 
838         if (JXG.exists(o) && JXG.exists(o.obj)) {
839             drag = o.obj;
840         } else {
841             return;
842         }
843         if (drag.elementClass!=JXG.OBJECT_CLASS_LINE) {
844             return;
845         }
846 
847         // New finger position
848         np1c = new JXG.Coords(JXG.COORDS_BY_SCREEN, this.getScrCoordsOfMouse(p1[0], p1[1]), this);
849         np2c = new JXG.Coords(JXG.COORDS_BY_SCREEN, this.getScrCoordsOfMouse(p2[0], p2[1]), this);
850 
851         if (JXG.exists(o.targets[0]) &&
852             JXG.exists(o.targets[1]) &&
853             !isNaN(o.targets[0].Xprev + o.targets[0].Yprev + o.targets[1].Xprev + o.targets[1].Yprev)) {
854 
855             np1 = np1c.usrCoords;
856             np2 = np2c.usrCoords;
857             // Previous finger position
858             op1 = (new JXG.Coords(JXG.COORDS_BY_SCREEN, [o.targets[0].Xprev,o.targets[0].Yprev], this)).usrCoords;
859             op2 = (new JXG.Coords(JXG.COORDS_BY_SCREEN, [o.targets[1].Xprev,o.targets[1].Yprev], this)).usrCoords;
860 
861             // Affine mid points of the old and new positions
862             omid = [1, (op1[1]+op2[1])*0.5, (op1[2]+op2[2])*0.5];
863             nmid = [1, (np1[1]+np2[1])*0.5, (np1[2]+np2[2])*0.5];
864 
865             // Old and new directions
866             od = JXG.Math.crossProduct(op1, op2);
867             nd = JXG.Math.crossProduct(np1, np2);
868             S = JXG.Math.crossProduct(od, nd);
869 
870             // If parallel, translate otherwise rotate
871             if (Math.abs(S[0])<JXG.Math.eps){
872                 return;
873                 t1 = this.create('transform', [nmid[1]-omid[1], nmid[2]-omid[2]], {type:'translate'});
874             } else {
875                 S[1] /= S[0];
876                 S[2] /= S[0];
877                 alpha = JXG.Math.Geometry.rad(omid.slice(1), S.slice(1), nmid.slice(1));
878                 t1 = this.create('transform', [alpha, S[1], S[2]], {type:'rotate'});
879             }
880             // Old midpoint of fingers after first transformation:
881             t1.update();
882             omid = JXG.Math.matVecMult(t1.matrix, omid);
883             omid[1] /= omid[0];
884             omid[2] /= omid[0];
885 
886             // Shift to the new mid point
887             t2 = this.create('transform', [nmid[1]-omid[1], nmid[2]-omid[2]], {type:'translate'});
888             t2.update();
889             omid = JXG.Math.matVecMult(t2.matrix, omid);
890 
891             d = JXG.Math.Geometry.distance(np1, np2) / JXG.Math.Geometry.distance(op1, op2);
892             // Scale
893             t3 = this.create('transform', [-nmid[1], -nmid[2]], {type:'translate'});
894             t4 = this.create('transform', [d, d], {type:'scale'});
895             t5 = this.create('transform', [nmid[1], nmid[2]], {type:'translate'});
896 
897             t1.melt(t2).melt(t3).melt(t4).melt(t5);
898             t1.applyOnce([drag.point1, drag.point2]);
899 
900             this.update(drag.point1);
901             drag.highlight();
902         }
903         drag.triggerEventHandlers('drag');
904         o.targets[0].Xprev = np1c.scrCoords[1];
905         o.targets[0].Yprev = np1c.scrCoords[2];
906         o.targets[1].Xprev = np2c.scrCoords[1];
907         o.targets[1].Yprev = np2c.scrCoords[2];
908     },
909 
910     highlightElements: function (x, y) {
911         var el, pEl;
912 
913         // Elements  below the mouse pointer which are not highlighted yet will be highlighted.
914         for (el in this.objects) {
915             pEl = this.objects[el];
916             if (pEl.visProp.highlight && JXG.exists(pEl.hasPoint) && pEl.visProp.visible && pEl.hasPoint(x, y)) {
917                 // this is required in any case because otherwise the box won't be shown until the point is dragged
918                 this.updateInfobox(pEl);
919                 if (!JXG.exists(this.highlightedObjects[el])) { // highlight only if not highlighted
920                     this.highlightedObjects[el] = pEl;
921                     pEl.highlight();
922                 }
923 
924                 if (pEl.mouseover) {
925                     pEl.triggerEventHandlers('move');
926                 } else {
927                     pEl.triggerEventHandlers('over');
928                     pEl.mouseover = true;
929                 }
930             }
931         }
932 
933         for (el in this.objects) {
934             pEl = this.objects[el];
935             if (pEl.mouseover) {
936                 if (!this.highlightedObjects[el]) {
937                     pEl.triggerEventHandlers('out');
938                     pEl.mouseover = false;
939                 }
940             }
941         }
942     },
943 
944     /**
945      * Helper function which returns a reasonable starting point for the object being dragged
946      * @param {JXG.GeometryElement} obj The object to be dragged
947      * @returns {Array} The starting point in usr coords
948      */
949    initXYstart: function (obj) {
950         var xy = [];
951 
952         if (obj.type == JXG.OBJECT_TYPE_LINE) {
953             xy.push(obj.point1.coords.usrCoords.slice(1));
954             xy.push(obj.point2.coords.usrCoords.slice(1));
955         } else if (obj.type == JXG.OBJECT_TYPE_CIRCLE) {
956             xy.push(obj.midpoint.coords.usrCoords.slice(1));
957         } else if (obj.type == JXG.OBJECT_TYPE_GLIDER) {
958             xy.push([obj.position, obj.position]);
959         } else {
960             xy.push(obj.coords.usrCoords.slice(1));
961         }
962 
963         return xy;
964     },
965 
966 /**********************************************************
967  *
968  * Event Handler
969  *
970  **********************************************************/
971 
972     /**
973      *  Add all possible event handlers to the board object
974      */
975     addEventHandlers: function () {
976         this.addMouseEventHandlers();
977         this.addTouchEventHandlers();
978    },
979 
980     addMouseEventHandlers: function () {
981         if (!this.hasMouseHandlers) {
982             JXG.addEvent(this.containerObj, 'mousedown', this.mouseDownListener, this);
983             JXG.addEvent(this.containerObj, 'mousemove', this.mouseMoveListener, this);
984             JXG.addEvent(document, 'mouseup', this.mouseUpListener,this);
985 
986             // EXPERIMENTAL: mouse wheel for zoom
987             JXG.addEvent(this.containerObj, 'mousewheel', this.mouseWheelListener, this);
988             // special treatment for firefox
989             JXG.addEvent(this.containerObj, 'DOMMouseScroll', this.mouseWheelListener, this);
990             this.hasMouseHandlers = true;
991 
992             // This one produces errors on IE
993             //   JXG.addEvent(this.containerObj, 'contextmenu', function (e) { e.preventDefault(); return false;}, this);
994             // this one works on IE, Firefox and Chromium with default configurations
995             // It's possible this doesn't work on some Safari or Opera versions by default, the user then has to allow the deactivation of the context menu.
996             this.containerObj.oncontextmenu = function (e) {if (JXG.exists(e)) e.preventDefault(); return false; };
997         }
998     },
999 
1000     addTouchEventHandlers: function () {
1001         if (!this.hasTouchHandlers) {
1002              // To run JSXGraph on mobile touch devices we need these event listeners.
1003             JXG.addEvent(this.containerObj, 'touchstart', this.touchStartListener, this);
1004             JXG.addEvent(this.containerObj, 'touchmove', this.touchMoveListener, this);
1005             JXG.addEvent(document, 'touchend', this.touchEndListener, this);
1006 
1007            // special events for iOS devices to enable gesture based zooming
1008 //            JXG.addEvent(this.containerObj, 'gesturestart', this.gestureStartListener, this);
1009             JXG.addEvent(this.containerObj, 'gesturechange', this.gestureChangeListener, this);
1010 //            JXG.addEvent(this.containerObj, 'gestureend', this.gestureEndListener, this);
1011 
1012             this.hasTouchHandlers = true;
1013         }
1014     },
1015 
1016     removeMouseEventHandlers: function () {
1017         if (this.hasMouseHandlers) {
1018             JXG.removeEvent(this.containerObj, 'mousedown', this.mouseDownListener, this);
1019             JXG.removeEvent(this.containerObj, 'mousemove', this.mouseMoveListener, this);
1020             JXG.removeEvent(document, 'mouseup', this.mouseUpListener, this);
1021 
1022             JXG.removeEvent(this.containerObj, 'mousewheel', this.mouseWheelListener, this);
1023             JXG.removeEvent(this.containerObj, 'DOMMouseScroll', this.mouseWheelListener, this);
1024 
1025             this.hasMouseHandlers = false; // should not be set to false,
1026             // because IMHO (and that's how I used it in the GUI) this variable should only reflect,
1027             // if the device is capable to handle mouse operations -- and this is still the case even
1028             // after removing the registered handlers
1029             // --- reverted:
1030             // hasMouseHandlers and hasTouchHandlers do NOT indicate whether or not the device is capable of
1031             // handling mouse events or touch events. They indicate whether or not the board has registered its
1032             // mouse or touch event handlers. This is their sole purpose and hence should NOT be set outside
1033             // of the core. Those fields are _true_ by default, hence their significance is very, very close to zero if
1034             // not set here.
1035             // See http://sourceforge.net/apps/trac/jsxgraph/changeset/2715 for the full changeset that used to fix
1036             // bug #75 (Coordinates of Gliders remain on Tablets) in the internal GUI bugtracker. Basically, these changes
1037             // cause the mouse handlers to be deactivated on touch devices. And those fields help with the book keeping
1038             // which type of event handlers are still attached (because they don't need to be detached multiple times).
1039         }
1040     },
1041 
1042     removeTouchEventHandlers: function () {
1043         if (this.hasTouchHandlers) {
1044             JXG.removeEvent(this.containerObj, 'touchstart', this.touchStartListener, this);
1045             JXG.removeEvent(this.containerObj, 'touchmove', this.touchMoveListener, this);
1046             JXG.removeEvent(document, 'touchend', this.touchEndListener, this);
1047 
1048 //          JXG.removeEvent(this.containerObj, 'gesturestart', this.gestureStartListener, this);
1049             JXG.removeEvent(this.containerObj, 'gesturechange', this.gestureChangeListener, this);
1050 //          JXG.removeEvent(this.containerObj, 'gestureend', this.gestureEndListener, this);
1051 
1052             this.hasTouchHandlers = false; // see above
1053         }
1054     },
1055 
1056     /**
1057      * Remove all event handlers from the board object
1058      */
1059     removeEventHandlers: function () {
1060         this.removeMouseEventHandlers();
1061         this.removeTouchEventHandlers();
1062     },
1063 
1064     /**
1065      * Handler for click on left arrow in the navigation bar
1066      * @private
1067      */
1068     clickLeftArrow: function () {
1069         this.moveOrigin(this.origin.scrCoords[1] + this.canvasWidth*0.1, this.origin.scrCoords[2]);
1070         return this;
1071     },
1072 
1073     /**
1074      * Handler for click on right arrow in the navigation bar
1075      * @private
1076      */
1077     clickRightArrow: function () {
1078         this.moveOrigin(this.origin.scrCoords[1] - this.canvasWidth*0.1, this.origin.scrCoords[2]);
1079         return this;
1080     },
1081 
1082     /**
1083      * Handler for click on up arrow in the navigation bar
1084      * @private
1085      */
1086     clickUpArrow: function () {
1087         this.moveOrigin(this.origin.scrCoords[1], this.origin.scrCoords[2] - this.canvasHeight*0.1);
1088         return this;
1089     },
1090 
1091     /**
1092      * Handler for click on down arrow in the navigation bar
1093      * @private
1094      */
1095     clickDownArrow: function () {
1096         this.moveOrigin(this.origin.scrCoords[1], this.origin.scrCoords[2] + this.canvasHeight*0.1);
1097         return this;
1098     },
1099 
1100     gestureStartListener: function (evt) {
1101         if (!this.options.zoom.wheel) {
1102             return true;
1103         }
1104 
1105         evt.preventDefault();
1106 
1107         if (this.mode === this.BOARD_MODE_NONE) {
1108             this.mode = this.BOARD_MODE_ZOOM;
1109             this.prevScale = evt.scale;
1110 
1111             this.oldZoomX = this.zoomX;
1112             this.oldZoomY = this.zoomY;
1113         }
1114 
1115         return false;
1116     },
1117 
1118     gestureChangeListener: function (evt) {
1119         var c;
1120 
1121         if (!this.options.zoom.wheel) {
1122             return true;
1123         }
1124 
1125         evt.preventDefault();
1126 
1127         if (this.mode === this.BOARD_MODE_NONE) {
1128             c = new JXG.Coords(JXG.COORDS_BY_SCREEN, this.getMousePosition(evt), this);
1129 
1130             if (this.prevScale < evt.scale) {
1131                 this.zoomIn(c.usrCoords[1], c.usrCoords[2]);
1132             } else {
1133                 this.zoomOut(c.usrCoords[1], c.usrCoords[2]);
1134             }
1135             this.prevScale = evt.scale;
1136         }
1137 
1138         return false;
1139     },
1140 
1141     gestureEndListener: function (evt) {
1142         if (!this.options.zoom.wheel) {
1143             return true;
1144         }
1145 
1146         evt.preventDefault();
1147 
1148         if (this.mode === this.BOARD_MODE_ZOOM) {
1149             this.mode = this.BOARD_MODE_NONE;
1150         }
1151 
1152         return false;
1153     },
1154 
1155     /**
1156      * Touch-Events
1157      */
1158     touchStartListener: function (evt) {
1159         var i, pos, elements, j, k, l,
1160             eps = this.options.precision.touch,
1161             obj, xy = [], found, targets;
1162 
1163         if (this.hasMouseHandlers) {
1164             this.removeMouseEventHandlers();
1165         }
1166 
1167         //evt.preventDefault();
1168         evt.stopPropagation();
1169 
1170         // prevent accidental selection of text
1171         if (document.selection && typeof document.selection.empty == 'function') {
1172             document.selection.empty();
1173         } else if (window.getSelection) {
1174             window.getSelection().removeAllRanges();
1175         }
1176 
1177         // move origin - but only if we're not in drag mode
1178         if ( this.options.pan
1179              && this.mode === this.BOARD_MODE_NONE
1180              && (evt.targetTouches.length == 2)
1181              && (JXG.Math.Geometry.distance([evt.targetTouches[0].screenX, evt.targetTouches[0].screenY], [evt.targetTouches[1].screenX, evt.targetTouches[1].screenY]) < 80)) {
1182 
1183             pos = this.getMousePosition(evt, 0);
1184             this.initMoveOrigin(pos[0], pos[1]);
1185 
1186             this.updateHooks(['touchstart', 'down'], evt);
1187             return false;
1188         }
1189 
1190         // multitouch
1191         this.options.precision.hasPoint = this.options.precision.touch;
1192 
1193         // assuming only points are getting dragged
1194         // todo: this is the most critical part. first we should run through the existing touches and collect all targettouches that don't belong to our
1195         // 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
1196         // 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
1197         // 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
1198         // stretching. as a last step we're going through the rest of the targettouches and initiate new move operations:
1199         //  * points have higher priority over other elements.
1200         //  * 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
1201         //    this element and add them.
1202         // ADDENDUM 11/10/11:
1203         // to allow the user to drag lines and circles with multitouch we have to change this here. some notes for me before implementation:
1204         //  (1) run through the touches control object,
1205         //  (2) try to find the targetTouches for every touch. on touchstart only new touches are added, hence we can find a targettouch
1206         //      for every target in our touches objects
1207         //  (3) if one of the targettouches was bound to a touches targets array, mark it
1208         //  (4) run through the targettouches. if the targettouch is marked, continue. otherwise check for elements below the targettouch:
1209         //      (a) if no element could be found: mark the target touches and continue
1210         //      --- in the following cases, "init" means:
1211         //           (i) check if the element is already used in another touches element, if so, mark the targettouch and continue
1212         //          (ii) if not, init a new touches element, add the targettouch to the touches property and mark it
1213         //      (b) if the element is a point, init
1214         //      (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
1215         //      (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
1216         //          add both to the touches array and mark them.
1217         for (i = 0; i < evt.targetTouches.length; i++) {
1218             evt.targetTouches[i].jxg_isused = false;
1219         }
1220 
1221         for (i = 0; i < this.touches.length; i++) {
1222             for (j = 0; j < this.touches[i].targets.length; j++) {
1223                 this.touches[i].targets[j].num = -1;
1224 
1225                 for (k = 0; k < evt.targetTouches.length; k++) {
1226                     // find the new targettouches
1227                     if (Math.abs(Math.pow(evt.targetTouches[k].screenX - this.touches[i].targets[j].X, 2) + Math.pow(evt.targetTouches[k].screenY - this.touches[i].targets[j].Y, 2)) < eps*eps) {
1228                         this.touches[i].targets[j].num = k;
1229 
1230                         this.touches[i].targets[j].X = evt.targetTouches[k].screenX;
1231                         this.touches[i].targets[j].Y = evt.targetTouches[k].screenY;
1232                         evt.targetTouches[k].jxg_isused = true;
1233                         break;
1234                     }
1235                 }
1236 
1237                 if (this.touches[i].targets[j].num === -1) {
1238                     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.');
1239                     this.touches[i].targets.splice(i, 1);
1240                 }
1241             }
1242         }
1243 
1244         // we just re-mapped the targettouches to our existing touches list. now we have to initialize some touches from additional targettouches
1245         for (i = 0; i < evt.targetTouches.length; i++) {
1246             if (!evt.targetTouches[i].jxg_isused) {
1247                 pos = this.getMousePosition(evt, i);
1248                 elements = this.initMoveObject(pos[0], pos[1]);
1249 
1250                 if (elements.length != 0) {
1251                     obj = elements[elements.length-1];
1252 
1253                     if (JXG.isPoint(obj) || obj.type === JXG.OBJECT_TYPE_TEXT) {
1254                         // it's a point, so it's single touch, so we just push it to our touches
1255 
1256                         targets = [{ num: i, X: evt.targetTouches[i].screenX, Y: evt.targetTouches[i].screenY, Xprev: NaN, Yprev: NaN, Xstart: [], Ystart: [] }];
1257 
1258                         // For the UNDO/REDO of object moves
1259                         xy = this.initXYstart(obj);
1260                         for (l=0; l<xy.length; l++) {
1261                             targets[0].Xstart.push(xy[l][0]);
1262                             targets[0].Ystart.push(xy[l][1]);
1263                         }
1264 
1265                         this.touches.push({ obj: obj, targets: targets });
1266 
1267                     } else if (obj.elementClass === JXG.OBJECT_CLASS_LINE) {
1268                         found = false;
1269                         // first check if this line is already capture in this.touches
1270                         for (j = 0; j < this.touches.length; j++) {
1271                             if (obj.id === this.touches[j].obj.id) {
1272                                 found = true;
1273                                 // only add it, if we don't have two targets in there already
1274                                 if (this.touches[j].targets.length === 1) {
1275 
1276                                     var target = { num: i, X: evt.targetTouches[i].screenX, Y: evt.targetTouches[i].screenY, Xprev: NaN, Yprev: NaN, Xstart: [], Ystart: [] };
1277 
1278                                     // For the UNDO/REDO of object moves
1279                                     xy = this.initXYstart(obj);
1280                                     for (l=0; l<xy.length; l++) {
1281                                         target.Xstart.push(xy[l][0]);
1282                                         target.Ystart.push(xy[l][1]);
1283                                     }
1284 
1285                                     this.touches[j].targets.push(target);
1286                                 }
1287 
1288                                 evt.targetTouches[i].jxg_isused = true;
1289                             }
1290                         }
1291 
1292                         // we couldn't find it in touches, so we just init a new touches
1293                         // IF there is a second touch targetting this line, we will find it later on, and then add it to
1294                         // the touches control object.
1295                         if (!found) {
1296 
1297                             targets = [{ num: i, X: evt.targetTouches[i].screenX, Y: evt.targetTouches[i].screenY, Xprev: NaN, Yprev: NaN, Xstart: [], Ystart: [] }];
1298 
1299                             // For the UNDO/REDO of object moves
1300                             xy = this.initXYstart(obj);
1301                             for (l=0; l<xy.length; l++) {
1302                                 targets[0].Xstart.push(xy[l][0]);
1303                                 targets[0].Ystart.push(xy[l][1]);
1304                             }
1305 
1306                             this.touches.push({ obj: obj, targets: targets });
1307                         }
1308 
1309                     } else if (obj.elementClass === JXG.OBJECT_CLASS_CIRCLE) {
1310                         found = false;
1311                         // first check if this line is already capture in this.touches
1312                         for (j = 0; j < this.touches.length; j++) {
1313                             if (obj.id === this.touches[j].obj.id) {
1314                                 // TODO: for now we only support single touch circle movement
1315                                 found = true;
1316                                 evt.targetTouches[i].jxg_isused = true;
1317                                 break;
1318                             }
1319                         }
1320 
1321                         // we couldn't find it in touches, so we just init a new touches
1322                         // IF there is a second touch targetting this line, we will find it later on, and then add it to
1323                         // the touches control object.
1324                         if (!found) {
1325                             targets = [{ num: i, X: evt.targetTouches[i].screenX, Y: evt.targetTouches[i].screenY, Xprev: NaN, Yprev: NaN, Xstart: [], Ystart: [] }];
1326 
1327                             // For the UNDO/REDO of object moves
1328                             xy = this.initXYstart(obj);
1329                             for (l=0; l<xy.length; l++) {
1330                                 targets[0].Xstart.push(xy[l][0]);
1331                                 targets[0].Ystart.push(xy[l][1]);
1332                             }
1333 
1334                             this.touches.push({ obj: obj, targets: targets });
1335                         }
1336                     }
1337                 }
1338 
1339                 evt.targetTouches[i].jxg_isused = true;
1340             }
1341         }
1342 
1343         if (JXG.isWebkitAndroid()) {
1344             var ti = new Date();
1345             this.touchMoveLast = ti.getTime()-200;
1346         }
1347 
1348         this.options.precision.hasPoint = this.options.precision.mouse;
1349 
1350         this.updateHooks(['touchstart', 'down'], evt);
1351         return false;
1352     },
1353 
1354     touchMoveListener: function (evt) {
1355         var i, j, pos;
1356 
1357         evt.preventDefault();
1358         evt.stopPropagation();
1359         // Reduce update frequency for Android devices
1360         if (JXG.isWebkitAndroid()) {
1361             var ti = new Date();
1362             ti = ti.getTime();
1363             if (ti-this.touchMoveLast<80) {
1364                 this.updateQuality = this.BOARD_QUALITY_HIGH;
1365                 this.updateHooks(['touchmove', 'move'], evt, this.mode);
1366                 return false;
1367             } else {
1368                 this.touchMoveLast = ti;
1369             }
1370         }
1371 
1372         this.dehighlightAll();   // As long as we do not highlight we need not dehighlight
1373                                  // Now we do highlight, so we may need to dehighlight
1374         if (this.mode != this.BOARD_MODE_DRAG) {
1375             this.renderer.hide(this.infobox);
1376         }
1377 
1378         this.options.precision.hasPoint = this.options.precision.touch;
1379         if (this.mode == this.BOARD_MODE_MOVE_ORIGIN) {
1380             pos = this.getMousePosition(evt, 0);
1381             this.moveOrigin(pos[0], pos[1]);
1382         } else if (this.mode == this.BOARD_MODE_DRAG) {
1383             // Runs over through all elements which are touched
1384             // by at least one finger.
1385             for (i = 0; i < this.touches.length; i++) {
1386                 // Touch by one finger:  this is possible for all elements that can be dragged
1387                 if (this.touches[i].targets.length === 1) {
1388                     this.touches[i].targets[0].X = evt.targetTouches[this.touches[i].targets[0].num].screenX;
1389                     this.touches[i].targets[0].Y = evt.targetTouches[this.touches[i].targets[0].num].screenY;
1390                     pos = this.getMousePosition(evt, this.touches[i].targets[0].num);
1391                     this.moveObject(pos[0], pos[1], this.touches[i]);
1392                 // Touch by two fingers: moving lines
1393                 } else if (this.touches[i].targets.length === 2 && this.touches[i].targets[0].num > -1 && this.touches[i].targets[1].num > -1) {
1394                     this.touches[i].targets[0].X = evt.targetTouches[this.touches[i].targets[0].num].screenX;
1395                     this.touches[i].targets[0].Y = evt.targetTouches[this.touches[i].targets[0].num].screenY;
1396                     this.touches[i].targets[1].X = evt.targetTouches[this.touches[i].targets[1].num].screenX;
1397                     this.touches[i].targets[1].Y = evt.targetTouches[this.touches[i].targets[1].num].screenY;
1398                     this.moveLine(
1399                         this.getMousePosition(evt, this.touches[i].targets[0].num),
1400                         this.getMousePosition(evt, this.touches[i].targets[1].num),
1401                         this.touches[i]
1402                         );
1403                 }
1404             }
1405         } else {
1406             for (i = 0; i < evt.targetTouches.length; i++) {
1407                 pos = this.getMousePosition(evt, i);
1408                 this.highlightElements(pos[0], pos[1]);
1409             }
1410         }
1411         if (this.mode != this.BOARD_MODE_DRAG) {
1412             this.renderer.hide(this.infobox);
1413         }
1414 
1415         this.options.precision.hasPoint = this.options.precision.mouse;
1416 
1417         this.updateHooks(['touchmove', 'move'], evt, this.mode);
1418         return false;
1419     },
1420 
1421     touchEndListener: function (evt) {
1422         var i, j, k,
1423             eps = this.options.precision.touch,
1424             tmpTouches = [], found, foundNumber;
1425 
1426         this.updateHooks(['touchend', 'up'], evt);
1427         this.renderer.hide(this.infobox);
1428 
1429         if (evt.targetTouches.length > 0) {
1430             for (i = 0; i < this.touches.length; i++) {
1431                 tmpTouches[i] = this.touches[i];
1432             }
1433             this.touches.length = 0;
1434 
1435             // assuming only points can be moved
1436             // todo: don't run through the targettouches but through the touches and check if all touches.targets are still available
1437             // if not, try to convert the operation, e.g. if a lines is rotated and translated with two fingers and one finger is lifted,
1438             // convert the operation to a simple one-finger-translation.
1439             // ADDENDUM 11/10/11:
1440             // see addendum to touchStartListener from 11/10/11
1441             // (1) run through the tmptouches
1442             // (2) check the touches.obj, if it is a
1443             //     (a) point, try to find the targettouch, if found keep it and mark the targettouch, else drop the touch.
1444             //     (b) line with
1445             //          (i) one target: try to find it, if found keep it mark the targettouch, else drop the touch.
1446             //         (ii) two targets: if none can be found, drop the touch. if one can be found, remove the other target. mark all found targettouches
1447             //     (c) circle with [proceed like in line]
1448 
1449             // init the targettouches marker
1450             for (i = 0; i < evt.targetTouches.length; i++) {
1451                 evt.targetTouches[i].jxg_isused = false;
1452 
1453             }
1454 
1455             for (i = 0; i < tmpTouches.length; i++) {
1456                 // could all targets of the current this.touches.obj be assigned to targettouches?
1457                 found = false;
1458                 foundNumber = 0;
1459 
1460                 for (j = 0; j < tmpTouches[i].targets.length; j++) {
1461                     tmpTouches[i].targets[j].found = false;
1462 
1463                     for (k = 0; k < evt.targetTouches.length; k++) {
1464                         if (Math.abs(Math.pow(evt.targetTouches[k].screenX - tmpTouches[i].targets[j].X, 2) + Math.pow(evt.targetTouches[k].screenY - tmpTouches[i].targets[j].Y, 2)) < eps*eps) {
1465                             tmpTouches[i].targets[j].found = true;
1466                             tmpTouches[i].targets[j].num = k;
1467                             tmpTouches[i].targets[j].X = evt.targetTouches[k].screenX;
1468                             tmpTouches[i].targets[j].Y = evt.targetTouches[k].screenY;
1469                             foundNumber++;
1470                             break;
1471                         }
1472                     }
1473                 }
1474 
1475                 if (JXG.isPoint(tmpTouches[i].obj)) {
1476                     found = tmpTouches[i].targets[0].found;
1477                 } else if (tmpTouches[i].obj.elementClass === JXG.OBJECT_CLASS_LINE) {
1478                     found = tmpTouches[i].targets[0].found || tmpTouches[i].targets[1].found;
1479                 } else if (tmpTouches[i].obj.elementClass === JXG.OBJECT_CLASS_CIRCLE) {
1480                     found = foundNumber === 1 || foundNumber === 3;
1481                 }
1482 
1483                 // if we found this object to be still dragged by the user, add it back to this.touches
1484                 if (found) {
1485                     this.touches.push({
1486                         obj: tmpTouches[i].obj,
1487                         targets: []
1488                     });
1489 
1490                     for (j = 0; j < tmpTouches[i].targets.length; j++) {
1491                         if (tmpTouches[i].targets[j].found) {
1492                             this.touches[this.touches.length-1].targets.push({
1493                                 num: tmpTouches[i].targets[j].num,
1494                                 X: tmpTouches[i].targets[j].screenX,
1495                                 Y: tmpTouches[i].targets[j].screenY,
1496                                 Xprev: NaN,
1497                                 Yprev: NaN,
1498                                 Xstart: tmpTouches[i].targets[j].Xstart,
1499                                 Ystart: tmpTouches[i].targets[j].Ystart
1500                             });
1501                         }
1502                     }
1503                 }
1504             }
1505         } else {
1506             this.updateQuality = this.BOARD_QUALITY_HIGH;
1507             this.mode = this.BOARD_MODE_NONE;
1508 
1509             if (this.mode !== this.BOARD_MODE_MOVE_ORIGIN) {
1510                 this.update();
1511             }
1512 
1513             this.touches.length = 0;
1514         }
1515 
1516         for (i = 0; i < this.downObjects.length; i++) {
1517             found = false;
1518             for (j = 0; j < this.touches.length; j++) {
1519                 if (this.touches.obj.id == this.downObjects[i].id) {
1520                     found = true;
1521                 }
1522             }
1523             if (!found) {
1524                 this.downObjects[i].triggerEventHandlers('up');
1525                 this.downObjects.splice(i, 1);
1526             }
1527         }
1528     },
1529 
1530     /**
1531      * This method is called by the browser when the mouse is moved.
1532      * @param {Event} Evt The browsers event object.
1533      * @returns {Boolean} True if no element is found under the current mouse pointer, false otherwise.
1534      */
1535     mouseDownListener: function (Evt) {
1536         var pos, elements, xy, r, i;
1537 
1538         // prevent accidental selection of text
1539         if (document.selection && typeof document.selection.empty == 'function') {
1540             document.selection.empty();
1541         } else if (window.getSelection) {
1542             window.getSelection().removeAllRanges();
1543         }
1544         pos = this.getMousePosition(Evt);
1545 
1546         if (this.options.pan && Evt.shiftKey) {
1547             this.initMoveOrigin(pos[0], pos[1]);
1548             r = false;
1549         } else {
1550 
1551             elements = this.initMoveObject(pos[0], pos[1]);
1552 
1553             // if no draggable object can be found, get out here immediately
1554             if (elements.length == 0) {
1555                 this.mode = this.BOARD_MODE_NONE;
1556                 r = true;
1557             } else {
1558                 this.mouse = {
1559                     obj: null,
1560                     targets: [{
1561                             X: pos[0],
1562                             Y: pos[1],
1563                             Xprev: NaN,
1564                             Yprev: NaN
1565                         }
1566                     ]
1567                 };
1568                 this.mouse.obj = elements[elements.length-1];
1569 
1570                 this.mouse.targets[0].Xstart = [];
1571                 this.mouse.targets[0].Ystart = [];
1572 
1573                 xy = this.initXYstart(this.mouse.obj);
1574 
1575                 for (i = 0; i < xy.length; i++) {
1576                   this.mouse.targets[0].Xstart.push(xy[i][0]);
1577                   this.mouse.targets[0].Ystart.push(xy[i][1]);
1578                 }
1579 
1580                 // prevent accidental text selection
1581                 // this could get us new trouble: input fields, links and drop down boxes placed as text
1582                 // on the board don't work anymore.
1583                 if (Evt && Evt.preventDefault) {
1584                     Evt.preventDefault();
1585                 } else if (window.event) {
1586                     window.event.returnValue = false;
1587                 }
1588             }
1589         }
1590 
1591         this.updateHooks(['mousedown', 'down'], Evt);
1592         return r;
1593     },
1594 
1595     /**
1596      * This method is called by the browser when the left mouse button is released.
1597      * @private
1598      */
1599     mouseUpListener: function (Evt) {
1600         var i;
1601 
1602         this.updateHooks(['mouseup', 'up'], Evt);
1603 
1604         // redraw with high precision
1605         this.updateQuality = this.BOARD_QUALITY_HIGH;
1606         this.mode = this.BOARD_MODE_NONE;
1607 
1608         if (this.mode !== this.BOARD_MODE_MOVE_ORIGIN) {
1609             this.update();
1610 
1611             for (i = 0; i < this.downObjects.length; i++) {
1612                 this.downObjects[i].triggerEventHandlers('up');
1613             }
1614 
1615             this.downObjects.length = 0;
1616         }
1617 
1618         // release dragged mouse object
1619         this.mouse = null;
1620     },
1621 
1622     /**
1623      * This method is called by the browser when the left mouse button is clicked.
1624      * @param {Event} Event The browsers event object.
1625      * @private
1626      */
1627     mouseMoveListener: function (Event) {
1628         var pos;
1629 
1630         pos = this.getMousePosition(Event);
1631 
1632         this.updateQuality = this.BOARD_QUALITY_LOW;
1633 
1634         this.dehighlightAll();
1635         if (this.mode != this.BOARD_MODE_DRAG) {
1636             this.renderer.hide(this.infobox);
1637         }
1638 
1639         // we have to check for three cases:
1640         //   * user moves origin
1641         //   * user drags an object
1642         //   * user just moves the mouse, here highlight all elements at
1643         //     the current mouse position
1644         if (this.mode == this.BOARD_MODE_MOVE_ORIGIN) {
1645             this.moveOrigin(pos[0] - this.drag_dx, pos[1] - this.drag_dy);
1646         } else if (this.mode == this.BOARD_MODE_DRAG) {
1647             this.moveObject(pos[0], pos[1], this.mouse);
1648         } else { // BOARD_MODE_NONE or BOARD_MODE_CONSTRUCT
1649             this.highlightElements(pos[0], pos[1]);
1650         }
1651         this.updateQuality = this.BOARD_QUALITY_HIGH;
1652 
1653         this.updateHooks(['mousemove', 'move'], Event, this.mode);
1654     },
1655 
1656     /**
1657      * Handler for mouse wheel events. Used to zoom in and out of the board.
1658      * @param {Event} Event
1659      * @returns {Boolean}
1660      */
1661     mouseWheelListener: function (Event) {
1662         if (!this.options.zoom.wheel) {
1663             return true;
1664         }
1665 
1666         Event = Event ? Event : window.event;
1667         var wd = Event.detail ? Event.detail*(-1) : Event.wheelDelta/40,
1668             pos = new JXG.Coords(JXG.COORDS_BY_SCREEN, this.getMousePosition(Event), this);
1669 
1670         if (wd > 0) {
1671             this.zoomIn(pos.usrCoords[1], pos.usrCoords[2]);
1672         } else {
1673             this.zoomOut(pos.usrCoords[1], pos.usrCoords[2]);
1674         }
1675 
1676         Event.preventDefault();
1677         return false;
1678     },
1679 
1680 /**********************************************************
1681  *
1682  * End of Event Handlers
1683  *
1684  **********************************************************/
1685 
1686     /**
1687      * Updates and displays a little info box to show coordinates of current selected points.
1688      * @param {JXG.GeometryElement} el A GeometryElement
1689      * @returns {JXG.Board} Reference to the board
1690      */
1691     updateInfobox: function (el) {
1692         var x, y, xc, yc;
1693 
1694         if (!el.visProp.showinfobox) {
1695             return this;
1696         }
1697         if (el.elementClass == JXG.OBJECT_CLASS_POINT) {
1698             xc = el.coords.usrCoords[1];
1699             yc = el.coords.usrCoords[2];
1700 
1701             this.infobox.setCoords(xc+this.infobox.distanceX/(this.unitX),
1702                 yc+this.infobox.distanceY/(this.unitY));
1703             if (typeof(el.infoboxText)!="string") {
1704                 x = Math.abs(xc);
1705                 if (x>0.1) {
1706                     x = xc.toFixed(2);
1707                 } else if (x>=0.01) {
1708                     x = xc.toFixed(4);
1709                 } else if (x>=0.0001) {
1710                     x = xc.toFixed(6);
1711                 } else {
1712                     x = xc;
1713                 }
1714                 y = Math.abs(yc);
1715                 if (y>0.1) {
1716                     y = yc.toFixed(2);
1717                 } else if (y>=0.01) {
1718                     y = yc.toFixed(4);
1719                 } else if (y>=0.0001) {
1720                     y = yc.toFixed(6);
1721                 } else {
1722                     y = yc;
1723                 }
1724 
1725                 this.highlightInfobox(x,y,el);
1726             } else
1727                 this.highlightCustomInfobox(el.infoboxText, el);
1728 
1729             this.renderer.show(this.infobox);
1730             this.renderer.updateText(this.infobox);
1731         }
1732         return this;
1733     },
1734 
1735     /**
1736      * Changes the text of the info box to what is provided via text.
1737      * @param {String} text
1738      * @returns {JXG.Board} Reference to the board.
1739      */
1740     highlightCustomInfobox: function (text) {
1741         this.infobox.setText('<span style="color:#bbbbbb;">' + text + '<'+'/span>');
1742         return this;
1743     },
1744 
1745     /**
1746      * Changes the text of the info box to show the given coordinates.
1747      * @param {Number} x
1748      * @param {Number} y
1749      * @param {JXG.Point} el The element the mouse is pointing at
1750      * @returns {JXG.Board} Reference to the board.
1751      */
1752     highlightInfobox: function (x, y, el) {
1753         this.highlightCustomInfobox('(' + x + ', ' + y + ')');
1754         return this;
1755     },
1756 
1757     /**
1758      * Remove highlighting of all elements.
1759      * @returns {JXG.Board} Reference to the board.
1760      */
1761     dehighlightAll: function () {
1762         var el, pEl, needsDehighlight = false;
1763 
1764         for (el in this.highlightedObjects) {
1765             pEl = this.highlightedObjects[el];
1766             pEl.noHighlight();
1767             needsDehighlight = true;
1768 
1769             // In highlightedObjects should only be objects which fulfill all these conditions
1770             // And in case of complex elements, like a turtle based fractal, it should be faster to
1771             // just de-highlight the element instead of checking hasPoint...
1772             // if ((!JXG.exists(pEl.hasPoint)) || !pEl.hasPoint(x, y) || !pEl.visProp.visible)
1773         }
1774 
1775         this.highlightedObjects = {};
1776 
1777         // We do not need to redraw during dehighlighting in CanvasRenderer
1778         // because we are redrawing anyhow
1779         //  -- We do need to redraw during dehighlighting. Otherwise objects won't be dehighlighted until
1780         // another object is highlighted.
1781         if (this.options.renderer=='canvas' && needsDehighlight) {
1782             this.prepareUpdate();
1783             this.renderer.suspendRedraw(this);
1784             this.updateRenderer();
1785             this.renderer.unsuspendRedraw();
1786         }
1787 
1788         return this;
1789     },
1790 
1791     /**
1792      * In case of snapToGrid activated this method caclulates the screen coords of mouse "snapped to grid".
1793      * @param {Number} x X coordinate in screen coordinates
1794      * @param {Number} y Y coordinate in screen coordinates
1795      * @returns {Array} Coordinates of the mouse in screen coordinates, snapped to grid.
1796      */
1797     getScrCoordsOfMouse: function (x, y) {
1798         if (this.options.grid.snapToGrid) {
1799             var newCoords = new JXG.Coords(JXG.COORDS_BY_SCREEN, [x,y], this);
1800             newCoords.setCoordinates(JXG.COORDS_BY_USER,
1801                 [Math.round((newCoords.usrCoords[1])*this.options.grid.snapSizeX)/this.options.grid.snapSizeX,
1802                     Math.round((newCoords.usrCoords[2])*this.options.grid.snapSizeY)/this.options.grid.snapSizeY]);
1803             return [newCoords.scrCoords[1], newCoords.scrCoords[2]];
1804         } else {
1805             return [x,y];
1806         }
1807     },
1808 
1809     /**
1810      * In case of snapToGrid activated this method calculates the user coords of mouse "snapped to grid".
1811      * @param {Event} Evt Event object containing the mouse coordinates.
1812      * @returns {Array} Coordinates of the mouse in screen coordinates, snapped to grid.
1813      */
1814     getUsrCoordsOfMouse: function (Evt) {
1815         var cPos = this.getCoordsTopLeftCorner(),
1816             absPos = JXG.getPosition(Evt),
1817             x = absPos[0]-cPos[0],
1818             y = absPos[1]-cPos[1],
1819             newCoords = new JXG.Coords(JXG.COORDS_BY_SCREEN, [x,y], this);
1820 
1821         return newCoords.usrCoords.slice(1);
1822     },
1823 
1824     /**
1825      * Collects all elements under current mouse position plus current user coordinates of mouse cursor.
1826      * @param {Event} Evt Event object containing the mouse coordinates.
1827      * @returns {Array} Array of elements at the current mouse position plus current user coordinates of mouse.
1828      */
1829     getAllUnderMouse: function (Evt) {
1830         var elList = this.getAllObjectsUnderMouse();
1831 
1832         elList.push(this.getUsrCoordsOfMouse(Evt));
1833 
1834         return elList;
1835     },
1836     /**
1837      * Collects all elements under current mouse position.
1838      * @param {Event} Evt Event object containing the mouse coordinates.
1839      * @returns {Array} Array of elements at the current mouse position.
1840      */
1841     getAllObjectsUnderMouse: function (Evt) {
1842         var cPos = this.getCoordsTopLeftCorner(),
1843             absPos = JXG.getPosition(Evt),
1844             dx = absPos[0]-cPos[0],
1845             dy = absPos[1]-cPos[1],
1846             elList = [];
1847 
1848         for (var el in this.objects) {
1849             if (this.objects[el].visProp.visible && this.objects[el].hasPoint && this.objects[el].hasPoint(dx, dy)) {
1850                 elList.push(this.objects[el]);
1851             }
1852         }
1853 
1854         return elList;
1855     },
1856 
1857     /**
1858      * Moves the origin and initializes an update of all elements.
1859      * @params {Number} x
1860      * @params {Number} y
1861      * @returns {JXG.Board} Reference to this board.
1862      */
1863     moveOrigin: function (x, y) {
1864         var el, ob;
1865 
1866         // This is not required, but to be downwards compatible, we should keep it for a while.
1867         // changed in version 0.91a
1868         if (JXG.exists(x) && JXG.exists(y)) {
1869             this.origin.scrCoords[1] = x;
1870             this.origin.scrCoords[2] = y;
1871         }
1872 
1873         for (ob in this.objects) {
1874             el = this.objects[ob];
1875             if (!el.visProp.frozen && (el.elementClass==JXG.OBJECT_CLASS_POINT ||
1876                 el.elementClass==JXG.OBJECT_CLASS_CURVE ||
1877                 el.type==JXG.OBJECT_TYPE_AXIS ||
1878                 el.type==JXG.OBJECT_TYPE_TEXT)) {
1879                 if (el.elementClass!=JXG.OBJECT_CLASS_CURVE && el.type!=JXG.OBJECT_TYPE_AXIS)
1880                     el.coords.usr2screen();
1881             }
1882         }
1883 
1884         this.clearTraces();
1885         this.fullUpdate();
1886 
1887         return this;
1888     },
1889 
1890     /**
1891      * Add conditional updates to the elements.
1892      * @param {String} str String containing coniditional update in geonext syntax
1893      */
1894     addConditions: function (str) {
1895         var plaintext = 'var el, x, y, c, rgbo;\n',
1896             i = str.indexOf('<data>'),
1897             j = str.indexOf('<'+'/data>'),
1898             term, m, left, right, name, el;
1899 
1900         if (i<0) {
1901             return;
1902         }
1903 
1904         while (i>=0) {
1905             term = str.slice(i+6,j);   // throw away <data>
1906             m = term.indexOf('=');
1907             left = term.slice(0,m);
1908             right = term.slice(m+1);
1909             m = left.indexOf('.');     // Dies erzeugt Probleme bei Variablennamen der Form " Steuern akt."
1910             name = left.slice(0,m);    //.replace(/\s+$/,''); // do NOT cut out name (with whitespace)
1911             el = this.elementsByName[JXG.unescapeHTML(name)];
1912 
1913             var property = left.slice(m+1).replace(/\s+/g,'').toLowerCase(); // remove whitespace in property
1914             right = JXG.GeonextParser.geonext2JS(right, this);
1915             right = right.replace(/this\.board\./g,'this.');
1916 
1917             // Debug
1918             if (!JXG.exists(this.elementsByName[name])){
1919                 JXG.debug("debug conditions: |"+name+"| undefined");
1920             }
1921             plaintext += "el = this.objects[\"" + el.id + "\"];\n";
1922 
1923             switch (property) {
1924                 case 'x':
1925                     plaintext += 'var y=el.coords.usrCoords[2];\n';  // y stays
1926                     plaintext += 'el.setPositionDirectly(JXG.COORDS_BY_USER,'+(right) +',y);\n';
1927                     plaintext += 'el.prepareUpdate().update();\n';
1928                     break;
1929                 case 'y':
1930                     plaintext += 'var x=el.coords.usrCoords[1];\n';  // x stays
1931                     plaintext += 'el.coords=new JXG.Coords(JXG.COORDS_BY_USER,[x,'+(right)+'],this);\n';
1932                     plaintext += 'el.setPositionDirectly(JXG.COORDS_BY_USER,x,'+(right) +');\n';
1933                     plaintext += 'el.prepareUpdate().update();\n';
1934                     break;
1935                 case 'visible':
1936                     plaintext += 'var c='+(right)+';\n';
1937                     plaintext += 'el.visProp.visible = c;\n';
1938                     plaintext += 'if (c) {el.showElement();} else {el.hideElement();}\n';
1939                     break;
1940                 case 'position':
1941                     plaintext += 'el.position = ' + (right) +';\n';
1942                     plaintext += 'el.prepareUpdate().update(true);\n';
1943                     break;
1944                 case 'stroke':
1945                     plaintext += 'rgbo = JXG.rgba2rgbo('+(right)+');\n';
1946                     plaintext += 'el.visProp.strokecolor = rgbo[0];\n';
1947                     plaintext += 'el.visProp.strokeopacity = rgbo[1];\n';
1948                     break;
1949                 case 'style':
1950                     plaintext += 'el.setStyle(' + (right) +');\n';
1951                     break;
1952                 case 'strokewidth':
1953                     plaintext += 'el.strokeWidth = ' + (right) +';\n';   // wird auch bei Punkten verwendet, was nicht realisiert ist.
1954                     break;
1955                 case 'fill':
1956                     plaintext += 'var rgbo = JXG.rgba2rgbo('+(right)+');\n';
1957                     plaintext += 'el.visProp.fillcolor = rgbo[0];\n';
1958                     plaintext += 'el.visProp.fillopacity = rgbo[1];\n';
1959                     break;
1960                 case 'label':
1961                     break;
1962                 default:
1963                     JXG.debug("property '" + property + "' in conditions not yet implemented:" + right);
1964                     break;
1965             }
1966             str = str.slice(j+7); // cut off "</data>"
1967             i = str.indexOf('<data>');
1968             j = str.indexOf('<'+'/data>');
1969         }
1970         plaintext += 'this.prepareUpdate().updateElements();\n';
1971         plaintext += 'return true;\n';
1972 
1973         this.updateConditions = new Function(plaintext);
1974         this.updateConditions();
1975     },
1976 
1977     /**
1978      * Computes the commands in the conditions-section of the gxt file.
1979      * It is evaluated after an update, before the unsuspendRedraw.
1980      * The function is generated in
1981      * @see JXG.Board#addConditions
1982      * @private
1983      */
1984     updateConditions: function () {
1985         return false;
1986     },
1987 
1988     /**
1989      * Calculates adequate snap sizes.
1990      * @returns {JXG.Board} Reference to the board.
1991      */
1992     calculateSnapSizes: function () {
1993         var p1 = new JXG.Coords(JXG.COORDS_BY_USER, [0, 0], this),
1994             p2 = new JXG.Coords(JXG.COORDS_BY_USER, [this.options.grid.gridX, this.options.grid.gridY], this),
1995             x = p1.scrCoords[1]-p2.scrCoords[1],
1996             y = p1.scrCoords[2]-p2.scrCoords[2];
1997 
1998         this.options.grid.snapSizeX = this.options.grid.gridX;
1999         while (Math.abs(x) > 25) {
2000             this.options.grid.snapSizeX *= 2;
2001             x /= 2;
2002         }
2003 
2004         this.options.grid.snapSizeY = this.options.grid.gridY;
2005         while (Math.abs(y) > 25) {
2006             this.options.grid.snapSizeY *= 2;
2007             y /= 2;
2008         }
2009 
2010         return this;
2011     },
2012 
2013     /**
2014      * Apply update on all objects with the new zoom-factors. Clears all traces.
2015      * @returns {JXG.Board} Reference to the board.
2016      */
2017     applyZoom: function () {
2018         var el, ob;
2019         for (ob in this.objects) {
2020             el = this.objects[ob];
2021             if (!el.visProp.frozen && (el.elementClass==JXG.OBJECT_CLASS_POINT ||
2022                 el.elementClass==JXG.OBJECT_CLASS_CURVE ||
2023                 el.type==JXG.OBJECT_TYPE_AXIS ||
2024                 el.type==JXG.OBJECT_TYPE_TEXT)) {
2025                 if (el.elementClass!=JXG.OBJECT_CLASS_CURVE && el.type!=JXG.OBJECT_TYPE_AXIS)
2026                     el.coords.usr2screen();
2027             }
2028         }
2029         this.calculateSnapSizes();
2030         this.clearTraces();
2031         this.fullUpdate();
2032 
2033         return this;
2034     },
2035 
2036     /**
2037      * Zooms into the board by the factors board.options.zoom.factorX and board.options.zoom.factorY and applies the zoom.
2038      * @returns {JXG.Board} Reference to the board
2039      */
2040     zoomIn: function (x, y) {
2041         var bb = this.getBoundingBox(),
2042             zX = this.options.zoom.factorX,
2043             zY = this.options.zoom.factorY,
2044             dX = (bb[2]-bb[0])*(1.0-1.0/zX),
2045             dY = (bb[1]-bb[3])*(1.0-1.0/zY),
2046             lr = 0.5, tr = 0.5;
2047 
2048         if (typeof x === 'number' && typeof y === 'number') {
2049             lr = (x - bb[0])/(bb[2] - bb[0]);
2050             tr = (bb[1] - y)/(bb[1] - bb[3]);
2051         }
2052 
2053         this.setBoundingBox([bb[0]+dX*lr, bb[1]-dY*tr, bb[2]-dX*(1-lr), bb[3]+dY*(1-tr)], false);
2054         this.zoomX *= zX;
2055         this.zoomY *= zY;
2056         this.applyZoom();
2057         return this;
2058     },
2059 
2060     /**
2061      * Zooms out of the board by the factors board.options.zoom.factorX and board.options.zoom.factorY and applies the zoom.
2062      * @returns {JXG.Board} Reference to the board
2063      */
2064     zoomOut: function (x, y) {
2065         var bb = this.getBoundingBox(),
2066             zX = this.options.zoom.factorX,
2067             zY = this.options.zoom.factorY,
2068             dX = (bb[2]-bb[0])*(1.0-zX),
2069             dY = (bb[1]-bb[3])*(1.0-zY),
2070             lr = 0.5, tr = 0.5;
2071 
2072         if (typeof x === 'number' && typeof y === 'number') {
2073             lr = (x - bb[0])/(bb[2] - bb[0]);
2074             tr = (bb[1] - y)/(bb[1] - bb[3]);
2075         }
2076 
2077         this.setBoundingBox([bb[0]+dX*lr, bb[1]-dY*tr, bb[2]-dX*(1-lr), bb[3]+dY*(1-tr)], false);
2078         this.zoomX /= zX;
2079         this.zoomY /= zY;
2080         this.applyZoom();
2081         return this;
2082     },
2083 
2084     /**
2085      * Resets zoom factor to 100%.
2086      * @returns {JXG.Board} Reference to the board
2087      */
2088     zoom100: function () {
2089         var bb = this.getBoundingBox(),
2090             dX = (bb[2]-bb[0])*(1.0-this.zoomX)*0.5,
2091             dY = (bb[1]-bb[3])*(1.0-this.zoomY)*0.5;
2092 
2093         this.setBoundingBox([bb[0]+dX, bb[1]-dY, bb[2]-dX, bb[3]+dY], false);
2094         this.zoomX = 1.0;
2095         this.zoomY = 1.0;
2096         this.applyZoom();
2097         return this;
2098     },
2099 
2100     /**
2101      * Zooms the board so every visible point is shown. Keeps aspect ratio.
2102      * @returns {JXG.Board} Reference to the board
2103      */
2104     zoomAllPoints: function () {
2105         var minX = 0, // (0,0) shall be visible, too
2106             maxX = 0,
2107             minY = 0,
2108             maxY = 0,
2109             el, border, borderX, borderY;
2110 
2111         for (el in this.objects) {
2112             if (JXG.isPoint(this.objects[el]) && this.objects[el].visProp.visible) {
2113                 if (this.objects[el].coords.usrCoords[1] < minX) {
2114                     minX = this.objects[el].coords.usrCoords[1];
2115                 } else if (this.objects[el].coords.usrCoords[1] > maxX) {
2116                     maxX = this.objects[el].coords.usrCoords[1];
2117                 }
2118                 if (this.objects[el].coords.usrCoords[2] > maxY) {
2119                     maxY = this.objects[el].coords.usrCoords[2];
2120                 } else if (this.objects[el].coords.usrCoords[2] < minY) {
2121                     minY = this.objects[el].coords.usrCoords[2];
2122                 }
2123             }
2124         }
2125 
2126         border = 50;
2127         borderX = border/(this.unitX);
2128         borderY = border/(this.unitY);
2129 
2130         this.zoomX = 1.0;
2131         this.zoomY = 1.0;
2132 
2133         this.setBoundingBox([minX-borderX, maxY+borderY, maxX+borderX, minY-borderY], true);
2134 
2135         this.applyZoom();
2136 
2137         return this;
2138     },
2139 
2140     /**
2141      * Reset the bounding box and the zoom level to 100% such that a given set of elements is within the board's viewport.
2142      * @param {Array} elements A set of elements given by id, reference, or name.
2143      * @returns {JXG.Board} Reference to the board.
2144      */
2145     zoomElements: function (elements) {
2146         var i, j, e, box,
2147             newBBox = [0, 0, 0, 0],
2148             dir = [1, -1, -1, 1];
2149 
2150         if (!JXG.isArray(elements) || elements.length === 0) {
2151             return this;
2152         }
2153 
2154         for (i = 0; i < elements.length; i++) {
2155             e = JXG.getRef(this, elements[i]);
2156 
2157             box = e.bounds();
2158             if (JXG.isArray(box)) {
2159                 if (JXG.isArray(newBBox)) {
2160                     for (j = 0; j < 4; j++) {
2161                         if (dir[j]*box[j] < dir[j]*newBBox[j]) {
2162                             newBBox[j] = box[j];
2163                         }
2164                     }
2165                 } else {
2166                     newBBox = box;
2167                 }
2168             }
2169         }
2170 
2171         if (JXG.isArray(newBBox)) {
2172             for (j = 0; j < 4; j++) {
2173                 newBBox[j] -= dir[j];
2174             }
2175 
2176             this.zoomX = 1.0;
2177             this.zoomY = 1.0;
2178             this.setBoundingBox(newBBox, true);
2179         }
2180 
2181         return this;
2182     },
2183 
2184     /**
2185      * Sets the zoom level to <tt>fX</tt> resp <tt>fY</tt>.
2186      * @param {Number} fX
2187      * @param {Number} fY
2188      * @returns {JXG.Board}
2189      */
2190     setZoom: function (fX, fY) {
2191         var oX = this.options.zoom.factorX, oY = this.options.zoom.factorY;
2192 
2193         this.options.zoom.factorX = fX/this.zoomX;
2194         this.options.zoom.factorY = fY/this.zoomY;
2195 
2196         this.zoomIn();
2197 
2198         this.options.zoom.factorX = oX;
2199         this.options.zoom.factorY = oY;
2200 
2201         return this;
2202     },
2203 
2204     /**
2205      * Removes object from board and renderer.
2206      * @param {JXG.GeometryElement} object The object to remove.
2207      * @returns {JXG.Board} Reference to the board
2208      */
2209     removeObject: function (object) {
2210         var el, i;
2211 
2212         if (JXG.isArray(object)) {
2213             for (i=0; i<object.length; i++)
2214                 this.removeObject(object[i]);
2215         }
2216 
2217         object = JXG.getReference(this, object);
2218 
2219         // If the object which is about to be removed unknown, do nothing.
2220         if (!JXG.exists(object)) {
2221             return this;
2222         }
2223 
2224         try {
2225             // remove all children.
2226             for (el in object.childElements) {
2227                 object.childElements[el].board.removeObject(object.childElements[el]);
2228             }
2229 
2230             for (el in this.objects) {
2231                 if (JXG.exists(this.objects[el].childElements))
2232                     delete(this.objects[el].childElements[object.id]);
2233             }
2234 
2235             // remove the object itself from our control structures
2236             delete(this.objects[object.id]);
2237             delete(this.elementsByName[object.name]);
2238 
2239             if (object.visProp.trace) {
2240                 object.clearTrace();
2241             }
2242 
2243             // the object deletion itself is handled by the object.
2244             if (JXG.exists(object.remove)) object.remove();
2245         } catch(e) {
2246             JXG.debug(object.id + ': Could not be removed: ' + e);
2247         }
2248 
2249         return this;
2250     },
2251 
2252 
2253     /**
2254      * Removes the ancestors of an object an the object itself from board and renderer.
2255      * @param {JXG.GeometryElement} object The object to remove.
2256      * @returns {JXG.Board} Reference to the board
2257      */
2258     removeAncestors: function (object) {
2259         for (var anc in object.ancestors)
2260             this.removeAncestors(object.ancestors[anc]);
2261         this.removeObject(object);
2262 
2263         return this;
2264     },
2265 
2266     /**
2267      * Initialize some objects which are contained in every GEONExT construction by default,
2268      * but are not contained in the gxt files.
2269      * @returns {JXG.Board} Reference to the board
2270      */
2271     initGeonextBoard: function () {
2272         var p1, p2, p3, l1, l2;
2273 
2274         p1 = this.create('point', [0, 0], {
2275             id: this.id + 'g00e0',
2276             name: 'Ursprung',
2277             withLabel: false,
2278             visible: false,
2279             fixed: true
2280         });
2281 
2282         p2 = this.create('point', [1, 0], {
2283             id: this.id + 'gX0e0',
2284             name: 'Punkt_1_0',
2285             withLabel: false,
2286             visible: false,
2287             fixed: true
2288         });
2289 
2290         p3 = this.create('point', [0, 1], {
2291             id: this.id + 'gY0e0',
2292             name: 'Punkt_0_1',
2293             withLabel: false,
2294             visible: false,
2295             fixed: true
2296         });
2297 
2298         l1 = this.create('line', [p1, p2], {
2299             id: this.id + 'gXLe0',
2300             name: 'X-Achse',
2301             withLabel: false,
2302             visible: false
2303         });
2304 
2305         l2 = this.create('line', [p1, p3], {
2306             id: this.id + 'gYLe0',
2307             name: 'Y-Achse',
2308             withLabel: false,
2309             visible: false
2310         });
2311 
2312         return this;
2313     },
2314 
2315     /**
2316      * Initialize the info box object which is used to display
2317      * the coordinates of points near the mouse pointer,
2318      * @returns {JXG.Board} Reference to the board
2319      */
2320     initInfobox: function () {
2321         this.infobox = this.create('text', [0, 0, '0,0'], {
2322             id: this.id + '_infobox',
2323             display: 'html',
2324             fixed: true
2325         });
2326         this.infobox.distanceX = -20;
2327         this.infobox.distanceY = 25;
2328 
2329         this.infobox.dump = false;
2330 
2331         this.renderer.hide(this.infobox);
2332         return this;
2333     },
2334 
2335     /**
2336      * Change the height and width of the board's container.
2337      * @param {Number} canvasWidth New width of the container.
2338      * @param {Number} canvasHeight New height of the container.
2339      * @returns {JXG.Board} Reference to the board
2340      */
2341     resizeContainer: function (canvasWidth, canvasHeight) {
2342         this.canvasWidth = parseFloat(canvasWidth);
2343         this.canvasHeight = parseFloat(canvasHeight);
2344         this.containerObj.style.width = (this.canvasWidth) + 'px';
2345         this.containerObj.style.height = (this.canvasHeight) + 'px';
2346 
2347         this.renderer.resize(this.canvasWidth, this.canvasHeight);
2348 
2349         return this;
2350     },
2351 
2352     /**
2353      * Lists the dependencies graph in a new HTML-window.
2354      * @returns {JXG.Board} Reference to the board
2355      */
2356     showDependencies: function () {
2357         var el, t, c, f, i;
2358 
2359         t = '<p>\n';
2360         for (el in this.objects) {
2361             i = 0;
2362             for (c in this.objects[el].childElements) {
2363                 i++;
2364             }
2365             if (i>=0) {
2366                 t += '<b>' + this.objects[el].id + ':<'+'/b> ';
2367             }
2368             for (c in this.objects[el].childElements) {
2369                 t += this.objects[el].childElements[c].id+'('+this.objects[el].childElements[c].name+')'+', ';
2370             }
2371             t += '<p>\n';
2372         }
2373         t += '<'+'/p>\n';
2374         f = window.open();
2375         f.document.open();
2376         f.document.write(t);
2377         f.document.close();
2378         return this;
2379     },
2380 
2381     /**
2382      * Lists the XML code of the construction in a new HTML-window.
2383      * @returns {JXG.Board} Reference to the board
2384      */
2385     showXML: function () {
2386         var f = window.open('');
2387         f.document.open();
2388         f.document.write('<pre>'+JXG.escapeHTML(this.xmlString)+'<'+'/pre>');
2389         f.document.close();
2390         return this;
2391     },
2392 
2393     /**
2394      * Sets for all objects the needsUpdate flag to "true".
2395      * @returns {JXG.Board} Reference to the board
2396      */
2397     prepareUpdate: function () {
2398         var el, pEl;
2399         for (el in this.objects) {
2400             pEl = this.objects[el];
2401             if (!this.needsFullUpdate && !pEl.needsRegularUpdate) { continue; }
2402             pEl.needsUpdate = true;
2403         }
2404         return this;
2405     },
2406 
2407     /**
2408      * Runs through all elements and calls their update() method.
2409      * @param {JXG.GeometryElement} drag Element that caused the update.
2410      * @returns {JXG.Board} Reference to the board
2411      */
2412     updateElements: function (drag) {
2413         var el, pEl;
2414 
2415         drag = JXG.getRef(this, drag);
2416         // if (drag==null) { isBeforeDrag = false; }
2417         for (el in this.objects) {
2418             pEl = this.objects[el];
2419             // For updates of an element we distinguish if the dragged element is updated or
2420             // other elements are updated.
2421             // The difference lies in the treatment of gliders.
2422             if (drag==null || pEl.id!=drag.id) {
2423                 pEl.update(true);   // an element following the dragged element is updated
2424             } else {
2425                 pEl.update(false);  // the dragged object itself is updated
2426             }
2427         }
2428         return this;
2429     },
2430 
2431     /**
2432      * Runs through all elements and calls their update() method.
2433      * @param {JXG.GeometryElement} drag Element that caused the update.
2434      * @returns {JXG.Board} Reference to the board
2435      */
2436     updateRenderer: function (drag) {
2437         var el, pEl;
2438         if (this.options.renderer=='canvas') {
2439             this.updateRendererCanvas(drag);
2440         } else {
2441             for (el in this.objects) {
2442                 pEl = this.objects[el];
2443                 //if ( !this.needsFullUpdate && (/*isBeforeDrag ||*/ !pEl.needsRegularUpdate) ) { continue; }
2444                 pEl.updateRenderer();
2445             }
2446         }
2447         return this;
2448     },
2449 
2450     /**
2451      * Runs through all elements and calls their update() method.
2452      * This is a special version for the CanvasRenderer.
2453      * Here, we have to do our own layer handling.
2454      * @param {JXG.GeometryElement} drag Element that caused the update.
2455      * @returns {JXG.Board} Reference to the board
2456      */
2457     updateRendererCanvas: function (drag) {
2458         var el, pEl, i,
2459             layers = this.options.layer,
2460             len = this.options.layer.numlayers,
2461             last = Number.NEGATIVE_INFINITY, mini, la;
2462 
2463         for (i=0;i<len;i++) {
2464             mini = Number.POSITIVE_INFINITY;
2465             for (la in layers) {
2466                 if (layers[la]>last && layers[la]<mini) {
2467                     mini = layers[la];
2468                 }
2469             }
2470             last = mini;
2471             for (el in this.objects) {
2472                 pEl = this.objects[el];
2473                 if (pEl.visProp.layer === mini) {
2474                     pEl.prepareUpdate().updateRenderer();
2475                 }
2476             }
2477         }
2478         return this;
2479     },
2480 
2481     /**
2482      * Adds a hook to this board. A hook is a function which will be called on every board update.
2483      * @param {Function} hook A function to be called by the board after an update occured.
2484      * @param {String} [m='update'] When the hook is to be called. Possible values are <i>mouseup</i>, <i>mousedown</i> and <i>update</i>.
2485      * @param {Object} [context=board] Determines the execution context the hook is called. This parameter is optional, default is the
2486      * board object the hook is attached to.
2487      * @returns {Number} Id of the hook, required to remove the hook from the board.
2488      */
2489     addHook: function (hook, m, context) {
2490         if (!JXG.exists(m))
2491             m = 'update';
2492 
2493         context = context || this;
2494         this.hooks.push({
2495             fn: hook,
2496             mode: m,
2497             context: context
2498         });
2499 
2500         if (m=='update') {
2501             hook.apply(context, [this]);
2502         }
2503 
2504         return (this.hooks.length-1);
2505     },
2506 
2507     /**
2508      * Removes a hook from the board.
2509      * @param {Number} id Id for the hook. The number you got when you added the hook.
2510      * @returns {JXG.Board} Reference to the board
2511      */
2512     removeHook: function (id) {
2513         this.hooks[id] = null;
2514         return this;
2515     },
2516 
2517     /**
2518      * Runs through all hooked functions and calls them.
2519      * @returns {JXG.Board} Reference to the board
2520      */
2521     updateHooks: function (m) {
2522         var i, j, len, lenh, args = arguments.length > 1 ? Array.prototype.slice.call(arguments, 1) : [];
2523 
2524         if (!JXG.exists(m))
2525             m = ['update'];
2526 
2527         if (!JXG.isArray(m)) {
2528             m = [m];
2529         }
2530 
2531         len = m.length;
2532         lenh = this.hooks.length;
2533         for (j = 0; j < len; j++) {
2534             for (i = 0; i < lenh; i++) {
2535                 if ((this.hooks[i] != null) && (this.hooks[i].mode == m[j])) {
2536                     this.hooks[i].fn.apply(this.hooks[i].context, args);
2537                 }
2538             }
2539         }
2540 
2541         return this;
2542     },
2543 
2544     /**
2545      * Adds a dependent board to this board.
2546      * @param {JXG.Board} board A reference to board which will be updated after an update of this board occured.
2547      * @returns {JXG.Board} Reference to the board
2548      */
2549     addChild: function (board) {
2550         this.dependentBoards.push(board);
2551         this.update();
2552         return this;
2553     },
2554 
2555     /**
2556      * Deletes a board from the list of dependent boards.
2557      * @param {JXG.Board} board Reference to the board which will be removed.
2558      * @returns {JXG.Board} Reference to the board
2559      */
2560     removeChild: function (board) {
2561         var i;
2562         for (i=this.dependentBoards.length-1; i>=0; i--) {
2563             if (this.dependentBoards[i] == board) {
2564                 this.dependentBoards.splice(i,1);
2565             }
2566         }
2567         return this;
2568     },
2569 
2570     /**
2571      * Runs through most elements and calls their update() method and update the conditions.
2572      * @param {Object} drag Element that caused the update.
2573      * @returns {JXG.Board} Reference to the board
2574      */
2575     update: function (drag) {
2576         var i, len, boardId, b;
2577 
2578         if (this.inUpdate || this.isSuspendedUpdate) {
2579             return this;
2580         }
2581         this.inUpdate = true;
2582 
2583         this.prepareUpdate(drag).updateElements(drag).updateConditions();
2584         this.renderer.suspendRedraw(this);
2585         this.updateRenderer(drag);
2586         this.renderer.unsuspendRedraw();
2587         this.updateHooks();
2588 
2589         // To resolve dependencies between boards
2590         //for (var board in JXG.JSXGraph.boards) {
2591         len = this.dependentBoards.length;
2592         for (i=0; i<len; i++) {
2593             boardId = this.dependentBoards[i].id;
2594             b = JXG.JSXGraph.boards[boardId];
2595             if ( b != this) {
2596                 b.updateQuality = this.updateQuality;
2597                 b.prepareUpdate().updateElements().updateConditions();
2598                 b.renderer.suspendRedraw();
2599                 b.updateRenderer();
2600                 b.renderer.unsuspendRedraw();
2601                 b.updateHooks();
2602             }
2603 
2604         }
2605 
2606         this.inUpdate = false;
2607         return this;
2608     },
2609 
2610     /**
2611      * Runs through all elements and calls their update() method and update the conditions.
2612      * This is necessary after zooming and changing the bounding box.
2613      * @returns {JXG.Board} Reference to the board
2614      */
2615     fullUpdate: function () {
2616         this.needsFullUpdate = true;
2617         this.update();
2618         this.needsFullUpdate = false;
2619         return this;
2620     },
2621 
2622     /**
2623      * Adds a grid to the board according to the settings given in board.options.
2624      * @returns {JXG.Board} Reference to the board.
2625      */
2626     addGrid: function () {
2627         this.create('grid', []);
2628 
2629         return this;
2630     },
2631 
2632     /**
2633      * Removes all grids assigned to this board. Warning: This method also removes all objects depending on one or
2634      * more of the grids.
2635      * @returns {JXG.Board} Reference to the board object.
2636      */
2637     removeGrids: function () {
2638         var i;
2639 
2640         for (i = 0; i < this.grids.length; i++) {
2641             this.removeObject(this.grids[i]);
2642         }
2643 
2644         this.grids.length = 0;
2645         this.update(); // needed for canvas renderer
2646 
2647         return this;
2648     },
2649 
2650     /**
2651      * Creates a new geometric element of type elementType.
2652      * @param {String} elementType Type of the element to be constructed given as a string e.g. 'point' or 'circle'.
2653      * @param {Array} parents Array of parent elements needed to construct the element e.g. coordinates for a point or two
2654      * points to construct a line. This highly depends on the elementType that is constructed. See the corresponding JXG.create*
2655      * methods for a list of possible parameters.
2656      * @param {Object} attributes An object containing the attributes to be set. This also depends on the elementType.
2657      * Common attributes are name, visible, strokeColor.
2658      * @returns {Object} Reference to the created element. This is usually a GeometryElement, but can be an array containing
2659      * two or more elements.
2660      */
2661     create: function (elementType, parents, attributes) {
2662         var el, i;
2663 
2664         elementType = elementType.toLowerCase();
2665 
2666         if (!JXG.exists(parents)) {
2667             parents = [];
2668         }
2669 
2670         if (!JXG.exists(attributes)) {
2671             attributes = {};
2672         }
2673 
2674         for (i = 0; i < parents.length; i++) {
2675             if (elementType != 'text' || i!=2) {
2676                 parents[i] = JXG.getReference(this, parents[i]);
2677             }
2678         }
2679 
2680         if (JXG.JSXGraph.elements[elementType] != null) {
2681             if (typeof JXG.JSXGraph.elements[elementType] == 'function') {
2682                 el = JXG.JSXGraph.elements[elementType](this, parents, attributes);
2683             } else {
2684                 el = JXG.JSXGraph.elements[elementType].creator(this, parents, attributes);
2685             }
2686         } else {
2687             throw new Error("JSXGraph: JXG.createElement: Unknown element type given: " + elementType);
2688         }
2689 
2690         if (!JXG.exists(el)) {
2691             JXG.debug("JSXGraph: JXG.createElement: failure creating " + elementType);
2692             return el;
2693         }
2694 
2695         if (el.prepareUpdate && el.update && el.updateRenderer) {
2696             el.prepareUpdate().update().updateRenderer();
2697         }
2698         return el;
2699     },
2700 
2701     /**
2702      * Deprecated name for {@link JXG.Board#create}.
2703      * @deprecated
2704      */
2705     createElement: JXG.shortcut(JXG.Board.prototype, 'create'),
2706 
2707 
2708     /**
2709      * Delete the elements drawn as part of a trace of an element.
2710      * @returns {JXG.Board} Reference to the board
2711      */
2712     clearTraces: function () {
2713         var el;
2714 
2715         for (el in this.objects) {
2716             this.objects[el].clearTrace();
2717         }
2718         this.numTraces = 0;
2719         return this;
2720     },
2721 
2722     /**
2723      * Stop updates of the board.
2724      * @returns {JXG.Board} Reference to the board
2725      */
2726     suspendUpdate: function () {
2727         this.isSuspendedUpdate = true;
2728         return this;
2729     },
2730 
2731     /**
2732      * Enable updates of the board.
2733      * @returns {JXG.Board} Reference to the board
2734      */
2735     unsuspendUpdate: function () {
2736         this.isSuspendedUpdate = false;
2737         this.update();
2738         return this;
2739     },
2740 
2741     /**
2742      * Set the bounding box of the board.
2743      * @param {Array} bbox New bounding box [x1,y1,x2,y2]
2744      * @param {Boolean} [keepaspectratio=false] If set to true, the aspect ratio will be 1:1, but
2745      * the resulting viewport may be larger.
2746      * @returns {JXG.Board} Reference to the board
2747      */
2748     setBoundingBox: function (bbox, keepaspectratio) {
2749         if (!JXG.isArray(bbox)) {
2750             return this;
2751         }
2752 
2753         var h, w,
2754             dim = JXG.getDimensions(this.container);
2755 
2756         this.canvasWidth = parseInt(dim.width);
2757         this.canvasHeight = parseInt(dim.height);
2758         w = this.canvasWidth;
2759         h = this.canvasHeight;
2760         if (keepaspectratio) {
2761             this.unitX = w/(bbox[2]-bbox[0]);
2762             this.unitY = h/(bbox[1]-bbox[3]);
2763             if (Math.abs(this.unitX)<Math.abs(this.unitY)) {
2764                 this.unitY = Math.abs(this.unitX)*this.unitY/Math.abs(this.unitY);
2765             } else {
2766                 this.unitX = Math.abs(this.unitY)*this.unitX/Math.abs(this.unitX);
2767             }
2768         } else {
2769             this.unitX = w/(bbox[2]-bbox[0]);
2770             this.unitY = h/(bbox[1]-bbox[3]);
2771         }
2772 
2773         this.moveOrigin(-this.unitX*bbox[0], this.unitY*bbox[1]);
2774         return this;
2775     },
2776 
2777     /**
2778      * Get the bounding box of the board.
2779      * @returns {Array} bounding box [x1,y1,x2,y2] upper left corner, lower right corner
2780      */
2781     getBoundingBox: function () {
2782         var ul = new JXG.Coords(JXG.COORDS_BY_SCREEN, [0,0], this),
2783             lr = new JXG.Coords(JXG.COORDS_BY_SCREEN, [this.canvasWidth, this.canvasHeight], this);
2784         return [ul.usrCoords[1],ul.usrCoords[2],lr.usrCoords[1],lr.usrCoords[2]];
2785     },
2786 
2787     /**
2788      * Adds an animation. Animations are controlled by the boards, so the boards need to be aware of the
2789      * animated elements. This function tells the board about new elements to animate.
2790      * @param {JXG.GeometryElement} element The element which is to be animated.
2791      * @returns {JXG.Board} Reference to the board
2792      */
2793     addAnimation: function (element) {
2794         this.animationObjects[element.id] = element;
2795 
2796         if (!this.animationIntervalCode) {
2797             this.animationIntervalCode = window.setInterval('JXG.JSXGraph.boards[\'' + this.id + '\'].animate();', 35);
2798         }
2799 
2800         return this;
2801     },
2802 
2803     /**
2804      * Cancels all running animations.
2805      * @returns {JXG.Board} Reference to the board
2806      */
2807     stopAllAnimation: function () {
2808         var el;
2809 
2810         for (el in this.animationObjects) {
2811             if (this.animationObjects[el] === null)
2812                 continue;
2813 
2814             this.animationObjects[el] = null;
2815             delete(this.animationObjects[el]);
2816         }
2817 
2818         window.clearInterval(this.animationIntervalCode);
2819         delete(this.animationIntervalCode);
2820 
2821         return this;
2822     },
2823 
2824     /**
2825      * General purpose animation function. This currently only supports moving points from one place to another. This
2826      * is faster than managing the animation per point, especially if there is more than one animated point at the same time.
2827      * @returns {JXG.Board} Reference to the board
2828      */
2829     animate: function () {
2830         var count = 0,
2831             el, o, newCoords, r, p, c,
2832             obj=null, cbtmp;
2833 
2834         for (el in this.animationObjects) {
2835             if (this.animationObjects[el] === null)
2836                 continue;
2837 
2838             count++;
2839             o = this.animationObjects[el];
2840             if (o.animationPath) {
2841                 if (JXG.isFunction (o.animationPath)) {
2842                     newCoords = o.animationPath(new Date().getTime() - o.animationStart);
2843                 } else {
2844                     newCoords = o.animationPath.pop();
2845                 }
2846 
2847                 if ((!JXG.exists(newCoords)) || (!JXG.isArray(newCoords) && isNaN(newCoords))) {
2848                     delete(o.animationPath);
2849                 } else {
2850                     //o.setPositionByTransform(JXG.COORDS_BY_USER, newCoords[0] - o.coords.usrCoords[1], newCoords[1] - o.coords.usrCoords[2]);
2851                     o.setPositionDirectly(JXG.COORDS_BY_USER, newCoords[0], newCoords[1]);
2852                     //this.update(o);  // May slow down the animation, but is important
2853                     // for dependent glider objects (see tangram.html).
2854                     // Otherwise the intended projection may be incorrect.
2855                     o.prepareUpdate().update().updateRenderer();
2856                     obj = o;
2857                 }
2858             }
2859             if (o.animationData) {
2860                 c = 0;
2861                 for (r in o.animationData) {
2862                     p = o.animationData[r].pop();
2863                     if (!JXG.exists(p)) {
2864                         delete(o.animationData[p]);
2865                     } else {
2866                         c++;
2867                         o.setProperty(r + ':' + p);
2868                     }
2869                 }
2870                 if (c==0)
2871                     delete(o.animationData);
2872             }
2873 
2874             if (!JXG.exists(o.animationData) && !JXG.exists(o.animationPath)) {
2875                 this.animationObjects[el] = null;
2876                 delete(this.animationObjects[el]);
2877                 if (JXG.exists(o.animationCallback)) {
2878                     cbtmp = o.animationCallback;
2879                     o.animationCallback = null;
2880                     cbtmp();
2881                 }
2882             }
2883         }
2884 
2885         if (count == 0) {
2886             window.clearInterval(this.animationIntervalCode);
2887             delete(this.animationIntervalCode);
2888         } else {
2889             this.update(obj);
2890         }
2891 
2892         return this;
2893     },
2894 
2895     /**
2896      * Initializes color blindness simulation.
2897      * @param {String} deficiency Describes the color blindness deficiency which is simulated. Accepted values are 'protanopia', 'deuteranopia', and 'tritanopia'.
2898      * @returns {JXG.Board} Reference to the board
2899      */
2900     emulateColorblindness: function (deficiency) {
2901         var e, o, brd=this;
2902 
2903         if (!JXG.exists(deficiency))
2904             deficiency = 'none';
2905 
2906         if (this.currentCBDef == deficiency)
2907             return this;
2908 
2909         for (e in brd.objects) {
2910             o = brd.objects[e];
2911             if (deficiency != 'none') {
2912                 if (this.currentCBDef == 'none') {
2913                     // this could be accomplished by JXG.extend, too. But do not use
2914                     // JXG.deepCopy as this could result in an infinite loop because in
2915                     // visProp there could be geometry elements which contain the board which
2916                     // contains all objects which contain board etc.
2917                     o.visPropOriginal = {
2918                         strokecolor: o.visProp.strokecolor,
2919                         fillcolor: o.visProp.fillcolor,
2920                         highlightstrokecolor: o.visProp.highlightstrokecolor,
2921                         highlightfillcolor: o.visProp.highlightfillcolor
2922                     };
2923                 }
2924                 o.setProperty({
2925                     strokecolor: JXG.rgb2cb(o.visPropOriginal.strokecolor, deficiency),
2926                     fillcolor: JXG.rgb2cb(o.visPropOriginal.fillcolor, deficiency),
2927                     highlightstrokecolor: JXG.rgb2cb(o.visPropOriginal.highlightstrokecolor, deficiency),
2928                     highlightfillcolor: JXG.rgb2cb(o.visPropOriginal.highlightfillcolor, deficiency)
2929                 });
2930             } else if (JXG.exists(o.visPropOriginal)) {
2931                 JXG.extend(o.visProp, o.visPropOriginal);
2932             }
2933         }
2934         this.currentCBDef = deficiency;
2935         this.update();
2936 
2937         return this;
2938     },
2939 
2940     /**
2941      * Return all elements that somehow depend on the element <tt>root</tt> and satisfy one of the <tt>filter</tt> rules.
2942      * <tt>filters</tt> are objects which's properties are compared to every element found in the dependency tree.
2943      * @param {JXG.GeometryElement} root Dependency tree root element
2944      * @param {Object} filters An arbitrary amount of objects which define filters for the elements to return. Only elements
2945      * that fulfill at least one filter are returned. The comparison is a direct comparison, i.e. nested objects won't be
2946      * compared.
2947      * @example
2948      * // This will return only points
2949      * var partPoints = board.getPartialConstruction(p, {elementClass: JXG.OBJECT_CLASS_POINT});
2950      *
2951      * // This will return only points and lines
2952      * var partPointsLines = board.getPartialConstruction(p, {elementClass: JXG.OBJECT_CLASS_POINT}, {elementClass: JXG.OBJECT_CLASS_LINE});
2953      */
2954     getPartialConstruction: function (root) {
2955         var filters, i;
2956 
2957         for (i = 1; i < arguments.length; i++) {
2958             filters.push(arguments[i]);
2959         }
2960     },
2961 
2962     /**
2963      * Function to animate a curve rolling on another curve.
2964      * @param {Curve} c1 JSXGraph curve building the floor where c2 rolls
2965      * @param {Curve} c2 JSXGraph curve which rolls on c1.
2966      * @param {number} start_c1 The parameter t such that c1(t) touches c2. This is the start position of the
2967      *                          rolling process
2968      * @param {Number} stepsize Increase in t in each step for the curve c1
2969      * @param {Number} time Delay time for setInterval()
2970      * @returns {Array} pointlist Array of points which are rolled in each step. This list should contain
2971      *      all points which define c2 and gliders on c2.
2972      *
2973      * @example
2974      *
2975      * // Line which will be the floor to roll upon.
2976      * var line = brd.create('curve', [function (t) { return t;}, function (t){ return 1;}], {strokeWidth:6});
2977      * // Center of the rolling circle
2978      * var C = brd.create('point',[0,2],{name:'C'});
2979      * // Starting point of the rolling circle
2980      * var P = brd.create('point',[0,1],{name:'P', trace:true});
2981      * // Circle defined as a curve. The circle "starts" at P, i.e. circle(0) = P
2982      * var circle = brd.create('curve',[
2983      *           function (t){var d = P.Dist(C),
2984      *                           beta = JXG.Math.Geometry.rad([C.X()+1,C.Y()],C,P);
2985      *                       t += beta;
2986      *                       return C.X()+d*Math.cos(t);
2987      *           },
2988      *           function (t){var d = P.Dist(C),
2989      *                           beta = JXG.Math.Geometry.rad([C.X()+1,C.Y()],C,P);
2990      *                       t += beta;
2991      *                       return C.Y()+d*Math.sin(t);
2992      *           },
2993      *           0,2*Math.PI],
2994      *           {strokeWidth:6, strokeColor:'green'});
2995      *
2996      * // Point on circle
2997      * var B = brd.create('glider',[0,2,circle],{name:'B', color:'blue',trace:false});
2998      * var roll = brd.createRoulette(line, circle, 0, Math.PI/20, 1, 100, [C,P,B]);
2999      * roll.start() // Start the rolling, to be stopped by roll.stop()
3000      *
3001      * </pre><div id="e5e1b53c-a036-4a46-9e35-190d196beca5" style="width: 300px; height: 300px;"></div>
3002      * <script type="text/javascript">
3003      * var brd = JXG.JSXGraph.initBoard('e5e1b53c-a036-4a46-9e35-190d196beca5', {boundingbox: [-5, 5, 5, -5], axis: true, showcopyright:false, shownavigation: false});
3004      * // Line which will be the floor to roll upon.
3005      * var line = brd.create('curve', [function (t) { return t;}, function (t){ return 1;}], {strokeWidth:6});
3006      * // Center of the rolling circle
3007      * var C = brd.create('point',[0,2],{name:'C'});
3008      * // Starting point of the rolling circle
3009      * var P = brd.create('point',[0,1],{name:'P', trace:true});
3010      * // Circle defined as a curve. The circle "starts" at P, i.e. circle(0) = P
3011      * var circle = brd.create('curve',[
3012      *           function (t){var d = P.Dist(C),
3013      *                           beta = JXG.Math.Geometry.rad([C.X()+1,C.Y()],C,P);
3014      *                       t += beta;
3015      *                       return C.X()+d*Math.cos(t);
3016      *           },
3017      *           function (t){var d = P.Dist(C),
3018      *                           beta = JXG.Math.Geometry.rad([C.X()+1,C.Y()],C,P);
3019      *                       t += beta;
3020      *                       return C.Y()+d*Math.sin(t);
3021      *           },
3022      *           0,2*Math.PI],
3023      *           {strokeWidth:6, strokeColor:'green'});
3024      *
3025      * // Point on circle
3026      * var B = brd.create('glider',[0,2,circle],{name:'B', color:'blue',trace:false});
3027      * var roll = brd.createRoulette(line, circle, 0, Math.PI/20, 1, 100, [C,P,B]);
3028      * roll.start() // Start the rolling, to be stopped by roll.stop()
3029      * </script><pre>
3030      *
3031      */
3032     createRoulette: function (c1, c2, start_c1, stepsize, direction, time, pointlist) {
3033         var brd = this;
3034         var Roulette = function () {
3035             var alpha = 0, Tx = 0, Ty = 0,
3036                 t1 = start_c1,
3037                 t2 = JXG.Math.Numerics.root(
3038                     function (t) {
3039                         var c1x = c1.X(t1),
3040                             c1y = c1.Y(t1),
3041                             c2x = c2.X(t),
3042                             c2y = c2.Y(t);
3043                         return (c1x-c2x)*(c1x-c2x) + (c1y-c2y)*(c1y-c2y);
3044                     },
3045                     [0,Math.PI*2]),
3046                 t1_new = 0.0, t2_new = 0.0,
3047                 c1dist,
3048                 rotation = brd.create('transform',[function (){ return alpha;}], {type:'rotate'}),
3049                 rotationLocal = brd.create('transform',[function (){ return alpha;},
3050                     function (){ return c1.X(t1);},
3051                     function (){ return c1.Y(t1);}],
3052                 {type:'rotate'}),
3053                 translate = brd.create('transform',[function (){ return Tx;}, function (){ return Ty;}], {type:'translate'}),
3054 
3055                 //
3056                 // arc length via Simpson's rule.
3057                 arclen = function (c,a,b) {
3058                     var cpxa = JXG.Math.Numerics.D(c.X)(a), cpya = JXG.Math.Numerics.D(c.Y)(a),
3059                         cpxb = JXG.Math.Numerics.D(c.X)(b), cpyb = JXG.Math.Numerics.D(c.Y)(b),
3060                         cpxab = JXG.Math.Numerics.D(c.X)((a+b)*0.5), cpyab = JXG.Math.Numerics.D(c.Y)((a+b)*0.5),
3061                         fa = Math.sqrt(cpxa*cpxa+cpya*cpya),
3062                         fb = Math.sqrt(cpxb*cpxb+cpyb*cpyb),
3063                         fab = Math.sqrt(cpxab*cpxab+cpyab*cpyab);
3064                     return (fa+4*fab+fb)*(b-a)/6.0;
3065                 },
3066                 exactDist = function (t) {
3067                     return c1dist - arclen(c2,t2,t);
3068                 },
3069                 beta = Math.PI/18.0,
3070                 beta9 = beta*9,
3071                 interval = null;
3072 
3073             this.rolling = function (){
3074                 t1_new = t1+direction*stepsize;
3075                 c1dist = arclen(c1,t1,t1_new);             // arc length between c1(t1) and c1(t1_new)
3076                 t2_new = JXG.Math.Numerics.root(exactDist, t2);
3077                 // find t2_new such that arc length between c2(t2) and c1(t2_new)
3078                 // equals c1dist.
3079 
3080                 var h = new JXG.Complex(c1.X(t1_new),c1.Y(t1_new));    // c1(t) as complex number
3081                 var g = new JXG.Complex(c2.X(t2_new),c2.Y(t2_new));    // c2(t) as complex number
3082                 var hp = new JXG.Complex(JXG.Math.Numerics.D(c1.X)(t1_new),JXG.Math.Numerics.D(c1.Y)(t1_new));
3083                 var gp = new JXG.Complex(JXG.Math.Numerics.D(c2.X)(t2_new),JXG.Math.Numerics.D(c2.Y)(t2_new));
3084                 var z = JXG.C.div(hp,gp);                  // z is angle between the tangents of
3085                 // c1 at t1_new, and c2 at t2_new
3086                 alpha = Math.atan2(z.imaginary, z.real);
3087                 z.div(JXG.C.abs(z));                       // Normalizing the quotient
3088                 z.mult(g);
3089                 Tx = h.real-z.real;
3090                 Ty = h.imaginary-z.imaginary;              // T = h(t1_new)-g(t2_new)*h'(t1_new)/g'(t2_new);
3091 
3092                 if (alpha <-beta && alpha>-beta9) {        // -(10-90) degrees: make corners roll smoothly
3093                     alpha = -beta;
3094                     rotationLocal.applyOnce(pointlist);
3095                 } else if (alpha>beta && alpha<beta9) {
3096                     alpha = beta;
3097                     rotationLocal.applyOnce(pointlist);
3098                 } else {
3099                     rotation.applyOnce(pointlist);
3100                     translate.applyOnce(pointlist);
3101                     t1 = t1_new;
3102                     t2 = t2_new;
3103                 }
3104                 brd.update();
3105             };
3106 
3107             this.start = function () {
3108                 if (time>0) {
3109                     interval = setInterval(this.rolling, time);
3110                 }
3111                 return this;
3112             };
3113 
3114             this.stop = function () {
3115                 clearInterval(interval);
3116                 return this;
3117             };
3118             return this;
3119         };
3120         return new Roulette();
3121     }
3122 });
3123