diff --git a/src/app.js b/src/app.js index 74697f7..40f2a27 100644 --- a/src/app.js +++ b/src/app.js @@ -7,13 +7,12 @@ import { Sidebar } from '@components/sidebar'; import { ControlPanel } from '@components/control-panel'; import { ComparePanel } from '@components/compare-panel'; import { MapLegend } from '@components/legend'; - - +import { AlertUser } from '@components/alert-user'; /** * renders the main content * - * @returns {JSX.Element} + * @returns JSX.Element * @constructor */ const Content = () => { @@ -33,6 +32,7 @@ const Content = () => { return ; }) } + @@ -45,7 +45,7 @@ const Content = () => { /** * renders the application * - * @returns {JSX.Element} + * @returns JSX.Element * @constructor */ export const App = () => { diff --git a/src/components/alert-user/alert-user.js b/src/components/alert-user/alert-user.js new file mode 100644 index 0000000..de554c8 --- /dev/null +++ b/src/components/alert-user/alert-user.js @@ -0,0 +1,23 @@ +import React, { Fragment } from "react"; +import { useLayers } from "@context"; +import { Dialog, DialogContent, Alert, Tooltip } from '@mui/material'; + +export const AlertUser = () => { + // get the message alert details from state + const { alertMsg, setAlertMsg } = useLayers(); + + return( + // render an alert only if there is one + (alertMsg !== null) ? ( + + setAlertMsg(null) }> + + + { alertMsg['msg'] } + + + + + ) : '' + ); +}; \ No newline at end of file diff --git a/src/components/alert-user/index.js b/src/components/alert-user/index.js new file mode 100644 index 0000000..d700693 --- /dev/null +++ b/src/components/alert-user/index.js @@ -0,0 +1 @@ +export * from './alert-user'; diff --git a/src/components/dialog/base-floating-dialog.js b/src/components/dialog/base-floating-dialog.js index 1af0b75..e5e12cd 100644 --- a/src/components/dialog/base-floating-dialog.js +++ b/src/components/dialog/base-floating-dialog.js @@ -1,18 +1,11 @@ import React, { Fragment, useState, useRef, forwardRef } from 'react'; -import { ToggleButtonGroup, ToggleButton, Box, Stack, Typography } from '@mui/material'; +import { ToggleButtonGroup, ToggleButton, Box, Stack, Typography, + CssBaseline, Dialog, DialogContent, DialogTitle, Paper, Slide, IconButton} from '@mui/material'; import Draggable from "react-draggable"; import PropTypes from 'prop-types'; import { Resizable } from "react-resizable"; import "react-resizable/css/styles.css"; -import CssBaseline from '@mui/material/CssBaseline'; -import Dialog from '@mui/material/Dialog'; -import DialogContent from '@mui/material/DialogActions'; -import DialogTitle from '@mui/material/DialogTitle'; - -import Paper from '@mui/material/Paper'; -import Slide from '@mui/material/Slide'; -import IconButton from '@mui/material/IconButton'; import CloseOutlinedIcon from '@mui/icons-material/CloseOutlined'; import { markUnclicked } from '@utils/map-utils'; @@ -55,7 +48,6 @@ export default function BaseFloatingDialog({ title, index, dialogObject, dataKey const minHeight = 175; const maxHeight = 500; - /** * the close dialog handler */ @@ -135,6 +127,20 @@ export default function BaseFloatingDialog({ title, index, dialogObject, dataKey APS Forecast : '' } + {(showLineButtonView("SWAN Nowcast")) ? + + SWAN Nowcast : '' + } + + {(showLineButtonView("SWAN Forecast")) ? + + SWAN Forecast : '' + } + {(showLineButtonView("NOAA Tidal Predictions")) ? 0) { // init the max value found let maxVal = 0; @@ -268,7 +286,7 @@ function CreateObsChart(c) { { status === 'pending' ? (
Gathering chart data...
) : - status === 'error' ? (
There was a problem with observation data for this location.
) : + status === 'error' ? (
There was a problem with collecting data for this location.
) : @@ -284,6 +302,8 @@ function CreateObsChart(c) { + + diff --git a/src/components/dialog/observation-dialog.js b/src/components/dialog/observation-dialog.js index a5fea96..edd43d6 100644 --- a/src/components/dialog/observation-dialog.js +++ b/src/components/dialog/observation-dialog.js @@ -24,6 +24,8 @@ export const ObservationDialog = (obs_data) => { "NOAA Tidal Predictions": false, "APS Nowcast": false, "APS Forecast": false, + "SWAN Nowcast": false, + "SWAN Forecast": false, "Difference (APS-OBS)": false }); // method to toggle the line view polarity @@ -38,6 +40,8 @@ export const ObservationDialog = (obs_data) => { "NOAA Tidal Predictions": false, "APS Nowcast": false, "APS Forecast": false, + "SWAN Nowcast": false, + "SWAN Forecast": false, "Difference (APS-OBS)": false }); const setLineButtonView = (item) => { diff --git a/src/components/map/adcirc-raster-layer.js b/src/components/map/adcirc-raster-layer.js index 9a405c4..cbd3d4b 100644 --- a/src/components/map/adcirc-raster-layer.js +++ b/src/components/map/adcirc-raster-layer.js @@ -1,8 +1,8 @@ -import React, { useEffect, useMemo, useState } from 'react'; -import { WMSTileLayer } from 'react-leaflet'; +import React, { useEffect, useMemo, useState, useCallback } from 'react'; +import { WMSTileLayer, useMap, useMapEvent } from 'react-leaflet'; import SldStyleParser from 'geostyler-sld-parser'; -import { getNamespacedEnvParam, restoreColorMapType } from '@utils/map-utils'; -import { useSettings } from '@context'; +import { getNamespacedEnvParam, markClicked, restoreColorMapType } from '@utils/map-utils'; +import { useLayers, useSettings } from '@context'; export const AdcircRasterLayer = (layer) => { const sldParser = new SldStyleParser(); @@ -15,7 +15,7 @@ export const AdcircRasterLayer = (layer) => { const [currentStyle, setCurrentStyle] = useState(""); - useEffect(() => { + useEffect(() => { if(layer.layer.properties) { let style = ""; switch(layer.layer.properties.product_type) { @@ -42,12 +42,90 @@ export const AdcircRasterLayer = (layer) => { }); }); } - }, [mapStyle]); - + }, [mapStyle]); + + // get the observation points selected, default layers and alert message from state + const { + setSelectedObservations, + defaultModelLayers, + setAlertMsg, + } = useLayers(); + + // capture the default layers + const layers = defaultModelLayers; + + // get a handle to the map + const map = useMap(); + + // create a list of worthy geo-point layer types + const validLayerTypes = new Set(['Maximum Water Level', 'Maximum Significant Wave Height']); + + // create a callback to handle a map click event + const onClick = useCallback((e) => { + // get the visible layer on the map + const layer = layers.find((layer) => layer.properties['product_type'] !== "obs" && layer.state.visible === true); + + // if this is a layer we can geo-point on + if (validLayerTypes.has(layer.properties['product_name'])) { + // round the coordinates + const lon = Number(e.latlng.lng).toFixed(5); + const lat = Number(e.latlng.lat).toFixed(5); + + // create an id for the point + const id = lon + ', ' + lat; + + // create a marker target icon around the observation clicked + markClicked(map, e, id); + + // get the FQDN of the UI data server + const data_url = `${getNamespacedEnvParam('REACT_APP_UI_DATA_URL')}`; + + // create the correct TDS URL without the hostname + const tds_url = layer.properties['tds_download_url'].replace('catalog', 'dodsC').replace('catalog.html', (layer.id.indexOf('swan') < 0 ? + 'fort' : 'swan_HS') + '.63.nc').split('/thredds')[1]; + + // get the hostname + const tds_svr = layer.properties['tds_download_url'].split('https://')[1].split('/thredds')[0].split('.')[0]; + + // generate the full url + const fullTDSURL = data_url + "get_geo_point_data?lon=" + e.latlng.lng + "&lat=" + e.latlng.lat + "&ensemble=nowcast&url=" + + tds_url + '&tds_svr=' + tds_svr; + + const l_props = layer.properties; + + // create a set of properties for this object + const pointProps = + { + "station_name": l_props['product_name'] + " " + id, + "lat": lat, + "lon": lon, + "location_name": l_props['product_name'] + "s over time (lon, lat): " + id, + "model_run_id": layer.group, + "data_source": (l_props['event_type'] + '_' + l_props['grid_type']).toUpperCase(), + "source_name": l_props['model'], + "source_instance": l_props['instance_name'], + "source_archive": l_props['location'], + "forcing_metclass": l_props['met_class'], + "location_type": "GeoPoint", + "grid_name": l_props['grid_type'].toUpperCase(), + "csvurl": fullTDSURL, + "id": id + }; + + // populate selectedObservations list with the newly selected observation point + setSelectedObservations(previous => [...previous, pointProps]); + } + else + setAlertMsg({'severity': 'warning', 'msg': 'Geo-point selection is not available for the ' + layer.properties['product_name'] + ' product.'}); + }); + + // assign the map click event for geo-point selections + useMapEvent('click', onClick); + // memorizing this params object prevents // that map flicker on state changes. const wmsLayerParams = useMemo(() => ({ - format:"image/png", + format: "image/png", transparent: true, sld_body: currentStyle, }), [currentStyle]); @@ -60,5 +138,4 @@ export const AdcircRasterLayer = (layer) => { opacity={layer.layer.state.opacity} /> ); - }; diff --git a/src/context/map-context.js b/src/context/map-context.js index ec3d413..0ad8e6e 100644 --- a/src/context/map-context.js +++ b/src/context/map-context.js @@ -366,6 +366,9 @@ export const LayersProvider = ({ children }) => { // used to track the view state of the share comment const [showShareComment, setShowShareComment] = useState(true); + // used to show alerts + const [alertMsg, setAlertMsg] = useState(null); + return ( { selectedObservations, setSelectedObservations, showShareComment, setShowShareComment, layerTypes, + alertMsg, setAlertMsg, toggleHurricaneLayerVisibility, toggleLayerVisibility, toggleLayerVisibility2, getAllLayersInvisible, getAllHurricaneLayersInvisible, getAllRasterLayersInvisible, diff --git a/webpack.config.js b/webpack.config.js index 9d9a08e..3724993 100644 --- a/webpack.config.js +++ b/webpack.config.js @@ -123,7 +123,8 @@ module.exports = { '@compare-layers': path.resolve(__dirname, 'src/components/trays/compare-layers/'), '@share': path.resolve(__dirname, 'src/components/trays/share/'), '@utils': path.resolve(__dirname, 'src/utils/'), - '@side-by-side': path.resolve(__dirname, 'src/components/side-by-side') + '@side-by-side': path.resolve(__dirname, 'src/components/side-by-side/'), + '@alert-user': path.resolve(__dirname, 'src/components/alert-user/') } },