Source: get-current-weather.js

/**
 * @function getCurrentWeather
 * @summary
 * Get Current Weather from API.WEATHER.GOV
 * @description
 * Uses the zip code in the Ad Parameters (adParameters.geoInfo.postalCode) to
 * 1.  Get the Lat/Lon from pterodactlyus (or raptor) centered on the zip code
 * 2.  Make an asynchronous call to API.WEATHER.GOV for the current weather at the closest station returning data
 * 3.  Emits an event 'CURRENT_WEATHER_RESULTS' when the data is received
 * 4.  Returns a JavaScript Promise when can be used as an alternative way to receive data or catch an error
 * 5.  The zip code from the Ad Parameters can be superceded by passing in options.zipCode
 *
 * Example Usage:
 * ```
 * window.$b.snippets.getCurrentWeather()
 * window.$b.on('CURRENT_WEATHER_RESULTS', function (data) {
 *   console.log(data.textDescription)
 * }
 * ```
 * Example of data object returned:
 * ```
 * {
 *   "@id": "https://api.weather.gov/stations/KOAK/observations/2018-09-06T18:53:00+00:00",
 *   "@type": "wx:ObservationStation",
 *   "elevation": {
 *     "value": 3,
 *     "unitCode": "unit:m"
 *   },
 *   "station": "https://api.weather.gov/stations/KOAK",
 *   "timestamp": "2018-09-06T18:53:00+00:00",
 *   "rawMessage": "KOAK 061853Z 29008KT 10SM BKN010 16/13 A3010 RMK AO2 SLP191 T01610128",
 *   "textDescription": "Mostly Cloudy",
 *   "icon": "https://api.weather.gov/icons/land/day/bkn?size=medium",
 *   "presentWeather": [],
 *   "temperature": {
 *     "value": 16.100000000000023,
 *     "unitCode": "unit:degC",
 *     "qualityControl": "qc:V"
 *   },
 *   "dewpoint": {
 *     "value": 12.800000000000011,
 *     "unitCode": "unit:degC",
 *     "qualityControl": "qc:V"
 *   },
 *   "windDirection": {
 *     "value": 290,
 *     "unitCode": "unit:degree_(angle)",
 *     "qualityControl": "qc:V"
 *   },
 *   "windSpeed": {
 *     "value": 4.1,
 *     "unitCode": "unit:m_s-1",
 *     "qualityControl": "qc:V"
 *   },
 *   "windGust": {
 *     "value": null,
 *     "unitCode": "unit:m_s-1",
 *     "qualityControl": "qc:Z"
 *   },
 *   "barometricPressure": {
 *     "value": 101930,
 *     "unitCode": "unit:Pa",
 *     "qualityControl": "qc:V"
 *   },
 *   "seaLevelPressure": {
 *     "value": 101910,
 *     "unitCode": "unit:Pa",
 *     "qualityControl": "qc:V"
 *   },
 *   "visibility": {
 *     "value": 16090,
 *     "unitCode": "unit:m",
 *     "qualityControl": "qc:C"
 *   },
 *   "maxTemperatureLast24Hours": {
 *     "value": null,
 *     "unitCode": "unit:degC",
 *     "qualityControl": null
 *   },
 *   "minTemperatureLast24Hours": {
 *     "value": null,
 *     "unitCode": "unit:degC",
 *     "qualityControl": null
 *   },
 *   "precipitationLastHour": {
 *     "value": null,
 *     "unitCode": "unit:m",
 *     "qualityControl": "qc:Z"
 *   },
 *   "precipitationLast3Hours": {
 *     "value": null,
 *     "unitCode": "unit:m",
 *     "qualityControl": "qc:Z"
 *   },
 *   "precipitationLast6Hours": {
 *     "value": null,
 *     "unitCode": "unit:m",
 *     "qualityControl": "qc:Z"
 *   },
 *   "relativeHumidity": {
 *     "value": 80.79823362700911,
 *     "unitCode": "unit:percent",
 *     "qualityControl": "qc:C"
 *   },
 *   "windChill": {
 *     "value": null,
 *     "unitCode": "unit:degC",
 *     "qualityControl": "qc:V"
 *   },
 *   "heatIndex": {
 *     "value": null,
 *     "unitCode": "unit:degC",
 *     "qualityControl": "qc:V"
 *   },
 *   "cloudLayers": [
 *     {
 *       "base": {
 *         "value": 300,
 *         "unitCode": "unit:m"
 *       },
 *       "amount": "BKN"
 *     }
 *   ],
 *   "geometry": {
 *     "type": "Point",
 *     "coordinates": [
 *       -122.22,
 *       37.72
 *     ]
 *   }
 * }
 * ```
 * @param {Object=} options
 * @param {string=} options.zipCode optional zip code to overrride value in Ad Parameters
 * @param {number=} timeout (default 30 seconds) maximum seconds to wait for api results
 * @returns {Promise}
 */
