define(["lib/jquery", "lib/underscore", "common/network/dispatcher",
"common/util/cap_utils", "common/util/log", "common/util/check",
"ARCore/embed"],
function($, _, Dispatcher,
CapUtils, Log, check,
Embed) {
"use strict";
var logger = Log("Coordinate");
var HD = 0;
var DEPTH = 1;
var PROJECTOR = 2;
var WORLD = 3;
var WINDOW = 4;
var systemStrings = {};
systemStrings[HD] = "hd";
systemStrings[DEPTH] = "depth";
systemStrings[PROJECTOR] = "projector";
systemStrings[WORLD] = "world";
systemStrings[WINDOW] = "window";
var validSystem = check.Match.Where(function(system) {
if(systemStrings[system] === undefined) {
throw new check.Match.Error(system + " is not a valid system.");
}
return true;
});
var getWindowOffset = function(callback) {
if(Embed.parentDispatcher) {
Embed.parentDispatcher.callRPC("lens:getWindowOffset", {}, function(r) {
callback(r.top, r.left);
});
}
else {
// we're top-level
callback(0, 0);
}
};
Embed._addPostInitCallback(function(embed) {
embed.dispatcher.registerAsyncRPC("lens:getWindowOffset", function(data, callback) {
getWindowOffset(function(top, left) {
var offset = $(embed.iframe).offset();
callback({
top: top + offset.top,
left: left + offset.left
});
});
});
});
var projectorToWindow = function(coord, callback) {
getWindowOffset(function(top, left) {
callback({
x: coord.x - left,
y: coord.y - top,
z: coord.z
});
});
};
var windowToProjector = function(coord, callback) {
getWindowOffset(function(top, left) {
callback({
x: coord.x + left,
y: coord.y + top,
z: coord.z
});
});
};
/**
* Creates a new Coordinate
*
* @param {System} system The coordinate system, such as {@link Lens.Coordinate.PROJECTOR}
* @param {Number} x X coordinate
* @param {Number} y Y coordinate
* @param {Number} [z] Z coordinate
*
*
* @class Immutable class representing a coordinate in a particular frame of
* reference.
*
* @property {System} system The coordinate system, such as {@link Lens.Coordinate.PROJECTOR}.
* @property {Number} x X coordinate.
* @property {Number} y Y coordinate.
* @property {Number} z Z coordinate, or `undefined`.
*
* @memberOf Lens
*/
var Coordinate = function(system, x, y, z) {
/** @alias Lens.Coordinate.prototype */
var self = {};
check(system, validSystem);
check(x, Number);
check(y, Number);
check(z, check.Match.Optional(Number));
self.x = x;
self.y = y;
self.z = z;
self.system = system;
/**
* Transforms this Coordinate to a Coordinate in a different system.
*
* @param {String} system The system to transform to, such as
* {@link Lens.Coordinate.WORLD}
* @param {Function} [callback] A function to call with the transformed
* coordinate.
*
*
* @return {Promise} A jQuery promise. You can attach callbacks using
* this promise or use jQuery's tools to do
* parallel transformations. If you passed a callback,
* that callback will automatically be registered as
* a completion handler for this promise.
*/
self.transform = function(system, callback) {
check(system, validSystem);
check(callback, check.Match.Optional(Function));
// asynchronously make sure TransformsModule is enabled, then call
// the coord_transform RPC, then resolve the deferred with the
// new coordinate.
var promise = $.Deferred(function(deferred) {
var submitTransformRequest = function(coord, from, to, callback) {
if(from === to) {
callback(coord);
return;
}
// construct parameters to be sent to Bridge
var params = {
from: systemStrings[from],
to: systemStrings[to],
x: x,
y: y
};
if (z !== undefined) {
params.z = z;
}
CapUtils.ensure("TransformsModule", logger, function(){
Dispatcher._bridge.callRPC("coord_transform", params, function(result) {
callback(result);
});
});
};
var returnResult = function(coord) {
if(system === WINDOW) {
// convert projector -> window
projectorToWindow(coord, function(windowCoord) {
deferred.resolve(Coordinate(system, windowCoord.x, windowCoord.y, windowCoord.z));
});
}
else {
deferred.resolve(Coordinate(system, coord.x, coord.y, coord.z));
}
};
// if we want to convert to window, we first convert to
// projector and then go projector -> window in
// returnResult()
var to = system;
if(to === WINDOW) {
to = PROJECTOR;
}
var from = self.system;
if(from === WINDOW) {
from = PROJECTOR;
// convert to projector coordinates
windowToProjector(self, function(projectorCoord) {
submitTransformRequest(projectorCoord, from, to, returnResult);
});
}
else {
submitTransformRequest(self, from, to, returnResult);
}
}).promise();
if(callback) {
promise.done(callback);
}
return promise;
};
Object.freeze(self);
return self;
};
_.extend(Coordinate, /** @lends Lens.Coordinate */ {
/**
* Returns the coordinates for the bounding box of an element. Does
* not take into account CSS transforms. Throws an error if given
* a CSS selector that does not match any elements, or a jQuery object
* with no elements.
*
* @param {Element | String | jQuery} el
* A DOM element, CSS selector, or jQuery object. This element must
* be rendered to the screen; it can't have been hidden with
* `display: none` in CSS or with jQuery's `.hide()`.
*
* @return {Array} A 2-element array whose first element is the top left
* corner of the element and whose second element is the
* bottom right corner.
*/
forElement: function(el) {
check(el, check.Match.Elementish);
var o, offsets, leftX, topY, rightX, bottomY;
o = $(el);
offsets = o.offset();
if(!offsets) {
throw "Could not calculate position of element: " + el;
}
leftX = offsets.left;
topY = offsets.top;
rightX = leftX + o.width();
bottomY = topY + o.height();
return [Coordinate(WINDOW, leftX, topY),
Coordinate(WINDOW, rightX, bottomY)];
},
/**
* Returns the offset between window coordinates and projector
* coordinates.
*
* @param {Function} callback
* Callback to invoke with the offset. Gets the top offset as the
* first argument and the left offset as the second argument.
*
* @memberof Lens.Coordinate
*/
getWindowOffset: getWindowOffset,
/**
* Coordinate frame of the HD camera. Units are pixels.
* @memberof Lens.Coordinate
*/
HD: HD,
/**
* Coordinate frame of the depth camera. Units are pixels.
* @memberof Lens.Coordinate
*/
DEPTH: DEPTH,
/**
* Coordinate frame of the projector/page. Units are pixels.
* @memberof Lens.Coordinate
*/
PROJECTOR: PROJECTOR,
/**
* Coordinate frame of the DOM window. For top-level apps, this is the
* same as the projector coordinate frame. For embedded Lens apps, this
* can be used to translate coordinates in the frame of the parent app
* (projector coordinates) to the frame of the child app (window
* coordinates).
* @memberof Lens.Coordinate
*/
WINDOW: WINDOW,
/**
* Coordinate frame of the depth camera. Units are millimeters.
* @memberof Lens.Coordinate
*/
WORLD: WORLD
});
// in dev mode, all coordinates are in the same frame of reference
// (because the camera is just a screenshot)
Lens._onDev(function() {
Dispatcher._bridge.registerRPC("coord_transform", function(data) {
return {
x: data.x,
y: data.y
};
});
});
Lens._addMember(Coordinate, "Coordinate");
return Coordinate;
});