-
-
Notifications
You must be signed in to change notification settings - Fork 164
/
coarse_reverse.js
174 lines (144 loc) · 5.65 KB
/
coarse_reverse.js
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
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;
return esDoc.data;
} catch( e ) {
// an error occurred when generating a new Document
logger.error('[controller:coarse_reverse][error]', e);
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;