diff --git a/package-lock.json b/package-lock.json index 9f11f2c3..0082929d 100644 --- a/package-lock.json +++ b/package-lock.json @@ -30,6 +30,7 @@ "mapbox-gl": "^3.1.2", "prop-types": "^15.8.1", "react": "^18.2.0", + "react-beautiful-dnd": "^13.1.1", "react-dom": "^18.2.0", "react-draggable": "^4.4.6", "react-leaflet": "^4.2.1", @@ -5420,6 +5421,16 @@ "@types/node": "*" } }, + "node_modules/@types/hoist-non-react-statics": { + "version": "3.3.5", + "resolved": "https://registry.npmjs.org/@types/hoist-non-react-statics/-/hoist-non-react-statics-3.3.5.tgz", + "integrity": "sha512-SbcrWzkKBw2cdwRTwQAswfpB9g9LJWfjtUeW/jvNwbhC8cpmmNYVePa+ncbUe0rGTQ7G3Ff6mYUN2VMfLVr+Sg==", + "license": "MIT", + "dependencies": { + "@types/react": "*", + "hoist-non-react-statics": "^3.3.0" + } + }, "node_modules/@types/html-minifier-terser": { "version": "6.1.0", "resolved": "https://registry.npmjs.org/@types/html-minifier-terser/-/html-minifier-terser-6.1.0.tgz", @@ -5540,6 +5551,18 @@ "csstype": "^3.0.2" } }, + "node_modules/@types/react-redux": { + "version": "7.1.33", + "resolved": "https://registry.npmjs.org/@types/react-redux/-/react-redux-7.1.33.tgz", + "integrity": "sha512-NF8m5AjWCkert+fosDsN3hAlHzpjSiXlVy9EgQEmLoBhaNXbmyeGs/aj5dQzKuF+/q+S7JQagorGDW8pJ28Hmg==", + "license": "MIT", + "dependencies": { + "@types/hoist-non-react-statics": "^3.3.0", + "@types/react": "*", + "hoist-non-react-statics": "^3.3.0", + "redux": "^4.0.0" + } + }, "node_modules/@types/react-transition-group": { "version": "4.4.10", "resolved": "https://registry.npmjs.org/@types/react-transition-group/-/react-transition-group-4.4.10.tgz", @@ -8052,6 +8075,15 @@ "postcss": "^8.4" } }, + "node_modules/css-box-model": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/css-box-model/-/css-box-model-1.2.1.tgz", + "integrity": "sha512-a7Vr4Q/kd/aw96bnJG332W9V9LkJO69JRcaCYDUqjp6/z0w6VcZjgAcTbgFxEPfBgdnAwlh3iwu+hLopa+flJw==", + "license": "MIT", + "dependencies": { + "tiny-invariant": "^1.0.6" + } + }, "node_modules/css-has-pseudo": { "version": "6.0.3", "resolved": "https://registry.npmjs.org/css-has-pseudo/-/css-has-pseudo-6.0.3.tgz", @@ -14251,6 +14283,12 @@ "url": "https://github.com/sponsors/streamich" } }, + "node_modules/memoize-one": { + "version": "5.2.1", + "resolved": "https://registry.npmjs.org/memoize-one/-/memoize-one-5.2.1.tgz", + "integrity": "sha512-zYiwtZUcYyXKo/np96AGZAckk+FWWsUdJ3cHGGmld7+AhvcWmQyGCYUh1hc4Q/pkOhb65dQR/pqCyK0cOaHz4Q==", + "license": "MIT" + }, "node_modules/merge-descriptors": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/merge-descriptors/-/merge-descriptors-1.0.1.tgz", @@ -16235,6 +16273,12 @@ "resolved": "https://registry.npmjs.org/quickselect/-/quickselect-2.0.0.tgz", "integrity": "sha512-RKJ22hX8mHe3Y6wH/N3wCM6BWtjaxIyyUIkpHOvfFnxdI4yD4tBXEBKSbriGujF6jnSVkJrffuo6vxACiSSxIw==" }, + "node_modules/raf-schd": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/raf-schd/-/raf-schd-4.0.3.tgz", + "integrity": "sha512-tQkJl2GRWh83ui2DiPTJz9wEiMN20syf+5oKfB03yYP7ioZcJwsIK8FjrtLwH1m7C7e+Tt2yYBlrOpdT+dyeIQ==", + "license": "MIT" + }, "node_modules/randombytes": { "version": "2.1.0", "resolved": "https://registry.npmjs.org/randombytes/-/randombytes-2.1.0.tgz", @@ -16338,6 +16382,25 @@ "node": ">=0.10.0" } }, + "node_modules/react-beautiful-dnd": { + "version": "13.1.1", + "resolved": "https://registry.npmjs.org/react-beautiful-dnd/-/react-beautiful-dnd-13.1.1.tgz", + "integrity": "sha512-0Lvs4tq2VcrEjEgDXHjT98r+63drkKEgqyxdA7qD3mvKwga6a5SscbdLPO2IExotU1jW8L0Ksdl0Cj2AF67nPQ==", + "license": "Apache-2.0", + "dependencies": { + "@babel/runtime": "^7.9.2", + "css-box-model": "^1.2.0", + "memoize-one": "^5.1.1", + "raf-schd": "^4.0.2", + "react-redux": "^7.2.0", + "redux": "^4.0.4", + "use-memo-one": "^1.1.1" + }, + "peerDependencies": { + "react": "^16.8.5 || ^17.0.0 || ^18.0.0", + "react-dom": "^16.8.5 || ^17.0.0 || ^18.0.0" + } + }, "node_modules/react-dom": { "version": "18.3.1", "resolved": "https://registry.npmjs.org/react-dom/-/react-dom-18.3.1.tgz", @@ -16437,6 +16500,37 @@ } } }, + "node_modules/react-redux": { + "version": "7.2.9", + "resolved": "https://registry.npmjs.org/react-redux/-/react-redux-7.2.9.tgz", + "integrity": "sha512-Gx4L3uM182jEEayZfRbI/G11ZpYdNAnBs70lFVMNdHJI76XYtR+7m0MN+eAs7UHBPhWXcnFPaS+9owSCJQHNpQ==", + "license": "MIT", + "dependencies": { + "@babel/runtime": "^7.15.4", + "@types/react-redux": "^7.1.20", + "hoist-non-react-statics": "^3.3.2", + "loose-envify": "^1.4.0", + "prop-types": "^15.7.2", + "react-is": "^17.0.2" + }, + "peerDependencies": { + "react": "^16.8.3 || ^17 || ^18" + }, + "peerDependenciesMeta": { + "react-dom": { + "optional": true + }, + "react-native": { + "optional": true + } + } + }, + "node_modules/react-redux/node_modules/react-is": { + "version": "17.0.2", + "resolved": "https://registry.npmjs.org/react-is/-/react-is-17.0.2.tgz", + "integrity": "sha512-w2GsyukL62IJnlaff/nRegPQR94C/XXamvMWmSHRJ4y7Ts/4ocGRmTHvOs8PSE6pB3dWOrD/nueuU5sduBsQ4w==", + "license": "MIT" + }, "node_modules/react-refresh": { "version": "0.14.2", "resolved": "https://registry.npmjs.org/react-refresh/-/react-refresh-0.14.2.tgz", @@ -16604,6 +16698,15 @@ "node": ">= 10.13.0" } }, + "node_modules/redux": { + "version": "4.2.1", + "resolved": "https://registry.npmjs.org/redux/-/redux-4.2.1.tgz", + "integrity": "sha512-LAUYz4lc+Do8/g7aeRa8JkyDErK6ekstQaqWQrNRW//MY1TvCEpMtpTWvlQ+FPbWCx+Xixu/6SHt5N0HR+SB4w==", + "license": "MIT", + "dependencies": { + "@babel/runtime": "^7.9.2" + } + }, "node_modules/reflect.getprototypeof": { "version": "1.0.6", "resolved": "https://registry.npmjs.org/reflect.getprototypeof/-/reflect.getprototypeof-1.0.6.tgz", @@ -18540,6 +18643,15 @@ "punycode": "^2.1.0" } }, + "node_modules/use-memo-one": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/use-memo-one/-/use-memo-one-1.1.3.tgz", + "integrity": "sha512-g66/K7ZQGYrI6dy8GLpVcMsBp4s17xNkYJVSMvTEevGy3nDxHOfE6z8BVE22+5G5x7t3+bhzrlTDB7ObrEE0cQ==", + "license": "MIT", + "peerDependencies": { + "react": "^16.8.0 || ^17.0.0 || ^18.0.0" + } + }, "node_modules/user-home": { "version": "1.1.1", "resolved": "https://registry.npmjs.org/user-home/-/user-home-1.1.1.tgz", diff --git a/package.json b/package.json index 0c7f8890..02ff2f42 100644 --- a/package.json +++ b/package.json @@ -69,6 +69,7 @@ "mapbox-gl": "^3.1.2", "prop-types": "^15.8.1", "react": "^18.2.0", + "react-beautiful-dnd": "^13.1.1", "react-dom": "^18.2.0", "react-draggable": "^4.4.6", "react-leaflet": "^4.2.1", diff --git a/src/components/sidebar/tray.js b/src/components/sidebar/tray.js index d9fff7e6..cae04cc7 100644 --- a/src/components/sidebar/tray.js +++ b/src/components/sidebar/tray.js @@ -19,8 +19,8 @@ export const Tray = ({ active, Contents, title, closeHandler }) => { width: TRAY_WIDTH, zIndex: 410, filter: 'drop-shadow(0 0 8px rgba(0, 0, 0, 0.2))', - overflowX: 'hidden', - overflowY: 'auto', + overflow: 'hidden', + // overflowY: 'auto', display: 'flex', flexDirection: 'column', ".tray-header": { diff --git a/src/components/trays/layers/delete-layer-button.js b/src/components/trays/layers/delete-layer-button.js index bca856fc..30b314f8 100644 --- a/src/components/trays/layers/delete-layer-button.js +++ b/src/components/trays/layers/delete-layer-button.js @@ -43,7 +43,7 @@ export const DeleteModelRunButton = ({ groupId }) => { sx={{ alignContent: 'right', m: 1, - 'filter': 'opacity(0.3)', + 'filter': 'opacity(0.5)', transition: 'filter 250ms', '&:hover': { 'filter': 'opacity(1.0)', diff --git a/src/components/trays/layers/list.js b/src/components/trays/layers/list.js index d5757be8..d73d8698 100644 --- a/src/components/trays/layers/list.js +++ b/src/components/trays/layers/list.js @@ -1,11 +1,10 @@ import React from 'react'; -import { - AccordionGroup, Box, - Divider, Accordion, AccordionSummary, AccordionDetails -} from '@mui/joy'; +import { AccordionGroup, Box, Divider, Accordion, AccordionSummary, AccordionDetails, Stack } from '@mui/joy'; import { useLayers } from '@context'; import { LayerCard } from './layer-card'; import { DeleteModelRunButton } from "@components/trays/layers/delete-layer-button"; +import { DragHandleRounded as Handle } from '@mui/icons-material'; +import { DragDropContext, Droppable, Draggable } from "react-beautiful-dnd"; /** * gets the header data property name index @@ -15,44 +14,44 @@ import { DeleteModelRunButton } from "@components/trays/layers/delete-layer-butt * @param type * @returns {string} */ -const getPropertyName = (layerProps, type) => { - // init the return - let ret_val = undefined; - - // capture the name of the element for tropical storms and advisory numbers - if (layerProps['met_class'] === 'tropical') { - switch (type) { - case 'stormOrModelEle': - ret_val = layerProps['storm_name']; - break; - case 'numberName': - ret_val = ' Adv: '; - break; - case 'numberEle': - ret_val = layerProps['advisory_number']; - break; +const getPropertyName = (layerProps, element_name) => { + // init the return + let ret_val = undefined; + + // capture the name of the element for tropical storms and advisory numbers + if (layerProps['met_class'] === 'tropical') { + // by the element name + switch (element_name) { + case 'stormOrModelEle': + ret_val = layerProps['storm_name']; + break; + case 'numberName': + ret_val = ' Adv: '; + break; + case 'numberEle': + ret_val = layerProps['advisory_number']; + break; + } } - } - // capture the name of the synoptic ADCIRC models and cycle numbers - else { - switch (type) { - case 'stormOrModelEle': - ret_val = layerProps['model']; - break; - case 'numberName': - ret_val = ' Cycle: '; - break; - case 'numberEle': - ret_val = layerProps['cycle']; - break; + // capture the name of the synoptic ADCIRC models and cycle numbers + else { + switch (element_name) { + case 'stormOrModelEle': + ret_val = layerProps['model']; + break; + case 'numberName': + ret_val = ' Cycle: '; + break; + case 'numberEle': + ret_val = layerProps['cycle']; + break; + } } - } - // return to the caller - return ret_val; + // return to the caller + return ret_val; }; - /** * gets the summary header text for the layer groups. * This takes into account the two types of runs (tropical, synoptic) @@ -61,13 +60,13 @@ const getPropertyName = (layerProps, type) => { * @returns {string} */ const getHeaderSummary = (layerProps) => { - // get the full accordian summary text - return layerProps['run_date'] + ': ' + - ((getPropertyName(layerProps, 'stormOrModelEle') === undefined) ? 'Data error' : getPropertyName(layerProps, 'stormOrModelEle').toUpperCase()) + - ', ' + getPropertyName(layerProps, 'numberName') + getPropertyName(layerProps, 'numberEle') + - ', Type: ' + layerProps['event_type'] + - ', Grid: ' + layerProps['grid_type'] + - ((layerProps['meteorological_model'] === 'None') ? '' : ', ' + layerProps['meteorological_model']); + // get the full accordian summary text + return layerProps['run_date'] + ': ' + + ((getPropertyName(layerProps, 'stormOrModelEle') === undefined) ? 'Data error' : getPropertyName(layerProps, 'stormOrModelEle').toUpperCase()) + + ', ' + getPropertyName(layerProps, 'numberName') + getPropertyName(layerProps, 'numberEle') + + ', Type: ' + layerProps['event_type'] + + ', Grid: ' + layerProps['grid_type'] + + ((layerProps['meteorological_model'] === 'None') ? '' : ', ' + layerProps['meteorological_model']); }; /** @@ -84,11 +83,11 @@ const renderLayerCards = (layers, group) => { // filter/map the layers to create/return the layer card list layers // capture the layers for this group - .filter(layer => ( layer['group'] === group) ) + .filter(layer => (layer['group'] === group)) // at this point we have the distinct runs .map((layer, idx) => { - layerCards.push( ); - }); + layerCards.push( ); + }); // return to the caller return layerCards; @@ -101,23 +100,64 @@ const renderLayerCards = (layers, group) => { * @returns {*[]} */ const getGroupList = (layers) => { - // init the group list - const groupList = []; - - // loop through the layers and get the unique groups - layers - // filter by the group name - .filter((groups, idx, self) => - ( idx === self.findIndex((t)=> ( t['group'] === groups['group']) ))) - // .sort((a, b) => - // a['run_date'] < b['run_date'] ? 1 : -1) - // at this point we have the distinct runs - .map((layer) => { - groupList.push(layer); - }); - - // return the list of groups - return groupList; + // init the group list + const groupList = []; + + // loop through the layers and get the unique groups + layers + // filter by the group name + .filter((group, idx, self) => + (idx === self.findIndex((t) => (t['group'] === group['group'])))) + // at this point we have the distinct runs + .map((layer) => { + groupList.push(layer); + }); + + // return the list of groups + return groupList; +}; + +/** + * reorder the list of groups + * + * @param grpList + * @param startIndex + * @param endIndex + * @returns {unknown[]} + */ +const reOrderGroups = (grpList, startIndex, endIndex) => { + // copy the list + const ret_val = Array.from(grpList); + + // get the item that is moving + const [removed] = ret_val.splice(startIndex, 1); + + // put it in the new position + ret_val.splice(endIndex, 0, removed); + + // return the result + return ret_val; +}; + +/** + * adds or updates the visibility of the layer on the map surface. + * + * presumably this will have to take the met class into consideration. + * + * @param layer + * @param group + * @returns {{ visible: boolean, opacity: 1.0 }} + */ +const newLayerDefaultState = (layer, group) => { + // if this is an obs layer and is the one just added + if (layer.group === group && + (layer.properties['product_type'] === 'obs' || layer.properties['product_type'] === 'maxele63')) + // make this layer visible + return ({ visible: true, opacity: 1.0 }); + // remove layer visibility + else + // make this layer invisible + return ({ visible: false, opacity: 1.0 }); }; /** @@ -127,44 +167,117 @@ const getGroupList = (layers) => { * @constructor */ export const LayersList = () => { - // get a handle to the layer state - const { defaultModelLayers } = useLayers(); - - // get the default layers - const layers = [...defaultModelLayers]; - - // get the unique groups in the selected run groups - const groupList = getGroupList(layers); - - // loop through the layers and put them away - return ( - - { - // loop through the layer groups and put them away - groupList - // filter by the group name - .filter((groups, idx, self) => - ( idx === self.findIndex((t)=> ( t['group'] === groups['group']) ))) - // at this point we have the distinct runs - .map((groups, idx) => { - return ( - - - - - { getHeaderSummary(groups['properties']) } - - - - - { renderLayerCards( layers, groups['group'] ) } - - - ); - }) - } - - - ); + // get a handle to the layer state + const {defaultModelLayers, setDefaultModelLayers} = useLayers(); + + // get the default layers + const layers = [...defaultModelLayers]; + + // get the unique groups in the selected model runs + const groupList = getGroupList(layers); + + /** + * handle the drag event + * + * @param result + */ + const onDragEnd = (result) => { + // handle case that there is no destination (could have been dragged out of the drop area) + if (!result.destination) { + return; + } + + // create an array of group ids + let grpList = []; + + // get the current layer groups + getGroupList(layers).map((item) => (grpList.push(item['group']))); + + // swap the elements + grpList = reOrderGroups(grpList, result.source.index, result.destination.index); + + // reorder the layers and put them back in state + reOrderLayers(grpList); + }; + + /** + * order the layers in state based on the new group list order + * + * @param grpList + * @returns {*[]} + */ + const reOrderLayers = (grpList) => { + // init the return + const newLayerList = []; + + // reorder the layers into a new array + grpList + // soin through the groups + .map((group) => ( + // spin through the layers + defaultModelLayers + // get the layers for this group + .filter((layer) => + (layer['group'] === group)) + // add the group layers into a new list + .map((layer) => + (newLayerList.push(layer))))); + + // reset the visible layer states for all model runs in the layer tray. + [...newLayerList].forEach((layer) => { + // perform the visible state logic + layer.state = newLayerDefaultState(layer, layer.group); + }); + + // now update the visible layer state for the top most model run + [...newLayerList].forEach((layer) => { + // perform the visible state logic + layer.state = newLayerDefaultState(layer, newLayerList[0].group); + }); + + // update the layer list in state + setDefaultModelLayers(newLayerList); + }; + + /** + * render the layers on the tray + */ + return ( + + + { (provided) => ( + + { + // loop through the layer groups and put them away + groupList + // filter by the group name + .filter((groups, idx, self) => + (idx === self.findIndex((t) => (t['group'] === groups['group'])))) + // at this point we have the distinct runs + .map((layer, idx) => ( + + {(provided) => ( + + + + + + { getHeaderSummary(layer['properties']) } + + + + + + + { renderLayerCards(layers, layer['group'] )} + + + )} + )) + } { provided.placeholder } + )} + + + ); }; diff --git a/src/components/trays/model-selection/catalogItems.js b/src/components/trays/model-selection/catalogItems.js index 5160d59f..1f281a5f 100644 --- a/src/components/trays/model-selection/catalogItems.js +++ b/src/components/trays/model-selection/catalogItems.js @@ -114,6 +114,9 @@ export default function CatalogItems(data) { return ({ visible: false, opacity: 1.0 }); }; + /** + * render the selected model runs + */ // do not render if there is no data if (data.data != null) { // if there was a warning getting the result @@ -170,7 +173,7 @@ export default function CatalogItems(data) { { // loop through the data members and put them away catalog['members'] - // filter by the group name + // filter by the group name, get the top 1 .filter((val, idx, self) => ( idx === self.findIndex((t)=> ( t['group'] === val['group']) ))) .sort((a, b) => a['properties'][numberEle] < b['properties'][numberEle] ? 1 : -1)