mirror of https://github.com/pelias/api.git
Diana Shkolnikov
9 years ago
128 changed files with 3428 additions and 1862 deletions
@ -1,6 +1,23 @@
|
||||
sudo: false |
||||
language: node_js |
||||
script: "npm run unit" |
||||
node_js: |
||||
- "0.10" |
||||
- "0.12" |
||||
sudo: false |
||||
- 0.10 |
||||
- 0.12 |
||||
- 4.4 |
||||
- 5.8 |
||||
matrix: |
||||
allow_failures: |
||||
- node_js: 4.4 |
||||
- node_js: 5.8 |
||||
env: |
||||
global: |
||||
- CXX=g++-4.8 |
||||
matrix: |
||||
- TEST_SUITE=unit |
||||
script: "npm run $TEST_SUITE" |
||||
addons: |
||||
apt: |
||||
sources: |
||||
- ubuntu-toolchain-r-test |
||||
packages: |
||||
- g++-4.8 |
||||
|
@ -1,9 +0,0 @@
|
||||
|
||||
#### Search: |
||||
|
||||
- Nearest Museums: http://pelias.mapzen.com/search?lat=51.533&lon=-0.0652&input=museum&size=40 |
||||
- Nearest Hotels: http://pelias.mapzen.com/search?lat=51.533&lon=-0.0652&input=hotel&size=40 |
||||
|
||||
#### Autocomplete: |
||||
|
||||
- Local Neighborhoods: http://pelias.mapzen.com/suggest/nearby?lat=40.7259&lon=-73.9806&input=e&layers=neighborhood&size=40 |
@ -0,0 +1,9 @@
|
||||
#!/bin/bash -ex |
||||
|
||||
rm -r docs || true |
||||
|
||||
curl -s http://localhost:3100/v1 > /dev/null || die "Pelias server does not appear to be running \ |
||||
on http://localhost:3100, run npm start in another window before generating docs." |
||||
|
||||
cd test/ciao |
||||
node ../../node_modules/ciao/bin/ciao -c ../ciao.json . -d ../../docs |
@ -1,45 +0,0 @@
|
||||
|
||||
var _ = require('lodash'), |
||||
peliasSchema = require('pelias-schema'), |
||||
peliasLogger = require( 'pelias-logger' ).get( 'api' ); |
||||
|
||||
var ADMIN_FIELDS = [ |
||||
'admin0', |
||||
'admin1', |
||||
'admin1_abbr', |
||||
'admin2', |
||||
'local_admin', |
||||
'locality', |
||||
'neighborhood', |
||||
'address.zip' |
||||
]; |
||||
|
||||
/** |
||||
* Get all admin fields that were expected and also found in schema |
||||
* |
||||
* @param {Object} [schema] optional: for testing only |
||||
* @param {Array} [expectedFields] optional: for testing only |
||||
* @param {Object} [logger] optional: for testing only |
||||
* @returns {Array.<string>} |
||||
*/ |
||||
function getAvailableAdminFields(schema, expectedFields, logger) { |
||||
|
||||
schema = schema || peliasSchema; |
||||
expectedFields = expectedFields || ADMIN_FIELDS; |
||||
logger = logger || peliasLogger; |
||||
|
||||
var actualFields = Object.keys(schema.mappings._default_.properties); |
||||
|
||||
// check if expected fields are actually in current schema
|
||||
var available = expectedFields.filter(function (field) { |
||||
return _.contains( actualFields, field ); |
||||
}); |
||||
|
||||
if (available.length === 0) { |
||||
logger.error('helper/adminFields: no expected admin fields found in schema'); |
||||
} |
||||
|
||||
return available; |
||||
} |
||||
|
||||
module.exports = getAvailableAdminFields; |
@ -1,35 +1,74 @@
|
||||
|
||||
var _ = require('lodash'), |
||||
check = require('check-types'), |
||||
schemas = require('./labelSchema.json'); |
||||
schemas = require('./labelSchema'); |
||||
|
||||
module.exports = function( record ){ |
||||
var schema = getSchema(record.country_a); |
||||
|
||||
var labelParts = [ record.name.default ]; |
||||
var labelParts = getInitialLabel(record); |
||||
|
||||
var schema = schemas.default; |
||||
for (var key in schema) { |
||||
var valueFunction = schema[key]; |
||||
|
||||
if (record.country_a && record.country_a.length && schemas[record.country_a]) { |
||||
schema = schemas[record.country_a]; |
||||
labelParts = valueFunction(record, labelParts); |
||||
} |
||||
|
||||
var buildOutput = function(parts, schemaArr, record) { |
||||
for (var i=0; i<schemaArr.length; i++) { |
||||
var fieldValue = record[schemaArr[i]]; |
||||
if (check.unemptyString(fieldValue) && !_.contains(parts, fieldValue)) { |
||||
parts.push( fieldValue ); |
||||
return parts; |
||||
} |
||||
} |
||||
return parts; |
||||
}; |
||||
// NOTE: while it may seem odd to call `uniq` on the list of label parts,
|
||||
// the effect is quite subtle. Take, for instance, a result for "Lancaster, PA"
|
||||
// the pseudo-object is:
|
||||
// {
|
||||
// 'name': 'Lancaster',
|
||||
// 'locality': 'Lancaster',
|
||||
// 'region_a': 'PA',
|
||||
// 'country_a': 'USA'
|
||||
// }
|
||||
//
|
||||
// the code up to this point generates the label:
|
||||
// `Lancaster, Lancaster, PA, USA`
|
||||
//
|
||||
// then the `unique` call reduces this to:
|
||||
// `Lancaster, PA, USA`
|
||||
//
|
||||
// this code doesn't have the same effect in the case of a venue or address
|
||||
// where the `name` field would contain the address or name of a point-of-interest
|
||||
//
|
||||
// Also see https://github.com/pelias/api/issues/429 for other ways that this is bad
|
||||
//
|
||||
// de-dupe, join, trim
|
||||
return _.uniq( labelParts ).join(', ').trim(); |
||||
|
||||
for (var key in schema) { |
||||
labelParts = buildOutput(labelParts, schema[key], record); |
||||
}; |
||||
|
||||
function getSchema(country_a) { |
||||
if (country_a && country_a.length && schemas[country_a]) { |
||||
return schemas[country_a]; |
||||
} |
||||
|
||||
// de-dupe outputs
|
||||
labelParts = _.unique( labelParts ); |
||||
return schemas.default; |
||||
|
||||
return labelParts.join(', ').trim(); |
||||
}; |
||||
} |
||||
|
||||
// helper function that sets a default label for non-US/CA regions
|
||||
// this is a very special case
|
||||
function getInitialLabel(record) { |
||||
if (isRegion(record.layer) && |
||||
isGeonamesOrWhosOnFirst(record.source) && |
||||
isUSAOrCAN(record.country_a)) { |
||||
return []; |
||||
} |
||||
|
||||
return [record.name]; |
||||
|
||||
} |
||||
|
||||
function isRegion(layer) { |
||||
return 'region' === layer; |
||||
} |
||||
|
||||
function isUSAOrCAN(country_a) { |
||||
return 'USA' === country_a || 'CAN' === country_a; |
||||
} |
||||
|
||||
function isGeonamesOrWhosOnFirst(source) { |
||||
return 'geonames' === source || 'whosonfirst' === source; |
||||
|
||||
} |
||||
|
@ -0,0 +1,66 @@
|
||||
var _ = require('lodash'), |
||||
check = require('check-types'); |
||||
|
||||
module.exports = { |
||||
'USA': { |
||||
'local': getFirstProperty(['localadmin', 'locality', 'neighbourhood', 'county']), |
||||
'regional': getUsState, |
||||
'country': getFirstProperty(['country_a']) |
||||
}, |
||||
'GBR': { |
||||
'local': getFirstProperty(['neighbourhood', 'county', 'localadmin', 'locality', 'macroregion', 'region']), |
||||
'regional': getFirstProperty(['county','country','region']) |
||||
}, |
||||
'SGP': { |
||||
'local': getFirstProperty(['neighbourhood', 'region', 'county', 'localadmin', 'locality']), |
||||
'regional': getFirstProperty(['county','country','region']) |
||||
}, |
||||
'SWE': { |
||||
'local': getFirstProperty(['neighbourhood', 'region', 'county', 'localadmin', 'locality']), |
||||
'regional': getFirstProperty(['country']) |
||||
}, |
||||
'default': { |
||||
'local': getFirstProperty(['localadmin', 'locality', 'neighbourhood', 'county', 'macroregion', 'region']), |
||||
'regional': getFirstProperty(['country']) |
||||
} |
||||
}; |
||||
|
||||
// find the first field of record that has a non-empty value that's not already in labelParts
|
||||
function getFirstProperty(fields) { |
||||
return function(record, labelParts) { |
||||
for (var i = 0; i < fields.length; i++) { |
||||
var fieldValue = record[fields[i]]; |
||||
|
||||
if (check.nonEmptyString(fieldValue) && !_.includes(labelParts, fieldValue)) { |
||||
labelParts.push( fieldValue ); |
||||
return labelParts; |
||||
} |
||||
|
||||
} |
||||
|
||||
return labelParts; |
||||
|
||||
}; |
||||
|
||||
} |
||||
|
||||
// this function is exclusively used for figuring out which field to use for US States
|
||||
// 1. if a US state is the most granular bit of info entered, the label should contain
|
||||
// the full state name, eg: Pennsylvania, USA
|
||||
// 2. otherwise, the state abbreviation should be used, eg: Lancaster, PA, USA
|
||||
// 3. if for some reason the abbreviation isn't available, use the full state name
|
||||
function getUsState(record, labelParts) { |
||||
if ('region' === record.layer && record.region) { |
||||
// add full state name when state is the most granular piece of info
|
||||
labelParts.push(record.region); |
||||
} else if (record.region_a) { |
||||
// otherwise just add the region code when available
|
||||
labelParts.push(record.region_a); |
||||
} else if (record.region) { |
||||
// add the full name when there's no region code available ()
|
||||
labelParts.push(record.region); |
||||
} |
||||
|
||||
return labelParts; |
||||
|
||||
} |
@ -1,22 +0,0 @@
|
||||
{ |
||||
"USA": { |
||||
"local": ["localadmin", "locality", "neighbourhood", "county"], |
||||
"regional": ["region_a", "region", "country"] |
||||
}, |
||||
"GBR": { |
||||
"local": ["neighbourhood", "county", "localadmin", "locality", "region"], |
||||
"regional": ["county","country","region"] |
||||
}, |
||||
"SGP": { |
||||
"local": ["neighbourhood", "region", "county", "localadmin", "locality"], |
||||
"regional": ["county","country","region"] |
||||
}, |
||||
"SWE": { |
||||
"local": ["neighbourhood", "region", "county", "localadmin", "locality"], |
||||
"regional": ["country"] |
||||
}, |
||||
"default": { |
||||
"local": ["localadmin", "locality", "neighbourhood", "county", "region"], |
||||
"regional": ["country"] |
||||
} |
||||
} |
@ -0,0 +1,11 @@
|
||||
module.exports = [ |
||||
'country', |
||||
'macroregion', |
||||
'region', |
||||
'macrocounty', |
||||
'county', |
||||
'localadmin', |
||||
'locality', |
||||
'borough', |
||||
'neighbourhood' |
||||
]; |
@ -1,87 +1,81 @@
|
||||
var extend = require('extend'), |
||||
_ = require('lodash'); |
||||
|
||||
var TYPE_TO_SOURCE = { |
||||
'geoname': 'gn', |
||||
'osmnode': 'osm', |
||||
'osmway': 'osm', |
||||
'admin0': 'qs', |
||||
'admin1': 'qs', |
||||
'admin2': 'qs', |
||||
'neighborhood': 'qs', |
||||
'locality': 'qs', |
||||
'local_admin': 'qs', |
||||
'osmaddress': 'osm', |
||||
'openaddresses': 'oa' |
||||
}; |
||||
function addStandardTargetsToAliases(standard, aliases) { |
||||
var combined = _.extend({}, aliases); |
||||
standard.forEach(function(target) { |
||||
if (combined[target] === undefined) { |
||||
combined[target] = [target]; |
||||
} |
||||
}); |
||||
|
||||
return combined; |
||||
} |
||||
|
||||
/* |
||||
* This doesn't include alias layers such as coarse |
||||
* Sources |
||||
*/ |
||||
var TYPE_TO_LAYER = { |
||||
'geoname': 'venue', |
||||
'osmnode': 'venue', |
||||
'osmway': 'venue', |
||||
'admin0': 'country', |
||||
'admin1': 'region', |
||||
'admin2': 'county', |
||||
'neighborhood': 'neighbourhood', |
||||
'locality': 'locality', |
||||
'local_admin': 'localadmin', |
||||
'osmaddress': 'address', |
||||
'openaddresses': 'address' |
||||
}; |
||||
|
||||
var SOURCE_TO_TYPE = { |
||||
'gn' : ['geoname'], |
||||
'geonames' : ['geoname'], |
||||
'oa' : ['openaddresses'], |
||||
'openaddresses' : ['openaddresses'], |
||||
'qs' : ['admin0', 'admin1', 'admin2', 'neighborhood', 'locality', 'local_admin'], |
||||
'quattroshapes' : ['admin0', 'admin1', 'admin2', 'neighborhood', 'locality', 'local_admin'], |
||||
'osm' : ['osmaddress', 'osmnode', 'osmway'], |
||||
'openstreetmap' : ['osmaddress', 'osmnode', 'osmway'] |
||||
// a list of all sources
|
||||
var SOURCES = ['openstreetmap', 'openaddresses', 'geonames', 'whosonfirst']; |
||||
|
||||
/* |
||||
* A list of alternate names for sources, mostly used to save typing |
||||
*/ |
||||
var SOURCE_ALIASES = { |
||||
'osm': ['openstreetmap'], |
||||
'oa': ['openaddresses'], |
||||
'gn': ['geonames'], |
||||
'wof': ['whosonfirst'] |
||||
}; |
||||
|
||||
/** |
||||
* This does not included alias layers, those are built separately |
||||
/* |
||||
* Create an object that contains all sources or aliases. The key is the source or alias, |
||||
* the value is either that source, or the canonical name for that alias if it's an alias. |
||||
*/ |
||||
var LAYER_TO_TYPE = { |
||||
'venue': ['geoname','osmnode','osmway'], |
||||
'address': ['osmaddress','openaddresses'], |
||||
'country': ['admin0'], |
||||
'region': ['admin1'], |
||||
'county': ['admin2'], |
||||
'locality': ['locality'], |
||||
'localadmin': ['local_admin'], |
||||
'neighbourhood': ['neighborhood'] |
||||
var SOURCE_MAPPING = addStandardTargetsToAliases(SOURCES, SOURCE_ALIASES); |
||||
|
||||
/* |
||||
* Layers |
||||
*/ |
||||
|
||||
/* |
||||
* A list of all layers in each source. This is used for convenience elswhere |
||||
* and to determine when a combination of source and layer parameters is |
||||
* not going to match any records and will return no results. |
||||
*/ |
||||
var LAYERS_BY_SOURCE = { |
||||
openstreetmap: [ 'address', 'venue' ], |
||||
openaddresses: [ 'address' ], |
||||
geonames: [ 'country', 'region', 'county', 'locality', 'venue' ], |
||||
whosonfirst: [ 'continent', 'macrocountry', 'country', 'dependency', 'region', |
||||
'locality', 'localadmin', 'county', 'macrohood', 'neighbourhood', 'microhood', 'disputed'] |
||||
}; |
||||
|
||||
/* |
||||
* A list of layer aliases that can be used to support specific use cases |
||||
* (like coarse geocoding) * or work around the fact that different sources |
||||
* may have layers that mean the same thing but have a different name |
||||
*/ |
||||
var LAYER_ALIASES = { |
||||
'coarse': ['admin0','admin1','admin2','neighborhood','locality','local_admin'] |
||||
'coarse': LAYERS_BY_SOURCE.whosonfirst |
||||
}; |
||||
|
||||
var LAYER_WITH_ALIASES_TO_TYPE = extend({}, LAYER_ALIASES, LAYER_TO_TYPE); |
||||
// create a list of all layers by combining each entry from LAYERS_BY_SOURCE
|
||||
var LAYERS = _.uniq(Object.keys(LAYERS_BY_SOURCE).reduce(function(acc, key) { |
||||
return acc.concat(LAYERS_BY_SOURCE[key]); |
||||
}, [])); |
||||
|
||||
/* |
||||
* derive the list of types, sources, and layers from above mappings |
||||
* Create the an object that has a key for each possible layer or alias, |
||||
* and returns either that layer, or all the layers in the alias |
||||
*/ |
||||
var TYPES = Object.keys(TYPE_TO_SOURCE); |
||||
var SOURCES = Object.keys(SOURCE_TO_TYPE); |
||||
var LAYERS = Object.keys(LAYER_TO_TYPE); |
||||
|
||||
var sourceAndLayerToType = function sourceAndLayerToType(source, layer) { |
||||
return _.intersection(SOURCE_TO_TYPE[source], LAYER_WITH_ALIASES_TO_TYPE[layer]); |
||||
}; |
||||
var LAYER_MAPPING = addStandardTargetsToAliases(LAYERS, LAYER_ALIASES); |
||||
|
||||
module.exports = { |
||||
types: TYPES, |
||||
sources: SOURCES, |
||||
layers: LAYERS, |
||||
type_to_source: TYPE_TO_SOURCE, |
||||
type_to_layer: TYPE_TO_LAYER, |
||||
source_to_type: SOURCE_TO_TYPE, |
||||
layer_to_type: LAYER_TO_TYPE, |
||||
layer_with_aliases_to_type: LAYER_WITH_ALIASES_TO_TYPE, |
||||
source_and_layer_to_type: sourceAndLayerToType |
||||
source_mapping: SOURCE_MAPPING, |
||||
layer_mapping: LAYER_MAPPING, |
||||
layers_by_source: LAYERS_BY_SOURCE |
||||
}; |
||||
|
@ -1,43 +0,0 @@
|
||||
var type_mapping = require( '../helper/type_mapping' ); |
||||
var _ = require('lodash'); |
||||
|
||||
/** |
||||
* Different parts of the code express "preferences" for which Elasticsearch types are going to be searched |
||||
* This method decides how to combine all the preferences. |
||||
* |
||||
* @param {Array} clean_types |
||||
* @returns {Array} |
||||
*/ |
||||
module.exports = function calculate_types(clean_types) { |
||||
//Check that at least one preference of types is defined
|
||||
if (!clean_types || !(clean_types.from_layers || clean_types.from_sources || clean_types.from_text_parser)) { |
||||
throw new Error('clean_types should not be null or undefined'); |
||||
} |
||||
|
||||
/* the layers and source parameters are cumulative: |
||||
* perform a set intersection of their specified types |
||||
*/ |
||||
if (clean_types.from_layers || clean_types.from_sources) { |
||||
var types = type_mapping.types; |
||||
|
||||
if (clean_types.from_layers) { |
||||
types = _.intersection(types, clean_types.from_layers); |
||||
} |
||||
|
||||
if (clean_types.from_sources) { |
||||
types = _.intersection(types, clean_types.from_sources); |
||||
} |
||||
|
||||
return types; |
||||
} |
||||
|
||||
/* |
||||
* Type restrictions requested by the address parser should only be used |
||||
* if both the source and layers parameters are empty, so do this last |
||||
*/ |
||||
if (clean_types.from_text_parser) { |
||||
return clean_types.from_text_parser; |
||||
} |
||||
|
||||
throw new Error('no types specified'); |
||||
}; |
@ -1,46 +0,0 @@
|
||||
var types_helper = require( '../helper/types' ); |
||||
|
||||
/** |
||||
* Validate the types specified to be searched. |
||||
* |
||||
* Elasticsearch interprets an empty array of types as "search anything" rather |
||||
* than "search nothing", so in the case of an empty array, return an error |
||||
* message instead of searching at all. |
||||
*/ |
||||
function middleware(req, res, next) { |
||||
req.clean = req.clean || {}; |
||||
|
||||
if (req.clean.hasOwnProperty('types')) { |
||||
|
||||
try { |
||||
var types = types_helper(req.clean.types); |
||||
|
||||
if ((types instanceof Array) && types.length === 0) { |
||||
var err = 'You have specified both the `sources` and `layers` ' + |
||||
'parameters in a combination that will return no results.'; |
||||
req.errors.push( err ); |
||||
} |
||||
|
||||
else { |
||||
req.clean.type = types; |
||||
} |
||||
|
||||
} |
||||
|
||||
// @todo: refactor this flow, it is confusing as `types_helper()` can throw
|
||||
// with an error "clean_types should not be null or undefined" which is
|
||||
// not returned to the user yet the return value CAN trigger a user error.
|
||||
// I would have liked to throw for BOTH cases and then handle the users errors
|
||||
// inside the 'catch' but this is not possible.
|
||||
// also: why are we deleting things from $clean?
|
||||
catch (err) { |
||||
// this means there were no types specified
|
||||
delete req.clean.types; |
||||
} |
||||
|
||||
} |
||||
|
||||
next(); |
||||
} |
||||
|
||||
module.exports = middleware; |
@ -0,0 +1,56 @@
|
||||
var logger = require('pelias-logger').get('api'); |
||||
var Document = require('pelias-model').Document; |
||||
|
||||
var placeTypes = require('../helper/placeTypes'); |
||||
|
||||
/** |
||||
* Convert WOF integer ids to Pelias formatted ids that can be used by the /place endpoint. |
||||
* This should probably be moved to the import pipeline once we are happy with the way this works. |
||||
*/ |
||||
|
||||
function setup() { |
||||
return function (req, res, next) { |
||||
// do nothing if no result data set
|
||||
if (!res || !res.data) { |
||||
return next(); |
||||
} |
||||
|
||||
res.data = res.data.map(normalizeParentIds); |
||||
|
||||
next(); |
||||
}; |
||||
} |
||||
|
||||
/** |
||||
* Update all parent ids in the admin hierarchy |
||||
* |
||||
* @param {object} place |
||||
* @return {object} |
||||
*/ |
||||
function normalizeParentIds(place) { |
||||
|
||||
if (place) { |
||||
placeTypes.forEach(function (placeType) { |
||||
if (place[placeType] && place[placeType].length > 0 && place[placeType][0]) { |
||||
place[placeType + '_gid'] = [ makeNewId(placeType, place[placeType + '_gid']) ]; |
||||
} |
||||
}); |
||||
} |
||||
|
||||
return place; |
||||
} |
||||
|
||||
/** |
||||
* Generate a valid Pelias ids from placetype and WOF id. |
||||
* Assumes all of the incoming ids are WOF ids. |
||||
* |
||||
* @param {string} placeType |
||||
* @param {number} id |
||||
* @return {string} |
||||
*/ |
||||
function makeNewId(placeType, id) { |
||||
var doc = new Document('whosonfirst', placeType, id); |
||||
return doc.getGid(); |
||||
} |
||||
|
||||
module.exports = setup; |
@ -0,0 +1,38 @@
|
||||
var logger = require('pelias-logger').get('api'); |
||||
|
||||
/** |
||||
* Parses the bounding box property in docs, if one is found |
||||
*/ |
||||
|
||||
function setup() { |
||||
return function (req, res, next) { |
||||
// do nothing if no result data set
|
||||
if (!res || !res.data) { |
||||
return next(); |
||||
} |
||||
|
||||
res.data = res.data.map(parseBBox); |
||||
|
||||
next(); |
||||
}; |
||||
} |
||||
|
||||
/* |
||||
* Parse the bbox property and form an object |
||||
*/ |
||||
function parseBBox(place) { |
||||
|
||||
if (place && place.bounding_box) { |
||||
try { |
||||
place.bounding_box = JSON.parse(place.bounding_box); |
||||
} |
||||
catch (err) { |
||||
logger.error('Invalid bounding_box json string:', place); |
||||
delete place.bounding_box; |
||||
} |
||||
} |
||||
|
||||
return place; |
||||
} |
||||
|
||||
module.exports = setup; |
@ -1,66 +1,49 @@
|
||||
var extend = require('extend'); |
||||
var _ = require('lodash'); |
||||
|
||||
/** |
||||
- P is a preferred English name |
||||
- Q is a preferred name (in other languages) |
||||
- V is a well-known (but unofficial) variant for the place |
||||
(e.g. "New York City" for New York) |
||||
- S is either a synonym or a colloquial name for the place |
||||
(e.g. "Big Apple" for New York), or a version of the name which |
||||
is stripped of accent characters. |
||||
- A is an abbreviation or code for the place (e.g. "NYC" for New |
||||
York) |
||||
*/ |
||||
// config mapping of old names to new ones
|
||||
var NAME_MAP = { |
||||
var PARENT_PROPS = require('../helper/placeTypes'); |
||||
|
||||
var ADDRESS_PROPS = { |
||||
'number': 'housenumber', |
||||
'zip': 'postalcode', |
||||
'alpha3': 'country_a', |
||||
'admin0': 'country', |
||||
'admin1': 'region', |
||||
'admin1_abbr': 'region_a', |
||||
'admin2': 'county', |
||||
'local_admin': 'localadmin', |
||||
'neighborhood': 'neighbourhood' |
||||
'street': 'street' |
||||
}; |
||||
|
||||
function setup() { |
||||
|
||||
function setup() { |
||||
return renamePlacenames; |
||||
} |
||||
|
||||
function renamePlacenames(req, res, next) { |
||||
|
||||
// do nothing if no result data set
|
||||
if (!res || !res.data) { |
||||
return next(); |
||||
} |
||||
|
||||
// loop through data items and remap placenames
|
||||
res.data = res.data.map(renameProperties); |
||||
res.data = res.data.map(renameOneRecord); |
||||
|
||||
next(); |
||||
} |
||||
|
||||
function renameProperties(place) { |
||||
var newPlace = {}; |
||||
Object.keys(place).forEach(function (property) { |
||||
if (property === 'address') { |
||||
extend(newPlace, renameProperties(place[property])); |
||||
} |
||||
else { |
||||
renameProperty(place, newPlace, property); |
||||
} |
||||
}); |
||||
return newPlace; |
||||
} |
||||
/* |
||||
* Rename the fields in one record |
||||
*/ |
||||
function renameOneRecord(place) { |
||||
if (place.address_parts) { |
||||
Object.keys(ADDRESS_PROPS).forEach(function (prop) { |
||||
place[ADDRESS_PROPS[prop]] = place.address_parts[prop]; |
||||
}); |
||||
} |
||||
|
||||
function renameProperty(oldObj, newObj, property) { |
||||
if (!oldObj.hasOwnProperty(property)) { |
||||
return; |
||||
// merge the parent block into the top level object to flatten the structure
|
||||
if (place.parent) { |
||||
PARENT_PROPS.forEach(function (prop) { |
||||
place[prop] = place.parent[prop]; |
||||
place[prop + '_a'] = place.parent[prop + '_a']; |
||||
place[prop + '_gid'] = place.parent[prop + '_id']; |
||||
}); |
||||
} |
||||
|
||||
newObj[(NAME_MAP[property] || property)] = oldObj[property]; |
||||
return place; |
||||
} |
||||
|
||||
module.exports = setup; |
||||
|
@ -0,0 +1,41 @@
|
||||
var _ = require('lodash'); |
||||
|
||||
/** |
||||
In the process of phasing out the 'quattroshapes' source in favour of 'whosonfirst' |
||||
we will emit a warning to users so they can begin upgrading their clients. |
||||
|
||||
In the interim we will automatically rewrite all requests for quattroshapes to whosonfirst. |
||||
|
||||
@todo: this is only temporary |
||||
@see: https://github.com/pelias/api/issues/442
|
||||
**/ |
||||
|
||||
function sanitize( raw, clean, opts ) { |
||||
// error & warning messages
|
||||
var messages = { errors: [], warnings: [] }; |
||||
|
||||
// only applicably when 'sources' param is privided
|
||||
if( raw.hasOwnProperty('sources') ){ |
||||
|
||||
var sources = raw.sources.split(','); |
||||
if (_.includes(sources, 'quattroshapes') || _.includes(sources, 'qs')) { |
||||
|
||||
// emit a warning message so users can transition.
|
||||
messages.warnings.push('You are using Quattroshapes as a data source in this query. ' + |
||||
'Quattroshapes has been disabled as a data source for Mapzen Search, and has been' + |
||||
'replaced by Who\'s on First, an actively maintained data project based on Quattroshapes' + |
||||
'Your existing queries WILL CONTINUE TO WORK for the foreseeable future, but results will ' + |
||||
'be coming from Who\'s on First and `sources=quattroshapes` will be interpreted as ' + |
||||
'`sources=whosonfirst`. If you have any questions, please email search@mapzen.com.'); |
||||
|
||||
// user requested 'quattroshapes', we will give them 'whosonfirst' instead.
|
||||
sources = _.without(sources, 'quattroshapes', 'qs'); |
||||
sources.push('whosonfirst'); |
||||
raw.sources = sources.join(','); |
||||
} |
||||
} |
||||
|
||||
return messages; |
||||
} |
||||
|
||||
module.exports = sanitize; |
@ -1,46 +0,0 @@
|
||||
|
||||
var _ = require('lodash'), |
||||
check = require('check-types'), |
||||
sources_map = require( '../query/sources' ); |
||||
|
||||
var ALL_SOURCES = Object.keys(sources_map), |
||||
ALL_SOURCES_JOINED = ALL_SOURCES.join(','); |
||||
|
||||
function sanitize( raw, clean ) { |
||||
|
||||
// error & warning messages
|
||||
var messages = { errors: [], warnings: [] }; |
||||
|
||||
// init clean.types (if not already init)
|
||||
clean.types = clean.types || {}; |
||||
|
||||
// default case (no layers specified in GET params)
|
||||
// don't even set the from_layers key in this case
|
||||
if( check.unemptyString( raw.source ) ){ |
||||
|
||||
var sources = raw.source.split(','); |
||||
|
||||
var invalid_sources = sources.filter(function(source) { |
||||
return !_.contains( ALL_SOURCES, source ); |
||||
}); |
||||
|
||||
if( invalid_sources.length > 0 ){ |
||||
invalid_sources.forEach( function( invalid ){ |
||||
messages.errors.push('\'' + invalid + '\' is an invalid source parameter. Valid options: ' + ALL_SOURCES_JOINED); |
||||
}); |
||||
} |
||||
|
||||
else { |
||||
var types = sources.reduce(function(acc, source) { |
||||
return acc.concat(sources_map[source]); |
||||
}, []); |
||||
|
||||
clean.types.from_source = types; |
||||
} |
||||
|
||||
} |
||||
|
||||
return messages; |
||||
} |
||||
|
||||
module.exports = sanitize; |
@ -0,0 +1,37 @@
|
||||
var _ = require( 'lodash' ); |
||||
var type_mapping = require( '../helper/type_mapping' ); |
||||
|
||||
/* |
||||
* This sanitiser depends on clean.layers and clean.sources |
||||
* so it has to be run after those sanitisers have been run |
||||
*/ |
||||
function sanitize( raw, clean ){ |
||||
var messages = { errors: [], warnings: [] }; |
||||
|
||||
var possible_errors = []; |
||||
var at_least_one_valid_combination = false; |
||||
|
||||
if (clean.layers && clean.sources) { |
||||
clean.sources.forEach(function(source) { |
||||
var layers_for_source = type_mapping.layers_by_source[source]; |
||||
clean.layers.forEach(function(layer) { |
||||
if (_.includes(layers_for_source, layer)) { |
||||
at_least_one_valid_combination = true; |
||||
} else { |
||||
var message = 'You have specified both the `sources` and `layers` ' + |
||||
'parameters in a combination that will return no results: the ' + |
||||
source + ' source has nothing in the ' + layer + ' layer'; |
||||
possible_errors.push(message); |
||||
} |
||||
}); |
||||
}); |
||||
|
||||
if (!at_least_one_valid_combination) { |
||||
messages.errors = possible_errors; |
||||
} |
||||
} |
||||
|
||||
return messages; |
||||
} |
||||
|
||||
module.exports = sanitize; |
@ -1,25 +0,0 @@
|
||||
var _ = require('lodash'); |
||||
|
||||
function setup( paramName, targetMap ) { |
||||
return function( raw, clean ){ |
||||
return sanitize( raw, clean ); |
||||
}; |
||||
} |
||||
|
||||
function sanitize( raw, clean, opts ) { |
||||
// error & warning messages
|
||||
var messages = { errors: [], warnings: [] }; |
||||
|
||||
if (_.includes(raw.sources, 'quattroshapes') || _.includes(raw.sources, 'qs')) { |
||||
messages.warnings.push( 'You are using Quattroshapes as a data source in this query. ' + |
||||
'Quattroshapes will be disabled as a data source for Mapzen Search in the next several ' + |
||||
'weeks, and is being replaced by Who\'s on First, an actively maintained data project ' + |
||||
'based on Quattroshapes. Your existing queries WILL CONTINUE TO WORK for the foreseeable ' + |
||||
'future, but results will be coming from Who\'s on First and `sources=quattroshapes` will ' + |
||||
'be deprecated. If you have any questions, please email search@mapzen.com.'); |
||||
} |
||||
|
||||
return messages; |
||||
} |
||||
|
||||
module.exports = setup; |
@ -0,0 +1,33 @@
|
||||
|
||||
#> layer alias |
||||
path: '/v1/reverse?point.lat=1&point.lon=2&layers=address' |
||||
|
||||
#? 200 ok |
||||
response.statusCode.should.be.equal 200 |
||||
response.should.have.header 'charset', 'utf8' |
||||
response.should.have.header 'content-type', 'application/json; charset=utf-8' |
||||
|
||||
#? valid geocoding block |
||||
should.exist json.geocoding |
||||
should.exist json.geocoding.version |
||||
should.exist json.geocoding.attribution |
||||
should.exist json.geocoding.query |
||||
should.exist json.geocoding.engine |
||||
should.exist json.geocoding.engine.name |
||||
should.exist json.geocoding.engine.author |
||||
should.exist json.geocoding.engine.version |
||||
should.exist json.geocoding.timestamp |
||||
|
||||
#? valid geojson |
||||
json.type.should.be.equal 'FeatureCollection' |
||||
json.features.should.be.instanceof Array |
||||
|
||||
#? expected errors |
||||
should.not.exist json.geocoding.errors |
||||
|
||||
#? expected warnings |
||||
should.not.exist json.geocoding.warnings |
||||
|
||||
#? inputs |
||||
json.geocoding.query['size'].should.eql 10 |
||||
json.geocoding.query.layers.should.eql ["address"] |
@ -0,0 +1,34 @@
|
||||
|
||||
#> quattroshapes is being phased out and so should emit a warning message |
||||
path: '/v1/reverse?point.lat=1&point.lon=2&sources=qs' |
||||
|
||||
#? 200 ok |
||||
response.statusCode.should.be.equal 200 |
||||
response.should.have.header 'charset', 'utf8' |
||||
response.should.have.header 'content-type', 'application/json; charset=utf-8' |
||||
|
||||
#? valid geocoding block |
||||
should.exist json.geocoding |
||||
should.exist json.geocoding.version |
||||
should.exist json.geocoding.attribution |
||||
should.exist json.geocoding.query |
||||
should.exist json.geocoding.engine |
||||
should.exist json.geocoding.engine.name |
||||
should.exist json.geocoding.engine.author |
||||
should.exist json.geocoding.engine.version |
||||
should.exist json.geocoding.timestamp |
||||
|
||||
#? valid geojson |
||||
json.type.should.be.equal 'FeatureCollection' |
||||
json.features.should.be.instanceof Array |
||||
|
||||
#? expected errors |
||||
should.not.exist json.geocoding.errors |
||||
|
||||
#? expected warnings |
||||
should.exist json.geocoding.warnings |
||||
json.geocoding.warnings.should.eql ['You are using Quattroshapes as a data source in this query. Quattroshapes has been disabled as a data source for Mapzen Search, and has beenreplaced by Who\'s on First, an actively maintained data project based on QuattroshapesYour existing queries WILL CONTINUE TO WORK for the foreseeable future, but results will be coming from Who\'s on First and `sources=quattroshapes` will be interpreted as `sources=whosonfirst`. If you have any questions, please email search@mapzen.com.' ] |
||||
|
||||
#? inputs |
||||
json.geocoding.query['size'].should.eql 10 |
||||
json.geocoding.query.sources.should.eql ['whosonfirst'] # should use 'whosonfirst' instead of 'quattroshapes' |
@ -0,0 +1,35 @@
|
||||
|
||||
#> quattroshapes is being phased out and so should emit a warning message |
||||
path: '/v1/search?sources=qs&text=a' |
||||
|
||||
#? 200 ok |
||||
response.statusCode.should.be.equal 200 |
||||
response.should.have.header 'charset', 'utf8' |
||||
response.should.have.header 'content-type', 'application/json; charset=utf-8' |
||||
|
||||
#? valid geocoding block |
||||
should.exist json.geocoding |
||||
should.exist json.geocoding.version |
||||
should.exist json.geocoding.attribution |
||||
should.exist json.geocoding.query |
||||
should.exist json.geocoding.engine |
||||
should.exist json.geocoding.engine.name |
||||
should.exist json.geocoding.engine.author |
||||
should.exist json.geocoding.engine.version |
||||
should.exist json.geocoding.timestamp |
||||
|
||||
#? valid geojson |
||||
json.type.should.be.equal 'FeatureCollection' |
||||
json.features.should.be.instanceof Array |
||||
|
||||
#? expected errors |
||||
should.not.exist json.geocoding.errors |
||||
|
||||
#? expected warnings |
||||
should.exist json.geocoding.warnings |
||||
json.geocoding.warnings.should.eql ['You are using Quattroshapes as a data source in this query. Quattroshapes has been disabled as a data source for Mapzen Search, and has beenreplaced by Who\'s on First, an actively maintained data project based on QuattroshapesYour existing queries WILL CONTINUE TO WORK for the foreseeable future, but results will be coming from Who\'s on First and `sources=quattroshapes` will be interpreted as `sources=whosonfirst`. If you have any questions, please email search@mapzen.com.' ] |
||||
|
||||
#? inputs |
||||
json.geocoding.query['size'].should.eql 10 |
||||
json.geocoding.query['text'].should.eql 'a' |
||||
json.geocoding.query.sources.should.eql ['whosonfirst'] # should use 'whosonfirst' instead of 'quattroshapes' |
@ -0,0 +1,51 @@
|
||||
var vs = require('../../../query/reverse_defaults'); |
||||
|
||||
module.exports = { |
||||
'query': { |
||||
'filtered': { |
||||
'query': { |
||||
'bool': { |
||||
'must': [] |
||||
} |
||||
}, |
||||
'filter': { |
||||
'bool': { |
||||
'must': [ |
||||
{ |
||||
'geo_distance': { |
||||
'distance': '500km', |
||||
'distance_type': 'plane', |
||||
'optimize_bbox': 'indexed', |
||||
'_cache': true, |
||||
'center_point': { |
||||
'lat': 29.49136, |
||||
'lon': -82.50622 |
||||
} |
||||
} |
||||
}, |
||||
{ |
||||
'terms': { |
||||
'source': ['test'] |
||||
} |
||||
} |
||||
] |
||||
} |
||||
} |
||||
} |
||||
}, |
||||
'sort': [ |
||||
'_score', |
||||
{ |
||||
'_geo_distance': { |
||||
'center_point': { |
||||
'lat': 29.49136, |
||||
'lon': -82.50622 |
||||
}, |
||||
'order': 'asc', |
||||
'distance_type': 'plane' |
||||
} |
||||
} |
||||
], |
||||
'size': vs.size, |
||||
'track_scores': true |
||||
}; |
@ -1,84 +0,0 @@
|
||||
var adminFields = require('../../../helper/adminFields'); |
||||
|
||||
module.exports.tests = {}; |
||||
|
||||
module.exports.tests.interface = function(test, common) { |
||||
test('validate fields', function(t) { |
||||
t.assert(adminFields instanceof Function, 'adminFields is a function'); |
||||
t.assert(adminFields() instanceof Array, 'adminFields() returns an array'); |
||||
t.assert(adminFields().length > 0, 'adminFields array is not empty'); |
||||
t.end(); |
||||
}); |
||||
}; |
||||
|
||||
module.exports.tests.lookupExistance = function(test, common) { |
||||
test('all expected fields in schema', function(t) { |
||||
|
||||
var expectedFields = [ |
||||
'one', |
||||
'two', |
||||
'three', |
||||
'four' |
||||
]; |
||||
var schema = { mappings: { _default_: { properties: {} } } }; |
||||
|
||||
// inject all expected fields into schema mock
|
||||
expectedFields.forEach(function (field) { |
||||
schema.mappings._default_.properties[field] = {}; |
||||
}); |
||||
|
||||
var res = adminFields(schema, expectedFields); |
||||
|
||||
t.deepEquals(res, expectedFields, 'all expected fields are returned'); |
||||
t.end(); |
||||
}); |
||||
|
||||
test('some expected fields in schema', function(t) { |
||||
|
||||
var expectedFields = [ |
||||
'one', |
||||
'two', |
||||
'three', |
||||
'four' |
||||
]; |
||||
var schema = { mappings: { _default_: { properties: {} } } }; |
||||
|
||||
// inject only some of the expected fields into schema mock
|
||||
expectedFields.slice(0, 3).forEach(function (field) { |
||||
schema.mappings._default_.properties[field] = {}; |
||||
}); |
||||
|
||||
var res = adminFields(schema, expectedFields); |
||||
|
||||
t.deepEquals(res, expectedFields.slice(0, 3), 'only matching expected fields are returned'); |
||||
t.end(); |
||||
}); |
||||
|
||||
test('no expected fields in schema', function(t) { |
||||
|
||||
var schema = { mappings: { _default_: { properties: { foo: {} } } } }; |
||||
|
||||
var logErrorCalled = false; |
||||
var logger = { |
||||
error: function () { |
||||
logErrorCalled = true; |
||||
}}; |
||||
|
||||
var res = adminFields(schema, undefined, logger); |
||||
|
||||
t.deepEquals(res, [], 'no admin fields found'); |
||||
t.assert(logErrorCalled, 'log error called'); |
||||
t.end(); |
||||
}); |
||||
}; |
||||
|
||||
module.exports.all = function (tape, common) { |
||||
|
||||
function test(name, testFunction) { |
||||
return tape('adminFields: ' + name, testFunction); |
||||
} |
||||
|
||||
for( var testCase in module.exports.tests ){ |
||||
module.exports.tests[testCase](test, common); |
||||
} |
||||
}; |
@ -0,0 +1,87 @@
|
||||
|
||||
var generator = require('../../../helper/labelGenerator'); |
||||
|
||||
module.exports.tests = {}; |
||||
|
||||
module.exports.tests.interface = function(test, common) { |
||||
test('interface', function(t) { |
||||
t.equal(typeof generator, 'function', 'valid function'); |
||||
t.end(); |
||||
}); |
||||
}; |
||||
|
||||
// GBR street address
|
||||
module.exports.tests.one_main_street_uk = function(test, common) { |
||||
test('one main street uk', function(t) { |
||||
var doc = { |
||||
'name': '1 Main St', |
||||
'housenumber': '1', |
||||
'street': 'Main St', |
||||
'postalcode': 'BT77 0BG', |
||||
'country_a': 'GBR', |
||||
'country': 'United Kingdom', |
||||
'region': 'Dungannon' |
||||
}; |
||||
t.equal(generator(doc),'1 Main St, Dungannon, United Kingdom'); |
||||
t.end(); |
||||
}); |
||||
}; |
||||
|
||||
// GBR venue
|
||||
module.exports.tests.hackney_city_farm = function(test, common) { |
||||
test('hackney city farm', function(t) { |
||||
var doc = { |
||||
'name': 'Hackney City Farm', |
||||
'country_a': 'GBR', |
||||
'country': 'United Kingdom', |
||||
'region': 'Hackney', |
||||
'county': 'Greater London', |
||||
'locality': 'London', |
||||
'neighbourhood': 'Haggerston' |
||||
}; |
||||
t.equal(generator(doc),'Hackney City Farm, Haggerston, Greater London'); |
||||
t.end(); |
||||
}); |
||||
}; |
||||
|
||||
// GBR country
|
||||
module.exports.tests.wales = function(test, common) { |
||||
test('wales', function(t) { |
||||
var doc = { |
||||
'name': 'Wales', |
||||
'country_a': 'GBR', |
||||
'country': 'United Kingdom', |
||||
'region': 'Wales' |
||||
}; |
||||
t.equal(generator(doc),'Wales, United Kingdom'); |
||||
t.end(); |
||||
}); |
||||
}; |
||||
|
||||
// GBR macroregion
|
||||
module.exports.tests.macroregion_trumps_region = function(test, common) { |
||||
test('macroregion should trump region when none of neighbourhood, county, localadmin, locality are available', function(t) { |
||||
var doc = { |
||||
'name': 'Name', |
||||
'country_a': 'GBR', |
||||
'country': 'Country Name', |
||||
'macroregion': 'Macroregion Name', |
||||
'region': 'Region Name' |
||||
}; |
||||
|
||||
t.equal(generator(doc), 'Name, Macroregion Name, Country Name'); |
||||
t.end(); |
||||
|
||||
}); |
||||
}; |
||||
|
||||
module.exports.all = function (tape, common) { |
||||
|
||||
function test(name, testFunction) { |
||||
return tape('label generator (GBR): ' + name, testFunction); |
||||
} |
||||
|
||||
for( var testCase in module.exports.tests ){ |
||||
module.exports.tests[testCase](test, common); |
||||
} |
||||
}; |
@ -0,0 +1,51 @@
|
||||
|
||||
var generator = require('../../../helper/labelGenerator'); |
||||
|
||||
module.exports.tests = {}; |
||||
|
||||
module.exports.tests.interface = function(test, common) { |
||||
test('interface', function(t) { |
||||
t.equal(typeof generator, 'function', 'valid function'); |
||||
t.end(); |
||||
}); |
||||
}; |
||||
|
||||
// SGP region
|
||||
module.exports.tests.north_west_singapore = function(test, common) { |
||||
test('north west singapore', function(t) { |
||||
var doc = { |
||||
'name': 'North West', |
||||
'country_a': 'SGP', |
||||
'country': 'Singapore', |
||||
'region': 'North West' |
||||
}; |
||||
t.equal(generator(doc),'North West, Singapore'); |
||||
t.end(); |
||||
}); |
||||
}; |
||||
|
||||
// SGP venue
|
||||
module.exports.tests.singapore_mcdonalds = function(test, common) { |
||||
test('singapore_mcdonalds', function(t) { |
||||
var doc = { |
||||
'name': 'McDonald\'s', |
||||
'country_a': 'SGP', |
||||
'country': 'Singapore', |
||||
'region': 'Central Singapore', |
||||
'locality': 'Singapore' |
||||
}; |
||||
t.equal(generator(doc),'McDonald\'s, Central Singapore, Singapore'); |
||||
t.end(); |
||||
}); |
||||
}; |
||||
|
||||
module.exports.all = function (tape, common) { |
||||
|
||||
function test(name, testFunction) { |
||||
return tape('label generator: ' + name, testFunction); |
||||
} |
||||
|
||||
for( var testCase in module.exports.tests ){ |
||||
module.exports.tests[testCase](test, common); |
||||
} |
||||
}; |
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in new issue