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;