Source: io/bufkit_map.js

const IEM_BUFKIT = 'https://mtarchive.geol.iastate.edu';
const AVIATION_STATIONS = new URL('../data/stations.json', import.meta.url);
const TILE_SERVER = 'https://tile.openstreetmap.org';
const TILE_SIZE = 256;
const stationCatalogByModelRun = new Map();
let aviationStationCatalog = null;
let nextDialogId = 0;
const SVG_NS = 'http://www.w3.org/2000/svg';
const US_STATE_NAMES = {
  AL: 'Alabama', AK: 'Alaska', AZ: 'Arizona', AR: 'Arkansas', CA: 'California', CO: 'Colorado',
  CT: 'Connecticut', DE: 'Delaware', FL: 'Florida', GA: 'Georgia', HI: 'Hawaii', ID: 'Idaho',
  IL: 'Illinois', IN: 'Indiana', IA: 'Iowa', KS: 'Kansas', KY: 'Kentucky', LA: 'Louisiana',
  ME: 'Maine', MD: 'Maryland', MA: 'Massachusetts', MI: 'Michigan', MN: 'Minnesota',
  MS: 'Mississippi', MO: 'Missouri', MT: 'Montana', NE: 'Nebraska', NV: 'Nevada',
  NH: 'New Hampshire', NJ: 'New Jersey', NM: 'New Mexico', NY: 'New York',
  NC: 'North Carolina', ND: 'North Dakota', OH: 'Ohio', OK: 'Oklahoma', OR: 'Oregon',
  PA: 'Pennsylvania', RI: 'Rhode Island', SC: 'South Carolina', SD: 'South Dakota',
  TN: 'Tennessee', TX: 'Texas', UT: 'Utah', VT: 'Vermont', VA: 'Virginia',
  WA: 'Washington', WV: 'West Virginia', WI: 'Wisconsin', WY: 'Wyoming',
  DC: 'District of Columbia', PR: 'Puerto Rico', GU: 'Guam', VI: 'U.S. Virgin Islands',
};

/**
 * Creates a plain-JavaScript BUFKIT location picker button and its map dialog.
 * @param {object} options Picker configuration.
 * @param {Document} options.document Document used to create the picker UI.
 * @param {HTMLElement} options.host SharpJS host used for theming and dialog placement.
 * @param {() => string} options.getModel Returns the selected BUFKIT model.
 * @param {() => Date} options.getRun Returns the selected BUFKIT run date and hour.
 * @param {(station: string) => void} options.onSelect Receives the selected station code.
 * @returns {{button: HTMLButtonElement, destroy: () => void}} Picker button and cleanup function.
 */
