1 /*
  2     Copyright 2008-2013
  3         Matthias Ehmann,
  4         Michael Gerhaeuser,
  5         Carsten Miller,
  6         Bianca Valentin,
  7         Alfred Wassermann,
  8         Peter Wilfahrt
  9 
 10     This file is part of JSXGraph.
 11 
 12     JSXGraph is free software dual licensed under the GNU LGPL or MIT License.
 13 
 14     You can redistribute it and/or modify it under the terms of the
 15 
 16       * GNU Lesser General Public License as published by
 17         the Free Software Foundation, either version 3 of the License, or
 18         (at your option) any later version
 19       OR
 20       * MIT License: https://github.com/jsxgraph/jsxgraph/blob/master/LICENSE.MIT
 21 
 22     JSXGraph is distributed in the hope that it will be useful,
 23     but WITHOUT ANY WARRANTY; without even the implied warranty of
 24     MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 25     GNU Lesser General Public License for more details.
 26 
 27     You should have received a copy of the GNU Lesser General Public License and
 28     the MIT License along with JSXGraph. If not, see <http://www.gnu.org/licenses/>
 29     and <http://opensource.org/licenses/MIT/>.
 30  */
 31 
 32 
 33 /*global JXG: true, define: true, window: true, document: true, navigator: true, module: true, global: true, self: true, require: true*/
 34 /*jslint nomen: true, plusplus: true*/
 35 
 36 /* depends:
 37  jxg
 38  utils/type
 39  */
 40 
 41 /**
 42  * @fileoverview The functions in this file help with the detection of the environment JSXGraph runs in. We can distinguish
 43  * between node.js, windows 8 app and browser, what rendering techniques are supported and (most of the time) if the device
 44  * the browser runs on is a tablet/cell or a desktop computer.
 45  */
 46 
 47 define(['jxg', 'utils/type'], function (JXG, Type) {
 48 
 49     "use strict";
 50 
 51     JXG.extend(JXG, /** @lends JXG */ {
 52         /**
 53          * Determines the property that stores the relevant information in the event object.
 54          * @type {String}
 55          * @default 'touches'
 56          */
 57         touchProperty: 'touches',
 58 
 59         /**
 60          * A document/window environment is available.
 61          * @type Boolean
 62          * @default false
 63          */
 64         isBrowser: typeof window === 'object' && typeof document === 'object',
 65 
 66         /**
 67          * Detect browser support for VML.
 68          * @returns {Boolean} True, if the browser supports VML.
 69          */
 70         supportsVML: function () {
 71             // From stackoverflow.com
 72             return this.isBrowser && !!document.namespaces;
 73         },
 74 
 75         /**
 76          * Detect browser support for SVG.
 77          * @returns {Boolean} True, if the browser supports SVG.
 78          */
 79         supportsSVG: function () {
 80             return this.isBrowser && document.implementation.hasFeature('http://www.w3.org/TR/SVG11/feature#BasicStructure', '1.1');
 81         },
 82 
 83         /**
 84          * Detect browser support for Canvas.
 85          * @returns {Boolean} True, if the browser supports HTML canvas.
 86          */
 87         supportsCanvas: function () {
 88             var c,
 89                 hasCanvas = false;
 90 
 91             if (this.isNode()) {
 92                 try {
 93                     c = (typeof module === 'object' ? module.require('canvas') : require('canvas'));
 94                     hasCanvas = true;
 95                 } catch (err) { }
 96             }
 97 
 98             return hasCanvas || (this.isBrowser && !!document.createElement('canvas').getContext);
 99         },
100 
101         /**
102          * True, if run inside a node.js environment.
103          * @returns {Boolean}
104          */
105         isNode: function () {
106             // this is not a 100% sure but should be valid in most cases
107 
108                 // we are not inside a browser
109             return !this.isBrowser && (
110                 // there is a module object (plain node, no requirejs)
111                 (typeof module === 'object' && !!module.exports) ||
112                 // there is a global object and requirejs is loaded
113                 (typeof global === 'object' && global.requirejsVars && !global.requirejsVars.isBrowser)
114             );
115         },
116 
117         /**
118          * True if run inside a webworker environment.
119          * @returns {Boolean}
120          */
121         isWebWorker: function () {
122             return !this.isBrowser && (typeof self === 'object' && typeof self.postMessage === 'function');
123         },
124 
125         /**
126          * Checks if the environments supports the W3C Pointer Events API {@link http://www.w3.org/Submission/pointer-events/}
127          * @return {Boolean}
128          */
129         supportsPointerEvents: function () {
130             return JXG.isBrowser && window.navigator && (window.navigator.msPointerEnabled || window.navigator.pointerEnabled);
131         },
132 
133         /**
134          * Determine if the current browser supports touch events
135          * @returns {Boolean} True, if the browser supports touch events.
136          */
137         isTouchDevice: function () {
138             return this.isBrowser && window.ontouchstart !== undefined;
139         },
140 
141         /**
142          * Detects if the user is using an Android powered device.
143          * @returns {Boolean}
144          */
145         isAndroid: function () {
146             return Type.exists(navigator) && navigator.userAgent.toLowerCase().indexOf('android') > -1;
147         },
148 
149         /**
150          * Detects if the user is using the default Webkit browser on an Android powered device.
151          * @returns {Boolean}
152          */
153         isWebkitAndroid: function () {
154             return this.isAndroid() && navigator.userAgent.indexOf(' AppleWebKit/') > -1;
155         },
156 
157         /**
158          * Detects if the user is using a Apple iPad / iPhone.
159          * @returns {Boolean}
160          */
161         isApple: function () {
162             return Type.exists(navigator) && (navigator.userAgent.indexOf('iPad') > -1 || navigator.userAgent.indexOf('iPhone') > -1);
163         },
164 
165         /**
166          * Detects if the user is using Safari on an Apple device.
167          * @returns {Boolean}
168          */
169         isWebkitApple: function () {
170             return this.isApple() && (navigator.userAgent.search(/Mobile\/[0-9A-Za-z\.]*Safari/) > -1);
171         },
172 
173         /**
174          * Returns true if the run inside a Windows 8 "Metro" App.
175          * @return {Boolean}
176          */
177         isMetroApp: function () {
178             return typeof window === 'object' && window.clientInformation && window.clientInformation.appName && window.clientInformation.appName.indexOf('MSAppHost') > -1;
179         },
180 
181         /**
182          * Detects if the user is using a Mozilla browser
183          * @returns {Boolean}
184          */
185         isMozilla: function () {
186             return Type.exists(navigator) &&
187                 navigator.userAgent.toLowerCase().indexOf('mozilla') > -1 &&
188                 navigator.userAgent.toLowerCase().indexOf('apple') === -1;
189         },
190 
191         /**
192          * Detects if the user is using a firefoxOS powered device.
193          * @returns {Boolean}
194          */
195         isFirefoxOS: function () {
196             return Type.exists(navigator) &&
197                 navigator.userAgent.toLowerCase().indexOf('android') === -1 &&
198                 navigator.userAgent.toLowerCase().indexOf('apple') === -1 &&
199                 navigator.userAgent.toLowerCase().indexOf('mobile') > -1 &&
200                 navigator.userAgent.toLowerCase().indexOf('mozilla') > -1;
201         },
202 
203         /**
204          * Internet Explorer version. Works only for IE > 4.
205          * @type Number
206          */
207         ieVersion: (function () {
208             var undef, div, all,
209                 v = 3;
210 
211             if (typeof document !== 'object') {
212                 return 0;
213             }
214 
215             div = document.createElement('div');
216             all = div.getElementsByTagName('i');
217 
218             do {
219                 div.innerHTML = '<!--[if gt IE ' + (++v) + ']><' + 'i><' + '/i><![endif]-->';
220             } while (all[0]);
221 
222             return v > 4 ? v : undef;
223 
224         }()),
225 
226         /**
227          * Reads the width and height of an HTML element.
228          * @param {String} elementId The HTML id of an HTML DOM node.
229          * @returns {Object} An object with the two properties width and height.
230          */
231         getDimensions: function (elementId, doc) {
232             var element, display, els, originalVisibility, originalPosition,
233                 originalDisplay, originalWidth, originalHeight;
234 
235             if (!JXG.isBrowser || elementId === null) {
236                 return {
237                     width: 500,
238                     height: 500
239                 };
240             }
241 
242             doc = doc || document;
243             // Borrowed from prototype.js
244             element = doc.getElementById(elementId);
245             if (!Type.exists(element)) {
246                 throw new Error("\nJSXGraph: HTML container element '" + elementId + "' not found.");
247             }
248 
249             display = element.style.display;
250 
251             // Work around a bug in Safari
252             if (display !== 'none' && display !== null) {
253                 return {width: element.offsetWidth, height: element.offsetHeight};
254             }
255 
256             // All *Width and *Height properties give 0 on elements with display set to none,
257             // hence we show the element temporarily
258             els = element.style;
259 
260             // save style
261             originalVisibility = els.visibility;
262             originalPosition = els.position;
263             originalDisplay = els.display;
264 
265             // show element
266             els.visibility = 'hidden';
267             els.position = 'absolute';
268             els.display = 'block';
269 
270             // read the dimension
271             originalWidth = element.clientWidth;
272             originalHeight = element.clientHeight;
273 
274             // restore original css values
275             els.display = originalDisplay;
276             els.position = originalPosition;
277             els.visibility = originalVisibility;
278 
279             return {
280                 width: originalWidth,
281                 height: originalHeight
282             };
283         },
284 
285         /**
286          * Adds an event listener to a DOM element.
287          * @param {Object} obj Reference to a DOM node.
288          * @param {String} type The event to catch, without leading 'on', e.g. 'mousemove' instead of 'onmousemove'.
289          * @param {Function} fn The function to call when the event is triggered.
290          * @param {Object} owner The scope in which the event trigger is called.
291          */
292         addEvent: function (obj, type, fn, owner) {
293             var el = function () {
294                 return fn.apply(owner, arguments);
295             };
296 
297             el.origin = fn;
298             owner['x_internal' + type] = owner['x_internal' + type] || [];
299             owner['x_internal' + type].push(el);
300 
301             // Non-IE browser
302             if (Type.exists(obj) && Type.exists(obj.addEventListener)) {
303                 obj.addEventListener(type, el, false);
304             }
305 
306             // IE
307             if (Type.exists(obj) && Type.exists(obj.attachEvent)) {
308                 obj.attachEvent('on' + type, el);
309             }
310         },
311 
312         /**
313          * Removes an event listener from a DOM element.
314          * @param {Object} obj Reference to a DOM node.
315          * @param {String} type The event to catch, without leading 'on', e.g. 'mousemove' instead of 'onmousemove'.
316          * @param {Function} fn The function to call when the event is triggered.
317          * @param {Object} owner The scope in which the event trigger is called.
318          */
319         removeEvent: function (obj, type, fn, owner) {
320             var i;
321 
322             if (!Type.exists(owner)) {
323                 JXG.debug('no such owner');
324                 return;
325             }
326 
327             if (!Type.exists(owner['x_internal' + type])) {
328                 JXG.debug('no such type: ' + type);
329                 return;
330             }
331 
332             if (!Type.isArray(owner['x_internal' + type])) {
333                 JXG.debug('owner[x_internal + ' + type + '] is not an array');
334                 return;
335             }
336 
337             i = Type.indexOf(owner['x_internal' + type], fn, 'origin');
338 
339             if (i === -1) {
340                 JXG.debug('no such event function in internal list: ' + fn);
341                 return;
342             }
343 
344             try {
345                 // Non-IE browser
346                 if (Type.exists(obj) && Type.exists(obj.removeEventListener)) {
347                     obj.removeEventListener(type, owner['x_internal' + type][i], false);
348                 }
349 
350                 // IE
351                 if (Type.exists(obj) && Type.exists(obj.detachEvent)) {
352                     obj.detachEvent('on' + type, owner['x_internal' + type][i]);
353                 }
354             } catch (e) {
355                 JXG.debug('event not registered in browser: (' + type + ' -- ' + fn + ')');
356             }
357 
358             owner['x_internal' + type].splice(i, 1);
359         },
360 
361         /**
362          * Removes all events of the given type from a given DOM node; Use with caution and do not use it on a container div
363          * of a {@link JXG.Board} because this might corrupt the event handling system.
364          * @param {Object} obj Reference to a DOM node.
365          * @param {String} type The event to catch, without leading 'on', e.g. 'mousemove' instead of 'onmousemove'.
366          * @param {Object} owner The scope in which the event trigger is called.
367          */
368         removeAllEvents: function (obj, type, owner) {
369             var i, len;
370             if (owner['x_internal' + type]) {
371                 len = owner['x_internal' + type].length;
372 
373                 for (i = len - 1; i >= 0; i--) {
374                     JXG.removeEvent(obj, type, owner['x_internal' + type][i].origin, owner);
375                 }
376 
377                 if (owner['x_internal' + type].length > 0) {
378                     JXG.debug('removeAllEvents: Not all events could be removed.');
379                 }
380             }
381         },
382 
383         /**
384          * Cross browser mouse / touch coordinates retrieval relative to the board's top left corner.
385          * @param {Object} [e] The browsers event object. If omitted, <tt>window.event</tt> will be used.
386          * @param {Number} [index] If <tt>e</tt> is a touch event, this provides the index of the touch coordinates, i.e. it determines which finger.
387          * @param {Object} [doc] The document object.
388          * @returns {Array} Contains the position as x,y-coordinates in the first resp. second component.
389          */
390         getPosition: function (e, index, doc) {
391             var i, len, evtTouches,
392                 posx = 0,
393                 posy = 0;
394 
395             if (!e) {
396                 e = window.event;
397             }
398             
399             doc = doc || document;
400             evtTouches = e[JXG.touchProperty];
401 
402             if (Type.exists(index) && Type.exists(evtTouches)) {
403                 if (index === -1) {
404                     len = evtTouches.length;
405 
406                     for (i = 0; i < len; i++) {
407                         if (evtTouches[i]) {
408                             e = evtTouches[i];
409                             break;
410                         }
411                     }
412                 } else {
413                     e = evtTouches[index];
414                 }
415             }
416 
417             if (e.pageX || e.pageY) {
418                 posx = e.pageX;
419                 posy = e.pageY;
420             } else if (e.clientX || e.clientY) {
421                 posx = e.clientX + doc.body.scrollLeft + doc.documentElement.scrollLeft;
422                 posy = e.clientY + doc.body.scrollTop + doc.documentElement.scrollTop;
423             }
424 
425             return [posx, posy];
426         },
427 
428         /**
429          * Calculates recursively the offset of the DOM element in which the board is stored.
430          * @param {Object} obj A DOM element
431          * @returns {Array} An array with the elements left and top offset.
432          */
433         getOffset: function (obj) {
434             var cPos,
435                 o = obj,
436                 o2 = obj,
437                 l = o.offsetLeft - o.scrollLeft,
438                 t = o.offsetTop - o.scrollTop;
439 
440             cPos = this.getCSSTransform([l, t], o);
441             l = cPos[0];
442             t = cPos[1];
443 
444             /*
445              * In Mozilla and Webkit: offsetParent seems to jump at least to the next iframe,
446              * if not to the body. In IE and if we are in an position:absolute environment
447              * offsetParent walks up the DOM hierarchy.
448              * In order to walk up the DOM hierarchy also in Mozilla and Webkit
449              * we need the parentNode steps.
450              */
451             o = o.offsetParent;
452             while (o) {
453                 l += o.offsetLeft;
454                 t += o.offsetTop;
455 
456                 if (o.offsetParent) {
457                     l += o.clientLeft - o.scrollLeft;
458                     t += o.clientTop - o.scrollTop;
459                 }
460 
461                 cPos = this.getCSSTransform([l, t], o);
462                 l = cPos[0];
463                 t = cPos[1];
464 
465                 o2 = o2.parentNode;
466 
467                 while (o2 !== o) {
468                     l += o2.clientLeft - o2.scrollLeft;
469                     t += o2.clientTop - o2.scrollTop;
470 
471                     cPos = this.getCSSTransform([l, t], o2);
472                     l = cPos[0];
473                     t = cPos[1];
474 
475                     o2 = o2.parentNode;
476                 }
477                 o = o.offsetParent;
478             }
479             return [l, t];
480         },
481 
482         /**
483          * Access CSS style sheets.
484          * @param {Object} obj A DOM element
485          * @param {String} stylename The CSS property to read.
486          * @returns The value of the CSS property and <tt>undefined</tt> if it is not set.
487          */
488         getStyle: function (obj, stylename) {
489             var r, doc;
490 
491             doc = obj.ownerDocument;
492             
493             // Non-IE
494             if (window.getComputedStyle) {
495                 r = doc.defaultView.getComputedStyle(obj, null).getPropertyValue(stylename);
496             // IE
497             } else if (obj.currentStyle && JXG.ieVersion >= 9) {
498                 r = obj.currentStyle[stylename];
499             } else {
500                 if (obj.style) {
501                     // make stylename lower camelcase
502                     stylename = stylename.replace(/-([a-z]|[0-9])/ig, function (all, letter) {
503                         return letter.toUpperCase();
504                     });
505                     r = obj.style[stylename];
506                 }
507             }
508 
509             return r;
510         },
511 
512         /**
513          * Reads css style sheets of a given element. This method is a getStyle wrapper and
514          * defaults the read value to <tt>0</tt> if it can't be parsed as an integer value.
515          * @param {DOMElement} el
516          * @param {string} css
517          * @returns {number}
518          */
519         getProp: function (el, css) {
520             var n = parseInt(this.getStyle(el, css), 10);
521             return isNaN(n) ? 0 : n;
522         },
523 
524         /**
525          * Correct position of upper left corner in case of
526          * a CSS transformation. Here, only translations are
527          * extracted. All scaling transformations are corrected
528          * in {@link JXG.Board#getMousePosition}.
529          * @param {Array} cPos Previously determined position
530          * @param {Object} obj A DOM element
531          * @returns {Array} The corrected position.
532          */
533         getCSSTransform: function (cPos, obj) {
534             var i, j, str, arrStr, start, len, len2, arr,
535                 t = ['transform', 'webkitTransform', 'MozTransform', 'msTransform', 'oTransform'];
536 
537             // Take the first transformation matrix
538             len = t.length;
539 
540             for (i = 0, str = ''; i < len; i++) {
541                 if (Type.exists(obj.style[t[i]])) {
542                     str = obj.style[t[i]];
543                     break;
544                 }
545             }
546 
547             /**
548              * Extract the coordinates and apply the transformation
549              * to cPos
550              */
551             if (str !== '') {
552                 start = str.indexOf('(');
553 
554                 if (start > 0) {
555                     len = str.length;
556                     arrStr = str.substring(start + 1, len - 1);
557                     arr = arrStr.split(',');
558 
559                     for (j = 0, len2 = arr.length; j < len2; j++) {
560                         arr[j] = parseFloat(arr[j]);
561                     }
562 
563                     if (str.indexOf('matrix') === 0) {
564                         cPos[0] += arr[4];
565                         cPos[1] += arr[5];
566                     } else if (str.indexOf('translateX') === 0) {
567                         cPos[0] += arr[0];
568                     } else if (str.indexOf('translateY') === 0) {
569                         cPos[1] += arr[0];
570                     } else if (str.indexOf('translate') === 0) {
571                         cPos[0] += arr[0];
572                         cPos[1] += arr[1];
573                     }
574                 }
575             }
576             return cPos;
577         },
578 
579         /**
580          * Scaling CSS transformations applied to the div element containing the JSXGraph constructions
581          * are determined. Not implemented are 'rotate', 'skew', 'skewX', 'skewY'.
582          * @returns {Array} 3x3 transformation matrix. See {@link JXG.Board#updateCSSTransforms}.
583          */
584         getCSSTransformMatrix: function (obj) {
585             var i, j, str, arrstr, start, len, len2, arr,
586                 t = ['transform', 'webkitTransform', 'MozTransform', 'msTransform', 'oTransform'],
587                 mat = [[1, 0, 0],
588                     [0, 1, 0],
589                     [0, 0, 1]];
590 
591             // Take the first transformation matrix
592             len = t.length;
593             for (i = 0, str = ''; i < len; i++) {
594                 if (Type.exists(obj.style[t[i]])) {
595                     str = obj.style[t[i]];
596                     break;
597                 }
598             }
599 
600             if (str !== '') {
601                 start = str.indexOf('(');
602 
603                 if (start > 0) {
604                     len = str.length;
605                     arrstr = str.substring(start + 1, len - 1);
606                     arr = arrstr.split(',');
607 
608                     for (j = 0, len2 = arr.length; j < len2; j++) {
609                         arr[j] = parseFloat(arr[j]);
610                     }
611 
612                     if (str.indexOf('matrix') === 0) {
613                         mat = [[1, 0, 0],
614                             [0, arr[0], arr[1]],
615                             [0, arr[2], arr[3]]];
616                         // Missing are rotate, skew, skewX, skewY
617                     } else if (str.indexOf('scaleX') === 0) {
618                         mat[1][1] = arr[0];
619                     } else if (str.indexOf('scaleY') === 0) {
620                         mat[2][2] = arr[0];
621                     } else if (str.indexOf('scale') === 0) {
622                         mat[1][1] = arr[0];
623                         mat[2][2] = arr[1];
624                     }
625                 }
626             }
627             return mat;
628         },
629 
630         /**
631          * Process data in timed chunks. Data which takes long to process, either because it is such
632          * a huge amount of data or the processing takes some time, causes warnings in browsers about
633          * irresponsive scripts. To prevent these warnings, the processing is split into smaller pieces
634          * called chunks which will be processed in serial order.
635          * Copyright 2009 Nicholas C. Zakas. All rights reserved. MIT Licensed
636          * @param {Array} items to do
637          * @param {Function} process Function that is applied for every array item
638          * @param {Object} context The scope of function process
639          * @param {Function} callback This function is called after the last array element has been processed.
640          */
641         timedChunk: function (items, process, context, callback) {
642             //create a clone of the original
643             var todo = items.concat(),
644                 timerFun = function () {
645                     var start = +new Date();
646 
647                     do {
648                         process.call(context, todo.shift());
649                     } while (todo.length > 0 && (+new Date() - start < 300));
650 
651                     if (todo.length > 0) {
652                         window.setTimeout(timerFun, 1);
653                     } else {
654                         callback(items);
655                     }
656                 };
657 
658             window.setTimeout(timerFun, 1);
659         }
660     });
661 
662     return JXG;
663 });
664