Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

geoNear support #1563

Closed
wants to merge 4 commits into from
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
72 changes: 72 additions & 0 deletions lib/model.js
Original file line number Diff line number Diff line change
Expand Up @@ -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.
*
Expand Down
194 changes: 194 additions & 0 deletions test/model.geonear.test.js
Original file line number Diff line number Diff line change
@@ -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();
});
});
});
});
});
});
});
});