From 7e0723a776efbfdfbf0d1462f8ffdbb323dde2a3 Mon Sep 17 00:00:00 2001 From: vkuchenik Date: Fri, 28 Aug 2026 13:11:56 -0700 Subject: [PATCH] add - descriptive tooltips and default to most recent vaa on mount --- client/src/components/Widgets/VaaMap.jsx | 111 ++++++++++++------ .../Widgets/settings/VaaMapSettings.jsx | 53 ++++++--- client/src/constants/widgetRegistry.jsx | 5 +- 3 files changed, 115 insertions(+), 54 deletions(-) diff --git a/client/src/components/Widgets/VaaMap.jsx b/client/src/components/Widgets/VaaMap.jsx index 61a6bdd..1ea059e 100644 --- a/client/src/components/Widgets/VaaMap.jsx +++ b/client/src/components/Widgets/VaaMap.jsx @@ -1,4 +1,4 @@ -import React, { useRef, useMemo, useEffect } from "react"; +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"; @@ -25,15 +25,15 @@ export default function VaaMap({ settings, widgetID}) { "obs_h_avs_m": 504.0, "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 " + "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": "Aira", + "volc_name": "Sabancaya", "volc_elev_m": 1117, "vaac_location": "Tokyo", "obs_va_datetime": "2026-08-25T00:50:00Z", @@ -45,27 +45,25 @@ export default function VaaMap({ settings, widgetID}) { "obs_h_avs_m": 4369, "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" + "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, selected} = useMemo(() => { + 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; - const selected = settings?.selected; - return {data, isLoading, error, selected}; - }, [settings]); - + 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 []; - return mock_data.map((vaa) => { + 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); @@ -79,6 +77,12 @@ export default function VaaMap({ settings, widgetID}) { 2: { fillColor: 'blue' }, } + const hourCode = { + 0: "+6 hours", + 1: "+12 hours", + 2: "+18 hours", + } + if (isLoading) return ( @@ -93,14 +97,12 @@ export default function VaaMap({ settings, widgetID}) { if (data?.length === 0) return ( - No flight level data available. + No VAA data available. ); return ( - {polygons?.map((polygon, index) => { + {polygons?.map((polygon) => { return polygon?.projections?.map((coordinates, index) => - {polygon?.volc_name} + + + + {polygon?.volc_name} + {hourCode[index]} + + + VAA Datetime: + {polygon?.vaa_datetime} + + + VAAC Location: + {polygon?.vaac_location} + + + Height From Vent: + {polygon?.obs_h_avs_m} (m) + + + ) })} - + ) } @@ -126,10 +147,7 @@ export default function VaaMap({ settings, widgetID}) { function convertDMtoDD(DMString) { const cleaned_DMString = DMString.trim().replace(/-/g, "").split(/\s+/); - const datetime = cleaned_DMString[0]; - const flight_level = Number(String(cleaned_DMString[1]).slice(6)) * 100; // in feet - - const coordinates = cleaned_DMString.slice(2).map((coordinate, index) => { + 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)); @@ -146,30 +164,49 @@ function convertDMtoDD(DMString) { return polygon; } -function ZoomToPolygon({ polygons, selected, settings }) { +function ZoomToPolygon({ polygons, settings }) { const map = useMap(); useEffect(() => { let targetBounds; + let default_vaa_object; + const selectedVAA = settings?.selected; + const defaultVAA = settings?.defaultVAA; - if (!selected && settings?.position) targetBounds = settings.position; - else if (selected) { - const vaaObject = polygons.find((vaa) => vaa.volc_name === selected); - targetBounds = vaaObject?.projections?.[0]; + // 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, - }); + 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, selected, polygons, settings]); + }, [map, polygons, settings.position, settings.selected, settings.defaultVAA]); } \ No newline at end of file diff --git a/client/src/components/Widgets/settings/VaaMapSettings.jsx b/client/src/components/Widgets/settings/VaaMapSettings.jsx index b890c3d..15fe27f 100644 --- a/client/src/components/Widgets/settings/VaaMapSettings.jsx +++ b/client/src/components/Widgets/settings/VaaMapSettings.jsx @@ -11,32 +11,55 @@ export default function VaaMapSettings({widgetId, currentSettings}) { const url = useBuildUrl("api/v2/comm_coor_vaa", currentSettings?.startDate, currentSettings?.endDate); const {data, isLoading, error} = VdapApi(url); - const unique_data = useMemo(() => { - if (!data || !Array.isArray(data)) return []; - const key = 'volc_name'; - return [...new Map(data.map(item => [item[key], item])).values()]; + // retrieving only the latest VAA for each volcano (resultant has no order) + const latest_vaas = useMemo(() => { + if (!data || !Array.isArray(data)) return undefined; + // retrieves last occurance of vaa, so the vaa's need to stay in rev-chrono order here to work + return [...new Map(data.map(item => [item['volc_name'], item])).values()]; }, [data]); - const filteredItems = useMemo(() => { - if (!inputValue.trim()) return unique_data; - return unique_data.filter((item) => item.volc_name.toLowerCase().includes(inputValue.toLowerCase())); - }, [unique_data, inputValue]); + useEffect(() => { + if (!latest_vaas) updateSettings(widgetId, {isLoading: isLoading, error: error}); + else updateSettings(widgetId, {data: latest_vaas, isLoading: isLoading, error: error}); + }, [updateSettings, widgetId, latest_vaas, isLoading, error]); - const collection = useMemo(() => { + // find volc_name of last issued vaa, map will default to this ash polygon + const default_vaa = useMemo(() => { + if (!latest_vaas || latest_vaas.length === 0) return undefined; + const chrono_order = latest_vaas.sort((a, b) => new Date(b.vaa_datetime) - new Date(a.vaa_datetime)); + // console.log(chrono_order); + // console.log(chrono_order[0].volc_name); + return chrono_order[0].volc_name; + }, [latest_vaas]); + + // set default vaa in settings + useEffect(() => { + if (default_vaa) updateSettings(widgetId, {defaultVAA: default_vaa}); + }, [default_vaa, updateSettings, widgetId]); + + + // filtering based on what user types in search bar + const filtered_vaas = useMemo(() => { + if (!inputValue.trim()) return latest_vaas; + return latest_vaas?.filter((item) => item.volc_name.toLowerCase().includes(inputValue.toLowerCase())); + }, [latest_vaas, inputValue]); + + const vaa_collection = useMemo(() => { return createListCollection({ - items: filteredItems.sort((a, b) => a.volc_name.localeCompare(b.volc_name)), + items: filtered_vaas?.sort((a, b) => a.volc_name.localeCompare(b.volc_name)), itemToString: (item) => item.volc_name, itemToValue: (item) => item.volc_name, }); - }, [filteredItems]); + }, [filtered_vaas]); + // update state w/ selected vaa so map zooms to polygon useEffect(() => { - updateSettings(widgetId, {data: unique_data, isLoading: isLoading, error: error, selected: inputValue}); - }, [updateSettings, widgetId, unique_data, isLoading, error, inputValue]); + updateSettings(widgetId, {selected: inputValue}); + }, [updateSettings, widgetId, inputValue]); return ( - setInputValue(e.inputValue)}> + setInputValue(e.inputValue)}> Select a Volcano @@ -49,7 +72,7 @@ export default function VaaMapSettings({widgetId, currentSettings}) { No VAA's found - {collection.items.map((vaa) => + {vaa_collection.items.map((vaa) => {vaa?.volc_name} diff --git a/client/src/constants/widgetRegistry.jsx b/client/src/constants/widgetRegistry.jsx index 11cc95c..da9466a 100644 --- a/client/src/constants/widgetRegistry.jsx +++ b/client/src/constants/widgetRegistry.jsx @@ -185,19 +185,20 @@ export const WIDGET_REGISTRY = { }, "vaa_map": { - name: "VAA Ash Polygon Map", + name: "VAA Polygon Projections Map", category: "Maps", description: "By selecting a volcano, the map will take you to it and show a polygon of the ash in 8, 12, and 18 hours", component: lazy(() => import('../components/Widgets/VaaMap.jsx')), settingsComponent: lazy(() => import('../components/Widgets/settings/VaaMapSettings.jsx')), defaultSettings: { - position: [45.61142, -122.49630], + position: [45.61142, -122.49630], // CVO's coordinates startDate: getToday(), endDate: getToday(), data: [], isLoading: false, isError: undefined, selected: undefined, + defaultVAA: undefined, }, allowedSizes: { medium: { w: 6, h: 12 }