1 /*
  2     Copyright 2008-2016
  3         Matthias Ehmann,
  4         Michael Gerhaeuser,
  5         Carsten Miller,
  6         Bianca Valentin,
  7         Alfred Wassermann,
  8         Peter Wilfahrt
  9 
 10     This file is part of JSXGraph.
 11 
 12     JSXGraph is free software dual licensed under the GNU LGPL or MIT License.
 13 
 14     You can redistribute it and/or modify it under the terms of the
 15 
 16       * GNU Lesser General Public License as published by
 17         the Free Software Foundation, either version 3 of the License, or
 18         (at your option) any later version
 19       OR
 20       * MIT License: https://github.com/jsxgraph/jsxgraph/blob/master/LICENSE.MIT
 21 
 22     JSXGraph is distributed in the hope that it will be useful,
 23     but WITHOUT ANY WARRANTY; without even the implied warranty of
 24     MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 25     GNU Lesser General Public License for more details.
 26 
 27     You should have received a copy of the GNU Lesser General Public License and
 28     the MIT License along with JSXGraph. If not, see <http://www.gnu.org/licenses/>
 29     and <http://opensource.org/licenses/MIT/>.
 30  */
 31 
 32 
 33 /*global JXG: true, define: true, AMprocessNode: true, MathJax: true, window: true, document: true, init: true, translateASCIIMath: true, google: true*/
 34 
 35 /*jslint nomen: true, plusplus: true*/
 36 
 37 /* depends:
 38  jxg
 39  base/constants
 40  base/coords
 41  options
 42  math/numerics
 43  math/math
 44  math/geometry
 45  math/complex
 46  parser/jessiecode
 47  parser/geonext
 48  utils/color
 49  utils/type
 50  utils/event
 51  utils/env
 52   elements:
 53    transform
 54    point
 55    line
 56    text
 57    grid
 58  */
 59 
 60 /**
 61  * @fileoverview The JXG.Board class is defined in this file. JXG.Board controls all properties and methods
 62  * used to manage a geonext board like managing geometric elements, managing mouse and touch events, etc.
 63  */
 64 
 65 define([
 66     'jxg', 'base/constants', 'base/coords', 'options', 'math/numerics', 'math/math', 'math/geometry', 'math/complex',
 67     'math/statistics',
 68     'parser/jessiecode', 'parser/geonext', 'utils/color', 'utils/type', 'utils/event', 'utils/env', 'base/transformation',
 69     'base/point', 'base/line', 'base/text', 'element/composition', 'base/composition'
 70 ], function (JXG, Const, Coords, Options, Numerics, Mat, Geometry, Complex, Statistics, JessieCode, GeonextParser, Color, Type,
 71                 EventEmitter, Env, Transform, Point, Line, Text, Composition, EComposition) {
 72 
 73     'use strict';
 74 
 75     /**
 76      * Constructs a new Board object.
 77      * @class JXG.Board controls all properties and methods used to manage a geonext board like managing geometric
 78      * elements, managing mouse and touch events, etc. You probably don't want to use this constructor directly.
 79      * Please use {@link JXG.JSXGraph#initBoard} to initialize a board.
 80      * @constructor
 81      * @param {String} container The id or reference of the HTML DOM element the board is drawn in. This is usually a HTML div.
 82      * @param {JXG.AbstractRenderer} renderer The reference of a renderer.
 83      * @param {String} id Unique identifier for the board, may be an empty string or null or even undefined.
 84      * @param {JXG.Coords} origin The coordinates where the origin is placed, in user coordinates.
 85      * @param {Number} zoomX Zoom factor in x-axis direction
 86      * @param {Number} zoomY Zoom factor in y-axis direction
 87      * @param {Number} unitX Units in x-axis direction
 88      * @param {Number} unitY Units in y-axis direction
 89      * @param {Number} canvasWidth  The width of canvas
 90      * @param {Number} canvasHeight The height of canvas
 91      * @param {Object} attributes The attributes object given to {@link JXG.JSXGraph#initBoard}
 92      * @borrows JXG.EventEmitter#on as this.on
 93      * @borrows JXG.EventEmitter#off as this.off
 94      * @borrows JXG.EventEmitter#triggerEventHandlers as this.triggerEventHandlers
 95      * @borrows JXG.EventEmitter#eventHandlers as this.eventHandlers
 96      */
 97     JXG.Board = function (container, renderer, id, origin, zoomX, zoomY, unitX, unitY, canvasWidth, canvasHeight, attributes) {
 98         /**
 99          * Board is in no special mode, objects are highlighted on mouse over and objects may be
100          * clicked to start drag&drop.
101          * @type Number
102          * @constant
103          */
104         this.BOARD_MODE_NONE = 0x0000;
105 
106         /**
107          * Board is in drag mode, objects aren't highlighted on mouse over and the object referenced in
108          * {JXG.Board#mouse} is updated on mouse movement.
109          * @type Number
110          * @constant
111          * @see JXG.Board#drag_obj
112          */
113         this.BOARD_MODE_DRAG = 0x0001;
114 
115         /**
116          * In this mode a mouse move changes the origin's screen coordinates.
117          * @type Number
118          * @constant
119          */
120         this.BOARD_MODE_MOVE_ORIGIN = 0x0002;
121 
122         /**
123          * Update is made with low quality, e.g. graphs are evaluated at a lesser amount of points.
124          * @type Number
125          * @constant
126          * @see JXG.Board#updateQuality
127          */
128         this.BOARD_QUALITY_LOW = 0x1;
129 
130         /**
131          * Update is made with high quality, e.g. graphs are evaluated at much more points.
132          * @type Number
133          * @constant
134          * @see JXG.Board#updateQuality
135          */
136         this.BOARD_QUALITY_HIGH = 0x2;
137 
138         /**
139          * Update is made with high quality, e.g. graphs are evaluated at much more points.
140          * @type Number
141          * @constant
142          * @see JXG.Board#updateQuality
143          */
144         this.BOARD_MODE_ZOOM = 0x0011;
145 
146         /**
147          * Pointer to the document element containing the board.
148          * @type Object
149          */
150         // Former version:
151         // this.document = attributes.document || document;
152         if (Type.exists(attributes.document) && attributes.document !== false) {
153             this.document = attributes.document;
154         } else if (typeof document !== 'undefined' && Type.isObject(document)) {
155             this.document = document;
156         }
157 
158         /**
159          * The html-id of the html element containing the board.
160          * @type String
161          */
162         this.container = container;
163 
164         /**
165          * Pointer to the html element containing the board.
166          * @type Object
167          */
168         this.containerObj = (Env.isBrowser ? this.document.getElementById(this.container) : null);
169 
170         if (Env.isBrowser && renderer.type !== 'no' && this.containerObj === null) {
171             throw new Error("\nJSXGraph: HTML container element '" + container + "' not found.");
172         }
173 
174         /**
175          * A reference to this boards renderer.
176          * @type JXG.AbstractRenderer
177          */
178         this.renderer = renderer;
179 
180         /**
181          * Grids keeps track of all grids attached to this board.
182          */
183         this.grids = [];
184 
185         /**
186          * Some standard options
187          * @type JXG.Options
188          */
189         this.options = Type.deepCopy(Options);
190         this.attr = attributes;
191 
192         /**
193          * Dimension of the board.
194          * @default 2
195          * @type Number
196          */
197         this.dimension = 2;
198 
199         this.jc = new JessieCode();
200         this.jc.use(this);
201 
202         /**
203          * Coordinates of the boards origin. This a object with the two properties
204          * usrCoords and scrCoords. usrCoords always equals [1, 0, 0] and scrCoords
205          * stores the boards origin in homogeneous screen coordinates.
206          * @type Object
207          */
208         this.origin = {};
209         this.origin.usrCoords = [1, 0, 0];
210         this.origin.scrCoords = [1, origin[0], origin[1]];
211 
212         /**
213          * Zoom factor in X direction. It only stores the zoom factor to be able
214          * to get back to 100% in zoom100().
215          * @type Number
216          */
217         this.zoomX = zoomX;
218 
219         /**
220          * Zoom factor in Y direction. It only stores the zoom factor to be able
221          * to get back to 100% in zoom100().
222          * @type Number
223          */
224         this.zoomY = zoomY;
225 
226         /**
227          * The number of pixels which represent one unit in user-coordinates in x direction.
228          * @type Number
229          */
230         this.unitX = unitX * this.zoomX;
231 
232         /**
233          * The number of pixels which represent one unit in user-coordinates in y direction.
234          * @type Number
235          */
236         this.unitY = unitY * this.zoomY;
237 
238         /**
239          * Keep aspect ratio if bounding box is set and the width/height ratio differs from the
240          * width/height ratio of the canvas.
241          */
242         this.keepaspectratio = false;
243 
244         /**
245          * Canvas width.
246          * @type Number
247          */
248         this.canvasWidth = canvasWidth;
249 
250         /**
251          * Canvas Height
252          * @type Number
253          */
254         this.canvasHeight = canvasHeight;
255 
256         // If the given id is not valid, generate an unique id
257         if (Type.exists(id) && id !== '' && Env.isBrowser && !Type.exists(this.document.getElementById(id))) {
258             this.id = id;
259         } else {
260             this.id = this.generateId();
261         }
262 
263         EventEmitter.eventify(this);
264 
265         this.hooks = [];
266 
267         /**
268          * An array containing all other boards that are updated after this board has been updated.
269          * @type Array
270          * @see JXG.Board#addChild
271          * @see JXG.Board#removeChild
272          */
273         this.dependentBoards = [];
274 
275         /**
276          * During the update process this is set to false to prevent an endless loop.
277          * @default false
278          * @type Boolean
279          */
280         this.inUpdate = false;
281 
282         /**
283          * An associative array containing all geometric objects belonging to the board. Key is the id of the object and value is a reference to the object.
284          * @type Object
285          */
286         this.objects = {};
287 
288         /**
289          * An array containing all geometric objects on the board in the order of construction.
290          * @type {Array}
291          */
292         this.objectsList = [];
293 
294         /**
295          * An associative array containing all groups belonging to the board. Key is the id of the group and value is a reference to the object.
296          * @type Object
297          */
298         this.groups = {};
299 
300         /**
301          * Stores all the objects that are currently running an animation.
302          * @type Object
303          */
304         this.animationObjects = {};
305 
306         /**
307          * An associative array containing all highlighted elements belonging to the board.
308          * @type Object
309          */
310         this.highlightedObjects = {};
311 
312         /**
313          * Number of objects ever created on this board. This includes every object, even invisible and deleted ones.
314          * @type Number
315          */
316         this.numObjects = 0;
317 
318         /**
319          * An associative array to store the objects of the board by name. the name of the object is the key and value is a reference to the object.
320          * @type Object
321          */
322         this.elementsByName = {};
323 
324         /**
325          * The board mode the board is currently in. Possible values are
326          * <ul>
327          * <li>JXG.Board.BOARD_MODE_NONE</li>
328          * <li>JXG.Board.BOARD_MODE_DRAG</li>
329          * <li>JXG.Board.BOARD_MODE_MOVE_ORIGIN</li>
330          * </ul>
331          * @type Number
332          */
333         this.mode = this.BOARD_MODE_NONE;
334 
335         /**
336          * The update quality of the board. In most cases this is set to {@link JXG.Board#BOARD_QUALITY_HIGH}.
337          * If {@link JXG.Board#mode} equals {@link JXG.Board#BOARD_MODE_DRAG} this is set to
338          * {@link JXG.Board#BOARD_QUALITY_LOW} to speed up the update process by e.g. reducing the number of
339          * evaluation points when plotting functions. Possible values are
340          * <ul>
341          * <li>BOARD_QUALITY_LOW</li>
342          * <li>BOARD_QUALITY_HIGH</li>
343          * </ul>
344          * @type Number
345          * @see JXG.Board#mode
346          */
347         this.updateQuality = this.BOARD_QUALITY_HIGH;
348 
349         /**
350          * If true updates are skipped.
351          * @type Boolean
352          */
353         this.isSuspendedRedraw = false;
354 
355         this.calculateSnapSizes();
356 
357         /**
358          * The distance from the mouse to the dragged object in x direction when the user clicked the mouse button.
359          * @type Number
360          * @see JXG.Board#drag_dy
361          * @see JXG.Board#drag_obj
362          */
363         this.drag_dx = 0;
364 
365         /**
366          * The distance from the mouse to the dragged object in y direction when the user clicked the mouse button.
367          * @type Number
368          * @see JXG.Board#drag_dx
369          * @see JXG.Board#drag_obj
370          */
371         this.drag_dy = 0;
372 
373         /**
374          * The last position where a drag event has been fired.
375          * @type Array
376          * @see JXG.Board#moveObject
377          */
378         this.drag_position = [0, 0];
379 
380         /**
381          * References to the object that is dragged with the mouse on the board.
382          * @type {@link JXG.GeometryElement}.
383          * @see {JXG.Board#touches}
384          */
385         this.mouse = {};
386 
387         /**
388          * Keeps track on touched elements, like {@link JXG.Board#mouse} does for mouse events.
389          * @type Array
390          * @see {JXG.Board#mouse}
391          */
392         this.touches = [];
393 
394         /**
395          * A string containing the XML text of the construction. This is set in {@link JXG.FileReader#parseString}.
396          * Only useful if a construction is read from a GEONExT-, Intergeo-, Geogebra-, or Cinderella-File.
397          * @type String
398          */
399         this.xmlString = '';
400 
401         /**
402          * Cached result of getCoordsTopLeftCorner for touch/mouseMove-Events to save some DOM operations.
403          * @type Array
404          */
405         this.cPos = [];
406 
407         /**
408          * Contains the last time (epoch, msec) since the last touchMove event which was not thrown away or since
409          * touchStart because Android's Webkit browser fires too much of them.
410          * @type Number
411          */
412         // this.touchMoveLast = 0;
413 
414         /**
415          * Contains the last time (epoch, msec) since the last getCoordsTopLeftCorner call which was not thrown away.
416          * @type Number
417          */
418         this.positionAccessLast = 0;
419 
420         /**
421          * Collects all elements that triggered a mouse down event.
422          * @type Array
423          */
424         this.downObjects = [];
425 
426         if (this.attr.showcopyright) {
427             this.renderer.displayCopyright(Const.licenseText, parseInt(this.options.text.fontSize, 10));
428         }
429 
430         /**
431          * Full updates are needed after zoom and axis translates. This saves some time during an update.
432          * @default false
433          * @type Boolean
434          */
435         this.needsFullUpdate = false;
436 
437         /**
438          * If reducedUpdate is set to true then only the dragged element and few (e.g. 2) following
439          * elements are updated during mouse move. On mouse up the whole construction is
440          * updated. This enables us to be fast even on very slow devices.
441          * @type Boolean
442          * @default false
443          */
444         this.reducedUpdate = false;
445 
446         /**
447          * The current color blindness deficiency is stored in this property. If color blindness is not emulated
448          * at the moment, it's value is 'none'.
449          */
450         this.currentCBDef = 'none';
451 
452         /**
453          * If GEONExT constructions are displayed, then this property should be set to true.
454          * At the moment there should be no difference. But this may change.
455          * This is set in {@link JXG.GeonextReader#readGeonext}.
456          * @type Boolean
457          * @default false
458          * @see JXG.GeonextReader#readGeonext
459          */
460         this.geonextCompatibilityMode = false;
461 
462         if (this.options.text.useASCIIMathML && translateASCIIMath) {
463             init();
464         } else {
465             this.options.text.useASCIIMathML = false;
466         }
467 
468         /**
469          * A flag which tells if the board registers mouse events.
470          * @type Boolean
471          * @default false
472          */
473         this.hasMouseHandlers = false;
474 
475         /**
476          * A flag which tells if the board registers touch events.
477          * @type Boolean
478          * @default false
479          */
480         this.hasTouchHandlers = false;
481 
482         /**
483          * A flag which stores if the board registered pointer events.
484          * @type {Boolean}
485          * @default false
486          */
487         this.hasPointerHandlers = false;
488 
489         /**
490          * This bool flag stores the current state of the mobile Safari specific gesture event handlers.
491          * @type {boolean}
492          * @default false
493          */
494         this.hasGestureHandlers = false;
495 
496         /**
497          * A flag which tells if the board the JXG.Board#mouseUpListener is currently registered.
498          * @type Boolean
499          * @default false
500          */
501         this.hasMouseUp = false;
502 
503         /**
504          * A flag which tells if the board the JXG.Board#touchEndListener is currently registered.
505          * @type Boolean
506          * @default false
507          */
508         this.hasTouchEnd = false;
509 
510         /**
511          * A flag which tells us if the board has a pointerUp event registered at the moment.
512          * @type {Boolean}
513          * @default false
514          */
515         this.hasPointerUp = false;
516 
517         /**
518          * Offset for large coords elements like images
519          * @type {Array}
520          * @private
521          * @default [0, 0]
522          */
523         this._drag_offset = [0, 0];
524 
525         /**
526          * A flag which tells us if the board is in the selecting mode
527          * @type {Boolean}
528          * @default false
529          */
530         this.selectingMode = false;
531 
532         /**
533          * A flag which tells us if the user is selecting
534          * @type {Boolean}
535          * @default false
536          */
537         this.isSelecting = false;
538 
539         /**
540          * A bounding box for the selection
541          * @type {Array}
542          * @default [ [0,0], [0,0] ]
543          */
544         this.selectingBox = [[0, 0], [0, 0]];
545 
546 
547         if (this.attr.registerevents) {
548             this.addEventHandlers();
549         }
550 
551         this.methodMap = {
552             update: 'update',
553             fullUpdate: 'fullUpdate',
554             on: 'on',
555             off: 'off',
556             trigger: 'trigger',
557             setView: 'setBoundingBox',
558             setBoundingBox: 'setBoundingBox',
559             migratePoint: 'migratePoint',
560             colorblind: 'emulateColorblindness',
561             suspendUpdate: 'suspendUpdate',
562             unsuspendUpdate: 'unsuspendUpdate',
563             clearTraces: 'clearTraces',
564             left: 'clickLeftArrow',
565             right: 'clickRightArrow',
566             up: 'clickUpArrow',
567             down: 'clickDownArrow',
568             zoomIn: 'zoomIn',
569             zoomOut: 'zoomOut',
570             zoom100: 'zoom100',
571             zoomElements: 'zoomElements',
572             remove: 'removeObject',
573             removeObject: 'removeObject'
574         };
575     };
576 
577     JXG.extend(JXG.Board.prototype, /** @lends JXG.Board.prototype */ {
578 
579         /**
580          * Generates an unique name for the given object. The result depends on the objects type, if the
581          * object is a {@link JXG.Point}, capital characters are used, if it is of type {@link JXG.Line}
582          * only lower case characters are used. If object is of type {@link JXG.Polygon}, a bunch of lower
583          * case characters prefixed with P_ are used. If object is of type {@link JXG.Circle} the name is
584          * generated using lower case characters. prefixed with k_ is used. In any other case, lower case
585          * chars prefixed with s_ is used.
586          * @param {Object} object Reference of an JXG.GeometryElement that is to be named.
587          * @returns {String} Unique name for the object.
588          */
589         generateName: function (object) {
590             var possibleNames, i,
591                 maxNameLength = this.attr.maxnamelength,
592                 pre = '',
593                 post = '',
594                 indices = [],
595                 name = '';
596 
597             if (object.type === Const.OBJECT_TYPE_TICKS) {
598                 return '';
599             }
600 
601             if (Type.isPoint(object)) {
602                 // points have capital letters
603                 possibleNames = ['', 'A', 'B', 'C', 'D', 'E', 'F', 'G', 'H', 'I', 'J', 'K', 'L', 'M', 'N', 'O',
604                     'P', 'Q', 'R', 'S', 'T', 'U', 'V', 'W', 'X', 'Y', 'Z'];
605             } else if (object.type === Const.OBJECT_TYPE_ANGLE) {
606                 possibleNames = ['', 'α', 'β', 'γ', 'δ', 'ε', 'ζ', 'η', 'θ',
607                     'ι', 'κ', 'λ', 'μ', 'ν', 'ξ', 'ο', 'π', 'ρ',
608                     'σ', 'τ', 'υ', 'φ', 'χ', 'ψ', 'ω'];
609             } else {
610                 // all other elements get lowercase labels
611                 possibleNames = ['', 'a', 'b', 'c', 'd', 'e', 'f', 'g', 'h', 'i', 'j', 'k', 'l', 'm', 'n', 'o',
612                     'p', 'q', 'r', 's', 't', 'u', 'v', 'w', 'x', 'y', 'z'];
613             }
614 
615             if (!Type.isPoint(object) &&
616                     object.elementClass !== Const.OBJECT_CLASS_LINE &&
617                     object.type !== Const.OBJECT_TYPE_ANGLE) {
618                 if (object.type === Const.OBJECT_TYPE_POLYGON) {
619                     pre = 'P_{';
620                 } else if (object.elementClass === Const.OBJECT_CLASS_CIRCLE) {
621                     pre = 'k_{';
622                 } else if (object.elementClass === Const.OBJECT_CLASS_TEXT) {
623                     pre = 't_{';
624                 } else {
625                     pre = 's_{';
626                 }
627                 post = '}';
628             }
629 
630             for (i = 0; i < maxNameLength; i++) {
631                 indices[i] = 0;
632             }
633 
634             while (indices[maxNameLength - 1] < possibleNames.length) {
635                 for (indices[0] = 1; indices[0] < possibleNames.length; indices[0]++) {
636                     name = pre;
637 
638                     for (i = maxNameLength; i > 0; i--) {
639                         name += possibleNames[indices[i - 1]];
640                     }
641 
642                     if (!Type.exists(this.elementsByName[name + post])) {
643                         return name + post;
644                     }
645 
646                 }
647                 indices[0] = possibleNames.length;
648 
649                 for (i = 1; i < maxNameLength; i++) {
650                     if (indices[i - 1] === possibleNames.length) {
651                         indices[i - 1] = 1;
652                         indices[i] += 1;
653                     }
654                 }
655             }
656 
657             return '';
658         },
659 
660         /**
661          * Generates unique id for a board. The result is randomly generated and prefixed with 'jxgBoard'.
662          * @returns {String} Unique id for a board.
663          */
664         generateId: function () {
665             var r = 1;
666 
667             // as long as we don't have a unique id generate a new one
668             while (Type.exists(JXG.boards['jxgBoard' + r])) {
669                 r = Math.round(Math.random() * 65535);
670             }
671 
672             return ('jxgBoard' + r);
673         },
674 
675         /**
676          * Composes an id for an element. If the ID is empty ('' or null) a new ID is generated, depending on the
677          * object type. Additionally, the id of the label is set. As a side effect {@link JXG.Board#numObjects}
678          * is updated.
679          * @param {Object} obj Reference of an geometry object that needs an id.
680          * @param {Number} type Type of the object.
681          * @returns {String} Unique id for an element.
682          */
683         setId: function (obj, type) {
684             var num = this.numObjects,
685                 elId = obj.id;
686 
687             this.numObjects += 1;
688 
689             // Falls Id nicht vorgegeben, eine Neue generieren:
690             if (elId === '' || !Type.exists(elId)) {
691                 elId = this.id + type + num;
692             }
693 
694             obj.id = elId;
695             this.objects[elId] = obj;
696             obj._pos = this.objectsList.length;
697             this.objectsList[this.objectsList.length] = obj;
698 
699             return elId;
700         },
701 
702         /**
703          * After construction of the object the visibility is set
704          * and the label is constructed if necessary.
705          * @param {Object} obj The object to add.
706          */
707         finalizeAdding: function (obj) {
708             if (!obj.visProp.visible) {
709                 this.renderer.hide(obj);
710             }
711         },
712 
713         finalizeLabel: function (obj) {
714             if (obj.hasLabel && !obj.label.visProp.islabel && !obj.label.visProp.visible) {
715                 this.renderer.hide(obj.label);
716             }
717         },
718 
719         /**********************************************************
720          *
721          * Event Handler helpers
722          *
723          **********************************************************/
724 
725         /**
726          * Calculates mouse coordinates relative to the boards container.
727          * @returns {Array} Array of coordinates relative the boards container top left corner.
728          */
729         getCoordsTopLeftCorner: function () {
730             var cPos, doc, crect,
731                 docElement = this.document.documentElement || this.document.body.parentNode,
732                 docBody = this.document.body,
733                 container = this.containerObj,
734                 viewport, content;
735 
736             /**
737              * During drags and origin moves the container element is usually not changed.
738              * Check the position of the upper left corner at most every 1000 msecs
739              */
740             if (this.cPos.length > 0 &&
741                     (this.mode === this.BOARD_MODE_DRAG || this.mode === this.BOARD_MODE_MOVE_ORIGIN ||
742                     (new Date()).getTime() - this.positionAccessLast < 1000)) {
743                 return this.cPos;
744             }
745             this.positionAccessLast = (new Date()).getTime();
746 
747             // Check if getBoundingClientRect exists. If so, use this as this covers *everything*
748             // even CSS3D transformations etc.
749             // Supported by all browsers but IE 6, 7.
750             if (container.getBoundingClientRect) {
751                 crect = container.getBoundingClientRect();
752                 cPos = [crect.left, crect.top];
753 
754                 // add border width
755                 cPos[0] += Env.getProp(container, 'border-left-width');
756                 cPos[1] += Env.getProp(container, 'border-top-width');
757 
758                 // vml seems to ignore paddings
759                 if (this.renderer.type !== 'vml') {
760                     // add padding
761                     cPos[0] += Env.getProp(container, 'padding-left');
762                     cPos[1] += Env.getProp(container, 'padding-top');
763                 }
764 
765                 this.cPos = cPos.slice();
766                 return this.cPos;
767             }
768 
769             //
770             //  OLD CODE
771             //  IE 6-7 only:
772             //
773             cPos = Env.getOffset(container);
774             doc = this.document.documentElement.ownerDocument;
775 
776             if (!this.containerObj.currentStyle && doc.defaultView) {     // Non IE
777                 // this is for hacks like this one used in wordpress for the admin bar:
778                 // html { margin-top: 28px }
779                 // seems like it doesn't work in IE
780 
781                 cPos[0] += Env.getProp(docElement, 'margin-left');
782                 cPos[1] += Env.getProp(docElement, 'margin-top');
783 
784                 cPos[0] += Env.getProp(docElement, 'border-left-width');
785                 cPos[1] += Env.getProp(docElement, 'border-top-width');
786 
787                 cPos[0] += Env.getProp(docElement, 'padding-left');
788                 cPos[1] += Env.getProp(docElement, 'padding-top');
789             }
790 
791             if (docBody) {
792                 cPos[0] += Env.getProp(docBody, 'left');
793                 cPos[1] += Env.getProp(docBody, 'top');
794             }
795 
796             // Google Translate offers widgets for web authors. These widgets apparently tamper with the clientX
797             // and clientY coordinates of the mouse events. The minified sources seem to be the only publicly
798             // available version so we're doing it the hacky way: Add a fixed offset.
799             // see https://groups.google.com/d/msg/google-translate-general/H2zj0TNjjpY/jw6irtPlCw8J
800             if (typeof google === 'object' && google.translate) {
801                 cPos[0] += 10;
802                 cPos[1] += 25;
803             }
804 
805             // add border width
806             cPos[0] += Env.getProp(container, 'border-left-width');
807             cPos[1] += Env.getProp(container, 'border-top-width');
808 
809             // vml seems to ignore paddings
810             if (this.renderer.type !== 'vml') {
811                 // add padding
812                 cPos[0] += Env.getProp(container, 'padding-left');
813                 cPos[1] += Env.getProp(container, 'padding-top');
814             }
815 
816             cPos[0] += this.attr.offsetx;
817             cPos[1] += this.attr.offsety;
818 
819             this.cPos = cPos.slice();
820             return this.cPos;
821         },
822 
823         /**
824          * Get the position of the mouse in screen coordinates, relative to the upper left corner
825          * of the host tag.
826          * @param {Event} e Event object given by the browser.
827          * @param {Number} [i] Only use in case of touch events. This determines which finger to use and should not be set
828          * for mouseevents.
829          * @returns {Array} Contains the mouse coordinates in user coordinates, ready  for {@link JXG.Coords}
830          */
831         getMousePosition: function (e, i) {
832             var cPos = this.getCoordsTopLeftCorner(),
833                 absPos,
834                 v;
835 
836             // position of mouse cursor relative to containers position of container
837             absPos = Env.getPosition(e, i, this.document);
838 
839             /**
840              * In case there has been no down event before.
841              */
842             if (!Type.exists(this.cssTransMat)) {
843                 this.updateCSSTransforms();
844             }
845             v = [1, absPos[0] - cPos[0], absPos[1] - cPos[1]];
846             v = Mat.matVecMult(this.cssTransMat, v);
847             v[1] /= v[0];
848             v[2] /= v[0];
849             return [v[1], v[2]];
850 
851             // Method without CSS transformation
852             /*
853              return [absPos[0] - cPos[0], absPos[1] - cPos[1]];
854              */
855         },
856 
857         /**
858          * Initiate moving the origin. This is used in mouseDown and touchStart listeners.
859          * @param {Number} x Current mouse/touch coordinates
860          * @param {Number} y Current mouse/touch coordinates
861          */
862         initMoveOrigin: function (x, y) {
863             this.drag_dx = x - this.origin.scrCoords[1];
864             this.drag_dy = y - this.origin.scrCoords[2];
865 
866             this.mode = this.BOARD_MODE_MOVE_ORIGIN;
867             this.updateQuality = this.BOARD_QUALITY_LOW;
868         },
869 
870         /**
871          * Collects all elements below the current mouse pointer and fulfilling the following constraints:
872          * <ul><li>isDraggable</li><li>visible</li><li>not fixed</li><li>not frozen</li></ul>
873          * @param {Number} x Current mouse/touch coordinates
874          * @param {Number} y current mouse/touch coordinates
875          * @param {Object} evt An event object
876          * @param {String} type What type of event? 'touch' or 'mouse'.
877          * @returns {Array} A list of geometric elements.
878          */
879         initMoveObject: function (x, y, evt, type) {
880             var pEl,
881                 el,
882                 collect = [],
883                 offset = [],
884                 haspoint,
885                 len = this.objectsList.length,
886                 dragEl = {visProp: {layer: -10000}};
887 
888             //for (el in this.objects) {
889             for (el = 0; el < len; el++) {
890                 pEl = this.objectsList[el];
891                 haspoint = pEl.hasPoint && pEl.hasPoint(x, y);
892 
893                 if (pEl.visProp.visible && haspoint) {
894                     pEl.triggerEventHandlers([type + 'down', 'down'], [evt]);
895                     this.downObjects.push(pEl);
896                 }
897 
898                 if (((this.geonextCompatibilityMode &&
899                         (Type.isPoint(pEl) ||
900                           pEl.elementClass === Const.OBJECT_CLASS_TEXT)) ||
901                         !this.geonextCompatibilityMode) &&
902                         pEl.isDraggable &&
903                         pEl.visProp.visible &&
904                         (!pEl.visProp.fixed) && /*(!pEl.visProp.frozen) &&*/
905                         haspoint) {
906                     // Elements in the highest layer get priority.
907                     if (pEl.visProp.layer > dragEl.visProp.layer ||
908                             (pEl.visProp.layer === dragEl.visProp.layer &&
909                              pEl.lastDragTime.getTime() >= dragEl.lastDragTime.getTime()
910                             )) {
911                         // If an element and its label have the focus
912                         // simultaneously, the element is taken.
913                         // This only works if we assume that every browser runs
914                         // through this.objects in the right order, i.e. an element A
915                         // added before element B turns up here before B does.
916                         if (!this.attr.ignorelabels || (!Type.exists(dragEl.label) || pEl !== dragEl.label)) {
917                             dragEl = pEl;
918                             collect.push(dragEl);
919 
920                             // Save offset for large coords elements.
921                             if (Type.exists(dragEl.coords)) {
922                                 offset.push(Statistics.subtract(dragEl.coords.scrCoords.slice(1), [x, y]));
923                             } else {
924                                 offset.push([0, 0]);
925                             }
926 
927                             // we can't drop out of this loop because of the event handling system
928                             //if (this.attr.takefirst) {
929                             //    return collect;
930                             //}
931                         }
932                     }
933                 }
934             }
935 
936             if (collect.length > 0) {
937                 this.mode = this.BOARD_MODE_DRAG;
938             }
939 
940             // A one-element array is returned.
941             if (this.attr.takefirst) {
942                 collect.length = 1;
943                 this._drag_offset = offset[0];
944             } else {
945                 collect = collect.slice(-1);
946                 this._drag_offset = offset[offset.length - 1];
947             }
948 
949             if (!this._drag_offset) {
950                 this._drag_offset = [0, 0];
951             }
952 
953             return collect;
954         },
955 
956         /**
957          * Moves an object.
958          * @param {Number} x Coordinate
959          * @param {Number} y Coordinate
960          * @param {Object} o The touch object that is dragged: {JXG.Board#mouse} or {JXG.Board#touches}.
961          * @param {Object} evt The event object.
962          * @param {String} type Mouse or touch event?
963          */
964         moveObject: function (x, y, o, evt, type) {
965             var newPos = new Coords(Const.COORDS_BY_SCREEN, this.getScrCoordsOfMouse(x, y), this),
966                 drag;
967 
968             if (!(o && o.obj)) {
969                 return;
970             }
971             drag = o.obj;
972 
973             /*
974              * Save the position.
975              */
976             this.drag_position = [newPos.scrCoords[1], newPos.scrCoords[2]];
977             this.drag_position = Statistics.add(this.drag_position, this._drag_offset);
978             //
979             // We have to distinguish between CoordsElements and other elements like lines.
980             // The latter need the difference between two move events.
981             if (Type.exists(drag.coords)) {
982                 drag.setPositionDirectly(Const.COORDS_BY_SCREEN, this.drag_position);
983             } else {
984                 this.renderer.hide(this.infobox); // Hide infobox in case the user has touched an intersection point
985                                                   // and drags the underlying line now.
986 
987                 if (!isNaN(o.targets[0].Xprev + o.targets[0].Yprev)) {
988                     drag.setPositionDirectly(Const.COORDS_BY_SCREEN,
989                         [newPos.scrCoords[1], newPos.scrCoords[2]],
990                         [o.targets[0].Xprev, o.targets[0].Yprev]
991                         );
992                 }
993                 // Remember the actual position for the next move event. Then we are able to
994                 // compute the difference vector.
995                 o.targets[0].Xprev = newPos.scrCoords[1];
996                 o.targets[0].Yprev = newPos.scrCoords[2];
997             }
998             // This may be necessary for some gliders
999             drag.prepareUpdate().update(false).updateRenderer();
1000 
1001             drag.triggerEventHandlers([type + 'drag', 'drag'], [evt]);
1002 
1003             this.updateInfobox(drag);
1004             this.update();
1005             drag.highlight(true);
1006 
1007             drag.lastDragTime = new Date();
1008         },
1009 
1010         /**
1011          * Moves elements in multitouch mode.
1012          * @param {Array} p1 x,y coordinates of first touch
1013          * @param {Array} p2 x,y coordinates of second touch
1014          * @param {Object} o The touch object that is dragged: {JXG.Board#touches}.
1015          * @param {Object} evt The event object that lead to this movement.
1016          */
1017         twoFingerMove: function (p1, p2, o, evt) {
1018             var np1c, np2c, drag;
1019 
1020             if (Type.exists(o) && Type.exists(o.obj)) {
1021                 drag = o.obj;
1022             } else {
1023                 return;
1024             }
1025 
1026             // New finger position
1027             np1c = new Coords(Const.COORDS_BY_SCREEN, this.getScrCoordsOfMouse(p1[0], p1[1]), this);
1028             np2c = new Coords(Const.COORDS_BY_SCREEN, this.getScrCoordsOfMouse(p2[0], p2[1]), this);
1029 
1030             if (drag.elementClass === Const.OBJECT_CLASS_LINE ||
1031                     drag.type === Const.OBJECT_TYPE_POLYGON) {
1032                 this.twoFingerTouchObject(np1c, np2c, o, drag);
1033             } else if (drag.elementClass === Const.OBJECT_CLASS_CIRCLE) {
1034                 this.twoFingerTouchCircle(np1c, np2c, o, drag);
1035             }
1036             drag.triggerEventHandlers(['touchdrag', 'drag'], [evt]);
1037 
1038             o.targets[0].Xprev = np1c.scrCoords[1];
1039             o.targets[0].Yprev = np1c.scrCoords[2];
1040             o.targets[1].Xprev = np2c.scrCoords[1];
1041             o.targets[1].Yprev = np2c.scrCoords[2];
1042         },
1043 
1044         /**
1045          * Moves a line or polygon with two fingers
1046          * @param {JXG.Coords} np1c x,y coordinates of first touch
1047          * @param {JXG.Coords} np2c x,y coordinates of second touch
1048          * @param {object} o The touch object that is dragged: {JXG.Board#touches}.
1049          * @param {object} drag The object that is dragged:
1050          */
1051         twoFingerTouchObject: function (np1c, np2c, o, drag) {
1052             var np1, np2, op1, op2,
1053                 nmid, omid, nd, od,
1054                 d,
1055                 S, alpha, t1, t2, t3, t4, t5,
1056                 ar, i, len;
1057 
1058             if (Type.exists(o.targets[0]) &&
1059                     Type.exists(o.targets[1]) &&
1060                     !isNaN(o.targets[0].Xprev + o.targets[0].Yprev + o.targets[1].Xprev + o.targets[1].Yprev)) {
1061                 np1 = np1c.usrCoords;
1062                 np2 = np2c.usrCoords;
1063                 // Previous finger position
1064                 op1 = (new Coords(Const.COORDS_BY_SCREEN, [o.targets[0].Xprev, o.targets[0].Yprev], this)).usrCoords;
1065                 op2 = (new Coords(Const.COORDS_BY_SCREEN, [o.targets[1].Xprev, o.targets[1].Yprev], this)).usrCoords;
1066 
1067                 // Affine mid points of the old and new positions
1068                 omid = [1, (op1[1] + op2[1]) * 0.5, (op1[2] + op2[2]) * 0.5];
1069                 nmid = [1, (np1[1] + np2[1]) * 0.5, (np1[2] + np2[2]) * 0.5];
1070 
1071                 // Old and new directions
1072                 od = Mat.crossProduct(op1, op2);
1073                 nd = Mat.crossProduct(np1, np2);
1074                 S = Mat.crossProduct(od, nd);
1075 
1076                 // If parallel, translate otherwise rotate
1077                 if (Math.abs(S[0]) < Mat.eps) {
1078                     return;
1079                 }
1080 
1081                 S[1] /= S[0];
1082                 S[2] /= S[0];
1083                 alpha = Geometry.rad(omid.slice(1), S.slice(1), nmid.slice(1));
1084                 t1 = this.create('transform', [alpha, S[1], S[2]], {type: 'rotate'});
1085 
1086                 // Old midpoint of fingers after first transformation:
1087                 t1.update();
1088                 omid = Mat.matVecMult(t1.matrix, omid);
1089                 omid[1] /= omid[0];
1090                 omid[2] /= omid[0];
1091 
1092                 // Shift to the new mid point
1093                 t2 = this.create('transform', [nmid[1] - omid[1], nmid[2] - omid[2]], {type: 'translate'});
1094                 t2.update();
1095                 //omid = Mat.matVecMult(t2.matrix, omid);
1096 
1097                 t1.melt(t2);
1098                 if (drag.visProp.scalable) {
1099                     // Scale
1100                     d = Geometry.distance(np1, np2) / Geometry.distance(op1, op2);
1101                     t3 = this.create('transform', [-nmid[1], -nmid[2]], {type: 'translate'});
1102                     t4 = this.create('transform', [d, d], {type: 'scale'});
1103                     t5 = this.create('transform', [nmid[1], nmid[2]], {type: 'translate'});
1104                     t1.melt(t3).melt(t4).melt(t5);
1105                 }
1106 
1107 
1108                 if (drag.elementClass === Const.OBJECT_CLASS_LINE) {
1109                     ar = [];
1110                     if (drag.point1.draggable()) {
1111                         ar.push(drag.point1);
1112                     }
1113                     if (drag.point2.draggable()) {
1114                         ar.push(drag.point2);
1115                     }
1116                     t1.applyOnce(ar);
1117                 } else if (drag.type === Const.OBJECT_TYPE_POLYGON) {
1118                     ar = [];
1119                     len = drag.vertices.length - 1;
1120                     for (i = 0; i < len; ++i) {
1121                         if (drag.vertices[i].draggable()) {
1122                             ar.push(drag.vertices[i]);
1123                         }
1124                     }
1125                     t1.applyOnce(ar);
1126                 }
1127 
1128                 this.update();
1129                 drag.highlight(true);
1130             }
1131         },
1132 
1133         /*
1134          * Moves a circle with two fingers
1135          * @param {JXG.Coords} np1c x,y coordinates of first touch
1136          * @param {JXG.Coords} np2c x,y coordinates of second touch
1137          * @param {object} o The touch object that is dragged: {JXG.Board#touches}.
1138          * @param {object} drag The object that is dragged:
1139          */
1140         twoFingerTouchCircle: function (np1c, np2c, o, drag) {
1141             var np1, np2, op1, op2,
1142                 d, alpha, t1, t2, t3, t4, t5;
1143 
1144             if (drag.method === 'pointCircle' ||
1145                     drag.method === 'pointLine') {
1146                 return;
1147             }
1148 
1149             if (Type.exists(o.targets[0]) &&
1150                     Type.exists(o.targets[1]) &&
1151                     !isNaN(o.targets[0].Xprev + o.targets[0].Yprev + o.targets[1].Xprev + o.targets[1].Yprev)) {
1152 
1153                 np1 = np1c.usrCoords;
1154                 np2 = np2c.usrCoords;
1155                 // Previous finger position
1156                 op1 = (new Coords(Const.COORDS_BY_SCREEN, [o.targets[0].Xprev, o.targets[0].Yprev], this)).usrCoords;
1157                 op2 = (new Coords(Const.COORDS_BY_SCREEN, [o.targets[1].Xprev, o.targets[1].Yprev], this)).usrCoords;
1158 
1159                 // Shift by the movement of the first finger
1160                 t1 = this.create('transform', [np1[1] - op1[1], np1[2] - op1[2]], {type: 'translate'});
1161                 alpha = Geometry.rad(op2.slice(1), np1.slice(1), np2.slice(1));
1162 
1163                 // Rotate and scale by the movement of the second finger
1164                 t2 = this.create('transform', [-np1[1], -np1[2]], {type: 'translate'});
1165                 t3 = this.create('transform', [alpha], {type: 'rotate'});
1166                 t1.melt(t2).melt(t3);
1167 
1168                 if (drag.visProp.scalable) {
1169                     d = Geometry.distance(np1, np2) / Geometry.distance(op1, op2);
1170                     t4 = this.create('transform', [d, d], {type: 'scale'});
1171                     t1.melt(t4);
1172                 }
1173                 t5 = this.create('transform', [np1[1], np1[2]], {type: 'translate'});
1174                 t1.melt(t5);
1175 
1176                 if (drag.center.draggable()) {
1177                     t1.applyOnce([drag.center]);
1178                 }
1179 
1180                 if (drag.method === 'twoPoints') {
1181                     if (drag.point2.draggable()) {
1182                         t1.applyOnce([drag.point2]);
1183                     }
1184                 } else if (drag.method === 'pointRadius') {
1185                     if (Type.isNumber(drag.updateRadius.origin)) {
1186                         drag.setRadius(drag.radius * d);
1187                     }
1188                 }
1189                 this.update(drag.center);
1190                 drag.highlight(true);
1191             }
1192         },
1193 
1194         highlightElements: function (x, y, evt, target) {
1195             var el, pEl, pId,
1196                 overObjects = {},
1197                 len = this.objectsList.length;
1198 
1199             // Elements  below the mouse pointer which are not highlighted yet will be highlighted.
1200             for (el = 0; el < len; el++) {
1201                 pEl = this.objectsList[el];
1202                 pId = pEl.id;
1203                 if (Type.exists(pEl.hasPoint) && pEl.visProp.visible && pEl.hasPoint(x, y)) {
1204                     // this is required in any case because otherwise the box won't be shown until the point is dragged
1205                     this.updateInfobox(pEl);
1206 
1207                     if (!Type.exists(this.highlightedObjects[pId])) { // highlight only if not highlighted
1208                         overObjects[pId] = pEl;
1209                         pEl.highlight();
1210                         this.triggerEventHandlers(['mousehit', 'hit'], [evt, pEl, target]);
1211                     }
1212 
1213                     if (pEl.mouseover) {
1214                         pEl.triggerEventHandlers(['mousemove', 'move'], [evt]);
1215                     } else {
1216                         pEl.triggerEventHandlers(['mouseover', 'over'], [evt]);
1217                         pEl.mouseover = true;
1218                     }
1219                 }
1220             }
1221 
1222             for (el = 0; el < len; el++) {
1223                 pEl = this.objectsList[el];
1224                 pId = pEl.id;
1225                 if (pEl.mouseover) {
1226                     if (!overObjects[pId]) {
1227                         pEl.triggerEventHandlers(['mouseout', 'out'], [evt]);
1228                         pEl.mouseover = false;
1229                     }
1230                 }
1231             }
1232         },
1233 
1234         /**
1235          * Helper function which returns a reasonable starting point for the object being dragged.
1236          * Formerly known as initXYstart().
1237          * @private
1238          * @param {JXG.GeometryElement} obj The object to be dragged
1239          * @param {Array} targets Array of targets. It is changed by this function.
1240          */
1241         saveStartPos: function (obj, targets) {
1242             var xy = [], i, len;
1243 
1244             if (obj.type === Const.OBJECT_TYPE_TICKS) {
1245                 xy.push([1, NaN, NaN]);
1246             } else if (obj.elementClass === Const.OBJECT_CLASS_LINE) {
1247                 xy.push(obj.point1.coords.usrCoords);
1248                 xy.push(obj.point2.coords.usrCoords);
1249             } else if (obj.elementClass === Const.OBJECT_CLASS_CIRCLE) {
1250                 xy.push(obj.center.coords.usrCoords);
1251                 if (obj.method === 'twoPoints') {
1252                     xy.push(obj.point2.coords.usrCoords);
1253                 }
1254             } else if (obj.type === Const.OBJECT_TYPE_POLYGON) {
1255                 len = obj.vertices.length - 1;
1256                 for (i = 0; i < len; i++) {
1257                     xy.push(obj.vertices[i].coords.usrCoords);
1258                 }
1259             } else if (obj.type === Const.OBJECT_TYPE_SECTOR) {
1260                 xy.push(obj.point1.coords.usrCoords);
1261                 xy.push(obj.point2.coords.usrCoords);
1262                 xy.push(obj.point3.coords.usrCoords);
1263             } else if (Type.isPoint(obj) || obj.type === Const.OBJECT_TYPE_GLIDER) {
1264                 xy.push(obj.coords.usrCoords);
1265             } else if (obj.elementClass === Const.OBJECT_CLASS_CURVE) {
1266                 if (JXG.exists(obj.parents)) {
1267                     len = obj.parents.length;
1268                     for (i = 0; i < len; i++) {
1269                         xy.push(this.select(obj.parents[i]).coords.usrCoords);
1270                     }
1271                 }
1272             } else {
1273                 try {
1274                     xy.push(obj.coords.usrCoords);
1275                 } catch (e) {
1276                     JXG.debug('JSXGraph+ saveStartPos: obj.coords.usrCoords not available: ' + e);
1277                 }
1278             }
1279 
1280             len = xy.length;
1281             for (i = 0; i < len; i++) {
1282                 targets.Zstart.push(xy[i][0]);
1283                 targets.Xstart.push(xy[i][1]);
1284                 targets.Ystart.push(xy[i][2]);
1285             }
1286         },
1287 
1288         mouseOriginMoveStart: function (evt) {
1289             var r = this.attr.pan.enabled && (!this.attr.pan.needshift || evt.shiftKey),
1290                 pos;
1291 
1292             if (r) {
1293                 pos = this.getMousePosition(evt);
1294                 this.initMoveOrigin(pos[0], pos[1]);
1295             }
1296 
1297             return r;
1298         },
1299 
1300         mouseOriginMove: function (evt) {
1301             var r = (this.mode === this.BOARD_MODE_MOVE_ORIGIN),
1302                 pos;
1303 
1304             if (r) {
1305                 pos = this.getMousePosition(evt);
1306                 this.moveOrigin(pos[0], pos[1], true);
1307             }
1308 
1309             return r;
1310         },
1311 
1312         touchOriginMoveStart: function (evt) {
1313             var touches = evt[JXG.touchProperty],
1314                 twoFingersCondition = (touches.length === 2 && Geometry.distance([touches[0].screenX, touches[0].screenY], [touches[1].screenX, touches[1].screenY]) < 120),
1315                 r = this.attr.pan.enabled && (!this.attr.pan.needtwofingers || twoFingersCondition),
1316                 pos;
1317 
1318             if (r) {
1319                 pos = this.getMousePosition(evt, 0);
1320                 this.initMoveOrigin(pos[0], pos[1]);
1321             }
1322 
1323             return r;
1324         },
1325 
1326         touchOriginMove: function (evt) {
1327             var r = (this.mode === this.BOARD_MODE_MOVE_ORIGIN),
1328                 pos;
1329 
1330             if (r) {
1331                 pos = this.getMousePosition(evt, 0);
1332                 this.moveOrigin(pos[0], pos[1], true);
1333             }
1334 
1335             return r;
1336         },
1337 
1338         originMoveEnd: function () {
1339             this.updateQuality = this.BOARD_QUALITY_HIGH;
1340             this.mode = this.BOARD_MODE_NONE;
1341         },
1342 
1343         /**********************************************************
1344          *
1345          * Event Handler
1346          *
1347          **********************************************************/
1348 
1349         /**
1350          *  Add all possible event handlers to the board object
1351          */
1352         addEventHandlers: function () {
1353             if (Env.supportsPointerEvents()) {
1354                 this.addPointerEventHandlers();
1355             } else {
1356                 this.addMouseEventHandlers();
1357                 this.addTouchEventHandlers();
1358             }
1359             //if (Env.isBrowser) {
1360             //Env.addEvent(window, 'resize', this.update, this);
1361             //}
1362         },
1363 
1364         /**
1365          * Registers the MSPointer* event handlers.
1366          */
1367         addPointerEventHandlers: function () {
1368             if (!this.hasPointerHandlers && Env.isBrowser) {
1369                 if (window.navigator.pointerEnabled) {  // IE11+
1370                     Env.addEvent(this.containerObj, 'pointerdown', this.pointerDownListener, this);
1371                     Env.addEvent(this.containerObj, 'pointermove', this.pointerMoveListener, this);
1372                 } else {
1373                     Env.addEvent(this.containerObj, 'MSPointerDown', this.pointerDownListener, this);
1374                     Env.addEvent(this.containerObj, 'MSPointerMove', this.pointerMoveListener, this);
1375                 }
1376                 Env.addEvent(this.containerObj, 'mousewheel', this.mouseWheelListener, this);
1377                 Env.addEvent(this.containerObj, 'DOMMouseScroll', this.mouseWheelListener, this);
1378 
1379                 this.hasPointerHandlers = true;
1380             }
1381         },
1382 
1383         /**
1384          * Registers mouse move, down and wheel event handlers.
1385          */
1386         addMouseEventHandlers: function () {
1387             if (!this.hasMouseHandlers && Env.isBrowser) {
1388                 Env.addEvent(this.containerObj, 'mousedown', this.mouseDownListener, this);
1389                 Env.addEvent(this.containerObj, 'mousemove', this.mouseMoveListener, this);
1390 
1391                 Env.addEvent(this.containerObj, 'mousewheel', this.mouseWheelListener, this);
1392                 Env.addEvent(this.containerObj, 'DOMMouseScroll', this.mouseWheelListener, this);
1393 
1394                 this.hasMouseHandlers = true;
1395 
1396                 // This one produces errors on IE
1397                 //   Env.addEvent(this.containerObj, 'contextmenu', function (e) { e.preventDefault(); return false;}, this);
1398 
1399                 // This one works on IE, Firefox and Chromium with default configurations. On some Safari
1400                 // or Opera versions the user must explicitly allow the deactivation of the context menu.
1401                 if (this.containerObj !== null) {
1402                     this.containerObj.oncontextmenu = function (e) {
1403                         if (Type.exists(e)) {
1404                             e.preventDefault();
1405                         }
1406 
1407                         return false;
1408                     };
1409                 }
1410             }
1411         },
1412 
1413         /**
1414          * Register touch start and move and gesture start and change event handlers.
1415          * @param {Boolean} appleGestures If set to false the gesturestart and gesturechange event handlers
1416          * will not be registered.
1417          */
1418         addTouchEventHandlers: function (appleGestures) {
1419             if (!this.hasTouchHandlers && Env.isBrowser) {
1420                 Env.addEvent(this.containerObj, 'touchstart', this.touchStartListener, this);
1421                 Env.addEvent(this.containerObj, 'touchmove', this.touchMoveListener, this);
1422 
1423                 if (!Type.exists(appleGestures) || appleGestures) {
1424                     Env.addEvent(this.containerObj, 'gesturestart', this.gestureStartListener, this);
1425                     Env.addEvent(this.containerObj, 'gesturechange', this.gestureChangeListener, this);
1426                     this.hasGestureHandlers = true;
1427                 }
1428 
1429                 this.hasTouchHandlers = true;
1430             }
1431         },
1432 
1433         /**
1434          * Remove MSPointer* Event handlers.
1435          */
1436         removePointerEventHandlers: function () {
1437             if (this.hasPointerHandlers && Env.isBrowser) {
1438                 if (window.navigator.pointerEnabled) {  // IE11+
1439                     Env.removeEvent(this.containerObj, 'pointerdown', this.pointerDownListener, this);
1440                     Env.removeEvent(this.containerObj, 'pointermove', this.pointerMoveListener, this);
1441                 } else {
1442                     Env.removeEvent(this.containerObj, 'MSPointerDown', this.pointerDownListener, this);
1443                     Env.removeEvent(this.containerObj, 'MSPointerMove', this.pointerMoveListener, this);
1444                 }
1445 
1446                 Env.removeEvent(this.containerObj, 'mousewheel', this.mouseWheelListener, this);
1447                 Env.removeEvent(this.containerObj, 'DOMMouseScroll', this.mouseWheelListener, this);
1448 
1449                 if (this.hasPointerUp) {
1450                     if (window.navigator.pointerEnabled) {  // IE11+
1451                         Env.removeEvent(this.document, 'pointerup', this.pointerUpListener, this);
1452                     } else {
1453                         Env.removeEvent(this.document, 'MSPointerUp', this.pointerUpListener, this);
1454                     }
1455                     this.hasPointerUp = false;
1456                 }
1457 
1458                 this.hasPointerHandlers = false;
1459             }
1460         },
1461 
1462         /**
1463          * De-register mouse event handlers.
1464          */
1465         removeMouseEventHandlers: function () {
1466             if (this.hasMouseHandlers && Env.isBrowser) {
1467                 Env.removeEvent(this.containerObj, 'mousedown', this.mouseDownListener, this);
1468                 Env.removeEvent(this.containerObj, 'mousemove', this.mouseMoveListener, this);
1469 
1470                 if (this.hasMouseUp) {
1471                     Env.removeEvent(this.document, 'mouseup', this.mouseUpListener, this);
1472                     this.hasMouseUp = false;
1473                 }
1474 
1475                 Env.removeEvent(this.containerObj, 'mousewheel', this.mouseWheelListener, this);
1476                 Env.removeEvent(this.containerObj, 'DOMMouseScroll', this.mouseWheelListener, this);
1477 
1478                 this.hasMouseHandlers = false;
1479             }
1480         },
1481 
1482         /**
1483          * Remove all registered touch event handlers.
1484          */
1485         removeTouchEventHandlers: function () {
1486             if (this.hasTouchHandlers && Env.isBrowser) {
1487                 Env.removeEvent(this.containerObj, 'touchstart', this.touchStartListener, this);
1488                 Env.removeEvent(this.containerObj, 'touchmove', this.touchMoveListener, this);
1489 
1490                 if (this.hasTouchEnd) {
1491                     Env.removeEvent(this.document, 'touchend', this.touchEndListener, this);
1492                     this.hasTouchEnd = false;
1493                 }
1494 
1495                 if (this.hasGestureHandlers) {
1496                     Env.removeEvent(this.containerObj, 'gesturestart', this.gestureStartListener, this);
1497                     Env.removeEvent(this.containerObj, 'gesturechange', this.gestureChangeListener, this);
1498                     this.hasGestureHandlers = false;
1499                 }
1500 
1501                 this.hasTouchHandlers = false;
1502             }
1503         },
1504 
1505         /**
1506          * Remove all event handlers from the board object
1507          */
1508         removeEventHandlers: function () {
1509             this.removeMouseEventHandlers();
1510             this.removeTouchEventHandlers();
1511             this.removePointerEventHandlers();
1512         },
1513 
1514         /**
1515          * Handler for click on left arrow in the navigation bar
1516          * @returns {JXG.Board} Reference to the board
1517          */
1518         clickLeftArrow: function () {
1519             this.moveOrigin(this.origin.scrCoords[1] + this.canvasWidth * 0.1, this.origin.scrCoords[2]);
1520             return this;
1521         },
1522 
1523         /**
1524          * Handler for click on right arrow in the navigation bar
1525          * @returns {JXG.Board} Reference to the board
1526          */
1527         clickRightArrow: function () {
1528             this.moveOrigin(this.origin.scrCoords[1] - this.canvasWidth * 0.1, this.origin.scrCoords[2]);
1529             return this;
1530         },
1531 
1532         /**
1533          * Handler for click on up arrow in the navigation bar
1534          * @returns {JXG.Board} Reference to the board
1535          */
1536         clickUpArrow: function () {
1537             this.moveOrigin(this.origin.scrCoords[1], this.origin.scrCoords[2] - this.canvasHeight * 0.1);
1538             return this;
1539         },
1540 
1541         /**
1542          * Handler for click on down arrow in the navigation bar
1543          * @returns {JXG.Board} Reference to the board
1544          */
1545         clickDownArrow: function () {
1546             this.moveOrigin(this.origin.scrCoords[1], this.origin.scrCoords[2] + this.canvasHeight * 0.1);
1547             return this;
1548         },
1549 
1550         /**
1551          * Triggered on iOS/Safari while the user inputs a gesture (e.g. pinch) and is used to zoom into the board.
1552          * Works on iOS/Safari and Android.
1553          * @param {Event} evt Browser event object
1554          * @returns {Boolean}
1555          */
1556         gestureChangeListener: function (evt) {
1557             var c,
1558                 zx = this.attr.zoom.factorx,
1559                 zy = this.attr.zoom.factory,
1560                 dist;
1561 
1562             if (!this.attr.zoom.wheel) {
1563                 return true;
1564             }
1565 
1566             evt.preventDefault();
1567 
1568             if (this.mode === this.BOARD_MODE_ZOOM) {
1569                 c = new Coords(Const.COORDS_BY_SCREEN, this.getMousePosition(evt, 0), this);
1570 
1571                 if (evt.scale === undefined) {
1572                     // Android pinch to zoom
1573                     dist = Geometry.distance([evt.touches[0].clientX, evt.touches[0].clientY],
1574                                     [evt.touches[1].clientX, evt.touches[1].clientY], 2);
1575 
1576                     // evt.scale is undefined in Android
1577                     evt.scale = dist / this.prevDist;
1578                 }
1579                 this.attr.zoom.factorx = evt.scale / this.prevScale;
1580                 this.attr.zoom.factory = evt.scale / this.prevScale;
1581 
1582                 this.zoomIn(c.usrCoords[1], c.usrCoords[2]);
1583                 this.prevScale = evt.scale;
1584 
1585                 this.attr.zoom.factorx = zx;
1586                 this.attr.zoom.factory = zy;
1587             }
1588 
1589             return false;
1590         },
1591 
1592         /**
1593          * Called by iOS/Safari as soon as the user starts a gesture (only works on iOS/Safari).
1594          * @param {Event} evt
1595          * @returns {Boolean}
1596          */
1597         gestureStartListener: function (evt) {
1598 
1599             if (!this.attr.zoom.wheel) {
1600                 return true;
1601             }
1602 
1603             evt.preventDefault();
1604             this.prevScale = 1;
1605 
1606             // Android pinch to zoom
1607             if (evt.scale === undefined) {
1608                 this.prevDist = Geometry.distance([evt.touches[0].clientX, evt.touches[0].clientY],
1609                                 [evt.touches[1].clientX, evt.touches[1].clientY], 2);
1610             }
1611 
1612             if (this.mode === this.BOARD_MODE_NONE) {
1613                 this.mode = this.BOARD_MODE_ZOOM;
1614             }
1615             return false;
1616         },
1617 
1618         /**
1619          * pointer-Events
1620          */
1621 
1622         /**
1623          * This method is called by the browser when a pointing device is pressed on the screen.
1624          * @param {Event} evt The browsers event object.
1625          * @param {Object} object If the object to be dragged is already known, it can be submitted via this parameter
1626          * @returns {Boolean} ...
1627          */
1628         pointerDownListener: function (evt, object) {
1629             var i, j, k, pos, elements, sel,
1630                 eps = this.options.precision.touch,
1631                 found, target, result;
1632 
1633             if (!this.hasPointerUp) {
1634                 if (window.navigator.pointerEnabled) {  // IE11+
1635                     Env.addEvent(this.document, 'pointerup', this.pointerUpListener, this);
1636                 } else {
1637                     Env.addEvent(this.document, 'MSPointerUp', this.pointerUpListener, this);
1638                 }
1639                 this.hasPointerUp = true;
1640             }
1641 
1642             if (this.hasMouseHandlers) {
1643                 this.removeMouseEventHandlers();
1644             }
1645 
1646             if (this.hasTouchHandlers) {
1647                 this.removeTouchEventHandlers();
1648             }
1649 
1650             // prevent accidental selection of text
1651             if (this.document.selection && Type.isFunction(this.document.selection.empty)) {
1652                 this.document.selection.empty();
1653             } else if (window.getSelection) {
1654                 sel = window.getSelection();
1655                 if (sel.removeAllRanges) {
1656                     try {
1657                         sel.removeAllRanges();
1658                     } catch (e) {}
1659                 }
1660             }
1661 
1662             // Touch or pen device
1663             if (JXG.isBrowser && (window.navigator.msMaxTouchPoints && window.navigator.msMaxTouchPoints > 1)) {
1664                 this.options.precision.hasPoint = eps;
1665             }
1666 
1667             // This should be easier than the touch events. Every pointer device gets its own pointerId, e.g. the mouse
1668             // always has id 1, fingers and pens get unique ids every time a pointerDown event is fired and they will
1669             // keep this id until a pointerUp event is fired. What we have to do here is:
1670             //  1. collect all elements under the current pointer
1671             //  2. run through the touches control structure
1672             //    a. look for the object collected in step 1.
1673             //    b. if an object is found, check the number of pointers. if appropriate, add the pointer.
1674 
1675             pos = this.getMousePosition(evt);
1676 
1677             // selection
1678             this._testForSelection(evt);
1679             if (this.selectingMode) {
1680                 this._startSelecting(pos);
1681                 this.triggerEventHandlers(['touchstartselecting', 'pointerstartselecting', 'startselecting'], [evt]);
1682                 return;     // don't continue as a normal click
1683             }
1684 
1685             if (object) {
1686                 elements = [ object ];
1687                 this.mode = this.BOARD_MODE_DRAG;
1688             } else {
1689                 elements = this.initMoveObject(pos[0], pos[1], evt, 'mouse');
1690             }
1691 
1692             // if no draggable object can be found, get out here immediately
1693             if (elements.length > 0) {
1694                 // check touches structure
1695                 target = elements[elements.length - 1];
1696                 found = false;
1697 
1698                 for (i = 0; i < this.touches.length; i++) {
1699                     // the target is already in our touches array, try to add the pointer to the existing touch
1700                     if (this.touches[i].obj === target) {
1701                         j = i;
1702                         k = this.touches[i].targets.push({
1703                             num: evt.pointerId,
1704                             X: pos[0],
1705                             Y: pos[1],
1706                             Xprev: NaN,
1707                             Yprev: NaN,
1708                             Xstart: [],
1709                             Ystart: [],
1710                             Zstart: []
1711                         }) - 1;
1712 
1713                         found = true;
1714                         break;
1715                     }
1716                 }
1717 
1718                 if (!found) {
1719                     k = 0;
1720                     j = this.touches.push({
1721                         obj: target,
1722                         targets: [{
1723                             num: evt.pointerId,
1724                             X: pos[0],
1725                             Y: pos[1],
1726                             Xprev: NaN,
1727                             Yprev: NaN,
1728                             Xstart: [],
1729                             Ystart: [],
1730                             Zstart: []
1731                         }]
1732                     }) - 1;
1733                 }
1734 
1735                 this.dehighlightAll();
1736                 target.highlight(true);
1737 
1738                 this.saveStartPos(target, this.touches[j].targets[k]);
1739 
1740                 // prevent accidental text selection
1741                 // this could get us new trouble: input fields, links and drop down boxes placed as text
1742                 // on the board don't work anymore.
1743                 if (evt && evt.preventDefault) {
1744                     evt.preventDefault();
1745                 } else if (window.event) {
1746                     window.event.returnValue = false;
1747                 }
1748             }
1749 
1750             if (this.touches.length > 0) {
1751                 evt.preventDefault();
1752                 evt.stopPropagation();
1753             }
1754 
1755             // move origin - but only if we're not in drag mode
1756             if (this.mode === this.BOARD_MODE_NONE && this.mouseOriginMoveStart(evt)) {
1757                 this.triggerEventHandlers(['touchstart', 'down', 'pointerdown', 'MSPointerDown'], [evt]