diff --git a/app/controllers/geocode-controller.js b/app/controllers/geocode-controller.js
new file mode 100644
index 000000000..084086da9
--- /dev/null
+++ b/app/controllers/geocode-controller.js
@@ -0,0 +1,27 @@
+const express = require('express');
+const configModel = require('../models/config-model');
+const getMapboxResponse = require('../lib/geocoder/mapbox');
+const getGoogleResponse = require('../lib/geocoder/google');
+
+function getGeocodeResponse(req, res) {
+ const provider = configModel.server.geocoder?.provider || 'google';
+ const geocoders = {
+ google: getGoogleResponse,
+ mapbox: getMapboxResponse,
+ };
+ const geocoder = geocoders[provider];
+ if (!geocoder) {
+ throw new Error(
+ 'Geocoder provider is not configured. Please configure `config.geocoder.provider`'
+ );
+ }
+ geocoder(req.query, (response) => res.status(200).json(response));
+}
+
+const router = express.Router();
+
+router.get('/geocoder', getGeocodeResponse);
+
+module.exports = function (app) {
+ app.use('/api/geo', router);
+};
diff --git a/app/lib/geocoder/google.js b/app/lib/geocoder/google.js
new file mode 100644
index 000000000..cbc76facd
--- /dev/null
+++ b/app/lib/geocoder/google.js
@@ -0,0 +1,60 @@
+const request = require('request');
+const configModel = require('../../models/config-model');
+
+function getGoogleResponse(query, callback) {
+ query = query || {};
+ const config = configModel.server.geocoder || {};
+ const url =
+ config.url || 'https://maps.googleapis.com/maps/api/geocode/json';
+
+ const accessToken =
+ config['api key'] || configModel.server.google['api key'];
+
+ const params = {
+ ...config.params,
+ key: accessToken,
+ // proximity: query.$center, // -93.17284130807734,45.070291367515466
+ // bbox: query.$bbox, // -93.13644718051957,45.05118347242032,-93.17284130807734,45.070291367515466
+ // limit: query.$limit,
+ address: query.address,
+ };
+
+ return request(
+ url,
+ {
+ qs: params,
+ },
+ (error, response, body) => {
+ let data;
+ try {
+ data = JSON.parse(body);
+ } catch (e) {
+ data = {
+ features: [],
+ };
+ }
+ data = data.results
+ ? data.results.map((f) => ({
+ geometry: {
+ type: 'Point',
+ coordinates: [
+ f.geometry.location.lon,
+ f.geometry.location.lat,
+ ], // [125.6, 10.1],
+ },
+ id: f.place_id,
+ type: 'Feature',
+ properties: {
+ name: f.formatted_address,
+ type: f.geometry.location_type,
+ // score: f.relevance,
+ // accuracy: f.properties.accuracy,
+ },
+ }))
+ : { error: data.message, status: data.status };
+ callback(data);
+ }
+ );
+}
+
+module.exports = getGoogleResponse;
diff --git a/app/lib/geocoder/mapbox.js b/app/lib/geocoder/mapbox.js
new file mode 100644
index 000000000..fbf27007f
--- /dev/null
+++ b/app/lib/geocoder/mapbox.js
@@ -0,0 +1,49 @@
+const request = require('request');
+const configModel = require('../../models/config-model');
+
+module.exports = function getMapboxResponse(query, callback) {
+ const config = configModel.server.geocoder || {};
+ const url =
+ config.url ||
+ 'https://api.mapbox.com/geocoding/v5/mapbox.places/{address}.json';
+ const accessToken = config['api key'];
+
+ const params = {
+ ...config.params,
+ access_token: accessToken,
+ proximity: query.$center, // -93.17284130807734,45.070291367515466
+ bbox: query.$bbox, // -93.13644718051957,45.05118347242032,-93.17284130807734,45.070291367515466
+ limit: query.$limit,
+ };
+
+ return request(
+ url.replace('{address}', query.address),
+ {
+ qs: params,
+ },
+ (error, response, body) => {
+ let data;
+ try {
+ data = JSON.parse(body);
+ } catch (e) {
+ data = {
+ features: [],
+ };
+ }
+ data = data.features
+ ? data.features.map((f) => ({
+ geometry: f.geometry,
+ id: f.id,
+ type: f.type,
+ properties: {
+ name: f.place_name,
+ type: f.place_type.join(','),
+ score: f.relevance,
+ accuracy: f.properties.accuracy,
+ },
+ }))
+ : { error: data.message };
+ callback(data);
+ }
+ );
+};
diff --git a/config/default-config.json b/config/default-config.json
index 7f2537f88..ec228e4ee 100644
--- a/config/default-config.json
+++ b/config/default-config.json
@@ -74,6 +74,10 @@
},
"api key": ""
},
+ "geocoder": {
+ "provider": "google",
+ "api key": ""
+ },
"piwik": {
"analytics": {
"tracker url": "",
@@ -86,7 +90,9 @@
"maps": [
{
"name": "streets",
- "tiles": ["https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png"],
+ "tiles": [
+ "https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png"
+ ],
"attribution": "© OpenStreetMap | Terms"
}
],
diff --git a/tutorials/10-configure.md b/tutorials/10-configure.md
index 13a393a8b..4cda8c9b9 100644
--- a/tutorials/10-configure.md
+++ b/tutorials/10-configure.md
@@ -141,6 +141,22 @@ Which analytics service you'd like to use, either `"google"` or `"piwik"` or if
- analytics -> domain: If you are running Enketo Express on a subdomain, you may need to add the subdomain there (without protocol), e.g. "odk.enke.to" for Google Analytics to pick up the data. When left empty (`""`) the value will be set to "auto" in the GA script.
- api key: The Google API key that is used for geocoding (i.e. the search box in the geo widgets). Can be obtained [here](https://console.developers.google.com/project). Make sure to enable the _GeoCoding API_ service. If you are using Google Maps layers, the same API key is used. Make sure to enable the _Google Maps JavaScript API v3_ service as well in that case (see next item).
+#### geocoder
+
+Enketo allows configuration of different geocoder providers. This configuration is entirely optional, and by default, Google will be used with the same Google api key provided above. If you prefer to use a different api key or provider, you can also configure this using these parameters.
+
+- provider: allows you to change the provider used by the backend geocode service. Current options include `google` and `mapbox`.
+- api key: allows you to set the API key for the geocoder provider. This option is required for mapbox and google.
+
+Example:
+
+```json
+ "geocoder": {
+ "provider": "mapbox",
+ "api key": "pk.12345"
+ },
+```
+
#### piwik
- analytics -> tracker url -> URL on which your piwik service is hosted. The protocol can be omitted, e.g. `"//enketo.piwikpro.com/"`. Required if piwik service is selected under [analytics](#analytics).