mirror of https://github.com/pelias/api.git
Severyn Kozak
10 years ago
42 changed files with 1134 additions and 179 deletions
@ -1,2 +1,5 @@
|
||||
node_modules |
||||
*.log |
||||
coverage |
||||
.idea |
||||
*.log |
||||
reports |
@ -0,0 +1,123 @@
|
||||
## /search |
||||
|
||||
Full text search endpoint which queries the elasticsearch doc store, slightly slower than suggest. |
||||
|
||||
#### Required Parameters |
||||
* **input**: query string |
||||
|
||||
#### Optional Parameters |
||||
* **lat**: latitude |
||||
* **lon**: longitude |
||||
* **zoom**: zoom level from which you wish to view the world |
||||
* **size**: number of results requested (defaults to 10) |
||||
* **layers**: datasets you wish to query (defaults to ```poi,admin,address```). |
||||
* valid values are ```poi```, ```admin``` or ```address``` |
||||
* ```poi``` expands internally to ```geoname```, ```osmnode```, ```osmway``` |
||||
* ```admin``` expands to ```admin0```, ```admin1```, ```admin2```, ```neighborhood```, ```locality```, ```local_admin``` |
||||
* ```address``` expands to ```osmaddress```, ```openaddresses``` |
||||
* can also be specific to one particular dataset, for example ```geoname``` |
||||
* **bbox**: the bounding box from which you want all your results to come |
||||
* can be one of the following comma separated string values |
||||
* "southwest_lng,southwest_lat,northeast_lng,northeast_lat" ```L.latLngBounds(southwestLatLng, northeastLatLng).toBBoxString()``` |
||||
* bottom left lon, bottom left lat, top right lon, top right lat |
||||
* left, bottom, right, top |
||||
* min longitude, min latitude, max longitude, max latitude |
||||
* **details**: indicates if results should contain detailed, should be `true` or `false` |
||||
* when false results will only contain `id`, `layer`, and `text` properties |
||||
* when true, all available properties will be included in results |
||||
|
||||
|
||||
## /search/coarse |
||||
|
||||
This is a coarse forward geocoder endpoint which only searches admin dataset layers. |
||||
|
||||
#### Required Parameters |
||||
* **input**: query string |
||||
|
||||
#### Optional Parameters |
||||
* **lat**: latitude |
||||
* **lon**: longitude |
||||
* **zoom**: zoom level from which you wish to view the world |
||||
* **bbox**: the bounding box frome which you want all your results to come |
||||
* **size**: (defaults to 10) |
||||
* **layers**: (defaults to ```admin```) |
||||
* **details**: (defaults to `true`) |
||||
|
||||
|
||||
## /suggest |
||||
|
||||
The autocomplete endpoint, it offers fast response time. Mixes results from around the provided lat/lon and also from precision level 1 and 3. |
||||
|
||||
#### Required Parameters |
||||
* **input**: query string |
||||
* **lat**: latitude |
||||
* **lon**: longitude |
||||
* lat/lon are **required** currently because of this [open issue](https://github.com/elasticsearch/elasticsearch/issues/6444) |
||||
|
||||
#### Optional Parameters |
||||
* **zoom**: zoom level from which you wish to view the world |
||||
* **size**: number of results requested (defaults to 10) |
||||
* **layers**: datasets you wish to query (defaults to ```poi,admin,address```) |
||||
* **details**: (defaults to `true`) |
||||
|
||||
|
||||
## /suggest/coarse |
||||
|
||||
Only queries the admin layers. |
||||
|
||||
#### Required Parameters |
||||
* **input**: query string |
||||
* **lat**: latitude from where you are searching |
||||
* **lon**: longitude |
||||
* lat/lon are **required** currently because of this [open issue](https://github.com/elasticsearch/elasticsearch/issues/6444) |
||||
|
||||
#### Optional Parameters |
||||
* **zoom**: zoom level from which you wish to view the world |
||||
* **size**: number of results requested (defaults to 10) |
||||
* **layers**: datasets you wish to query (defaults to ```admin```) |
||||
* **details**: (defaults to `true`) |
||||
|
||||
|
||||
## /suggest/nearby |
||||
|
||||
Works as autocomplete for places located near a latitude/longitude, this endpoint is the same as ```/suggest``` but the results are all from within 50 kilometers of the specified point. Unlike ```/suggest```, ```/suggest/nearby``` does not mix results from different precision levels (500km, 1000km etc from lat/lon). |
||||
|
||||
#### Required Parameters |
||||
* **input**: query string |
||||
* **lat**: latitude |
||||
* **lon**: longitude |
||||
* lat/lon are **required** currently because of this [open issue](https://github.com/elasticsearch/elasticsearch/issues/6444) |
||||
|
||||
#### Optional Parameters |
||||
* **zoom**: zoom level from which you wish to view the world |
||||
* **size**: number of results you need (defaults to 10) |
||||
* **layers**: datasets you wish to query (defaults to ```poi,admin,address```) |
||||
* **details**: (defaults to `true`) |
||||
|
||||
|
||||
## /reverse |
||||
|
||||
Reverse geocoding endpoint. |
||||
|
||||
#### Required Parameters |
||||
* **lat**: latitude |
||||
* **lon**: longitude |
||||
|
||||
#### Optional Parameters |
||||
* **zoom**: zoom level from which you wish to view the world |
||||
* **bbox**: bounding box |
||||
* **layers**: (defaults to ```poi,admin,address```) |
||||
* **details**: (defaults to `true`) |
||||
|
||||
|
||||
## /doc |
||||
|
||||
Retrieves a document or multiple documents at once. |
||||
|
||||
#### Required Parameters |
||||
* one of **id** or **ids** |
||||
* **id**: |
||||
* unique id of the document to be retrieved |
||||
* should be in the form of type:id, for example: ```geoname:4163334``` |
||||
* **ids**: |
||||
* if multiple docs are to be fetched in bulk, an array of ids |
@ -1,22 +1,31 @@
|
||||
|
||||
var pkg = require('../package'); |
||||
var markdown = require('markdown').markdown; |
||||
var fs = require('fs'); |
||||
|
||||
function setup(){ |
||||
|
||||
function controller( req, res, next ){ |
||||
|
||||
// stats
|
||||
var styleString = '<style>html{font-family:monospace}</style>'; |
||||
var text = '# Pelias API\n'; |
||||
text += '### Version: ['+ pkg.version+ '](https://github.com/pelias/api/releases)\n'; |
||||
text += fs.readFileSync( './DOCS.md', 'utf8'); |
||||
var indexHtml = styleString + markdown.toHTML(text); |
||||
|
||||
function controller( req, res, next ) { |
||||
if (req.accepts('html')) { |
||||
res.send(indexHtml); |
||||
return; |
||||
} |
||||
// default behaviour
|
||||
res.json({ |
||||
name: pkg.name, |
||||
version: { |
||||
number: pkg.version |
||||
} |
||||
}); |
||||
|
||||
} |
||||
|
||||
return controller; |
||||
|
||||
} |
||||
|
||||
module.exports = setup; |
||||
module.exports = setup; |
||||
|
@ -0,0 +1,119 @@
|
||||
/** |
||||
* These values specify how much a record that matches a certain category |
||||
* should be boosted in elasticsearch results. |
||||
*/ |
||||
|
||||
module.exports = { |
||||
'transport': 10, |
||||
'transport:air': 20, |
||||
'transport:air:aerodrome': 20, |
||||
'transport:air:airport': 20, |
||||
'recreation': 10, |
||||
'religion': 10, |
||||
'education': 10, |
||||
'entertainment': 10, |
||||
'nightlife': 10, |
||||
'food': 10, |
||||
'government': 10, |
||||
'professional': 10, |
||||
'finance': 10, |
||||
'health': 10, |
||||
'retail': 10, |
||||
'transport:public': 10, |
||||
'transport:bus': 10, |
||||
'transport:taxi': 10, |
||||
'transport:sea': 10, |
||||
'accomodation': 10, |
||||
'transport:station': 10, |
||||
'food:bagel': 10, |
||||
'food:barbecue': 10, |
||||
'food:bougatsa': 10, |
||||
'food:burger': 10, |
||||
'food:cake': 10, |
||||
'food:casserole': 10, |
||||
'food:chicken': 10, |
||||
'food:coffee_shop': 10, |
||||
'food:crepe': 10, |
||||
'food:couscous': 10, |
||||
'food:curry': 10, |
||||
'food:dessert': 10, |
||||
'food:donut': 10, |
||||
'food:empanada': 10, |
||||
'food:fish': 10, |
||||
'food:fish_and_chips': 10, |
||||
'food:fried_food': 10, |
||||
'food:friture': 10, |
||||
'food:gyro': 10, |
||||
'food:ice_cream': 10, |
||||
'food:kebab': 10, |
||||
'food:mediterranean': 10, |
||||
'food:noodle': 10, |
||||
'food:pancake': 10, |
||||
'food:pasta': 10, |
||||
'food:pie': 10, |
||||
'food:pizza': 10, |
||||
'food:regional': 10, |
||||
'food:sandwich': 10, |
||||
'food:sausage': 10, |
||||
'food:savory_pancakes': 10, |
||||
'food:seafood': 10, |
||||
'food:steak': 10, |
||||
'food:sub': 10, |
||||
'food:sushi': 10, |
||||
'food:tapas': 10, |
||||
'food:vegan': 10, |
||||
'food:vegetarian': 10, |
||||
'food:wings': 10, |
||||
'food:cuisine:african': 10, |
||||
'food:cuisine:american': 10, |
||||
'food:cuisine:arab': 10, |
||||
'food:cuisine:argentinian': 10, |
||||
'food:cuisine:asian': 10, |
||||
'food:cuisine:australian': 10, |
||||
'food:cuisine:baiana': 10, |
||||
'food:cuisine:balkan': 10, |
||||
'food:cuisine:basque': 10, |
||||
'food:cuisine:bavarian': 10, |
||||
'food:cuisine:belarusian': 10, |
||||
'food:cuisine:brazilian': 10, |
||||
'food:cuisine:cantonese': 10, |
||||
'food:cuisine:capixaba': 10, |
||||
'food:cuisine:caribbean': 10, |
||||
'food:cuisine:chinese': 10, |
||||
'food:cuisine:croatian': 10, |
||||
'food:cuisine:czech': 10, |
||||
'food:cuisine:danish': 10, |
||||
'food:cuisine:french': 10, |
||||
'food:cuisine:gaucho': 10, |
||||
'food:cuisine:german': 10, |
||||
'food:cuisine:greek': 10, |
||||
'food:cuisine:hunan': 10, |
||||
'food:cuisine:hungarian': 10, |
||||
'food:cuisine:indian': 10, |
||||
'food:cuisine:international': 10, |
||||
'food:cuisine:iranian': 10, |
||||
'food:cuisine:italian': 10, |
||||
'food:cuisine:japanese': 10, |
||||
'food:cuisine:korean': 10, |
||||
'food:cuisine:kyo_ryouri': 10, |
||||
'food:cuisine:latin_american': 10, |
||||
'food:cuisine:lebanese': 10, |
||||
'food:cuisine:malagasy': 10, |
||||
'food:cuisine:mexican': 10, |
||||
'food:cuisine:mineira': 10, |
||||
'food:cuisine:okinawa_ryori': 10, |
||||
'food:cuisine:pakistani': 10, |
||||
'food:cuisine:peruvian': 10, |
||||
'food:cuisine:polish': 10, |
||||
'food:cuisine:portuguese': 10, |
||||
'food:cuisine:rhenish': 10, |
||||
'food:cuisine:russian': 10, |
||||
'food:cuisine:shandong': 10, |
||||
'food:cuisine:sichuan': 10, |
||||
'food:cuisine:spanish': 10, |
||||
'food:cuisine:thai': 10, |
||||
'food:cuisine:turkish': 10, |
||||
'food:cuisine:vietnamese': 10, |
||||
'food:cuisine:westphalian': 10, |
||||
'transport:rail': 10 |
||||
}; |
@ -1,9 +1,12 @@
|
||||
var logger = require( '../src/logger' ); |
||||
|
||||
// handle application errors
|
||||
function middleware(err, req, res, next) { |
||||
logger.error( 'Error:', err ); |
||||
logger.error( 'Stack trace:', err.trace ); |
||||
res.header('Cache-Control','no-cache'); |
||||
if( res.statusCode < 400 ){ res.status(500); } |
||||
res.json({ error: err }); |
||||
res.json({ error: typeof err === 'string' ? err : 'internal server error' }); |
||||
} |
||||
|
||||
module.exports = middleware; |
||||
module.exports = middleware; |
||||
|
@ -0,0 +1,38 @@
|
||||
|
||||
var isObject = require('is-object'); |
||||
|
||||
// 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( !isObject( params ) ){ |
||||
params = {}; |
||||
} |
||||
|
||||
// default case (no categories specified in GET params)
|
||||
if('string' !== typeof params.categories || !params.categories.length){ |
||||
clean.categories = []; |
||||
} |
||||
else { |
||||
// parse GET params
|
||||
clean.categories = params.categories.split(',') |
||||
.map(function (cat) { |
||||
return cat.toLowerCase().trim(); // lowercase inputs
|
||||
}) |
||||
.filter( function( cat ) { |
||||
return ( cat.length > 0 ); |
||||
}); |
||||
} |
||||
|
||||
// pass validated params to next middleware
|
||||
req.clean = clean; |
||||
|
||||
return { 'error': false }; |
||||
|
||||
} |
||||
|
||||
// export function
|
||||
module.exports = sanitize; |
@ -0,0 +1,39 @@
|
||||
var isObject = require('is-object'); |
||||
|
||||
// validate inputs, convert types and apply defaults
|
||||
function sanitize( req, default_value ){ |
||||
|
||||
var clean = req.clean || {}; |
||||
var params= req.query; |
||||
|
||||
if (default_value === undefined) { |
||||
default_value = true; |
||||
} |
||||
|
||||
default_value = !!default_value; |
||||
|
||||
// ensure the input params are a valid object
|
||||
if( !isObject( params ) ){ |
||||
params = {}; |
||||
} |
||||
|
||||
if (params.details !== undefined) { |
||||
var details = params.details; |
||||
|
||||
if (typeof params.details === 'string') { |
||||
details = params.details === 'true'; |
||||
} |
||||
|
||||
clean.details = details === true || details === 1;
|
||||
} else { |
||||
clean.details = default_value; |
||||
} |
||||
|
||||
req.clean = clean; |
||||
|
||||
return {'error':false}; |
||||
|
||||
} |
||||
|
||||
// export function
|
||||
module.exports = sanitize; |
@ -1,96 +1,114 @@
|
||||
var isObject = require('is-object'); |
||||
|
||||
|
||||
// validate inputs, convert types and apply defaults
|
||||
function sanitize( req, latlon_is_required ){ |
||||
|
||||
module.exports = function sanitize( req, latlon_is_required ){ |
||||
|
||||
var clean = req.clean || {}; |
||||
var params= req.query; |
||||
var params = req.query; |
||||
latlon_is_required = latlon_is_required || false; |
||||
|
||||
// ensure the input params are a valid object
|
||||
if( Object.prototype.toString.call( params ) !== '[object Object]' ){ |
||||
if( !isObject( params ) ){ |
||||
params = {}; |
||||
} |
||||
|
||||
var is_invalid_lat = function(lat) { |
||||
return isNaN( lat ) || lat < -90 || lat > 90; |
||||
}; |
||||
|
||||
var is_invalid_lon = function(lon) { |
||||
return isNaN( lon ) || lon < -180 || lon > 180; |
||||
}; |
||||
|
||||
// lat
|
||||
var lat = parseFloat( params.lat, 10 ); |
||||
if (!isNaN(lat)) { |
||||
if( is_invalid_lat(lat) ){ |
||||
return { |
||||
'error': true, |
||||
'message': 'invalid param \'lat\': must be >-90 and <90' |
||||
}; |
||||
} |
||||
clean.lat = lat; |
||||
} else if (latlon_is_required) { |
||||
try { |
||||
sanitize_coord( 'lat', clean, params.lat, latlon_is_required ); |
||||
sanitize_coord( 'lon', clean, params.lon, latlon_is_required ); |
||||
sanitize_zoom_level(clean, params.zoom); |
||||
sanitize_bbox(clean, params.bbox); |
||||
} |
||||
catch (err) { |
||||
return { |
||||
'error': true, |
||||
'message': 'missing param \'lat\': must be >-90 and <90' |
||||
'message': err.message |
||||
}; |
||||
} |
||||
|
||||
// lon
|
||||
var lon = parseFloat( params.lon, 10 ); |
||||
if (!isNaN(lon)) { |
||||
if( is_invalid_lon(lon) ){ |
||||
return { |
||||
'error': true, |
||||
'message': 'invalid param \'lon\': must be >-180 and <180' |
||||
req.clean = clean; |
||||
|
||||
return { 'error': false }; |
||||
}; |
||||
|
||||
|
||||
/** |
||||
* Parse and validate bbox parameter |
||||
* bbox = bottom_left lon, bottom_left lat, top_right lon, top_right lat |
||||
* bbox = left, bottom, right, top |
||||
* bbox = min Longitude, min Latitude, max Longitude, max Latitude |
||||
* |
||||
* @param {object} clean |
||||
* @param {string} param |
||||
*/ |
||||
function sanitize_bbox( clean, param ) { |
||||
if( !param ) { |
||||
return; |
||||
} |
||||
|
||||
var bbox = []; |
||||
var bboxArr = param.split( ',' ); |
||||
|
||||
if( Array.isArray( bboxArr ) && bboxArr.length === 4 ) { |
||||
|
||||
bbox = bboxArr.filter( function( latlon, index ) { |
||||
latlon = parseFloat( latlon, 10 ); |
||||
return !(lat_lon_checks[(index % 2 === 0 ? 'lon' : 'lat')].is_invalid( latlon )); |
||||
}); |
||||
|
||||
if( bbox.length === 4 ) { |
||||
clean.bbox = { |
||||
right: Math.max( bbox[0], bbox[2] ), |
||||
top: Math.max( bbox[1], bbox[3] ), |
||||
left: Math.min( bbox[0], bbox[2] ), |
||||
bottom: Math.min( bbox[1], bbox[3] ) |
||||
}; |
||||
} else { |
||||
throw new Error('invalid bbox'); |
||||
} |
||||
clean.lon = lon; |
||||
} else if (latlon_is_required) { |
||||
return { |
||||
'error': true, |
||||
'message': 'missing param \'lon\': must be >-180 and <180' |
||||
}; |
||||
} |
||||
} |
||||
|
||||
// zoom level
|
||||
var zoom = parseInt( params.zoom, 10 ); |
||||
if( !isNaN( zoom ) ){ |
||||
clean.zoom = Math.min( Math.max( zoom, 1 ), 18 ); // max
|
||||
}
|
||||
|
||||
// bbox
|
||||
// bbox = bottom_left lat, bottom_left lon, top_right lat, top_right lon
|
||||
// bbox = left,bottom,right,top
|
||||
// bbox = min Longitude , min Latitude , max Longitude , max Latitude
|
||||
if (params.bbox) { |
||||
var bbox = []; |
||||
var bboxArr = params.bbox.split(','); |
||||
if( Array.isArray(bboxArr) && bboxArr.length === 4 ) { |
||||
bbox = bboxArr.filter(function(latlon, index) { |
||||
latlon = parseFloat(latlon, 10); |
||||
return !(index % 2 === 0 ? is_invalid_lat(latlon) : is_invalid_lon(latlon));
|
||||
}); |
||||
if (bbox.length === 4) { |
||||
clean.bbox = { |
||||
top : Math.max(bbox[0], bbox[2]), |
||||
right : Math.max(bbox[1], bbox[3]), |
||||
bottom: Math.min(bbox[0], bbox[2]), |
||||
left : Math.min(bbox[1], bbox[3]) |
||||
}; |
||||
} else { |
||||
return { |
||||
'error': true, |
||||
'message': 'invalid bbox' |
||||
}; |
||||
} |
||||
/** |
||||
* Validate lat,lon values |
||||
* |
||||
* @param {string} coord lat|lon |
||||
* @param {object} clean |
||||
* @param {string} param |
||||
* @param {bool} latlon_is_required |
||||
*/ |
||||
function sanitize_coord( coord, clean, param, latlon_is_required ) { |
||||
var value = parseFloat( param, 10 ); |
||||
if ( !isNaN( value ) ) { |
||||
if( lat_lon_checks[coord].is_invalid( value ) ){ |
||||
throw new Error( 'invalid ' + lat_lon_checks[coord].error_msg ); |
||||
} |
||||
}
|
||||
|
||||
req.clean = clean; |
||||
|
||||
return { 'error': false }; |
||||
clean[coord] = value; |
||||
} |
||||
else if (latlon_is_required) { |
||||
throw new Error('missing ' + lat_lon_checks[coord].error_msg); |
||||
} |
||||
} |
||||
|
||||
function sanitize_zoom_level( clean, param ) { |
||||
var zoom = parseInt( param, 10 ); |
||||
if( !isNaN( zoom ) ){ |
||||
clean.zoom = Math.min( Math.max( zoom, 1 ), 18 ); // max
|
||||
} |
||||
} |
||||
|
||||
// export function
|
||||
module.exports = sanitize; |
||||
var lat_lon_checks = { |
||||
lat: { |
||||
is_invalid: function is_invalid_lat(lat) { |
||||
return isNaN( lat ) || lat < -90 || lat > 90; |
||||
}, |
||||
error_msg: 'param \'lat\': must be >-90 and <90' |
||||
}, |
||||
lon: { |
||||
is_invalid: function is_invalid_lon(lon) { |
||||
return isNaN(lon) || lon < -180 || lon > 180; |
||||
}, |
||||
error_msg: 'param \'lon\': must be >-180 and <180' |
||||
} |
||||
}; |
||||
|
||||
|
@ -1,10 +0,0 @@
|
||||
|
||||
#> jsonp |
||||
path: '/?callback=test' |
||||
|
||||
#? content-type header correctly set |
||||
response.should.have.header 'Content-Type','application/javascript; charset=utf-8' |
||||
|
||||
#? should respond with jsonp |
||||
should.exist response.body |
||||
response.body.substr(0,5).should.equal 'test('; |
Loading…
Reference in new issue