Source: map/renderConditions/yahooEEW/renderYahooEEW.js

import { map, mapboxgl } from "../../initMap.js";
import { internalBound } from "../../internal/internalBound.js";

const earthRadius = 6371.0087714;
const epicenterIconSize = 30 / 31;
const emptyFeatureCollection = Object.freeze({
  type: "FeatureCollection",
  features: [],
});

let currentYahooEEWBounds = null;
let epicenterImagePromise = null;

/**
 * Parses a coordinate string and converts it into decimal degrees.
 *
 * @private
 *
 * @param {String} coordStr Coordinate string (example: N11.2 or E32)
 *
 * @returns {Number|null} Parsed coordinate in decimal degrees, or null if invalid
 */
function parseCoordinate(coordStr) {
  if (!coordStr) return null;
  const direction = coordStr.charAt(0);
  const value = parseFloat(coordStr.substring(1));
  if (isNaN(value)) return null;
  return direction === "S" || direction === "W" ? -value : value;
}

/**
 * Creates a GeoJSON circle feature.
 *
 * @private
 *
 * @param {Array} center [longitude, latitude] of the circle's center
 * @param {Number} radiusInKm Radius of the circle in kilometers
 * @param {Number} points Number of points to use for the circle geometry
 * @returns {Object} GeoJSON feature representing the circle
 */
function createGeoJSONCircle(center, radiusInKm, points = 64) {
  const coords = [];
  const d = radiusInKm / earthRadius;
  const lat1 = (center[1] * Math.PI) / 180;
  const lon1 = (center[0] * Math.PI) / 180;
  const sinLat1 = Math.sin(lat1);
  const cosLat1 = Math.cos(lat1);
  const sinD = Math.sin(d);
  const cosD = Math.cos(d);

  for (let i = 0; i <= points; i++) {
    const brng = ((i * 360) / points) * (Math.PI / 180);
    const lat2 = Math.asin(sinLat1 * cosD + cosLat1 * sinD * Math.cos(brng));
    const lon2 =
      lon1 +
      Math.atan2(
        Math.sin(brng) * sinD * cosLat1,
        cosD - sinLat1 * Math.sin(lat2),
      );
    coords.push([(lon2 * 180) / Math.PI, (lat2 * 180) / Math.PI]);
  }

  return {
    type: "Feature",
    geometry: { type: "Polygon", coordinates: [coords] },
    properties: {},
  };
}

/**
 * Extends the given bounds to include the area covered by a circle.
 *
 * @private
 *
 * @param {Array} center [longitude, latitude] of the circle's center
 * @param {Number} radiusInKm Radius of the circle in kilometers
 * @param {mapboxgl.LngLatBounds} bounds Bounds to extend
 *
 * @returns {void}
 */
function extendBoundsWithCircle(center, radiusInKm, bounds) {
  const d = radiusInKm / earthRadius;
  const lat1 = (center[1] * Math.PI) / 180;
  const lon1 = (center[0] * Math.PI) / 180;
  const sinLat1 = Math.sin(lat1);
  const cosLat1 = Math.cos(lat1);
  const sinD = Math.sin(d);
  const cosD = Math.cos(d);

  for (const brng of [0, Math.PI / 2, Math.PI, (3 * Math.PI) / 2]) {
    const lat2 = Math.asin(sinLat1 * cosD + cosLat1 * sinD * Math.cos(brng));
    const lon2 =
      lon1 +
      Math.atan2(
        Math.sin(brng) * sinD * cosLat1,
        cosD - sinLat1 * Math.sin(lat2),
      );
    bounds.extend([(lon2 * 180) / Math.PI, (lat2 * 180) / Math.PI]);
  }
}

/**
 * Ensures that the epicenter icon image is loaded and added to the map.
 *
 * @private
 *
 * @returns {Promise<void>} Resolves when the image is loaded and added to the map, or if it already exists
 */
function ensureEpicenterImage() {
  if (!epicenterImagePromise) {
    epicenterImagePromise = new Promise((resolve) => {
      map.loadImage("/assets/basemap/icons/epicenter.png", (error, image) => {
        if (error) {
          console.error(
            "[renderYahooEEW] Failed to load epicenter icon",
            error,
          );
          resolve();
          return;
        }
        if (!map.hasImage("epicenter")) map.addImage("epicenter", image);
        resolve();
      });
    });
  }
  return epicenterImagePromise;
}