function getCurrentWeather (options) {
  options = options || {}
  var eventName = 'CURRENT_WEATHER_RESULTS'
  var errorEvent = 'CURRENT_WEATHER_ERROR'
  var blink = window.$b || {}
  var adParameters = blink.adParameters || {}
  var zipCode = options.zipCode
  var isZipValid = function (zip) {
    return /^\d{5,5}$/.test(zip)
  }
  var isLocationValid = function (locationObj) {
    return typeof locationObj.latitude === 'number' && typeof locationObj.longitude === 'number'
  }
  if (!isZipValid(zipCode)) {
    var message
    if (adParameters.geoInfo && adParameters.geoInfo.postalCode) {
      zipCode = adParameters.geoInfo.postalCode
      if (!isZipValid(zipCode)) {
        message = 'Invalid zip code: ' + zipCode
        console.error(errorEvent, message)
        try {
          window.$b.emit(errorEvent, message)
        } catch (err) {
          console.error(err)
        }
        return Promise.reject(new Error(message))
      }
    } else {
      message = 'Ad Parameter has no geoInfo.postalCode'
      console.error(errorEvent, message)
      try {
        window.$b.emit(errorEvent, message)
      } catch (err) {
        console.error(err)
      }
      return Promise.reject(new Error(message))
    }
  }
  var timeout = options.timeout
  if ((typeof timeout !== 'number') || (timeout <= 0)) {
    timeout = 30000
  } else {
    timeout *= 1000
  }
  /*
   * Wrap emit in try catch in case user has faulty code in their error handler
   */
  var emitErrorAndReject = function (options) {
    var error = options.error
    var reject = options.reject
    try {
      window.$b.emit(errorEvent, error)
    } catch (err) {
      console.error(err)
    }
    reject(error)
  }
  // var zipUrl = 'https://pterodactylus.tremorvideodsp.com/ws/zipcodes/ZIP.json'
  var zipUrl = 'https://raptor.tremorvideodsp.com/ws/zipcodes/ZIP.json'
  var stationUrl = 'https://api.weather.gov/points/LAT,LON/stations'
  zipUrl = zipUrl.replace('ZIP', zipCode)
  return new Promise(function (resolve, reject) {
    $.ajax({
      url: zipUrl,
      method: 'GET',
      timeout: timeout
    })
      .fail(function (err) {
        var message = zipUrl + ': ' + err.statusText
        console.error(errorEvent, message)
        var error = new Error(message)
        emitErrorAndReject({ error: error, reject: reject })
      })
      .then(function (json) {
        if (!isLocationValid(json)) {
          var message = 'Zipcode "' + zipCode + '" is invalid'
          console.error(errorEvent, message)
          var error = new Error(message)
          emitErrorAndReject({ error: error, reject: reject })
          return
        }
        stationUrl = stationUrl
          .replace('LAT', json.latitude)
          .replace('LON', json.longitude)
        $.ajax({
          url: stationUrl,
          method: 'GET',
          timeout: timeout
        })
          .fail(function (err) {
            var message = stationUrl + ': ' + err.statusText
            var error = new Error(message)
            emitErrorAndReject({ error: error, reject: reject })
          })
          .done(function (data) {
            // console.log('station: ', data)
            var message
            var i = 0
            var station
            if (!(data.features instanceof Array) || !data.features.length) {
              message = 'URL: ' + stationUrl + ' did not return features array'
              console.error(errorEvent, message)
              var error = new Error(message)
              emitErrorAndReject({ error: error, reject: reject })
            } else {
              var tryGetCurrentWeatherData = function (station) {
                getCurrentWeatherData({ station: station, timeout: timeout })
                  .then(function (data) {
                    console.log(eventName, data)
                    resolve(data)
                    try {
                      window.$b.emit(eventName, data)
                    } catch (err) {
                      console.error(err)
                    }
                  })
                  .catch(function (err) {
                    console.log(err)
                    i += 1
                    if (i < data.features.length) {
                      station = data.features[i].id.replace(/^.*\/(.*)$/, '$1')
                      tryGetCurrentWeatherData(station)
                    } else {
                      message = 'No stations returned current weather data'
                      console.error(errorEvent, message)
                      var error = new Error(message)
                      emitErrorAndReject({ error: error, reject: reject })
                    }
                  })
              }
              station = data.features[i].id.replace(/^.*\/(.*)$/, '$1')
              tryGetCurrentWeatherData(station)
            }
          })
      })
  })
}

function getCurrentWeatherData (options) {
  var station = options.station
  var timeout = options.timeout
  return new Promise(function (resolve, reject) {
    // DEPRECATED
    // var weatherUrl = 'https://api.weather.gov/stations/STATION/observations/current'
    var weatherUrl = 'https://api.weather.gov/stations/STATION/observations/latest'
    var currentWeatherUrl = weatherUrl.replace('STATION', station)
    var errorEvent = 'CURRENT_WEATHER_ERROR'
    $.ajax({
      url: currentWeatherUrl,
      method: 'GET',
      timeout: timeout
    })
      .done(function (data) {
        if (!(data.properties instanceof Object)) {
          var message = 'URL: ' + currentWeatherUrl + ' did not return data.properties'
          console.error(errorEvent, message)
          var error = new Error(message)
          try {
            window.$b.emit(errorEvent, error)
          } catch (err) {
            console.error(err)
          }
          reject(error)
        } else {
          data.properties.geometry = data.geometry
          resolve(data.properties)
        }
      })
      .fail(function () {
        var message = 'URL: ' + currentWeatherUrl + ' did not return data'
        console.error(errorEvent, message)
        var error = new Error(message)
        try {
          window.$b.emit(errorEvent, error)
        } catch (err) {
          console.error(err)
        }
        reject(error)
      })
  })
}

window.$b = window.$b || {}
window.$b.snippets = window.$b.snippets || {}
var snippets = window.$b.snippets
snippets.getCurrentWeather = getCurrentWeather

if (window.module) {
  window.module.exports = getCurrentWeather
}