export function createBufkitMapButton({ document: doc, host, getModel, getRun, onSelect }) {
  const button = doc.createElement('button');
  button.type = 'button';
  button.className = 'sjs-button';
  button.textContent = 'Choose station from map…';
  button.setAttribute('aria-haspopup', 'dialog');

  const dialog = doc.createElement('dialog');
  dialog.className = 'sjs-bufkit-map-dialog';
  const titleId = `sjs-bufkit-map-title-${++nextDialogId}`;
  dialog.setAttribute('aria-labelledby', titleId);
  dialog.innerHTML = `
    <style>
      .sjs-bufkit-map-dialog {
        width: min(960px, 96vw);
        max-width: 96vw;
        max-height: 92vh;
        padding: 0;
        color: var(--sjs-text, #e8eef2);
        background: var(--sjs-surface, #0d1014);
        border: 1px solid var(--sjs-border, rgba(255, 255, 255, .28));
        border-radius: 8px;
        box-shadow: 0 12px 40px rgba(0, 0, 0, .65);
        font: 14px/1.4 var(--sjs-font, "Segoe UI", Arial, sans-serif);
      }
      .sjs-bufkit-map-dialog::backdrop { background: rgba(0, 0, 0, .72); }
      .sjs-bufkit-map-shell { padding: 14px; }
      .sjs-bufkit-map-header { display: flex; align-items: center; justify-content: space-between; gap: 10px; }
      .sjs-bufkit-map-header h2 { margin: 0; font-size: 1rem; }
      .sjs-bufkit-map-close {
        border: 0; border-radius: 4px; padding: 5px 9px; cursor: pointer;
        color: inherit; background: var(--sjs-button-bg, #1f262e); font: inherit;
      }
      .sjs-bufkit-map-tools { display: flex; align-items: center; gap: 8px; margin: 10px 0 6px; }
      .sjs-bufkit-map-search {
        flex: 1; min-width: 0; padding: 7px 9px; color: inherit;
        background: var(--sjs-surface-2, #161b21);
        border: 1px solid var(--sjs-border, rgba(255, 255, 255, .28)); border-radius: 4px; font: inherit;
      }
      .sjs-bufkit-map-status { min-height: 1.4em; margin: 0 0 6px; color: var(--sjs-muted, #8ea3b3); font-size: .82rem; }
      .sjs-bufkit-map-viewport {
        position: relative; width: 100%; height: min(62vh, 590px); min-height: 280px;
        overflow: hidden; background: #17232c; border: 1px solid var(--sjs-border, rgba(255, 255, 255, .28));
        border-radius: 5px; touch-action: none; cursor: grab;
      }
      .sjs-bufkit-map-viewport.is-dragging { cursor: grabbing; }
      .sjs-bufkit-map-svg { display: block; width: 100%; height: 100%; }
      .sjs-bufkit-map-marker { fill: #f4b942; stroke: #111820; stroke-width: 1.5; cursor: pointer; }
      .sjs-bufkit-map-marker:hover, .sjs-bufkit-map-marker:focus { fill: #fff; stroke: #e65353; stroke-width: 2.5; outline: none; }
      .sjs-bufkit-map-tooltip {
        position: absolute; z-index: 2; display: none; max-width: 260px; padding: 7px 9px;
        color: #fff; background: rgba(10, 14, 18, .94); border: 1px solid rgba(255, 255, 255, .3);
        border-radius: 4px; pointer-events: none; font-size: .8rem; box-shadow: 0 2px 8px #0008;
      }
      .sjs-bufkit-map-attribution {
        position: absolute; right: 4px; bottom: 3px; padding: 2px 5px;
        color: #17212a; background: rgba(255, 255, 255, .8); font-size: 10px;
      }
      .sjs-bufkit-map-attribution a { color: #17212a; }
      .sjs-bufkit-map-footer { display: flex; justify-content: space-between; gap: 8px; margin-top: 7px; color: var(--sjs-muted, #8ea3b3); font-size: .75rem; }
      .sjs-bufkit-map-zoom { display: flex; gap: 4px; }
      .sjs-bufkit-map-zoom button {
        min-width: 30px; border: 0; border-radius: 4px; padding: 4px 8px; cursor: pointer;
        color: inherit; background: var(--sjs-button-bg, #1f262e); font: inherit;
      }
      @media (max-width: 600px) {
        .sjs-bufkit-map-shell { padding: 10px; }
        .sjs-bufkit-map-viewport { height: 58vh; min-height: 240px; }
        .sjs-bufkit-map-footer { flex-direction: column; }
      }
    </style>
    <div class="sjs-bufkit-map-shell">
      <div class="sjs-bufkit-map-header">
        <h2 id="${titleId}">Choose a BUFKIT station</h2>
        <button class="sjs-bufkit-map-close" type="button" aria-label="Close station map">Close</button>
      </div>
      <div class="sjs-bufkit-map-tools">
        <input class="sjs-bufkit-map-search" type="search" autocomplete="off" placeholder="Find by station, city, state, or country" aria-label="Search BUFKIT stations">
        <div class="sjs-bufkit-map-zoom">
          <button type="button" data-zoom="in" aria-label="Zoom in">+</button>
          <button type="button" data-zoom="out" aria-label="Zoom out">−</button>
        </div>
      </div>
      <p class="sjs-bufkit-map-status" role="status" aria-live="polite"></p>
      <div class="sjs-bufkit-map-viewport" aria-label="BUFKIT station map">
        <svg class="sjs-bufkit-map-svg" role="group" aria-label="Station locations"></svg>
        <div class="sjs-bufkit-map-tooltip" role="tooltip"></div>
        <div class="sjs-bufkit-map-attribution">Map tiles © <a href="https://www.openstreetmap.org/copyright" target="_blank" rel="noopener noreferrer">OpenStreetMap contributors</a></div>
      </div>
      <div class="sjs-bufkit-map-footer">
        <span>Hover or focus a marker for its city, state, country, and station code. Click to fill Station.</span>
        <span>Station metadata: <a href="https://aviationweather.gov/" target="_blank" rel="noopener noreferrer">Aviation Weather Center</a></span>
      </div>
    </div>
  `;
  host.append(dialog);

  const viewport = dialog.querySelector('.sjs-bufkit-map-viewport');
  const svg = dialog.querySelector('.sjs-bufkit-map-svg');
  const tooltip = dialog.querySelector('.sjs-bufkit-map-tooltip');
  const status = dialog.querySelector('.sjs-bufkit-map-status');
  const search = dialog.querySelector('.sjs-bufkit-map-search');
  let stations = [];
  let visibleStations = [];
  let stationsByCode = new Map();
  let center = { lat: 39.5, lon: -98.5 };
  let zoom = 4;
  let pointer = null;
  let dragging = false;
  let destroyed = false;
  let resizeObserver = null;

  /**
   * Opens the station map, loads the selected model's available sites, and focuses search.
   * @returns {Promise<void>} Resolves when the request and initial drawing finish.
   */
  async function openMap() {
    if (!dialog.open) dialog.showModal();
    search.value = '';
    status.textContent = 'Loading BUFKIT stations…';
    svg.replaceChildren();
    try {
      const model = String(getModel() || 'hrrr').toLowerCase();
      const run = getRun();
      const [stationCodes, catalog] = await Promise.all([
        listBufkitStationCodes(model, run),
        getAviationStationCatalog(),
      ]);
      stations = matchStationMetadata(stationCodes, catalog);
      visibleStations = stations;
      stationsByCode = new Map();
      for (const station of stations) stationsByCode.set(station.code, station);
      if (!stations.length) {
        status.textContent = `The ${model.toUpperCase()} archive listing had no stations that could be matched to location metadata.`;
      } else {
        const missing = stationCodes.length - stations.length;
        status.textContent = `${stations.length} ${model.toUpperCase()} locations available${missing ? `; ${missing} station code${missing === 1 ? '' : 's'} lack map metadata` : ''}. Drag to pan or use the wheel to zoom.`;
      }
      requestAnimationFrame(drawMap);
      search.focus();
    } catch (error) {
      console.log(error);
      status.textContent = error.message || String(error);
    }
  }

  /**
   * Converts a geographic point to Web Mercator world-pixel coordinates.
   * @param {number} lat Latitude in degrees.
   * @param {number} lon Longitude in degrees.
   * @param {number} atZoom Slippy-map zoom level.
   * @returns {{x: number, y: number}} Projected world-pixel coordinate.
   */
  function project(lat, lon, atZoom) {
    const size = TILE_SIZE * (2 ** atZoom);
    const clampedLat = Math.max(-85.05112878, Math.min(85.05112878, lat));
    const sin = Math.sin(clampedLat * Math.PI / 180);
    return {
      x: ((lon + 180) / 360) * size,
      y: (0.5 - Math.log((1 + sin) / (1 - sin)) / (4 * Math.PI)) * size,
    };
  }

  /**
   * Converts Web Mercator world-pixel coordinates back to latitude and longitude.
   * @param {number} x Projected x-coordinate.
   * @param {number} y Projected y-coordinate.
   * @param {number} atZoom Slippy-map zoom level.
   * @returns {{lat: number, lon: number}} Geographic coordinate.
   */
  function unproject(x, y, atZoom) {
    const size = TILE_SIZE * (2 ** atZoom);
    return {
      lon: (x / size) * 360 - 180,
      lat: (Math.atan(Math.sinh(Math.PI * (1 - (2 * y) / size))) * 180) / Math.PI,
    };
  }

  /**
   * Creates an SVG element in the map's namespace.
   * @param {string} tag SVG tag name.
   * @param {object} attrs Attributes to assign.
   * @returns {SVGElement} Constructed SVG element.
   */
  function svgElement(tag, attrs = {}) {
    const element = doc.createElementNS(SVG_NS, tag);
    for (const [name, value] of Object.entries(attrs)) element.setAttribute(name, String(value));
    return element;
  }

  /**
   * Draws OSM tiles and matching station markers for the current view.
   * @returns {void}
   */
  function drawMap() {
    if (destroyed || !dialog.open) return;
    const bounds = viewport.getBoundingClientRect();
    const width = Math.max(1, Math.round(bounds.width));
    const height = Math.max(1, Math.round(bounds.height));
    const centerPoint = project(center.lat, center.lon, zoom);
    const left = centerPoint.x - width / 2;
    const top = centerPoint.y - height / 2;
    const firstX = Math.floor(left / TILE_SIZE);
    const lastX = Math.floor((left + width) / TILE_SIZE);
    const firstY = Math.floor(top / TILE_SIZE);
    const lastY = Math.floor((top + height) / TILE_SIZE);
    const worldTiles = 2 ** zoom;

    svg.setAttribute('viewBox', `0 0 ${width} ${height}`);
    svg.replaceChildren();
    for (let tileY = firstY; tileY <= lastY; tileY++) {
      if (tileY < 0 || tileY >= worldTiles) continue;
      for (let tileX = firstX; tileX <= lastX; tileX++) {
        const wrappedX = ((tileX % worldTiles) + worldTiles) % worldTiles;
        const image = svgElement('image', {
          x: tileX * TILE_SIZE - left,
          y: tileY * TILE_SIZE - top,
          width: TILE_SIZE,
          height: TILE_SIZE,
          href: `${TILE_SERVER}/${zoom}/${wrappedX}/${tileY}.png`,
          preserveAspectRatio: 'none',
        });
        svg.append(image);
      }
    }

    const markerRadius = zoom >= 7 ? 5 : zoom >= 5 ? 4 : 3;
    for (const station of visibleStations) {
      const point = project(station.lat, station.lon, zoom);
      const x = point.x - left;
      const y = point.y - top;
      if (x < -10 || x > width + 10 || y < -10 || y > height + 10) continue;
      const marker = svgElement('circle', {
        class: 'sjs-bufkit-map-marker',
        'data-station-code': station.code,
        cx: x,
        cy: y,
        r: markerRadius,
        tabindex: 0,
        role: 'button',
        'aria-label': stationAriaLabel(station),
      });
      svg.append(marker);
    }
  }

  /**
   * Prevents a station marker press from starting a map pan.
   * @param {PointerEvent} event Marker pointer event.
   * @returns {void}
   */
  function handleMarkerPointerDown(event) {
    if (event.target.classList.contains('sjs-bufkit-map-marker')) event.stopPropagation();
  }

  /**
   * Selects the station represented by an activated SVG marker.
   * @param {Event} event Marker activation event.
   * @returns {void}
   */
  function handleMarkerClick(event) {
    const code = event.target.dataset.stationCode;
    const station = stationsByCode.get(code);
    if (station) selectStation(event, station);
  }

  /**
   * Shows station metadata when a marker is hovered or receives keyboard focus.
   * @param {Event} event Bubbling pointer or focus event.
   * @returns {void}
   */
  function handleMarkerHover(event) {
    const station = stationsByCode.get(event.target.dataset.stationCode);
    if (station) showTooltip(event, station);
  }

  /**
   * Hides marker metadata when the pointer or keyboard focus leaves the marker.
   * @param {Event} event Bubbling pointer or focus event.
   * @returns {void}
   */
  function handleMarkerLeave(event) {
    const marker = event.target.closest?.('.sjs-bufkit-map-marker');
    const next = event.relatedTarget?.closest?.('.sjs-bufkit-map-marker');
    if (marker && marker !== next) hideTooltip();
  }

  /**
   * Activates a focused station marker with Enter or Space.
   * @param {KeyboardEvent} event Marker keyboard event.
   * @returns {void}
   */
  function handleMarkerKeyDown(event) {
    if (event.key !== 'Enter' && event.key !== ' ') return;
    const station = stationsByCode.get(event.target.dataset.stationCode);
    if (!station) return;
    event.preventDefault();
    selectStation(event, station);
  }

  /**
   * Formats the four station fields shown to users and assistive technology.
   * @param {{code:string, city:string, state:string, country:string}} station Station metadata.
   * @returns {string} Accessible station description.
   */
  function stationAriaLabel(station) {
    return `${station.city}, ${station.state}, ${station.country}; station ${station.code}`;
  }

  /**
   * Positions and displays the station information tooltip.
   * @param {Event} event Pointer or focus event.
   * @param {{code:string, city:string, state:string, country:string}} station Station metadata.
   * @returns {void}
   */
  function showTooltip(event, station) {
    tooltip.replaceChildren();
    const city = doc.createElement('strong');
    city.textContent = station.city;
    tooltip.append(city, doc.createElement('br'));
    tooltip.append(doc.createTextNode(`State: ${station.state} · Country: ${station.country}`), doc.createElement('br'));
    tooltip.append(doc.createTextNode(`Station code: ${station.code}`));
    tooltip.style.display = 'block';
    moveTooltip(event);
  }

  /**
   * Moves the tooltip near a pointer event while keeping it within the map viewport.
   * @param {Event} event Pointer or focus event.
   * @returns {void}
   */
  function moveTooltip(event) {
    const rect = viewport.getBoundingClientRect();
    const clientX = Number.isFinite(event.clientX) ? event.clientX : rect.left + rect.width / 2;
    const clientY = Number.isFinite(event.clientY) ? event.clientY : rect.top + rect.height / 2;
    tooltip.style.left = `${Math.max(4, Math.min(rect.width - 270, clientX - rect.left + 12))}px`;
    tooltip.style.top = `${Math.max(4, Math.min(rect.height - 75, clientY - rect.top + 12))}px`;
  }

  /**
   * Hides the currently visible station tooltip.
   * @returns {void}
   */
  function hideTooltip() {
    tooltip.style.display = 'none';
  }

  /**
   * Writes a clicked station code to the controls and closes the picker.
   * @param {Event} event Click or keyboard activation event.
   * @param {{code:string}} station Selected station metadata.
   * @returns {void}
   */
  function selectStation(event, station) {
    event.stopPropagation();
    onSelect(station.code);
    dialog.close();
  }

  /**
   * Filters map stations by code and place fields, then pans to a unique match.
   * @returns {void}
   */
  function filterStations() {
    const query = search.value.trim().toLocaleLowerCase();
    if (!query) {
      visibleStations = stations;
      status.textContent = `${stations.length} BUFKIT locations shown. Drag to pan or use the wheel to zoom.`;
      drawMap();
      return;
    }
    const matches = [];
    for (const station of stations) {
      const description = `${station.code} ${station.city} ${station.state} ${station.country}`.toLocaleLowerCase();
      if (description.includes(query)) matches.push(station);
    }
    visibleStations = matches;
    if (matches.length === 1) {
      center = { lat: matches[0].lat, lon: matches[0].lon };
      zoom = Math.max(zoom, 6);
    }
    status.textContent = `${matches.length} matching ${matches.length === 1 ? 'station' : 'stations'} shown.`;
    drawMap();
  }

  /**
   * Starts a pointer drag unless the user pressed a station marker.
   * @param {PointerEvent} event Pointer down event.
   * @returns {void}
   */
  function startPan(event) {
    if (event.target.classList.contains('sjs-bufkit-map-marker')) return;
    pointer = { id: event.pointerId, x: event.clientX, y: event.clientY };
    dragging = true;
    viewport.classList.add('is-dragging');
    viewport.setPointerCapture(event.pointerId);
  }

  /**
   * Updates the map center while dragging the pointer.
   * @param {PointerEvent} event Pointer move event.
   * @returns {void}
   */
  function panMap(event) {
    if (!dragging || !pointer || event.pointerId !== pointer.id) return;
    const dx = event.clientX - pointer.x;
    const dy = event.clientY - pointer.y;
    const centerPoint = project(center.lat, center.lon, zoom);
    center = unproject(centerPoint.x - dx, centerPoint.y - dy, zoom);
    pointer.x = event.clientX;
    pointer.y = event.clientY;
    drawMap();
  }

  /**
   * Finishes a pointer drag and restores the map cursor.
   * @param {PointerEvent} event Pointer up or cancellation event.
   * @returns {void}
   */
  function stopPan(event) {
    if (pointer && event.pointerId !== pointer.id) return;
    pointer = null;
    dragging = false;
    viewport.classList.remove('is-dragging');
  }

  /**
   * Changes zoom while keeping the current map center fixed.
   * @param {number} delta Zoom increment, normally 1 or -1.
   * @returns {void}
   */
  function changeZoom(delta) {
    zoom = Math.max(2, Math.min(10, zoom + delta));
    drawMap();
  }

  /**
   * Closes the map dialog from its close button.
   * @returns {void}
   */
  function closeDialog() {
    dialog.close();
  }

  /**
   * Handles zoom-in button activation.
   * @returns {void}
   */
  function zoomIn() {
    changeZoom(1);
  }

  /**
   * Handles zoom-out button activation.
   * @returns {void}
   */
  function zoomOut() {
    changeZoom(-1);
  }

  /**
   * Zooms in or out in response to the mouse wheel.
   * @param {WheelEvent} event Wheel event over the map.
   * @returns {void}
   */
  function zoomByWheel(event) {
    event.preventDefault();
    changeZoom(event.deltaY < 0 ? 1 : -1);
  }

  button.addEventListener('click', openMap);
  dialog.querySelector('.sjs-bufkit-map-close').addEventListener('click', closeDialog);
  dialog.querySelector('[data-zoom="in"]').addEventListener('click', zoomIn);
  dialog.querySelector('[data-zoom="out"]').addEventListener('click', zoomOut);
  search.addEventListener('input', filterStations);
  svg.addEventListener('pointerdown', handleMarkerPointerDown);
  svg.addEventListener('click', handleMarkerClick);
  svg.addEventListener('mouseover', handleMarkerHover);
  svg.addEventListener('mousemove', moveTooltip);
  svg.addEventListener('mouseout', handleMarkerLeave);
  svg.addEventListener('focusin', handleMarkerHover);
  svg.addEventListener('focusout', handleMarkerLeave);
  svg.addEventListener('keydown', handleMarkerKeyDown);
  viewport.addEventListener('pointerdown', startPan);
  viewport.addEventListener('pointermove', panMap);
  viewport.addEventListener('pointerup', stopPan);
  viewport.addEventListener('pointercancel', stopPan);
  viewport.addEventListener('wheel', zoomByWheel, { passive: false });
  dialog.addEventListener('close', hideTooltip);
  if (typeof ResizeObserver === 'function') {
    resizeObserver = new ResizeObserver(drawMap);
    resizeObserver.observe(viewport);
  }

  /**
   * Removes the picker dialog, button, and resize observer.
   * @returns {void}
   */
  function destroyPicker() {
    destroyed = true;
    resizeObserver?.disconnect();
    dialog.close();
    dialog.remove();
    button.remove();
  }

  return { button, destroy: destroyPicker };
}

