Browse Source

Merge pull request #5 from hkrishna/reverse

Search + Reverse
pull/11/head
Harish Krishna 10 years ago
parent
commit
462e5a2399
  1. 15
      app.js
  2. 2
      controller/search.js
  3. 65
      middleware/search.js
  4. 31
      query/reverse.js
  5. 43
      query/search.js
  6. 26
      sanitiser/_input.js
  7. 47
      sanitiser/_latlonzoom.js
  8. 40
      sanitiser/_layers.js
  9. 17
      sanitiser/_sanitize.js
  10. 27
      sanitiser/_size.js
  11. 23
      sanitiser/reverse.js
  12. 84
      sanitiser/sanitise.js
  13. 26
      sanitiser/suggest.js
  14. 14
      test/ciao/reverse/success.coffee
  15. 52
      test/unit/query/reverse.js
  16. 58
      test/unit/query/search.js
  17. 3
      test/unit/run.js
  18. 15
      test/unit/sanitiser/sanitise.js

15
app.js

@ -8,10 +8,15 @@ app.use( require('./middleware/headers') );
app.use( require('./middleware/cors') );
app.use( require('./middleware/jsonp') );
var middlewares = {};
middlewares.search = require('./middleware/search');
/** ----------------------- sanitisers ----------------------- **/
var sanitisers = {};
sanitisers.sanitiser = require('./sanitiser/sanitise');
sanitisers.suggest = require('./sanitiser/suggest');
sanitisers.search = sanitisers.suggest;
sanitisers.reverse = require('./sanitiser/reverse');
/** ----------------------- controllers ----------------------- **/
@ -26,10 +31,14 @@ controllers.search = require('./controller/search');
app.get( '/', controllers.index() );
// suggest API
app.get( '/suggest', sanitisers.sanitiser.middleware, controllers.suggest() );
app.get( '/suggest', sanitisers.suggest.middleware, controllers.suggest() );
// search API
app.get( '/search', sanitisers.sanitiser.middleware, controllers.search() );
app.get( '/search', sanitisers.search.middleware, middlewares.search.middleware, controllers.search() );
// reverse API
app.get( '/reverse', sanitisers.reverse.middleware, controllers.search(undefined, require('query/reverse')) );
/** ----------------------- error middleware ----------------------- **/

2
controller/search.js

@ -39,4 +39,4 @@ function setup( backend, query ){
return controller;
}
module.exports = setup;
module.exports = setup;

65
middleware/search.js

@ -0,0 +1,65 @@
function deg2rad(degrees) {
return Math.PI*degrees/180;
}
function rad2deg(radians) {
return 180.0*radians/Math.PI;
}
// Semi-axes of WGS-84 geoidal reference
var WGS84_a = 6378137.0; // Major semiaxis [m]
var WGS84_b = 6356752.3; // Minor semiaxis [m]
// Earth radius at a given latitude, according to the WGS-84 ellipsoid [m]
function WGS84EarthRadius(lat){
// http://en.wikipedia.org/wiki/Earth_radius
var An = WGS84_a*WGS84_a * Math.cos(lat);
var Bn = WGS84_b*WGS84_b * Math.sin(lat);
var Ad = WGS84_a * Math.cos(lat);
var Bd = WGS84_b * Math.sin(lat);
return Math.sqrt( (An*An + Bn*Bn)/(Ad*Ad + Bd*Bd) );
}
// Bounding box surrounding the point at given coordinates,
// assuming local approximation of Earth surface as a sphere
// of radius given by WGS84
function boundingBox(latitudeInDegrees, longitudeInDegrees, halfSideInKm) {
var lat = deg2rad(latitudeInDegrees);
var lon = deg2rad(longitudeInDegrees);
var halfSide = 1000000*halfSideInKm;
// Radius of Earth at given latitude
var radius = WGS84EarthRadius(lat);
// Radius of the parallel at given latitude
var pradius = radius*Math.cos(lat);
var latMin = lat - halfSide/radius;
var latMax = lat + halfSide/radius;
var lonMin = lon - halfSide/pradius;
var lonMax = lon + halfSide/pradius;
return {
'top_left': {
'lat': rad2deg(latMin),
'lon': rad2deg(lonMin)
},
'bottom_right': {
'lat': rad2deg(latMax),
'lon': rad2deg(lonMax)
}
};
}
// middleware
function middleware(req, res, next){
req.clean = req.clean || {};
// ideally, bbox should be part of the req (and not to be calculated)
// TBD
req.clean.bbox = boundingBox(req.query.lat, req.query.lon, 2000);
next();
}
// middleware
module.exports.middleware = middleware

