diff --git a/packages/arcgis-rest-geocoder/README.md b/packages/arcgis-rest-geocoder/README.md index c61ae3b4e1..e095db680c 100644 --- a/packages/arcgis-rest-geocoder/README.md +++ b/packages/arcgis-rest-geocoder/README.md @@ -23,12 +23,21 @@ import { geocode } from '@esri/arcgis-rest-geocoder'; geocode("LAX") .then((response) => { - response.candidates[0].location; // => { x: -118.409, y: 33.943, spatialReference: { wkid: 4326 } } + response.candidates[0].location; + // => { x: -118.409, y: 33.943 } }); ``` ### [API Reference](https://esri.github.io/arcgis-rest-js/api/geocoder/) +* [`geocode("1 World Way Los Angeles 90045")`](https://esri.github.io/arcgis-rest-js/api/geocoder/geocode/) + +* [`suggest("Starb")`](https://esri.github.io/arcgis-rest-js/api/geocoder/suggest/) + +* [`reverseGeocode([-118.409,33.943 ])`](https://esri.github.io/arcgis-rest-js/api/geocoder/reverseGeocode/) + +* [`bulkGeocode()`](https://esri.github.io/arcgis-rest-js/api/geocoder/bulkGeocode/) + ### Issues If something isn't working the way you expected, please take a look at [previously logged issues](https://github.com/Esri/arcgis-rest-js/issues) first. Have you found a new bug? Want to request a new feature? We'd [**love**](https://github.com/Esri/arcgis-rest-js/issues/new) to hear from you. diff --git a/packages/arcgis-rest-geocoder/src/bulk.ts b/packages/arcgis-rest-geocoder/src/bulk.ts new file mode 100644 index 0000000000..cd17a7597a --- /dev/null +++ b/packages/arcgis-rest-geocoder/src/bulk.ts @@ -0,0 +1,102 @@ +/* Copyright (c) 2017 Environmental Systems Research Institute, Inc. + * Apache-2.0 */ + +import { request } from "@esri/arcgis-rest-request"; + +import { ISpatialReference, IPoint } from "@esri/arcgis-rest-common-types"; + +import { worldGeocoder, IEndpointRequestOptions } from "./helpers"; + +// it'd be better if doc didnt display these properties in alphabetical order +export interface IAddressBulk { + /** + * A unique id must be passed along for each individual address. + */ + OBJECTID: number; + address?: string; + address2?: string; + address3?: string; + neighborhood?: string; + city?: string; + subregion?: string; + /** + * The World Geocoding Service considers US states regions. + */ + region?: string; + postal?: number; + postalExt?: number; + countryCode?: string; +} + +export interface IBulkGeocodeRequestOptions extends IEndpointRequestOptions { + addresses: IAddressBulk[]; +} + +export interface IBulkGeocodeResponse { + spatialReference: ISpatialReference; + locations: Array<{ + address: string; + location: IPoint; + score: number; + attributes: object; + }>; +} + +/** + * Used to geocode a batch of addresses + * + * ```js + * import { bulkGeocode } from '@esri/arcgis-rest-geocoder'; + * import { ApplicationSession } from '@esri/arcgis-rest-auth'; + * + * const addresses = [ + * { "OBJECTID": 1, "SingleLine": "380 New York Street 92373" }, + * { "OBJECTID": 2, "SingleLine": "1 World Way Los Angeles 90045" } + * ]; + * + * bulkGeocode({ addresses, authentication: session }) + * .then((response) => { + * response.locations[0].location; // => { x: -117, y: 34, spatialReference: { wkid: 4326 } } + * }); + * ``` + * + * @param requestOptions - Request options to pass to the geocoder, including an array of addresses and authentication session. + * @returns A Promise that will resolve with the data from the response. + */ +export function bulkGeocode( + requestOptions: IBulkGeocodeRequestOptions // must POST +) { + const options: IBulkGeocodeRequestOptions = { + endpoint: worldGeocoder, + params: { + forStorage: true, + addresses: { records: [] } + }, + ...requestOptions + }; + + requestOptions.addresses.forEach(address => { + options.params.addresses.records.push({ attributes: address }); + }); + + // the SAS service doesnt support anonymous requests + if (!requestOptions.authentication && options.endpoint === worldGeocoder) { + return Promise.reject( + "bulk geocoding using the ArcGIS service requires authentication" + ); + } + + return request(options.endpoint + "geocodeAddresses", options).then( + response => { + const sr = response.spatialReference; + response.locations.forEach(function(address: { location: IPoint }) { + address.location.spatialReference = sr; + }); + return response; + } + ); +} + +export default { + bulkGeocode +}; diff --git a/packages/arcgis-rest-geocoder/src/geocode.ts b/packages/arcgis-rest-geocoder/src/geocode.ts new file mode 100644 index 0000000000..229ff151d7 --- /dev/null +++ b/packages/arcgis-rest-geocoder/src/geocode.ts @@ -0,0 +1,117 @@ +/* Copyright (c) 2017 Environmental Systems Research Institute, Inc. + * Apache-2.0 */ + +import { request, IParams } from "@esri/arcgis-rest-request"; + +import { + IExtent, + ISpatialReference, + IPoint +} from "@esri/arcgis-rest-common-types"; + +import { worldGeocoder, IEndpointRequestOptions } from "./helpers"; + +export interface IGeocodeParams extends IParams { + /** + * You can create an autocomplete experience by making a call to suggest with partial text and then passing through the magicKey and complete address that are returned to geocode. + * ```js + * import { suggest, geocode } from '@esri/arcgis-rest-geocoder'; + * suggest("LAX") + * .then((response) => { + * response.suggestions[2].magicKey; // => "dHA9MCNsb2M9Mjk3ODc2MCNsbmc9MzMjcGw9ODkxNDg4I2xicz0xNDoxNDc4MTI1MA==" + * }); + * geocode("LAX, 1 World Way, Los Angeles, CA, 90045, USA", {magicKey: "dHA9MCN..."}) + * ``` + */ + magicKey?: string; +} + +export interface IGeocodeRequestOptions extends IEndpointRequestOptions { + address?: string; + address2?: string; + address3?: string; + neighborhood?: string; + city?: string; + subregion?: string; + /** + * The World Geocoding Service expects US states to be passed in as a 'region'. + */ + region?: string; + postal?: number; + postalExt?: number; + countryCode?: string; +} + +export interface IGeocodeResponse { + spatialReference: ISpatialReference; + candidates: Array<{ + address: string; + location: IPoint; + extent: IExtent; + attributes: object; + }>; +} + +/** + * Used to determine the location of a single address or point of interest + * + * ```js + * import { geocode } from '@esri/arcgis-rest-geocoder'; + * + * geocode("LAX") + * .then((response) => { + * response.candidates[0].location; // => { x: -118.409, y: 33.943, spatialReference: { wkid: 4326 } } + * }); + * + * geocode({ + * params: { + * address: "1600 Pennsylvania Ave", + * postal: 20500, + * countryCode: "USA" + * } + * }) + * .then((response) => { + * response.candidates[0].location; // => { x: -77.036533, y: 38.898719, spatialReference: { wkid: 4326 } } + * }); + * ``` + * + * @param address String representing the address or point of interest or RequestOptions to pass to the endpoint. + * @returns A Promise that will resolve with address candidates for the request. + */ +export function geocode( + address: string | IGeocodeRequestOptions +): Promise { + let options: IGeocodeRequestOptions = { + endpoint: worldGeocoder, + params: {} + }; + + if (typeof address === "string") { + options.params.singleLine = address; + } else { + options.endpoint = address.endpoint || worldGeocoder; + options = { + ...options, + ...address + }; + } + + // add spatialReference property to individual matches + return request(options.endpoint + "findAddressCandidates", options).then( + response => { + const sr = response.spatialReference; + response.candidates.forEach(function(candidate: { + location: IPoint; + extent: IExtent; + }) { + candidate.location.spatialReference = sr; + candidate.extent.spatialReference = sr; + }); + return response; + } + ); +} + +export default { + geocode +}; diff --git a/packages/arcgis-rest-geocoder/src/geocoder.ts b/packages/arcgis-rest-geocoder/src/geocoder.ts deleted file mode 100644 index 89b92d178a..0000000000 --- a/packages/arcgis-rest-geocoder/src/geocoder.ts +++ /dev/null @@ -1,424 +0,0 @@ -/* Copyright (c) 2017 Environmental Systems Research Institute, Inc. - * Apache-2.0 */ - -import { - request, - IRequestOptions, - IParams, - warn -} from "@esri/arcgis-rest-request"; -// import { IAuthenticatedRequestOptions } from "@esri/arcgis-rest-auth"; - -import { - IExtent, - ISpatialReference, - IPoint -} from "@esri/arcgis-rest-common-types"; - -// https always -const worldGeocoder = - "https://geocode.arcgis.com/arcgis/rest/services/World/GeocodeServer/"; - -// it'd be better if doc didnt display these properties in alphabetical order -export interface IAddressBulk { - /** - * A unique id must be passed along for each individual address. - */ - OBJECTID: number; - address?: string; - address2?: string; - address3?: string; - neighborhood?: string; - city?: string; - subregion?: string; - /** - * The World Geocoding Service considers US states regions. - */ - region?: string; - postal?: number; - postalExt?: number; - countryCode?: string; -} - -export interface ILocation { - latitude?: number; - longitude?: number; - lat?: number; - long?: number; -} - -function isLocationArray( - coords: ILocation | IPoint | [number, number] -): coords is [number, number] { - return (coords as [number, number]).length === 2; -} - -function isLocation( - coords: ILocation | IPoint | [number, number] -): coords is ILocation { - return ( - (coords as ILocation).latitude !== undefined || - (coords as ILocation).lat !== undefined - ); -} - -export interface IGeocodeParams extends IParams { - /** - * You can create an autocomplete experience by making a call to suggest with partial text and then passing through the magicKey and complete address that are returned to geocode. - * ```js - * import { suggest, geocode } from '@esri/arcgis-rest-geocoder'; - * suggest("LAX") - * .then((response) => { - * response.suggestions[2].magicKey; // => "dHA9MCNsb2M9Mjk3ODc2MCNsbmc9MzMjcGw9ODkxNDg4I2xicz0xNDoxNDc4MTI1MA==" - * }); - * geocode("LAX, 1 World Way, Los Angeles, CA, 90045, USA", {magicKey: "dHA9MCN..."}) - * ``` - */ - magicKey?: string; -} - -// nice to have: verify custom endpoints contain 'GeocodeServer' and end in a '/' -export interface IEndpointRequestOptions extends IRequestOptions { - /** - * Any ArcGIS Geocoding service (example: http://sampleserver6.arcgisonline.com/arcgis/rest/services/Locators/SanDiego/GeocodeServer ) - */ - endpoint?: string; -} - -export interface IGeocodeRequestOptions extends IEndpointRequestOptions { - address?: string; - address2?: string; - address3?: string; - neighborhood?: string; - city?: string; - subregion?: string; - /** - * The World Geocoding Service expects US states to be passed in as a 'region'. - */ - region?: string; - postal?: number; - postalExt?: number; - countryCode?: string; -} - -export interface ISuggestRequestOptions extends IEndpointRequestOptions { - /** - * You can create an autocomplete experience by making a call to suggest with partial text and then passing through the magicKey and complete address that are returned to geocode. - * ```js - * import { suggest, geocode } from '@esri/arcgis-rest-geocoder'; - * suggest("LAX") - * .then((response) => { - * response.suggestions[2].magicKey; // => "dHA9MCNsb2M9Mjk3ODc2MCNsbmc9MzMjcGw9ODkxNDg4I2xicz0xNDoxNDc4MTI1MA==" - * }); - * geocode("LAX, 1 World Way, Los Angeles, CA, 90045, USA", {magicKey: "dHA9MCN..."}) - * ``` - */ - magicKey?: string; -} - -export interface IBulkGeocodeRequestOptions extends IEndpointRequestOptions { - addresses: IAddressBulk[]; -} - -export interface IGeocodeResponse { - spatialReference: ISpatialReference; - candidates: Array<{ - address: string; - location: IPoint; - extent: IExtent; - attributes: object; - }>; -} - -export interface IReverseGeocodeResponse { - address: { - [key: string]: any; - }; - location: IPoint; -} - -export interface ISuggestResponse { - suggestions: Array<{ - text: string; - magicKey: string; - isCollection: boolean; - }>; -} - -export interface IBulkGeocodeResponse { - spatialReference: ISpatialReference; - locations: Array<{ - address: string; - location: IPoint; - score: number; - attributes: object; - }>; -} - -export interface IGetGeocodeServiceResponse { - currentVersion: number; - serviceDescription: string; - addressFields: any[]; - countries: string[]; - capabilities: string; -} - -/** - * Used to determine the location of a single address or point of interest - * - * ```js - * import { geocode } from '@esri/arcgis-rest-geocoder'; - * - * geocode("LAX") - * .then((response) => { - * response.candidates[0].location; // => { x: -118.409, y: 33.943, spatialReference: { wkid: 4326 } } - * }); - * - * geocode({ - * params: { - * address: "1600 Pennsylvania Ave", - * postal: 20500, - * countryCode: "USA" - * } - * }) - * .then((response) => { - * response.candidates[0].location; // => { x: -77.036533, y: 38.898719, spatialReference: { wkid: 4326 } } - * }); - * ``` - * - * @param address String representing the address or point of interest or RequestOptions to pass to the endpoint. - * @returns A Promise that will resolve with address candidates for the request. - */ -export function geocode( - address: string | IGeocodeRequestOptions -): Promise { - let options: IGeocodeRequestOptions = { - endpoint: worldGeocoder, - params: {} - }; - - if (typeof address === "string") { - options.params.singleLine = address; - } else { - options.endpoint = address.endpoint || worldGeocoder; - options = { - ...options, - ...address - }; - } - - // add spatialReference property to individual matches - return request(options.endpoint + "findAddressCandidates", options).then( - response => { - const sr = response.spatialReference; - response.candidates.forEach(function(candidate: { - location: IPoint; - extent: IExtent; - }) { - candidate.location.spatialReference = sr; - candidate.extent.spatialReference = sr; - }); - return response; - } - ); -} - -/** - * Used to return a placename suggestion for a partial string - * - * ```js - * import { suggest } from '@esri/arcgis-rest-geocoder'; - * - * suggest("Starb") - * .then((response) => { - * response.address.PlaceName; // => "Starbucks" - * }); - * ``` - * - * @param requestOptions - Options for the request including authentication and other optional parameters. - * @returns A Promise that will resolve with the data from the response. - */ -export function suggest( - partialText: string, - requestOptions?: ISuggestRequestOptions -): Promise { - const options: ISuggestRequestOptions = { - endpoint: worldGeocoder, - params: {}, - ...requestOptions - }; - - // is this the most concise way to mixin these optional parameters? - if (requestOptions && requestOptions.params) { - options.params = requestOptions.params; - } - - if (requestOptions && requestOptions.magicKey) { - options.params.magicKey = requestOptions.magicKey; - } - - options.params.text = partialText; - - return request(options.endpoint + "suggest", options); -} - -/** - * Used to determine the address of a location. - * - * ```js - * import { reverseGeocode } from '@esri/arcgis-rest-geocoder'; - * - * // long, lat - * reverseGeocode([-118.409,33.943 ]) - * .then((response) => { - * response.address.PlaceName; // => "LA Airport" - * }); - * - * // or - * reverseGeocode({ long: -118.409, lat: 33.943 }) - * reverseGeocode({ latitude: 33.943, latitude: -118.409 }) - * reverseGeocode({ x: -118.409, y: 33.9425 }) // wgs84 is assumed - * reverseGeocode({ x: -13181226, y: 4021085, spatialReference: { wkid: 3857 }) - * ``` - * - * @param coordinates - the location you'd like to associate an address with. - * @param requestOptions - Additional options for the request including authentication. - * @returns A Promise that will resolve with the data from the response. - */ -export function reverseGeocode( - coords: IPoint | ILocation | [number, number], - requestOptions?: IEndpointRequestOptions -): Promise { - const options: IGeocodeRequestOptions = { - endpoint: worldGeocoder, - params: {}, - ...requestOptions - }; - - if (isLocationArray(coords)) { - options.params.location = coords.join(); - } else if (isLocation(coords)) { - if (coords.lat) { - options.params.location = coords.long + "," + coords.lat; - } - if (coords.latitude) { - options.params.location = coords.longitude + "," + coords.latitude; - } - } else { - // if input is a point, we can pass it straight through, with or without a spatial reference - options.params.location = coords; - } - - return request(options.endpoint + "reverseGeocode", options); -} - -/** - * Used to geocode a batch of addresses - * - * ```js - * import { bulkGeocode } from '@esri/arcgis-rest-geocoder'; - * import { ApplicationSession } from '@esri/arcgis-rest-auth'; - * - * const addresses = [ - * { "OBJECTID": 1, "SingleLine": "380 New York Street 92373" }, - * { "OBJECTID": 2, "SingleLine": "1 World Way Los Angeles 90045" } - * ]; - * - * bulkGeocode({ addresses, authentication: session }) - * .then((response) => { - * response.locations[0].location; // => { x: -117, y: 34, spatialReference: { wkid: 4326 } } - * }); - * ``` - * - * @param requestOptions - Request options to pass to the geocoder, including an array of addresses and authentication session. - * @returns A Promise that will resolve with the data from the response. - */ -export function bulkGeocode( - requestOptions: IBulkGeocodeRequestOptions // must POST -) { - // passing authentication is mandatory - const options: IBulkGeocodeRequestOptions = { - endpoint: worldGeocoder, - ...requestOptions - }; - - requestOptions.params = { - forStorage: true, - addresses: { records: null }, - ...requestOptions.params - }; - - const parsedAddresses: any[] = []; - - requestOptions.addresses.forEach(address => { - parsedAddresses.push({ attributes: address }); - }); - - requestOptions.params.addresses.records = parsedAddresses; - - if (!requestOptions.authentication) { - return Promise.reject("bulk geocoding requires authentication"); - } - - return request(options.endpoint + "geocodeAddresses", requestOptions).then( - response => { - const sr = response.spatialReference; - response.locations.forEach(function(address: { location: IPoint }) { - address.location.spatialReference = sr; - }); - return response; - } - ); -} - -/** - * Used to fetch metadata from a geocoding service. - * - * ```js - * import { getGeocoderServiceInfo } from '@esri/arcgis-rest-geocoder'; - * - * getGeocoderServiceInfo() - * .then((response) => { - * response.serviceDescription; // => 'World Geocoder' - * }); - * ``` - * - * @param requestOptions - Request options can contain a custom geocoding service to fetch metadata from. - * @returns A Promise that will resolve with the data from the response. - */ -export function getGeocodeService( - requestOptions?: IEndpointRequestOptions -): Promise { - const url = (requestOptions && requestOptions.endpoint) || worldGeocoder; - - const options: IEndpointRequestOptions = { - httpMethod: "GET", - maxUrlLength: 2000, - ...requestOptions - }; - - return request(url, options); -} - -/** - * Deprecated. Please use `getGeocodeService()` instead. - * - * @param requestOptions - Request options can contain a custom geocoding service to fetch metadata from. - * @returns A Promise that will resolve with the data from the response. - */ -export function serviceInfo( - requestOptions?: IEndpointRequestOptions -): Promise { - warn( - "serviceInfo() will be deprecated in the next major release. please use getGeocoderServiceInfo() instead." - ); - return getGeocodeService(requestOptions); -} - -export default { - geocode, - suggest, - reverseGeocode, - bulkGeocode, - serviceInfo -}; diff --git a/packages/arcgis-rest-geocoder/src/helpers.ts b/packages/arcgis-rest-geocoder/src/helpers.ts new file mode 100644 index 0000000000..1726f4f7b1 --- /dev/null +++ b/packages/arcgis-rest-geocoder/src/helpers.ts @@ -0,0 +1,81 @@ +/* Copyright (c) 2017 Environmental Systems Research Institute, Inc. + * Apache-2.0 */ + +import { request, IRequestOptions, warn } from "@esri/arcgis-rest-request"; + +import { IPoint } from "@esri/arcgis-rest-common-types"; + +// https always +export const worldGeocoder = + "https://geocode.arcgis.com/arcgis/rest/services/World/GeocodeServer/"; + +export interface ILocation { + latitude?: number; + longitude?: number; + lat?: number; + long?: number; +} + +// nice to have: verify custom endpoints contain 'GeocodeServer' and end in a '/' +export interface IEndpointRequestOptions extends IRequestOptions { + /** + * Any ArcGIS Geocoding service (example: http://sampleserver6.arcgisonline.com/arcgis/rest/services/Locators/SanDiego/GeocodeServer ) + */ + endpoint?: string; +} + +export interface IGetGeocodeServiceResponse { + currentVersion: number; + serviceDescription: string; + addressFields: any[]; + countries: string[]; + capabilities: string; +} + +/** + * Used to fetch metadata from a geocoding service. + * + * ```js + * import { getGeocoderServiceInfo } from '@esri/arcgis-rest-geocoder'; + * + * getGeocoderServiceInfo() + * .then((response) => { + * response.serviceDescription; // => 'World Geocoder' + * }); + * ``` + * + * @param requestOptions - Request options can contain a custom geocoding service to fetch metadata from. + * @returns A Promise that will resolve with the data from the response. + */ +export function getGeocodeService( + requestOptions?: IEndpointRequestOptions +): Promise { + const url = (requestOptions && requestOptions.endpoint) || worldGeocoder; + + const options: IEndpointRequestOptions = { + httpMethod: "GET", + maxUrlLength: 2000, + ...requestOptions + }; + + return request(url, options); +} + +/** + * Deprecated. Please use [`getGeocodeService()`](../getGeocodeService/) instead. + * + * @param requestOptions - Request options can contain a custom geocoding service to fetch metadata from. + * @returns A Promise that will resolve with the data from the response. + */ +export function serviceInfo( + requestOptions?: IEndpointRequestOptions +): Promise { + warn( + "serviceInfo() will be deprecated in the next major release. please use getGeocoderServiceInfo() instead." + ); + return getGeocodeService(requestOptions); +} + +export default { + getGeocodeService +}; diff --git a/packages/arcgis-rest-geocoder/src/index.ts b/packages/arcgis-rest-geocoder/src/index.ts index 72ee8e5779..0b4201ea6b 100644 --- a/packages/arcgis-rest-geocoder/src/index.ts +++ b/packages/arcgis-rest-geocoder/src/index.ts @@ -1 +1,4 @@ -export * from "./geocoder"; +export * from "./geocode"; +export * from "./suggest"; +export * from "./reverse"; +export * from "./bulk"; diff --git a/packages/arcgis-rest-geocoder/src/reverse.ts b/packages/arcgis-rest-geocoder/src/reverse.ts new file mode 100644 index 0000000000..bcd6279059 --- /dev/null +++ b/packages/arcgis-rest-geocoder/src/reverse.ts @@ -0,0 +1,84 @@ +/* Copyright (c) 2017 Environmental Systems Research Institute, Inc. + * Apache-2.0 */ + +import { request } from "@esri/arcgis-rest-request"; + +import { IPoint } from "@esri/arcgis-rest-common-types"; + +import { worldGeocoder, ILocation, IEndpointRequestOptions } from "./helpers"; + +export interface IReverseGeocodeResponse { + address: { + [key: string]: any; + }; + location: IPoint; +} + +function isLocationArray( + coords: ILocation | IPoint | [number, number] +): coords is [number, number] { + return (coords as [number, number]).length === 2; +} + +function isLocation( + coords: ILocation | IPoint | [number, number] +): coords is ILocation { + return ( + (coords as ILocation).latitude !== undefined || + (coords as ILocation).lat !== undefined + ); +} + +/** + * Used to determine the address of a location. + * + * ```js + * import { reverseGeocode } from '@esri/arcgis-rest-geocoder'; + * + * // long, lat + * reverseGeocode([-118.409,33.943 ]) + * .then((response) => { + * response.address.PlaceName; // => "LA Airport" + * }); + * + * // or + * reverseGeocode({ long: -118.409, lat: 33.943 }) + * reverseGeocode({ latitude: 33.943, latitude: -118.409 }) + * reverseGeocode({ x: -118.409, y: 33.9425 }) // wgs84 is assumed + * reverseGeocode({ x: -13181226, y: 4021085, spatialReference: { wkid: 3857 }) + * ``` + * + * @param coordinates - the location you'd like to associate an address with. + * @param requestOptions - Additional options for the request including authentication. + * @returns A Promise that will resolve with the data from the response. + */ +export function reverseGeocode( + coords: IPoint | ILocation | [number, number], + requestOptions?: IEndpointRequestOptions +): Promise { + const options: IEndpointRequestOptions = { + endpoint: worldGeocoder, + params: {}, + ...requestOptions + }; + + if (isLocationArray(coords)) { + options.params.location = coords.join(); + } else if (isLocation(coords)) { + if (coords.lat) { + options.params.location = coords.long + "," + coords.lat; + } + if (coords.latitude) { + options.params.location = coords.longitude + "," + coords.latitude; + } + } else { + // if input is a point, we can pass it straight through, with or without a spatial reference + options.params.location = coords; + } + + return request(options.endpoint + "reverseGeocode", options); +} + +export default { + reverseGeocode +}; diff --git a/packages/arcgis-rest-geocoder/src/suggest.ts b/packages/arcgis-rest-geocoder/src/suggest.ts new file mode 100644 index 0000000000..eda1fcbd58 --- /dev/null +++ b/packages/arcgis-rest-geocoder/src/suggest.ts @@ -0,0 +1,72 @@ +/* Copyright (c) 2017 Environmental Systems Research Institute, Inc. + * Apache-2.0 */ + +import { request } from "@esri/arcgis-rest-request"; + +import { worldGeocoder, IEndpointRequestOptions } from "./helpers"; + +export interface ISuggestRequestOptions extends IEndpointRequestOptions { + /** + * You can create an autocomplete experience by making a call to suggest with partial text and then passing through the magicKey and complete address that are returned to geocode. + * ```js + * import { suggest, geocode } from '@esri/arcgis-rest-geocoder'; + * suggest("LAX") + * .then((response) => { + * response.suggestions[2].magicKey; // => "dHA9MCNsb2M9Mjk3ODc2MCNsbmc9MzMjcGw9ODkxNDg4I2xicz0xNDoxNDc4MTI1MA==" + * }); + * geocode("LAX, 1 World Way, Los Angeles, CA, 90045, USA", {magicKey: "dHA9MCN..."}) + * ``` + */ + magicKey?: string; +} + +export interface ISuggestResponse { + suggestions: Array<{ + text: string; + magicKey: string; + isCollection: boolean; + }>; +} + +/** + * Used to return a placename suggestion for a partial string + * + * ```js + * import { suggest } from '@esri/arcgis-rest-geocoder'; + * + * suggest("Starb") + * .then((response) => { + * response.address.PlaceName; // => "Starbucks" + * }); + * ``` + * + * @param requestOptions - Options for the request including authentication and other optional parameters. + * @returns A Promise that will resolve with the data from the response. + */ +export function suggest( + partialText: string, + requestOptions?: ISuggestRequestOptions +): Promise { + const options: ISuggestRequestOptions = { + endpoint: worldGeocoder, + params: {}, + ...requestOptions + }; + + // is this the most concise way to mixin these optional parameters? + if (requestOptions && requestOptions.params) { + options.params = requestOptions.params; + } + + if (requestOptions && requestOptions.magicKey) { + options.params.magicKey = requestOptions.magicKey; + } + + options.params.text = partialText; + + return request(options.endpoint + "suggest", options); +} + +export default { + suggest +}; diff --git a/packages/arcgis-rest-geocoder/test/geocoder.test.ts b/packages/arcgis-rest-geocoder/test/geocoder.test.ts index 40b1bf4e65..90f5fafba4 100644 --- a/packages/arcgis-rest-geocoder/test/geocoder.test.ts +++ b/packages/arcgis-rest-geocoder/test/geocoder.test.ts @@ -1,11 +1,12 @@ -import { - geocode, - suggest, - reverseGeocode, - bulkGeocode, - serviceInfo, - getGeocodeService -} from "../src/index"; +import { geocode } from "../src/geocode"; + +import { suggest } from "../src/suggest"; + +import { reverseGeocode } from "../src/reverse"; + +import { bulkGeocode } from "../src/bulk"; + +import { serviceInfo, getGeocodeService } from "../src/helpers"; import * as fetchMock from "fetch-mock"; @@ -365,9 +366,46 @@ describe("geocode", () => { bulkGeocode({ addresses }) // tslint:disable-next-line - .then(response => {}) .catch(e => { - expect(e).toEqual("bulk geocoding requires authentication"); + expect(e).toEqual( + "bulk geocoding using the ArcGIS service requires authentication" + ); + done(); + }); + }); + + it("should send a bulk geocoding request to a custom url without a token", done => { + fetchMock.once("*", GeocodeAddresses); + + bulkGeocode({ + addresses, + endpoint: + "https://customer.gov/arcgis/rest/services/CompositeGeocoder/GeocodeServer/" + }) + // tslint:disable-next-line + .then(response => { + expect(fetchMock.called()).toEqual(true); + const [url, options]: [string, RequestInit] = fetchMock.lastCall("*"); + expect(url).toEqual( + "https://customer.gov/arcgis/rest/services/CompositeGeocoder/GeocodeServer/geocodeAddresses" + ); + expect(options.method).toBe("POST"); + expect(options.body).toContain("f=json"); + expect(options.body).toContain( + `addresses=${encodeURIComponent( + '{"records":[{"attributes":{"OBJECTID":1,"SingleLine":"380 New York St. Redlands 92373"}},{"attributes":{"OBJECTID":2,"SingleLine":"1 World Way Los Angeles 90045"}}]}' + )}` + ); + // expect(options.body).toContain("token=token"); + expect(response.spatialReference.latestWkid).toEqual(4326); + expect(response.locations[0].address).toEqual( + "380 New York St, Redlands, California, 92373" + ); + expect(response.locations[0].location.x).toEqual(-117.19567031799994); + // the only property this lib tacks on + expect(response.locations[0].location.spatialReference.wkid).toEqual( + 4326 + ); done(); }); });