Source: ARCore/contour_search.js

define(["lib/jquery",                "lib/underscore",           "common/util/log",
        "common/network/dispatcher", "common/models/contour",    "common/util/dom_utils",
        "common/util/id_generator",  "common/util/binding_list", "common/util/cap_utils",
        "./coordinate",              "common/util/check",        "common/constants"],
function($,                           _,                          Log,
         Dispatcher,                  Contour,                    DOMUtils,
         IDGenerator,                 BindingList,                CapUtils,
         Coordinate,                  check,                      Constants) {

    "use strict";

    var logger = Log("Contours");

    var runningSearches = {};

    var windowOffset = {left: 0, top: 0};

    var updateWindowOffset = function(cb) {
        Coordinate.getWindowOffset(function(top, left) {
            windowOffset = {left: left, top: top};
            cb();
        });
    };

    // converts points coming in from the bridge (in Projector coordinates) to
    // points in Window coordinates.
    // WARNING: first call updateWindowOffset to ensure the cached window offset
    // is up-to-date
    var convertPoints = function(points) {
        return points.map(function(point) {
            return [
                point[0] - windowOffset.left,
                point[1] - windowOffset.top
            ];
        });
    };

    /**
     * Registers a new ContourSearch. When a Contour object appear in the
     * ContourSearch area, new Contour objects will be created, and changes of
     * the Contour objects will be tracked.
     *
     * @param {Object} [opts]
     *        Parameters specifying the bounds of the  search.
     * @param {Array<Number>} [opts.x]
     *        A pair of numbers specifying the upper and lower x-axis bounds.
     * @param {Array<Number>} [opts.y]
     *        A pair of number specifying the upper and lower y-axis bounds.
     * @param {String | Element | jQuery} [opts.el]
     *        A selector, element, or jQuery object. If this parameter is given,
     *        the bounding box of the element will be used to provide x- and
     *        y-axis bounds. The opts.x and opts.y parameters take precedence
     *        over this. Note that the bounds are *not* recalculated if the
     *        element moves.
     * @param {Number} [opts.threshold]
     *        The number of points that must be inside the x and y bounds for
     *        the contour to be considered inside this search. Defaults to 1.
     * @param {Number} [opts.classification]
     *        Desired classification for the contour. Must be one of the constants
     *        from @link{Lens.ContourSearch}
     * @class  ContourSearch locations are areas that LuminAR will watch. If new
     *         objects appear in the region, new {@link Contour} objects will be
     *         created and move/disappear callbacks will be fired.
     * @memberOf Lens
     */
    var ContourSearch = function(opts){
        check(opts, check.Match.Optional({
            x: check.Match.Optional(check.Match.Pair(Number)),
            y: check.Match.Optional(check.Match.Pair(Number)),
            el: check.Match.Optional(check.Match.Elementish),
            threshold: check.Match.Optional(Number),
            classification: check.Match.Optional(Number)
        }));

        // TODO: check that opts.classification is valid

        /** @alias Lens.ContourSearch.prototype */
        var self = {};

        // random id used to identify this contour in runningSearches;
        var id = IDGenerator.uuid();
        runningSearches[id] = self;

        opts = opts || {};
        var threshold = opts.threshold || 1;
        var appearSubscribers    = BindingList();
        var moveSubscribers      = BindingList();
        var disappearSubscribers = BindingList();

        // contour id -> contour
        // for all contours currently inside this ContourSearch
        var contours = {};

        // set xRange and yRange
        var xRange = [0, Number.MAX_VALUE];
        var yRange = [0, Number.MAX_VALUE];

        if(opts.el) {
            var coords = Coordinate.forElement(opts.el);
            xRange = [coords[0].x, coords[1].x];
            yRange = [coords[0].y, coords[1].y];
        }

        if(opts.x) {
            xRange = opts.x;
        }

        if(opts.y) {
            yRange = opts.y;
        }

        var pointInSearchZone = function(point) {
            if((point[0] < xRange[0]) || (point[0] > xRange[1])) {
                return false;
            }

            if((point[1] < yRange[0]) || (point[1] > yRange[1])) {
                return false;
            }

            return true;
        };

        var contourInSearchZone = function(points, contourClassification){
            var pointRange = _.filter(points, pointInSearchZone).length >= threshold;
            var classificationsMatch = true;
            if (opts.classification!==undefined) {
                classificationsMatch = opts.classification === contourClassification;
            }
            return pointRange && classificationsMatch;
        };

        // called when we get a contour disappearance message. Also called by
        // contourUpdateReceived when a contour leaves this ContourSearch.
        var contourDisappeared = function(contourData) {
            var contour = contours[contourData.id];

            if(contour) {
                contour._end();
                disappearSubscribers.callAll(contour);
            }

            delete contours[contourData.id];
        };

        // called when we get a new or moved contour
        var contourUpdateReceived = function(contourData) {
            var points = convertPoints(contourData.points);

            if(contourInSearchZone(points, contourData.classification)) {
                // are we already tracking this contour?
                var contour = contours[contourData.id];

                if(contour) {
                    // if so, update the existing contour
                    contour._update(points, contourData.cm, contourData.moments, contourData.children, contourData.classification);
                    moveSubscribers.callAll(contour);
                }
                else {
                    // new contour
                    contour = Contour(contourData.id, points, contourData.cm, contourData.moments, contourData.children, contourData.classification);
                    contours[contourData.id] = contour;

                    appearSubscribers.callAll(contour);
                }
            }
            else {
                if(contours[contourData.id]) {
                    // if we're tracking the contour, but now it's not in our
                    // search zone, that means it's disappeared.
                    contourDisappeared(contourData);
                }
            }
        };

        // subscribe contourDisappeared and contourUpdateReceived
        var dispatcherBinding = Dispatcher._bridge.subscribe("contour", function(data){
            updateWindowOffset(function() {
                _.each(data.new, function(contourData){
                    contourUpdateReceived(contourData);
                });

                _.each(data.change, function(contourData){
                    contourUpdateReceived(contourData);
                });

                _.each(data.done, function(contourData){
                    contourDisappeared(contourData);
                });
            });
        });

        /**
         * Registers a function to be called whenever a Contour appears in this
         * region.
         *
         * @param  {Function} callback
         *     The function to call. Gets one {@link Contour} as an
         *     argument. Use {@link Contour#move} or{@link Contour#disappeared}
         *     to register additional handlers for that Contour.
         *
         * @return {Binding} A Binding that allows this handler to be cleared.
         */
        self.appeared = function(callback) {
            check(callback, Function);
            return appearSubscribers.add(callback);
        };

        /**
         * Registers a function to be called whenever a contour in this search
         * area moves.
         *
         * @param  {Function} callback
         *     The function to call. Gets a {@link Contour} as an argument.
         *
         * @return {Binding} A Binding that allows this handler to be cleared.
         */
        self.moved = function(callback){
            check(callback, Function);
            return moveSubscribers.add(callback);
        };

        /**
         * Registers a function to be called whenever a contour leaves this
         * search area.
         *
         * @param  {Function} callback
         *     The function to call. Gets a {@link Contour} as an argument.
         *
         * @return {Binding} A Binding that allows this handler to be cleared.
         */
        self.disappeared = function(callback){
            check(callback, Function);
            return disappearSubscribers.add(callback);
        };

        /**
         * Stops watching the search area. No more callbacks will be called.
         */
        self.stop = function() {
            dispatcherBinding.clear();
            delete runningSearches[id];
        };

        // make sure Contours are on
        CapUtils.ensure("ContoursModule", logger);

        Object.freeze(self);
        return self;
    };

    _.extend(ContourSearch, /** @lends Lens.ContourSearch */  {
        /**
         * Stops all existing ContourSearches.
         */
        clear: function(){
            _.invoke(runningSearches, "stop");
        },

        /**
         * The classification number for an unclassified contour.
         * @type{Number}
         */
        CLASSIFICATION_NONE: Constants.vision.CLASSIFICATION_NONE,

        /**
         * The circle contour classification number.
         * @type{Number}
         */
        CLASSIFICATION_CIRCLE: Constants.vision.CLASSIFICATION_CIRCLE,

        /**
         * The maximum classification value (for verification).
         * @type{Number}
         */
        CLASSIFICATION_MAX: Constants.vision.CLASSIFICATION_MAX
    });

    Lens._addMember(ContourSearch, "ContourSearch");
    return ContourSearch;
});