/**
 * Initializes the necessary sources (if they don't exist) and layers for rendering Yahoo EEW data on the map.
 *
 * @private
 *
 * @returns {void}
 */
function initSources() {
  const waveLayerDefs = [
    {
      id: "yahoo-eew-pwave",
      paint: {
        "line-color": "#35b4fb",
        "line-width": 1,
        "line-emissive-strength": 1,
      },
    },
    {
      id: "yahoo-eew-swave",
      paint: {
        "line-color": "#f6521f",
        "line-width": 1,
        "line-emissive-strength": 1,
      },
    },
  ];

  for (const { id, paint } of waveLayerDefs) {
    if (!map.getSource(id)) {
      map.addSource(id, { type: "geojson", data: emptyFeatureCollection });
      map.addLayer({ id, type: "line", source: id, paint });
    }
  }

  if (!map.getSource("yahoo-eew-epicenter")) {
    map.addSource("yahoo-eew-epicenter", {
      type: "geojson",
      data: emptyFeatureCollection,
    });
    map.addLayer({
      id: "yahoo-eew-epicenter",
      type: "symbol",
      source: "yahoo-eew-epicenter",
      layout: {
        "icon-image": "epicenter",
        "icon-size": epicenterIconSize,
        "icon-allow-overlap": true,
      },
    });
  }
}

/**
 * Renders wave (a circle) on the map.
 *
 * @private
 *
 * @param {string} id Layer ID
 * @param {Array} center [longitude, latitude] of the wave's center
 * @param {Number} radius Radius of the wave in kilometers
 * @param {mapboxgl.LngLatBounds} bounds Bounds to extend
 *
 * @returns {void}
 */
function renderWave(id, center, radius, bounds) {
  const src = map.getSource(id);
  if (!src) return;

  if (!isNaN(radius) && radius > 0) {
    src.setData({
      type: "FeatureCollection",
      features: [createGeoJSONCircle(center, radius)],
    });
    extendBoundsWithCircle(center, radius, bounds);
  } else {
    src.setData(emptyFeatureCollection);
  }
}

/**
 * Returns the current bounds of the Yahoo EEW data on the map.
 *
 * @returns {mapboxgl.LngLatBounds|null} Current bounds of the Yahoo EEW layer
 */
export function getYahooEEWBounds() {
  return currentYahooEEWBounds;
}

/**
 * Clears the Yahoo EEW sources by setting them to empty data and resets the current bounds.
 *
 * @private
 *
 * @returns {void}
 */
function clearEEWSources() {
  ["yahoo-eew-pwave", "yahoo-eew-swave", "yahoo-eew-epicenter"].forEach(
    (id) => {
      const src = map.getSource(id);
      if (src) src.setData(emptyFeatureCollection);
    },
  );
  currentYahooEEWBounds = null;
}

/**
 * Renders Yahoo EEW data on the map.
 *
 * @param {Object} eewData JSON data containing EEW information
 *
 * @returns {Promise<void>}
 */
export async function renderYahooEEW(eewData) {
  if (!eewData || !eewData.psWave || !eewData.hypoInfo) {
    clearEEWSources();
    return;
  }

  const psWaveItem = eewData.psWave.items?.[0];
  const hypoItem = eewData.hypoInfo.items?.[0];

  if (!psWaveItem || !hypoItem) {
    clearEEWSources();
    return;
  }

  const epicenterLat = parseCoordinate(hypoItem.latitude);
  const epicenterLng = parseCoordinate(hypoItem.longitude);

  if (epicenterLat === null || epicenterLng === null) {
    clearEEWSources();
    return;
  }

  const center = [epicenterLng, epicenterLat];
  const pRadius = parseFloat(psWaveItem.pRadius);
  const sRadius = parseFloat(psWaveItem.sRadius);

  await ensureEpicenterImage();
  initSources();

  const bounds = new mapboxgl.LngLatBounds();
  bounds.extend(center);

  renderWave("yahoo-eew-pwave", center, pRadius, bounds);
  renderWave("yahoo-eew-swave", center, sRadius, bounds);

  map.getSource("yahoo-eew-epicenter").setData({
    type: "FeatureCollection",
    features: [
      {
        type: "Feature",
        geometry: { type: "Point", coordinates: center },
        properties: {},
      },
    ],
  });

  currentYahooEEWBounds = bounds;
  internalBound(bounds);
}