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