/**
 * Finds recent BUFKIT archive listings for all cycles on the selected run date.
 * @param {string} model BUFKIT model directory.
 * @param {Date} run Selected run date.
 * @returns {Promise<string[]>} Available station ids in the model archive.
 */
async function listBufkitStationCodes(model, run) {
  if (!(run instanceof Date) || Number.isNaN(run.getTime())) {
    throw new Error('Choose a valid BUFKIT run date before opening the station map.');
  }
  const selectedRun = run;
  const cacheKey = `${model}:${selectedRun.toISOString().slice(0, 10)}`;
  if (stationCatalogByModelRun.has(cacheKey)) return stationCatalogByModelRun.get(cacheKey);

  const request = Promise.all([
    fetchBufkitDirectory(model, selectedRun, 0),
    //fetchBufkitDirectory(model, selectedRun, 6),
    //fetchBufkitDirectory(model, selectedRun, 12),
    //fetchBufkitDirectory(model, selectedRun, 18),
  ]).then(combineStationLists);
  stationCatalogByModelRun.set(cacheKey, request);
  try {
    return await request;
  } catch (error) {
    stationCatalogByModelRun.delete(cacheKey);
    throw error;
  }
}

/**
 * Merges station IDs returned by the four possible model initialization cycles.
 * @param {string[][]} lists BUFKIT directory station lists.
 * @returns {string[]} Sorted unique station IDs.
 */
