UNPKG

universal-geocoder

Version:

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

438 lines (400 loc) 11.7 kB
import { ExternalLoaderBody, ExternalLoaderHeaders, ExternalLoaderInterface, ExternalLoaderParams, } from "ExternalLoader"; import { ErrorCallback, GeocodedResultsCallback, ProviderHelpers, ProviderInterface, ProviderOptionsInterface, YandexGeocoded, YandexGeocodeQuery, YandexGeocodeQueryObject, YandexReverseQuery, YandexReverseQueryObject, defaultProviderOptions, } from "provider"; import AdminLevel from "AdminLevel"; import { flattenObject } from "utils"; export type YandexKind = | "house" | "street" | "metro" | "district" | "locality" | "area" | "province" | "country" | "region" | "hydro" | "railway_station" | "station" | "route" | "vegetation" | "airport" | "entrance" | "other"; export type YandexPrecision = | "exact" | "number" | "near" | "range" | "street" | "other"; interface YandexRequestParams { [param: string]: string | undefined; readonly apikey?: string; readonly geocode: string; readonly format: string; readonly lang?: string; readonly kind?: YandexKind; readonly rspn?: "0" | "1"; readonly ll?: string; readonly spn?: string; readonly bbox?: string; readonly skip?: string; readonly results?: string; readonly jsonpCallback?: string; } export interface YandexResult { metaDataProperty: { GeocoderMetaData: { kind: YandexKind; text: string; precision: YandexPrecision; AddressDetails: { Country: { AddressLine: string; CountryNameCode: string; CountryName: string; AdministrativeArea?: { AdministrativeAreaName: string; SubAdministrativeArea?: { SubAdministrativeAreaName: string; Locality?: { LocalityName: string; Thoroughfare?: { ThoroughfareName: string; Premise: { PremiseNumber: string; }; }; }; }; }; }; }; }; }; description: string; name: string; boundedBy: { Envelope: { lowerCorner: string; upperCorner: string; }; }; Point: { pos: string; }; } export interface YandexResponse { response: { GeoObjectCollection: { metaDataProperty: { GeocoderResponseMetaData: { request: string; suggest?: { fix: string; }; found: string; results: string; skip: string; }; }; featureMember: { GeoObject: YandexResult; }[]; }; }; } interface YandexFlattenedAddressDetails { CountryNameCode?: string; CountryName?: string; AdministrativeAreaName?: string; SubAdministrativeAreaName?: string; LocalityName?: string; DependentLocalityName?: string; ThoroughfareName?: string; PremiseNumber?: string; } export interface YandexProviderOptionsInterface extends ProviderOptionsInterface { readonly apiKey: string; } export const defaultYandexProviderOptions = { ...defaultProviderOptions, apiKey: "", }; type YandexGeocodedResultsCallback = GeocodedResultsCallback<YandexGeocoded>; export default class YandexProvider implements ProviderInterface<YandexGeocoded> { private externalLoader: ExternalLoaderInterface; private options: YandexProviderOptionsInterface; public constructor( _externalLoader: ExternalLoaderInterface, options: YandexProviderOptionsInterface = defaultYandexProviderOptions ) { this.externalLoader = _externalLoader; this.options = { ...defaultYandexProviderOptions, ...options }; if (!this.options.apiKey) { throw new Error( 'An API key is required for the Yandex provider. Please add it in the "apiKey" option.' ); } } public geocode( query: string | YandexGeocodeQuery | YandexGeocodeQueryObject ): Promise<YandexGeocoded[]>; public geocode( query: string | YandexGeocodeQuery | YandexGeocodeQueryObject, callback: YandexGeocodedResultsCallback, errorCallback?: ErrorCallback ): void; public geocode( query: string | YandexGeocodeQuery | YandexGeocodeQueryObject, callback?: YandexGeocodedResultsCallback, errorCallback?: ErrorCallback ): void | Promise<YandexGeocoded[]> { const geocodeQuery = ProviderHelpers.getGeocodeQueryFromParameter( query, YandexGeocodeQuery ); if (geocodeQuery.getIp()) { throw new Error( "The Yandex provider does not support IP geolocation, only location geocoding." ); } this.externalLoader.setOptions({ protocol: this.options.useSsl ? "https" : "http", host: "geocode-maps.yandex.ru", pathname: "1.x", }); let rspn: "0" | "1" | undefined; if ((<YandexGeocodeQuery>geocodeQuery).getBounded() === false) { rspn = "0"; } else if ((<YandexGeocodeQuery>geocodeQuery).getBounded() === true) { rspn = "1"; } const params: YandexRequestParams = this.withCommonParams( { geocode: geocodeQuery.getText() || "", rspn, ll: (<YandexGeocodeQuery>geocodeQuery).getProximity() ? `${(<YandexGeocodeQuery>geocodeQuery).getProximity()?.longitude},${ (<YandexGeocodeQuery>geocodeQuery).getProximity()?.latitude }` : undefined, spn: (<YandexGeocodeQuery>geocodeQuery).getSpan() ? `${(<YandexGeocodeQuery>geocodeQuery).getSpan()?.spanLongitude},${ (<YandexGeocodeQuery>geocodeQuery).getSpan()?.spanLatitude }` : undefined, bbox: geocodeQuery.getBounds() ? `${geocodeQuery.getBounds()?.longitudeSW},${ geocodeQuery.getBounds()?.latitudeSW }~${geocodeQuery.getBounds()?.longitudeNE},${ geocodeQuery.getBounds()?.latitudeNE }` : undefined, }, <YandexGeocodeQuery>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: YandexReverseQuery | YandexReverseQueryObject ): Promise<YandexGeocoded[]>; public geodecode( query: YandexReverseQuery | YandexReverseQueryObject, callback: YandexGeocodedResultsCallback, errorCallback?: ErrorCallback ): void; public geodecode( latitude: number | string, longitude: number | string ): Promise<YandexGeocoded[]>; public geodecode( latitude: number | string, longitude: number | string, callback: YandexGeocodedResultsCallback, errorCallback?: ErrorCallback ): void; public geodecode( latitudeOrQuery: | number | string | YandexReverseQuery | YandexReverseQueryObject, longitudeOrCallback?: number | string | YandexGeocodedResultsCallback, callbackOrErrorCallback?: YandexGeocodedResultsCallback | ErrorCallback, errorCallback?: ErrorCallback ): void | Promise<YandexGeocoded[]> { const reverseQuery = ProviderHelpers.getReverseQueryFromParameters( latitudeOrQuery, longitudeOrCallback, YandexReverseQuery ); const reverseCallback = ProviderHelpers.getCallbackFromParameters( longitudeOrCallback, callbackOrErrorCallback ); const reverseErrorCallback = ProviderHelpers.getErrorCallbackFromParameters( longitudeOrCallback, callbackOrErrorCallback, errorCallback ); this.externalLoader.setOptions({ protocol: this.options.useSsl ? "https" : "http", host: "geocode-maps.yandex.ru", pathname: "1.x", }); const params: YandexRequestParams = this.withCommonParams( { geocode: `${reverseQuery.getCoordinates().longitude},${ reverseQuery.getCoordinates().latitude }`, kind: (<YandexReverseQuery>reverseQuery).getTypes() ? (<YandexReverseQuery>reverseQuery).getTypes()?.[0] : undefined, }, <YandexReverseQuery>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< YandexRequestParams, "geocode" | "rspn" | "ll" | "spn" | "bbox" | "kind" >, query: YandexGeocodeQuery | YandexReverseQuery ): YandexRequestParams { return { ...params, apikey: this.options.apiKey, format: "json", lang: query.getLocale(), results: query.getLimit().toString(), skip: query.getSkip()?.toString(), jsonpCallback: this.options.useJsonp ? "callback" : undefined, }; } public executeRequest( params: ExternalLoaderParams, callback: YandexGeocodedResultsCallback, headers?: ExternalLoaderHeaders, body?: ExternalLoaderBody, errorCallback?: ErrorCallback ): void { this.externalLoader.executeRequest( params, (data: YandexResponse) => { callback( data.response.GeoObjectCollection.featureMember.map((result) => YandexProvider.mapToGeocoded(result.GeoObject) ) ); }, headers, body, errorCallback ); } public static mapToGeocoded(result: YandexResult): YandexGeocoded { const point = result.Point.pos.split(" "); const latitude = parseFloat(point[1]); const longitude = parseFloat(point[0]); const addressDetails: YandexFlattenedAddressDetails = flattenObject( result.metaDataProperty.GeocoderMetaData.AddressDetails ); const streetNumber = addressDetails.PremiseNumber; const streetName = addressDetails.ThoroughfareName; const subLocality = addressDetails.DependentLocalityName; const locality = addressDetails.LocalityName; const region = addressDetails.AdministrativeAreaName; const country = addressDetails.CountryName; const countryCode = addressDetails.CountryNameCode; const types = [result.metaDataProperty.GeocoderMetaData.kind]; const { precision } = result.metaDataProperty.GeocoderMetaData; let geocoded = YandexGeocoded.create({ coordinates: { latitude, longitude, }, streetNumber, streetName, subLocality, locality, region, country, countryCode, types, precision, }); const adminLevels: ( | "AdministrativeAreaName" | "SubAdministrativeAreaName" )[] = ["AdministrativeAreaName", "SubAdministrativeAreaName"]; adminLevels.forEach((adminLevel, level) => { if (addressDetails[adminLevel]) { geocoded.addAdminLevel( AdminLevel.create({ level: level + 1, name: addressDetails[adminLevel] || "", }) ); } }); const lowerCorner = result.boundedBy.Envelope.lowerCorner.split(" "); const upperCorner = result.boundedBy.Envelope.upperCorner.split(" "); geocoded = <YandexGeocoded>geocoded.withBounds({ latitudeSW: parseFloat(lowerCorner[1]), longitudeSW: parseFloat(lowerCorner[0]), latitudeNE: parseFloat(upperCorner[1]), longitudeNE: parseFloat(upperCorner[0]), }); return geocoded; } }