From 18775eb6f9c8b1cf626786b69b0557bb3b5a82ea Mon Sep 17 00:00:00 2001 From: Will McClellan Date: Tue, 31 Oct 2017 16:15:15 +1100 Subject: [PATCH] Support for querying within a polygon --- examples/app.js | 1 + examples/test.js | 28 +++++++++++++++++ index.js | 74 ++++++++++++++++++++++++++++++++++++++++++++- test.js | 78 ++++++++++++++++++++++++++++++++++++++++++++++++ 4 files changed, 180 insertions(+), 1 deletion(-) diff --git a/examples/app.js b/examples/app.js index 9f61fe6..98896c9 100644 --- a/examples/app.js +++ b/examples/app.js @@ -15,6 +15,7 @@ const qs = new MongoQS({ custom: { bbox: 'geojson', near: 'geojson', + within: 'geojson', }, }); diff --git a/examples/test.js b/examples/test.js index 1e54a51..5a30f66 100644 --- a/examples/test.js +++ b/examples/test.js @@ -99,6 +99,34 @@ describe('Example App', () => { .end(done); }); + it('returns places inside polygon', (done) => { + const polygon = [ + 5.9490966796875, + 61.025705069868856, + 5.987548828125, + 60.994423108456154, + 6.10565185546875, + 60.98376689595989, + 6.1962890625, + 61.016390262027116, + 6.115264892578125, + 61.04964487995011, + 5.969696044921875, + 61.0516390483921, + 5.9490966796875, + 61.025705069868856, + ].join(','); + + app.get(`${url}?within=${polygon}`) + .expect(200) + .expect((res) => { + assert.equal(res.body.length, 2); + assert.equal(res.body[0].name, 'Solrenningen'); + assert.equal(res.body[1].name, 'Norddalshytten'); + }) + .end(done); + }); + it('returns places with any of the following tags', (done) => { app.get(`${url}?tags[]=Båt&tags[]=Stekeovn`) .expect(200) diff --git a/index.js b/index.js index 0fb7d3b..4ac2d49 100644 --- a/index.js +++ b/index.js @@ -27,6 +27,10 @@ module.exports = function MongoQS(options) { this.custom.near = this.customNear(this.custom.near); } + if (this.custom.within) { + this.custom.within = this.customWithin(this.custom.within); + } + if (this.custom.after) { this.custom.after = this.customAfter(this.custom.after); } @@ -100,6 +104,74 @@ module.exports.prototype.customNear = field => (query, point) => { } }; +module.exports.prototype.customWithin = field => (query, pointList) => { + const pointStrArr = pointList.split(','); + + //need at least four points to make a polygon + if(pointStrArr.length < 8) { + return {}; + } + + //ensure even amount of points + if(pointStrArr.length % 2 !== 0) { + return {}; + } + + //convert every string to a float + var pointNumArr = []; + + try { + pointStrArr.forEach(str => { + var num = parseFloat(str, 10); + + //if float conversion fails from bad input, return empty + if(isNaN(num)) { + throw new Error('Invalid value passed as polygon coordinate') + } + + pointNumArr.push(num); + }) + } catch (err) { + return {} + } + + //build our coordinates array + var coordinates = []; + + try { + for(var i = 0 ; i < pointNumArr.length ; i = i + 2) { + //should never happen from previous check + if(i+1 >= pointNumArr.length) { + throw new Error("Odd number of points given") + } + + coordinates.push([pointNumArr[i], pointNumArr[i+1]]); + } + } catch (err) { + return {} + } + + // check if last coordinate matches first, if not go ahead an add first into list + var lastIndex = coordinates.length-1; + var firstLon = coordinates[0][0] + var firstLat = coordinates[0][1] + var lastLon = coordinates[lastIndex][0] + var lastLat = coordinates[lastIndex][1] + + if (firstLon !== lastLon || firstLat !== lastLat) { + return {} + } + + query[field] = { + $geoWithin: { + $geometry : { + type: 'Polygon', + coordinates: [coordinates] + } + } + } +}; + function parseDate(value) { let date = value; @@ -192,7 +264,7 @@ module.exports.prototype.parseString = function parseString(string, array) { break; default: break; - } + } break; default: ret.org = org = op + org; diff --git a/test.js b/test.js index 23fb26e..416d18a 100644 --- a/test.js +++ b/test.js @@ -43,6 +43,45 @@ describe('customBBOX()', () => { }); }); +describe('customWithin()', () => { + it('does not return $geoWithin query for invalid polygon', () => { + [ + // non matching first last + '1,1,2,2,2,3,1,2,2,2', + // not enough points + '1,1,2,2,2,3', + // odd length + '1,1,2,2,2,3,1', + // invalid value + '1,1,2,2,2,3,1,2,2,a', + ].forEach((polygon) => { + mqs.customWithin('gojson')(query, polygon); + assert.deepEqual(query, {}); + }); + }); + + it('returns $geoWithin query for valid polygon', () => { + const string = '1,1,2,2,2,3,1,2,1,1'; + mqs.customWithin('geojson')(query, string); + assert.deepEqual(query, { + geojson: { + $geoWithin: { + $geometry: { + type: 'Polygon', + coordinates: [[ + [1, 1], + [2, 2], + [2, 3], + [1, 2], + [1, 1], + ]], + }, + }, + }, + }); + }); +}); + describe('customNear()', () => { it('does not return $near query for invalid point', () => { ['0123', '0,'].forEach((bbox) => { @@ -107,6 +146,45 @@ describe('customNear()', () => { }); }); +describe('customWithin()', () => { + it('does not return $geoWithin query for invalid polygon', () => { + [ + // non matching first last + '1,1,2,2,2,3,1,2,2,2', + // not enough points + '1,1,2,2,2,3', + // odd length + '1,1,2,2,2,3,1', + // invalid value + '1,1,2,2,2,3,1,2,2,a', + ].forEach((polygon) => { + mqs.customWithin('gojson')(query, polygon); + assert.deepEqual(query, {}); + }); + }); + + it('returns $geoWithin query for valid polygon', () => { + const string = '1,1,2,2,2,3,1,2,1,1'; + mqs.customWithin('geojson')(query, string); + assert.deepEqual(query, { + geojson: { + $geoWithin: { + $geometry: { + type: 'Polygon', + coordinates: [[ + [1, 1], + [2, 2], + [2, 3], + [1, 2], + [1, 1], + ]], + }, + }, + }, + }); + }); +}); + describe('customAfter()', () => { it('does not return after query for invalid date', () => { ['foo', '2015-13-40'].forEach((date) => {