/*
* Copyright 2009, Google Inc.
* All rights reserved.
*
* Redistribution and use in source and binary forms, with or without
* modification, are permitted provided that the following conditions are
* met:
*
* * Redistributions of source code must retain the above copyright
* notice, this list of conditions and the following disclaimer.
* * Redistributions in binary form must reproduce the above
* copyright notice, this list of conditions and the following disclaimer
* in the documentation and/or other materials provided with the
* distribution.
* * Neither the name of Google Inc. nor the names of its
* contributors may be used to endorse or promote products derived from
* this software without specific prior written permission.
*
* THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
* "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
* LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
* A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
* OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
* SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
* LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
* DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
* THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
* (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
* OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
*/
/**
* @fileoverview This file contains various utility functions for o3d. It
* puts them in the "util" module on the o3djs object.
*
*/
o3djs.provide('o3djs.util');
o3djs.require('o3djs.io');
o3djs.require('o3djs.event');
o3djs.require('o3djs.error');
/**
* A Module with various utilities.
* @namespace
*/
o3djs.util = o3djs.util || {};
/**
* The name of the o3d plugin. Used to find the plugin when checking
* for its version.
* @type {string}
*/
o3djs.util.PLUGIN_NAME = 'O3D Plugin';
/**
* The version of the plugin needed to use this version of the javascript
* utility libraries.
* @type {string}
*/
o3djs.util.REQUIRED_VERSION = '0.1.38.0';
/**
* A URL at which to download the client.
* @type {string}
*/
o3djs.util.PLUGIN_DOWNLOAD_URL = 'http://tools.google.com/dlpage/o3d';
/**
* The Renderer InitStatus constants so we don't need an o3d object to look
* them up.
* @enum {number}
*/
o3djs.util.rendererInitStatus = {
NO_PLUGIN: -1,
UNINITIALIZED: 0,
SUCCESS: 1,
OUT_OF_RESOURCES: 2,
GPU_NOT_UP_TO_SPEC: 3,
INITIALIZATION_ERROR: 4
};
/**
* This implements a JavaScript version of currying. Currying allows you to
* take a function and fix its initial arguments, resulting in a function
* expecting only the remaining arguments when it is invoked. For example:
*
* function add(a, b) {
* return a + b;
* }
* var increment = o3djs.util.curry(add, 1);
* var result = increment(10);
*
* Now result equals 11.
* @param {!function(...): *} func The function to curry.
* @return {!function(...): *} The curried function.
*/
o3djs.util.curry = function(func) {
var outerArgs = [];
for (var i = 1; i < arguments.length; ++i) {
outerArgs.push(arguments[i]);
}
return function() {
var innerArgs = outerArgs.slice();
for (var i = 0; i < arguments.length; ++i) {
innerArgs.push(arguments[i]);
}
return func.apply(this, innerArgs);
}
}
/**
* Gets the URI in which the current page is located, omitting the file name.
* @return {string} The base URI of the page. If the page is
* "http://some.com/folder/somepage.html" returns
* "http://some.com/folder/".
*/
o3djs.util.getCurrentURI = function() {
var path = window.location.href;
var index = path.lastIndexOf('/');
return path.substring(0, index + 1);
};
/**
* Given a URI that is relative to the current page, returns the absolute
* URI.
* @param {string} uri URI relative to the current page.
* @return {string} Absolute uri. If the page is
* "http://some.com/folder/sompage.html" and you pass in
* "images/someimage.jpg" will return
* "http://some.com/folder/images/someimage.jpg".
*/
o3djs.util.getAbsoluteURI = function(uri) {
return o3djs.util.getCurrentURI() + uri;
};
/**
* Searches an array for a specific value.
* @param {!Array.<*>} array Array to search.
* @param {*} value Value to search for.
* @return {boolean} True if value is in array.
*/
o3djs.util.arrayContains = function(array, value) {
for (var i = 0; i < array.length; i++) {
if (array[i] == value) {
return true;
}
}
return false;
};
/**
* Searches for all transforms with a "o3d.tags" ParamString
* that contains specific tag keywords assuming comma separated
* words.
* @param {!o3d.Transform} treeRoot Root of tree to search for tags.
* @param {string} searchTags Tags to look for. eg "camera", "ogre,dragon".
* @return {!Array.} Array of transforms.
*/
o3djs.util.getTransformsInTreeByTags = function(treeRoot, searchTags) {
var splitTags = searchTags.split(',');
var transforms = treeRoot.getTransformsInTree();
var found = [];
for (var n = 0; n < transforms.length; n++) {
var tagParam = transforms[n].getParam('collada.tags');
if (tagParam) {
var tags = tagParam.value.split(',');
for (var t = 0; t < tags.length; t++) {
if (o3djs.util.arrayContains(splitTags, tags[t])) {
found[found.length] = transforms[n];
break;
}
}
}
}
return found;
};
/**
* Finds transforms in the tree by prefix.
* @param {!o3d.Transform} treeRoot Root of tree to search.
* @param {string} prefix Prefix to look for.
* @return {!Array.} Array of transforms matching prefix.
*/
o3djs.util.getTransformsInTreeByPrefix = function(treeRoot, prefix) {
var found = [];
var transforms = treeRoot.getTransformsInTree();
for (var ii = 0; ii < transforms.length; ii++) {
var transform = transforms[ii];
if (transform.name.indexOf(prefix) == 0) {
found[found.length] = transform;
}
}
return found;
};
/**
* Finds the bounding box of all primitives in the tree, in the local space of
* the tree root. This will use existing bounding boxes on transforms and
* elements, but not create new ones.
* @param {!o3d.Transform} treeRoot Root of tree to search.
* @return {!o3d.BoundingBox} The boundinding box of the tree.
*/
o3djs.util.getBoundingBoxOfTree = function(treeRoot) {
// If we already have a bounding box, use that one.
var box = treeRoot.boundingBox;
if (box.valid) {
return box;
}
var o3d = o3djs.base.o3d;
// Otherwise, create it as the union of all the children bounding boxes and
// all the shape bounding boxes.
var transforms = treeRoot.children;
for (var i = 0; i < transforms.length; ++i) {
var transform = transforms[i];
var childBox = o3djs.util.getBoundingBoxOfTree(transform);
if (childBox.valid) {
// transform by the child local matrix.
childBox = childBox.mul(transform.localMatrix);
if (box.valid) {
box = box.add(childBox);
} else {
box = childBox;
}
}
}
var shapes = treeRoot.shapes;
for (var i = 0; i < shapes.length; ++i) {
var elements = shapes[i].elements;
for (var j = 0; j < elements.length; ++j) {
var elementBox = elements[j].boundingBox;
if (!elementBox.valid) {
elementBox = elements[j].getBoundingBox(0);
}
if (box.valid) {
box = box.add(elementBox);
} else {
box = elementBox;
}
}
}
return box;
};
/**
* Returns the smallest power of 2 that is larger than or equal to size.
* @param {number} size Size to get power of 2 for.
* @return {number} smallest power of 2 that is larger than or equal to size.
*/
o3djs.util.getPowerOfTwoSize = function(size) {
var powerOfTwo = 1;
while (size) {
size = size >> 1;
powerOfTwo = powerOfTwo << 1;
}
return powerOfTwo;
};
/**
* Gets the version of the installed plugin.
* @return {?string} version string in 'major.minor.revision.build' format.
* If the plugin does not exist returns null.
*/
o3djs.util.getPluginVersion = function() {
var version = null;
var description = null;
if (navigator.plugins != null && navigator.plugins.length > 0) {
var plugin = navigator.plugins[o3djs.util.PLUGIN_NAME];
if (plugin) {
description = plugin.description;
}
} else if (o3djs.base.IsMSIE()) {
try {
var activeXObject = new ActiveXObject('o3d_host.O3DHostControl');
description = activeXObject.description;
} catch (e) {
// O3D plugin was not found.
}
}
if (description) {
var re = /.*version:(\d+)\.(\d+)\.(\d+)\.(\d+).*/;
// Parse the version out of the description.
var parts = re.exec(description);
if (parts && parts.length == 5) {
// make sure the format is #.#.#.# no whitespace, no trailing comments
version = '' + parseInt(parts[1], 10) + '.' +
parseInt(parts[2], 10) + '.' +
parseInt(parts[3], 10) + '.' +
parseInt(parts[4], 10);
}
}
return version;
};
/**
* Checks if the required version of the plugin in available.
* @param {string} requiredVersion version string in
* "major.minor.revision.build" format. You can leave out any non-important
* numbers for example "3" = require major version 3, "2.4" = require major
* version 2, minor version 4.
* @return {boolean} True if the required version is available.
*/
o3djs.util.requiredVersionAvailable = function(requiredVersion) {
var version = o3djs.util.getPluginVersion();
if (!version) {
return false;
}
var haveParts = version.split('.');
var requiredParts = requiredVersion.split('.');
if (requiredParts.length > 4) {
throw Error('requiredVersion has more than 4 parts!');
}
for (var pp = 0; pp < requiredParts.length; ++pp) {
var have = parseInt(haveParts[pp], 10);
var required = parseInt(requiredParts[pp], 10);
if (have < required) {
return false;
}
if (have > required) {
return true;
}
}
return true;
};
/**
* Offers the user the option to download the plugin.
*
* Finds all divs with the id "^o3d" and inserts a message and link
* inside to download the plugin. If no areas exist OR if none of them are
* large enough for the message then displays an alert.
*
* @param {string} opt_id The id to look for. This can be a regular
* expression. The default is "^o3d".
* @param {string} opt_tag The type of tag to look for. The default is "div".
*/
o3djs.util.offerPlugin = function(opt_id, opt_tag) {
var tag = opt_tag || 'div';
var id = opt_id || '^o3d';
var havePlugin = o3djs.util.requiredVersionAvailable('');
var elements = document.getElementsByTagName(tag);
var addedMessage = false;
// TODO: This needs to be localized OR we could insert a html like
//
// in which case google could serve the message localized and update the
// link.
var subMessage =
(havePlugin ?
'This page requires a newer version of the O3D plugin.' :
'This page requires the O3D plugin to be installed.');
var message =
'
'
for (var ee = 0; ee < elements.length; ++ee) {
var element = elements[ee];
if (element.id && element.id.match(id)) {
if (element.clientWidth >= 200 &&
element.clientHeight >= 200 &&
element.style.display.toLowerCase() != 'none' &&
element.style.visibility.toLowerCase() != 'hidden') {
addedMessage = true;
element.innerHTML = message;
}
}
}
if (!addedMessage) {
if (confirm(subMessage + '\n\nClick OK to download.')) {
window.location = o3djs.util.PLUGIN_DOWNLOAD_URL;
}
}
};
/**
* Tells the user their graphics card is not able to run the plugin or is out
* of resources etc.
*
* Finds all divs with the id "^o3d" and inserts a message. If no areas
* exist OR if none of them are large enough for the message then displays an
* alert.
*
* @param {!o3d.Renderer.InitStatus} initStatus The initializaion status of
* the renderer.
* @param {string} error An error message. Will be '' if there is no message.
* @param {string} opt_id The id to look for. This can be a regular
* expression. The default is "^o3d".
* @param {string} opt_tag The type of tag to look for. The default is "div".
*/
o3djs.util.informNoGraphics = function(initStatus, error, opt_id, opt_tag) {
var tag = opt_tag || 'div';
var id = opt_id || '^o3d';
var elements = document.getElementsByTagName(tag);
var addedMessage = false;
var subMessage;
var message;
var alertMessage = '';
var alertFunction = function() { };
var moreInfo = function(error) {
var html = '';
if (error.length > 0) {
html = '' +
'
More Info: ' + error + '
';
}
return html;
};
// TODO: This needs to be localized OR we could insert a html like
//
// in which case google could serve the message localized and update the
// link.
if (initStatus == o3djs.util.rendererInitStatus.GPU_NOT_UP_TO_SPEC) {
subMessage =
'We are terribly sorry but it appears your graphics card is not ' +
'able to run o3d. We are working on a solution.';
message =
'
';
alertMessage = '\n\nClick OK to go to the o3d website.';
alertFunction = function() {
window.location = o3djs.util.PLUGIN_DOWNLOAD_URL;
};
} else if (initStatus == o3djs.util.rendererInitStatus.OUT_OF_RESOURCES) {
subMessage =
'Your graphics system appears to be out of resources. Try closing ' +
'some applications and then refreshing this page.';
message =
'
' +
'
' + subMessage +
moreInfo(error) +
'
';
} else {
subMessage =
'A unknown error has prevented O3D from starting. Try downloading ' +
'new drivers or checking for OS updates.';
message =
'
' +
'
' + subMessage +
moreInfo(error) +
'
';
}
for (var ee = 0; ee < elements.length; ++ee) {
var element = elements[ee];
if (element.id && element.id.match(id)) {
if (element.clientWidth >= 200 &&
element.clientHeight >= 200 &&
element.style.display.toLowerCase() != 'none' &&
element.style.visibility.toLowerCase() != 'hidden') {
addedMessage = true;
element.innerHTML = message;
}
}
}
if (!addedMessage) {
if (confirm(subMessage + alertMessage)) {
alertFunction();
}
}
};
/**
* Handles failure to create the plugin.
*
* @param {!o3d.Renderer.InitStatus} initStatus The initializaion status of
* the renderer.
* @param {string} error An error message. Will be '' if there is no message.
* @param {string} opt_id The id to look for. This can be a regular
* expression. The default is "^o3d".
* @param {string} opt_tag The type of tag to look for. The default is "div".
*/
o3djs.util.informPluginFailure = function(initStatus, error, opt_id, opt_tag) {
if (initStatus == o3djs.util.rendererInitStatus.NO_PLUGIN) {
o3djs.util.offerPlugin(opt_id, opt_tag);
} else {
o3djs.util.informNoGraphics(initStatus, error, opt_id, opt_tag);
}
};
/**
* Utility to get the text contents of a DOM element with a particular ID.
* Currently only supports textarea and script nodes.
* @param {string} id The Node id.
* @return {string} The text content.
*/
o3djs.util.getElementContentById = function(id) {
// DOM manipulation is not currently supported in IE.
o3djs.BROWSER_ONLY = true;
var node = document.getElementById(id);
if (!node) {
throw 'getElementContentById could not find node with id ' + id;
}
switch (node.tagName) {
case 'TEXTAREA':
return node.value;
case 'SCRIPT':
return node.text;
default:
throw 'getElementContentById does not no how to get content from a ' +
node.tagName + ' element';
}
};
/**
* Utility to get an element from the DOM by ID. This must be used from V8
* in preference to document.getElementById because we do not currently
* support invoking methods on DOM objects in IE.
* @param {string} id The Node id.
* @return {Node} The node or null if not found.
*/
o3djs.util.getElementById = function(id) {
o3djs.BROWSER_ONLY = true;
return document.getElementById(id);
};
/**
* Identifies a JavaScript engine.
*/
o3djs.util.Engine = {
/**
* The JavaScript engine provided by the browser.
*/
BROWSER: 0,
/**
* The V8 JavaScript engine embedded in the plugin.
*/
V8: 1
};
/**
* The engine selected as the main engine (the one the makeClients callback
* will be invoked on).
* @private
* @type {o3djs.util.Engine}
*/
o3djs.util.mainEngine_ = o3djs.util.Engine.BROWSER;
/**
* Select an engine to use as the main engine (the one the makeClients
* callback will be invoked on). If an embedded engine is requested, one
* element must be identified with the id 'o3d'. The callback will be invoked
* in this element.
* @param {o3djs.util.Engine} engine The engine.
*/
o3djs.util.setMainEngine = function(engine) {
o3djs.util.mainEngine_ = engine;
};
/**
* A regex used to cleanup the string representation of a function before
* it is evaled.
* @private
* @type {!RegExp}
*/
o3djs.util.fixFunctionString_ = /^\s*function\s+[^\s]+\s*\(([^)]*)\)/
/**
* Evaluate a callback function in the V8 engine.
* @param {!Object} clientElement The plugin containing the V8 engine.
* @param {!function(...): *} callback A function to call.
* @param {!Object} thisArg The value to be bound to "this".
* @param {!Array.<*>} args The arguments to pass to the callback.
* @return {*} The result of calling the callback.
*/
o3djs.util.callV8 = function(clientElement, callback, thisArg, args) {
// Sometimes a function will be converted to a string like this:
// function foo(a, b) { ... }
// In this case, convert to this form:
// function(a, b) { ... }
var functionString = callback.toString();
functionString = functionString.replace(o3djs.util.fixFunctionString_,
'function($1)');
// Make a V8 function that will invoke the callback.
var v8Code =
'function(thisArg, args) {\n' +
' var localArgs = [];\n' +
' var numArgs = args.length;\n' +
' for (var i = 0; i < numArgs; ++i) {\n' +
' localArgs.push(args[i]);\n' +
' }\n' +
' var func = ' + functionString + ';\n' +
' return func.apply(thisArg, localArgs);\n' +
'}\n';
// Evaluate the function in V8.
var v8Function = clientElement.eval(v8Code);
return v8Function(thisArg, args);
};
/**
* A regex to remove .. from a URI.
* @private
* @type {!RegExp}
*/
o3djs.util.stripDotDot_ = /\/[^\/]+\/\.\./;
/**
* Turn a URI into an absolute URI.
* @param {string} uri The URI.
* @return {string} The absolute URI.
*/
o3djs.util.toAbsoluteUri = function(uri) {
if (uri.indexOf('://') == -1) {
var baseUri = document.location.toString();
var lastSlash = baseUri.lastIndexOf('/');
if (lastSlash != -1) {
baseUri = baseUri.substring(0, lastSlash);
}
uri = baseUri + '/' + uri;
}
do {
var lastUri = uri;
uri = uri.replace(o3djs.util.stripDotDot_, '');
} while (lastUri !== uri);
return uri;
};
/**
* The script URIs.
* @private
* @type {!Array.}
*/
o3djs.util.scriptUris_ = [];
/**
* Add a script URI. Scripts that are referenced from script tags that are
* within this URI are automatically loaded into the alternative JavaScript
* main JavaScript engine. Do not include directories of scripts that are
* included with o3djs.require. These are always available. This mechanism
* is not able to load scripts in a different domain from the document.
* @param {string} uri The URI.
*/
o3djs.util.addScriptUri = function(uri) {
o3djs.util.scriptUris_.push(o3djs.util.toAbsoluteUri(uri));
};
/**
* Determine whether a URI is a script URI that should be loaded into the
* alternative main JavaScript engine.
* @param {string} uri The URI.
* @return {boolean} Whether it is a script URI.
*/
o3djs.util.isScriptUri = function(uri) {
uri = o3djs.util.toAbsoluteUri(uri);
for (var i = 0; i < o3djs.util.scriptUris_.length; ++i) {
var scriptUri = o3djs.util.scriptUris_[i];
if (uri.substring(0, scriptUri.length) === scriptUri) {
return true;
}
}
return false;
};
/**
* Concatenate the text of all the script tags in the document and invokes
* the callback when complete. This function is asynchronous if any of the
* script tags reference JavaScript through a URI.
* @private
* @return {string} The script tag text.
*/
o3djs.util.getScriptTagText_ = function() {
var scriptTagText = '';
var scriptElements = document.getElementsByTagName('script');
for (var i = 0; i < scriptElements.length; ++i) {
var scriptElement = scriptElements[i];
if (scriptElement.type === '' ||
scriptElement.type === 'text/javascript') {
if ('text' in scriptElement && scriptElement.text) {
scriptTagText += scriptElement.text;
}
if ('src' in scriptElement && scriptElement.src &&
o3djs.util.isScriptUri(scriptElement.src)) {
// It would be better to make this an asynchronous load but the script
// file is very likely to be in the browser cache because it should
// have just been loaded via the browser script tag.
scriptTagText += o3djs.io.loadTextFileSynchronous(scriptElement.src);
}
}
}
return scriptTagText;
};
/**
* Creates a client element. In other words it creates an