← back to Bertha

src/lib/weather.js

141 lines

const NWS_BASE = 'https://api.weather.gov';
const USER_AGENT = 'Bertha-Weather-Bot/1.0 (steve@designerwallcoverings.com)';

async function nwsFetch(url) {
  const res = await fetch(url, {
    headers: {
      'User-Agent': USER_AGENT,
      'Accept': 'application/geo+json'
    }
  });
  if (!res.ok) throw new Error(`NWS API ${res.status}: ${res.statusText} for ${url}`);
  return res.json();
}

// Get gridpoint mapping for a lat/lon
async function getGridPoint(lat, lon) {
  return nwsFetch(`${NWS_BASE}/points/${lat},${lon}`);
}

// Get forecast for a gridpoint
async function getForecast(office, gridX, gridY) {
  return nwsFetch(`${NWS_BASE}/gridpoints/${office}/${gridX},${gridY}/forecast`);
}

// Get hourly forecast for a gridpoint
async function getHourlyForecast(office, gridX, gridY) {
  return nwsFetch(`${NWS_BASE}/gridpoints/${office}/${gridX},${gridY}/forecast/hourly`);
}

// Get raw gridpoint data (quantitative)
async function getGridData(office, gridX, gridY) {
  return nwsFetch(`${NWS_BASE}/gridpoints/${office}/${gridX},${gridY}`);
}

// Get active alerts for a point
async function getAlerts(lat, lon) {
  return nwsFetch(`${NWS_BASE}/alerts/active?point=${lat},${lon}`);
}

// Get station observations
async function getStationObs(stationId) {
  return nwsFetch(`${NWS_BASE}/stations/${stationId}/observations/latest`);
}

// Get list of stations near a point
async function getNearbyStations(office, gridX, gridY) {
  return nwsFetch(`${NWS_BASE}/gridpoints/${office}/${gridX},${gridY}/stations`);
}

// City coordinate lookup
const CITY_COORDS = {
  'New York': { lat: 40.7128, lon: -74.0060 },
  'Los Angeles': { lat: 34.0522, lon: -118.2437 },
  'Chicago': { lat: 41.8781, lon: -87.6298 },
  'Houston': { lat: 29.7604, lon: -95.3698 },
  'Phoenix': { lat: 33.4484, lon: -112.0740 },
  'Philadelphia': { lat: 39.9526, lon: -75.1652 },
  'San Antonio': { lat: 29.4241, lon: -98.4936 },
  'San Diego': { lat: 32.7157, lon: -117.1611 },
  'Dallas': { lat: 32.7767, lon: -96.7970 },
  'San Francisco': { lat: 37.7749, lon: -122.4194 },
  'Seattle': { lat: 47.6062, lon: -122.3321 },
  'Denver': { lat: 39.7392, lon: -104.9903 },
  'Boston': { lat: 42.3601, lon: -71.0589 },
  'Miami': { lat: 25.7617, lon: -80.1918 },
  'Atlanta': { lat: 33.7490, lon: -84.3880 },
  'Minneapolis': { lat: 44.9778, lon: -93.2650 },
  'Detroit': { lat: 42.3314, lon: -83.0458 },
  'Portland': { lat: 45.5051, lon: -122.6750 },
  'Las Vegas': { lat: 36.1699, lon: -115.1398 },
  'Nashville': { lat: 36.1627, lon: -86.7816 },
  'Washington': { lat: 38.9072, lon: -77.0369 },
  'Cleveland': { lat: 41.4993, lon: -81.6944 },
  'Pittsburgh': { lat: 40.4406, lon: -79.9959 },
  'St. Louis': { lat: 38.6270, lon: -90.1994 },
  'Kansas City': { lat: 39.0997, lon: -94.5786 },
  'Tampa': { lat: 27.9506, lon: -82.4572 },
  'New Orleans': { lat: 29.9511, lon: -90.0715 },
  'Oklahoma City': { lat: 35.4676, lon: -97.5164 },
};

function getCityCoords(cityName) {
  return CITY_COORDS[cityName] || null;
}

// Generate geom_hash for consistent location keys
function geomHash(lat, lon) {
  return `${Math.round(lat * 100)}_${Math.round(lon * 100)}`;
}

// Extract forecast features for a specific variable and time
function extractForecastFeatures(forecastData, variable, targetTime) {
  const periods = forecastData?.properties?.periods || [];
  if (!periods.length) return null;

  const features = {
    variable,
    forecast_count: periods.length,
    values: [],
    timestamps: []
  };

  for (const period of periods) {
    let value = null;
    if (variable === 'temperature') {
      value = period.temperature;
      features.unit = period.temperatureUnit;
    } else if (variable === 'wind') {
      const match = period.windSpeed?.match(/(\d+)/);
      if (match) value = parseInt(match[1]);
      features.unit = 'mph';
    } else if (variable === 'precipitation') {
      const prob = period.probabilityOfPrecipitation?.value;
      value = prob != null ? prob : 0;
      features.unit = 'percent';
    }

    if (value !== null) {
      features.values.push(value);
      features.timestamps.push(period.startTime);
    }
  }

  if (features.values.length > 0) {
    features.mean = features.values.reduce((a, b) => a + b, 0) / features.values.length;
    features.min = Math.min(...features.values);
    features.max = Math.max(...features.values);
    features.range = features.max - features.min;
    features.latest = features.values[0];
  }

  return features;
}

module.exports = {
  getGridPoint, getForecast, getHourlyForecast, getGridData,
  getAlerts, getStationObs, getNearbyStations,
  getCityCoords, geomHash, extractForecastFeatures,
  CITY_COORDS, NWS_BASE
};