Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add option for a promise based externalGeocoder #385

Merged
merged 8 commits into from
Aug 17, 2020
1 change: 1 addition & 0 deletions API.md
Original file line number Diff line number Diff line change
Expand Up @@ -92,6 +92,7 @@ A geocoder component using the [Mapbox Geocoding API][55]
- `options.language` **[string][57]?** Specify the language to use for response text and query result weighting. Options are IETF language tags comprised of a mandatory ISO 639-1 language code and optionally one or more IETF subtags for country or script. More than one value can also be specified, separated by commas. Defaults to the browser's language settings.
- `options.filter` **[Function][66]?** A function which accepts a Feature in the [Carmen GeoJSON][67] format to filter out results from the Geocoding API response before they are included in the suggestions list. Return `true` to keep the item, `false` otherwise.
- `options.localGeocoder` **[Function][66]?** A function accepting the query string which performs local geocoding to supplement results from the Mapbox Geocoding API. Expected to return an Array of GeoJSON Features in the [Carmen GeoJSON][67] format.
- `options.externalGeocoder` **[Function][66]?** A function accepting the query string and current features list which performs geocoding to supplement results from the Mapbox Geocoding API. Expected to return a Promise which resolves to an Array of GeoJSON Features in the [Carmen GeoJSON][67] format.
- `options.reverseMode` **(distance | score)** Set the factors that are used to sort nearby results. (optional, default `distance`)
- `options.reverseGeocode` **[boolean][61]** If `true`, enable reverse geocoding mode. In reverse geocoding, search input is expected to be coordinates in the form `lat, lon`, with suggestions being the reverse geocodes. (optional, default `false`)
- `options.enableEventLogging` **[Boolean][61]** Allow Mapbox to collect anonymous usage statistics from the plugin. (optional, default `true`)
Expand Down
7 changes: 7 additions & 0 deletions debug/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -77,6 +77,13 @@ var geocoder = new MapboxGeocoder({
localGeocoder: function(query) {
return coordinatesGeocoder(query);
},
externalGeocoder: function(query, features) {
// peak at the query and features before calling the external api
if(query.length > 5 && features[0].relevance != 1) {
return fetch('/mock-api.json')
.then(response => response.json())
}
},
mapboxgl: mapboxgl
});