function combineStationLists(lists) {
  return [...new Set(lists.flat())].sort();
}

/**
 * Fetches and parses one IEM model-run directory listing.
 * @param {string} model BUFKIT model directory.
 * @param {Date} run Selected run date.
 * @param {number} hour Forecast initialization hour in UTC.
 * @returns {Promise<string[]>} Station ids found in the directory.
 */
async function fetchBufkitDirectory(model, run, hour) {
  const day = new Date(Date.UTC(run.getUTCFullYear(), run.getUTCMonth(), run.getUTCDate()));
  const datePath = `${day.getUTCFullYear()}/${pad(day.getUTCMonth() + 1)}/${pad(day.getUTCDate())}`;
  const directory = `${IEM_BUFKIT}/${datePath}/bufkit/${pad(hour)}/${encodeURIComponent(model)}/`;
  const response = await fetch(directory);
  if (response.status === 404) return [];
  if (!response.ok) throw new Error(`Could not load the IEM BUFKIT station list (HTTP ${response.status}).`);

  const html = await response.text();
  const prefix = model === 'gfs' ? 'gfs3' : model === 'nam' && (hour === 6 || hour === 18) ? 'namm' : model;
  const ids = [];
  const listing = new DOMParser().parseFromString(html, 'text/html');
  for (const link of listing.querySelectorAll('a')) {
    const fileName = link.textContent.trim();
    if (!fileName.toLowerCase().endsWith('.buf')) continue;
    const stem = fileName.slice(0, -4);
    const marker = `${prefix}_`;
    if (stem.toLowerCase().startsWith(marker.toLowerCase())) {
      const code = stem.slice(marker.length).trim();
      if (code) ids.push(code);
    }
  }
  return ids;
}

