diff --git a/src/app.js b/src/app.js index 7c0e64b1..4425dab2 100644 --- a/src/app.js +++ b/src/app.js @@ -3,6 +3,7 @@ import { Map } from '@components/map'; import { ObservationDialog } from "@components/dialog/observation-dialog"; import { useLayers } from '@context'; import { Sidebar } from '@components/sidebar'; +import { ControlPanel } from '@components/control-panel'; export const App = () => { // install the selected observation list from the layer context @@ -21,6 +22,7 @@ export const App = () => { } + ); }; diff --git a/src/components/control-panel/control-panel.js b/src/components/control-panel/control-panel.js new file mode 100644 index 00000000..92436109 --- /dev/null +++ b/src/components/control-panel/control-panel.js @@ -0,0 +1,328 @@ +import React, { useEffect } from 'react'; +import axios from 'axios'; +import { useLayers } from '@context/map-context'; +import { useQuery } from '@tanstack/react-query'; +import { + Box, + Card, + Divider, + IconButton, + Stack, + Switch, + ToggleButtonGroup, + Typography, +} from '@mui/joy'; +import { + Air as MaxWindVelocityIcon, + Flood as MaxInundationIcon, + KeyboardArrowLeft, + KeyboardArrowRight, + Tsunami as SwanIcon, + Water as MaxElevationIcon, +} from '@mui/icons-material'; +import apsLogo from '@images/aps-trans-logo.png'; + +const layerIcons = { + maxele63: , + maxwvel63: , + swan_HS_max63: , + maxinundepth63: , +}; + +export const ControlPanel = () => { + + //const { defaultModelLayers, setDefaultModelLayers, toggleLayerVisibility, makeAllLayersInvisible } = useLayers(); + const { defaultModelLayers, setDefaultModelLayers, toggleLayerVisibility } = useLayers(); + + const data_url = `${process.env.REACT_APP_UI_DATA_URL}get_ui_data?limit=1&use_v3_sp=true`; + //const gs_wms_url = `${process.env.REACT_APP_GS_DATA_URL}wms`; + + const layers = [...defaultModelLayers]; + const maxele_layer = layers.find((layer) => layer.properties.product_type === "maxele63"); + const obs_layer = layers.find((layer) => layer.properties.product_type === "obs"); + + const [currentLayerSelection, setCurrentLayerSelection] = React.useState('maxele63'); + const [checked, setChecked] = React.useState(true); + + // keep track of which model run to retrieve + const [ runCycle, setRunCycle] = React.useState(0); + const [ runDate, setRunDate] = React.useState(""); + const [ instanceName, setInstanceName] = React.useState(""); + const [ metClass, setMetClass] = React.useState(""); + const [ eventType, setEventType] = React.useState(""); + const [ topLayers, setTopLayers ] = React.useState([]); + + // when cycle buttons are pushed on the control panel + // either the previous or next cycle of the displayed + // adcirc model run will be displayed on the map + // if the run is not already in memory, it will have + // to be retrieved from the get_ui_data api + // retrieve adcirc data and layers from filter data provided + // then populate default layers state + // all params are strings, and runDate format is YYYY-MM-DD + const [filters, setFilters] = React.useState(); + const [initialDataFetched, setInitialDataFetched] = React.useState(false); + + const newLayerDefaultState = (layer) => { + const { product_type } = layer.properties; + + if (['obs', currentLayerSelection].includes(product_type)) { + return ({ + visible: true, + }); + } + + return ({ + visible: false, + }); + }; + + const parseAndAddLayers = (d) => { + + // first see if this set of layers already exists in default layers + if (d.catalog[0].members && defaultModelLayers.find(layer => layer.id === d.catalog[0].members[0].id)) { + console.log("already have this one"); + } + // if not, add these layers to default layers + else { + // add visibity state property to retrieved catalog layers + const newLayers = []; + d.catalog[0].members.forEach((layer) => { + newLayers.push({ + ...layer, + state: newLayerDefaultState(layer) + }); + }); + //makeAllLayersInvisible(); + setTopLayers([...newLayers]); + setDefaultModelLayers([...newLayers, ...defaultModelLayers]); + } + }; + + // useQuery function + const setNewLayers = async() => { + // retrieve the set of layers for the new cycle + const { isError, data, error } = await axios.get(data_url, {params: filters}); + + if (isError) { + alert(error); + } else {} + // add data returned to default layers, if they are not already there. + parseAndAddLayers(data); + + return(data); + }; + useQuery( {queryKey: ['apsviz-data', filters], queryFn: setNewLayers, enabled: !!filters}); + + + const date2String = (date) => { + const str = date.getFullYear() + + '-' + String(date.getMonth() + 1).padStart(2, '0') + + '-' + String(date.getDate()).padStart(2, '0'); + + return str; + }; + + const string2Date = (str) => { + const dateParts = str.split('-'); + const newDate = new Date(dateParts[0], dateParts[1]-1, dateParts[2]); + + return newDate; + }; + + // set initial values the currently display layers + useEffect(() => { + if ((layers[0]) && (!initialDataFetched)) { + setInstanceName(layers[0].properties.instance_name); + setMetClass(layers[0].properties.met_class); + setEventType(layers[0].properties.event_type); + + setRunCycle(parseInt(layers[0].properties.cycle)); + setRunDate(layers[0].properties.run_date); + setInitialDataFetched(true); + setTopLayers([...defaultModelLayers]); + } + + }, [layers]); + + // switch to the model run layer selected via icon button + const layerChange = async (event, newValue) => { + + setCurrentLayerSelection(newValue); + // turn off the old + layers.map(layer => { + if (layer.layers.includes(currentLayerSelection)) { + toggleLayerVisibility(layer.id); + } + }); + // Yikes! need another way to do this - but it works for now + await new Promise(r => setTimeout(r, 1)); + // turn on the new + layers.map(layer => { + if (layer.layers.includes(newValue)) { + toggleLayerVisibility(layer.id); + } + }); + }; + + // switch on/off the observation layer, if it exists + const toggleObsLayer = (event) => { + setChecked(event.target.checked); + toggleLayerVisibility(obs_layer.id); + }; + + // cycle to the next model run cycle and retrieve the + // layers associated with that cycle/date + const changeModelRunCycle = (e) => { + + const direction = e.currentTarget.getAttribute("button-key"); + + // TODO: Need to update this to also support tropical storms + // const runId = layers[0].id.split('-')[0]; + // const metClass = layers[0].properties.met_class; + // const eventType = layers[0].properties.event_type; + const currentDate = string2Date(runDate); + let currentCycle = Number(runCycle) + 0; + + if (direction === "next") { + // set properties for next model run + if (currentCycle === 18) { // need to push date to next day + currentDate.setDate(currentDate.getDate() + 1); + currentCycle = 0; + } else { + currentCycle += 6; + } + } else { // previous + // set properties for previous model run + if (currentCycle === 0) { // need to push date to previous day + currentDate.setDate(currentDate.getDate() - 1); + currentCycle = 18; + } else { + currentCycle -= 6; + } + } + + setRunDate(date2String(currentDate)); + const cycle = String(currentCycle).padStart(2, '0'); + setRunCycle(cycle); + + const newFilters = {"instance_name": instanceName, + "met_class": metClass, + "event_type": eventType, + "run_date": date2String(currentDate), + "cycle": cycle + }; + + setFilters(newFilters); + }; + + return ( + + + + + + + { + layers.length && ( + + Model run date: {runDate} + + ) + } + + + + Cycle {runCycle} + + + + + + {/* TODO: NOTE: If this is a tropical storm run, we need to change cycle to advisoy + Also probabaly want to add a switch for hurricane layers - which + involves making a request to the MetGet API + Third need to implement actual code to load different model runs each time + up/down arrows are clicked. This has to time managed in some way so that + Geoserver is not inundated with requests */} + + { // grid name + layers.length && ( + {layers[0].properties.grid_type} grid + ) + } + + { // observations toggle + layers.some(layer => layer.properties.product_type === "obs") && ( + } + >Observations + ) + } + + {/* layer selection */} + + { // have to do wierd stuff to get maxele first and default button + maxele_layer && ( + + { layerIcons[maxele_layer.properties.product_type] } + + ) + } + { + topLayers + .filter(layer => layer.properties.product_type != "obs" && layer.properties.product_type != "maxele63") + .map(layer => ( + + { layerIcons[layer.properties.product_type] } + + )) + } + + + + ); +}; \ No newline at end of file diff --git a/src/components/control-panel/index.js b/src/components/control-panel/index.js new file mode 100644 index 00000000..06c1453e --- /dev/null +++ b/src/components/control-panel/index.js @@ -0,0 +1 @@ +export * from './control-panel'; \ No newline at end of file diff --git a/src/components/map/default-layers.js b/src/components/map/default-layers.js index dcb52627..e1a41f73 100644 --- a/src/components/map/default-layers.js +++ b/src/components/map/default-layers.js @@ -4,23 +4,22 @@ import { CircleMarker } from 'leaflet'; import { useLayers } from '@context'; import { markClicked } from '@utils/map-utils'; -const newLayerDefaultState = layer => { +const newLayerDefaultState = (layer) => { const { product_type } = layer.properties; - + if (['obs', 'maxele63'].includes(product_type)) { return ({ visible: true, opacity: 1.0, }); } - + return ({ visible: false, opacity: 1.0, }); -}; - - + }; + export const DefaultLayers = () => { const [obsData, setObsData] = useState(""); @@ -200,4 +199,4 @@ export const DefaultLayers = () => { }; ); -}; \ No newline at end of file +}; diff --git a/src/context/map-context.js b/src/context/map-context.js index 4789d4cb..229ec426 100644 --- a/src/context/map-context.js +++ b/src/context/map-context.js @@ -7,6 +7,7 @@ import { Air as WindVelocityIcon, Water as WaterLevelIcon, BlurOn as WaterSurfaceIcon, + Flood as FloodIcon, } from '@mui/icons-material'; export const LayersContext = createContext({}); @@ -32,8 +33,12 @@ const layerTypes = { hec_ras_water_surface: { icon: WaterSurfaceIcon, }, + maxinundepth63: { + icon: FloodIcon, + }, }; + export const LayersProvider = ({ children }) => { const [defaultModelLayers, setDefaultModelLayers] = useState([]); const [filteredModelLayers, setFilteredModelLayers] = useState([]); @@ -59,6 +64,17 @@ export const LayersProvider = ({ children }) => { ]); }; + const makeAllRasterLayersInvisible = () => { + const newLayers = []; + const currentLayers = [...defaultModelLayers]; + currentLayers.forEach((layer, idx) => { + const alteredLayer = currentLayers[idx]; + alteredLayer.state.visible = false; + newLayers.push(alteredLayer); + }); + setDefaultModelLayers([...newLayers]); + }; + const swapLayers = (i, j) => { // ensure our pair has i < j const [a, b] = [i, j].sort(); @@ -111,6 +127,7 @@ export const LayersProvider = ({ children }) => { swapLayers, removeLayer, layerTypes, + makeAllRasterLayersInvisible, setLayerOpacity, }} > diff --git a/src/utils/index.js b/src/utils/index.js new file mode 100644 index 00000000..40fa5b76 --- /dev/null +++ b/src/utils/index.js @@ -0,0 +1 @@ +export * from "./map-utils"; \ No newline at end of file