Source: Geoclient.js

/**
 * @module nyc/Geoclient
 */

import nyc from 'nyc'
import Locator from 'nyc/Locator'
import Geocoder from 'nyc/Geocoder'
import fetchTimeout from 'nyc/fetchTimeout'

const proj4 = nyc.proj4

/**
 * @desc A class for geocoding using the New York City Geoclient API
 * @public
 * @class
 * @extends module:nyc/Geocoder~Geocoder
 * @fires module:nyc/Locator~Locator#geocoded
 * @fires module:nyc/Locator~Locator#geolocated
 * @fires module:nyc/Locator~Locator#ambiguous
 * @fires module:nyc/Locator~Locator#error
 * @see https://developer.cityofnewyork.us/api/geoclient-api
 */
class Geoclient extends Geocoder {
/**
 * @desc Create an instance of Geoclient
 * @public
 * @constructor
 * @param {module:nyc/Geoclient~Geoclient.Options} options Constructor options
 */
  constructor(options) {
    super(options)
    /**
     * @private
     * @member {string}
     */
    this.url = `${options.url}&input=`
    /**
     * @desc The epsg code
     * @public
     * @member {string}
     */
    this.projection = options.projection || 'EPSG:3857'
  }
  /**
   * @desc Geocode an input string representing a location
   * @public
   * @override
   * @method
   * @param {string} input The value to geocode
   * @returns {Promise<module:nyc/Locator~Locator.Result|module:nyc/Locator~Locator.Ambiguous>} The result of the search request
   */
  search(input) {
    return new Promise((resolve, reject) => {
      input = input.trim()
      if (input.length === 5 && !isNaN(input)) {
        this.resolveZip(input, resolve)
      } else if (input.length) {
        input = encodeURIComponent(input.replace(/"/g, '').replace(/'/g, '').replace(/&/g, ' and '))
        fetchTimeout(`${this.url}${input}`).then(response => {
          return response.json()
        }).then(json => {
          this.geoclient(json, resolve)
        }).catch(error => {
          this.error(error, reject)
        })
      }
    })
  }
  resolveZip(input, resolve) {
    const p = this.project(Geoclient.ZIP_CODE_POINTS[input])
    let result
    if (p) {
      result = {
        coordinate: p,
        accuracy: Locator.Accuracy.ZIP_CODE,
        type: 'geocoded',
        zip: true,
        name: input
      }
      resolve(result)
      this.trigger('geocoded', result)
    } else {
      result = {input: input, type: 'ambiguous', possible: []}
      resolve(result)
      this.trigger('ambiguous', result)
    }
  }
  /**
   * @private
   * @method
   * @param {Array<number>} coordinate Coordinate
   * @return {Array<number>} Reprojected coordinate
   */
  project(coordinate) {
    if (coordinate && this.projection !== 'EPSG:2263') {
      return proj4('EPSG:2263', this.projection, coordinate)
    }
    return coordinate
  }
  /**
   * @private
   * @method
   * @param {Object} location Response object
   * @return {boolean} If mappable
   */
  isMappable(location) {
    return location &&
      location.coordinate &&
      !isNaN(location.coordinate[0]) &&
      !isNaN(location.coordinate[1])
  }
  /**
   * @private
   * @method
   * @param {Object} response Response object
   * @param {function} resolve Resolve
   */
  geoclient(response, resolve) {
    const results = response.results
    const nothing = {
      type: 'ambiguous',
      input: response.input,
      possible: []
    }
    if (response.status === 'OK') {
      const location = this.parse(results[0])
      if (results.length === 1 && this.isMappable(location)) {
        location.type = 'geocoded'
        resolve(location)
        this.trigger('geocoded', location)
      } else {
        const possible = this.possible(results, resolve)
        if (possible.length > 1) {
          const ambiguous = {
            type: 'ambiguous',
            input: response.input,
            possible
          }
          resolve(ambiguous)
          this.trigger('ambiguous', ambiguous)
        } else if (possible.length === 1) {
          possible[0].type = 'geocoded'
          resolve(possible[0])
          this.trigger('geocoded', possible[0])
        } else {
          resolve(nothing)
          this.trigger('ambiguous', nothing)
        }
      }
    } else {
      resolve(nothing)
      this.trigger('ambiguous', nothing)
    }
  }
  /**
   * @private
   * @method
   * @param {Array<module:nyc.Locator~Locator.Ambiguous>} results Results
   * @param {function} resolve Resolve
   * @returns {Array} Possible results
   */
  possible(results, resolve) {
    let possible = []
    results.forEach(result => {
      const location = this.parse(result)
      if (location) {
        possible.push(location)
      } else {
        const possibleMap = {}
        const response = result.response
        let more = true
        let i = 1
        while (more) {
          const lowHouseNumber = response['giLowHouseNumber' + i]
          const highHouseNumber = response['giHighHouseNumber' + i]
          if (lowHouseNumber) {
            const lowName = nyc.capitalize(`${lowHouseNumber} ${response['giStreetName' + i]}, ${Geoclient.BOROS[response['giBoroughCode' + i]]}`) + ', NY'
            possibleMap[lowName] = {name: lowName}
          }
          if (highHouseNumber) {
            const highName = nyc.capitalize(`${highHouseNumber} ${response['giStreetName' + i]}, ${Geoclient.BOROS[response['giBoroughCode' + i]]}`) + ', NY'
            possibleMap[highName] = {name: highName}
          }
          i = i + 1
          more = lowHouseNumber || highHouseNumber
        }
        possible = Object.values(possibleMap)
      }
    })
    return possible
  }
  /**
   * @private
   * @method
   * @param {Object} result Result
   * @return {Locator.Result} Locator result
   */
  parse(result) {
    const typ = result.request.split(' ')[0], r = result.response
    let ln1, p, a
    if (typ === 'intersection') {
      ln1 = `${r.streetName1} and ${r.streetName2}`
      p = [r.xCoordinate * 1, r.yCoordinate * 1]
      a = Locator.Accuracy.MEDIUM
    } else if (typ === 'blockface') {
      ln1 = r.firstStreetNameNormalized + ' btwn ' + r.secondStreetNameNormalized + ' & ' + r.thirdStreetNameNormalized
      p = [((r.fromXCoordinate * 1) + (r.toXCoordinate * 1)) / 2, ((r.fromYCoordinate * 1) + (r.toYCoordinate * 1)) / 2]
      a = Locator.Accuracy.LOW
    } else { /* address, bbl, bin, place */
      const x = r.internalLabelXCoordinate
      const y = r.internalLabelYCoordinate
      ln1 = (r.houseNumber ? (r.houseNumber + ' ') : '') + (r.firstStreetNameNormalized || r.giStreetName1)
      p = [(x && y ? x : r.xCoordinate) * 1, (x && y ? y : r.yCoordinate) * 1]
      a = x && y ? Locator.Accuracy.HIGH : Locator.Accuracy.MEDIUM
    }
    if (!this.isMappable({coordinate: p})) {
      console.warn('No coordinate', result)
      return
    }
    try {
      return {
        type: 'geocoded',
        coordinate: this.project(p),
        data: r,
        accuracy: a, /* approximation */
        name: nyc.capitalize(`${ln1.replace(/  +/g, ' ')}, ${Geoclient.BOROS[r.firstBoroughName]}`) +
          `, NY ${(r.zipCode || r.leftSegmentZipCode || '')}`
      }
    } catch (badCoord) {
      console.warn('No coordinate', result)
    }
  }
  /**
   * @private
   * @method
   * @param {Object} error Resolve
   * @param {function} reject Resolve
   */
  error(error, reject) {
    console.error('Geoclient error', error)
    reject({type: 'error', error: error})
    this.trigger('error', error)
  }
}

/**
 * @desc Constructor options for {@link module:nyc/Geoclient~Geoclient}
 * @public
 * @typedef {Object}
 * @property {string} url The URL for accessing the Geoclient API (see {@link https://developer.cityofnewyork.us/api/geoclient-api})
 * @property {string} [projection=EPSG:3857] The EPSG code of the projection for output geometries (i.e. EPSG:2263)
 */
Geoclient.Options

/**
 * @private
 * @const
 * @type {Object<string, string>}
 */
Geoclient.BOROS = {
  '1': 'Manhattan',
  '2': 'Bronx',
  '3': 'Brooklyn',
  '4': 'Queens',
  '5': 'Staten Island',
  'MANHATTAN': 'Manhattan',
  'BRONX': 'Bronx',
  'BROOKLYN': 'Brooklyn',
  'QUEENS': 'Queens',
  'STATEN IS': 'Staten Island'
}

/**
 * @private
 * @const
 * @type {Object<string, Array<number>>}
 */
Geoclient.ZIP_CODE_POINTS = {
  '10474': [1018466, 233787],
  '10455': [1007889, 236616],
  '10037': [1001661, 235627],
  '10024': [991450, 225457],
  '10454': [1007449, 232863],
  '10026': [997179, 231835],
  '10025': [993331, 230012],
  '10035': [1006595, 228454],
  '11101': [1001319, 211034],
  '11364': [1051553, 211923],
  '10018': [984278, 215509],
  '10020': [990012, 215729],
  '11005': [1064789, 214295],
  '10017': [991869, 213252],
  '10001': [983613, 213360],
  '10011': [982781, 210664],
  '10016': [990291, 210665],
  '11104': [1006224, 210564],
  '11109': [996054, 210740],
  '10010': [988989, 208515],
  '11367': [1032392, 205392],
  '10014': [982449, 206620],
  '10003': [987465, 205563],
  '11222': [998773, 204620],
  '11379': [1017432, 200740],
  '11429': [1056859, 198057],
  '11435': [1036960, 195018],
  '11415': [1031798, 197376],
  '11418': [1031001, 193330],
  '11433': [1043368, 193721],
  '11451': [1039899, 194697],
  '11221': [1004438, 191873],
  '11421': [1023878, 191241],
  '11372': [1016528, 213126],
  '11004': [1065606, 210363],
  '11040': [1065956, 213534],
  '10002': [988242, 200171],
  '10314': [939259, 155728],
  '11228': [980891, 164066],
  '11209': [976172, 165379],
  '10304': [962458, 165679],
  '10456': [1007883, 242372],
  '10472': [1021444, 241624],
  '10031': [998088, 239814],
  '10039': [1001519, 240436],
  '10459': [1013835, 240084],
  '10451': [1005368, 238290],
  '10473': [1022475, 237217],
  '10030': [1000193, 237302],
  '10027': [999088, 234131],
  '10461': [1028296, 247902],
  '10457': [1012388, 247405],
  '10460': [1015132, 243510],
  '10032': [1000382, 244917],
  '10452': [1005446, 244483],
  '11414': [1027256, 178677],
  '11232': [983612, 177801],
  '10034': [1005713, 255122],
  '10033': [1001707, 248780],
  '10462': [1020685, 249420],
  '10040': [1003863, 251914],
  '10453': [1008152, 249805],
  '11102': [1004636, 220364],
  '10021': [997020, 219120],
  '11361': [1047797, 217779],
  '11358': [1040709, 216377],
  '11362': [1057749, 215220],
  '10044': [996385, 214347],
  '11369': [1019957, 217722],
  '11103': [1008295, 217197],
  '11106': [1001397, 217519],
  '11368': [1024870, 212082],
  '11377': [1010574, 210678],
  '10036': [985413, 216821],
  '11355': [1033762, 213010],
  '10302': [945946, 169124],
  '10465': [1031495, 239091],
  '11691': [1053783, 158810],
  '11096': [1054217, 162199],
  '11223': [991665, 156781],
  '11208': [1019700, 184749],
  '11207': [1013544, 183721],
  '11413': [1053380, 183328],
  '11217': [989547, 187967],
  '11238': [994291, 186584],
  '11231': [983057, 186217],
  '11422': [1057272, 180607],
  '11420': [1036899, 184708],
  '11417': [1027410, 185709],
  '11215': [989882, 181773],
  '11357': [1036533, 225804],
  '10029': [999950, 227538],
  '00083': [991590, 220538],
  '11356': [1028115, 225277],
  '11359': [1046026, 228012],
  '10303': [933820, 171004],
  '11234': [1004323, 163783],
  '11360': [1044984, 223892],
  '11105': [1011836, 221755],
  '10128': [997765, 224257],
  '11371': [1022003, 220457],
  '10023': [988765, 222068],
  '11363': [1055047, 220385],
  '10028': [995910, 222872],
  '11354': [1032198, 219054],
  '11216': [998312, 187241],
  '11416': [1025796, 188584],
  '11233': [1006523, 186300],
  '11436': [1040697, 185554],
  '11213': [1000284, 183308],
  '11212': [1008387, 180764],
  '11225': [997345, 180829],
  '11218': [990799, 173649],
  '11226': [995711, 174598],
  '11219': [985166, 169604],
  '11210': [999101, 167896],
  '11230': [993494, 166072],
  '11419': [1033365, 190202],
  '11434': [1046577, 185969],
  '11204': [987887, 164576],
  '10471': [1012587, 266570],
  '10470': [1024748, 268409],
  '10466': [1024313, 263999],
  '10467': [1019786, 258782],
  '11430': [1043607, 175068],
  '11203': [1002509, 175950],
  '10469': [1026678, 255623],
  '10468': [1013897, 258046],
  '10458': [1015534, 253712],
  '11378': [1009641, 202115],
  '10009': [990043, 203961],
  '10012': [984832, 203596],
  '10013': [982892, 201665],
  '10007': [982504, 199552],
  '11237': [1004600, 197083],
  '11385': [1011367, 195195],
  '10038': [983418, 197803],
  '11206': [1000221, 195003],
  '10006': [980563, 197293],
  '11412': [1050773, 193231],
  '10005': [981339, 197038],
  '11251': [993428, 194599],
  '11411': [1057763, 192413],
  '11201': [986968, 192156],
  '10004': [972576, 193652],
  '11205': [995897, 192159],
  '10305': [963310, 156060],
  '11229': [999717, 158278],
  '11214': [984000, 159330],
  '10306': [950937, 145144],
  '11694': [1025530, 149310],
  '11224': [984306, 149092],
  '10308': [942704, 140020],
  '10282': [979981, 200244],
  '11211': [999857, 199848],
  '11249': [993704, 198278],
  '11370': [1016758, 227518],
  '10065': [994666, 217698],
  '10075': [995301, 221633],
  '10069': [987047, 221849],
  '10281': [980036, 198787],
  '11373': [1017948, 208465],
  '10279': [981949, 198823],
  '10165': [990124, 213288],
  '10168': [990733, 212960],
  '10105': [990067, 217259],
  '10118': [988212, 211939],
  '10176': [990123, 214490],
  '10162': [997739, 219667],
  '10170': [990955, 213470],
  '10112': [989735, 215947],
  '10122': [986550, 213178],
  '10107': [989098, 218425],
  '10103': [990705, 216275],
  '10174': [991094, 213106],
  '10166': [990733, 213798],
  '10169': [990872, 214089],
  '10167': [991259, 214199],
  '10177': [990927, 214381],
  '10172': [991398, 214417],
  '10171': [991426, 214709],
  '10270': [982060, 196710],
  '10104': [990123, 216312],
  '10271': [981339, 197293],
  '10110': [989486, 213943],
  '10175': [989847, 214016],
  '10151': [991702, 217332],
  '10173': [990040, 213907],
  '10178': [990429, 212814],
  '10119': [986217, 213068],
  '10121': [986439, 212486],
  '10123': [986827, 213032],
  '10106': [989624, 218024],
  '10158': [991177, 212122],
  '10041': [981727, 195435],
  '10120': [987242, 212449],
  '10278': [983113, 199880],
  '10155': [993143, 216531],
  '10043': [982337, 195836],
  '10081': [981838, 197074],
  '10096': [989071, 214235],
  '10097': [988461, 216931],
  '10196': [989458, 214963],
  '10275': [980368, 196382],
  '10265': [982060, 196164],
  '10045': [981866, 197329],
  '10047': [988047, 208660],
  '10080': [980785, 198167],
  '10203': [981062, 196965],
  '10259': [981478, 197439],
  '10260': [981921, 196564],
  '10285': [980147, 199224],
  '10286': [981727, 196637],
  '10463': [1007644, 260043],
  '10475': [1030489, 258509],
  '10464': [1036131, 258338],
  '10309': [923594, 132955],
  '10307': [916899, 124411],
  '10280': [979509, 197257],
  '10048': [980812, 198532],
  '10055': [991564, 215802],
  '10019': [989652, 217477],
  '10111': [990483, 215838],
  '10153': [991896, 217478],
  '10154': [991896, 215328],
  '10152': [991979, 215584],
  '10115': [994299, 234675],
  '10022': [993087, 215620],
  '11235': [1000973, 149936],
  '11693': [1036606, 153266],
  '11692': [1041045, 155461],
  '11697': [1009230, 143240],
  '10312': [935583, 136827],
  '11239': [1021573, 175899],
  '11236': [1011642, 172461],
  '11220': [979421, 172519],
  '10301': [961077, 172967],
  '10310': [950582, 169591],
  '11426': [1060737, 207724],
  '11365': [1041364, 208618],
  '11001': [1065508, 206646],
  '11375': [1026883, 201848],
  '11427': [1053373, 205042],
  '11374': [1022389, 203771],
  '11366': [1039491, 203659],
  '11423': [1048812, 200621],
  '11428': [1055655, 202207],
  '11432': [1041634, 200093]
}

export default Geoclient