1 /**
  2  * The Render Engine
  3  * Engine Support Class
  4  *
  5  * @fileoverview A support class for the engine with useful methods
  6  *               to manipulate arrays, parse JSON, and handle query parameters.
  7  *
  8  * @author: Brett Fattori (brettf@renderengine.com)
  9  * @author: $Author: bfattori@gmail.com $
 10  * @version: $Revision: 1569 $
 11  *
 12  * Copyright (c) 2011 Brett Fattori (brettf@renderengine.com)
 13  *
 14  * Permission is hereby granted, free of charge, to any person obtaining a copy
 15  * of this software and associated documentation files (the "Software"), to deal
 16  * in the Software without restriction, including without limitation the rights
 17  * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
 18  * copies of the Software, and to permit persons to whom the Software is
 19  * furnished to do so, subject to the following conditions:
 20  *
 21  * The above copyright notice and this permission notice shall be included in
 22  * all copies or substantial portions of the Software.
 23  *
 24  * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
 25  * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
 26  * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
 27  * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
 28  * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
 29  * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
 30  * THE SOFTWARE.
 31  *
 32  */
 33 
 34 /**
 35  * @class A static class with support methods the engine or games can use.
 36  *        Many of the methods can be used to manipulate arrays.  Additional
 37  *        methods are provided to access query parameters, and generate or
 38  *        read JSON.  A system capabilities method, {@link #sysInfo}, can be
 39  *        used to query the environment for support of features.
 40  * @static
 41  */
 42 R.engine.Support = Base.extend(/** @scope R.engine.Support.prototype */{
 43     constructor:null,
 44 
 45     /**
 46      * Get the index of an element in the specified array.
 47      *
 48      * @param array {Array} The array to scan
 49      * @param obj {Object} The object to find
 50      * @param [from=0] {Number} The index to start at, defaults to zero.
 51      * @memberOf R.engine.Support
 52      */
 53     indexOf:function (array, obj, from) {
 54         return array && R.isArray(array) ? array.indexOf(obj, from) : -1;
 55     },
 56 
 57     /**
 58      * Remove an element from an array.  This method modifies the array
 59      * directly.
 60      *
 61      * @param array {Array} The array to modify
 62      * @param obj {Object} The object to remove
 63      * @memberOf R.engine.Support
 64      */
 65     arrayRemove:function (array, obj) {
 66         if (!array || !R.isArray(array)) {
 67             return;
 68         }
 69 
 70         var idx = R.engine.Support.indexOf(array, obj);
 71         if (idx != -1) {
 72             array.splice(idx, 1);
 73         }
 74     },
 75 
 76     /**
 77      * Returns <tt>true</tt> if the string, after trimming, is either
 78      * empty or is null.
 79      *
 80      * @param str {String} The string to test
 81      * @return {Boolean} <tt>true</tt> if the string is empty or <tt>null</tt>
 82      * @memberOf R.engine.Support
 83      */
 84     isEmpty:function (str) {
 85         return R.isEmpty(str);
 86     },
 87 
 88     /**
 89      * Calls a provided callback function once for each element in
 90      * an array, and constructs a new array of all the values for which
 91      * callback returns a <tt>true</tt> value. callback is invoked only
 92      * for indexes of the array which have assigned values; it is not invoked
 93      * for indexes which have been deleted or which have never been assigned
 94      * values. Array elements which do not pass the callback test are simply
 95      * skipped, and are not included in the new array.
 96      *
 97      * @param array {Array} The array to filter.
 98      * @param fn {Function} The callback to invoke.  It will be passed three
 99      *                      arguments: The element value, the index of the element,
100      *                      and the array being traversed.
101      * @param [thisp=null] {Object} Used as <tt>this</tt> for each invocation of the
102      *                       callback.
103      * @memberOf R.engine.Support
104      */
105     filter:function (array, fn, thisp) {
106         return array && R.isArray(array) ? array.filter(fn, thisp) : undefined;
107     },
108 
109     /**
110      * Executes a callback for each element within an array.
111      *
112      * @param array {Array} The array to operate on
113      * @param fn {Function} The function to apply to each element.  It will be passed three
114      *                      arguments: The element value, the index of the element,
115      *                      and the array being traversed.
116      * @param [thisp=null] {Object} An optional "this" pointer to use in the callback
117      * @memberOf R.engine.Support
118      */
119     forEach:function (array, fn, thisp) {
120         return array && R.isArray(array) ? array.forEach(fn, thisp) : undefined;
121     },
122 
123     /**
124      * Fill the specified array with <tt>size</tt> elements
125      * each with the value "<tt>value</tt>".  Modifies the provided
126      * array directly.
127      *
128      * @param {Array} arr The array to fill
129      * @param {Number} size The size of the array to fill
130      * @param {Object} value The value to put at each index
131      * @memberOf R.engine.Support
132      */
133     fillArray:function (arr, size, value) {
134         for (var i = 0; i < size; i++) {
135             arr[i] = value;
136         }
137     },
138 
139     /**
140      * Get the path from a fully qualified URL, not including the trailing
141      * slash character.
142      *
143      * @param url {String} The URL
144      * @return {String} The path
145      * @memberOf R.engine.Support
146      */
147     getPath:function (url) {
148         return R.isString(url) ? url.substr(0, url.lastIndexOf("/")) : undefined;
149     },
150 
151     /**
152      * Get the query parameters from the window location object.  The
153      * object returned will contain a key/value pair for each argument
154      * found.
155      *
156      * @return {Object} A generic <tt>Object</tt> with a key and value for each query argument.
157      * @memberOf R.engine.Support
158      */
159     getQueryParams:function () {
160         if (!R.engine.Support.parms) {
161             R.engine.Support.parms = {};
162             var p = window.location.toString().split("?")[1];
163             if (p) {
164                 p = p.split("&");
165                 for (var x = 0; x < p.length; x++) {
166                     var v = p[x].split("=");
167                     R.engine.Support.parms[v[0]] = (v.length > 1 ? v[1] : "");
168                 }
169             }
170         }
171         return R.engine.Support.parms;
172     },
173 
174     /**
175      * Check for a query parameter and to see if it evaluates to one of the following:
176      * <tt>true</tt>, <tt>1</tt>, <tt>yes</tt>, or <tt>y</tt>.  If so, returns <tt>true</tt>.
177      *
178      * @param paramName {String} The query parameter name
179      * @return {Boolean} <tt>true</tt> if the query parameter exists and is one of the specified values.
180      * @memberOf R.engine.Support
181      */
182     checkBooleanParam:function (paramName) {
183         return (R.engine.Support.getQueryParams()[paramName] &&
184             (R.engine.Support.getQueryParams()[paramName].toLowerCase() != "0" ||
185                 R.engine.Support.getQueryParams()[paramName].toLowerCase() != "false"));
186     },
187 
188     /**
189      * Check for a query parameter and to see if it evaluates to the specified value.
190      * If so, returns <tt>true</tt>.
191      *
192      * @param paramName {String} The query parameter name
193      * @param val {String} The value to check for
194      * @return {Boolean} <tt>true</tt> if the query parameter exists and is the value specified
195      * @memberOf R.engine.Support
196      */
197     checkStringParam:function (paramName, val) {
198         return (R.engine.Support.getStringParam(paramName, null) == val);
199     },
200 
201     /**
202      * Check for a query parameter and to see if it evaluates to the specified number.
203      * If so, returns <tt>true</tt>.
204      *
205      * @param paramName {String} The query parameter name
206      * @param val {Number} The number to check for
207      * @return {Boolean} <tt>true</tt> if the query parameter exists and is the value specified
208      * @memberOf R.engine.Support
209      */
210     checkNumericParam:function (paramName, val) {
211         var num = R.engine.Support.getStringParam(paramName, null);
212         return (R.isNumber(num) && num == val);
213     },
214 
215     /**
216      * Get a numeric query parameter, or the default specified if the parameter
217      * doesn't exist.
218      *
219      * @param paramName {String} The name of the parameter
220      * @param defaultVal {Number} The number to return if the parameter doesn't exist
221      * @return {Number} The value
222      * @memberOf R.engine.Support
223      */
224     getNumericParam:function (paramName, defaultVal) {
225         return Number(R.engine.Support.getStringParam(paramName, defaultVal));
226     },
227 
228     /**
229      * Get a string query parameter, or the default specified if the parameter
230      * doesn't exist.
231      *
232      * @param paramName {String} The name of the parameter
233      * @param defaultVal {String} The string to return if the parameter doesn't exist
234      * @return {String} The value
235      * @memberOf R.engine.Support
236      */
237     getStringParam:function (paramName, defaultVal) {
238         return (R.engine.Support.getQueryParams()[paramName] || defaultVal);
239     },
240 
241     /**
242      * Returns specified object as a JavaScript Object Notation (JSON) string.
243      *
244      * @param object {Object} Must not be undefined or contain undefined types and variables.
245      * @return String
246      * @memberOf R.engine.Support
247      * @deprecated Use <tt>JSON.stringify()</tt>
248      */
249     toJSON:function (o) {
250         return window.JSON.stringify(o);
251     },
252 
253     /**
254      * Parses specified JavaScript Object Notation (JSON) string back into its corresponding object.
255      *
256      * @param jsonString
257      * @return Object
258      * @see http://www.json.org
259      * @memberOf R.engine.Support
260      * @deprecated Use <tt>JSON.parse()</tt> instead
261      */
262     parseJSON:function (jsonString) {
263         return JSON.parse(jsonString);
264     },
265 
266     /**
267      * Determine the OS platform from the user agent string, if possible
268      * @private
269      * @memberOf R.engine.Support
270      */
271     checkOS:function () {
272         // Scrape the userAgent to get the OS
273         var uA = navigator.userAgent.toLowerCase();
274         return /windows nt 6\.0/.test(uA) ? "Windows Vista" :
275             /windows nt 6\.1/.test(uA) ? "Windows 7" :
276                 /windows nt 5\.1/.test(uA) ? "Windows XP" :
277                     /windows/.test(uA) ? "Windows" :
278                         /android 1\./.test(uA) ? "Android 1.x" :
279                             /android 2\./.test(uA) ? "Android 2.x" :
280                                 /android/.test(uA) ? "Android" :
281                                     /x11/.test(uA) ? "X11" :
282                                         /linux/.test(uA) ? "Linux" :
283                                             /Mac OS X/.test(uA) ? "Mac OS X" :
284                                                 /macintosh/.test(uA) ? "Macintosh" :
285                                                     /iphone|ipad|ipod/.test(uA) ? "iOS" :
286                                                         "unknown";
287     },
288 
289     /**
290      * Gets an object that is a collation of a number of browser and
291      * client settings.  You can use this information to tailor a game
292      * to the environment it is running within.
293      * <ul>
294      * <li>browser - A string indicating the browser type (safari, mozilla, opera, msie)</li>
295      * <li>version - The browser version</li>
296      * <li>agent - The user agent</li>
297      * <li>platform - The platform the browser is running on</li>
298      * <li>cpu - The CPU on the machine the browser is running on</li>
299      * <li>OS - The operating system the browser is running on</li>
300      * <li>language - The browser's language</li>
301      * <li>online - If the browser is running in online mode</li>
302      * <li>cookies - If the browser supports cookies</li>
303      * <li>fullscreen - If the browser is running in fullscreen mode</li>
304      * <li>width - The browser's viewable width</li>
305      * <li>height - The browser's viewable height</li>
306      * <li>viewWidth - The innerWidth of the viewport</li>
307      * <li>viewHeight - The innerHeight of the viewport</li>
308      * <li>support:
309      *    <ul><li>xhr - Browser supports XMLHttpRequest object</li>
310      *    <li>geo - navigator.geolocation is supported</li>
311      *    <li>threads - Browser supports Worker threads</li>
312      *    <li>sockets - Browser supports WebSocket object</li>
313      *    <li>storage:
314      *       <ul><li>cookie - Cookie support. Reports an object with "maxLength", or <code>false</code></li>
315      *       <li>local - localStorage support</li>
316      *       <li>session - sessionStorage support</li>
317      *       <li>indexeddb - indexedDB support</li>
318      *       <li>sqllite - SQL lite support</li>
319      *       <li>audio - HTML5 Audio support</li>
320      *       <li>video - HTML5 Video support</li>
321      *       </ul>
322      *    </li>
323      *    <li>canvas:
324      *       <ul><li>defined - Canvas is either native or emulated</li>
325      *       <li>text - Supports text</li>
326      *       <li>textMetrics - Supports text measurement</li>
327      *            <li>contexts:
328      *              <ul><li>ctx2D - Supports 2D</li>
329      *                <li>ctxGL - Supports webGL</li>
330      *                </ul>
331      *            </li>
332      *       </ul>
333      *    </li>
334      *    </ul>
335      * </li>
336      * </ul>
337      * @return {Object} An object with system information
338      * @memberOf R.engine.Support
339      */
340     sysInfo:function () {
341         if (!R.engine.Support._sysInfo) {
342 
343             // Canvas and Storage support defaults
344             var canvasSupport = {
345                     defined:false,
346                     text:false,
347                     textMetrics:false,
348                     contexts:{
349                         "2D":false,
350                         "GL":false
351                     }
352                 },
353                 storageSupport = {
354                     cookie:false,
355                     local:false,
356                     session:false,
357                     indexeddb:false,
358                     sqllite:false
359                 };
360 
361             // Check for canvas support
362             try {
363                 var canvas = document.createElement("canvas");
364                 if (typeof canvas !== "undefined" && R.isFunction(canvas.getContext)) {
365                     canvasSupport.defined = true;
366                     var c2d = canvas.getContext("2d");
367                     if (typeof c2d !== "undefined") {
368                         canvasSupport.contexts["2D"] = true;
369 
370                         // Does it support native text
371                         canvasSupport.text = (R.isFunction(c2d.fillText));
372                         canvasSupport.textMetrics = (R.isFunction(c2d.measureText));
373                     } else {
374                         canvasSupport.contexts["2D"] = false;
375                     }
376 
377                     try {
378                         var webGL = canvas.getContext("webgl");
379                         if (typeof webGL !== "undefined") {
380                             canvasSupport.contexts["GL"] = true;
381                         }
382                     } catch (ex) {
383                         canvasSupport.contexts["GL"] = false;
384                     }
385                 }
386             } catch (ex) { /* ignore */
387             }
388 
389             // Check storage support
390             try {
391                 try {
392                     // Drop a cookie, then look for it (3kb max)
393                     for (var i = 0, j = []; i < 3072; i++) {
394                         j.push("x");
395                     }
396                     window.document.cookie = "tre.test=" + j.join("") + ";path=/";
397                     var va = window.document.cookie.match('(?:^|;)\\s*tre.test=([^;]*)'),
398                         supported = !!va;
399                     if (supported) {
400                         // expire the cookie before returning
401                         window.document.cookie = "tre.test=;path=/;expires=" + new Date(R.now() - 1).toGMTString();
402                     }
403                     storageSupport.cookie = supported ? { "maxLength":va[1].length } : false;
404                 } catch (ex) { /* ignored */
405                 }
406 
407                 try {
408                     storageSupport.local = (typeof localStorage !== "undefined");
409                     storageSupport.session = (typeof sessionStorage !== "undefined");
410                 } catch (ex) {
411                     // Firefox bug (https://bugzilla.mozilla.org/show_bug.cgi?id=389002)
412                 }
413 
414                 try {
415                     storageSupport.indexeddb = (typeof mozIndexedDB !== "undefined");
416                 } catch (ex) { /* ignored */
417                 }
418 
419                 storageSupport.sqllite = R.isFunction(window.openDatabase);
420             } catch (ex) { /* ignored */
421             }
422 
423             // Build support object
424             R.engine.Support._sysInfo = {
425                 "browser":R.browser.chrome ? "chrome" :
426                     (R.browser.android ? "android" :
427                         (R.browser.Wii ? "wii" :
428                             (R.browser.safariMobile ? "safarimobile" :
429                                 (R.browser.safari ? "safari" :
430                                     (R.browser.firefox ? "firefox" :
431                                         (R.browser.mozilla ? "mozilla" :
432                                             (R.browser.opera ? "opera" :
433                                                 (R.browser.msie ? "msie" : "unknown")))))))),
434                 "version":R.browser.version,
435                 "agent":navigator.userAgent,
436                 "platform":navigator.platform,
437                 "cpu":navigator.cpuClass || navigator.oscpu,
438                 "OS":R.engine.Support.checkOS(),
439                 "language":navigator.language,
440                 "online":navigator.onLine,
441                 "fullscreen":window.fullScreen || false,
442                 "support":{
443                     "audio":(typeof Audio !== "undefined"),
444                     "video":(typeof Video !== "undefined"),
445                     "xhr":(typeof XMLHttpRequest !== "undefined"),
446                     "threads":(typeof Worker !== "undefined"),
447                     "sockets":(typeof WebSocket !== "undefined"),
448                     "storage":storageSupport,
449                     "geo":(typeof navigator.geolocation !== "undefined"),
450                     "canvas":canvasSupport
451                 }
452             };
453 
454             $(document).ready(function () {
455                 // When the document is ready, we'll go ahead and get the width and height added in
456                 R.engine.Support._sysInfo = $.extend(R.engine.Support._sysInfo, {
457                     "width":$(window).width(),
458                     "height":$(window).height(),
459                     "viewWidth":$(document).width(),
460                     "viewHeight":$(document).height()
461                 });
462             });
463         }
464         return R.engine.Support._sysInfo;
465     },
466 
467     /**
468      * When the object is no longer <tt>undefined</tt>, the function will
469      * be executed.
470      * @param obj {Object} The object to wait for
471      * @param fn {Function} The function to execute when the object is ready
472      * @memberOf R.engine.Support
473      */
474     whenReady:function (obj, fn) {
475         var whenObject = {
476             callback:fn,
477             object:obj
478         }, sleeper;
479 
480         sleeper.fn = fn;
481         sleeper.obj = obj;
482 
483         sleeper = R.bind(whenObject, function () {
484             if (typeof this.object != "undefined") {
485                 this.callback();
486             } else {
487                 setTimeout(sleeper, 50);
488             }
489         });
490         sleeper();
491     },
492 
493     /**
494      * Displays the virtual D-pad on the screen, if enabled via <tt>R.Engine.options.useVirtualControlPad</tt>,
495      * and wires up the appropriate events for the current browser.
496      */
497     showDPad:function () {
498         if (!R.Engine.options.useVirtualControlPad) {
499             return;
500         }
501 
502         R.debug.Console.debug("Virtual D-pad Enabled.");
503 
504         // Events to track based on platform
505         var downEvent, upEvent;
506         switch (R.engine.Support.sysInfo().browser) {
507             case "safarimobile":
508             case "android":
509                 downEvent = "touchstart";
510                 upEvent = "touchend";
511                 break;
512             default:
513                 downEvent = "mousedown";
514                 upEvent = "mouseup";
515         }
516 
517         var dpad = $("<div class='virtual-d-pad'></div>"),
518             vpad = $("<div class='virtual-buttons'></div>"), dpButtons = [], vbButtons = [], i = 0;
519 
520 
521         // Decodes the key mapping
522         function getMappedKey(key) {
523             if (key.indexOf("R.engine.Events.") != -1) {
524                 return R.engine.Events[key.split(".")[3]];
525             } else {
526                 return key.toUpperCase().charCodeAt(0);
527             }
528         }
529 
530         // Don't allow touches in the virtual pads to propagate
531         dpad.bind(downEvent, function (evt) {
532             evt.preventDefault();
533             evt.stopPropagation();
534         });
535         vpad.bind(downEvent, function (evt) {
536             evt.preventDefault();
537             evt.stopPropagation();
538         });
539 
540         // D-pad buttons
541         $.each(R.Engine.options.virtualPad, function (key, v) {
542             if (R.Engine.options.virtualPad[key] != false) {
543                 dpButtons[i++] = [key, getMappedKey(v), $("<div class='button " + key + "'></div>")];
544             }
545         });
546 
547         $.each(dpButtons, function () {
548             dpad.append(this[2]);
549         });
550         $(document.body).append(dpad);
551 
552         // Virtual Pad Buttons
553         i = 0;
554         $.each(R.Engine.options.virtualButtons, function (key, v) {
555             if (R.Engine.options.virtualButtons[key] != false) {
556                 vbButtons[i++] = [key, getMappedKey(v), $("<div class='button " + key + "'>" + key + "</div>")];
557             }
558         });
559 
560         $.each(vbButtons, function () {
561             vpad.append(this[2]);
562         });
563         $(document.body).append(vpad);
564 
565         // Wire up the buttons to fire keyboard events on the context
566         var allButtons = dpButtons.concat(vbButtons);
567         $.each(allButtons, function () {
568             var key = this;
569             key[2].bind(downEvent,function () {
570                 R.debug.Console.debug("virtual keydown: " + key[1]);
571                 var e = $.Event("keydown", {
572                     which:key[1]
573                 });
574                 R.Engine.getDefaultContext().jQ().trigger(e);
575             }).bind(upEvent, function () {
576                     R.debug.Console.debug("virtual keyup: " + key[1]);
577                     var e = $.Event("keyup", {
578                         which:key[1]
579                     });
580                     R.Engine.getDefaultContext().jQ().trigger(e);
581                 });
582         });
583     }
584 });
585 
586