diff --git a/package-lock.json b/package-lock.json index 1c9c3910..a7df5eae 100644 --- a/package-lock.json +++ b/package-lock.json @@ -25,6 +25,7 @@ "d3": "^7.8.5", "dayjs": "^1.11.11", "dotenv": "^16.4.5", + "html-to-image": "^1.11.11", "leaflet": "^1.9.4", "mapbox-gl": "^3.1.2", "prop-types": "^15.8.1", @@ -34,6 +35,7 @@ "react-leaflet": "^4.2.1", "react-map-gl": "^7.1.7", "react-query": "^3.39.3", + "react-router-dom": "^6.23.1", "react-timeago": "^7.2.0", "recharts": "^2.12.6" }, @@ -5033,6 +5035,15 @@ "react-dom": "^18.0.0" } }, + "node_modules/@remix-run/router": { + "version": "1.16.1", + "resolved": "https://registry.npmjs.org/@remix-run/router/-/router-1.16.1.tgz", + "integrity": "sha512-es2g3dq6Nb07iFxGk5GuHN20RwBZOsuDQN7izWIisUcv9r+d2C5jQxqmgkdebXgReWfiyUabcki6Fg77mSNrig==", + "license": "MIT", + "engines": { + "node": ">=14.0.0" + } + }, "node_modules/@sinclair/typebox": { "version": "0.27.8", "resolved": "https://registry.npmjs.org/@sinclair/typebox/-/typebox-0.27.8.tgz", @@ -7207,6 +7218,7 @@ "resolved": "https://registry.npmjs.org/braces/-/braces-3.0.3.tgz", "integrity": "sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA==", "dev": true, + "license": "MIT", "dependencies": { "fill-range": "^7.1.1" }, @@ -10143,6 +10155,7 @@ "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.1.1.tgz", "integrity": "sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg==", "dev": true, + "license": "MIT", "dependencies": { "to-regex-range": "^5.0.1" }, @@ -11039,6 +11052,12 @@ "node": ">= 12" } }, + "node_modules/html-to-image": { + "version": "1.11.11", + "resolved": "https://registry.npmjs.org/html-to-image/-/html-to-image-1.11.11.tgz", + "integrity": "sha512-9gux8QhvjRO/erSnDPv28noDZcPZmYE7e1vFsBLKLlRlKDSqNJYebj6Qz1TGd5lsRV+X+xYyjCKjuZdABinWjA==", + "license": "MIT" + }, "node_modules/html-webpack-plugin": { "version": "5.6.0", "resolved": "https://registry.npmjs.org/html-webpack-plugin/-/html-webpack-plugin-5.6.0.tgz", @@ -11712,6 +11731,7 @@ "resolved": "https://registry.npmjs.org/is-number/-/is-number-7.0.0.tgz", "integrity": "sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==", "dev": true, + "license": "MIT", "engines": { "node": ">=0.12.0" } @@ -16425,6 +16445,38 @@ "node": ">=0.10.0" } }, + "node_modules/react-router": { + "version": "6.23.1", + "resolved": "https://registry.npmjs.org/react-router/-/react-router-6.23.1.tgz", + "integrity": "sha512-fzcOaRF69uvqbbM7OhvQyBTFDVrrGlsFdS3AL+1KfIBtGETibHzi3FkoTRyiDJnWNc2VxrfvR+657ROHjaNjqQ==", + "license": "MIT", + "dependencies": { + "@remix-run/router": "1.16.1" + }, + "engines": { + "node": ">=14.0.0" + }, + "peerDependencies": { + "react": ">=16.8" + } + }, + "node_modules/react-router-dom": { + "version": "6.23.1", + "resolved": "https://registry.npmjs.org/react-router-dom/-/react-router-dom-6.23.1.tgz", + "integrity": "sha512-utP+K+aSTtEdbWpC+4gxhdlPFwuEfDKq8ZrPFU65bbRJY+l706qjR7yaidBpo3MSeA/fzwbXWbKBI6ftOnP3OQ==", + "license": "MIT", + "dependencies": { + "@remix-run/router": "1.16.1", + "react-router": "6.23.1" + }, + "engines": { + "node": ">=14.0.0" + }, + "peerDependencies": { + "react": ">=16.8", + "react-dom": ">=16.8" + } + }, "node_modules/react-smooth": { "version": "4.0.1", "resolved": "https://registry.npmjs.org/react-smooth/-/react-smooth-4.0.1.tgz", @@ -18111,6 +18163,7 @@ "resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz", "integrity": "sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==", "dev": true, + "license": "MIT", "dependencies": { "is-number": "^7.0.0" }, diff --git a/package.json b/package.json index edf6d89e..a3566b44 100644 --- a/package.json +++ b/package.json @@ -64,6 +64,7 @@ "d3": "^7.8.5", "dayjs": "^1.11.11", "dotenv": "^16.4.5", + "html-to-image": "^1.11.11", "leaflet": "^1.9.4", "mapbox-gl": "^3.1.2", "prop-types": "^15.8.1", @@ -73,6 +74,7 @@ "react-leaflet": "^4.2.1", "react-map-gl": "^7.1.7", "react-query": "^3.39.3", + "react-router-dom": "^6.23.1", "react-timeago": "^7.2.0", "recharts": "^2.12.6" }, diff --git a/src/app.js b/src/app.js index eae1e6ff..b101fb59 100644 --- a/src/app.js +++ b/src/app.js @@ -1,30 +1,57 @@ import React, { Fragment } from 'react'; +import { BrowserRouter, Route, Routes } from "react-router-dom"; 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'; import { MapLegend } from '@components/legend'; +import { Share } from '@share/share'; -export const App = () => { +/** + * renders the main content + * + * @returns {JSX.Element} + * @constructor + */ +const Content = () => { // install the selected observation list from the layer context - const { - selectedObservations - } = useLayers(); + const { selectedObservations } = useLayers(); + // render all the application content return ( - { - // for each observation selected - selectedObservations.map (function (obs) { - // render the observation - return ; - }) - } + { + // for each observation selected + selectedObservations.map (function (obs) { + // render the observation + return ; + }) + } + + + ); +}; + +/** + * renders the application + * + * @returns {JSX.Element} + * @constructor + */ +export const App = () => { + // render the application + return ( + + + + } /> + + ); }; diff --git a/src/components/control-panel/control-panel.js b/src/components/control-panel/control-panel.js index b7df62f2..ae6d7cf2 100644 --- a/src/components/control-panel/control-panel.js +++ b/src/components/control-panel/control-panel.js @@ -277,7 +277,7 @@ export const ControlPanel = () => { '&:hover': { filter: 'opacity(1.0)' }, height: 'auto', width: '300px', - zIndex: 999, + zIndex: 401, borderRadius: 'sm', }} > diff --git a/src/components/dialog/base-floating-dialog.js b/src/components/dialog/base-floating-dialog.js index 33343893..54660c60 100644 --- a/src/components/dialog/base-floating-dialog.js +++ b/src/components/dialog/base-floating-dialog.js @@ -63,13 +63,18 @@ export default function BaseFloatingDialog({ title, dialogObject, dataKey, dataL disableEnforceFocus style={{ pointerEvents: 'none' }} PaperProps={{ sx: { width: 750, height: 485, pointerEvents: 'auto'} }} - sx={{ width: 750, height: 485, '.MuiBackdrop-root': { backgroundColor: 'transparent' }}} + sx={{ zIndex: 402, width: 750, height: 485, '.MuiBackdrop-root': { backgroundColor: 'transparent' }}} > - { title } + { title } - { dialogObject } + { dialogObject } - + + ); diff --git a/src/components/legend/legend.js b/src/components/legend/legend.js index ee579130..86341efc 100644 --- a/src/components/legend/legend.js +++ b/src/components/legend/legend.js @@ -64,7 +64,7 @@ export const MapLegend = () => { height: 'auto', width: '100px', padding: '10px', - zIndex: 999, + zIndex: 401, borderRadius: 'sm', visibility: legendVisibilty, }} diff --git a/src/components/map/default-layers.js b/src/components/map/default-layers.js index 546fcce4..0a4f7b12 100644 --- a/src/components/map/default-layers.js +++ b/src/components/map/default-layers.js @@ -5,6 +5,7 @@ import { useLayers } from '@context'; import { useQuery } from '@tanstack/react-query'; import axios from 'axios'; import { markClicked } from '@utils/map-utils'; +import { useLocation } from "react-router-dom"; const newLayerDefaultState = (layer) => { const { product_type } = layer.properties; @@ -23,10 +24,19 @@ const newLayerDefaultState = (layer) => { }; export const DefaultLayers = () => { - const [obsData, setObsData] = useState(""); const map = useMap(); + // get the hash location (if any) + const { hash } = useLocation(); + + let share_run = ''; + + if (hash !== '') { + share_run = '&run_id=' + hash.split('=')[1]; + share_run = share_run.split(',')[0]; + } + const { defaultModelLayers, setDefaultModelLayers, @@ -88,7 +98,7 @@ export const DefaultLayers = () => { }; // create the URLs to the data endpoints - const data_url = `${process.env.REACT_APP_UI_DATA_URL}get_ui_data_secure?limit=1&use_new_wb=true&use_v3_sp=true`; + const data_url = `${process.env.REACT_APP_UI_DATA_URL}get_ui_data_secure?limit=1&use_new_wb=true&use_v3_sp=true` + share_run; const gs_wms_url = `${process.env.REACT_APP_GS_DATA_URL}wms`; const gs_wfs_url = `${process.env.REACT_APP_GS_DATA_URL}`; diff --git a/src/components/share/buildlink.js b/src/components/share/buildlink.js new file mode 100644 index 00000000..06cfb121 --- /dev/null +++ b/src/components/share/buildlink.js @@ -0,0 +1,64 @@ +import React, { Fragment } from 'react'; +import { IconButton } from '@mui/joy'; +import ShareRoundedIcon from '@mui/icons-material/ShareRounded'; +import {useLayers} from "@context/map-context"; + +/** + * renders the link builder to recreate the current view elsewhere + * + * @returns {JSX.Element} + * @constructor + */ +export const BuildLink = () => { + // get the layers in state + const { defaultModelLayers } = useLayers(); + + /** + * create the query string that can be used to share the current view + */ + const createLink = () => { + // get the list of selected layers + const groups = defaultModelLayers + // get all the distinct groups + .filter((val, idx, self) => + ( idx === self.findIndex((t)=> ( t['group'] === val['group'] )))) + // return the group name + .map((mbr) => ( + mbr['group'] + )) + // generate a query string + .join(','); + + // check to see if there was one or more groups selected + if (groups !== '') { + // copy the link to the cut/paste buffer + copyTextToClipboard(window.location.origin + '/#share=' + groups).then(); + + // tell the user what just happened + alert('The share link has been copied to the clipboard.'); + } + // no layers were selected on the map + else + alert('There were no layers selected.'); + }; + + /** + * async function to copy the share link to the clipboard + * + * @param text + * @returns {Promise} + */ + async function copyTextToClipboard(text) { + // wait for the copy to complete + return await navigator.clipboard.writeText(text); + } + + // render the button + return ( + + Share  + + + + ); +}; diff --git a/src/components/share/index.js b/src/components/share/index.js new file mode 100644 index 00000000..63e68594 --- /dev/null +++ b/src/components/share/index.js @@ -0,0 +1,3 @@ +export * from './share'; +export * from './buildlink'; +export * from './screenshot'; diff --git a/src/components/share/screenshot.js b/src/components/share/screenshot.js new file mode 100644 index 00000000..04f8da96 --- /dev/null +++ b/src/components/share/screenshot.js @@ -0,0 +1,78 @@ +import React, { Fragment } from 'react'; +import { IconButton } from '@mui/joy'; +import AddAPhotoRoundedIcon from '@mui/icons-material/AddAPhotoRounded'; +import { toJpeg } from 'html-to-image'; // toPng, toBlob, toPixelData, toSvg + +/** + * creates a screenshot of the app surface. this method expects a + * reference to the parent view that will be turned into an image. + * usage: + * + * import { Screenshot } from "@screen-shot/screenshot"; + * + * + * @returns {JSX.Element} + * @constructor + */ +export const Screenshot = () => { + /** + * Creates a filename for the download target + * + * @param extension + * @param names + * @returns {string} + */ + const createFileName = (extension = "", ...names) => { + // no file extension will result in no file name returned + if (!extension) { + return ""; + } + + // return the filename + return `${names.join("")}.${extension}`; + }; + + /** + * initiates the screenshot + * + * @param node + * @returns {Promise<*>} + */ + const takeScreenShot = async (node) => { + // return the rendering + return await toJpeg(node); + }; + + /** + * create the imaginary link to download the image + * + * @param image + * @param name + * @param extension + */ + const download = (image, { name = "screencap-" + new Date().toISOString(), extension = "jpg" } = {}) => { + // create a target element + const a = document.createElement("a"); + + // specify the image + a.href = image; + + // create a file name + a.download = createFileName(extension, name); + + // execute the link + a.click(); + }; + + // click handler to initiate the image download + const downloadScreenshot = () => takeScreenShot(document.body).then(download); + + // render the button to download the image + return ( + + + + + + ); +}; \ No newline at end of file diff --git a/src/components/share/share.js b/src/components/share/share.js new file mode 100644 index 00000000..79438539 --- /dev/null +++ b/src/components/share/share.js @@ -0,0 +1,44 @@ +import React, { Fragment } from 'react'; +import { Card, Stack } from '@mui/joy'; +import { BuildLink } from './buildlink'; +// import { Screenshot } from './screenshot'; + +/** + * renders the shared content on the app as defined in the query string + * + * @returns {JSX.Element} + * @constructor + */ +export const Share = () => { + return ( + + + + + {/**/} + + + + ); +}; diff --git a/src/components/sidebar/sidebar.js b/src/components/sidebar/sidebar.js index 851b4dcd..182a72bf 100644 --- a/src/components/sidebar/sidebar.js +++ b/src/components/sidebar/sidebar.js @@ -28,7 +28,7 @@ export const Sidebar = () => { position: 'absolute', top: 0, left: 0, height: '100vh', - zIndex: 999, + zIndex: 401, maxWidth: '68px', overflow: 'hidden', p: 0, diff --git a/src/components/sidebar/tray.js b/src/components/sidebar/tray.js index 9053b382..2f43f896 100644 --- a/src/components/sidebar/tray.js +++ b/src/components/sidebar/tray.js @@ -17,7 +17,7 @@ export const Tray = ({ active, Contents, title, closeHandler }) => { transition: 'transform 250ms', height: '100vh', width: TRAY_WIDTH, - zIndex: 998, + zIndex: 401, filter: 'drop-shadow(0 0 8px rgba(0, 0, 0, 0.2))', overflowX: 'hidden', overflowY: 'auto', diff --git a/src/components/trays/help_about/helpAboutTray.js b/src/components/trays/help-about/helpAboutTray.js similarity index 54% rename from src/components/trays/help_about/helpAboutTray.js rename to src/components/trays/help-about/helpAboutTray.js index 4a5327e9..02023bcf 100644 --- a/src/components/trays/help_about/helpAboutTray.js +++ b/src/components/trays/help-about/helpAboutTray.js @@ -9,10 +9,10 @@ import { AccordionGroup, Accordion, AccordionSummary, AccordionDetails, Stack, T export const HelpAboutTray = () => { // used to collapse other open accordions const [index, setIndex] = React.useState(0); + const [subIndex, setSubIndex] = React.useState(-1); // render the form return ( - @@ -41,46 +41,103 @@ export const HelpAboutTray = () => { { setIndex(expanded ? 4 : null); }}> + How do I capture a screenshot ? + + + { setSubIndex(expanded ? 0 : null); }}> + Edge + +
    +
  • Right click the browser surface.
  • +
  • Select `Screenshot` from the context menu that appears.
  • +
  • At the top of the browser you can select to capture a portion of the browser or the entire browser surface.
  • +
  • A dialog of the screenshot will appear where you can save it to the Downloads folder or the cut/paste buffer.
  • +
