mirror of
https://github.com/meteor/meteor.git
synced 2026-05-02 03:01:46 -04:00
Merge branch 'minimongo-near' into devel
Conflicts: History.md LICENSE.txt
This commit is contained in:
@@ -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
|
||||
|
||||
@@ -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
1
packages/geojson-utils/.gitignore
vendored
Normal file
@@ -0,0 +1 @@
|
||||
.build*
|
||||
380
packages/geojson-utils/geojson-utils.js
Normal file
380
packages/geojson-utils/geojson-utils.js
Normal 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)]
|
||||
};
|
||||
};
|
||||
|
||||
})();
|
||||
102
packages/geojson-utils/geojson-utils.tests.js
Normal file
102
packages/geojson-utils/geojson-utils.tests.js
Normal 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 };
|
||||
}
|
||||
});
|
||||
|
||||
16
packages/geojson-utils/package.js
Normal file
16
packages/geojson-utils/package.js
Normal 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');
|
||||
});
|
||||
|
||||
4
packages/geojson-utils/post.js
Normal file
4
packages/geojson-utils/post.js
Normal 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;
|
||||
|
||||
4
packages/geojson-utils/pre.js
Normal file
4
packages/geojson-utils/pre.js
Normal 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:{}};
|
||||
|
||||
@@ -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 ##
|
||||
|
||||
@@ -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);
|
||||
});
|
||||
};
|
||||
|
||||
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
}]
|
||||
}]
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
@@ -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',
|
||||
|
||||
@@ -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;
|
||||
};
|
||||
};
|
||||
|
||||
|
||||
Reference in New Issue
Block a user