import { useRef, useMemo, useEffect } from "react"; import {MapContainer, useMap, TileLayer, Polygon, Tooltip} from "react-leaflet"; import "leaflet/dist/leaflet.css"; import {Flex, Spinner, Text} from "@chakra-ui/react"; import L from 'leaflet'; export default function VaaMap({ settings, widgetID}) { const mapRef = useRef(null); // TEST DATA const mock_data = useMemo( () => [ { "vaa_datetime": "2026-08-18T18:01:00Z", "vnum": 342090, "volc_name": "Fuego", "volc_elev_m": 3763, "vaac_location": "Washington", "obs_va_datetime": "2026-08-18T17:30:00Z", "obs_va_epoch": 1787074200000, "aviation_color_code": null, "obs_fl": "140", "obs_fl_dir": "SW", "obs_h_asl_m": 4267.0, "obs_h_avs_m": 504.0, "projections": { "six": "25/2030Z SFC/FL160 N1446 W09202 - N1428 W09052 - N1423 W09200 - N1446 W09202", "twelve": "26/0230Z SFC/FL160 N1444 W09203 - N1428 W09052 - N1422 W09201 - N1444 W09203", "eighteen": "26/0830Z SFC/FL160 N1441 W09204 - N1428 W09052 - N1419 W09202 - N1441 W09204" } }, { "vaa_datetime": "2026-08-25T01:01:00Z", "vnum": 282080, "volc_name": "Sabancaya", "volc_elev_m": 1117, "vaac_location": "Tokyo", "obs_va_datetime": "2026-08-25T00:50:00Z", "obs_va_epoch": 1787619000000, "aviation_color_code": null, "obs_fl": "180", "obs_fl_dir": null, "obs_h_asl_m": 5486, "obs_h_avs_m": 4369, "projections": { "six": "14/0200Z SFC/FL250 S1532 W07036 - S1552 W07029 - S1613 W07035 - S1546 W07150 - S1532 W07036 ", "twelve": "14/0800Z SFC/FL250 S1513 W07045 - S1527 W07033 - S1551 W07029 - S1544 W07150 - S1513 W07045 ", "eighteen": "14/1400Z SFC/FL250 S1511 W07044 - S1531 W07028 - S1550 W07027 - S1546 W07151 - S1511 W07044 " }, }, ] , []); const {data, isLoading, error} = useMemo(() => { const data = settings?.data?.sort((a, b) => new Date(b.vaa_datetime) - new Date(a.vaa_datetime)); const isLoading = settings?.isLoading; const error = settings?.error; return {data, isLoading, error}; }, [settings.data, settings.isLoading, settings.error]); // convert Degree & Minutes (DM) to Decimal Degrees (DD) const polygons = useMemo(() => { if (!mock_data) return []; // TODO: replace with real data return mock_data.map((vaa) => { // TODO: replace with real data const six_hrs = convertDMtoDD(vaa.projections.six); const twelve_hrs = convertDMtoDD(vaa.projections.twelve); const eighteen_hrs = convertDMtoDD(vaa.projections.eighteen); return {...vaa, projections: [six_hrs, twelve_hrs, eighteen_hrs]}; }) }, [mock_data]); const lineColors= { 0: { color: 'red' }, 1: { color: 'purple' }, 2: { fillColor: 'blue' }, } const hourCode = { 0: "+6 hours", 1: "+12 hours", 2: "+18 hours", } if (isLoading) return ( ); if (error) return ( {error} ); if (data?.length === 0) return ( No VAA data available. ); return ( {polygons?.map((polygon) => { return polygon?.projections?.map((coordinates, index) => {polygon?.volc_name} {hourCode[index]} VAA Datetime: {polygon?.vaa_datetime} VAAC Location: {polygon?.vaac_location} Height From Vent: {polygon?.obs_h_avs_m} (m) ) })} ) } function convertDMtoDD(DMString) { const cleaned_DMString = DMString.trim().replace(/-/g, "").split(/\s+/); const coordinates = cleaned_DMString.slice(2).map((coordinate) => { const direction = coordinate[0]; if (direction === "N" || direction === "S") { // 4 digits long const lat = Number(coordinate.slice(1, 3)) + Number((Number(coordinate.slice(3, 5)) / 60).toFixed(5)); return direction === "S" ? -lat : lat; // south has negative coordinates } else if (direction === "E" || direction === "W") { // 5 digits long const long = Number(coordinate.slice(1, 4)) + Number((Number(coordinate.slice(4, 6)) / 60).toFixed(5)); return direction === "W" ? -long : long; // west has negative coordinates } }); // pair up lat and long into their own array pairs let polygon = []; for (let i = 0; i < coordinates.length; i+=2) polygon.push([coordinates[i], coordinates[i+1]]); return polygon; } function ZoomToPolygon({ polygons, settings }) { const map = useMap(); useEffect(() => { let targetBounds; let default_vaa_object; const selectedVAA = settings?.selected; const defaultVAA = settings?.defaultVAA; // selectedVAA and defualtVAA are filled in dynamically so they may not always be defined, defualt to hardcoded CVO coordinates if (!selectedVAA && !defaultVAA) { targetBounds = [settings.position]; // console.log("zooming to cvo"); } // if defaultVAA is defined and user hasn't selected a vaa to pan to, then use defaultVAA coordinates else if (!selectedVAA) { // console.log("zooming to", defaultVAA); default_vaa_object = polygons.find((vaa) => vaa.volc_name === defaultVAA); // if for whatever reason defaultVAA isn't found in list of vaa data, default to hardcoded CVO coordinates default_vaa_object ? targetBounds = default_vaa_object?.projections?.[0] : targetBounds = [settings?.position]; } else if (selectedVAA) { // console.log("zooming to", selectedVAA); const vaa_object = polygons.find((vaa) => vaa.volc_name === selectedVAA); // if selected VAA is not found (say user just started typing in search) will default to defaultVAA, unless defualtVAA is also undefined vaa_object ? targetBounds = vaa_object?.projections?.[0] : (default_vaa_object ? targetBounds = default_vaa_object?.projections?.[0] : targetBounds = [settings?.position]); } if (targetBounds) { try { const bounds = L.latLngBounds(targetBounds); if (bounds.isValid()) map.fitBounds(bounds, {padding: [50, 50], animate: true, maxZoom: 8}); } catch (err) { console.warn('Invalid bounds structure passed to fitBounds:', targetBounds, err); } } }, [map, polygons, settings.position, settings.selected, settings.defaultVAA]); }