/**
 * Retrieves and caches the public Aviation Weather Center station metadata.
 * @returns {Promise<object[]>} Station records with identifiers and geographic metadata.
 */
async function getAviationStationCatalog() {
  aviationStationCatalog ??= fetch(AVIATION_STATIONS)
    .then(parseAviationStationResponse)
    .then(validateAviationStationRecords)
    .catch(resetAviationStationCatalog);
  return aviationStationCatalog;
}

/**
 * Parses the HTTP response from the aviation station catalog.
 * @param {Response} response Station catalog response.
 * @returns {Promise<unknown>} Parsed station JSON.
 */
function parseAviationStationResponse(response) {
  if (!response.ok) throw new Error(`Could not load station location metadata (HTTP ${response.status}).`);
  return response.json();
}

/**
 * Ensures the aviation station catalog has the expected array shape.
 * @param {unknown} records Parsed station data.
 * @returns {object[]} Valid station records.
 */
function validateAviationStationRecords(records) {
  if (!Array.isArray(records)) throw new Error('The station location service returned an invalid catalog.');
  return records;
}

/**
 * Clears a rejected catalog request so that a later map opening can retry.
 * @param {Error} error Catalog loading error.
 * @returns {never} Rethrows the loading error.
 */
function resetAviationStationCatalog(error) {
  aviationStationCatalog = null;
  throw error;
}

