Skip to content

Commit

Permalink
adding new geocoder APlace.io (#349)
Browse files Browse the repository at this point in the history
  • Loading branch information
Evoks authored Oct 29, 2023
1 parent 4a12840 commit 4c4d630
Show file tree
Hide file tree
Showing 4 changed files with 590 additions and 1 deletion.
6 changes: 5 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@ const options = {

// Optional depending on the providers
fetch: customFetchImplementation,
apiKey: 'YOUR_API_KEY', // for Mapquest, OpenCage, Google Premier
apiKey: 'YOUR_API_KEY', // for Mapquest, OpenCage, APlace, Google Premier
formatter: null // 'gpx', 'string', ...
};

Expand Down Expand Up @@ -98,6 +98,10 @@ const geocoder = NodeGeocoder({
## Geocoder Providers (in alphabetical order)

- `agol` : ArcGis Online Geocoding service. Supports geocoding and reverse. Requires a client_id & client_secret
- `aplace` : APlace.io Geocoding service. Supports geocoding and reverse. Requires an access token ([read about access tokens here](https://aplace.io/en/documentation/general/authentication)) using `options.apiKey`
- For `geocode` you can use simple string parameter or an object containing the different parameters (`type`, `address`, `zip`, `city`, `country`, `countryCode` and `countries`). See available values for `type` and `countries` parameters [here](https://aplace.io/en/documentation/rest-api/search)
- For `reverse`, you can pass over `{lat, lon}`
- For both methods, use `options.language` (either `fr` or `en`) to specify the language of the results
- `datasciencetoolkit` : DataScienceToolkitGeocoder. Supports IPv4 geocoding and address geocoding. Use `options.host` to specify a local instance
- `freegeoip` : FreegeoipGeocoder. Supports IP geocoding
- `geocodio`: Geocodio, Supports address geocoding and reverse geocoding (US only)
Expand Down
312 changes: 312 additions & 0 deletions lib/geocoder/aplacegeocoder.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,312 @@
'use strict';

var util = require('util'),
net = require('net'),
AbstractGeocoder = require('./abstractgeocoder');

var ValueError = require('../error/valueerror.js');

function formatGeocoderName(name) {
return name.toLowerCase().replace(/geocoder$/, '');
}

/**
* APlaceGeocoder Constructor
* @param <object> httpAdapter Http Adapter
* @param <object> options Options (language, apiKey)
*/
var APlaceGeocoder = function (httpAdapter, options) {
this.supportIPv4 = false;
this.supportIPv6 = false;
this.supportAddress = true;
APlaceGeocoder.super_.call(this, httpAdapter);

if (!this.constructor.name) {
throw new Error('The Constructor must be named');
}

this.name = formatGeocoderName(this.constructor.name);

if (!httpAdapter || httpAdapter == 'undefined') {
throw new Error(this.constructor.name + ' need an httpAdapter');
}
this.httpAdapter = httpAdapter;

if (options) {
for (var k in options) {
this.options[k] = options[k];
}
}

if (!this.options.language) {
this.options.language = 'en';
}

if (!this.options.apiKey) {
throw new Error('You must specify a apiKey (see https://aplace.io/en/documentation/general/authentication)');
}

if (this.options.apiKey && !httpAdapter.supportsHttps()) {
throw new Error('You must use https http adapter');
}

this.options = options;
};

util.inherits(APlaceGeocoder, AbstractGeocoder);

// APlace geocoding API endpoint
APlaceGeocoder.prototype._geocoderEndpoint =
'https://api.aplace.io/api/v1.0/search';


// APlace reverse API endpoint
APlaceGeocoder.prototype._reverseEndpoint =
'https://api.aplace.io/api/v1.0/pip';

/**
* Reverse geocoding
* @param {lat:<number>,lon:<number>} lat: Latitude, lon: Longitude
* @param <function> callback Callback method
*/
APlaceGeocoder.prototype.reverse = function (query, callback) {
return this._reverse(query, callback);
};

/**
* Geocode
* @param <string> value Value to geocode
* @param <function> callback Callback method
*/
APlaceGeocoder.prototype.geocode = function (value, callback) {
if (
net.isIPv4(value) &&
(!this.supportIPv4 || this.supportIPv4 == 'undefined')
) {
throw new ValueError(
this.constructor.name + ' does not support geocoding IPv4'
);
}

if (
net.isIPv6(value) &&
(!this.supportIPv6 || this.supportIPv6 == 'undefined')
) {
throw new ValueError(
this.constructor.name + ' does not support geocoding IPv6'
);
}
return this._geocode(value, callback);
};

/**
* Batch Geocode
* @param <string[]> values Valueas to geocode
* @param <function> callback Callback method
*/
APlaceGeocoder.prototype.batchGeocode = function (values, callback) {
Promise.all(
values.map(value =>
new Promise(resolve => {
this.geocode(value, (error, value) => {
resolve({
error,
value
});
});
})
)
)
.then(data => callback(null, data));
};

APlaceGeocoder.prototype._reverse = function (query, callback) {
try {
const that = this;
const lat = query.lat;
const lon = query.lon;

if (!lat || !lon) {
throw new ValueError('no valid lat or lon given');
}

const params = {
key: this.options.apiKey,
lat: lat,
lon: lon
};

this.httpAdapter.get(this._reverseEndpoint, params, function (err, response) {
if (err) {
return callback(err);
} else {
if (!response.session_id) {
return callback(
new Error(
'Status is ' +
response.status +
'.' +
(response.error_message ? ' ' + response.error_message : '')
),
{ raw: response }
);
}
var results = [];

if (response.data && response.data.length > 0) {
results.push(that._formatResult(response.data[0]));
}
results.raw = response;
callback(false, results);
}
});
} catch (error) {
return callback(error);
}
}

APlaceGeocoder.prototype._geocode = function (value, callback) {
try {
const that = this;
let query;

if (typeof value === 'string') {
query = value;
}
if (typeof value === 'object') {
const queryKeys = ['address', 'zip', 'city', 'country', 'countryCode'];
const queryParts = [];
for (const key in queryKeys) {
if (value[key] && value[key].length > 0) {
queryParts.push(value[key]);
}
}
query = queryParts.join(' ');
}
const params = {
key: this.options.apiKey,
q: query.trim(),
lang: this.options.language,
type: 'house_number'
};

const queryParams = ['type', 'lat', 'lon', 'radius', 'countries'];
for (const key in queryParams) {
if (value[queryParams[key]]) {
params[queryParams[key]] = value[queryParams[key]];
}
}

const validResultsTypes = ['house_number', 'road', 'quarter', 'city', 'county', 'state', 'region', 'country'];
if (params.type) {
if (typeof params.type === 'string') {
if (validResultsTypes.indexOf(params.type) === -1) {
throw new Error('type must be one of ' + validResultsTypes.join(', '));
}
}
}

for (const key in params) {
if (!params[key]) {
delete params[key];
}
}

this.httpAdapter.get(this._geocoderEndpoint, params, function (err, response) {
if (err) {
return callback(err);
} else {
if (!response.session_id) {
return callback(
new Error(
'Status is ' +
response.status +
'.' +
(response.error_message ? ' ' + response.error_message : '')
),
{ raw: response }
);
}
var results = [];

if (response.data && response.data.length > 0) {
results.push(that._formatResult(response.data[0]));
}
results.raw = response;
callback(false, results);
}
});
} catch (error) {
return callback(error);
}
}

APlaceGeocoder.prototype._formatResult = function (result) {
let formattedAddress = result.match;
if (result.match_details && result.match_details.length > 0) {
formattedAddress += ', ' + result.match_details;
}
var extractedObj = {
formattedAddress: formattedAddress || null,
latitude: result.lat,
longitude: result.lon,
extra: {},
administrativeLevels: {}
};

for (const key in result.address) {
switch (key) {
// Address
case 'postal_code':
extractedObj.zipcode = result.address.postcode;
break;

case 'road':
extractedObj.streetName = result.address.road;
break;

case 'street_number':
extractedObj.streetNumber = result.address.house_number;
break;

case 'country':
extractedObj.administrativeLevels.level1long = result.address.country;
extractedObj.administrativeLevels.level1short = result.address.country_code;
extractedObj.extra.country = result.address.country;
extractedObj.extra.countryCode = result.address.country_code;
break;

case 'region':
extractedObj.administrativeLevels.level2long = result.address.region;
extractedObj.administrativeLevels.level2short = result.address.region_code;
extractedObj.extra.region = result.address.region;
extractedObj.extra.regionCode = result.address.region_code;
break;

case 'state':
extractedObj.administrativeLevels.level3long = result.address.state;
extractedObj.administrativeLevels.level3short = result.address.state_code;
extractedObj.extra.state = result.address.state;
extractedObj.extra.stateCode = result.address.state_code;
break;

case 'county':
extractedObj.administrativeLevels.level4long = result.address.county;
extractedObj.extra.county = result.address.county;
break;

case 'city':
extractedObj.administrativeLevels.level5long = result.address.city;
break;

case 'quarter':
extractedObj.administrativeLevels.level6long = result.address.quarter;
extractedObj.extra.quarter = result.address.quarter;
extractedObj.neighbourhood = result.address.quarter;
break;
}
}
return extractedObj;
};

module.exports = APlaceGeocoder;
4 changes: 4 additions & 0 deletions lib/geocoderfactory.js
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@ const SmartyStreets = require('./geocoder/smartystreetsgeocoder.js');
const TeleportGeocoder = require('./geocoder/teleportgeocoder.js');
const OpendataFranceGeocoder = require('./geocoder/opendatafrancegeocoder.js');
const MapBoxGeocoder = require('./geocoder/mapboxgeocoder.js');
const APlaceGeocoder = require('./geocoder/aplacegeocoder.js');

/**
* Geocoder Facotry
Expand Down Expand Up @@ -151,6 +152,9 @@ const GeocoderFactory = {
if (geocoderName === 'mapbox') {
return new MapBoxGeocoder(adapter, extra);
}
if (geocoderName === 'aplace') {
return new APlaceGeocoder(adapter, extra);
}
throw new Error('No geocoder provider find for : ' + geocoderName);
},
/**
Expand Down
Loading

0 comments on commit 4c4d630

Please sign in to comment.