diff --git a/lib/model.js b/lib/model.js index a3dfdc8766a..d92bfe90f53 100644 --- a/lib/model.js +++ b/lib/model.js @@ -1603,6 +1603,78 @@ Model.mapReduce = function mapReduce (o, callback) { }); } + +/** + * geoNear support for Mongoose + * + * ####Options: + * - `lean` {Boolean} return the raw object + * - All options supported by the driver are also supported + * + * @param {Object/Array} GeoJSON point or legacy coordinate pair [x,y] to search near + * @param {Object} options for the qurery + * @param {Function} callback for the query + * @see http://docs.mongodb.org/manual/core/2dsphere/ + * @see http://mongodb.github.io/node-mongodb-native/api-generated/collection.html?highlight=geonear#geoNear + * @api public + */ + +Model.geoNear = function (near, options, callback) { + if ('function' == typeof options) { + callback = options; + options = {}; + } + + if (!('function' == typeof callback)) { + throw new Error("Must pass a callback to geoNear"); + } + + if (!near) { + return callback(new Error("Must pass a near option to geoNear")); + } + + var x,y; + + if (Array.isArray(near)) { + if (near.length != 2) { + return callback(new Error("If using legacy coordinates, must be an array of size 2 for geoNear")); + } + x = near[0]; + y = near[1]; + } else { + if (!near.type || near.type != "Point" || !Array.isArray(near.coordinates)) { + return callback(new Error("Must pass either a legacy coordinate array or GeoJSON Point to geoNear")); + } + + x = near.coordinates[0]; + y = near.coordinates[1]; + } + var self = this; + this.collection.geoNear(x, y, options, function (err, res) { + if (err || res.errmsg) { + return callback(err || new Error(res.errmsg +" Code: " + res.code.toString()); + } + + if (!options.lean) { + var count = res.results.length; + var errSeen = false; + for (var i=0; i < res.results.length; i++) { + var temp = res.results[i].obj; + res.results[i].obj = new self(); + res.results[i].obj.init(temp, function (err) { + if (err && !errSeen) { + errSeen = true; + return callback(err); + } + --count || (!errSeen && callback(err, res)); + }); + } + } else { + callback(err, res); + } + }); +}; + /** * Perform aggregations on collections. * diff --git a/test/model.geonear.test.js b/test/model.geonear.test.js new file mode 100644 index 00000000000..c8329c14e04 --- /dev/null +++ b/test/model.geonear.test.js @@ -0,0 +1,194 @@ + +var start = require('./common') + , assert = require('assert') + , mongoose = start.mongoose + , random = require('../lib/utils').random + , Schema = mongoose.Schema + , DocumentObjectId = mongoose.Types.ObjectId + +/** + * Setup + */ + +var schema = new Schema({ + pos : [Number], + type: String +}); + +schema.index({ "pos" : "2dsphere"}); + +mongoose.model('Geo', schema, 'geo' + random()); + +describe('model', function(){ + describe('geoNear', function () { + it('works with legacy coordinate points', function (done) { + + var db = start(); + var Geo = db.model('Geo'); + assert.ok(Geo.geoNear instanceof Function); + + var geos = []; + geos[0] = new Geo({ pos : [10,10], type : "place"}); + geos[1] = new Geo({ pos : [15,5], type : "place"}); + geos[2] = new Geo({ pos : [20,15], type : "house"}); + geos[3] = new Geo({ pos : [1,-1], type : "house"}); + var count = geos.length; + + for (var i=0; i < geos.length; i++) { + geos[i].save(function () { + --count || next(); + }); + } + + function next() { + Geo.geoNear([9,9], { spherical : true, maxDistance : .1 }, function (err, results, stats) { + assert.ifError(err); + + assert.equal(1, results.results.length); + assert.equal(1, results.ok); + + assert.equal(results.results[0].obj.type, 'place'); + assert.equal(results.results[0].obj.pos.length, 2); + assert.equal(results.results[0].obj.pos[0], 10); + assert.equal(results.results[0].obj.pos[1], 10); + assert.equal(results.results[0].obj.id, geos[0].id); + assert.ok(results.results[0].obj instanceof Geo); + Geo.remove(function () { + db.close(); + done(); + }); + }); + } + }); + it('works with GeoJSON coordinate points', function (done) { + + var db = start(); + var Geo = db.model('Geo'); + assert.ok(Geo.geoNear instanceof Function); + + var geos = []; + geos[0] = new Geo({ pos : [10,10], type : "place"}); + geos[1] = new Geo({ pos : [15,5], type : "place"}); + geos[2] = new Geo({ pos : [20,15], type : "house"}); + geos[3] = new Geo({ pos : [1,-1], type : "house"}); + var count = geos.length; + + for (var i=0; i < geos.length; i++) { + geos[i].save(function () { + --count || next(); + }); + } + + function next() { + var pnt = { type : "Point", coordinates : [9,9] }; + Geo.geoNear(pnt, { spherical : true, maxDistance : .1 }, function (err, results, stats) { + assert.ifError(err); + + assert.equal(1, results.results.length); + assert.equal(1, results.ok); + + assert.equal(results.results[0].obj.type, 'place'); + assert.equal(results.results[0].obj.pos.length, 2); + assert.equal(results.results[0].obj.pos[0], 10); + assert.equal(results.results[0].obj.pos[1], 10); + assert.equal(results.results[0].obj.id, geos[0].id); + assert.ok(results.results[0].obj instanceof Geo); + Geo.remove(function () { + db.close(); + done(); + }); + }); + } + }); + it('works with lean', function (done) { + + var db = start(); + var Geo = db.model('Geo'); + assert.ok(Geo.geoNear instanceof Function); + + var geos = []; + geos[0] = new Geo({ pos : [10,10], type : "place"}); + geos[1] = new Geo({ pos : [15,5], type : "place"}); + geos[2] = new Geo({ pos : [20,15], type : "house"}); + geos[3] = new Geo({ pos : [1,-1], type : "house"}); + var count = geos.length; + + for (var i=0; i < geos.length; i++) { + geos[i].save(function () { + --count || next(); + }); + } + + function next() { + var pnt = { type : "Point", coordinates : [9,9] }; + Geo.geoNear(pnt, { spherical : true, maxDistance : .1, lean : true }, function (err, results, stats) { + assert.ifError(err); + + assert.equal(1, results.results.length); + assert.equal(1, results.ok); + + assert.equal(results.results[0].obj.type, 'place'); + assert.equal(results.results[0].obj.pos.length, 2); + assert.equal(results.results[0].obj.pos[0], 10); + assert.equal(results.results[0].obj.pos[1], 10); + assert.equal(results.results[0].obj._id, geos[0].id); + assert.ok(!(results.results[0].obj instanceof Geo)); + Geo.remove(function () { + db.close(); + done(); + }); + }); + } + }); + it('throws the correct error messages', function (done) { + + var db = start(); + var Geo = db.model('Geo'); + var g = new Geo({ pos : [10,10], type : "place"}); + g.save(function() { + var threw = false; + Geo.geoNear("1,2", {}, function (e) { + assert.ok(e); + assert.equal(e.message, "Must pass either a legacy coordinate array or GeoJSON Point to geoNear"); + + Geo.geoNear([1], {}, function (e) { + assert.ok(e); + assert.equal(e.message, "If using legacy coordinates, must be an array of size 2 for geoNear"); + + Geo.geoNear({ type : "Square" }, {}, function (e) { + assert.ok(e); + assert.equal(e.message, "Must pass either a legacy coordinate array or GeoJSON Point to geoNear"); + + Geo.geoNear({ type : "Point", coordinates : "1,2" }, {}, function (e) { + assert.ok(e); + assert.equal(e.message, "Must pass either a legacy coordinate array or GeoJSON Point to geoNear"); + + try { + Geo.geoNear({ type : "test" }, { near : [1,2] }, []); + } catch(e) { + threw = true; + assert.ok(e); + assert.equal(e.message, "Must pass a callback to geoNear"); + } + + assert.ok(threw); + threw = false; + + try { + Geo.geoNear({ type : "test" }, { near : [1,2] }); + } catch(e) { + threw = true; + assert.ok(e); + assert.equal(e.message, "Must pass a callback to geoNear"); + } + + assert.ok(threw); + done(); + }); + }); + }); + }); + }); + }); + }); +});