UNPKG

node-geocoder

Version:

Node Geocoder, node geocoding library, supports google maps, mapquest, open street map, tom tom, promise

392 lines (391 loc) 14.3 kB
"use strict"; const readline = require('readline'); const AbstractGeocoder = require('./abstractgeocoder'); const ValueError = require('./../error/valueerror'); const OPTIONS = [ 'apiKey', 'appId', 'appCode', 'language', 'politicalView', 'country', 'state', 'production' ]; /** * Constructor * @param <object> httpAdapter Http Adapter * @param <object> options Options (appId, appCode, language, politicalView, country, state, production) */ class HereGeocoder extends AbstractGeocoder { constructor(httpAdapter, options) { super(httpAdapter, options); this.options = options; OPTIONS.forEach(option => { if (!options[option] || options[option] == 'undefined') { this.options[option] = null; } }); if (!this.options.apiKey && !(this.options.appId && this.options.appCode)) { throw new Error('You must specify apiKey to use Here Geocoder'); } } /** * Geocode * @param <string> value Value to geocode (Address) * @param <function> callback Callback method */ _geocode(value, callback) { var _this = this; var params = this._prepareQueryString(); if (value.address) { if (value.language) { params.language = value.language; } if (value.politicalView) { params.politicalview = value.politicalView; } if (value.country) { params.country = value.country; if (value.state) { params.state = value.state; } else { delete params.state; } } if (value.zipcode) { params.postalcode = value.zipcode; } params.searchtext = value.address; } else { params.searchtext = value; } this.httpAdapter.get(this._geocodeEndpoint, params, function (err, result) { var results = []; results.raw = result; if (err) { return callback(err, results); } else { if (result.type === 'ApplicationError') { return callback(new ValueError(result.Details), results); } if (result.error === 'Unauthorized') { return callback(new ValueError(result.error_description), results); } var view = result.Response?.View[0]; if (!view) { return callback(false, results); } // Format each geocoding result results = view.Result.map(_this._formatResult); results.raw = result; callback(false, results); } }); } /** * Reverse geocoding * @param {lat:<number>,lon:<number>} lat: Latitude, lon: Longitude * @param <function> callback Callback method */ _reverse(query, callback) { var lat = query.lat; var lng = query.lon; var _this = this; var params = this._prepareQueryString(); params.pos = lat + ',' + lng; params.mode = 'trackPosition'; this.httpAdapter.get(this._reverseEndpoint, params, function (err, result) { var results = []; results.raw = result; if (err) { return callback(err, results); } else { var view = result.Response.View[0]; if (!view) { return callback(false, results); } // Format each geocoding result results = view.Result.map(_this._formatResult); results.raw = result; callback(false, results); } }); } _formatResult(result) { var location = result.Location || {}; var address = location.Address || {}; var i; var extractedObj = { formattedAddress: address.Label || null, latitude: location.DisplayPosition.Latitude, longitude: location.DisplayPosition.Longitude, country: null, countryCode: address.Country || null, state: address.State || null, county: address.County || null, city: address.City || null, zipcode: address.PostalCode || null, district: address.District || null, streetName: address.Street || null, streetNumber: address.HouseNumber || null, building: address.Building || null, extra: { herePlaceId: location.LocationId || null, confidence: result.Relevance || 0 }, administrativeLevels: {} }; for (i = 0; i < address.AdditionalData.length; i++) { var additionalData = address.AdditionalData[i]; switch (additionalData.key) { //Country 2-digit code case 'Country2': extractedObj.countryCode = additionalData.value; break; //Country name case 'CountryName': extractedObj.country = additionalData.value; break; //State name case 'StateName': extractedObj.administrativeLevels.level1long = additionalData.value; extractedObj.state = additionalData.value; break; //County name case 'CountyName': extractedObj.administrativeLevels.level2long = additionalData.value; extractedObj.county = additionalData.value; } } return extractedObj; } _prepareQueryString() { var params = { additionaldata: 'Country2,true', gen: 8 }; // Deprecated if (this.options.appId) { params.app_id = this.options.appId; } // Deprecated if (this.options.appCode) { params.app_code = this.options.appCode; } if (this.options.apiKey) { params.apiKey = this.options.apiKey; } if (this.options.language) { params.language = this.options.language; } if (this.options.politicalView) { params.politicalview = this.options.politicalView; } if (this.options.country) { params.country = this.options.country; } if (this.options.state) { params.state = this.options.state; } if (this.options.limit) { params.maxresults = this.options.limit; } return params; } async _batchGeocode(values, callback) { try { const jobId = await this.__createJob(values); await this.__pollJobStatus(jobId); const rawResults = await this._getJobResults(jobId); const results = this.__parseBatchResults(rawResults); callback(false, results); } catch (error) { callback(error, null); } } async __createJob(values) { const { country } = this.options; const body = `recId|searchText${country ? '|country' : ''}` + '\n' + values .map((value, ix) => `${ix + 1}|"${value}"${country ? `|${country}` : ''}`) .join(' \n') + '\n'; const params = { ...this._prepareQueryString(), action: 'run', outdelim: '|', indelim: '|', header: false, outputcombined: true, outcols: 'latitude,longitude,locationLabel,houseNumber,street,district,city,postalCode,county,state,addressDetailsCountry,country,building,locationId' }; const options = { body, headers: { 'content-type': 'text/plain', accept: 'application/json' } }; const creteJobReq = await new Promise((resolve, reject) => { this.httpAdapter.post(this._batchGeocodeEndpoint, params, options, (err, result) => { if (err) return reject(err); resolve(result); }); }); const jobRes = await creteJobReq.json(); if (jobRes.type === 'ApplicationError') { throw new Error(jobRes.Details); } return jobRes.Response.MetaInfo.RequestId; } async __pollJobStatus(jobId) { let completed = false; let stalledResultsCount = 500; const url = `${this._batchGeocodeEndpoint}/${jobId}`; const params = { ...this._prepareQueryString(), action: 'status' }; for (; !completed && stalledResultsCount > 0; stalledResultsCount--) { const jobStatus = await new Promise((resolve, reject) => { this.httpAdapter.get(url, params, (err, result) => { if (err) return reject(err); resolve(result); }); }); if (jobStatus.Response.Status === 'completed') { completed = true; break; } } if (!completed) { throw new Error('Job timeout'); } } async _getJobResults(jobId) { // fetch job results const params = { ...this._prepareQueryString(), outputcompressed: false }; const jobResult = await new Promise((resolve, reject) => { this.httpAdapter.get(`${this._batchGeocodeEndpoint}/${jobId}/result`, params, (err, result) => { if (err) return reject(err); resolve(result); }, true); }); const jobResultLineReadeer = readline.createInterface({ input: jobResult.body, crlfDelay: Infinity }); const res = []; for await (const line of jobResultLineReadeer) { const [recId, , , /*seqNumber*/ /*seqLength*/ latitude, longitude, locationLabel, houseNumber, street, district, city, postalCode, county, state, addressDetailsCountry, country, building, locationId] = line.split('|'); const index = Number(recId) - 1; // minus one because our index starts at 0 and theirs at 1 res[index] = res[index] || { error: null, values: [] }; res[index].values.push({ latitude: Number(latitude), longitude: Number(longitude), houseNumber, street, locationLabel, district, city, postalCode, county, state, addressDetailsCountry, // country name. See formatting country, // contry code. See formatting building, locationId }); } // fetch job erros sepparately const jobErrors = await new Promise((resolve, reject) => { this.httpAdapter.get(`${this._batchGeocodeEndpoint}/${jobId}/errors`, params, (err, result) => { if (err) return reject(err); resolve(result); }, true); }); const jobErrorsLineReader = readline.createInterface({ input: jobErrors.body, crlfDelay: Infinity }); for await (const line of jobErrorsLineReader) { const matches = line.match(/Line Number:(?<index>\d+)\s+(?<line>.*)/); if (matches && matches.groups && matches.index) { const index = Number(matches.groups.index) - 2; // minus one because the first line is the header & one less because our index starts at 0 while theirs at 1 res[index] = res[index] || { error: null, values: [] }; res[index].error = matches.groups.line; } else { throw new Error(`Unexpected error line format: "${line}"`); } } return res; } __parseBatchResults(results) { return results.map(result => { const { values, error } = result; return { error, value: values.map(value => { const { latitude, longitude, district, city, county, state, addressDetailsCountry, country, building } = value; return { formattedAddress: value.locationLabel, latitude, longitude, country: addressDetailsCountry, countryCode: country, state, county, city, zipcode: value.postalCode, district, streetName: value.street, streetNumber: value.houseNumber, building, extra: { herePlaceId: value.locationId, confidence: null }, provider: 'here' }; }) }; }); } } Object.defineProperties(HereGeocoder.prototype, { // Here geocoding API endpoint _geocodeEndpoint: { get: function () { return 'https://geocoder.ls.hereapi.com/6.2/geocode.json'; } }, // Here reverse geocoding API endpoint _reverseEndpoint: { get: function () { return 'https://reverse.geocoder.ls.hereapi.com/6.2/reversegeocode.json'; } }, // Here batch geocoding API endpoint _batchGeocodeEndpoint: { get: function () { return 'https://batch.geocoder.ls.hereapi.com/6.2/jobs'; } } }); module.exports = HereGeocoder;