universal-geocoder
Version:
Universal geocoding abstraction server-side and client-side with multiple built-in providers
362 lines (330 loc) • 9.47 kB
text/typescript
import {
ExternalLoaderBody,
ExternalLoaderHeaders,
ExternalLoaderInterface,
ExternalLoaderParams,
} from "ExternalLoader";
import {
BingGeocoded,
ErrorCallback,
GeocodedResultsCallback,
ProviderHelpers,
ProviderInterface,
ProviderOptionsInterface,
defaultProviderOptions,
} from "provider";
import {
GeocodeQuery,
GeocodeQueryObject,
ReverseQuery,
ReverseQueryObject,
} from "query";
import { FlatBoundingBox, FlatCoordinates } from "types";
import AdminLevel from "AdminLevel";
import { ResponseError } from "error";
interface BingRequestParams {
[param: string]: string | undefined;
readonly key?: string;
readonly inclnb?: string;
readonly incl?: string;
readonly maxRes?: string;
readonly includeEntityTypes?: string;
readonly c?: string;
readonly jsonpCallback?: string;
}
export type BingPrecision = "High" | "Medium" | "Low";
export interface BingResult {
__type: string;
bbox: FlatBoundingBox;
name: string;
point: {
type: string;
coordinates: FlatCoordinates;
};
address: {
addressLine: string;
neighborhood?: string;
adminDistrict: string;
adminDistrict2: string;
countryRegion: string;
countryRegionIso2?: string;
landmark?: string;
formattedAddress: string;
locality: string;
postalCode: string;
};
confidence: BingPrecision;
entityType: string;
geocodePoints: {
type: string;
coordinates: FlatCoordinates;
calculationMethod:
| "Interpolation"
| "InterpolationOffset"
| "Parcel"
| "Rooftop";
usageTypes: ("Display" | "Route")[];
}[];
queryParseValues?: {
property:
| " AddressLine"
| "Locality"
| "AdminDistrict"
| "AdminDistrict2"
| "PostalCode"
| "CountryRegion"
| "Landmark";
value: string;
};
matchCodes: ("Good" | "Ambiguous" | "UpHierarchy")[];
}
export interface BingResponse {
statusCode: number;
statusDescription: string | null;
authenticationResultCode:
| "ValidCredentials"
| "InvalidCredentials"
| "CredentialsExpired"
| "NotAuthorized"
| "NoCredentials"
| "None";
traceId: string;
copyright: string;
brandLogoUri: string;
resourceSets: {
estimatedTotal: number;
resources: BingResult[];
}[];
errorDetails?: string[];
}
export interface BingProviderOptionsInterface extends ProviderOptionsInterface {
readonly apiKey: string;
}
export const defaultBingProviderOptions = {
...defaultProviderOptions,
apiKey: "",
};
type BingGeocodedResultsCallback = GeocodedResultsCallback<BingGeocoded>;
export default class BingProvider implements ProviderInterface<BingGeocoded> {
private externalLoader: ExternalLoaderInterface;
private options: BingProviderOptionsInterface;
public constructor(
_externalLoader: ExternalLoaderInterface,
options: BingProviderOptionsInterface = defaultBingProviderOptions
) {
this.externalLoader = _externalLoader;
this.options = { ...defaultBingProviderOptions, ...options };
if (!this.options.apiKey) {
throw new Error(
'An API key is required for the Bing provider. Please add it in the "apiKey" option.'
);
}
}
public geocode(
query: string | GeocodeQuery | GeocodeQueryObject
): Promise<BingGeocoded[]>;
public geocode(
query: string | GeocodeQuery | GeocodeQueryObject,
callback: BingGeocodedResultsCallback,
errorCallback?: ErrorCallback
): void;
public geocode(
query: string | GeocodeQuery | GeocodeQueryObject,
callback?: BingGeocodedResultsCallback,
errorCallback?: ErrorCallback
): void | Promise<BingGeocoded[]> {
const geocodeQuery = ProviderHelpers.getGeocodeQueryFromParameter(query);
if (geocodeQuery.getIp()) {
throw new Error(
"The Bing provider does not support IP geolocation, only location geocoding."
);
}
this.externalLoader.setOptions({
protocol: this.options.useSsl ? "https" : "http",
host: "dev.virtualearth.net",
pathname: `REST/v1/Locations/${geocodeQuery.getText()}`,
});
const params: BingRequestParams = this.withCommonParams(
{
maxRes: geocodeQuery.getLimit()
? geocodeQuery.getLimit().toString()
: undefined,
},
geocodeQuery
);
if (!callback) {
return new Promise((resolve, reject) =>
this.executeRequest(
params,
(results) => resolve(results),
{},
{},
(error) => reject(error)
)
);
}
return this.executeRequest(params, callback, {}, {}, errorCallback);
}
public geodecode(
query: ReverseQuery | ReverseQueryObject
): Promise<BingGeocoded[]>;
public geodecode(
query: ReverseQuery | ReverseQueryObject,
callback: BingGeocodedResultsCallback,
errorCallback?: ErrorCallback
): void;
public geodecode(
latitude: number | string,
longitude: number | string
): Promise<BingGeocoded[]>;
public geodecode(
latitude: number | string,
longitude: number | string,
callback: BingGeocodedResultsCallback,
errorCallback?: ErrorCallback
): void;
public geodecode(
latitudeOrQuery: number | string | ReverseQuery | ReverseQueryObject,
longitudeOrCallback?: number | string | BingGeocodedResultsCallback,
callbackOrErrorCallback?: BingGeocodedResultsCallback | ErrorCallback,
errorCallback?: ErrorCallback
): void | Promise<BingGeocoded[]> {
const reverseQuery = ProviderHelpers.getReverseQueryFromParameters(
latitudeOrQuery,
longitudeOrCallback
);
const reverseCallback = ProviderHelpers.getCallbackFromParameters(
longitudeOrCallback,
callbackOrErrorCallback
);
const reverseErrorCallback = ProviderHelpers.getErrorCallbackFromParameters(
longitudeOrCallback,
callbackOrErrorCallback,
errorCallback
);
this.externalLoader.setOptions({
protocol: this.options.useSsl ? "https" : "http",
host: "dev.virtualearth.net",
pathname: `REST/v1/Locations/${reverseQuery.getCoordinates().latitude},${
reverseQuery.getCoordinates().longitude
}`,
});
const params: BingRequestParams = this.withCommonParams({}, reverseQuery);
if (!reverseCallback) {
return new Promise((resolve, reject) =>
this.executeRequest(
params,
(results) => resolve(results),
{},
{},
(error) => reject(error)
)
);
}
return this.executeRequest(
params,
reverseCallback,
{},
{},
reverseErrorCallback
);
}
private withCommonParams(
params: Pick<BingRequestParams, "maxRes">,
query: GeocodeQuery | ReverseQuery
): BingRequestParams {
return {
...params,
key: this.options.apiKey,
incl: "ciso2",
c: query.getLocale(),
jsonpCallback: this.options.useJsonp ? "jsonp" : undefined,
};
}
public executeRequest(
params: ExternalLoaderParams,
callback: BingGeocodedResultsCallback,
headers?: ExternalLoaderHeaders,
body?: ExternalLoaderBody,
errorCallback?: ErrorCallback
): void {
this.externalLoader.executeRequest(
params,
(data: BingResponse) => {
callback(
data.resourceSets[0].resources.map((result: BingResult) =>
BingProvider.mapToGeocoded(result, data.copyright)
)
);
},
headers,
body,
(error) => {
const response = <Response>error.getResponse();
response.json().then((data: BingResponse) => {
const errorMessage =
data.errorDetails && data.errorDetails.length > 0
? data.errorDetails[0]
: data.statusDescription || "";
if (errorCallback) {
errorCallback(new ResponseError(errorMessage, data));
return;
}
setTimeout(() => {
throw new Error(errorMessage);
});
});
}
);
}
public static mapToGeocoded(
result: BingResult,
attribution?: string
): BingGeocoded {
const latitude = result.point.coordinates[0];
const longitude = result.point.coordinates[1];
const { formattedAddress } = result.address;
const streetName = result.address.addressLine;
const { locality, postalCode } = result.address;
const region = result.address.adminDistrict;
const country = result.address.countryRegion;
const countryCode = result.address.countryRegionIso2;
const precision = result.confidence;
let geocoded = BingGeocoded.create({
coordinates: {
latitude,
longitude,
},
formattedAddress,
streetName,
locality,
postalCode,
region,
country,
countryCode,
attribution,
precision,
});
geocoded = <BingGeocoded>geocoded.withBounds({
latitudeSW: result.bbox[0],
longitudeSW: result.bbox[1],
latitudeNE: result.bbox[2],
longitudeNE: result.bbox[3],
});
const adminLevels: ("adminDistrict" | "adminDistrict2")[] = [
"adminDistrict",
"adminDistrict2",
];
adminLevels.forEach((adminLevel, level) => {
if (result.address[adminLevel]) {
geocoded.addAdminLevel(
AdminLevel.create({
level: level + 1,
name: result.address[adminLevel] || "",
})
);
}
});
return geocoded;
}
}