+
+
+ + { setSubIndex(expanded ? 1 : null); }}> + Firefox + +
    +
  • Right click the browser surface
  • +
  • Select `Take screenshot` from the context menu that appears
  • +
  • At the top of the browser you can select to capture the visible portion of the browser or the entire browser surface.
  • +
  • A dialog of the screenshot will appear where you can save it to the Downloads folder or the cut/paste buffer.
  • +
+
+
+ + { setSubIndex(expanded ? 2 : null); }}> + Chrome + +
    +
  • Install and activate the Chrome Full Page Screen Capture browser extension.
  • +
  • A small camera icon will appear in the top right corner of the browser.
  • +
  • Click the camera icon.
  • +
  • Click the “download image” icon and the image will be saved to the Downloads folder.
  • +
+
+
+ + { setSubIndex(expanded ? 3 : null); }}> + Safari + +
    +
  • Install and activate Awesome Screenshot
  • +
  • Navigate to the target page in Safari
  • +
  • Click the Awesome Screenshot icon (looks like a tiny camera lens) to the left of the Safari address bar
  • +
  • Click “Capture entire page.” An image opens in a new tab.
  • +
  • Click the “Done” button.
  • +
  • Follow the directions to save the file.
  • +
+
+
+
+
+
+ + { setIndex(expanded ? 5 : null); }}> What are some features of this application? Add some content here... - { setIndex(expanded ? 5 : null); }}> + { setIndex(expanded ? 6 : null); }}> How do I add/remove Layers on the map? Add some content here... - { setIndex(expanded ? 6 : null); }}> + { setIndex(expanded ? 7 : null); }}> How do I move through synoptic cycles? Add some content here... - { setIndex(expanded ? 7 : null); }}> + { setIndex(expanded ? 8 : null); }}> What do the icons on the left mean? Add some content here... - { setIndex(expanded ? 8 : null); }}> + { setIndex(expanded ? 9 : null); }}> What are some user settings? Add some content here... - { setIndex(expanded ? 9 : null); }}> + { setIndex(expanded ? 10 : null); }}> How do I change the base map? Add some content here... - { setIndex(expanded ? 10 : null); }}> + { setIndex(expanded ? 11 : null); }}> How do I view observation data? Add some content here... - { setIndex(expanded ? 11 : null); }}> + { setIndex(expanded ? 12 : null); }}> How do I show/hide layers? Add some content here... - { setIndex(expanded ? 12 : null); }}> + { setIndex(expanded ? 13 : null); }}> How do I reorder layers on the map? Add some content here... diff --git a/src/components/trays/help_about/help_about.js b/src/components/trays/help-about/help_about.js similarity index 100% rename from src/components/trays/help_about/help_about.js rename to src/components/trays/help-about/help_about.js diff --git a/src/components/trays/help_about/index.js b/src/components/trays/help-about/index.js similarity index 92% rename from src/components/trays/help_about/index.js rename to src/components/trays/help-about/index.js index 111ae270..84955d56 100644 --- a/src/components/trays/help_about/index.js +++ b/src/components/trays/help-about/index.js @@ -9,7 +9,7 @@ import { HelpAbout } from "./help_about.js"; export const icon = ; // create a title for this tray element -export const title = 'ADCIRC Help/About'; +export const title = 'APSViz Help/About'; /** * render the removal component diff --git a/src/components/trays/hurricanes.js b/src/components/trays/hurricanes.js deleted file mode 100644 index c946bfda..00000000 --- a/src/components/trays/hurricanes.js +++ /dev/null @@ -1,6 +0,0 @@ -import React from 'react'; -import { Storm as HurricaneIcon } from '@mui/icons-material'; - -export const icon = ; -export const title = 'Hurricanes'; -export const trayContents = () =>
Coming soon!
; diff --git a/src/components/trays/index.js b/src/components/trays/index.js index 6f72e87d..9f886710 100644 --- a/src/components/trays/index.js +++ b/src/components/trays/index.js @@ -3,7 +3,8 @@ import * as layers from './layers'; import * as model_selection from './model-selection'; import * as remove_items from './remove'; import * as settings from './settings'; -import * as help_about from './help_about'; +import * as help_about from './help-about'; +//import * as screen_shot from './screenshot'; export default { layers, @@ -12,6 +13,7 @@ export default { remove_items, settings, help_about +// ,screen_shot }; /* diff --git a/src/components/trays/model-selection/catalogItems.js b/src/components/trays/model-selection/catalogItems.js index 7f0fc571..98434ff7 100644 --- a/src/components/trays/model-selection/catalogItems.js +++ b/src/components/trays/model-selection/catalogItems.js @@ -76,6 +76,7 @@ export default function CatalogItems(data) { // reload the default layers less the layer group that was unselected setDefaultModelLayers(newLayers); } + // else add these layers to state else if (!defaultModelLayers.find(layer => layer.group === layerGroup) && checked) { // loop through the select layers in the group and add the default layer state selectedLayers.forEach((layer) => { @@ -92,9 +93,6 @@ export default function CatalogItems(data) { // save the items to state so they can be rendered setDefaultModelLayers([...newLayers, ...defaultModelLayers]); } - else - // TODO: the checkbox checked value should follow what is in the defaultModelLayers state - console.warn(`Layer group ${layerGroup} already exists.`); }; /** diff --git a/src/components/trays/settings/basemap.js b/src/components/trays/settings/basemap.js index cf03b8ed..938ce7b9 100644 --- a/src/components/trays/settings/basemap.js +++ b/src/components/trays/settings/basemap.js @@ -94,7 +94,7 @@ export function BaseMaps() { [`& .${radioClasses.radio}`]: { display: 'contents', '& > svg': { - zIndex: 2, + zIndex: 1, position: 'absolute', top: '-8px', right: '-8px', diff --git a/src/index.js b/src/index.js index e32dcd18..7a546546 100644 --- a/src/index.js +++ b/src/index.js @@ -1,4 +1,4 @@ -import React from 'react'; +import React, { Fragment } from 'react'; import { createRoot } from 'react-dom/client'; import { App } from './app'; import { LayersProvider, SettingsProvider } from '@context'; @@ -29,19 +29,22 @@ const materialTheme = materialExtendTheme(); // render the app specifying the material and joy providers const ProvisionedApp = () => { + // render the app return ( - - - - - - - - - - - - + + + + + + + + + + + + + + ); }; diff --git a/webpack.config.js b/webpack.config.js index 0eb0257d..24145787 100644 --- a/webpack.config.js +++ b/webpack.config.js @@ -99,12 +99,13 @@ module.exports = { '@components': path.resolve(__dirname, 'src/components/'), '@content': path.resolve(__dirname, 'src/content/'), '@context': path.resolve(__dirname, 'src/context/'), - '@dialog': path.resolve(__dirname, 'src/components/dialog'), + '@dialog': path.resolve(__dirname, 'src/components/dialog/'), '@help-about': path.resolve(__dirname, 'src/components/trays/help-about/'), '@hooks': path.resolve(__dirname, 'src/hooks/'), '@images': path.resolve(__dirname, 'src/images/'), - '@model-selection': path.resolve(__dirname, 'src/components/trays/model-selection'), - '@utils': path.resolve(__dirname, 'src/utils/'), + '@model-selection': path.resolve(__dirname, 'src/components/trays/model-selection/'), + '@share': path.resolve(__dirname, 'src/components/share/'), + '@utils': path.resolve(__dirname, 'src/utils/') } },