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