Source: ARCore/coordinate.js

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;
});