/**
 * Matches IEM BUFKIT ids to aviation station records and keeps map-ready entries.
 * @param {string[]} codes BUFKIT ids taken from the IEM file names.
 * @param {object[]} records Aviation station catalog.
 * @returns {Array<{code:string, city:string, state:string, country:string, lat:number, lon:number}>} Mapped BUFKIT sites.
 */
function matchStationMetadata(codes, records) {
  const index = new Map();
  /**
   * Adds a normalized identifier unless another station already owns it.
   * @param {string} id Station identifier.
   * @param {object} record Station metadata.
   * @returns {void}
   */
  function addId(id, record) {
    const key = String(id || '').trim().toUpperCase();
    if (key && !index.has(key)) index.set(key, record);
  }
  for (const record of records) {
    for (const key of ['icaoId', 'faaId', 'iataId', 'id', 'wmoId']) addId(record[key], record);
  }
  for (const record of records) {
    for (const key of ['icaoId', 'faaId', 'iataId', 'id', 'wmoId']) {
      const id = String(record[key] || '').trim().toUpperCase();
      if (/^K[A-Z0-9]{3}$/.test(id)) addId(id.slice(1), record);
      if (/^[A-Z0-9]{3}$/.test(id)) addId(`K${id}`, record);
    }
  }

  const mapped = [];
  for (const code of codes) {
    const record = index.get(String(code).toUpperCase());
    const lat = Number(record?.lat);
    const lon = Number(record?.lon);
    if (!record || !Number.isFinite(lat) || !Number.isFinite(lon)) continue;
    mapped.push({
      code,
      city: String(record.site || code).trim(),
      state: stationStateName(record.state, record.country),
      country: countryName(record.country),
      lat,
      lon,
    });
  }
  return mapped.sort(compareStationCodes);
}