Expand Down
3 changes: 3 additions & 0 deletions debug/mock-api.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
[
{"id":"place.7673410831246050","type":"Feature","place_type":["place"],"relevance":1,"properties":{"wikidata":"Q61"},"text_en-US":"Washington","language_en-US":"en","place_name_en-US":"Washington, District of Columbia, United States of America","text":"Washington","language":"en","place_name":"SERVER: Washington, District of Columbia, United States of America","matching_place_name":"Washington, DC, United States of America","bbox":[-77.1197609567342,38.79155738,-76.909391,38.99555093],"center":[-77.0366,38.895],"geometry":{"type":"Point","coordinates":[-77.0366,38.895]},"context":[{"id":"region.14064402149979320","short_code":"US-DC","wikidata":"Q3551781","text_en-US":"District of Columbia","language_en-US":"en","text":"District of Columbia","language":"en"},{"id":"country.19678805456372290","wikidata":"Q30","short_code":"us","text_en-US":"United States of America","language_en-US":"en","text":"United States of America","language":"en"}]}
]
138 changes: 82 additions & 56 deletions lib/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,7 @@ var subtag = require('subtag');
* @param {string} [options.language] Specify the language to use for response text and query result weighting. Options are IETF language tags comprised of a mandatory ISO 639-1 language code and optionally one or more IETF subtags for country or script. More than one value can also be specified, separated by commas. Defaults to the browser's language settings.
* @param {Function} [options.filter] A function which accepts a Feature in the [Carmen GeoJSON](https://github.com/mapbox/carmen/blob/master/carmen-geojson.md) format to filter out results from the Geocoding API response before they are included in the suggestions list. Return `true` to keep the item, `false` otherwise.
* @param {Function} [options.localGeocoder] A function accepting the query string which performs local geocoding to supplement results from the Mapbox Geocoding API. Expected to return an Array of GeoJSON Features in the [Carmen GeoJSON](https://github.com/mapbox/carmen/blob/master/carmen-geojson.md) format.
* @param {Function} [options.externalGeocoder] A function accepting the query string and current features list which performs geocoding to supplement results from the Mapbox Geocoding API. Expected to return a Promise which resolves to an Array of GeoJSON Features in the [Carmen GeoJSON](https://github.com/mapbox/carmen/blob/master/carmen-geojson.md) format.
* @param {distance|score} [options.reverseMode=distance] - Set the factors that are used to sort nearby results.
* @param {boolean} [options.reverseGeocode=false] If `true`, enable reverse geocoding mode. In reverse geocoding, search input is expected to be coordinates in the form `lat, lon`, with suggestions being the reverse geocodes.
* @param {Boolean} [options.enableEventLogging=true] Allow Mapbox to collect anonymous usage statistics from the plugin.
Expand Down Expand Up @@ -411,6 +412,7 @@ MapboxGeocoder.prototype = {
'mode'
];
var self = this;
var geocoderError = null;
// Create config object
var config = keys.reduce(function(config, key) {
if (self.options[key]) {
Expand Down Expand Up @@ -465,71 +467,95 @@ MapboxGeocoder.prototype = {
localGeocoderRes = [];
}
}
var externalGeocoderRes = [];

request.catch(function(error){
geocoderError = error;
}.bind(this))
.then(
function(response) {
this._loadingEl.style.display = 'none';

var res = {};

if (!response){
res = {
type: 'FeatureCollection',
features: []
}
} else if (response.statusCode == '200') {
res = response.body;
res.request = response.request
res.headers = response.headers
}

request.then(
function(response) {
this._loadingEl.style.display = 'none';

var res = {};
res.config = config;

if (!response){
res = {
type: 'FeatureCollection',
features: []
if (this.fresh){
this.eventManager.start(this);
this.fresh = false;
}
}else if (response.statusCode == '200') {
res = response.body;
res.request = response.request
res.headers = response.headers
}

res.config = config;
if (this.fresh){
this.eventManager.start(this);
this.fresh = false;
}
// supplement Mapbox Geocoding API results with locally populated results
res.features = res.features
? localGeocoderRes.concat(res.features)
: localGeocoderRes;

// apply results filter if provided
if (this.options.filter && res.features.length) {
res.features = res.features.filter(this.options.filter);
}
// supplement Mapbox Geocoding API results with locally populated results
res.features = res.features
? localGeocoderRes.concat(res.features)
: localGeocoderRes;

if (this.options.externalGeocoder) {

externalGeocoderRes = this.options.externalGeocoder(searchInput, res.features) || [];
// supplement Mapbox Geocoding API results with features returned by a promise
return externalGeocoderRes.then(function(features) {
res.features = res.features ? features.concat(res.features) : features;
return res;
}, function(){
// on error, display the original result
return res;
});
}
return res;

if (res.features.length) {
this._clearEl.style.display = 'block';
this._eventEmitter.emit('results', res);
this._typeahead.update(res.features);
} else {
this._clearEl.style.display = 'none';
this._typeahead.selected = null;
this._renderNoResults();
this._eventEmitter.emit('results', res);
}
}.bind(this)).then(
function(res) {
if (geocoderError) {
throw geocoderError;
}

}.bind(this)
);
// apply results filter if provided
if (this.options.filter && res.features.length) {
res.features = res.features.filter(this.options.filter);
}

request.catch(
function(err) {
this._loadingEl.style.display = 'none';
if (res.features.length) {
this._clearEl.style.display = 'block';
this._eventEmitter.emit('results', res);
this._typeahead.update(res.features);
} else {
this._clearEl.style.display = 'none';
this._typeahead.selected = null;
this._renderNoResults();
this._eventEmitter.emit('results', res);
}

// in the event of an error in the Mapbox Geocoding API still display results from the localGeocoder
if (localGeocoderRes.length && this.options.localGeocoder) {
this._clearEl.style.display = 'block';
this._typeahead.update(localGeocoderRes);
} else {
this._clearEl.style.display = 'none';
this._typeahead.selected = null;
this._renderError();
}
}.bind(this)
).catch(
function(err) {
this._loadingEl.style.display = 'none';

// in the event of an error in the Mapbox Geocoding API still display results from the localGeocoder
if ((localGeocoderRes.length && this.options.localGeocoder) || (externalGeocoderRes.length && this.options.externalGeocoder) ) {
this._clearEl.style.display = 'block';
this._typeahead.update(localGeocoderRes);
} else {
this._clearEl.style.display = 'none';
this._typeahead.selected = null;
this._renderError();
}

this._eventEmitter.emit('results', { features: localGeocoderRes });
this._eventEmitter.emit('error', { error: err });
}.bind(this)
);
this._eventEmitter.emit('results', { features: localGeocoderRes });
this._eventEmitter.emit('error', { error: err });
}.bind(this)
);

return request;
},
Expand Down
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@
"unpkg": "dist/mapbox-gl-geocoder.min.js",
"style": "lib/mapbox-gl-geocoder.css",
"scripts": {
"start": "budo debug/index.js --live -- -t brfs ",
"start": "budo debug/index.js --dir debug --live -- -t brfs ",
"prepublish": "NODE_ENV=production && mkdir -p dist && browserify --standalone MapboxGeocoder lib/index.js | uglifyjs -c -m > dist/mapbox-gl-geocoder.min.js && cp lib/mapbox-gl-geocoder.css dist/",
"test": "browserify -t envify test/index.js test/events.test.js | smokestack -b firefox | tap-status | tap-color",
"docs": "documentation build lib/index.js --format=md > API.md",
Expand Down
76 changes: 64 additions & 12 deletions test/test.geocoder.js
Original file line number Diff line number Diff line change
Expand Up @@ -315,6 +315,56 @@ test('geocoder', function(tt) {
);
});

tt.test('options.externalGeocoder', function(t) {
t.plan(3);
setup({
flyTo: false,
limit: 6,
externalGeocoder: function() {
return Promise.resolve([
{
"id":"place.7673410831246050",
"type":"Feature",
"place_name":"Promise: Washington, District of Columbia, United States of America",
"geometry":{"type":"Point","coordinates":[-77.0366,38.895]}
}
])
}
});

geocoder.query('Washington, DC');
geocoder.on(
'results',
once(function(e) {
t.equal(e.features.length, 7, 'External geocoder used');

geocoder.query('DC');
geocoder.on(
'results',
once(function(e) {
t.equal(
e.features.length,
7,
'External geocoder supplement remote response'
);

geocoder.query('District of Columbia');
geocoder.on(
'results',
once(function(e) {
t.equal(
e.features[0].place_name,
'Promise: Washington, District of Columbia, United States of America',
'External geocoder results above remote response'
);
})
);
})
);
})
);
});

tt.test('country bbox', function(t) {
t.plan(2);
setup({});
Expand Down Expand Up @@ -626,8 +676,8 @@ test('geocoder', function(tt) {
once(function() {
t.ok(mapFlyMethod.calledOnce, "The map flyTo was called when the option was set to true");
var calledWithArgs = mapFlyMethod.args[0][0];
t.equals(+calledWithArgs.center[0].toFixed(4), +-122.4797165.toFixed(4), 'the map is directed to fly to the right longitude');
t.equals(+calledWithArgs.center[1].toFixed(4), +37.81878675.toFixed(4), 'the map is directed to fly to the right latitude');
t.equals(+calledWithArgs.center[0].toFixed(4), +-122.4809.toFixed(4), 'the map is directed to fly to the right longitude');
t.equals(+calledWithArgs.center[1].toFixed(4), +37.8181.toFixed(4), 'the map is directed to fly to the right latitude');
t.deepEqual(calledWithArgs.zoom, 16, 'the map is directed to fly to the right zoom');
})
);
Expand All @@ -650,8 +700,8 @@ test('geocoder', function(tt) {
once(function() {
t.ok(mapFlyMethod.calledOnce, "The map flyTo was called when the option was set to true");
var calledWithArgs = mapFlyMethod.args[0][0];
t.equals(+calledWithArgs.center[0].toFixed(4), +-122.4797165.toFixed(4), 'the map is directed to fly to the right longitude');
t.equals(+calledWithArgs.center[1].toFixed(4), +37.81878675.toFixed(4), 'the map is directed to fly to the right latitude'); t.deepEqual(calledWithArgs.zoom, 4, 'the selected result overrides the constructor zoom option');
t.equals(+calledWithArgs.center[0].toFixed(4), +-122.4809.toFixed(4), 'the map is directed to fly to the right longitude');
t.equals(+calledWithArgs.center[1].toFixed(4), +37.8181.toFixed(4), 'the map is directed to fly to the right latitude'); t.deepEqual(calledWithArgs.zoom, 4, 'the selected result overrides the constructor zoom option');
t.deepEqual(calledWithArgs.speed, 5, 'speed argument is passed to the flyTo method');
})
);
Expand Down Expand Up @@ -929,14 +979,16 @@ test('geocoder', function(tt) {
geocoder.on(
'result',
once(function() {
t.notEqual(geocoder._typeahead.data.length, 0, 'the suggestions menu has some options in it after a query');
geocoder._renderMessage("<h1>This is a test</h1>");
t.equals(geocoder._typeahead.data.length, 0, 'the data was cleared from the suggestions');
t.equals(geocoder._typeahead.selected, null, 'the selected option was cleared from the suggestions');
t.ok(typeaheadRenderErrorSpy.calledOnce, 'the renderError method was called exactly once');
var calledWithArgs = typeaheadRenderErrorSpy.args[0][0];
t.equals(calledWithArgs, "<h1>This is a test</h1>", 'the error rendering function was called with the correct message');
t.end();
setTimeout(function() {
t.notEqual(geocoder._typeahead.data.length, 0, 'the suggestions menu has some options in it after a query');
geocoder._renderMessage("<h1>This is a test</h1>");
t.equals(geocoder._typeahead.data.length, 0, 'the data was cleared from the suggestions');
t.equals(geocoder._typeahead.selected, null, 'the selected option was cleared from the suggestions');
t.ok(typeaheadRenderErrorSpy.calledOnce, 'the renderError method was called exactly once');
var calledWithArgs = typeaheadRenderErrorSpy.args[0][0];
t.equals(calledWithArgs, "<h1>This is a test</h1>", 'the error rendering function was called with the correct message');
t.end();
});
})
);
});
Expand Down
58 changes: 31 additions & 27 deletions test/test.ui.js
Original file line number Diff line number Diff line change
Expand Up @@ -58,19 +58,21 @@ test('Geocoder#inputControl', function(tt) {
'clear',
once(function() {
t.pass('input was cleared');
t.equals(geocoder.fresh, false, 'the geocoder is fresh again')
t.equals(geocoder.mapMarker, null, 'the marker was reset on clear')

geocoder.setInput('Paris');
t.equals(inputEl.value, 'Paris', 'value populates in input');

geocoder.setInput('90,45');
t.equals(
inputEl.value,
'90,45',
'valid LngLat value populates in input'
);
t.end();
setTimeout(function () {
t.equals(geocoder.fresh, false, 'the geocoder is fresh again')
t.equals(geocoder.mapMarker, null, 'the marker was reset on clear')

geocoder.setInput('Paris');
t.equals(inputEl.value, 'Paris', 'value populates in input');

geocoder.setInput('90,45');
t.equals(
inputEl.value,
'90,45',
'valid LngLat value populates in input'
);
t.end();
});
})
);

Expand Down Expand Up @@ -460,20 +462,22 @@ test('Geocoder#addTo(String) -- no map', function(tt) {
'clear',
once(function() {
t.pass('input was cleared');
t.equals(geocoder.fresh, false, 'the geocoder is fresh again')

geocoder.setInput('Paris');
t.equals(inputEl.value, 'Paris', 'value populates in input');

geocoder.setInput('90,45');
t.equals(
inputEl.value,
'90,45',
'valid LngLat value populates in input'
);
// teardown
container.remove();
t.end();
setTimeout(function() {
t.equals(geocoder.fresh, false, 'the geocoder is fresh again')

geocoder.setInput('Paris');
t.equals(inputEl.value, 'Paris', 'value populates in input');

geocoder.setInput('90,45');
t.equals(
inputEl.value,
'90,45',
'valid LngLat value populates in input'
);
// teardown
container.remove();
t.end();
});
})
);

Expand Down