Merge branch 'minimongo-near' into devel

Conflicts:
	History.md
	LICENSE.txt
This commit is contained in:
Slava Kim
2013-10-07 16:55:10 -07:00
13 changed files with 766 additions and 47 deletions

View File

@@ -30,6 +30,8 @@
* Improve behavior of `$ne`, `$nin`, and `$not` selectors with objects containing
arrays. #1451
* `$near` operator for `2d` and `2dsphere` indices.
#### DDP
* Fix infinite loop if a client disconnects while a long yielding method is

View File

@@ -533,6 +533,11 @@ uid2: https://github.com/coreh/uid2
Copyright (c) 2013 Marco Aurelio
----------
geojson-utils: https://github.com/maxogden/geojson-js-utils
----------
Copyright (c) 2010 Max Ogden
==============
Apache License

1
packages/geojson-utils/.gitignore vendored Normal file
View File

@@ -0,0 +1 @@
.build*

View File

@@ -0,0 +1,380 @@
(function () {
var gju = {};
// Export the geojson object for **CommonJS**
if (typeof module !== 'undefined' && module.exports) {
module.exports = 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[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))
* 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
},
// 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)]
};
};
})();

View File

@@ -0,0 +1,102 @@
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": [-122.260000705719, 37.80919060818706]}
var navalBase = {"type": "Point",
"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);
};
// 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 };
}
});

View File

@@ -0,0 +1,16 @@
Package.describe({
summary: 'GeoJSON utility functions (from https://github.com/maxogden/geojson-js-utils)',
internal: true
});
Package.on_use(function (api) {
api.export('GeoJSON');
api.add_files(['pre.js', 'geojson-utils.js', 'post.js']);
});
Package.on_test(function (api) {
api.use('tinytest');
api.use('geojson-utils');
api.add_files(['geojson-utils.tests.js'], 'client');
});

View File

@@ -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;

View File

@@ -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:{}};

View File

@@ -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 ##

View File

@@ -87,12 +87,18 @@ 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 {
// 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.sort_f = options.sort ? LocalCollection._compileSort(options.sort) : null;
self.selector_f = LocalCollection._compileSelector(selector, self);
self.sort_f = (isGeoQuery(selector) || options.sort) ?
LocalCollection._compileSort(options.sort || [], self) : null;
}
self.skip = options.skip;
self.limit = options.limit;
@@ -488,7 +494,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 +561,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
@@ -1175,3 +1181,23 @@ LocalCollection._compileProjection = function (fields) {
return res;
};
};
// Searches $near operator in the selector recursively
// (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));
});
};
// 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);
// Note: _.isObject matches objects and arrays
return _.isObject(val) && isGeoQuerySpecial(val);
});
};

View File

@@ -2266,3 +2266,98 @@ 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: 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);
}
// GeoJSON tests
coll = new LocalCollection();
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, i) { coll.insert(_.extend(x, { x: i })); });
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");
// 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 }]
});
});
test.throws(function () {
coll.find({
$and: [{
$and: [{
location: {
$near: {
$geometry: {
type: "Point",
coordinates: [-122.4154282, 37.7746115]
},
$maxDistance: 1
}
}
}]
}]
});
});
});

View File

@@ -7,6 +7,8 @@ Package.on_use(function (api) {
api.export('LocalCollection');
api.use(['underscore', 'json', 'ejson', 'ordered-dict', 'deps',
'random', 'ordered-dict']);
// This package is used for geo-location queries such as $near
api.use('geojson-utils');
api.add_files([
'minimongo.js',
'selector.js',
@@ -17,6 +19,7 @@ Package.on_use(function (api) {
});
Package.on_test(function (api) {
api.use('geojson-utils', 'client');
api.use('minimongo', 'client');
api.use('test-helpers', 'client');
api.use(['tinytest', 'underscore', 'ejson', 'ordered-dict',

View File

@@ -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) {
@@ -74,12 +74,13 @@ var compileValueSelector = function (valueSelector) {
_.each(valueSelector, function (operand, operator) {
if (!_.has(VALUE_OPERATORS, operator))
throw new Error("Unrecognized operator: " + operator);
// Special case for location operators
operatorFunctions.push(VALUE_OPERATORS[operator](
operand, valueSelector.$options));
operand, valueSelector, cursor));
});
return function (value) {
return function (value, doc) {
return _.all(operatorFunctions, function (f) {
return f(value);
return f(value, doc);
});
};
}
@@ -95,38 +96,38 @@ var compileValueSelector = function (valueSelector) {
// XXX can factor out common logic below
var LOGICAL_OPERATORS = {
"$and": function(subSelector) {
"$and": function(subSelector, operators, cursor) {
if (!isArray(subSelector) || _.isEmpty(subSelector))
throw Error("$and/$or/$nor must be nonempty array");
var subSelectorFunctions = _.map(
subSelector, compileDocumentSelector);
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);
});
};
},
"$or": function(subSelector) {
"$or": function(subSelector, operators, cursor) {
if (!isArray(subSelector) || _.isEmpty(subSelector))
throw Error("$and/$or/$nor must be nonempty array");
var subSelectorFunctions = _.map(
subSelector, compileDocumentSelector);
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);
});
};
},
"$nor": function(subSelector) {
"$nor": function(subSelector, operators, cursor) {
if (!isArray(subSelector) || _.isEmpty(subSelector))
throw Error("$and/$or/$nor must be nonempty array");
var subSelectorFunctions = _.map(
subSelector, compileDocumentSelector);
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);
});
};
},
@@ -141,6 +142,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))
@@ -212,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);
};
},
@@ -255,7 +263,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
@@ -287,22 +296,80 @@ var VALUE_OPERATORS = {
return function (value) { return true; };
},
"$elemMatch": function (operand) {
var matcher = compileDocumentSelector(operand);
return function (value) {
"$elemMatch": function (operand, selector, cursor) {
var matcher = compileDocumentSelector(operand, cursor);
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);
};
},
"$near": function (operand, operators, cursor) {
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
// the second one to y no matter what user passes.
// In case user passes { lon: x, lat: y } returns [x, y]
function pointToArray (point) {
return _.map(point, _.identity);
}
// 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) {
var dist = null;
switch (mode) {
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;
}
// Used later in sorting by distance, since $near queries are sorted by
// distance from closest to farthest.
if (cursor) {
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;
};
},
"$maxDistance": function () {
// evaluation happens in the $near operator
return function () { return true; }
}
};
@@ -536,7 +603,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) === '$') {
@@ -544,11 +611,13 @@ 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);
perKeySelectors.push(function (doc) {
var valueSelectorFunc =
compileValueSelector(subSelector, docSelector, cursor);
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
@@ -569,15 +638,20 @@ var compileDocumentSelector = function (docSelector) {
(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 called w/o wholeDoc, doc is considered the original by default
if (wholeDoc === undefined)
wholeDoc = doc;
return _.all(perKeySelectors, function (f) {
return f(doc);
return f(doc, wholeDoc);
});
};
};
@@ -585,7 +659,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);};
@@ -608,7 +682,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:
@@ -624,7 +698,7 @@ LocalCollection._compileSelector = function (selector) {
// 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) {
@@ -652,8 +726,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
@@ -705,3 +785,4 @@ LocalCollection._compileSort = function (spec) {
return 0;
};
};