universal-geocoder
Version:
Universal geocoding abstraction server-side and client-side with multiple built-in providers
367 lines (339 loc) • 10.9 kB
text/typescript
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."
);
}
}