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