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)