You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
 
 

178 lines
5.7 KiB

const logger = require('pelias-logger').get('coarse_reverse');
const _ = require('lodash');
const Document = require('pelias-model').Document;
const Debug = require('../helper/debug');
const debugLog = new Debug('controller:coarse_reverse');
// do not change order, other functionality depends on most-to-least granular order
const coarse_granularities = [
'neighbourhood',
'borough',
'locality',
'localadmin',
'county',
'macrocounty',
'region',
'macroregion',
'dependency',
'country',
'empire',
'continent',
'ocean',
'marinearea'
];
// remove non-coarse layers and return what's left (or all if empty)
function getEffectiveLayers(requested_layers) {
// remove non-coarse layers
const non_coarse_layers_removed = _.without(requested_layers, 'venue', 'address', 'street');
// if resulting array is empty, use all coarse granularities
if (_.isEmpty(non_coarse_layers_removed)) {
return coarse_granularities;
}
// otherwise use requested layers with non-coarse layers removed
return non_coarse_layers_removed;
}
// drop from coarse_granularities until there's one that was requested
// this depends on coarse_granularities being ordered
function getApplicableRequestedLayers(requested_layers) {
return _.dropWhile(coarse_granularities, (coarse_granularity) => {
return !_.includes(requested_layers, coarse_granularity);
});
}
// removing non-coarse layers could leave effective_layers empty, so it's
// important to check for empty layers here
function hasResultsAtRequestedLayers(results, requested_layers) {
return !_.isEmpty(_.intersection(_.keys(results), requested_layers));
}
// get the most granular layer from the results by taking the head of the intersection
// of coarse_granularities (which are ordered) and the result layers
// ['neighbourhood', 'borough', 'locality'] - ['locality', 'borough'] = 'borough'
// this depends on coarse_granularities being ordered
function getMostGranularLayerOfResult(result_layers) {
return _.head(_.intersection(coarse_granularities, result_layers));
}
// create a model.Document from what's left, using the most granular
// result available as the starting point
function synthesizeDoc(results) {
// find the most granular layer to use as the document layer
const most_granular_layer = getMostGranularLayerOfResult(_.keys(results));
const id = results[most_granular_layer][0].id;
try {
const doc = new Document('whosonfirst', most_granular_layer, id.toString());
doc.setName('default', results[most_granular_layer][0].name);
// assign the administrative hierarchy
_.keys(results).forEach((layer) => {
doc.addParent(layer, results[layer][0].name, results[layer][0].id.toString(), results[layer][0].abbr || undefined);
});
// set centroid if available
if (_.has(results[most_granular_layer][0], 'centroid')) {
doc.setCentroid( results[most_granular_layer][0].centroid );
}
// set bounding box if available
if (_.has(results[most_granular_layer][0], 'bounding_box')) {
const parsed_bounding_box = results[most_granular_layer][0].bounding_box.split(',').map(parseFloat);
doc.setBoundingBox({
upperLeft: {
lat: parsed_bounding_box[3],
lon: parsed_bounding_box[0]
},
lowerRight: {
lat: parsed_bounding_box[1],
lon: parsed_bounding_box[2]
}
});
}
const esDoc = doc.toESDocument();
esDoc.data._id = esDoc._id;
esDoc.data._type = esDoc._type;
return esDoc.data;
} catch( e ) {
// an error occurred when generating a new Document
logger.info(`[controller:coarse_reverse][error]`);
logger.error(e);
logger.info(results);
return null;
}
}
function setup(service, should_execute) {
function controller(req, res, next) {
// do not run controller when a request validation error has occurred
if (!should_execute(req, res)) {
return next();
}
const initialTime = debugLog.beginTimer(req);
// return a warning to the caller that boundary.circle.radius will be ignored
if (!_.isUndefined(req.clean['boundary.circle.radius'])) {
req.warnings.push('boundary.circle.radius is not applicable for coarse reverse');
}
// because coarse reverse is called when non-coarse reverse didn't return
// anything, treat requested layers as if it didn't contain non-coarse layers
const effective_layers = getEffectiveLayers(req.clean.layers);
debugLog.push(req, {effective_layers: effective_layers});
const centroid = {
lat: req.clean['point.lat'],
lon: req.clean['point.lon']
};
service(req, (err, results, metadata) => {
// if there's an error, log it and bail
if (err) {
logger.error('error contacting PIP service', err);
return next();
}
const logInfo = {
controller: 'coarse_reverse',
queryType: 'pip',
response_time: _.get(metadata, 'response_time'),
result_count: _.size(results)
};
logger.info('pip', logInfo);
// now keep everything from the response that is equal to or less granular
// than the most granular layer requested. that is, if effective_layers=['county'],
// remove neighbourhoods, boroughs, localities, localadmins
const applicable_results = _.pick(results, getApplicableRequestedLayers(effective_layers));
res.meta = {};
res.data = [];
// if there's a result at the requested layer(s), synthesize a doc from results
if (hasResultsAtRequestedLayers(applicable_results, effective_layers)) {
const doc = synthesizeDoc(applicable_results);
if (doc){
res.data.push(doc);
}
}
debugLog.stopTimer(req, initialTime);
return next();
});
}
return controller;
}
module.exports = setup;