From 420be7abc67711ea2ce1f70a89a564fa74d4f1e0 Mon Sep 17 00:00:00 2001 From: Slava Kim Date: Tue, 24 Sep 2013 15:13:04 -0700 Subject: [PATCH 01/31] First $near commit. Workaround for weird $near + $maxDistance syntax. --- packages/minimongo/minimongo_tests.js | 10 ++++++++++ packages/minimongo/selector.js | 25 ++++++++++++++++++++++++- 2 files changed, 34 insertions(+), 1 deletion(-) diff --git a/packages/minimongo/minimongo_tests.js b/packages/minimongo/minimongo_tests.js index 46146a51ef..33683a5958 100644 --- a/packages/minimongo/minimongo_tests.js +++ b/packages/minimongo/minimongo_tests.js @@ -2266,3 +2266,13 @@ Tinytest.add("minimongo - count on cursor with limit", function(test){ c.stop(); }); + +Tinytest.add("minimongo - $near operator tests", function (test) { + var coll = new LocalCollection(); + coll.insert({ rest: { loc: [2, 3] } }); + coll.insert({ rest: { loc: [-3, 3] } }); + coll.insert({ rest: { loc: [5, 5] } }); + + test.equal(coll.find({ 'rest.loc': { $near: [0, 0], $maxDistance: 4 } }).count(), 3); +}); + diff --git a/packages/minimongo/selector.js b/packages/minimongo/selector.js index 057e0d9195..17c0b4ee2f 100644 --- a/packages/minimongo/selector.js +++ b/packages/minimongo/selector.js @@ -74,8 +74,13 @@ var compileValueSelector = function (valueSelector) { _.each(valueSelector, function (operand, operator) { if (!_.has(VALUE_OPERATORS, operator)) throw new Error("Unrecognized operator: " + operator); + var options = valueSelector.$options; + // Special case for location operators + if (operator === "$near") + options = _.extend(options || {}, + { $maxDistance: valueSelector.$maxDistance }); operatorFunctions.push(VALUE_OPERATORS[operator]( - operand, valueSelector.$options)); + operand, options)); }); return function (value) { return _.all(operatorFunctions, function (f) { @@ -303,6 +308,24 @@ var VALUE_OPERATORS = { return function (value) { return !matcher(value); }; + }, + + "$near": function (operand, options) { + var matcher = compileValueSelector(operand); + var distance = options.$maxDistance; + if (distance === undefined) + distance = -1; + return function (value) { + return distance + }; + }, + + "$maxDistance": function () { + // $maxDistance is always considered my $near and is + // ignored if the one is not present + return function () { + return true; + } } }; From 49aec0c2d483e02de537f5e760b359082988f78d Mon Sep 17 00:00:00 2001 From: Slava Kim Date: Tue, 24 Sep 2013 17:00:07 -0700 Subject: [PATCH 02/31] Simplest possible implementation of $near. Still no sorting --- packages/minimongo/minimongo_tests.js | 3 ++- packages/minimongo/selector.js | 39 ++++++++++++++++----------- 2 files changed, 25 insertions(+), 17 deletions(-) diff --git a/packages/minimongo/minimongo_tests.js b/packages/minimongo/minimongo_tests.js index 33683a5958..32f30df33d 100644 --- a/packages/minimongo/minimongo_tests.js +++ b/packages/minimongo/minimongo_tests.js @@ -2273,6 +2273,7 @@ Tinytest.add("minimongo - $near operator tests", function (test) { coll.insert({ rest: { loc: [-3, 3] } }); coll.insert({ rest: { loc: [5, 5] } }); - test.equal(coll.find({ 'rest.loc': { $near: [0, 0], $maxDistance: 4 } }).count(), 3); + test.equal(coll.find({ 'rest.loc': { $near: [0, 0], $maxDistance: 30 } }).count(), 3); + test.equal(coll.find({ 'rest.loc': { $near: [0, 0], $maxDistance: 4 } }).count(), 1); }); diff --git a/packages/minimongo/selector.js b/packages/minimongo/selector.js index 17c0b4ee2f..b62b764b7d 100644 --- a/packages/minimongo/selector.js +++ b/packages/minimongo/selector.js @@ -74,13 +74,9 @@ var compileValueSelector = function (valueSelector) { _.each(valueSelector, function (operand, operator) { if (!_.has(VALUE_OPERATORS, operator)) throw new Error("Unrecognized operator: " + operator); - var options = valueSelector.$options; // Special case for location operators - if (operator === "$near") - options = _.extend(options || {}, - { $maxDistance: valueSelector.$maxDistance }); operatorFunctions.push(VALUE_OPERATORS[operator]( - operand, options)); + operand, valueSelector)); }); return function (value) { return _.all(operatorFunctions, function (f) { @@ -260,7 +256,8 @@ var VALUE_OPERATORS = { }; }, - "$regex": function (operand, options) { + "$regex": function (operand, operators) { + var options = operators.$options; if (options !== undefined) { // Options passed in $options (even the empty string) always overrides // options in the RegExp object itself. (See also @@ -310,22 +307,32 @@ var VALUE_OPERATORS = { }; }, - "$near": function (operand, options) { + "$near": function (operand, operators) { + function distance (a, b) { + var x = a[0] - b[0]; + var y = a[1] - b[1]; + return Math.sqrt(x * x + y * y); + } + // Makes sure we get 2 elements array and assume the first one to be x and + // the second one to y no matter what user passes. + // In case user passes { lon: x, lat: y } returns [x, y] + function pointToArray (point) { + var npoint = []; + _.each(point, function (xy) { npoint.push(xy); }); + return npoint; + } var matcher = compileValueSelector(operand); - var distance = options.$maxDistance; - if (distance === undefined) - distance = -1; + var maxDistance = operators.$maxDistance; + var point = pointToArray(operand); return function (value) { - return distance + return maxDistance === undefined ? true : + distance(point, pointToArray(value)) <= maxDistance; }; }, "$maxDistance": function () { - // $maxDistance is always considered my $near and is - // ignored if the one is not present - return function () { - return true; - } + // evaluation happens in the $near operator + return function () { return true; } } }; From 83a944fdb486348b312ee9d1d10e0e4923cd01bd Mon Sep 17 00:00:00 2001 From: Slava Kim Date: Wed, 25 Sep 2013 08:18:16 -0700 Subject: [PATCH 03/31] In minimongo selector compilation pass along the whole selector and original cursor when possible. So this information can be used by weird operators such as $near or $. --- packages/minimongo/minimongo.js | 8 +++--- packages/minimongo/selector.js | 47 ++++++++++++++++++++------------- 2 files changed, 32 insertions(+), 23 deletions(-) diff --git a/packages/minimongo/minimongo.js b/packages/minimongo/minimongo.js index 2fae504cea..3b8d64b218 100644 --- a/packages/minimongo/minimongo.js +++ b/packages/minimongo/minimongo.js @@ -87,11 +87,11 @@ LocalCollection.Cursor = function (collection, selector, options) { if (LocalCollection._selectorIsId(selector)) { // stash for fast path self.selector_id = LocalCollection._idStringify(selector); - self.selector_f = LocalCollection._compileSelector(selector); + self.selector_f = LocalCollection._compileSelector(selector, self); self.sort_f = undefined; } else { self.selector_id = undefined; - self.selector_f = LocalCollection._compileSelector(selector); + self.selector_f = LocalCollection._compileSelector(selector, self); self.sort_f = options.sort ? LocalCollection._compileSort(options.sort) : null; } self.skip = options.skip; @@ -488,7 +488,7 @@ LocalCollection.prototype.remove = function (selector, callback) { var remove = []; var queriesToRecompute = []; - var selector_f = LocalCollection._compileSelector(selector); + var selector_f = LocalCollection._compileSelector(selector, self); // Avoid O(n) for "remove a single doc by ID". var specificIds = LocalCollection._idsMatchedBySelector(selector); @@ -555,7 +555,7 @@ LocalCollection.prototype.update = function (selector, mod, options, callback) { } if (!options) options = {}; - var selector_f = LocalCollection._compileSelector(selector); + var selector_f = LocalCollection._compileSelector(selector, self); // Save the original results of any query that we might need to // _recomputeResults on, because _modifyAndNotify will mutate the objects in diff --git a/packages/minimongo/selector.js b/packages/minimongo/selector.js index b62b764b7d..d2ba62cb38 100644 --- a/packages/minimongo/selector.js +++ b/packages/minimongo/selector.js @@ -29,7 +29,7 @@ var hasOperators = function(valueSelector) { return !!theseAreOperators; // {} has no operators }; -var compileValueSelector = function (valueSelector) { +var compileValueSelector = function (valueSelector, selector, cursor) { if (valueSelector == null) { // undefined or null return function (value) { return _anyIfArray(value, function (x) { @@ -76,7 +76,7 @@ var compileValueSelector = function (valueSelector) { throw new Error("Unrecognized operator: " + operator); // Special case for location operators operatorFunctions.push(VALUE_OPERATORS[operator]( - operand, valueSelector)); + operand, valueSelector, cursor)); }); return function (value) { return _.all(operatorFunctions, function (f) { @@ -96,11 +96,12 @@ var compileValueSelector = function (valueSelector) { // XXX can factor out common logic below var LOGICAL_OPERATORS = { - "$and": function(subSelector) { + "$and": function(subSelector, _, cursor) { if (!isArray(subSelector) || _.isEmpty(subSelector)) throw Error("$and/$or/$nor must be nonempty array"); var subSelectorFunctions = _.map( - subSelector, compileDocumentSelector); + subSelector, function (selector) { + return compileDocumentSelector(selector, cursor); }); return function (doc) { return _.all(subSelectorFunctions, function (f) { return f(doc); @@ -108,11 +109,12 @@ var LOGICAL_OPERATORS = { }; }, - "$or": function(subSelector) { + "$or": function(subSelector, _, cursor) { if (!isArray(subSelector) || _.isEmpty(subSelector)) throw Error("$and/$or/$nor must be nonempty array"); var subSelectorFunctions = _.map( - subSelector, compileDocumentSelector); + subSelector, function (selector) { + return compileDocumentSelector(selector, cursor); }); return function (doc) { return _.any(subSelectorFunctions, function (f) { return f(doc); @@ -120,11 +122,12 @@ var LOGICAL_OPERATORS = { }; }, - "$nor": function(subSelector) { + "$nor": function(subSelector, _, cursor) { if (!isArray(subSelector) || _.isEmpty(subSelector)) throw Error("$and/$or/$nor must be nonempty array"); var subSelectorFunctions = _.map( - subSelector, compileDocumentSelector); + subSelector, function (selector) { + return compileDocumentSelector(selector, cursor); }); return function (doc) { return _.all(subSelectorFunctions, function (f) { return !f(doc); @@ -289,8 +292,8 @@ var VALUE_OPERATORS = { return function (value) { return true; }; }, - "$elemMatch": function (operand) { - var matcher = compileDocumentSelector(operand); + "$elemMatch": function (operand, selector, cursor) { + var matcher = compileDocumentSelector(operand, cursor); return function (value) { if (!isArray(value)) return false; @@ -307,8 +310,9 @@ var VALUE_OPERATORS = { }; }, - "$near": function (operand, operators) { - function distance (a, b) { + "$near": function (operand, operators, cursor) { + function distance (a, b, mode) { + // XXX if mode is 2dsphere, compute differently var x = a[0] - b[0]; var y = a[1] - b[1]; return Math.sqrt(x * x + y * y); @@ -325,8 +329,11 @@ var VALUE_OPERATORS = { var maxDistance = operators.$maxDistance; var point = pointToArray(operand); return function (value) { - return maxDistance === undefined ? true : - distance(point, pointToArray(value)) <= maxDistance; + var dist = distance(point, pointToArray(value), cursor._2dMode); + // Used later in sorting by distance, since $near queries are sorted by + // distance from closest to farthest. + cursor._distance[value._id] = dist; + return maxDistance === undefined ? true : dist <= maxDistance; }; }, @@ -566,7 +573,7 @@ LocalCollection._makeLookupFunction = function (key) { }; // The main compilation function for a given selector. -var compileDocumentSelector = function (docSelector) { +var compileDocumentSelector = function (docSelector, cursor) { var perKeySelectors = []; _.each(docSelector, function (subSelector, key) { if (key.substr(0, 1) === '$') { @@ -574,10 +581,12 @@ var compileDocumentSelector = function (docSelector) { // this function), or $where. if (!_.has(LOGICAL_OPERATORS, key)) throw new Error("Unrecognized logical operator: " + key); - perKeySelectors.push(LOGICAL_OPERATORS[key](subSelector)); + perKeySelectors.push( + LOGICAL_OPERATORS[key](subSelector, docSelector, cursor)); } else { var lookUpByIndex = LocalCollection._makeLookupFunction(key); - var valueSelectorFunc = compileValueSelector(subSelector); + var valueSelectorFunc = + compileValueSelector(subSelector, docSelector, cursor); perKeySelectors.push(function (doc) { var branchValues = lookUpByIndex(doc); // We apply the selector to each "branched" value and return true if any @@ -615,7 +624,7 @@ var compileDocumentSelector = function (docSelector) { // Given a selector, return a function that takes one argument, a // document, and returns true if the document matches the selector, // else false. -LocalCollection._compileSelector = function (selector) { +LocalCollection._compileSelector = function (selector, cursor) { // you can pass a literal function instead of a selector if (selector instanceof Function) return function (doc) {return selector.call(doc);}; @@ -638,7 +647,7 @@ LocalCollection._compileSelector = function (selector) { EJSON.isBinary(selector)) throw new Error("Invalid selector: " + selector); - return compileDocumentSelector(selector); + return compileDocumentSelector(selector, cursor); }; // Give a sort spec, which can be in any of these forms: From 6158599ab4b34d38e55adb76ff9c0541d6eed3ef Mon Sep 17 00:00:00 2001 From: Slava Kim Date: Wed, 25 Sep 2013 10:27:53 -0700 Subject: [PATCH 04/31] Some docs. --- packages/minimongo/selector.js | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/packages/minimongo/selector.js b/packages/minimongo/selector.js index d2ba62cb38..6f25aac103 100644 --- a/packages/minimongo/selector.js +++ b/packages/minimongo/selector.js @@ -145,6 +145,13 @@ var LOGICAL_OPERATORS = { } }; +// Each value operator is a function with args: +// - operand - Anything +// - operators - Object - operators on the same level (neighbours) +// - cursor - Object - original cursor +// returns a function with args: +// - value - a value the operator is tested against +// - doc - the whole document tested in this query var VALUE_OPERATORS = { "$in": function (operand) { if (!isArray(operand)) From cd3edc6a2b609862c6ecf0ce9c49a60af2f1d86e Mon Sep 17 00:00:00 2001 From: Slava Kim Date: Wed, 25 Sep 2013 10:29:13 -0700 Subject: [PATCH 05/31] Naming unused param as '_' wasn't the best idea. --- packages/minimongo/selector.js | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/packages/minimongo/selector.js b/packages/minimongo/selector.js index 6f25aac103..991b9d1418 100644 --- a/packages/minimongo/selector.js +++ b/packages/minimongo/selector.js @@ -96,7 +96,7 @@ var compileValueSelector = function (valueSelector, selector, cursor) { // XXX can factor out common logic below var LOGICAL_OPERATORS = { - "$and": function(subSelector, _, cursor) { + "$and": function(subSelector, operators, cursor) { if (!isArray(subSelector) || _.isEmpty(subSelector)) throw Error("$and/$or/$nor must be nonempty array"); var subSelectorFunctions = _.map( @@ -109,7 +109,7 @@ var LOGICAL_OPERATORS = { }; }, - "$or": function(subSelector, _, cursor) { + "$or": function(subSelector, operators, cursor) { if (!isArray(subSelector) || _.isEmpty(subSelector)) throw Error("$and/$or/$nor must be nonempty array"); var subSelectorFunctions = _.map( @@ -122,7 +122,7 @@ var LOGICAL_OPERATORS = { }; }, - "$nor": function(subSelector, _, cursor) { + "$nor": function(subSelector, operators, cursor) { if (!isArray(subSelector) || _.isEmpty(subSelector)) throw Error("$and/$or/$nor must be nonempty array"); var subSelectorFunctions = _.map( From 5888a9b114fc85b6ce15fa51233deb72222d9f25 Mon Sep 17 00:00:00 2001 From: Slava Kim Date: Wed, 25 Sep 2013 11:14:04 -0700 Subject: [PATCH 06/31] Pass the whole doc and the neighbor operators around in minimongo selector matchers. So various operators can look up what is the doc they are matching --- packages/minimongo/selector.js | 66 +++++++++++++++++----------------- 1 file changed, 34 insertions(+), 32 deletions(-) diff --git a/packages/minimongo/selector.js b/packages/minimongo/selector.js index 991b9d1418..add85e0008 100644 --- a/packages/minimongo/selector.js +++ b/packages/minimongo/selector.js @@ -78,9 +78,9 @@ var compileValueSelector = function (valueSelector, selector, cursor) { operatorFunctions.push(VALUE_OPERATORS[operator]( operand, valueSelector, cursor)); }); - return function (value) { + return function (value, doc) { return _.all(operatorFunctions, function (f) { - return f(value); + return f(value, doc); }); }; } @@ -99,12 +99,11 @@ var LOGICAL_OPERATORS = { "$and": function(subSelector, operators, cursor) { if (!isArray(subSelector) || _.isEmpty(subSelector)) throw Error("$and/$or/$nor must be nonempty array"); - var subSelectorFunctions = _.map( - subSelector, function (selector) { - return compileDocumentSelector(selector, cursor); }); - return function (doc) { + var subSelectorFunctions = _.map(subSelector, function (selector) { + return compileDocumentSelector(selector, cursor); }); + return function (doc, wholeDoc) { return _.all(subSelectorFunctions, function (f) { - return f(doc); + return f(doc, wholeDoc); }); }; }, @@ -112,12 +111,11 @@ var LOGICAL_OPERATORS = { "$or": function(subSelector, operators, cursor) { if (!isArray(subSelector) || _.isEmpty(subSelector)) throw Error("$and/$or/$nor must be nonempty array"); - var subSelectorFunctions = _.map( - subSelector, function (selector) { - return compileDocumentSelector(selector, cursor); }); - return function (doc) { + var subSelectorFunctions = _.map(subSelector, function (selector) { + return compileDocumentSelector(selector, cursor); }); + return function (doc, wholeDoc) { return _.any(subSelectorFunctions, function (f) { - return f(doc); + return f(doc, wholeDoc); }); }; }, @@ -125,12 +123,11 @@ var LOGICAL_OPERATORS = { "$nor": function(subSelector, operators, cursor) { if (!isArray(subSelector) || _.isEmpty(subSelector)) throw Error("$and/$or/$nor must be nonempty array"); - var subSelectorFunctions = _.map( - subSelector, function (selector) { - return compileDocumentSelector(selector, cursor); }); - return function (doc) { + var subSelectorFunctions = _.map(subSelector, function (selector) { + return compileDocumentSelector(selector, cursor); }); + return function (doc, wholeDoc) { return _.all(subSelectorFunctions, function (f) { - return !f(doc); + return !f(doc, wholeDoc); }); }; }, @@ -223,11 +220,11 @@ var VALUE_OPERATORS = { if (!isArray(operand)) throw new Error("Argument to $nin must be array"); var inFunction = VALUE_OPERATORS.$in(operand); - return function (value) { + return function (value, doc) { // Field doesn't exist, so it's not-in operand if (value === undefined) return true; - return !inFunction(value); + return !inFunction(value, doc); }; }, @@ -301,19 +298,19 @@ var VALUE_OPERATORS = { "$elemMatch": function (operand, selector, cursor) { var matcher = compileDocumentSelector(operand, cursor); - return function (value) { + return function (value, doc) { if (!isArray(value)) return false; return _.any(value, function (x) { - return matcher(x); + return matcher(x, doc); }); }; }, - "$not": function (operand) { - var matcher = compileValueSelector(operand); - return function (value) { - return !matcher(value); + "$not": function (operand, operators, cursor) { + var matcher = compileValueSelector(operand, operators, cursor); + return function (value, doc) { + return !matcher(value, doc); }; }, @@ -332,14 +329,15 @@ var VALUE_OPERATORS = { _.each(point, function (xy) { npoint.push(xy); }); return npoint; } - var matcher = compileValueSelector(operand); var maxDistance = operators.$maxDistance; var point = pointToArray(operand); - return function (value) { + return function (value, doc) { var dist = distance(point, pointToArray(value), cursor._2dMode); // Used later in sorting by distance, since $near queries are sorted by // distance from closest to farthest. - cursor._distance[value._id] = dist; + if (!cursor._distance) + cursor._distance = {}; + cursor._distance[doc._id] = dist; return maxDistance === undefined ? true : dist <= maxDistance; }; }, @@ -594,7 +592,7 @@ var compileDocumentSelector = function (docSelector, cursor) { var lookUpByIndex = LocalCollection._makeLookupFunction(key); var valueSelectorFunc = compileValueSelector(subSelector, docSelector, cursor); - perKeySelectors.push(function (doc) { + perKeySelectors.push(function (doc, wholeDoc) { var branchValues = lookUpByIndex(doc); // We apply the selector to each "branched" value and return true if any // match. However, for "negative" selectors like $ne or $not we actually @@ -615,15 +613,19 @@ var compileDocumentSelector = function (docSelector, cursor) { (subSelector.$not || subSelector.$ne || subSelector.$nin)) ? _.all : _.any; - return combiner(branchValues, valueSelectorFunc); + return combiner(branchValues, function (val) { + return valueSelectorFunc(val, wholeDoc); + }); }); } }); - return function (doc) { + return function (doc, wholeDoc) { + if (wholeDoc === undefined) + wholeDoc = doc; return _.all(perKeySelectors, function (f) { - return f(doc); + return f(doc, wholeDoc); }); }; }; From 0587f3293b4a0c441ac58c88539af04fbecfbad1 Mon Sep 17 00:00:00 2001 From: Slava Kim Date: Wed, 25 Sep 2013 11:43:36 -0700 Subject: [PATCH 07/31] Sort takes into consideration the distance from given point on $near queries. --- packages/minimongo/minimongo.js | 2 +- packages/minimongo/minimongo_tests.js | 12 ++++++++++++ packages/minimongo/selector.js | 10 ++++++++-- 3 files changed, 21 insertions(+), 3 deletions(-) diff --git a/packages/minimongo/minimongo.js b/packages/minimongo/minimongo.js index 3b8d64b218..d19b6f12cf 100644 --- a/packages/minimongo/minimongo.js +++ b/packages/minimongo/minimongo.js @@ -92,7 +92,7 @@ LocalCollection.Cursor = function (collection, selector, options) { } else { self.selector_id = undefined; self.selector_f = LocalCollection._compileSelector(selector, self); - self.sort_f = options.sort ? LocalCollection._compileSort(options.sort) : null; + self.sort_f = options.sort ? LocalCollection._compileSort(options.sort, self) : null; } self.skip = options.skip; self.limit = options.limit; diff --git a/packages/minimongo/minimongo_tests.js b/packages/minimongo/minimongo_tests.js index 32f30df33d..dd0160ba76 100644 --- a/packages/minimongo/minimongo_tests.js +++ b/packages/minimongo/minimongo_tests.js @@ -2275,5 +2275,17 @@ Tinytest.add("minimongo - $near operator tests", function (test) { test.equal(coll.find({ 'rest.loc': { $near: [0, 0], $maxDistance: 30 } }).count(), 3); test.equal(coll.find({ 'rest.loc': { $near: [0, 0], $maxDistance: 4 } }).count(), 1); + var points = coll.find({ 'rest.loc': { $near: [0, 0], $maxDistance: 6 } }).fetch(); + _.each(points, function (point, i, points) { + test.isTrue(!i || distance([0, 0], point.rest.loc) >= distance([0, 0], points[i - 1].rest.loc)); + }); + + function distance(a, b) { + var x = a[0] - b[0]; + var y = a[1] - b[1]; + return Math.sqrt(x * x + y * y); + } + + // XXX more tests! }); diff --git a/packages/minimongo/selector.js b/packages/minimongo/selector.js index add85e0008..726d50652d 100644 --- a/packages/minimongo/selector.js +++ b/packages/minimongo/selector.js @@ -672,7 +672,7 @@ LocalCollection._compileSelector = function (selector, cursor) { // first object comes first in order, 1 if the second object comes // first, or 0 if neither object comes before the other. -LocalCollection._compileSort = function (spec) { +LocalCollection._compileSort = function (spec, cursor) { var sortSpecParts = []; if (spec instanceof Array) { @@ -700,8 +700,14 @@ LocalCollection._compileSort = function (spec) { throw Error("Bad sort specification: ", JSON.stringify(spec)); } + // If there are no sorting rules specified, try to sort on _distance hidden + // fields on cursor we may acquire if query involved $near operator. if (sortSpecParts.length === 0) - return function () {return 0;}; + return function (a, b) { + if (!cursor || !cursor._distance) + return 0; + return cursor._distance[a._id] - cursor._distance[b._id]; + }; // reduceValue takes in all the possible values for the sort key along various // branches, and returns the min or max value (according to the bool From 1de8d392b6f6cc3c88daf9de84e26bf7c5cbd996 Mon Sep 17 00:00:00 2001 From: Slava Kim Date: Wed, 25 Sep 2013 18:07:06 -0700 Subject: [PATCH 08/31] EnsureIndex accepts 2d or 2dsphere options to understand what user wants in minimongo. --- packages/minimongo/minimongo.js | 15 +++++++++++++++ packages/minimongo/selector.js | 2 +- 2 files changed, 16 insertions(+), 1 deletion(-) diff --git a/packages/minimongo/minimongo.js b/packages/minimongo/minimongo.js index d19b6f12cf..5301a37554 100644 --- a/packages/minimongo/minimongo.js +++ b/packages/minimongo/minimongo.js @@ -1175,3 +1175,18 @@ LocalCollection._compileProjection = function (fields) { return res; }; }; + +// This function exists for convenience of using geo-location operators such as +// $near and doesn't actually build any indexes. Function will still throw an +// exception if you try to ensure index on anything rather than "2d" or +// "2dsphere" on a single field. +LocalCollection.prototype.ensureIndex = function (keys, options) { + if (options || _.keys(keys).length !== 1 || + !_.contains(["2d", "2dsphere"], _.values(keys)[0])) + throw new Error("Can only call _ensureIndex on server collections " + + "(exception for geo-location indexes)"); + + var self = this; + self._2dMode = _.values(keys)[0]; +}; + diff --git a/packages/minimongo/selector.js b/packages/minimongo/selector.js index 726d50652d..9d7df43146 100644 --- a/packages/minimongo/selector.js +++ b/packages/minimongo/selector.js @@ -332,7 +332,7 @@ var VALUE_OPERATORS = { var maxDistance = operators.$maxDistance; var point = pointToArray(operand); return function (value, doc) { - var dist = distance(point, pointToArray(value), cursor._2dMode); + var dist = distance(point, pointToArray(value), cursor.collection._2dMode); // Used later in sorting by distance, since $near queries are sorted by // distance from closest to farthest. if (!cursor._distance) From fb7309881ddb7e6c4c58edf57536595d0d8fe167 Mon Sep 17 00:00:00 2001 From: Slava Kim Date: Fri, 27 Sep 2013 12:38:49 -0700 Subject: [PATCH 09/31] GeoJSON Utils package. Not sure if it is good enough, let's see in later tests. --- packages/geojson-utils/.gitignore | 1 + packages/geojson-utils/geojson-utils.js | 382 ++++++++++++++++++ packages/geojson-utils/geojson-utils.tests.js | 55 +++ packages/geojson-utils/package.js | 15 + 4 files changed, 453 insertions(+) create mode 100644 packages/geojson-utils/.gitignore create mode 100644 packages/geojson-utils/geojson-utils.js create mode 100644 packages/geojson-utils/geojson-utils.tests.js create mode 100644 packages/geojson-utils/package.js diff --git a/packages/geojson-utils/.gitignore b/packages/geojson-utils/.gitignore new file mode 100644 index 0000000000..677a6fc263 --- /dev/null +++ b/packages/geojson-utils/.gitignore @@ -0,0 +1 @@ +.build* diff --git a/packages/geojson-utils/geojson-utils.js b/packages/geojson-utils/geojson-utils.js new file mode 100644 index 0000000000..59da072d4d --- /dev/null +++ b/packages/geojson-utils/geojson-utils.js @@ -0,0 +1,382 @@ +(function () { + var gju = {}; + + // Export the geojson object for **CommonJS** + if (typeof module !== 'undefined' && module.exports) { + module.exports = gju; + } + + // Export for Meteor uni-package + GeoJSON = gju; + + // adapted from http://www.kevlindev.com/gui/math/intersection/Intersection.js + gju.lineStringsIntersect = function (l1, l2) { + var intersects = []; + for (var i = 0; i <= l1.coordinates.length - 2; ++i) { + for (var j = 0; j <= l2.coordinates.length - 2; ++j) { + var a1 = { + x: l1.coordinates[i][1], + y: l1.coordinates[i][0] + }, + a2 = { + x: l1.coordinates[i + 1][1], + y: l1.coordinates[i + 1][0] + }, + b1 = { + x: l2.coordinates[j][1], + y: l2.coordinates[j][0] + }, + b2 = { + x: l2.coordinates[j + 1][1], + y: l2.coordinates[j + 1][0] + }, + ua_t = (b2.x - b1.x) * (a1.y - b1.y) - (b2.y - b1.y) * (a1.x - b1.x), + ub_t = (a2.x - a1.x) * (a1.y - b1.y) - (a2.y - a1.y) * (a1.x - b1.x), + u_b = (b2.y - b1.y) * (a2.x - a1.x) - (b2.x - b1.x) * (a2.y - a1.y); + if (u_b != 0) { + var ua = ua_t / u_b, + ub = ub_t / u_b; + if (0 <= ua && ua <= 1 && 0 <= ub && ub <= 1) { + intersects.push({ + 'type': 'Point', + 'coordinates': [a1.x + ua * (a2.x - a1.x), a1.y + ua * (a2.y - a1.y)] + }); + } + } + } + } + if (intersects.length == 0) intersects = false; + return intersects; + } + + // Bounding Box + + function boundingBoxAroundPolyCoords (coords) { + var xAll = [], yAll = [] + + for (var i = 0; i < coords[0].length; i++) { + xAll.push(coords[0][i][1]) + yAll.push(coords[0][i][0]) + } + + xAll = xAll.sort(function (a,b) { return a - b }) + yAll = yAll.sort(function (a,b) { return a - b }) + + return [ [xAll[0], yAll[0]], [xAll[xAll.length - 1], yAll[yAll.length - 1]] ] + } + + gju.pointInBoundingBox = function (point, bounds) { + return !(point.coordinates[1] < bounds[0][0] || point.coordinates[1] > bounds[1][0] || point.coordinates[0] < bounds[0][1] || point.coordinates[0] > bounds[1][1]) + } + + // Point in Polygon + // http://www.ecse.rpi.edu/Homepages/wrf/Research/Short_Notes/pnpoly.html#Listing the Vertices + + function pnpoly (x,y,coords) { + var vert = [ [0,0] ] + + for (var i = 0; i < coords.length; i++) { + for (var j = 0; j < coords[i].length; j++) { + vert.push(coords[i][j]) + } + vert.push([0,0]) + } + + var inside = false + for (var i = 0, j = vert.length - 1; i < vert.length; j = i++) { + if (((vert[i][0] > y) != (vert[j][0] > y)) && (x < (vert[j][1] - vert[i][1]) * (y - vert[i][0]) / (vert[j][0] - vert[i][0]) + vert[i][1])) inside = !inside + } + + return inside + } + + gju.pointInPolygon = function (p, poly) { + var coords = (poly.type == "Polygon") ? [ poly.coordinates ] : poly.coordinates + + var insideBox = false + for (var i = 0; i < coords.length; i++) { + if (gju.pointInBoundingBox(p, boundingBoxAroundPolyCoords(coords[i]))) insideBox = true + } + if (!insideBox) return false + + var insidePoly = false + for (var i = 0; i < coords.length; i++) { + if (pnpoly(p.coordinates[1], p.coordinates[0], coords[i])) insidePoly = true + } + + return insidePoly + } + + gju.numberToRadius = function (number) { + return number * Math.PI / 180; + } + + gju.numberToDegree = function (number) { + return number * 180 / Math.PI; + } + + // written with help from @tautologe + gju.drawCircle = function (radiusInMeters, centerPoint, steps) { + var center = [centerPoint.coordinates[1], centerPoint.coordinates[0]], + dist = (radiusInMeters / 1000) / 6371, + // convert meters to radiant + radCenter = [gju.numberToRadius(center[0]), gju.numberToRadius(center[1])], + steps = steps || 15, + // 15 sided circle + poly = [[center[0], center[1]]]; + for (var i = 0; i < steps; i++) { + var brng = 2 * Math.PI * i / steps; + var lat = Math.asin(Math.sin(radCenter[0]) * Math.cos(dist) + + Math.cos(radCenter[0]) * Math.sin(dist) * Math.cos(brng)); + var lng = radCenter[1] + Math.atan2(Math.sin(brng) * Math.sin(dist) * Math.cos(radCenter[0]), + Math.cos(dist) - Math.sin(radCenter[0]) * Math.sin(lat)); + poly[i] = []; + poly[i][1] = gju.numberToDegree(lat); + poly[i][0] = gju.numberToDegree(lng); + } + return { + "type": "Polygon", + "coordinates": [poly] + }; + } + + // assumes rectangle starts at lower left point + gju.rectangleCentroid = function (rectangle) { + var bbox = rectangle.coordinates[0]; + var xmin = bbox[0][0], + ymin = bbox[0][1], + xmax = bbox[2][0], + ymax = bbox[2][1]; + var xwidth = xmax - xmin; + var ywidth = ymax - ymin; + return { + 'type': 'Point', + 'coordinates': [xmin + xwidth / 2, ymin + ywidth / 2] + }; + } + + // from http://www.movable-type.co.uk/scripts/latlong.html + gju.pointDistance = function (pt1, pt2) { + var lon1 = pt1.coordinates[1], + lat1 = pt1.coordinates[0], + lon2 = pt2.coordinates[1], + lat2 = pt2.coordinates[0], + dLat = gju.numberToRadius(lat2 - lat1), + dLon = gju.numberToRadius(lon2 - lon1), + a = Math.pow(Math.sin(dLat / 2), 2) + Math.cos(gju.numberToRadius(lat1)) + * Math.cos(gju.numberToRadius(lat2)) * Math.pow(Math.sin(dLon / 2), 2), + c = 2 * Math.atan2(Math.sqrt(a), Math.sqrt(1 - a)); + return (6371 * c) * 1000; // returns meters + }, + + // checks if geometry lies entirely within a circle + // works with Point, LineString, Polygon + gju.geometryWithinRadius = function (geometry, center, radius) { + if (geometry.type == 'Point') { + return gju.pointDistance(geometry, center) <= radius; + } else if (geometry.type == 'LineString' || geometry.type == 'Polygon') { + var point = {}; + var coordinates; + if (geometry.type == 'Polygon') { + // it's enough to check the exterior ring of the Polygon + coordinates = geometry.coordinates[0]; + } else { + coordinates = geometry.coordinates; + } + for (var i in coordinates) { + point.coordinates = coordinates[i]; + if (gju.pointDistance(point, center) > radius) { + return false; + } + } + } + return true; + } + + // adapted from http://paulbourke.net/geometry/polyarea/javascript.txt + gju.area = function (polygon) { + var area = 0; + // TODO: polygon holes at coordinates[1] + var points = polygon.coordinates[0]; + var j = points.length - 1; + var p1, p2; + + for (var i = 0; i < points.length; j = i++) { + var p1 = { + x: points[i][1], + y: points[i][0] + }; + var p2 = { + x: points[j][1], + y: points[j][0] + }; + area += p1.x * p2.y; + area -= p1.y * p2.x; + } + + area /= 2; + return area; + }, + + // adapted from http://paulbourke.net/geometry/polyarea/javascript.txt + gju.centroid = function (polygon) { + var f, x = 0, + y = 0; + // TODO: polygon holes at coordinates[1] + var points = polygon.coordinates[0]; + var j = points.length - 1; + var p1, p2; + + for (var i = 0; i < points.length; j = i++) { + var p1 = { + x: points[i][1], + y: points[i][0] + }; + var p2 = { + x: points[j][1], + y: points[j][0] + }; + f = p1.x * p2.y - p2.x * p1.y; + x += (p1.x + p2.x) * f; + y += (p1.y + p2.y) * f; + } + + f = gju.area(polygon) * 6; + return { + 'type': 'Point', + 'coordinates': [y / f, x / f] + }; + }, + + gju.simplify = function (source, kink) { /* source[] array of geojson points */ + /* kink in metres, kinks above this depth kept */ + /* kink depth is the height of the triangle abc where a-b and b-c are two consecutive line segments */ + kink = kink || 20; + source = source.map(function (o) { + return { + lng: o.coordinates[0], + lat: o.coordinates[1] + } + }); + + var n_source, n_stack, n_dest, start, end, i, sig; + var dev_sqr, max_dev_sqr, band_sqr; + var x12, y12, d12, x13, y13, d13, x23, y23, d23; + var F = (Math.PI / 180.0) * 0.5; + var index = new Array(); /* aray of indexes of source points to include in the reduced line */ + var sig_start = new Array(); /* indices of start & end of working section */ + var sig_end = new Array(); + + /* check for simple cases */ + + if (source.length < 3) return (source); /* one or two points */ + + /* more complex case. initialize stack */ + + n_source = source.length; + band_sqr = kink * 360.0 / (2.0 * Math.PI * 6378137.0); /* Now in degrees */ + band_sqr *= band_sqr; + n_dest = 0; + sig_start[0] = 0; + sig_end[0] = n_source - 1; + n_stack = 1; + + /* while the stack is not empty ... */ + while (n_stack > 0) { + + /* ... pop the top-most entries off the stacks */ + + start = sig_start[n_stack - 1]; + end = sig_end[n_stack - 1]; + n_stack--; + + if ((end - start) > 1) { /* any intermediate points ? */ + + /* ... yes, so find most deviant intermediate point to + either side of line joining start & end points */ + + x12 = (source[end].lng() - source[start].lng()); + y12 = (source[end].lat() - source[start].lat()); + if (Math.abs(x12) > 180.0) x12 = 360.0 - Math.abs(x12); + x12 *= Math.cos(F * (source[end].lat() + source[start].lat())); /* use avg lat to reduce lng */ + d12 = (x12 * x12) + (y12 * y12); + + for (i = start + 1, sig = start, max_dev_sqr = -1.0; i < end; i++) { + + x13 = source[i].lng() - source[start].lng(); + y13 = source[i].lat() - source[start].lat(); + if (Math.abs(x13) > 180.0) x13 = 360.0 - Math.abs(x13); + x13 *= Math.cos(F * (source[i].lat() + source[start].lat())); + d13 = (x13 * x13) + (y13 * y13); + + x23 = source[i].lng() - source[end].lng(); + y23 = source[i].lat() - source[end].lat(); + if (Math.abs(x23) > 180.0) x23 = 360.0 - Math.abs(x23); + x23 *= Math.cos(F * (source[i].lat() + source[end].lat())); + d23 = (x23 * x23) + (y23 * y23); + + if (d13 >= (d12 + d23)) dev_sqr = d23; + else if (d23 >= (d12 + d13)) dev_sqr = d13; + else dev_sqr = (x13 * y12 - y13 * x12) * (x13 * y12 - y13 * x12) / d12; // solve triangle + if (dev_sqr > max_dev_sqr) { + sig = i; + max_dev_sqr = dev_sqr; + } + } + + if (max_dev_sqr < band_sqr) { /* is there a sig. intermediate point ? */ + /* ... no, so transfer current start point */ + index[n_dest] = start; + n_dest++; + } else { /* ... yes, so push two sub-sections on stack for further processing */ + n_stack++; + sig_start[n_stack - 1] = sig; + sig_end[n_stack - 1] = end; + n_stack++; + sig_start[n_stack - 1] = start; + sig_end[n_stack - 1] = sig; + } + } else { /* ... no intermediate points, so transfer current start point */ + index[n_dest] = start; + n_dest++; + } + } + + /* transfer last point */ + index[n_dest] = n_source - 1; + n_dest++; + + /* make return array */ + var r = new Array(); + for (var i = 0; i < n_dest; i++) + r.push(source[index[i]]); + + return r.map(function (o) { + return { + type: "Point", + coordinates: [o.lng, o.lat] + } + }); + } + + // http://www.movable-type.co.uk/scripts/latlong.html#destPoint + gju.destinationPoint = function (pt, brng, dist) { + dist = dist/6371; // convert dist to angular distance in radians + brng = gju.numberToRadius(brng); + + var lat1 = gju.numberToRadius(pt.coordinates[0]); + var lon1 = gju.numberToRadius(pt.coordinates[1]); + + var lat2 = Math.asin( Math.sin(lat1)*Math.cos(dist) + + Math.cos(lat1)*Math.sin(dist)*Math.cos(brng) ); + var lon2 = lon1 + Math.atan2(Math.sin(brng)*Math.sin(dist)*Math.cos(lat1), + Math.cos(dist)-Math.sin(lat1)*Math.sin(lat2)); + lon2 = (lon2+3*Math.PI) % (2*Math.PI) - Math.PI; // normalise to -180..+180ยบ + + return { + 'type': 'Point', + 'coordinates': [gju.numberToDegree(lat2), gju.numberToDegree(lon2)] + }; + }; + +})(); diff --git a/packages/geojson-utils/geojson-utils.tests.js b/packages/geojson-utils/geojson-utils.tests.js new file mode 100644 index 0000000000..38a2ce9ea1 --- /dev/null +++ b/packages/geojson-utils/geojson-utils.tests.js @@ -0,0 +1,55 @@ +var gju = GeoJSON; + +Tinytest.add("geojson-utils - line intersects", function (test) { + var diagonalUp = { "type": "LineString","coordinates": [ + [0, 0], [10, 10] + ]} + var diagonalDown = { "type": "LineString","coordinates": [ + [10, 0], [0, 10] + ]} + var farAway = { "type": "LineString","coordinates": [ + [100, 100], [110, 110] + ]} + + test.isTrue(gju.lineStringsIntersect(diagonalUp, diagonalDown)); + test.isFalse(gju.lineStringsIntersect(diagonalUp, farAway)); +}); + +// Used by two tests +var box = { + "type": "Polygon", + "coordinates": [ + [ [0, 0], [10, 0], [10, 10], [0, 10] ] + ] +}; + +Tinytest.add("geojson-utils - inside/outside of the box", function (test) { + + var inBox = {"type": "Point", "coordinates": [5, 5]} + var outBox = {"type": "Point", "coordinates": [15, 15]} + + test.isTrue(gju.pointInPolygon(inBox, box)); + test.isFalse(gju.pointInPolygon(outBox, box)); +}); + +Tinytest.add("geojson-utils - drawCircle", function (test) { + test.length(gju.drawCircle(10, {"type": "Point", "coordinates": [0, 0]}). + coordinates[0], 15); + test.length(gju.drawCircle(10, {"type": "Point", "coordinates": [0, 0]}, 50). + coordinates[0], 50); +}); + +Tinytest.add("geojson-utils - centroid", function (test) { + var centroid = gju.rectangleCentroid(box) + test.equal(centroid.coordinates[0], 5); + test.equal(centroid.coordinates[1], 5); +}); + +Tinytest.add("geojson-utils - point distance", function (test) { + var fairyLand = {"type": "Point", + "coordinates": [37.80919060818706, -122.260000705719]} + var navalBase = {"type": "Point", + "coordinates": [37.78774223089045, -122.32083320617676]} + test.equal(Math.floor(gju.pointDistance(fairyLand, navalBase)), 5852); +}); + diff --git a/packages/geojson-utils/package.js b/packages/geojson-utils/package.js new file mode 100644 index 0000000000..d687834461 --- /dev/null +++ b/packages/geojson-utils/package.js @@ -0,0 +1,15 @@ +Package.describe({ + summary: "Meteor's client-side datastore: a port of MongoDB to Javascript", + internal: true +}); + +Package.on_use(function (api) { + api.export('GeoJSON'); + api.add_files(['geojson-utils.js']); +}); + +Package.on_test(function (api) { + api.use('tinytest'); + api.add_files(['geojson-utils.js', 'geojson-utils.tests.js'], 'client'); +}); + From a1a555558a6a02ba7e7f2b997c6716b863577f99 Mon Sep 17 00:00:00 2001 From: Slava Kim Date: Fri, 27 Sep 2013 14:24:24 -0700 Subject: [PATCH 10/31] Comment on constant in distance calculations. --- packages/geojson-utils/geojson-utils.js | 1 + 1 file changed, 1 insertion(+) diff --git a/packages/geojson-utils/geojson-utils.js b/packages/geojson-utils/geojson-utils.js index 59da072d4d..af33fe6284 100644 --- a/packages/geojson-utils/geojson-utils.js +++ b/packages/geojson-utils/geojson-utils.js @@ -166,6 +166,7 @@ a = Math.pow(Math.sin(dLat / 2), 2) + Math.cos(gju.numberToRadius(lat1)) * Math.cos(gju.numberToRadius(lat2)) * Math.pow(Math.sin(dLon / 2), 2), c = 2 * Math.atan2(Math.sqrt(a), Math.sqrt(1 - a)); + // Earth radius is 6371 km return (6371 * c) * 1000; // returns meters }, From d68661192f9f9b034fd8e4d284498a26a58ddb65 Mon Sep 17 00:00:00 2001 From: Slava Kim Date: Fri, 27 Sep 2013 14:54:56 -0700 Subject: [PATCH 11/31] Imply GeoJSON Utils package and use it to calculate distance between geojson objects. --- packages/minimongo/minimongo.js | 5 ++++- packages/minimongo/package.js | 3 +++ packages/minimongo/selector.js | 33 +++++++++++++++++++++++++++++---- 3 files changed, 36 insertions(+), 5 deletions(-) diff --git a/packages/minimongo/minimongo.js b/packages/minimongo/minimongo.js index 5301a37554..9b739b15cf 100644 --- a/packages/minimongo/minimongo.js +++ b/packages/minimongo/minimongo.js @@ -1185,8 +1185,11 @@ LocalCollection.prototype.ensureIndex = function (keys, options) { !_.contains(["2d", "2dsphere"], _.values(keys)[0])) throw new Error("Can only call _ensureIndex on server collections " + "(exception for geo-location indexes)"); - + var self = this; self._2dMode = _.values(keys)[0]; + + if (self._2dMode === "2dsphere" && GeoJSON === undefined) + throw new Error("Need geojson-utils package for GeoJSON calculations"); }; diff --git a/packages/minimongo/package.js b/packages/minimongo/package.js index 7502f33d08..51e0217ec4 100644 --- a/packages/minimongo/package.js +++ b/packages/minimongo/package.js @@ -7,6 +7,8 @@ Package.on_use(function (api) { api.export('LocalCollection'); api.use(['underscore', 'json', 'ejson', 'ordered-dict', 'deps', 'random', 'ordered-dict']); + // If you really want geolocation queries to work, add this package + api.imply('geojson-utils'); api.add_files([ 'minimongo.js', 'selector.js', @@ -18,6 +20,7 @@ Package.on_use(function (api) { Package.on_test(function (api) { api.use('minimongo', 'client'); + api.use('geojson-utils', 'client'); api.use('test-helpers', 'client'); api.use(['tinytest', 'underscore', 'ejson', 'ordered-dict', 'random', 'deps']); diff --git a/packages/minimongo/selector.js b/packages/minimongo/selector.js index 9d7df43146..3e5557e3d2 100644 --- a/packages/minimongo/selector.js +++ b/packages/minimongo/selector.js @@ -315,10 +315,13 @@ var VALUE_OPERATORS = { }, "$near": function (operand, operators, cursor) { - function distance (a, b, mode) { - // XXX if mode is 2dsphere, compute differently + function distanceCoordinatePairs (a, b) { + a = pointToArray(a); + b = pointToArray(b); var x = a[0] - b[0]; var y = a[1] - b[1]; + if (_.isNaN(x) || _.isNan(y)) + return null; return Math.sqrt(x * x + y * y); } // Makes sure we get 2 elements array and assume the first one to be x and @@ -330,14 +333,36 @@ var VALUE_OPERATORS = { return npoint; } var maxDistance = operators.$maxDistance; - var point = pointToArray(operand); + var point = operand; return function (value, doc) { - var dist = distance(point, pointToArray(value), cursor.collection._2dMode); + var dist = null; + switch (cursor.collection._2dMode) { + case "2d": + dist = distanceCoordinatePairs(point, value); + break; + case "2dsphere": + // XXX: for now, we don't calculate the actual distance between, say, + // polygon and circle. If people care about this use-case it will get + // a priority. + if (value.type === "Point") + dist = GeoJSON.pointDistance(point, value); + else + dist = GeoJSON.geometryWithinRadius(value, point, maxDistance) ? + 0 : maxDistance + 1; + break; + default: + throw new Error("Unknown mode for distance calculations."); + } // Used later in sorting by distance, since $near queries are sorted by // distance from closest to farthest. if (!cursor._distance) cursor._distance = {}; cursor._distance[doc._id] = dist; + + // Distance couldn't parse a geometry object + if (dist === null) + return false; + return maxDistance === undefined ? true : dist <= maxDistance; }; }, From b5230e6fee347901bf0e5a79e56d64ca132489e6 Mon Sep 17 00:00:00 2001 From: Slava Kim Date: Fri, 27 Sep 2013 14:59:51 -0700 Subject: [PATCH 12/31] Fix package's description --- packages/geojson-utils/package.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/geojson-utils/package.js b/packages/geojson-utils/package.js index d687834461..15d3fd3f7a 100644 --- a/packages/geojson-utils/package.js +++ b/packages/geojson-utils/package.js @@ -1,5 +1,5 @@ Package.describe({ - summary: "Meteor's client-side datastore: a port of MongoDB to Javascript", + summary: 'GeoJSON utility functions', internal: true }); From 8fa42acdbe18a0c954e67918a5323bb48110b542 Mon Sep 17 00:00:00 2001 From: Slava Kim Date: Fri, 27 Sep 2013 15:02:17 -0700 Subject: [PATCH 13/31] Calculation method by default is 2d --- packages/minimongo/selector.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/minimongo/selector.js b/packages/minimongo/selector.js index 3e5557e3d2..281ada3329 100644 --- a/packages/minimongo/selector.js +++ b/packages/minimongo/selector.js @@ -320,7 +320,7 @@ var VALUE_OPERATORS = { b = pointToArray(b); var x = a[0] - b[0]; var y = a[1] - b[1]; - if (_.isNaN(x) || _.isNan(y)) + if (_.isNaN(x) || _.isNaN(y)) return null; return Math.sqrt(x * x + y * y); } @@ -336,7 +336,7 @@ var VALUE_OPERATORS = { var point = operand; return function (value, doc) { var dist = null; - switch (cursor.collection._2dMode) { + switch (cursor.collection._2dMode || "2d") { case "2d": dist = distanceCoordinatePairs(point, value); break; From a5534ca414bd5ce23a7edbb47e0686a52c50c12c Mon Sep 17 00:00:00 2001 From: Slava Kim Date: Fri, 27 Sep 2013 15:53:25 -0700 Subject: [PATCH 14/31] A lot of generated tests to actually make sure distance is correct. --- packages/geojson-utils/geojson-utils.js | 8 ++--- packages/geojson-utils/geojson-utils.tests.js | 30 +++++++++++++++++-- 2 files changed, 32 insertions(+), 6 deletions(-) diff --git a/packages/geojson-utils/geojson-utils.js b/packages/geojson-utils/geojson-utils.js index af33fe6284..28ce187606 100644 --- a/packages/geojson-utils/geojson-utils.js +++ b/packages/geojson-utils/geojson-utils.js @@ -157,10 +157,10 @@ // from http://www.movable-type.co.uk/scripts/latlong.html gju.pointDistance = function (pt1, pt2) { - var lon1 = pt1.coordinates[1], - lat1 = pt1.coordinates[0], - lon2 = pt2.coordinates[1], - lat2 = pt2.coordinates[0], + var lon1 = pt1.coordinates[0], + lat1 = pt1.coordinates[1], + lon2 = pt2.coordinates[0], + lat2 = pt2.coordinates[1], dLat = gju.numberToRadius(lat2 - lat1), dLon = gju.numberToRadius(lon2 - lon1), a = Math.pow(Math.sin(dLat / 2), 2) + Math.cos(gju.numberToRadius(lat1)) diff --git a/packages/geojson-utils/geojson-utils.tests.js b/packages/geojson-utils/geojson-utils.tests.js index 38a2ce9ea1..5aae2f7d4f 100644 --- a/packages/geojson-utils/geojson-utils.tests.js +++ b/packages/geojson-utils/geojson-utils.tests.js @@ -47,9 +47,35 @@ Tinytest.add("geojson-utils - centroid", function (test) { Tinytest.add("geojson-utils - point distance", function (test) { var fairyLand = {"type": "Point", - "coordinates": [37.80919060818706, -122.260000705719]} + "coordinates": [-122.260000705719, 37.80919060818706]} var navalBase = {"type": "Point", - "coordinates": [37.78774223089045, -122.32083320617676]} + "coordinates": [-122.32083320617676, 37.78774223089045]} test.equal(Math.floor(gju.pointDistance(fairyLand, navalBase)), 5852); }); +Tinytest.add("geojson-utils - points distance generated tests", function (test) { + var floatEqual = function (a, b) { + test.isTrue(Math.abs(a - b) < 0.000001); + }; + floatEqual(GeoJSON.pointDistance( {"type":"Point","coordinates":[-19.416501816827804,-13.442164216190577]} , {"type":"Point","coordinates":[8.694866622798145,-8.511979941977188]} ), 3115066.2536578891 ); +floatEqual(GeoJSON.pointDistance( {"type":"Point","coordinates":[151.2841189110186,-56.14564002258703]} , {"type":"Point","coordinates":[167.77983197313733,0.05544793023727834]} ), 6423493.2321747802 ); +floatEqual(GeoJSON.pointDistance( {"type":"Point","coordinates":[100.28413630579598,-88.02313695591874]} , {"type":"Point","coordinates":[36.48786173714325,53.44207073468715]} ), 15848950.0402601473 ); +floatEqual(GeoJSON.pointDistance( {"type":"Point","coordinates":[-70.34899035631679,76.51596869179048]} , {"type":"Point","coordinates":[154.91605914011598,-73.60970971290953]} ), 18714226.5425080135 ); +floatEqual(GeoJSON.pointDistance( {"type":"Point","coordinates":[96.28682994353585,58.77288202662021]} , {"type":"Point","coordinates":[-118.33936230326071,72.07877089688554]} ), 5223022.7731127860 ); +floatEqual(GeoJSON.pointDistance( {"type":"Point","coordinates":[140.35530551429838,10.507104953983799]} , {"type":"Point","coordinates":[-67.73368513956666,38.075836981181055]} ), 13874476.3135112207 ); +floatEqual(GeoJSON.pointDistance( {"type":"Point","coordinates":[69.55582775664516,86.25450283149257]} , {"type":"Point","coordinates":[-18.446230484172702,6.116170521359891]} ), 9314403.3309389465 ); +floatEqual(GeoJSON.pointDistance( {"type":"Point","coordinates":[163.83647522330284,-65.7211532376241]} , {"type":"Point","coordinates":[-159.2198902608361,-78.42975475382991]} ), 1831929.5917785936 ); +floatEqual(GeoJSON.pointDistance( {"type":"Point","coordinates":[-178.9383797585033,-54.87420454365201]} , {"type":"Point","coordinates":[-175.35227065649815,-84.04084282391705]} ), 3244710.9344544266 ); +floatEqual(GeoJSON.pointDistance( {"type":"Point","coordinates":[-48.63219943456352,11.284161149058491]} , {"type":"Point","coordinates":[-179.12627786491066,-51.95622375886887]} ), 13691492.4666933995 ); +floatEqual(GeoJSON.pointDistance( {"type":"Point","coordinates":[140.29684206470847,-67.20720696030185]} , {"type":"Point","coordinates":[-109.37452355003916,36.03131077555008]} ), 14525055.6462231465 ); +floatEqual(GeoJSON.pointDistance( {"type":"Point","coordinates":[-154.6698773431126,58.322094617411494]} , {"type":"Point","coordinates":[61.18583445576951,-4.3424885796848685]} ), 13261602.4336371962 ); +floatEqual(GeoJSON.pointDistance( {"type":"Point","coordinates":[122.5562841903884,10.43972848681733]} , {"type":"Point","coordinates":[-11.756078708684072,-43.86124441982247]} ), 14275427.5511620939 ); +floatEqual(GeoJSON.pointDistance( {"type":"Point","coordinates":[-67.91648306301795,-86.38826347864233]} , {"type":"Point","coordinates":[163.577536230674,12.987319261068478]} ), 11699799.3615680672 ); +floatEqual(GeoJSON.pointDistance( {"type":"Point","coordinates":[91.65140007715672,17.595150742679834]} , {"type":"Point","coordinates":[135.80393003183417,22.307532118167728]} ), 4628773.1129429890 ); +floatEqual(GeoJSON.pointDistance( {"type":"Point","coordinates":[-112.70280818711035,34.45729674655013]} , {"type":"Point","coordinates":[-127.42168210959062,-25.51327457977459]} ), 6846704.0253010122 ); +floatEqual(GeoJSON.pointDistance( {"type":"Point","coordinates":[-161.55807900894433,-77.40711871231906]} , {"type":"Point","coordinates":[-92.66313794790767,-89.12077954714186]} ), 1368055.9401701286 ); +floatEqual(GeoJSON.pointDistance( {"type":"Point","coordinates":[39.966264681424946,9.890176948625594]} , {"type":"Point","coordinates":[-159.88646019320004,40.60383598925546]} ), 14041503.0409814864 ); +floatEqual(GeoJSON.pointDistance( {"type":"Point","coordinates":[-57.48232689569704,86.64061016729102]} , {"type":"Point","coordinates":[59.53941993578337,-75.73194969259202]} ), 18560499.7346975356 ); +floatEqual(GeoJSON.pointDistance( {"type":"Point","coordinates":[-142.0938081513159,80.76813141163439]} , {"type":"Point","coordinates":[14.891517050098628,64.56322408467531]} ), 3793112.6186894816 ); +}); + From 9b8971ede65c9755f4662c58628137c8454aeb56 Mon Sep 17 00:00:00 2001 From: Slava Kim Date: Fri, 27 Sep 2013 16:58:38 -0700 Subject: [PATCH 15/31] Proved to have working api for 2dsphere. --- packages/minimongo/minimongo.js | 2 +- packages/minimongo/selector.js | 9 +++++---- 2 files changed, 6 insertions(+), 5 deletions(-) diff --git a/packages/minimongo/minimongo.js b/packages/minimongo/minimongo.js index 9b739b15cf..8f71186d1a 100644 --- a/packages/minimongo/minimongo.js +++ b/packages/minimongo/minimongo.js @@ -1180,7 +1180,7 @@ LocalCollection._compileProjection = function (fields) { // $near and doesn't actually build any indexes. Function will still throw an // exception if you try to ensure index on anything rather than "2d" or // "2dsphere" on a single field. -LocalCollection.prototype.ensureIndex = function (keys, options) { +LocalCollection.prototype._ensureIndex = function (keys, options) { if (options || _.keys(keys).length !== 1 || !_.contains(["2d", "2dsphere"], _.values(keys)[0])) throw new Error("Can only call _ensureIndex on server collections " + diff --git a/packages/minimongo/selector.js b/packages/minimongo/selector.js index 281ada3329..46b4f25915 100644 --- a/packages/minimongo/selector.js +++ b/packages/minimongo/selector.js @@ -332,11 +332,12 @@ var VALUE_OPERATORS = { _.each(point, function (xy) { npoint.push(xy); }); return npoint; } - var maxDistance = operators.$maxDistance; - var point = operand; + var mode = cursor.collection._2dMode || "2d"; + var maxDistance = mode === "2d" ? operators.$maxDistance : operand.$maxDistance; + var point = mode === "2d" ? operand : operand.$geometry; return function (value, doc) { var dist = null; - switch (cursor.collection._2dMode || "2d") { + switch (mode) { case "2d": dist = distanceCoordinatePairs(point, value); break; @@ -351,7 +352,7 @@ var VALUE_OPERATORS = { 0 : maxDistance + 1; break; default: - throw new Error("Unknown mode for distance calculations."); + throw new Error("Unknown mode for distance calculations: " + mode); } // Used later in sorting by distance, since $near queries are sorted by // distance from closest to farthest. From 3f976e874f0e34e06f5c6e8ab240f73e2c1a64f7 Mon Sep 17 00:00:00 2001 From: Slava Kim Date: Fri, 27 Sep 2013 17:14:40 -0700 Subject: [PATCH 16/31] Let _compileSort handle empty sorting rules. So the line that says: "sort by distance by default" will work --- packages/minimongo/minimongo.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/minimongo/minimongo.js b/packages/minimongo/minimongo.js index 8f71186d1a..57bb896461 100644 --- a/packages/minimongo/minimongo.js +++ b/packages/minimongo/minimongo.js @@ -92,7 +92,7 @@ LocalCollection.Cursor = function (collection, selector, options) { } else { self.selector_id = undefined; self.selector_f = LocalCollection._compileSelector(selector, self); - self.sort_f = options.sort ? LocalCollection._compileSort(options.sort, self) : null; + self.sort_f = LocalCollection._compileSort(options.sort || [], self); } self.skip = options.skip; self.limit = options.limit; From 3e6da8b2666532c0f9fd7bc5c05a6a295aa606e7 Mon Sep 17 00:00:00 2001 From: Slava Kim Date: Fri, 27 Sep 2013 17:35:07 -0700 Subject: [PATCH 17/31] Don't mess the sorting unless it's a geo query --- packages/minimongo/minimongo.js | 9 ++++++++- packages/minimongo/selector.js | 1 + 2 files changed, 9 insertions(+), 1 deletion(-) diff --git a/packages/minimongo/minimongo.js b/packages/minimongo/minimongo.js index 57bb896461..6f1f3ec886 100644 --- a/packages/minimongo/minimongo.js +++ b/packages/minimongo/minimongo.js @@ -92,7 +92,8 @@ LocalCollection.Cursor = function (collection, selector, options) { } else { self.selector_id = undefined; self.selector_f = LocalCollection._compileSelector(selector, self); - self.sort_f = LocalCollection._compileSort(options.sort || [], self); + self.sort_f = (isGeoQuery(selector) || options.sort) ? + LocalCollection._compileSort(options.sort || [], self) : null; } self.skip = options.skip; self.limit = options.limit; @@ -1193,3 +1194,9 @@ LocalCollection.prototype._ensureIndex = function (keys, options) { throw new Error("Need geojson-utils package for GeoJSON calculations"); }; +var isGeoQuery = function (selector) { + return _.any(selector, function (val, key) { + return key === "$near" || (_.isObject(val) && isGeoQuery(val)); + }); +}; + diff --git a/packages/minimongo/selector.js b/packages/minimongo/selector.js index 46b4f25915..3ccf462a27 100644 --- a/packages/minimongo/selector.js +++ b/packages/minimongo/selector.js @@ -785,3 +785,4 @@ LocalCollection._compileSort = function (spec, cursor) { return 0; }; }; + From 638dd0566b389e9ed9d627c06c72c75768f39ab1 Mon Sep 17 00:00:00 2001 From: Slava Kim Date: Fri, 27 Sep 2013 18:00:27 -0700 Subject: [PATCH 18/31] More tests. And weakly depend, not imply! --- packages/minimongo/minimongo.js | 2 +- packages/minimongo/minimongo_tests.js | 28 ++++++++++++++++++++++++++- packages/minimongo/package.js | 4 ++-- packages/minimongo/selector.js | 3 +++ 4 files changed, 33 insertions(+), 4 deletions(-) diff --git a/packages/minimongo/minimongo.js b/packages/minimongo/minimongo.js index 6f1f3ec886..67f5929f83 100644 --- a/packages/minimongo/minimongo.js +++ b/packages/minimongo/minimongo.js @@ -1190,7 +1190,7 @@ LocalCollection.prototype._ensureIndex = function (keys, options) { var self = this; self._2dMode = _.values(keys)[0]; - if (self._2dMode === "2dsphere" && GeoJSON === undefined) + if (self._2dMode === "2dsphere" && !Package['geojson-utils']) throw new Error("Need geojson-utils package for GeoJSON calculations"); }; diff --git a/packages/minimongo/minimongo_tests.js b/packages/minimongo/minimongo_tests.js index dd0160ba76..3469d50b50 100644 --- a/packages/minimongo/minimongo_tests.js +++ b/packages/minimongo/minimongo_tests.js @@ -2286,6 +2286,32 @@ Tinytest.add("minimongo - $near operator tests", function (test) { return Math.sqrt(x * x + y * y); } - // XXX more tests! + // GeoJSON tests + coll = new LocalCollection(); + var data = [{ "category" : "LARCENY/THEFT", "descript" : "GRAND THEFT OF PROPERTY", "address" : "900 Block of MINNA ST", "location" : { "type" : "Point", "coordinates" : [ -122.41538270191, 37.774683628213 ] }, "_id" : "eRHmRvNtHbCjNw6ox" }, + { "category" : "LARCENY/THEFT", "descript" : "PETTY THEFT FROM LOCKED AUTO", "address" : "900 Block of MINNA ST", "location" : { "type" : "Point", "coordinates" : [ -122.415386041221, 37.7747879744156 ] }, "_id" : "fc9KktqwLcfKaKBDr" }, + { "category" : "OTHER OFFENSES", "descript" : "POSSESSION OF BURGLARY TOOLS", "address" : "900 Block of MINNA ST", "location" : { "type" : "Point", "coordinates" : [ -122.415386041221, 37.7747879744156 ] }, "_id" : "fr2gw6x8agCXwcKkK" }, + { "category" : "WEAPON LAWS", "descript" : "POSS OF PROHIBITED WEAPON", "address" : "900 Block of MINNA ST", "location" : { "type" : "Point", "coordinates" : [ -122.415386041221, 37.7747879744156 ] }, "_id" : "nn3oaDPoeGC5JwqsS" }, + { "category" : "BURGLARY", "descript" : "BURGLARY OF STORE, FORCIBLE ENTRY", "address" : "100 Block of 10TH ST", "location" : { "type" : "Point", "coordinates" : [ -122.415449723856, 37.7749518087273 ] }, "_id" : "qm2F7wnB5D6yjKBNn" }]; + + _.each(data, function (x) { coll.insert(x); }); + coll._ensureIndex({ location: "2dsphere" }); + + var close15 = coll.find({ location: { $near: { + $geometry: { type: "Point", + coordinates: [-122.4154282, 37.7746115] }, + $maxDistance: 15 } } }).fetch(); + test.length(close15, 1); + test.equal(close15[0].descript, "GRAND THEFT OF PROPERTY"); + + var close20 = coll.find({ location: { $near: { + $geometry: { type: "Point", + coordinates: [-122.4154282, 37.7746115] }, + $maxDistance: 20 } } }).fetch(); + test.length(close20, 4); + test.equal(close20[0].descript, "GRAND THEFT OF PROPERTY"); + test.equal(close20[1].descript, "PETTY THEFT FROM LOCKED AUTO"); + test.equal(close20[2].descript, "POSSESSION OF BURGLARY TOOLS"); + test.equal(close20[3].descript, "POSS OF PROHIBITED WEAPON"); }); diff --git a/packages/minimongo/package.js b/packages/minimongo/package.js index 51e0217ec4..2c881346e3 100644 --- a/packages/minimongo/package.js +++ b/packages/minimongo/package.js @@ -8,7 +8,7 @@ Package.on_use(function (api) { api.use(['underscore', 'json', 'ejson', 'ordered-dict', 'deps', 'random', 'ordered-dict']); // If you really want geolocation queries to work, add this package - api.imply('geojson-utils'); + api.use('geojson-utils', { weak: true }); api.add_files([ 'minimongo.js', 'selector.js', @@ -19,8 +19,8 @@ Package.on_use(function (api) { }); Package.on_test(function (api) { - api.use('minimongo', 'client'); api.use('geojson-utils', 'client'); + api.use('minimongo', 'client'); api.use('test-helpers', 'client'); api.use(['tinytest', 'underscore', 'ejson', 'ordered-dict', 'random', 'deps']); diff --git a/packages/minimongo/selector.js b/packages/minimongo/selector.js index 3ccf462a27..1cd120473c 100644 --- a/packages/minimongo/selector.js +++ b/packages/minimongo/selector.js @@ -342,6 +342,9 @@ var VALUE_OPERATORS = { dist = distanceCoordinatePairs(point, value); break; case "2dsphere": + // Presence of this package is checked when user ensures 2dsphere + // index on collection. + var GeoJSON = Package['geojson-utils'].GeoJSON; // XXX: for now, we don't calculate the actual distance between, say, // polygon and circle. If people care about this use-case it will get // a priority. From e127ed25aaa53b299d3f40828e0648de8ac41ec6 Mon Sep 17 00:00:00 2001 From: Slava Kim Date: Fri, 27 Sep 2013 19:59:30 -0700 Subject: [PATCH 19/31] Touch History.md --- History.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/History.md b/History.md index a81fadcf71..6abbe60e9a 100644 --- a/History.md +++ b/History.md @@ -23,6 +23,8 @@ MINIMONGO * Fix various bugs with using Mongo ObjectIds. (XXX bad description! this is 3cfeed and 008847) +* `$near` operator for `2d` and `2dsphere` indices. + * Improve behavior of `$ne`, $nin`, and `$not` selectors with objects containing arrays. #1451 From f902d6f55edb06da094b81281270c8868bbd339b Mon Sep 17 00:00:00 2001 From: Slava Kim Date: Mon, 30 Sep 2013 15:16:26 -0700 Subject: [PATCH 20/31] Don't have to ensure index to distinguish 2d vs 2dsphere queries. --- packages/minimongo/minimongo.js | 17 ----------------- packages/minimongo/minimongo_tests.js | 1 - packages/minimongo/selector.js | 9 ++++----- 3 files changed, 4 insertions(+), 23 deletions(-) diff --git a/packages/minimongo/minimongo.js b/packages/minimongo/minimongo.js index 67f5929f83..ee5cbacd45 100644 --- a/packages/minimongo/minimongo.js +++ b/packages/minimongo/minimongo.js @@ -1177,23 +1177,6 @@ LocalCollection._compileProjection = function (fields) { }; }; -// This function exists for convenience of using geo-location operators such as -// $near and doesn't actually build any indexes. Function will still throw an -// exception if you try to ensure index on anything rather than "2d" or -// "2dsphere" on a single field. -LocalCollection.prototype._ensureIndex = function (keys, options) { - if (options || _.keys(keys).length !== 1 || - !_.contains(["2d", "2dsphere"], _.values(keys)[0])) - throw new Error("Can only call _ensureIndex on server collections " + - "(exception for geo-location indexes)"); - - var self = this; - self._2dMode = _.values(keys)[0]; - - if (self._2dMode === "2dsphere" && !Package['geojson-utils']) - throw new Error("Need geojson-utils package for GeoJSON calculations"); -}; - var isGeoQuery = function (selector) { return _.any(selector, function (val, key) { return key === "$near" || (_.isObject(val) && isGeoQuery(val)); diff --git a/packages/minimongo/minimongo_tests.js b/packages/minimongo/minimongo_tests.js index 3469d50b50..d6e11b61b4 100644 --- a/packages/minimongo/minimongo_tests.js +++ b/packages/minimongo/minimongo_tests.js @@ -2295,7 +2295,6 @@ Tinytest.add("minimongo - $near operator tests", function (test) { { "category" : "BURGLARY", "descript" : "BURGLARY OF STORE, FORCIBLE ENTRY", "address" : "100 Block of 10TH ST", "location" : { "type" : "Point", "coordinates" : [ -122.415449723856, 37.7749518087273 ] }, "_id" : "qm2F7wnB5D6yjKBNn" }]; _.each(data, function (x) { coll.insert(x); }); - coll._ensureIndex({ location: "2dsphere" }); var close15 = coll.find({ location: { $near: { $geometry: { type: "Point", diff --git a/packages/minimongo/selector.js b/packages/minimongo/selector.js index 1cd120473c..3fd13896d0 100644 --- a/packages/minimongo/selector.js +++ b/packages/minimongo/selector.js @@ -332,7 +332,8 @@ var VALUE_OPERATORS = { _.each(point, function (xy) { npoint.push(xy); }); return npoint; } - var mode = cursor.collection._2dMode || "2d"; + // GeoJSON query is marked as $geometry property + var mode = _.isObject(operand) && _.has(operand, '$geometry') ? "2dsphere" : "2d"; var maxDistance = mode === "2d" ? operators.$maxDistance : operand.$maxDistance; var point = mode === "2d" ? operand : operand.$geometry; return function (value, doc) { @@ -342,8 +343,8 @@ var VALUE_OPERATORS = { dist = distanceCoordinatePairs(point, value); break; case "2dsphere": - // Presence of this package is checked when user ensures 2dsphere - // index on collection. + if (!_.has(Package, 'geojson-utils')) + throw new Error('Need geojson-utils package for GeoJSON calculations'); var GeoJSON = Package['geojson-utils'].GeoJSON; // XXX: for now, we don't calculate the actual distance between, say, // polygon and circle. If people care about this use-case it will get @@ -354,8 +355,6 @@ var VALUE_OPERATORS = { dist = GeoJSON.geometryWithinRadius(value, point, maxDistance) ? 0 : maxDistance + 1; break; - default: - throw new Error("Unknown mode for distance calculations: " + mode); } // Used later in sorting by distance, since $near queries are sorted by // distance from closest to farthest. From 56aeac63ddd2a7e703c78cb63b71e5601df629b9 Mon Sep 17 00:00:00 2001 From: Slava Kim Date: Mon, 30 Sep 2013 15:33:31 -0700 Subject: [PATCH 21/31] Typo in NOTES. --- packages/minimongo/NOTES | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/minimongo/NOTES b/packages/minimongo/NOTES index 017aeda689..e2db1fb0e2 100644 --- a/packages/minimongo/NOTES +++ b/packages/minimongo/NOTES @@ -46,7 +46,7 @@ It just hasn't been looked at/thought about yet. upsert combined with $-operators might work, but hasn't actually been looked at or tested. -In general, the API needs tests, espectially update. (On the other +In general, the API needs tests, especially update. (On the other hand, the underlying selector and mutator code is quite well tested.) ## OTHER STUFF ## From 9e898fe1e975877595fe1df8b2a96f7db18a0854 Mon Sep 17 00:00:00 2001 From: Slava Kim Date: Mon, 30 Sep 2013 15:46:31 -0700 Subject: [PATCH 22/31] Add a link to the original geojson-utils and add the copyright notice to LICENSE --- LICENSE.txt | 6 ++++++ packages/geojson-utils/package.js | 1 + 2 files changed, 7 insertions(+) diff --git a/LICENSE.txt b/LICENSE.txt index 319a50a963..8c9a5d378f 100644 --- a/LICENSE.txt +++ b/LICENSE.txt @@ -516,6 +516,12 @@ Copyright 2011, Robert Mustacchi. All rights reserved. Copyright 2011, Joyent, Inc. All rights reserved. +---------- +geojson-utils: https://github.com/maxogden/geojson-js-utils +---------- + +Copyright (c) 2010 Max Ogden + ============== Apache License ============== diff --git a/packages/geojson-utils/package.js b/packages/geojson-utils/package.js index 15d3fd3f7a..9cc423db0b 100644 --- a/packages/geojson-utils/package.js +++ b/packages/geojson-utils/package.js @@ -1,5 +1,6 @@ Package.describe({ summary: 'GeoJSON utility functions', + url: 'https://github.com/maxogden/geojson-js-utils', internal: true }); From aacd87505ea5b69927b5fa6a9943c160d3354cb1 Mon Sep 17 00:00:00 2001 From: Slava Kim Date: Wed, 2 Oct 2013 16:39:41 -0700 Subject: [PATCH 23/31] Put generated tests in more readable form. --- packages/geojson-utils/geojson-utils.tests.js | 61 +++++++++++++------ 1 file changed, 41 insertions(+), 20 deletions(-) diff --git a/packages/geojson-utils/geojson-utils.tests.js b/packages/geojson-utils/geojson-utils.tests.js index 5aae2f7d4f..73cc5d1554 100644 --- a/packages/geojson-utils/geojson-utils.tests.js +++ b/packages/geojson-utils/geojson-utils.tests.js @@ -57,25 +57,46 @@ Tinytest.add("geojson-utils - points distance generated tests", function (test) var floatEqual = function (a, b) { test.isTrue(Math.abs(a - b) < 0.000001); }; - floatEqual(GeoJSON.pointDistance( {"type":"Point","coordinates":[-19.416501816827804,-13.442164216190577]} , {"type":"Point","coordinates":[8.694866622798145,-8.511979941977188]} ), 3115066.2536578891 ); -floatEqual(GeoJSON.pointDistance( {"type":"Point","coordinates":[151.2841189110186,-56.14564002258703]} , {"type":"Point","coordinates":[167.77983197313733,0.05544793023727834]} ), 6423493.2321747802 ); -floatEqual(GeoJSON.pointDistance( {"type":"Point","coordinates":[100.28413630579598,-88.02313695591874]} , {"type":"Point","coordinates":[36.48786173714325,53.44207073468715]} ), 15848950.0402601473 ); -floatEqual(GeoJSON.pointDistance( {"type":"Point","coordinates":[-70.34899035631679,76.51596869179048]} , {"type":"Point","coordinates":[154.91605914011598,-73.60970971290953]} ), 18714226.5425080135 ); -floatEqual(GeoJSON.pointDistance( {"type":"Point","coordinates":[96.28682994353585,58.77288202662021]} , {"type":"Point","coordinates":[-118.33936230326071,72.07877089688554]} ), 5223022.7731127860 ); -floatEqual(GeoJSON.pointDistance( {"type":"Point","coordinates":[140.35530551429838,10.507104953983799]} , {"type":"Point","coordinates":[-67.73368513956666,38.075836981181055]} ), 13874476.3135112207 ); -floatEqual(GeoJSON.pointDistance( {"type":"Point","coordinates":[69.55582775664516,86.25450283149257]} , {"type":"Point","coordinates":[-18.446230484172702,6.116170521359891]} ), 9314403.3309389465 ); -floatEqual(GeoJSON.pointDistance( {"type":"Point","coordinates":[163.83647522330284,-65.7211532376241]} , {"type":"Point","coordinates":[-159.2198902608361,-78.42975475382991]} ), 1831929.5917785936 ); -floatEqual(GeoJSON.pointDistance( {"type":"Point","coordinates":[-178.9383797585033,-54.87420454365201]} , {"type":"Point","coordinates":[-175.35227065649815,-84.04084282391705]} ), 3244710.9344544266 ); -floatEqual(GeoJSON.pointDistance( {"type":"Point","coordinates":[-48.63219943456352,11.284161149058491]} , {"type":"Point","coordinates":[-179.12627786491066,-51.95622375886887]} ), 13691492.4666933995 ); -floatEqual(GeoJSON.pointDistance( {"type":"Point","coordinates":[140.29684206470847,-67.20720696030185]} , {"type":"Point","coordinates":[-109.37452355003916,36.03131077555008]} ), 14525055.6462231465 ); -floatEqual(GeoJSON.pointDistance( {"type":"Point","coordinates":[-154.6698773431126,58.322094617411494]} , {"type":"Point","coordinates":[61.18583445576951,-4.3424885796848685]} ), 13261602.4336371962 ); -floatEqual(GeoJSON.pointDistance( {"type":"Point","coordinates":[122.5562841903884,10.43972848681733]} , {"type":"Point","coordinates":[-11.756078708684072,-43.86124441982247]} ), 14275427.5511620939 ); -floatEqual(GeoJSON.pointDistance( {"type":"Point","coordinates":[-67.91648306301795,-86.38826347864233]} , {"type":"Point","coordinates":[163.577536230674,12.987319261068478]} ), 11699799.3615680672 ); -floatEqual(GeoJSON.pointDistance( {"type":"Point","coordinates":[91.65140007715672,17.595150742679834]} , {"type":"Point","coordinates":[135.80393003183417,22.307532118167728]} ), 4628773.1129429890 ); -floatEqual(GeoJSON.pointDistance( {"type":"Point","coordinates":[-112.70280818711035,34.45729674655013]} , {"type":"Point","coordinates":[-127.42168210959062,-25.51327457977459]} ), 6846704.0253010122 ); -floatEqual(GeoJSON.pointDistance( {"type":"Point","coordinates":[-161.55807900894433,-77.40711871231906]} , {"type":"Point","coordinates":[-92.66313794790767,-89.12077954714186]} ), 1368055.9401701286 ); -floatEqual(GeoJSON.pointDistance( {"type":"Point","coordinates":[39.966264681424946,9.890176948625594]} , {"type":"Point","coordinates":[-159.88646019320004,40.60383598925546]} ), 14041503.0409814864 ); -floatEqual(GeoJSON.pointDistance( {"type":"Point","coordinates":[-57.48232689569704,86.64061016729102]} , {"type":"Point","coordinates":[59.53941993578337,-75.73194969259202]} ), 18560499.7346975356 ); -floatEqual(GeoJSON.pointDistance( {"type":"Point","coordinates":[-142.0938081513159,80.76813141163439]} , {"type":"Point","coordinates":[14.891517050098628,64.56322408467531]} ), 3793112.6186894816 ); + + // Pairs of points we will be looking a distance between + var tests = [[[-19.416501816827804,-13.442164216190577], [8.694866622798145,-8.511979941977188]], + [[151.2841189110186,-56.14564002258703], [167.77983197313733,0.05544793023727834]], + [[100.28413630579598,-88.02313695591874], [36.48786173714325,53.44207073468715]], + [[-70.34899035631679,76.51596869179048], [154.91605914011598,-73.60970971290953]], + [[96.28682994353585,58.77288202662021], [-118.33936230326071,72.07877089688554]], + [[140.35530551429838,10.507104953983799], [-67.73368513956666,38.075836981181055]], + [[69.55582775664516,86.25450283149257], [-18.446230484172702,6.116170521359891]], + [[163.83647522330284,-65.7211532376241], [-159.2198902608361,-78.42975475382991]], + [[-178.9383797585033,-54.87420454365201], [-175.35227065649815,-84.04084282391705]], + [[-48.63219943456352,11.284161149058491], [-179.12627786491066,-51.95622375886887]], + [[140.29684206470847,-67.20720696030185], [-109.37452355003916,36.03131077555008]], + [[-154.6698773431126,58.322094617411494], [61.18583445576951,-4.3424885796848685]], + [[122.5562841903884,10.43972848681733], [-11.756078708684072,-43.86124441982247]], + [[-67.91648306301795,-86.38826347864233], [163.577536230674,12.987319261068478]], + [[91.65140007715672,17.595150742679834], [135.80393003183417,22.307532118167728]], + [[-112.70280818711035,34.45729674655013], [-127.42168210959062,-25.51327457977459]], + [[-161.55807900894433,-77.40711871231906], [-92.66313794790767,-89.12077954714186]], + [[39.966264681424946,9.890176948625594], [-159.88646019320004,40.60383598925546]], + [[-57.48232689569704,86.64061016729102], [59.53941993578337,-75.73194969259202]], + [[-142.0938081513159,80.76813141163439], [14.891517050098628,64.56322408467531]]]; + + // correct distance between two points + var answers = [3115066.2536578891, 6423493.2321747802, 15848950.0402601473, + 18714226.5425080135, 5223022.7731127860, 13874476.3135112207, + 9314403.3309389465, 1831929.5917785936, 3244710.9344544266, + 13691492.4666933995, 14525055.6462231465, 13261602.4336371962, + 14275427.5511620939, 11699799.3615680672, 4628773.1129429890, + 6846704.0253010122, 1368055.9401701286, 14041503.0409814864, + 18560499.7346975356, 3793112.6186894816]; + + _.each(tests, function (pair, testN) { + var distance = GeoJSON.pointDistance.apply(this, _.map(pair, toGeoJSONPoint)); + test.isTrue(Math.abs(distance - answers[testN]) < 0.00000001, + "Wrong distance between points " + JSON.stringify(pair) + ": " + distance); + }); + + function toGeoJSONPoint (coordinates) { + return { type: "Point", coordinates: coordinates }; + } }); From 1e103b8e46bb9022492ec0d600ca05bdfa2d6027 Mon Sep 17 00:00:00 2001 From: Slava Kim Date: Wed, 2 Oct 2013 18:37:38 -0700 Subject: [PATCH 24/31] Modify test data so the result order is obvious. --- packages/minimongo/minimongo_tests.js | 13 +++++++------ 1 file changed, 7 insertions(+), 6 deletions(-) diff --git a/packages/minimongo/minimongo_tests.js b/packages/minimongo/minimongo_tests.js index d6e11b61b4..3f74b4fd83 100644 --- a/packages/minimongo/minimongo_tests.js +++ b/packages/minimongo/minimongo_tests.js @@ -2288,13 +2288,14 @@ Tinytest.add("minimongo - $near operator tests", function (test) { // GeoJSON tests coll = new LocalCollection(); - var data = [{ "category" : "LARCENY/THEFT", "descript" : "GRAND THEFT OF PROPERTY", "address" : "900 Block of MINNA ST", "location" : { "type" : "Point", "coordinates" : [ -122.41538270191, 37.774683628213 ] }, "_id" : "eRHmRvNtHbCjNw6ox" }, - { "category" : "LARCENY/THEFT", "descript" : "PETTY THEFT FROM LOCKED AUTO", "address" : "900 Block of MINNA ST", "location" : { "type" : "Point", "coordinates" : [ -122.415386041221, 37.7747879744156 ] }, "_id" : "fc9KktqwLcfKaKBDr" }, - { "category" : "OTHER OFFENSES", "descript" : "POSSESSION OF BURGLARY TOOLS", "address" : "900 Block of MINNA ST", "location" : { "type" : "Point", "coordinates" : [ -122.415386041221, 37.7747879744156 ] }, "_id" : "fr2gw6x8agCXwcKkK" }, - { "category" : "WEAPON LAWS", "descript" : "POSS OF PROHIBITED WEAPON", "address" : "900 Block of MINNA ST", "location" : { "type" : "Point", "coordinates" : [ -122.415386041221, 37.7747879744156 ] }, "_id" : "nn3oaDPoeGC5JwqsS" }, - { "category" : "BURGLARY", "descript" : "BURGLARY OF STORE, FORCIBLE ENTRY", "address" : "100 Block of 10TH ST", "location" : { "type" : "Point", "coordinates" : [ -122.415449723856, 37.7749518087273 ] }, "_id" : "qm2F7wnB5D6yjKBNn" }]; + var data = [{ "category" : "BURGLARY", "descript" : "BURGLARY OF STORE, FORCIBLE ENTRY", "address" : "100 Block of 10TH ST", "location" : { "type" : "Point", "coordinates" : [ -122.415449723856, 37.7749518087273 ] } }, + { "category" : "WEAPON LAWS", "descript" : "POSS OF PROHIBITED WEAPON", "address" : "900 Block of MINNA ST", "location" : { "type" : "Point", "coordinates" : [ -122.415386041221, 37.7747879744156 ] } }, + { "category" : "LARCENY/THEFT", "descript" : "GRAND THEFT OF PROPERTY", "address" : "900 Block of MINNA ST", "location" : { "type" : "Point", "coordinates" : [ -122.41538270191, 37.774683628213 ] } }, + { "category" : "LARCENY/THEFT", "descript" : "PETTY THEFT FROM LOCKED AUTO", "address" : "900 Block of MINNA ST", "location" : { "type" : "Point", "coordinates" : [ -122.415396041221, 37.7747879744156 ] } }, + { "category" : "OTHER OFFENSES", "descript" : "POSSESSION OF BURGLARY TOOLS", "address" : "900 Block of MINNA ST", "location" : { "type" : "Point", "coordinates" : [ -122.415386041221, 37.7747879734156 ] } } + ]; - _.each(data, function (x) { coll.insert(x); }); + _.each(data, function (x, i) { coll.insert(_.extend(x, { x: i })); }); var close15 = coll.find({ location: { $near: { $geometry: { type: "Point", From b58c85f323953567d9c88ecf57f984ee86f8344a Mon Sep 17 00:00:00 2001 From: Slava Kim Date: Wed, 2 Oct 2013 19:19:51 -0700 Subject: [PATCH 25/31] Glasser's comments --- packages/minimongo/selector.js | 15 ++++++++------- 1 file changed, 8 insertions(+), 7 deletions(-) diff --git a/packages/minimongo/selector.js b/packages/minimongo/selector.js index 3fd13896d0..dd116f22c5 100644 --- a/packages/minimongo/selector.js +++ b/packages/minimongo/selector.js @@ -328,9 +328,7 @@ var VALUE_OPERATORS = { // the second one to y no matter what user passes. // In case user passes { lon: x, lat: y } returns [x, y] function pointToArray (point) { - var npoint = []; - _.each(point, function (xy) { npoint.push(xy); }); - return npoint; + return _.map(point, _.identity); } // GeoJSON query is marked as $geometry property var mode = _.isObject(operand) && _.has(operand, '$geometry') ? "2dsphere" : "2d"; @@ -344,7 +342,7 @@ var VALUE_OPERATORS = { break; case "2dsphere": if (!_.has(Package, 'geojson-utils')) - throw new Error('Need geojson-utils package for GeoJSON calculations'); + throw new Error('Need geojson-utils package for GeoJSON calculations (such as $near)'); var GeoJSON = Package['geojson-utils'].GeoJSON; // XXX: for now, we don't calculate the actual distance between, say, // polygon and circle. If people care about this use-case it will get @@ -358,9 +356,11 @@ var VALUE_OPERATORS = { } // Used later in sorting by distance, since $near queries are sorted by // distance from closest to farthest. - if (!cursor._distance) - cursor._distance = {}; - cursor._distance[doc._id] = dist; + if (cursor) { + if (!cursor._distance) + cursor._distance = {}; + cursor._distance[doc._id] = dist; + } // Distance couldn't parse a geometry object if (dist === null) @@ -650,6 +650,7 @@ var compileDocumentSelector = function (docSelector, cursor) { return function (doc, wholeDoc) { + // If called w/o wholeDoc, doc is considered the original by default if (wholeDoc === undefined) wholeDoc = doc; return _.all(perKeySelectors, function (f) { From cd500b036cb46f7d44f54b91750d249da51287b3 Mon Sep 17 00:00:00 2001 From: Slava Kim Date: Thu, 3 Oct 2013 13:43:13 -0700 Subject: [PATCH 26/31] Dissallow $near to be inside $or/$and/$nor/$not --- packages/minimongo/minimongo.js | 16 ++++++++++++++++ 1 file changed, 16 insertions(+) diff --git a/packages/minimongo/minimongo.js b/packages/minimongo/minimongo.js index ee5cbacd45..1329a12006 100644 --- a/packages/minimongo/minimongo.js +++ b/packages/minimongo/minimongo.js @@ -90,6 +90,11 @@ LocalCollection.Cursor = function (collection, selector, options) { self.selector_f = LocalCollection._compileSelector(selector, self); self.sort_f = undefined; } else { + // MongoDB throws different errors on different branching operators + // containing $near + if (isGeoQuerySpecial(selector)) + throw new Error("$near can't be inside $or/$and/$nor/$not"); + self.selector_id = undefined; self.selector_f = LocalCollection._compileSelector(selector, self); self.sort_f = (isGeoQuery(selector) || options.sort) ? @@ -1177,9 +1182,20 @@ LocalCollection._compileProjection = function (fields) { }; }; +// Searches $near operator in the selector recursively +// (including all $or/$and/$nor/$not branches) var isGeoQuery = function (selector) { return _.any(selector, function (val, key) { return key === "$near" || (_.isObject(val) && isGeoQuery(val)); }); }; +// Checks if $near appears under some $or/$and/$nor/$not branch +var isGeoQuerySpecial = function (selector) { + return _.any(selector, function (val, key) { + if (_.contains(['$or', '$and', '$nor', '$not'], key)) + return isGeoQuery(val); + return _.isObject(val) && isGeoQuerySpecial(val); + }); +}; + From 43aa4de53002905f20963bdce6ffe42956869e69 Mon Sep 17 00:00:00 2001 From: Slava Kim Date: Thu, 3 Oct 2013 14:11:29 -0700 Subject: [PATCH 27/31] Tests for minimongo's restriction of $near to be inside $or/$and/$nor/$not --- packages/minimongo/minimongo_tests.js | 29 +++++++++++++++++++++++++++ 1 file changed, 29 insertions(+) diff --git a/packages/minimongo/minimongo_tests.js b/packages/minimongo/minimongo_tests.js index 3f74b4fd83..c8a5a13e72 100644 --- a/packages/minimongo/minimongo_tests.js +++ b/packages/minimongo/minimongo_tests.js @@ -2313,5 +2313,34 @@ Tinytest.add("minimongo - $near operator tests", function (test) { test.equal(close20[1].descript, "PETTY THEFT FROM LOCKED AUTO"); test.equal(close20[2].descript, "POSSESSION OF BURGLARY TOOLS"); test.equal(close20[3].descript, "POSS OF PROHIBITED WEAPON"); + + // Any combinations of $near with $or/$and/$nor/$not should throw an error + test.throws(function () { + coll.find({ location: { + $not: { + $near: { + $geometry: { + type: "Point", + coordinates: [-122.4154282, 37.7746115] + }, $maxDistance: 20 } } } }); + }); + test.throws(function () { + coll.find({ + $and: [ { location: { $near: { $geometry: { type: "Point", coordinates: [-122.4154282, 37.7746115] }, $maxDistance: 20 }}}, + { x: 0 }] + }); + }); + test.throws(function () { + coll.find({ + $or: [ { location: { $near: { $geometry: { type: "Point", coordinates: [-122.4154282, 37.7746115] }, $maxDistance: 20 }}}, + { x: 0 }] + }); + }); + test.throws(function () { + coll.find({ + $nor: [ { location: { $near: { $geometry: { type: "Point", coordinates: [-122.4154282, 37.7746115] }, $maxDistance: 1 }}}, + { x: 0 }] + }); + }); }); From 3b26a51296bb563571ffd19cc17897a032d64c14 Mon Sep 17 00:00:00 2001 From: Slava Kim Date: Thu, 3 Oct 2013 14:13:51 -0700 Subject: [PATCH 28/31] Move url to summary --- packages/geojson-utils/package.js | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/packages/geojson-utils/package.js b/packages/geojson-utils/package.js index 9cc423db0b..6a4d7be14b 100644 --- a/packages/geojson-utils/package.js +++ b/packages/geojson-utils/package.js @@ -1,6 +1,5 @@ Package.describe({ - summary: 'GeoJSON utility functions', - url: 'https://github.com/maxogden/geojson-js-utils', + summary: 'GeoJSON utility functions (from https://github.com/maxogden/geojson-js-utils)', internal: true }); From 7c9122ef09cadeca9b14783454a0bed796b44ee5 Mon Sep 17 00:00:00 2001 From: Slava Kim Date: Mon, 7 Oct 2013 14:30:54 -0700 Subject: [PATCH 29/31] Make geojson-utils a strong dependency of minimongo. --- packages/minimongo/package.js | 4 ++-- packages/minimongo/selector.js | 2 -- 2 files changed, 2 insertions(+), 4 deletions(-) diff --git a/packages/minimongo/package.js b/packages/minimongo/package.js index 2c881346e3..dec89d4788 100644 --- a/packages/minimongo/package.js +++ b/packages/minimongo/package.js @@ -7,8 +7,8 @@ Package.on_use(function (api) { api.export('LocalCollection'); api.use(['underscore', 'json', 'ejson', 'ordered-dict', 'deps', 'random', 'ordered-dict']); - // If you really want geolocation queries to work, add this package - api.use('geojson-utils', { weak: true }); + // This package is used for geo-location queries such as $near + api.use('geojson-utils'); api.add_files([ 'minimongo.js', 'selector.js', diff --git a/packages/minimongo/selector.js b/packages/minimongo/selector.js index dd116f22c5..38416cf1af 100644 --- a/packages/minimongo/selector.js +++ b/packages/minimongo/selector.js @@ -341,8 +341,6 @@ var VALUE_OPERATORS = { dist = distanceCoordinatePairs(point, value); break; case "2dsphere": - if (!_.has(Package, 'geojson-utils')) - throw new Error('Need geojson-utils package for GeoJSON calculations (such as $near)'); var GeoJSON = Package['geojson-utils'].GeoJSON; // XXX: for now, we don't calculate the actual distance between, say, // polygon and circle. If people care about this use-case it will get From d1518f4f17924f7760f2a8ae53d584b0fee32559 Mon Sep 17 00:00:00 2001 From: Slava Kim Date: Mon, 7 Oct 2013 14:45:00 -0700 Subject: [PATCH 30/31] Package wrapper for geojson-utils package to keep original code unchanged. --- packages/geojson-utils/geojson-utils.js | 3 --- packages/geojson-utils/package.js | 5 +++-- packages/geojson-utils/post.js | 4 ++++ packages/geojson-utils/pre.js | 4 ++++ 4 files changed, 11 insertions(+), 5 deletions(-) create mode 100644 packages/geojson-utils/post.js create mode 100644 packages/geojson-utils/pre.js diff --git a/packages/geojson-utils/geojson-utils.js b/packages/geojson-utils/geojson-utils.js index 28ce187606..82d8068263 100644 --- a/packages/geojson-utils/geojson-utils.js +++ b/packages/geojson-utils/geojson-utils.js @@ -6,9 +6,6 @@ module.exports = gju; } - // Export for Meteor uni-package - GeoJSON = gju; - // adapted from http://www.kevlindev.com/gui/math/intersection/Intersection.js gju.lineStringsIntersect = function (l1, l2) { var intersects = []; diff --git a/packages/geojson-utils/package.js b/packages/geojson-utils/package.js index 6a4d7be14b..a82fb5e62d 100644 --- a/packages/geojson-utils/package.js +++ b/packages/geojson-utils/package.js @@ -5,11 +5,12 @@ Package.describe({ Package.on_use(function (api) { api.export('GeoJSON'); - api.add_files(['geojson-utils.js']); + api.add_files(['pre.js', 'geojson-utils.js', 'post.js']); }); Package.on_test(function (api) { api.use('tinytest'); - api.add_files(['geojson-utils.js', 'geojson-utils.tests.js'], 'client'); + api.use('geojson-utils'); + api.add_files(['geojson-utils.tests.js'], 'client'); }); diff --git a/packages/geojson-utils/post.js b/packages/geojson-utils/post.js new file mode 100644 index 0000000000..7ac8bff75e --- /dev/null +++ b/packages/geojson-utils/post.js @@ -0,0 +1,4 @@ +// This exports object was created in pre.js. Now copy the `exports` object +// from it into the package-scope variable `GeoJSON`, which will get exported. +GeoJSON = module.exports; + diff --git a/packages/geojson-utils/pre.js b/packages/geojson-utils/pre.js new file mode 100644 index 0000000000..99ad3ca65d --- /dev/null +++ b/packages/geojson-utils/pre.js @@ -0,0 +1,4 @@ +// Define an object named exports. This will cause geojson-utils.js to put `gju` +// as a field on it, instead of in the global namespace. See also post.js. +module = {exports:{}}; + From 24b3ca13e1c0bbb4ad1e7acc9bf2e7194d9f25a7 Mon Sep 17 00:00:00 2001 From: Slava Kim Date: Mon, 7 Oct 2013 16:19:45 -0700 Subject: [PATCH 31/31] Another test for minimongo $near inside $and clause It throws no matter how deep is $near. --- packages/minimongo/minimongo.js | 2 ++ packages/minimongo/minimongo_tests.js | 17 +++++++++++++++++ packages/minimongo/selector.js | 1 - 3 files changed, 19 insertions(+), 1 deletion(-) diff --git a/packages/minimongo/minimongo.js b/packages/minimongo/minimongo.js index 1329a12006..b355ccb9b4 100644 --- a/packages/minimongo/minimongo.js +++ b/packages/minimongo/minimongo.js @@ -1186,6 +1186,7 @@ LocalCollection._compileProjection = function (fields) { // (including all $or/$and/$nor/$not branches) var isGeoQuery = function (selector) { return _.any(selector, function (val, key) { + // Note: _.isObject matches objects and arrays return key === "$near" || (_.isObject(val) && isGeoQuery(val)); }); }; @@ -1195,6 +1196,7 @@ var isGeoQuerySpecial = function (selector) { return _.any(selector, function (val, key) { if (_.contains(['$or', '$and', '$nor', '$not'], key)) return isGeoQuery(val); + // Note: _.isObject matches objects and arrays return _.isObject(val) && isGeoQuerySpecial(val); }); }; diff --git a/packages/minimongo/minimongo_tests.js b/packages/minimongo/minimongo_tests.js index c8a5a13e72..34df9dba0b 100644 --- a/packages/minimongo/minimongo_tests.js +++ b/packages/minimongo/minimongo_tests.js @@ -2342,5 +2342,22 @@ Tinytest.add("minimongo - $near operator tests", function (test) { { x: 0 }] }); }); + test.throws(function () { + coll.find({ + $and: [{ + $and: [{ + location: { + $near: { + $geometry: { + type: "Point", + coordinates: [-122.4154282, 37.7746115] + }, + $maxDistance: 1 + } + } + }] + }] + }); + }); }); diff --git a/packages/minimongo/selector.js b/packages/minimongo/selector.js index 38416cf1af..3a1e5394a8 100644 --- a/packages/minimongo/selector.js +++ b/packages/minimongo/selector.js @@ -341,7 +341,6 @@ var VALUE_OPERATORS = { dist = distanceCoordinatePairs(point, value); break; case "2dsphere": - var GeoJSON = Package['geojson-utils'].GeoJSON; // XXX: for now, we don't calculate the actual distance between, say, // polygon and circle. If people care about this use-case it will get // a priority.