UNPKG

universal-geocoder

Version:

Universal geocoding abstraction server-side and client-side with multiple built-in providers

367 lines (339 loc) 10.9 kB
import { ExternalLoaderBody, ExternalLoaderHeaders, ExternalLoaderParams, } from "ExternalLoader"; import Geocoded from "Geocoded"; import { ErrorCallback, GeocodedResultsCallback, ProviderHelpers, ProviderInterface, ProviderOptionsInterface, defaultProviderOptions, } from "provider"; import { GeocodeQuery, GeocodeQueryObject, ReverseQuery, ReverseQueryObject, } from "query"; export interface ChainProviderOptionsInterface extends ProviderOptionsInterface { readonly providers: ProviderInterface<Geocoded>[]; readonly parallelize?: boolean; readonly first?: boolean; } export const defaultChainProviderOptions = { ...defaultProviderOptions, providers: [], }; type ChainGeocodedResultsCallback = GeocodedResultsCallback<Geocoded>; export default class ChainProvider implements ProviderInterface<Geocoded> { private options: ChainProviderOptionsInterface; public constructor( options: ChainProviderOptionsInterface = defaultChainProviderOptions ) { this.options = options; } public geocode( query: string | GeocodeQuery | GeocodeQueryObject ): Promise<Geocoded[]>; public geocode( query: string | GeocodeQuery | GeocodeQueryObject, callback: ChainGeocodedResultsCallback, errorCallback?: ErrorCallback ): void; public geocode( query: string | GeocodeQuery | GeocodeQueryObject, callback?: ChainGeocodedResultsCallback, errorCallback?: ErrorCallback ): void | Promise<Geocoded[]> { if (this.options.parallelize || this.options.first) { if (!callback) { return new Promise((resolve, reject) => this.geocodeAllProviders( query, (results) => resolve(results), (error) => reject(error) ) ); } return this.geocodeAllProviders(query, callback, errorCallback); } if (!callback) { return new Promise((resolve, reject) => this.geocodeNextProvider( this.options.providers, query, (results) => resolve(results), (error) => reject(error) ) ); } return this.geocodeNextProvider( this.options.providers, query, callback, errorCallback ); } public geodecode( query: ReverseQuery | ReverseQueryObject ): Promise<Geocoded[]>; public geodecode( query: ReverseQuery | ReverseQueryObject, callback: ChainGeocodedResultsCallback, errorCallback?: ErrorCallback ): void; public geodecode( latitude: number | string, longitude: number | string ): Promise<Geocoded[]>; public geodecode( latitude: number | string, longitude: number | string, callback: ChainGeocodedResultsCallback, errorCallback?: ErrorCallback ): void; public geodecode( latitudeOrQuery: number | string | ReverseQuery | ReverseQueryObject, longitudeOrCallback?: number | string | ChainGeocodedResultsCallback, callbackOrErrorCallback?: ChainGeocodedResultsCallback | ErrorCallback, errorCallback?: ErrorCallback ): void | Promise<Geocoded[]> { const reverseQuery = ProviderHelpers.getReverseQueryFromParameters( latitudeOrQuery, longitudeOrCallback ); const reverseCallback = ProviderHelpers.getCallbackFromParameters( longitudeOrCallback, callbackOrErrorCallback ); const reverseErrorCallback = ProviderHelpers.getErrorCallbackFromParameters( longitudeOrCallback, callbackOrErrorCallback, errorCallback ); if (this.options.parallelize || this.options.first) { if (!reverseCallback) { return new Promise((resolve, reject) => this.geodecodeAllProviders( reverseQuery, (results) => resolve(results), (error) => reject(error) ) ); } return this.geodecodeAllProviders( reverseQuery, reverseCallback, reverseErrorCallback ); } if (!reverseCallback) { return new Promise((resolve, reject) => this.geodecodeNextProvider( this.options.providers, reverseQuery, (results) => resolve(results), (error) => reject(error) ) ); } return this.geodecodeNextProvider( this.options.providers, reverseQuery, reverseCallback, reverseErrorCallback ); } private geocodeNextProvider( providers: ProviderInterface<Geocoded>[], query: string | GeocodeQuery | GeocodeQueryObject, callback: ChainGeocodedResultsCallback, errorCallback?: ErrorCallback ): void { const [provider, ...nextProviders] = providers; const resultCallback: ChainGeocodedResultsCallback = (results) => { if (results.length > 0) { callback(results); return; } this.geocodeNextProvider(nextProviders, query, callback, errorCallback); }; const resultErrorCallback: ErrorCallback = (responseError) => { if (errorCallback) { errorCallback(responseError); } if (!errorCallback) { // eslint-disable-next-line no-console console.error( `An error has occurred when geocoding with the provider ${provider.constructor.name}`, responseError ); } resultCallback([]); }; provider.geocode(query, resultCallback, resultErrorCallback); } private geodecodeNextProvider( providers: ProviderInterface<Geocoded>[], reverseQuery: ReverseQuery, callback: ChainGeocodedResultsCallback, errorCallback?: ErrorCallback ): void { const [provider, ...nextProviders] = providers; const resultCallback: ChainGeocodedResultsCallback = (results) => { if (results.length > 0) { callback(results); return; } this.geodecodeNextProvider( nextProviders, reverseQuery, callback, errorCallback ); }; const resultErrorCallback: ErrorCallback = (responseError) => { if (errorCallback) { errorCallback(responseError); } if (!errorCallback) { // eslint-disable-next-line no-console console.error( `An error has occurred when geodecoding with the provider ${provider.constructor.name}`, responseError ); } resultCallback([]); }; provider.geodecode(reverseQuery, resultCallback, resultErrorCallback); } private geocodeAllProviders( query: string | GeocodeQuery | GeocodeQueryObject, callback: ChainGeocodedResultsCallback, errorCallback?: ErrorCallback ): void { const providerResults: Map<string, Geocoded[]> = new Map(); let callbackCalled = false; const getProviderResult = () => this.options.providers.reduce((result, provider) => { let providerResult = result; if (undefined === providerResult && this.options.first) { providerResult = []; } if (undefined === providerResult) { return undefined; } if (providerResult.length > 0) { return providerResult; } return providerResults.get(provider.constructor.name); }, <undefined | Geocoded[]>[]); const resultProviderCallback: ( providerName: string ) => ChainGeocodedResultsCallback = (providerName) => (results) => { providerResults.set(providerName, results); const providerResult = getProviderResult(); if (!callbackCalled && providerResult) { callback(providerResult); callbackCalled = true; } }; const resultProviderErrorCallback: (providerName: string) => ErrorCallback = (providerName) => (responseError) => { if (errorCallback) { errorCallback(responseError); } if (!errorCallback) { // eslint-disable-next-line no-console console.error( `An error has occurred when geocoding with the provider ${providerName}`, responseError ); } resultProviderCallback(providerName)([]); }; this.options.providers.forEach((provider) => { const providerName = provider.constructor.name; provider.geocode( query, resultProviderCallback(providerName), resultProviderErrorCallback(providerName) ); }); } private geodecodeAllProviders( reverseQuery: ReverseQuery, callback: ChainGeocodedResultsCallback, errorCallback?: ErrorCallback ): void { const providerResults: Map<string, Geocoded[]> = new Map(); let callbackCalled = false; const getProviderResult = () => this.options.providers.reduce((result, provider) => { let providerResult = result; if (undefined === providerResult && this.options.first) { providerResult = []; } if (undefined === providerResult) { return undefined; } if (providerResult.length > 0) { return providerResult; } return providerResults.get(provider.constructor.name); }, <undefined | Geocoded[]>[]); const resultProviderCallback: ( providerName: string ) => ChainGeocodedResultsCallback = (providerName) => (results) => { providerResults.set(providerName, results); const providerResult = getProviderResult(); if (!callbackCalled && providerResult) { callback(providerResult); callbackCalled = true; } }; const resultProviderErrorCallback: (providerName: string) => ErrorCallback = (providerName) => (responseError) => { if (errorCallback) { errorCallback(responseError); } if (!errorCallback) { // eslint-disable-next-line no-console console.error( `An error has occurred when geodecoding with the provider ${providerName}`, responseError ); } resultProviderCallback(providerName)([]); }; this.options.providers.forEach((provider) => { const providerName = provider.constructor.name; provider.geodecode( reverseQuery, resultProviderCallback(providerName), resultProviderErrorCallback(providerName) ); }); } // eslint-disable-next-line class-methods-use-this public executeRequest( // eslint-disable-next-line @typescript-eslint/no-unused-vars params: ExternalLoaderParams, // eslint-disable-next-line @typescript-eslint/no-unused-vars callback: ChainGeocodedResultsCallback, // eslint-disable-next-line @typescript-eslint/no-unused-vars headers?: ExternalLoaderHeaders, // eslint-disable-next-line @typescript-eslint/no-unused-vars body?: ExternalLoaderBody, // eslint-disable-next-line @typescript-eslint/no-unused-vars errorCallback?: ErrorCallback ): void { throw new Error( "executeRequest cannot be called directly from the chain provider." ); } }