31
query/reverse.js

@ -0,0 +1,31 @@
var logger = require('../src/logger');
// Build pelias search query
function generate( params ){
var cmd = {
"query":{
"filtered" : {
"query" : {
"match_all" : {}
},
"filter" : {
"geo_distance" : {
"distance" : "1km",
"center_point" : {
"lat": params.lat,
"lon": params.lon
}
}
}
}
},
"size": 1
};
// logger.log( 'cmd', JSON.stringify( cmd, null, 2 ) );
return cmd;
}
module.exports = generate;

43
query/search.js

@ -5,23 +5,36 @@ function generate( params ){
var cmd = {
"query":{
"filtered" : {
"query" : {
"match" : {
"name.default": params.input
}
},
"filter" : {
"geo_distance" : {
"distance" : "200km",
"center_point" : {
"lat": params.lat,
"lon": params.lon
}
"query_string" : {
"query": params.input,
"fields": ['name.default'],
"default_operator": 'OR'
}
},
"filter": {
"geo_bounding_box": {
"center_point": {
"top_left": {
"lat": params.bbox.top_left.lat,
"lon": params.bbox.top_left.lon
},
"bottom_right": {
"lat": params.bbox.bottom_right.lat,
"lon": params.bbox.bottom_right.lon
}
}
}
}
},
},
"sort" : [{
"_geo_distance" : {
"center_point" : {
"lat": params.lat,
"lon": params.lon
},
"order": 'asc',
"unit": 'km'
}
}],
"size": params.size
};

26
sanitiser/_input.js

@ -0,0 +1,26 @@
// validate inputs, convert types and apply defaults
function sanitize( req ){
req.clean = req.clean || {};
var params= req.query;
// ensure the input params are a valid object
if( Object.prototype.toString.call( params ) !== '[object Object]' ){
params = {};
}
// input text
if('string' !== typeof params.input || !params.input.length){
return {
'error': true,
'message': 'invalid param \'input\': text length, must be >0'
};
}
req.clean.input = params.input;
return { 'error': false };
}
// export function
module.exports = sanitize;

47
sanitiser/_latlonzoom.js

@ -0,0 +1,47 @@
// validate inputs, convert types and apply defaults
function sanitize( req ){
var clean = req.clean || {};
var params= req.query;
// ensure the input params are a valid object
if( Object.prototype.toString.call( params ) !== '[object Object]' ){
params = {};
}
// lat
var lat = parseFloat( params.lat, 10 );
if( isNaN( lat ) || lat < 0 || lat > 90 ){
return {
'error': true,
'message': 'invalid param \'lat\': must be >0 and <90'
}
}
clean.lat = lat;
// lon
var lon = parseFloat( params.lon, 10 );
if( isNaN( lon ) || lon < -180 || lon > 180 ){
return {
'error': true,
'message': 'invalid param \'lon\': must be >-180 and <180'
}
}
clean.lon = lon;
// zoom level
var zoom = parseInt( params.zoom, 10 );
if( !isNaN( zoom ) ){
clean.zoom = Math.min( Math.max( zoom, 1 ), 18 ); // max
} else {
clean.zoom = 10; // default
}
req.clean = clean;
return { 'error': false };
}
// export function
module.exports = sanitize;

40
sanitiser/_layers.js

@ -0,0 +1,40 @@
var indeces = require('../query/indeces');
// validate inputs, convert types and apply defaults
function sanitize( req ){
var clean = req.clean || {};
var params= req.query;
// ensure the input params are a valid object
if( Object.prototype.toString.call( params ) !== '[object Object]' ){
params = {};
}
// which layers to query
if('string' === typeof params.layers && params.layers.length){
var layers = params.layers.split(',').map( function( layer ){
return layer.toLowerCase(); // lowercase inputs
});
for( var x=0; x<layers.length; x++ ){
if( -1 === indeces.indexOf( layers[x] ) ){
return {
'error': true,
'message': 'invalid param \'layer\': must be one or more of ' + indeces.join(',')
}
}
}
clean.layers = layers;
}
else {
clean.layers = indeces; // default (all layers)
}
req.clean = clean;
return { 'error': false };
}
// export function
module.exports = sanitize;

17
sanitiser/_sanitize.js

@ -0,0 +1,17 @@
function sanitize( req, sanitiser, cb ){
req.clean = req.clean || {};
for (var s in sanitiser) {
var sanity = sanitiser[s](req);
if (sanity.error) {
return cb(sanity.message);
}
}
return cb( undefined, req.clean );
}
// export function
module.exports = sanitize;

27
sanitiser/_size.js

@ -0,0 +1,27 @@
// validate inputs, convert types and apply defaults
function sanitize( req ){
var clean = req.clean || {};
var params= req.query;
// ensure the input params are a valid object
if( Object.prototype.toString.call( params ) !== '[object Object]' ){
params = {};
}
// total results
var size = parseInt( params.size, 10 );
if( !isNaN( size ) ){
clean.size = Math.min( Math.max( size, 1 ), 40 ); // max
} else {
clean.size = 10; // default
}
req.clean = clean;
return {'error':false};
}
// export function
module.exports = sanitize;

23
sanitiser/reverse.js

@ -0,0 +1,23 @@
var logger = require('../src/logger'),
_sanitize = require('../sanitiser/_sanitize'),
sanitiser = {
latlonzoom: require('../sanitiser/_latlonzoom')
};
var sanitize = function(req, cb) { _sanitize(req, sanitiser, cb); }
// export sanitize for testing
module.exports.sanitize = sanitize;
// middleware
module.exports.middleware = function( req, res, next ){
sanitize( req, function( err, clean ){
if( err ){
res.status(400); // 400 Bad Request
return next(err);
}
req.clean = clean;
next();
});
};

84
sanitiser/sanitise.js

@ -1,84 +0,0 @@
var logger = require('../src/logger'),
indeces = require('../query/indeces');
// validate inputs, convert types and apply defaults
function sanitize( params, cb ){
var clean = {};
// ensure the input params are a valid object
if( Object.prototype.toString.call( params ) !== '[object Object]' ){
params = {};
}
// input text
if('string' !== typeof params.input || !params.input.length){
return cb( 'invalid param \'input\': text length, must be >0' );
}
clean.input = params.input;
// total results
var size = parseInt( params.size, 10 );
if( !isNaN( size ) ){
clean.size = Math.min( Math.max( size, 1 ), 40 ); // max
} else {
clean.size = 10; // default
}
// which layers to query
if('string' === typeof params.layers && params.layers.length){
var layers = params.layers.split(',').map( function( layer ){
return layer.toLowerCase(); // lowercase inputs
});
for( var x=0; x<layers.length; x++ ){
if( -1 === indeces.indexOf( layers[x] ) ){
return cb( 'invalid param \'layer\': must be one or more of ' + indeces.join(',') );
}
}
clean.layers = layers;
}
else {
clean.layers = indeces; // default (all layers)
}
// lat
var lat = parseFloat( params.lat, 10 );
if( isNaN( lat ) || lat < 0 || lat > 90 ){
return cb( 'invalid param \'lat\': must be >0 and <90' );
}
clean.lat = lat;
// lon
var lon = parseFloat( params.lon, 10 );
if( isNaN( lon ) || lon < -180 || lon > 180 ){
return cb( 'invalid param \'lon\': must be >-180 and <180' );
}
clean.lon = lon;
// zoom level
var zoom = parseInt( params.zoom, 10 );
if( !isNaN( zoom ) ){
clean.zoom = Math.min( Math.max( zoom, 1 ), 18 ); // max
} else {
clean.zoom = 10; // default
}
return cb( undefined, clean );
}
// export function
module.exports = sanitize;
// middleware
module.exports.middleware = function( req, res, next ){
sanitize( req.query, function( err, clean ){
if( err ){
res.status(400); // 400 Bad Request
return next(err);
}
req.clean = clean;
next();
});
};

26
sanitiser/suggest.js

@ -0,0 +1,26 @@
var logger = require('../src/logger'),
_sanitize = require('../sanitiser/_sanitize'),
sanitizers = {
input: require('../sanitiser/_input'),
size: require('../sanitiser/_size'),
layers: require('../sanitiser/_layers'),
latlonzoom: require('../sanitiser/_latlonzoom')
};
var sanitize = function(req, cb) { _sanitize(req, sanitizers, cb); }
// export sanitize for testing
module.exports.sanitize = sanitize;
// middleware
module.exports.middleware = function( req, res, next ){
sanitize( req, function( err, clean ){
if( err ){
res.status(400); // 400 Bad Request
return next(err);
}
req.clean = clean;
next();
});
};

14
test/ciao/reverse/success.coffee

@ -0,0 +1,14 @@
#> valid reverse query
path: '/reverse?lat=29.49136&lon=-82.50622'
#? 200 ok
response.statusCode.should.equal 200
#? valid response
now = new Date().getTime()
should.exist json
should.not.exist json.error
should.exist json.date
json.date.should.be.within now-2000, now+2000
should.exist json.body
json.body.should.be.instanceof Array

52
test/unit/query/reverse.js

@ -0,0 +1,52 @@
var generate = require('../../../query/reverse');
module.exports.tests = {};
module.exports.tests.interface = function(test, common) {
test('valid interface', function(t) {
t.equal(typeof generate, 'function', 'valid function');
t.end();
});
};
module.exports.tests.query = function(test, common) {
test('valid query', function(t) {
var query = generate({
lat: 29.49136, lon: -82.50622
});
var expected = {
query:{
filtered : {
query : {
match_all : {}
},
filter : {
geo_distance : {
distance : '1km',
center_point : {
lat: 29.49136,
lon: -82.50622
}
}
}
}
},
size: 1
};
t.deepEqual(query, expected, 'valid reverse query');
t.end();
});
};
module.exports.all = function (tape, common) {
function test(name, testFunction) {
return tape('reverse query ' + name, testFunction);
}
for( var testCase in module.exports.tests ){
module.exports.tests[testCase](test, common);
}
};

58
test/unit/query/search.js

@ -14,30 +14,54 @@ module.exports.tests.query = function(test, common) {
test('valid query', function(t) {
var query = generate({
input: 'test', size: 10,
lat: 0, lon: 0,
lat: 29.49136, lon: -82.50622,
bbox: {
top_left: {
lat: 11.51053655297385,
lon: -103.16362455862279
},
bottom_right: {
lat: 47.472183447026154,
lon: -61.84881544137721
}
},
layers: ['test']
});
var expected = {
query: {
filtered : {
query : {
match : {
"name.default": 'test'
}
},
filter : {
geo_distance : {
distance : '200km',
center_point : {
lat: 0,
lon: 0
}
query:{
query_string : {
query: 'test',
fields: ['name.default'],
default_operator: 'OR'
}
},
filter: {
geo_bounding_box: {
center_point: {
top_left: {
lat: 11.51053655297385,
lon: -103.16362455862279
},
bottom_right: {
lat: 47.472183447026154,
lon: -61.84881544137721
}
}
}
}
},
},
sort : [{
_geo_distance : {
center_point : {
lat: 29.49136,
lon: -82.50622
},
order: 'asc',
unit: 'km'
}
}],
size: 10
};
t.deepEqual(query, expected, 'valid search query');
t.end();
});

3
test/unit/run.js

@ -9,7 +9,8 @@ var tests = [
require('./sanitiser/sanitise'),
require('./query/indeces'),
require('./query/suggest'),
require('./query/search')
require('./query/search'),
require('./query/reverse')
];
tests.map(function(t) {

15
test/unit/sanitiser/sanitise.js

@ -1,7 +1,10 @@
var sanitize = require('../../../sanitiser/sanitise'),
var suggest = require('../../../sanitiser/suggest'),
_sanitize = suggest.sanitize,
middleware = suggest.middleware,
defaultError = 'invalid param \'input\': text length, must be >0',
defaultClean = { input: 'test', lat: 0, layers: [ 'geoname', 'osmnode', 'osmway', 'admin0', 'admin1', 'admin2', 'neighborhood' ], lon: 0, size: 10, zoom: 10 };
defaultClean = { input: 'test', lat: 0, layers: [ 'geoname', 'osmnode', 'osmway', 'admin0', 'admin1', 'admin2', 'neighborhood' ], lon: 0, size: 10, zoom: 10 },
sanitize = function(query, cb) { _sanitize({'query':query}, cb); }
module.exports.tests = {};
@ -12,8 +15,8 @@ module.exports.tests.interface = function(test, common) {
t.end();
});
test('middleware interface', function(t) {
t.equal(typeof sanitize.middleware, 'function', 'middleware is a function');
t.equal(sanitize.middleware.length, 3, 'sanitize is valid middleware');
t.equal(typeof middleware, 'function', 'middleware is a function');
t.equal(middleware.length, 3, 'sanitizee has a valid middleware');
t.end();
});
};
@ -175,7 +178,7 @@ module.exports.tests.middleware_failure = function(test, common) {
t.equal(message, defaultError);
t.end();
};
sanitize.middleware( {}, res, next );
middleware( {}, res, next );
});
};
@ -187,7 +190,7 @@ module.exports.tests.middleware_success = function(test, common) {
t.deepEqual(req.clean, defaultClean);
t.end();
};
sanitize.middleware( req, undefined, next );
middleware( req, undefined, next );
});
};

Loading…
Cancel
Save