/**
 * Expands US state and territory abbreviations while retaining other region labels.
 * @param {string} state State or province code from the station catalog.
 * @param {string} country Country code from the station catalog.
 * @returns {string} Human-readable state or province name.
 */
function stationStateName(state, country) {
  const code = String(state || '').trim().toUpperCase();
  if (!code) return 'Unknown';
  return String(country || '').toUpperCase() === 'US' ? US_STATE_NAMES[code] || code : code;
}

/**
 * Expands a country code using the browser's localized region-name data.
 * @param {string} country Country code from the station catalog.
 * @returns {string} Human-readable country name, or the source code if unknown.
 */
function countryName(country) {
  const code = String(country || '').trim().toUpperCase();
  if (!code) return 'Unknown';
  const displayNames = new Intl.DisplayNames(['en'], { type: 'region' });
  return displayNames.of(code) || code;
}

/**
 * Sorts mapped stations by their BUFKIT station code.
 * @param {{code:string}} a First station.
 * @param {{code:string}} b Second station.
 * @returns {number} Locale-aware ordering result.
 */
function compareStationCodes(a, b) {
  return a.code.localeCompare(b.code);
}

/**
 * Formats an integer as a zero-padded UTC date or hour component.
 * @param {number} value Integer to format.
 * @returns {string} Two-character string.
 */
function pad(value) {
  return String(value).padStart(2, '0');
}