diff --git a/lib/geo.js b/lib/geo.js index 1a689a86..9bef1222 100644 --- a/lib/geo.js +++ b/lib/geo.js @@ -12,109 +12,82 @@ var assert = require('assert'); */ exports.nearFilter = function nearFilter(where) { - function nearSearch(clause, parentKeys) { - if (typeof clause !== 'object') { - return false; - } - parentKeys = parentKeys || []; + var result = false; - Object.keys(clause).forEach(function(clauseKey) { - if (Array.isArray(clause[clauseKey])) { - clause[clauseKey].forEach(function(el, index) { - var ret = nearSearch(el, parentKeys.concat(clauseKey).concat(index)); - if (ret) return ret; - }); - } else { - if (clause[clauseKey].hasOwnProperty('near')) { - var result = clause[clauseKey]; - nearResults.push({ - near: result.near, - maxDistance: result.maxDistance, - minDistance: result.minDistance, - unit: result.unit, - // If key is at root, define a single string, otherwise append it to the full path array - mongoKey: parentKeys.length ? parentKeys.concat(clauseKey) : clauseKey, - key: clauseKey, - }); - } + if (where && typeof where === 'object') { + Object.keys(where).forEach(function(key) { + var ex = where[key]; + + if (ex && ex.near) { + result = { + near: ex.near, + maxDistance: ex.maxDistance, + minDistance: ex.minDistance, + unit: ex.unit, + key: key, + }; } }); } - var nearResults = []; - nearSearch(where); - return (!nearResults.length ? false : nearResults); + return result; }; /*! - * Filter a set of results using the given filters returned by `nearFilter()`. - * Can support multiple locations, but will include results from all of them. - * - * WARNING: "or" operator with GeoPoint does not work as expected, eg: - * {where: {or: [{location: {near: (29,-90)}},{name:'Sean'}]}} - * Will actually work as if you had used "and". This is because geo filtering - * takes place outside of the SQL query, so the result set of "name = Sean" is - * returned by the database, and then the location filtering happens in the app - * logic. So the "near" operator is always an "and" of the SQL filters, and "or" - * of other GeoPoint filters. - * - * Additionally, since this step occurs after the SQL result set is returned, - * if using GeoPoints with pagination the result set may be smaller than the - * page size. The page size is enforced at the DB level, and then we may - * remove results at the Geo-app level. If we "limit: 25", but 4 of those results - * do not have a matching geopoint field, the request will only return 21 results. - * This may make it erroneously look like a given page is the end of the result set. + * Filter a set of objects using the given `nearFilter`. */ -exports.filter = function(rawResults, filters) { +exports.filter = function(arr, filter) { + var origin = filter.near; + var max = filter.maxDistance > 0 ? filter.maxDistance : false; + var min = filter.minDistance > 0 ? filter.minDistance : false; + var unit = filter.unit; + var key = filter.key; + + // create distance index var distances = {}; - var results = []; + var result = []; - filters.forEach(function(filter) { - var origin = filter.near; - var max = filter.maxDistance > 0 ? filter.maxDistance : false; - var min = filter.minDistance > 0 ? filter.minDistance : false; - var unit = filter.unit; - var key = filter.key; + arr.forEach(function(obj) { + var loc = obj[key]; - // create distance index - rawResults.forEach(function(result) { - var loc = result[key]; + // filter out objects without locations + if (!loc) return; - // filter out results without locations - if (!loc) return; + if (!(loc instanceof GeoPoint)) { + loc = GeoPoint(loc); + } - if (!(loc instanceof GeoPoint)) loc = GeoPoint(loc); + if (typeof loc.lat !== 'number') return; + if (typeof loc.lng !== 'number') return; - if (typeof loc.lat !== 'number') return; - if (typeof loc.lng !== 'number') return; + var d = GeoPoint.distanceBetween(origin, loc, {type: unit}); - var d = GeoPoint.distanceBetween(origin, loc, {type: unit}); - - // filter result if distance is either < minDistance or > maxDistance - if ((min && d < min) || (max && d > max)) return; - - distances[result.id] = d; - results.push(result); - }); - - results.sort(function(resA, resB) { - var a = resA[key]; - var b = resB[key]; - - if (a && b) { - var da = distances[resA.id]; - var db = distances[resB.id]; - - if (db === da) return 0; - return da > db ? 1 : -1; - } else { - return 0; - } - }); + if (min && d < min) { + return; + } + if (max && d > max) { + // dont add + } else { + distances[obj.id] = d; + result.push(obj); + } }); - return results; + return result.sort(function(objA, objB) { + var a = objA[key]; + var b = objB[key]; + + if (a && b) { + var da = distances[objA.id]; + var db = distances[objB.id]; + + if (db === da) return 0; + return da > db ? 1 : -1; + } else { + return 0; + } + }); }; exports.GeoPoint = GeoPoint; @@ -312,3 +285,4 @@ function geoDistance(x1, y1, x2, y2, options) { return 2 * Math.asin(f) * EARTH_RADIUS[type]; } + diff --git a/test/basic-querying.test.js b/test/basic-querying.test.js index 2d973d55..d9859eb2 100644 --- a/test/basic-querying.test.js +++ b/test/basic-querying.test.js @@ -38,7 +38,6 @@ describe('basic-querying', function() { name: String, }, ], - addressLoc: {type: 'GeoPoint'}, }); db.automigrate(done); @@ -554,147 +553,6 @@ describe('basic-querying', function() { }); }); - describe('geo queries', function() { - describe('near filter', function() { - it('supports a basic "near" query', function(done) { - User.find({ - where: { - addressLoc: { - near: '29.9,-90.07', - }, - }, - }, function(err, users) { - if (err) done(err); - users.should.have.property('length', 3); - users[0].name.should.equal('John Lennon'); - users[0].addressLoc.should.not.be.null(); - done(); - }); - }); - - it('supports "near" inside a coumpound query with "and"', function(done) { - User.find({ - where: { - and: [ - { - addressLoc: { - near: '29.9,-90.07', - }, - }, - { - vip: true, - }, - ], - }, - }, function(err, users) { - if (err) done(err); - users.should.have.property('length', 2); - users[0].name.should.equal('John Lennon'); - users[0].addressLoc.should.not.be.null(); - users[0].vip.should.be.true(); - done(); - }); - }); - - it('supports "near" inside a complex coumpound query with multiple "and"', function(done) { - User.find({ - where: { - and: [ - { - and: [ - { - addressLoc: { - near: '29.9,-90.07', - }, - }, - { - order: 2, - }, - ], - }, - { - vip: true, - }, - ], - }, - }, function(err, users) { - if (err) done(err); - users.should.have.property('length', 1); - users[0].name.should.equal('John Lennon'); - users[0].addressLoc.should.not.be.null(); - users[0].vip.should.be.true(); - users[0].order.should.equal(2); - done(); - }); - }); - - it('supports multiple "near" queries with "or"', function(done) { - User.find({ - where: { - or: [ - { - addressLoc: { - near: '29.9,-90.04', - maxDistance: 300, - }, - }, - { - addressLoc: { - near: '22.97,-88.03', - maxDistance: 50, - }, - }, - ], - }, - }, function(err, users) { - if (err) done(err); - users.should.have.property('length', 2); - users[0].addressLoc.should.not.be.null(); - users[0].name.should.equal('Paul McCartney'); - users[1].addressLoc.should.not.equal(null); - users[1].name.should.equal('John Lennon'); - done(); - }); - }); - - it('supports multiple "near" queries with "or" ' + - 'inside a coumpound query with "and"', function(done) { - User.find({ - where: { - and: [ - { - or: [ - { - addressLoc: { - near: '29.9,-90.04', - maxDistance: 300, - }, - }, - { - addressLoc: { - near: '22.7,-89.03', - maxDistance: 50, - }, - }, - ], - }, - { - vip: true, - }, - ], - }, - }, function(err, users) { - if (err) done(err); - users.should.have.property('length', 1); - users[0].addressLoc.should.not.be.null(); - users[0].name.should.equal('John Lennon'); - users[0].vip.should.be.true(); - done(); - }); - }); - }); - }); - it('should only include fields as specified', function(done) { var remaining = 0; @@ -732,9 +590,8 @@ describe('basic-querying', function() { } sample({name: true}).expect(['name']); - sample({name: false}).expect([ - 'id', 'seq', 'email', 'role', 'order', 'birthday', 'vip', 'address', 'friends', 'addressLoc', - ]); + sample({name: false}).expect(['id', 'seq', 'email', 'role', 'order', 'birthday', 'vip', + 'address', 'friends']); sample({name: false, id: true}).expect(['id']); sample({id: true}).expect(['id']); sample('id').expect(['id']); @@ -1148,7 +1005,6 @@ function seed(done) { {name: 'George Harrison'}, {name: 'Ringo Starr'}, ], - addressLoc: {lat: 29.97, lng: -90.03}, }, { seq: 1, @@ -1169,15 +1025,8 @@ function seed(done) { {name: 'George Harrison'}, {name: 'Ringo Starr'}, ], - addressLoc: {lat: 22.97, lng: -88.03}, - }, - { - seq: 2, - name: 'George Harrison', - order: 5, - vip: false, - addressLoc: {lat: 22.7, lng: -89.03}, }, + {seq: 2, name: 'George Harrison', order: 5, vip: false}, {seq: 3, name: 'Ringo Starr', order: 6, vip: false}, {seq: 4, name: 'Pete Best', order: 4}, {seq: 5, name: 'Stuart Sutcliffe', order: 3, vip: true}, diff --git a/test/geo.test.js b/test/geo.test.js index a005c372..20fc2dd2 100644 --- a/test/geo.test.js +++ b/test/geo.test.js @@ -147,8 +147,8 @@ describe('GeoPoint', function() { }, }; var filter = nearFilter(where); - filter[0].key.should.equal('location'); - filter[0].should.have.properties({ + filter.key.should.equal('location'); + filter.should.have.properties({ key: 'location', near: { lat: 40.77492964101182, @@ -187,7 +187,7 @@ describe('GeoPoint', function() { lng: 121.483687, }, }]; - var filter = [{ + var filter = { key: 'location', near: { lat: 30.278562, @@ -195,7 +195,7 @@ describe('GeoPoint', function() { }, unit: 'meters', minDistance: 10000, - }]; + }; var results = geoFilter(points, filter); results.length.should.be.equal(3); });