universal-geocoder
Version:
Universal geocoding abstraction server-side and client-side with multiple built-in providers
439 lines (404 loc) • 11.9 kB
text/typescript
import {
ExternalLoaderBody,
ExternalLoaderHeaders,
ExternalLoaderInterface,
ExternalLoaderParams,
} from "ExternalLoader";
import {
ErrorCallback,
GeocodedResultsCallback,
MapQuestGeocoded,
MapQuestGeocodeQuery,
MapQuestGeocodeQueryObject,
ProviderHelpers,
ProviderInterface,
ProviderOptionsInterface,
defaultProviderOptions,
} from "provider";
import { ReverseQuery, ReverseQueryObject } from "query";
import AdminLevel, { ADMIN_LEVEL_CODES } from "AdminLevel";
import { ResponseError } from "error";
interface MapQuestRequestParams {
[param: string]: string | undefined;
readonly key: string;
readonly location?: string;
readonly boundingBox?: string;
readonly maxResults?: string;
readonly jsonpCallback?: string;
}
export interface MapQuestCoordinates {
lat: number;
lng: number;
}
export interface MapQuestResult {
latLng: MapQuestCoordinates;
displayLatLng: MapQuestCoordinates;
street: string;
sideOfStreet: string;
adminArea1?: string;
adminArea1Type?: string;
adminArea3?: string;
adminArea3Type?: string;
adminArea4?: string;
adminArea4Type?: string;
adminArea5?: string;
adminArea5Type?: string;
adminArea6?: string;
adminArea6Type?: string;
postalCode: string;
type: "s" | "v";
linkId: string;
dragPoint: boolean;
geocodeQuality:
| "POINT"
| "ADDRESS"
| "INTERSECTION"
| "STREET"
| "COUNTRY"
| "STATE"
| "COUNTY"
| "CITY"
| "NEIGHBORHOOD"
| "ZIP"
| "ZIP_EXTENDED";
geocodeQualityCode: string;
mapUrl: string;
}
export interface MapQuestResponse {
info: {
statuscode: 0 | 400 | 403 | 500;
copyright: {
text: string;
imageUrl: string;
imageAltText: string;
};
messages: string[];
};
results: {
providedLocation: {
location?: string;
latLng?: MapQuestCoordinates;
};
locations: MapQuestResult[];
}[];
}
export interface MapQuestProviderOptionsInterface
extends ProviderOptionsInterface {
readonly apiKey: string;
readonly method?: "GET" | "POST";
readonly source?: "nominatim" | "mapquest";
}
export const defaultMapQuestProviderOptions: MapQuestProviderOptionsInterface =
{
...defaultProviderOptions,
apiKey: "",
method: "GET",
source: "mapquest",
};
type MapQuestGeocodedResultsCallback =
GeocodedResultsCallback<MapQuestGeocoded>;
export default class MapQuestProvider
implements ProviderInterface<MapQuestGeocoded>
{
private externalLoader: ExternalLoaderInterface;
private options: MapQuestProviderOptionsInterface;
public constructor(
_externalLoader: ExternalLoaderInterface,
options: MapQuestProviderOptionsInterface = defaultMapQuestProviderOptions
) {
this.externalLoader = _externalLoader;
this.options = { ...defaultMapQuestProviderOptions, ...options };
if (!this.options.apiKey) {
throw new Error(
'An API key is required for the MapQuest provider. Please add it in the "apiKey" option.'
);
}
if (!["GET", "POST"].includes(this.options.method || "")) {
throw new Error('The "method" option must either be "GET" or "POST".');
}
if (!["mapquest", "nominatim"].includes(this.options.source || "")) {
throw new Error(
'The "source" option must either be "mapquest" or "nominatim".'
);
}
}
public geocode(
query: string | MapQuestGeocodeQuery | MapQuestGeocodeQueryObject
): Promise<MapQuestGeocoded[]>;
public geocode(
query: string | MapQuestGeocodeQuery | MapQuestGeocodeQueryObject,
callback: MapQuestGeocodedResultsCallback,
errorCallback?: ErrorCallback
): void;
public geocode(
query: string | MapQuestGeocodeQuery | MapQuestGeocodeQueryObject,
callback?: MapQuestGeocodedResultsCallback,
errorCallback?: ErrorCallback
): void | Promise<MapQuestGeocoded[]> {
const geocodeQuery = ProviderHelpers.getGeocodeQueryFromParameter(
query,
MapQuestGeocodeQuery
);
if (geocodeQuery.getIp()) {
throw new Error(
"The MapQuest provider does not support IP geolocation, only location geocoding."
);
}
this.externalLoader.setOptions({
method: this.options.method,
protocol: this.options.useSsl ? "https" : "http",
host:
this.options.source === "nominatim"
? "open.mapquestapi.com"
: "www.mapquestapi.com",
pathname: "geocoding/v1/address",
});
let requestParams: {
location?: string;
boundingBox?: string;
maxResults?: string;
} = {
boundingBox: geocodeQuery.getBounds()
? `${geocodeQuery.getBounds()?.latitudeNE},${
geocodeQuery.getBounds()?.longitudeSW
},${geocodeQuery.getBounds()?.latitudeSW},${
geocodeQuery.getBounds()?.longitudeNE
}`
: undefined,
maxResults: geocodeQuery.getLimit().toString(),
};
if ((<MapQuestGeocodeQuery>geocodeQuery).getLocation()) {
requestParams = {
...(<MapQuestGeocodeQuery>geocodeQuery).getLocation(),
...requestParams,
};
} else {
requestParams = {
location: geocodeQuery.getText(),
...requestParams,
};
}
requestParams = this.options.method === "GET" ? requestParams : {};
const params: MapQuestRequestParams = this.withCommonParams(requestParams);
const body =
this.options.method === "POST"
? {
location: (<MapQuestGeocodeQuery>geocodeQuery).getLocation()
? <ExternalLoaderBody>(
(<MapQuestGeocodeQuery>geocodeQuery).getLocation()
)
: geocodeQuery.getText(),
options: {
boundingBox: geocodeQuery.getBounds()
? {
ul: {
lat: geocodeQuery.getBounds()?.latitudeNE,
lng: geocodeQuery.getBounds()?.longitudeSW,
},
lr: {
lat: geocodeQuery.getBounds()?.latitudeSW,
lng: geocodeQuery.getBounds()?.longitudeNE,
},
}
: undefined,
maxResults: geocodeQuery.getLimit().toString(),
},
}
: {};
if (!callback) {
return new Promise((resolve, reject) =>
this.executeRequest(
params,
(results) => resolve(results),
{},
body,
(error) => reject(error)
)
);
}
return this.executeRequest(params, callback, {}, body, errorCallback);
}
public geodecode(
query: ReverseQuery | ReverseQueryObject
): Promise<MapQuestGeocoded[]>;
public geodecode(
query: ReverseQuery | ReverseQueryObject,
callback: MapQuestGeocodedResultsCallback,
errorCallback?: ErrorCallback
): void;
public geodecode(
latitude: number | string,
longitude: number | string
): Promise<MapQuestGeocoded[]>;
public geodecode(
latitude: number | string,
longitude: number | string,
callback: MapQuestGeocodedResultsCallback,
errorCallback?: ErrorCallback
): void;
public geodecode(
latitudeOrQuery: number | string | ReverseQuery | ReverseQueryObject,
longitudeOrCallback?: number | string | MapQuestGeocodedResultsCallback,
callbackOrErrorCallback?: MapQuestGeocodedResultsCallback | ErrorCallback,
errorCallback?: ErrorCallback
): void | Promise<MapQuestGeocoded[]> {
const reverseQuery = ProviderHelpers.getReverseQueryFromParameters(
latitudeOrQuery,
longitudeOrCallback
);
const reverseCallback = ProviderHelpers.getCallbackFromParameters(
longitudeOrCallback,
callbackOrErrorCallback
);
const reverseErrorCallback = ProviderHelpers.getErrorCallbackFromParameters(
longitudeOrCallback,
callbackOrErrorCallback,
errorCallback
);
this.externalLoader.setOptions({
method: this.options.method,
protocol: this.options.useSsl ? "https" : "http",
host:
this.options.source === "nominatim"
? "open.mapquestapi.com"
: "www.mapquestapi.com",
pathname: "geocoding/v1/reverse",
});
const requestParams =
this.options.method === "GET"
? {
location: `${reverseQuery.getCoordinates().latitude},${
reverseQuery.getCoordinates().longitude
}`,
}
: {};
const params: MapQuestRequestParams = this.withCommonParams(requestParams);
const body =
this.options.method === "POST"
? {
location: {
latLng: {
lat: reverseQuery.getCoordinates().latitude,
lng: reverseQuery.getCoordinates().longitude,
},
},
}
: {};
if (!reverseCallback) {
return new Promise((resolve, reject) =>
this.executeRequest(
params,
(results) => resolve(results),
{},
body,
(error) => reject(error)
)
);
}
return this.executeRequest(
params,
reverseCallback,
{},
body,
reverseErrorCallback
);
}
private withCommonParams(
params: Pick<
MapQuestRequestParams,
"location" | "boundingBox" | "maxResults"
>
): MapQuestRequestParams {
return {
...params,
key: this.options.apiKey || "",
jsonpCallback: this.options.useJsonp ? "callback" : undefined,
};
}
public executeRequest(
params: ExternalLoaderParams,
callback: MapQuestGeocodedResultsCallback,
headers?: ExternalLoaderHeaders,
body?: ExternalLoaderBody,
errorCallback?: ErrorCallback
): void {
this.externalLoader.executeRequest(
params,
(data: MapQuestResponse) => {
if (data.info.statuscode !== 0) {
const errorMessage = `An error has occurred (${
data.info.statuscode
}): ${data.info.messages.join(" / ")}`;
if (errorCallback) {
errorCallback(new ResponseError(errorMessage, data));
return;
}
setTimeout(() => {
throw new Error(errorMessage);
});
return;
}
callback(
data.results[0].locations.map((result: MapQuestResult) =>
MapQuestProvider.mapToGeocoded(result, data.info.copyright.text)
)
);
},
headers,
body,
errorCallback
);
}
public static mapToGeocoded(
result: MapQuestResult,
attribution?: string
): MapQuestGeocoded {
const latitude = result.latLng.lat;
const longitude = result.latLng.lng;
const streetName = result.street;
const subLocality = result.adminArea6;
const locality = result.adminArea5;
const { postalCode } = result;
const region = result.adminArea4;
const country = result.adminArea1;
const countryCode = result.adminArea1;
const precision = result.geocodeQuality;
const precisionCode = result.geocodeQualityCode;
const { mapUrl } = result;
const geocoded = MapQuestGeocoded.create({
coordinates: {
latitude,
longitude,
},
streetName,
subLocality,
locality,
postalCode,
region,
country,
countryCode,
attribution,
precision,
precisionCode,
mapUrl,
});
if (result.adminArea3) {
geocoded.addAdminLevel(
AdminLevel.create({
level: ADMIN_LEVEL_CODES.STATE_CODE,
name: result.adminArea3,
code: result.adminArea3.length === 2 ? result.adminArea3 : undefined,
})
);
}
if (result.adminArea4) {
geocoded.addAdminLevel(
AdminLevel.create({
level: ADMIN_LEVEL_CODES.COUNTY_CODE,
name: result.adminArea4,
})
);
}
return geocoded;
}
}