diff --git a/frontend/package-lock.json b/frontend/package-lock.json index 1490ae23c..d26bab47c 100644 --- a/frontend/package-lock.json +++ b/frontend/package-lock.json @@ -8,6 +8,7 @@ "name": "webviz", "version": "0.0.0", "dependencies": { + "@equinor/eds-core-react": "^0.42.5", "@equinor/esv-intersection": "^3.0.10", "@headlessui/react": "^1.7.8", "@mui/base": "^5.0.0-beta.3", @@ -883,6 +884,31 @@ "resolved": "https://registry.npmjs.org/@emotion/weak-memoize/-/weak-memoize-0.3.1.tgz", "integrity": "sha512-EsBwpc7hBUJWAsNPBmJy4hxWx12v6bshQsldrVmjxJoc3isbxhOrF2IcCpaXxfvq03NwkI7sbsOLXbYuqF/8Ww==" }, + "node_modules/@equinor/eds-core-react": { + "version": "0.42.5", + "resolved": "https://registry.npmjs.org/@equinor/eds-core-react/-/eds-core-react-0.42.5.tgz", + "integrity": "sha512-Js5mgPhLrrOYCx8FbFds+ZhX4vru1qq+nXEjw8rQGrIGzEZQGGBMyLXnvVZ5nM/mSjx061aMv2CPL3Eeu82qcQ==", + "dependencies": { + "@babel/runtime": "^7.25.0", + "@equinor/eds-icons": "^0.21.0", + "@equinor/eds-tokens": "0.9.2", + "@equinor/eds-utils": "0.8.5", + "@floating-ui/react": "^0.26.22", + "@internationalized/date": "^3.5.5", + "@react-aria/utils": "^3.25.1", + "@react-stately/calendar": "^3.5.3", + "@react-stately/datepicker": "^3.10.1", + "@react-types/shared": "^3.24.1", + "@tanstack/react-virtual": "3.10.8", + "downshift": "9.0.8", + "react-aria": "^3.34.1" + }, + "peerDependencies": { + "react": ">=16.8", + "react-dom": ">=16.8", + "styled-components": ">=5.1" + } + }, "node_modules/@equinor/eds-icons": { "version": "0.21.0", "resolved": "https://registry.npmjs.org/@equinor/eds-icons/-/eds-icons-0.21.0.tgz", @@ -901,6 +927,24 @@ "pnpm": ">=4" } }, + "node_modules/@equinor/eds-utils": { + "version": "0.8.5", + "resolved": "https://registry.npmjs.org/@equinor/eds-utils/-/eds-utils-0.8.5.tgz", + "integrity": "sha512-4AwltyJg51rjBBB4a4g4dGh9JlR+9mc/1AvRsV+nJqdpjjUgDeVBXukLN8Dh2CgyX1+0q3iH3TWq7bwOzd7n5Q==", + "dependencies": { + "@babel/runtime": "^7.24.0", + "@equinor/eds-tokens": "0.9.2" + }, + "engines": { + "node": ">=10.0.0", + "pnpm": ">=4" + }, + "peerDependencies": { + "react": ">=16.8", + "react-dom": ">=16.8", + "styled-components": ">=4.2" + } + }, "node_modules/@equinor/esv-intersection": { "version": "3.0.10", "resolved": "https://registry.npmjs.org/@equinor/esv-intersection/-/esv-intersection-3.0.10.tgz", @@ -1475,6 +1519,51 @@ "resolved": "https://registry.npmjs.org/@floating-ui/utils/-/utils-0.2.8.tgz", "integrity": "sha512-kym7SodPp8/wloecOpcmSnWJsK7M0E5Wg8UcFA+uO4B9s5d0ywXOEro/8HM9x0rW+TljRzul/14UYz3TleT3ig==" }, + "node_modules/@formatjs/ecma402-abstract": { + "version": "2.2.4", + "resolved": "https://registry.npmjs.org/@formatjs/ecma402-abstract/-/ecma402-abstract-2.2.4.tgz", + "integrity": "sha512-lFyiQDVvSbQOpU+WFd//ILolGj4UgA/qXrKeZxdV14uKiAUiPAtX6XAn7WBCRi7Mx6I7EybM9E5yYn4BIpZWYg==", + "dependencies": { + "@formatjs/fast-memoize": "2.2.3", + "@formatjs/intl-localematcher": "0.5.8", + "tslib": "2" + } + }, + "node_modules/@formatjs/fast-memoize": { + "version": "2.2.3", + "resolved": "https://registry.npmjs.org/@formatjs/fast-memoize/-/fast-memoize-2.2.3.tgz", + "integrity": "sha512-3jeJ+HyOfu8osl3GNSL4vVHUuWFXR03Iz9jjgI7RwjG6ysu/Ymdr0JRCPHfF5yGbTE6JCrd63EpvX1/WybYRbA==", + "dependencies": { + "tslib": "2" + } + }, + "node_modules/@formatjs/icu-messageformat-parser": { + "version": "2.9.4", + "resolved": "https://registry.npmjs.org/@formatjs/icu-messageformat-parser/-/icu-messageformat-parser-2.9.4.tgz", + "integrity": "sha512-Tbvp5a9IWuxUcpWNIW6GlMQYEc4rwNHR259uUFoKWNN1jM9obf9Ul0e+7r7MvFOBNcN+13K7NuKCKqQiAn1QEg==", + "dependencies": { + "@formatjs/ecma402-abstract": "2.2.4", + "@formatjs/icu-skeleton-parser": "1.8.8", + "tslib": "2" + } + }, + "node_modules/@formatjs/icu-skeleton-parser": { + "version": "1.8.8", + "resolved": "https://registry.npmjs.org/@formatjs/icu-skeleton-parser/-/icu-skeleton-parser-1.8.8.tgz", + "integrity": "sha512-vHwK3piXwamFcx5YQdCdJxUQ1WdTl6ANclt5xba5zLGDv5Bsur7qz8AD7BevaKxITwpgDeU0u8My3AIibW9ywA==", + "dependencies": { + "@formatjs/ecma402-abstract": "2.2.4", + "tslib": "2" + } + }, + "node_modules/@formatjs/intl-localematcher": { + "version": "0.5.8", + "resolved": "https://registry.npmjs.org/@formatjs/intl-localematcher/-/intl-localematcher-0.5.8.tgz", + "integrity": "sha512-I+WDNWWJFZie+jkfkiK5Mp4hEDyRSEvmyfYadflOno/mmKJKcB17fEpEH0oJu/OWhhCJ8kJBDz2YMd/6cDl7Mg==", + "dependencies": { + "tslib": "2" + } + }, "node_modules/@headlessui/react": { "version": "1.7.16", "resolved": "https://registry.npmjs.org/@headlessui/react/-/react-1.7.16.tgz", @@ -1531,6 +1620,39 @@ "react": "*" } }, + "node_modules/@internationalized/date": { + "version": "3.6.0", + "resolved": "https://registry.npmjs.org/@internationalized/date/-/date-3.6.0.tgz", + "integrity": "sha512-+z6ti+CcJnRlLHok/emGEsWQhe7kfSmEW+/6qCzvKY67YPh7YOBfvc7+/+NXq+zJlbArg30tYpqLjNgcAYv2YQ==", + "dependencies": { + "@swc/helpers": "^0.5.0" + } + }, + "node_modules/@internationalized/message": { + "version": "3.1.6", + "resolved": "https://registry.npmjs.org/@internationalized/message/-/message-3.1.6.tgz", + "integrity": "sha512-JxbK3iAcTIeNr1p0WIFg/wQJjIzJt9l/2KNY/48vXV7GRGZSv3zMxJsce008fZclk2cDC8y0Ig3odceHO7EfNQ==", + "dependencies": { + "@swc/helpers": "^0.5.0", + "intl-messageformat": "^10.1.0" + } + }, + "node_modules/@internationalized/number": { + "version": "3.6.0", + "resolved": "https://registry.npmjs.org/@internationalized/number/-/number-3.6.0.tgz", + "integrity": "sha512-PtrRcJVy7nw++wn4W2OuePQQfTqDzfusSuY1QTtui4wa7r+rGVtR75pO8CyKvHvzyQYi3Q1uO5sY0AsB4e65Bw==", + "dependencies": { + "@swc/helpers": "^0.5.0" + } + }, + "node_modules/@internationalized/string": { + "version": "3.2.5", + "resolved": "https://registry.npmjs.org/@internationalized/string/-/string-3.2.5.tgz", + "integrity": "sha512-rKs71Zvl2OKOHM+mzAFMIyqR5hI1d1O6BBkMK2/lkfg3fkmVh9Eeg0awcA8W2WqYqDOv6a86DIOlFpggwLtbuw==", + "dependencies": { + "@swc/helpers": "^0.5.0" + } + }, "node_modules/@isaacs/cliui": { "version": "8.0.2", "resolved": "https://registry.npmjs.org/@isaacs/cliui/-/cliui-8.0.2.tgz", @@ -3278,201 +3400,1665 @@ "integrity": "sha512-AXXFaAJ8yebyqzoNB9fu2pHoo/nWX+xZlaRwoeYUxEqBO+Zj4msE5G+BhGBll9lYEKv9Hfks52PAF2X7qDYXQA==", "dev": true, "dependencies": { - "esbuild": "^0.18.10", - "postcss": "^8.4.27", - "rollup": "^3.27.1" + "esbuild": "^0.18.10", + "postcss": "^8.4.27", + "rollup": "^3.27.1" + }, + "bin": { + "vite": "bin/vite.js" + }, + "engines": { + "node": "^14.18.0 || >=16.0.0" + }, + "funding": { + "url": "https://github.com/vitejs/vite?sponsor=1" + }, + "optionalDependencies": { + "fsevents": "~2.3.2" + }, + "peerDependencies": { + "@types/node": ">= 14", + "less": "*", + "lightningcss": "^1.21.0", + "sass": "*", + "stylus": "*", + "sugarss": "*", + "terser": "^5.4.0" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + }, + "less": { + "optional": true + }, + "lightningcss": { + "optional": true + }, + "sass": { + "optional": true + }, + "stylus": { + "optional": true + }, + "sugarss": { + "optional": true + }, + "terser": { + "optional": true + } + } + }, + "node_modules/@playwright/experimental-ct-react": { + "version": "1.40.1", + "resolved": "https://registry.npmjs.org/@playwright/experimental-ct-react/-/experimental-ct-react-1.40.1.tgz", + "integrity": "sha512-a2ubB04+pSswpWOgIwgBcSvvdvVNv4Cz8wud5ZLV5+4fcRqRACxFlGJPiVHw1zanhDSD+rH6H9+zaNm/o1iJHw==", + "dev": true, + "dependencies": { + "@playwright/experimental-ct-core": "1.40.1", + "@vitejs/plugin-react": "^4.0.0" + }, + "bin": { + "playwright": "cli.js" + }, + "engines": { + "node": ">=16" + } + }, + "node_modules/@playwright/test": { + "version": "1.40.1", + "resolved": "https://registry.npmjs.org/@playwright/test/-/test-1.40.1.tgz", + "integrity": "sha512-EaaawMTOeEItCRvfmkI9v6rBkF1svM8wjl/YPRrg2N2Wmp+4qJYkWtJsbew1szfKKDm6fPLy4YAanBhIlf9dWw==", + "dev": true, + "dependencies": { + "playwright": "1.40.1" + }, + "bin": { + "playwright": "cli.js" + }, + "engines": { + "node": ">=16" + } + }, + "node_modules/@plotly/d3": { + "version": "3.8.1", + "resolved": "https://registry.npmjs.org/@plotly/d3/-/d3-3.8.1.tgz", + "integrity": "sha512-x49ThEu1FRA00kTso4Jdfyf2byaCPLBGmLjAYQz5OzaPyLUhHesX3/Nfv2OHEhynhdy2UB39DLXq6thYe2L2kg==", + "peer": true + }, + "node_modules/@plotly/d3-sankey": { + "version": "0.7.2", + "resolved": "https://registry.npmjs.org/@plotly/d3-sankey/-/d3-sankey-0.7.2.tgz", + "integrity": "sha512-2jdVos1N3mMp3QW0k2q1ph7Gd6j5PY1YihBrwpkFnKqO+cqtZq3AdEYUeSGXMeLsBDQYiqTVcihYfk8vr5tqhw==", + "peer": true, + "dependencies": { + "d3-array": "1", + "d3-collection": "1", + "d3-shape": "^1.2.0" + } + }, + "node_modules/@plotly/d3-sankey-circular": { + "version": "0.33.1", + "resolved": "https://registry.npmjs.org/@plotly/d3-sankey-circular/-/d3-sankey-circular-0.33.1.tgz", + "integrity": "sha512-FgBV1HEvCr3DV7RHhDsPXyryknucxtfnLwPtCKKxdolKyTFYoLX/ibEfX39iFYIL7DYbVeRtP43dbFcrHNE+KQ==", + "peer": true, + "dependencies": { + "d3-array": "^1.2.1", + "d3-collection": "^1.0.4", + "d3-shape": "^1.2.0", + "elementary-circuits-directed-graph": "^1.0.4" + } + }, + "node_modules/@plotly/d3-sankey-circular/node_modules/d3-array": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/d3-array/-/d3-array-1.2.4.tgz", + "integrity": "sha512-KHW6M86R+FUPYGb3R5XiYjXPq7VzwxZ22buHhAEVG5ztoEcZZMLov530mmccaqA1GghZArjQV46fuc8kUqhhHw==", + "peer": true + }, + "node_modules/@plotly/d3-sankey-circular/node_modules/d3-path": { + "version": "1.0.9", + "resolved": "https://registry.npmjs.org/d3-path/-/d3-path-1.0.9.tgz", + "integrity": "sha512-VLaYcn81dtHVTjEHd8B+pbe9yHWpXKZUC87PzoFmsFrJqgFwDe/qxfp5MlfsfM1V5E/iVt0MmEbWQ7FVIXh/bg==", + "peer": true + }, + "node_modules/@plotly/d3-sankey-circular/node_modules/d3-shape": { + "version": "1.3.7", + "resolved": "https://registry.npmjs.org/d3-shape/-/d3-shape-1.3.7.tgz", + "integrity": "sha512-EUkvKjqPFUAZyOlhY5gzCxCeI0Aep04LwIRpsZ/mLFelJiUfnK56jo5JMDSE7yyP2kLSb6LtF+S5chMk7uqPqw==", + "peer": true, + "dependencies": { + "d3-path": "1" + } + }, + "node_modules/@plotly/d3-sankey/node_modules/d3-array": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/d3-array/-/d3-array-1.2.4.tgz", + "integrity": "sha512-KHW6M86R+FUPYGb3R5XiYjXPq7VzwxZ22buHhAEVG5ztoEcZZMLov530mmccaqA1GghZArjQV46fuc8kUqhhHw==", + "peer": true + }, + "node_modules/@plotly/d3-sankey/node_modules/d3-path": { + "version": "1.0.9", + "resolved": "https://registry.npmjs.org/d3-path/-/d3-path-1.0.9.tgz", + "integrity": "sha512-VLaYcn81dtHVTjEHd8B+pbe9yHWpXKZUC87PzoFmsFrJqgFwDe/qxfp5MlfsfM1V5E/iVt0MmEbWQ7FVIXh/bg==", + "peer": true + }, + "node_modules/@plotly/d3-sankey/node_modules/d3-shape": { + "version": "1.3.7", + "resolved": "https://registry.npmjs.org/d3-shape/-/d3-shape-1.3.7.tgz", + "integrity": "sha512-EUkvKjqPFUAZyOlhY5gzCxCeI0Aep04LwIRpsZ/mLFelJiUfnK56jo5JMDSE7yyP2kLSb6LtF+S5chMk7uqPqw==", + "peer": true, + "dependencies": { + "d3-path": "1" + } + }, + "node_modules/@plotly/point-cluster": { + "version": "3.1.9", + "resolved": "https://registry.npmjs.org/@plotly/point-cluster/-/point-cluster-3.1.9.tgz", + "integrity": "sha512-MwaI6g9scKf68Orpr1pHZ597pYx9uP8UEFXLPbsCmuw3a84obwz6pnMXGc90VhgDNeNiLEdlmuK7CPo+5PIxXw==", + "peer": true, + "dependencies": { + "array-bounds": "^1.0.1", + "binary-search-bounds": "^2.0.4", + "clamp": "^1.0.1", + "defined": "^1.0.0", + "dtype": "^2.0.0", + "flatten-vertex-data": "^1.0.2", + "is-obj": "^1.0.1", + "math-log2": "^1.0.1", + "parse-rect": "^1.2.0", + "pick-by-alias": "^1.2.0" + } + }, + "node_modules/@popperjs/core": { + "version": "2.11.8", + "resolved": "https://registry.npmjs.org/@popperjs/core/-/core-2.11.8.tgz", + "integrity": "sha512-P1st0aksCrn9sGZhp8GMYwBnQsbvAWsZAX44oXNNvLHGqAOcoVxmjZiohstwQ7SqKnbR47akdNi+uleWD8+g6A==", + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/popperjs" + } + }, + "node_modules/@probe.gl/env": { + "version": "4.0.9", + "resolved": "https://registry.npmjs.org/@probe.gl/env/-/env-4.0.9.tgz", + "integrity": "sha512-AOmVMD0/j78mX+k4+qX7ZhE0sY9H+EaJgIO6trik0BwV6VcrwxTGCGFAeuRsIGhETDnye06tkLXccYatYxAYwQ==" + }, + "node_modules/@probe.gl/log": { + "version": "4.0.9", + "resolved": "https://registry.npmjs.org/@probe.gl/log/-/log-4.0.9.tgz", + "integrity": "sha512-ebuZaodSRE9aC+3bVC7cKRHT8garXeT1jTbj1R5tQRqQYc9iGeT3iemVOHx5bN9Q6gAs/0j54iPI+1DvWMAW4A==", + "dependencies": { + "@probe.gl/env": "4.0.9" + } + }, + "node_modules/@probe.gl/stats": { + "version": "4.0.9", + "resolved": "https://registry.npmjs.org/@probe.gl/stats/-/stats-4.0.9.tgz", + "integrity": "sha512-Q9Xt/sJUQaMsbjRKjOscv2t7wXIymTrOEJ4a3da4FTCn7bkKvcdxdyFAQySCrtPxE+YZ5I5lXpWPgv9BwmpE1g==" + }, + "node_modules/@react-aria/breadcrumbs": { + "version": "3.5.19", + "resolved": "https://registry.npmjs.org/@react-aria/breadcrumbs/-/breadcrumbs-3.5.19.tgz", + "integrity": "sha512-mVngOPFYVVhec89rf/CiYQGTfaLRfHFtX+JQwY7sNYNqSA+gO8p4lNARe3Be6bJPgH+LUQuruIY9/ZDL6LT3HA==", + "dependencies": { + "@react-aria/i18n": "^3.12.4", + "@react-aria/link": "^3.7.7", + "@react-aria/utils": "^3.26.0", + "@react-types/breadcrumbs": "^3.7.9", + "@react-types/shared": "^3.26.0", + "@swc/helpers": "^0.5.0" + }, + "peerDependencies": { + "react": "^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1" + } + }, + "node_modules/@react-aria/button": { + "version": "3.11.0", + "resolved": "https://registry.npmjs.org/@react-aria/button/-/button-3.11.0.tgz", + "integrity": "sha512-b37eIV6IW11KmNIAm65F3SEl2/mgj5BrHIysW6smZX3KoKWTGYsYfcQkmtNgY0GOSFfDxMCoolsZ6mxC00nSDA==", + "dependencies": { + "@react-aria/focus": "^3.19.0", + "@react-aria/interactions": "^3.22.5", + "@react-aria/toolbar": "3.0.0-beta.11", + "@react-aria/utils": "^3.26.0", + "@react-stately/toggle": "^3.8.0", + "@react-types/button": "^3.10.1", + "@react-types/shared": "^3.26.0", + "@swc/helpers": "^0.5.0" + }, + "peerDependencies": { + "react": "^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1" + } + }, + "node_modules/@react-aria/calendar": { + "version": "3.6.0", + "resolved": "https://registry.npmjs.org/@react-aria/calendar/-/calendar-3.6.0.tgz", + "integrity": "sha512-tZ3nd5DP8uxckbj83Pt+4RqgcTWDlGi7njzc7QqFOG2ApfnYDUXbIpb/Q4KY6JNlJskG8q33wo0XfOwNy8J+eg==", + "dependencies": { + "@internationalized/date": "^3.6.0", + "@react-aria/i18n": "^3.12.4", + "@react-aria/interactions": "^3.22.5", + "@react-aria/live-announcer": "^3.4.1", + "@react-aria/utils": "^3.26.0", + "@react-stately/calendar": "^3.6.0", + "@react-types/button": "^3.10.1", + "@react-types/calendar": "^3.5.0", + "@react-types/shared": "^3.26.0", + "@swc/helpers": "^0.5.0" + }, + "peerDependencies": { + "react": "^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1", + "react-dom": "^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1" + } + }, + "node_modules/@react-aria/checkbox": { + "version": "3.15.0", + "resolved": "https://registry.npmjs.org/@react-aria/checkbox/-/checkbox-3.15.0.tgz", + "integrity": "sha512-z/8xd4em7o0MroBXwkkwv7QRwiJaA1FwqMhRUb7iqtBGP2oSytBEDf0N7L09oci32a1P4ZPz2rMK5GlLh/PD6g==", + "dependencies": { + "@react-aria/form": "^3.0.11", + "@react-aria/interactions": "^3.22.5", + "@react-aria/label": "^3.7.13", + "@react-aria/toggle": "^3.10.10", + "@react-aria/utils": "^3.26.0", + "@react-stately/checkbox": "^3.6.10", + "@react-stately/form": "^3.1.0", + "@react-stately/toggle": "^3.8.0", + "@react-types/checkbox": "^3.9.0", + "@react-types/shared": "^3.26.0", + "@swc/helpers": "^0.5.0" + }, + "peerDependencies": { + "react": "^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1" + } + }, + "node_modules/@react-aria/color": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/@react-aria/color/-/color-3.0.2.tgz", + "integrity": "sha512-dSM5qQRcR1gRGYCBw0IGRmc29gjfoht3cQleKb8MMNcgHYa2oi5VdCs2yKXmYFwwVC6uPtnlNy9S6e0spqdr+w==", + "dependencies": { + "@react-aria/i18n": "^3.12.4", + "@react-aria/interactions": "^3.22.5", + "@react-aria/numberfield": "^3.11.9", + "@react-aria/slider": "^3.7.14", + "@react-aria/spinbutton": "^3.6.10", + "@react-aria/textfield": "^3.15.0", + "@react-aria/utils": "^3.26.0", + "@react-aria/visually-hidden": "^3.8.18", + "@react-stately/color": "^3.8.1", + "@react-stately/form": "^3.1.0", + "@react-types/color": "^3.0.1", + "@react-types/shared": "^3.26.0", + "@swc/helpers": "^0.5.0" + }, + "peerDependencies": { + "react": "^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1", + "react-dom": "^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1" + } + }, + "node_modules/@react-aria/combobox": { + "version": "3.11.0", + "resolved": "https://registry.npmjs.org/@react-aria/combobox/-/combobox-3.11.0.tgz", + "integrity": "sha512-s88YMmPkMO1WSoiH1KIyZDLJqUwvM2wHXXakj3cYw1tBHGo4rOUFq+JWQIbM5EDO4HOR4AUUqzIUd0NO7t3zyg==", + "dependencies": { + "@react-aria/i18n": "^3.12.4", + "@react-aria/listbox": "^3.13.6", + "@react-aria/live-announcer": "^3.4.1", + "@react-aria/menu": "^3.16.0", + "@react-aria/overlays": "^3.24.0", + "@react-aria/selection": "^3.21.0", + "@react-aria/textfield": "^3.15.0", + "@react-aria/utils": "^3.26.0", + "@react-stately/collections": "^3.12.0", + "@react-stately/combobox": "^3.10.1", + "@react-stately/form": "^3.1.0", + "@react-types/button": "^3.10.1", + "@react-types/combobox": "^3.13.1", + "@react-types/shared": "^3.26.0", + "@swc/helpers": "^0.5.0" + }, + "peerDependencies": { + "react": "^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1", + "react-dom": "^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1" + } + }, + "node_modules/@react-aria/datepicker": { + "version": "3.12.0", + "resolved": "https://registry.npmjs.org/@react-aria/datepicker/-/datepicker-3.12.0.tgz", + "integrity": "sha512-VYNXioLfddIHpwQx211+rTYuunDmI7VHWBRetCpH3loIsVFuhFSRchTQpclAzxolO3g0vO7pMVj9VYt7Swp6kg==", + "dependencies": { + "@internationalized/date": "^3.6.0", + "@internationalized/number": "^3.6.0", + "@internationalized/string": "^3.2.5", + "@react-aria/focus": "^3.19.0", + "@react-aria/form": "^3.0.11", + "@react-aria/i18n": "^3.12.4", + "@react-aria/interactions": "^3.22.5", + "@react-aria/label": "^3.7.13", + "@react-aria/spinbutton": "^3.6.10", + "@react-aria/utils": "^3.26.0", + "@react-stately/datepicker": "^3.11.0", + "@react-stately/form": "^3.1.0", + "@react-types/button": "^3.10.1", + "@react-types/calendar": "^3.5.0", + "@react-types/datepicker": "^3.9.0", + "@react-types/dialog": "^3.5.14", + "@react-types/shared": "^3.26.0", + "@swc/helpers": "^0.5.0" + }, + "peerDependencies": { + "react": "^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1", + "react-dom": "^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1" + } + }, + "node_modules/@react-aria/dialog": { + "version": "3.5.20", + "resolved": "https://registry.npmjs.org/@react-aria/dialog/-/dialog-3.5.20.tgz", + "integrity": "sha512-l0GZVLgeOd3kL3Yj8xQW7wN3gn9WW3RLd/SGI9t7ciTq+I/FhftjXCWzXLlOCCTLMf+gv7eazecECtmoWUaZWQ==", + "dependencies": { + "@react-aria/focus": "^3.19.0", + "@react-aria/overlays": "^3.24.0", + "@react-aria/utils": "^3.26.0", + "@react-types/dialog": "^3.5.14", + "@react-types/shared": "^3.26.0", + "@swc/helpers": "^0.5.0" + }, + "peerDependencies": { + "react": "^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1", + "react-dom": "^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1" + } + }, + "node_modules/@react-aria/disclosure": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/@react-aria/disclosure/-/disclosure-3.0.0.tgz", + "integrity": "sha512-xO9QTQSvymujTjCs1iCQ4+dKZvtF/rVVaFZBKlUtqIqwTHMdqeZu4fh5miLEnTyVLNHMGzLrFggsd8Q+niC9Og==", + "dependencies": { + "@react-aria/ssr": "^3.9.7", + "@react-aria/utils": "^3.26.0", + "@react-stately/disclosure": "^3.0.0", + "@react-types/button": "^3.10.1", + "@swc/helpers": "^0.5.0" + }, + "peerDependencies": { + "react": "^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1", + "react-dom": "^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1" + } + }, + "node_modules/@react-aria/dnd": { + "version": "3.8.0", + "resolved": "https://registry.npmjs.org/@react-aria/dnd/-/dnd-3.8.0.tgz", + "integrity": "sha512-JiqHY3E9fDU5Kb4gN22cuK6QNlpMCGe6ngR/BV+Q8mLEsdoWcoUAYOtYXVNNTRvCdVbEWI87FUU+ThyPpoDhNQ==", + "dependencies": { + "@internationalized/string": "^3.2.5", + "@react-aria/i18n": "^3.12.4", + "@react-aria/interactions": "^3.22.5", + "@react-aria/live-announcer": "^3.4.1", + "@react-aria/overlays": "^3.24.0", + "@react-aria/utils": "^3.26.0", + "@react-stately/dnd": "^3.5.0", + "@react-types/button": "^3.10.1", + "@react-types/shared": "^3.26.0", + "@swc/helpers": "^0.5.0" + }, + "peerDependencies": { + "react": "^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1", + "react-dom": "^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1" + } + }, + "node_modules/@react-aria/focus": { + "version": "3.19.0", + "resolved": "https://registry.npmjs.org/@react-aria/focus/-/focus-3.19.0.tgz", + "integrity": "sha512-hPF9EXoUQeQl1Y21/rbV2H4FdUR2v+4/I0/vB+8U3bT1CJ+1AFj1hc/rqx2DqEwDlEwOHN+E4+mRahQmlybq0A==", + "dependencies": { + "@react-aria/interactions": "^3.22.5", + "@react-aria/utils": "^3.26.0", + "@react-types/shared": "^3.26.0", + "@swc/helpers": "^0.5.0", + "clsx": "^2.0.0" + }, + "peerDependencies": { + "react": "^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1" + } + }, + "node_modules/@react-aria/form": { + "version": "3.0.11", + "resolved": "https://registry.npmjs.org/@react-aria/form/-/form-3.0.11.tgz", + "integrity": "sha512-oXzjTiwVuuWjZ8muU0hp3BrDH5qjVctLOF50mjPvqUbvXQTHhoDxWweyIXPQjGshaqBd2w4pWaE4A2rG2O/apw==", + "dependencies": { + "@react-aria/interactions": "^3.22.5", + "@react-aria/utils": "^3.26.0", + "@react-stately/form": "^3.1.0", + "@react-types/shared": "^3.26.0", + "@swc/helpers": "^0.5.0" + }, + "peerDependencies": { + "react": "^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1" + } + }, + "node_modules/@react-aria/grid": { + "version": "3.11.0", + "resolved": "https://registry.npmjs.org/@react-aria/grid/-/grid-3.11.0.tgz", + "integrity": "sha512-lN5FpQgu2Rq0CzTPWmzRpq6QHcMmzsXYeClsgO3108uVp1/genBNAObYVTxGOKe/jb9q99trz8EtIn05O6KN1g==", + "dependencies": { + "@react-aria/focus": "^3.19.0", + "@react-aria/i18n": "^3.12.4", + "@react-aria/interactions": "^3.22.5", + "@react-aria/live-announcer": "^3.4.1", + "@react-aria/selection": "^3.21.0", + "@react-aria/utils": "^3.26.0", + "@react-stately/collections": "^3.12.0", + "@react-stately/grid": "^3.10.0", + "@react-stately/selection": "^3.18.0", + "@react-types/checkbox": "^3.9.0", + "@react-types/grid": "^3.2.10", + "@react-types/shared": "^3.26.0", + "@swc/helpers": "^0.5.0" + }, + "peerDependencies": { + "react": "^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1", + "react-dom": "^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1" + } + }, + "node_modules/@react-aria/gridlist": { + "version": "3.10.0", + "resolved": "https://registry.npmjs.org/@react-aria/gridlist/-/gridlist-3.10.0.tgz", + "integrity": "sha512-UcblfSZ7kJBrjg9mQ5VbnRevN81UiYB4NuL5PwIpBpridO7tnl4ew6+96PYU7Wj1chHhPS3x0b0zmuSVN7A0LA==", + "dependencies": { + "@react-aria/focus": "^3.19.0", + "@react-aria/grid": "^3.11.0", + "@react-aria/i18n": "^3.12.4", + "@react-aria/interactions": "^3.22.5", + "@react-aria/selection": "^3.21.0", + "@react-aria/utils": "^3.26.0", + "@react-stately/collections": "^3.12.0", + "@react-stately/list": "^3.11.1", + "@react-stately/tree": "^3.8.6", + "@react-types/shared": "^3.26.0", + "@swc/helpers": "^0.5.0" + }, + "peerDependencies": { + "react": "^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1", + "react-dom": "^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1" + } + }, + "node_modules/@react-aria/i18n": { + "version": "3.12.4", + "resolved": "https://registry.npmjs.org/@react-aria/i18n/-/i18n-3.12.4.tgz", + "integrity": "sha512-j9+UL3q0Ls8MhXV9gtnKlyozq4aM95YywXqnmJtzT1rYeBx7w28hooqrWkCYLfqr4OIryv1KUnPiCSLwC2OC7w==", + "dependencies": { + "@internationalized/date": "^3.6.0", + "@internationalized/message": "^3.1.6", + "@internationalized/number": "^3.6.0", + "@internationalized/string": "^3.2.5", + "@react-aria/ssr": "^3.9.7", + "@react-aria/utils": "^3.26.0", + "@react-types/shared": "^3.26.0", + "@swc/helpers": "^0.5.0" + }, + "peerDependencies": { + "react": "^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1" + } + }, + "node_modules/@react-aria/interactions": { + "version": "3.22.5", + "resolved": "https://registry.npmjs.org/@react-aria/interactions/-/interactions-3.22.5.tgz", + "integrity": "sha512-kMwiAD9E0TQp+XNnOs13yVJghiy8ET8L0cbkeuTgNI96sOAp/63EJ1FSrDf17iD8sdjt41LafwX/dKXW9nCcLQ==", + "dependencies": { + "@react-aria/ssr": "^3.9.7", + "@react-aria/utils": "^3.26.0", + "@react-types/shared": "^3.26.0", + "@swc/helpers": "^0.5.0" + }, + "peerDependencies": { + "react": "^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1" + } + }, + "node_modules/@react-aria/label": { + "version": "3.7.13", + "resolved": "https://registry.npmjs.org/@react-aria/label/-/label-3.7.13.tgz", + "integrity": "sha512-brSAXZVTey5RG/Ex6mTrV/9IhGSQFU4Al34qmjEDho+Z2qT4oPwf8k7TRXWWqzOU0ugYxekYbsLd2zlN3XvWcg==", + "dependencies": { + "@react-aria/utils": "^3.26.0", + "@react-types/shared": "^3.26.0", + "@swc/helpers": "^0.5.0" + }, + "peerDependencies": { + "react": "^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1" + } + }, + "node_modules/@react-aria/link": { + "version": "3.7.7", + "resolved": "https://registry.npmjs.org/@react-aria/link/-/link-3.7.7.tgz", + "integrity": "sha512-eVBRcHKhNSsATYWv5wRnZXRqPVcKAWWakyvfrYePIKpC3s4BaHZyTGYdefk8ZwZdEOuQZBqLMnjW80q1uhtkuA==", + "dependencies": { + "@react-aria/focus": "^3.19.0", + "@react-aria/interactions": "^3.22.5", + "@react-aria/utils": "^3.26.0", + "@react-types/link": "^3.5.9", + "@react-types/shared": "^3.26.0", + "@swc/helpers": "^0.5.0" + }, + "peerDependencies": { + "react": "^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1" + } + }, + "node_modules/@react-aria/listbox": { + "version": "3.13.6", + "resolved": "https://registry.npmjs.org/@react-aria/listbox/-/listbox-3.13.6.tgz", + "integrity": "sha512-6hEXEXIZVau9lgBZ4VVjFR3JnGU+fJaPmV3HP0UZ2ucUptfG0MZo24cn+ZQJsWiuaCfNFv5b8qribiv+BcO+Kg==", + "dependencies": { + "@react-aria/interactions": "^3.22.5", + "@react-aria/label": "^3.7.13", + "@react-aria/selection": "^3.21.0", + "@react-aria/utils": "^3.26.0", + "@react-stately/collections": "^3.12.0", + "@react-stately/list": "^3.11.1", + "@react-types/listbox": "^3.5.3", + "@react-types/shared": "^3.26.0", + "@swc/helpers": "^0.5.0" + }, + "peerDependencies": { + "react": "^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1", + "react-dom": "^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1" + } + }, + "node_modules/@react-aria/live-announcer": { + "version": "3.4.1", + "resolved": "https://registry.npmjs.org/@react-aria/live-announcer/-/live-announcer-3.4.1.tgz", + "integrity": "sha512-4X2mcxgqLvvkqxv2l1n00jTzUxxe0kkLiapBGH1LHX/CxA1oQcHDqv8etJ2ZOwmS/MSBBiWnv3DwYHDOF6ubig==", + "dependencies": { + "@swc/helpers": "^0.5.0" + } + }, + "node_modules/@react-aria/menu": { + "version": "3.16.0", + "resolved": "https://registry.npmjs.org/@react-aria/menu/-/menu-3.16.0.tgz", + "integrity": "sha512-TNk+Vd3TbpBPUxEloAdHRTaRxf9JBK7YmkHYiq0Yj5Lc22KS0E2eTyhpPM9xJvEWN2TlC5TEvNfdyui2kYWFFQ==", + "dependencies": { + "@react-aria/focus": "^3.19.0", + "@react-aria/i18n": "^3.12.4", + "@react-aria/interactions": "^3.22.5", + "@react-aria/overlays": "^3.24.0", + "@react-aria/selection": "^3.21.0", + "@react-aria/utils": "^3.26.0", + "@react-stately/collections": "^3.12.0", + "@react-stately/menu": "^3.9.0", + "@react-stately/selection": "^3.18.0", + "@react-stately/tree": "^3.8.6", + "@react-types/button": "^3.10.1", + "@react-types/menu": "^3.9.13", + "@react-types/shared": "^3.26.0", + "@swc/helpers": "^0.5.0" + }, + "peerDependencies": { + "react": "^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1", + "react-dom": "^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1" + } + }, + "node_modules/@react-aria/meter": { + "version": "3.4.18", + "resolved": "https://registry.npmjs.org/@react-aria/meter/-/meter-3.4.18.tgz", + "integrity": "sha512-tTX3LLlmDIHqrC42dkdf+upb1c4UbhlpZ52gqB64lZD4OD4HE+vMTwNSe+7MRKMLvcdKPWCRC35PnxIHZ15kfQ==", + "dependencies": { + "@react-aria/progress": "^3.4.18", + "@react-types/meter": "^3.4.5", + "@react-types/shared": "^3.26.0", + "@swc/helpers": "^0.5.0" + }, + "peerDependencies": { + "react": "^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1" + } + }, + "node_modules/@react-aria/numberfield": { + "version": "3.11.9", + "resolved": "https://registry.npmjs.org/@react-aria/numberfield/-/numberfield-3.11.9.tgz", + "integrity": "sha512-3tiGPx2y4zyOV7PmdBASes99ZZsFTZAJTnU45Z+p1CW4131lw7y2ZhbojBl7U6DaXAJvi1z6zY6cq2UE9w5a0Q==", + "dependencies": { + "@react-aria/i18n": "^3.12.4", + "@react-aria/interactions": "^3.22.5", + "@react-aria/spinbutton": "^3.6.10", + "@react-aria/textfield": "^3.15.0", + "@react-aria/utils": "^3.26.0", + "@react-stately/form": "^3.1.0", + "@react-stately/numberfield": "^3.9.8", + "@react-types/button": "^3.10.1", + "@react-types/numberfield": "^3.8.7", + "@react-types/shared": "^3.26.0", + "@swc/helpers": "^0.5.0" + }, + "peerDependencies": { + "react": "^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1", + "react-dom": "^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1" + } + }, + "node_modules/@react-aria/overlays": { + "version": "3.24.0", + "resolved": "https://registry.npmjs.org/@react-aria/overlays/-/overlays-3.24.0.tgz", + "integrity": "sha512-0kAXBsMNTc/a3M07tK9Cdt/ea8CxTAEJ223g8YgqImlmoBBYAL7dl5G01IOj67TM64uWPTmZrOklBchHWgEm3A==", + "dependencies": { + "@react-aria/focus": "^3.19.0", + "@react-aria/i18n": "^3.12.4", + "@react-aria/interactions": "^3.22.5", + "@react-aria/ssr": "^3.9.7", + "@react-aria/utils": "^3.26.0", + "@react-aria/visually-hidden": "^3.8.18", + "@react-stately/overlays": "^3.6.12", + "@react-types/button": "^3.10.1", + "@react-types/overlays": "^3.8.11", + "@react-types/shared": "^3.26.0", + "@swc/helpers": "^0.5.0" + }, + "peerDependencies": { + "react": "^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1", + "react-dom": "^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1" + } + }, + "node_modules/@react-aria/progress": { + "version": "3.4.18", + "resolved": "https://registry.npmjs.org/@react-aria/progress/-/progress-3.4.18.tgz", + "integrity": "sha512-FOLgJ9t9i1u3oAAimybJG6r7/soNPBnJfWo4Yr6MmaUv90qVGa1h6kiuM5m9H/bm5JobAebhdfHit9lFlgsCmg==", + "dependencies": { + "@react-aria/i18n": "^3.12.4", + "@react-aria/label": "^3.7.13", + "@react-aria/utils": "^3.26.0", + "@react-types/progress": "^3.5.8", + "@react-types/shared": "^3.26.0", + "@swc/helpers": "^0.5.0" + }, + "peerDependencies": { + "react": "^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1" + } + }, + "node_modules/@react-aria/radio": { + "version": "3.10.10", + "resolved": "https://registry.npmjs.org/@react-aria/radio/-/radio-3.10.10.tgz", + "integrity": "sha512-NVdeOVrsrHgSfwL2jWCCXFsWZb+RMRZErj5vthHQW4nkHECGOzeX56VaLWTSvdoCPqi9wdIX8A6K9peeAIgxzA==", + "dependencies": { + "@react-aria/focus": "^3.19.0", + "@react-aria/form": "^3.0.11", + "@react-aria/i18n": "^3.12.4", + "@react-aria/interactions": "^3.22.5", + "@react-aria/label": "^3.7.13", + "@react-aria/utils": "^3.26.0", + "@react-stately/radio": "^3.10.9", + "@react-types/radio": "^3.8.5", + "@react-types/shared": "^3.26.0", + "@swc/helpers": "^0.5.0" + }, + "peerDependencies": { + "react": "^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1" + } + }, + "node_modules/@react-aria/searchfield": { + "version": "3.7.11", + "resolved": "https://registry.npmjs.org/@react-aria/searchfield/-/searchfield-3.7.11.tgz", + "integrity": "sha512-wFf6QxtBFfoxy0ANxI0+ftFEBGynVCY0+ce4H4Y9LpUTQsIKMp3sdc7LoUFORWw5Yee6Eid5cFPQX0Ymnk+ZJg==", + "dependencies": { + "@react-aria/i18n": "^3.12.4", + "@react-aria/textfield": "^3.15.0", + "@react-aria/utils": "^3.26.0", + "@react-stately/searchfield": "^3.5.8", + "@react-types/button": "^3.10.1", + "@react-types/searchfield": "^3.5.10", + "@react-types/shared": "^3.26.0", + "@swc/helpers": "^0.5.0" + }, + "peerDependencies": { + "react": "^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1" + } + }, + "node_modules/@react-aria/select": { + "version": "3.15.0", + "resolved": "https://registry.npmjs.org/@react-aria/select/-/select-3.15.0.tgz", + "integrity": "sha512-zgBOUNy81aJplfc3NKDJMv8HkXjBGzaFF3XDzNfW8vJ7nD9rcTRUN5SQ1XCEnKMv12B/Euk9zt6kd+tX0wk1vQ==", + "dependencies": { + "@react-aria/form": "^3.0.11", + "@react-aria/i18n": "^3.12.4", + "@react-aria/interactions": "^3.22.5", + "@react-aria/label": "^3.7.13", + "@react-aria/listbox": "^3.13.6", + "@react-aria/menu": "^3.16.0", + "@react-aria/selection": "^3.21.0", + "@react-aria/utils": "^3.26.0", + "@react-aria/visually-hidden": "^3.8.18", + "@react-stately/select": "^3.6.9", + "@react-types/button": "^3.10.1", + "@react-types/select": "^3.9.8", + "@react-types/shared": "^3.26.0", + "@swc/helpers": "^0.5.0" + }, + "peerDependencies": { + "react": "^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1", + "react-dom": "^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1" + } + }, + "node_modules/@react-aria/selection": { + "version": "3.21.0", + "resolved": "https://registry.npmjs.org/@react-aria/selection/-/selection-3.21.0.tgz", + "integrity": "sha512-52JJ6hlPcM+gt0VV3DBmz6Kj1YAJr13TfutrKfGWcK36LvNCBm1j0N+TDqbdnlp8Nue6w0+5FIwZq44XPYiBGg==", + "dependencies": { + "@react-aria/focus": "^3.19.0", + "@react-aria/i18n": "^3.12.4", + "@react-aria/interactions": "^3.22.5", + "@react-aria/utils": "^3.26.0", + "@react-stately/selection": "^3.18.0", + "@react-types/shared": "^3.26.0", + "@swc/helpers": "^0.5.0" + }, + "peerDependencies": { + "react": "^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1", + "react-dom": "^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1" + } + }, + "node_modules/@react-aria/separator": { + "version": "3.4.4", + "resolved": "https://registry.npmjs.org/@react-aria/separator/-/separator-3.4.4.tgz", + "integrity": "sha512-dH+qt0Mdh0nhKXCHW6AR4DF8DKLUBP26QYWaoThPdBwIpypH/JVKowpPtWms1P4b36U6XzHXHnTTEn/ZVoCqNA==", + "dependencies": { + "@react-aria/utils": "^3.26.0", + "@react-types/shared": "^3.26.0", + "@swc/helpers": "^0.5.0" + }, + "peerDependencies": { + "react": "^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1" + } + }, + "node_modules/@react-aria/slider": { + "version": "3.7.14", + "resolved": "https://registry.npmjs.org/@react-aria/slider/-/slider-3.7.14.tgz", + "integrity": "sha512-7rOiKjLkEZ0j7mPMlwrqivc+K4OSfL14slaQp06GHRiJkhiWXh2/drPe15hgNq55HmBQBpA0umKMkJcqVgmXPA==", + "dependencies": { + "@react-aria/focus": "^3.19.0", + "@react-aria/i18n": "^3.12.4", + "@react-aria/interactions": "^3.22.5", + "@react-aria/label": "^3.7.13", + "@react-aria/utils": "^3.26.0", + "@react-stately/slider": "^3.6.0", + "@react-types/shared": "^3.26.0", + "@react-types/slider": "^3.7.7", + "@swc/helpers": "^0.5.0" + }, + "peerDependencies": { + "react": "^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1" + } + }, + "node_modules/@react-aria/spinbutton": { + "version": "3.6.10", + "resolved": "https://registry.npmjs.org/@react-aria/spinbutton/-/spinbutton-3.6.10.tgz", + "integrity": "sha512-nhYEYk7xUNOZDaqiQ5w/nHH9ouqjJbabTWXH+KK7UR1oVGfo4z1wG94l8KWF3Z6SGGnBxzLJyTBguZ4g9aYTSg==", + "dependencies": { + "@react-aria/i18n": "^3.12.4", + "@react-aria/live-announcer": "^3.4.1", + "@react-aria/utils": "^3.26.0", + "@react-types/button": "^3.10.1", + "@react-types/shared": "^3.26.0", + "@swc/helpers": "^0.5.0" + }, + "peerDependencies": { + "react": "^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1", + "react-dom": "^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1" + } + }, + "node_modules/@react-aria/ssr": { + "version": "3.9.7", + "resolved": "https://registry.npmjs.org/@react-aria/ssr/-/ssr-3.9.7.tgz", + "integrity": "sha512-GQygZaGlmYjmYM+tiNBA5C6acmiDWF52Nqd40bBp0Znk4M4hP+LTmI0lpI1BuKMw45T8RIhrAsICIfKwZvi2Gg==", + "dependencies": { + "@swc/helpers": "^0.5.0" + }, + "engines": { + "node": ">= 12" + }, + "peerDependencies": { + "react": "^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1" + } + }, + "node_modules/@react-aria/switch": { + "version": "3.6.10", + "resolved": "https://registry.npmjs.org/@react-aria/switch/-/switch-3.6.10.tgz", + "integrity": "sha512-FtaI9WaEP1tAmra1sYlAkYXg9x75P5UtgY8pSbe9+1WRyWbuE1QZT+RNCTi3IU4fZ7iJQmXH6+VaMyzPlSUagw==", + "dependencies": { + "@react-aria/toggle": "^3.10.10", + "@react-stately/toggle": "^3.8.0", + "@react-types/shared": "^3.26.0", + "@react-types/switch": "^3.5.7", + "@swc/helpers": "^0.5.0" + }, + "peerDependencies": { + "react": "^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1" + } + }, + "node_modules/@react-aria/table": { + "version": "3.16.0", + "resolved": "https://registry.npmjs.org/@react-aria/table/-/table-3.16.0.tgz", + "integrity": "sha512-9xF9S3CJ7XRiiK92hsIKxPedD0kgcQWwqTMtj3IBynpQ4vsnRiW3YNIzrn9C3apjknRZDTSta8O2QPYCUMmw2A==", + "dependencies": { + "@react-aria/focus": "^3.19.0", + "@react-aria/grid": "^3.11.0", + "@react-aria/i18n": "^3.12.4", + "@react-aria/interactions": "^3.22.5", + "@react-aria/live-announcer": "^3.4.1", + "@react-aria/utils": "^3.26.0", + "@react-aria/visually-hidden": "^3.8.18", + "@react-stately/collections": "^3.12.0", + "@react-stately/flags": "^3.0.5", + "@react-stately/table": "^3.13.0", + "@react-types/checkbox": "^3.9.0", + "@react-types/grid": "^3.2.10", + "@react-types/shared": "^3.26.0", + "@react-types/table": "^3.10.3", + "@swc/helpers": "^0.5.0" + }, + "peerDependencies": { + "react": "^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1", + "react-dom": "^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1" + } + }, + "node_modules/@react-aria/tabs": { + "version": "3.9.8", + "resolved": "https://registry.npmjs.org/@react-aria/tabs/-/tabs-3.9.8.tgz", + "integrity": "sha512-Nur/qRFBe+Zrt4xcCJV/ULXCS3Mlae+B89bp1Gl20vSDqk6uaPtGk+cS5k03eugOvas7AQapqNJsJgKd66TChw==", + "dependencies": { + "@react-aria/focus": "^3.19.0", + "@react-aria/i18n": "^3.12.4", + "@react-aria/selection": "^3.21.0", + "@react-aria/utils": "^3.26.0", + "@react-stately/tabs": "^3.7.0", + "@react-types/shared": "^3.26.0", + "@react-types/tabs": "^3.3.11", + "@swc/helpers": "^0.5.0" + }, + "peerDependencies": { + "react": "^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1", + "react-dom": "^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1" + } + }, + "node_modules/@react-aria/tag": { + "version": "3.4.8", + "resolved": "https://registry.npmjs.org/@react-aria/tag/-/tag-3.4.8.tgz", + "integrity": "sha512-exWl52bsFtJuzaqMYvSnLteUoPqb3Wf+uICru/yRtREJsWVqjJF38NCVlU73Yqd9qMPTctDrboSZFAWAWKDxoA==", + "dependencies": { + "@react-aria/gridlist": "^3.10.0", + "@react-aria/i18n": "^3.12.4", + "@react-aria/interactions": "^3.22.5", + "@react-aria/label": "^3.7.13", + "@react-aria/selection": "^3.21.0", + "@react-aria/utils": "^3.26.0", + "@react-stately/list": "^3.11.1", + "@react-types/button": "^3.10.1", + "@react-types/shared": "^3.26.0", + "@swc/helpers": "^0.5.0" + }, + "peerDependencies": { + "react": "^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1", + "react-dom": "^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1" + } + }, + "node_modules/@react-aria/textfield": { + "version": "3.15.0", + "resolved": "https://registry.npmjs.org/@react-aria/textfield/-/textfield-3.15.0.tgz", + "integrity": "sha512-V5mg7y1OR6WXYHdhhm4FC7QyGc9TideVRDFij1SdOJrIo5IFB7lvwpOS0GmgwkVbtr71PTRMjZnNbrJUFU6VNA==", + "dependencies": { + "@react-aria/focus": "^3.19.0", + "@react-aria/form": "^3.0.11", + "@react-aria/label": "^3.7.13", + "@react-aria/utils": "^3.26.0", + "@react-stately/form": "^3.1.0", + "@react-stately/utils": "^3.10.5", + "@react-types/shared": "^3.26.0", + "@react-types/textfield": "^3.10.0", + "@swc/helpers": "^0.5.0" + }, + "peerDependencies": { + "react": "^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1" + } + }, + "node_modules/@react-aria/toggle": { + "version": "3.10.10", + "resolved": "https://registry.npmjs.org/@react-aria/toggle/-/toggle-3.10.10.tgz", + "integrity": "sha512-QwMT/vTNrbrILxWVHfd9zVQ3mV2NdBwyRu+DphVQiFAXcmc808LEaIX2n0lI6FCsUDC9ZejCyvzd91/YemdZ1Q==", + "dependencies": { + "@react-aria/focus": "^3.19.0", + "@react-aria/interactions": "^3.22.5", + "@react-aria/utils": "^3.26.0", + "@react-stately/toggle": "^3.8.0", + "@react-types/checkbox": "^3.9.0", + "@react-types/shared": "^3.26.0", + "@swc/helpers": "^0.5.0" + }, + "peerDependencies": { + "react": "^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1" + } + }, + "node_modules/@react-aria/toolbar": { + "version": "3.0.0-beta.11", + "resolved": "https://registry.npmjs.org/@react-aria/toolbar/-/toolbar-3.0.0-beta.11.tgz", + "integrity": "sha512-LM3jTRFNDgoEpoL568WaiuqiVM7eynSQLJis1hV0vlVnhTd7M7kzt7zoOjzxVb5Uapz02uCp1Fsm4wQMz09qwQ==", + "dependencies": { + "@react-aria/focus": "^3.19.0", + "@react-aria/i18n": "^3.12.4", + "@react-aria/utils": "^3.26.0", + "@react-types/shared": "^3.26.0", + "@swc/helpers": "^0.5.0" + }, + "peerDependencies": { + "react": "^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1" + } + }, + "node_modules/@react-aria/tooltip": { + "version": "3.7.10", + "resolved": "https://registry.npmjs.org/@react-aria/tooltip/-/tooltip-3.7.10.tgz", + "integrity": "sha512-Udi3XOnrF/SYIz72jw9bgB74MG/yCOzF5pozHj2FH2HiJlchYv/b6rHByV/77IZemdlkmL/uugrv/7raPLSlnw==", + "dependencies": { + "@react-aria/focus": "^3.19.0", + "@react-aria/interactions": "^3.22.5", + "@react-aria/utils": "^3.26.0", + "@react-stately/tooltip": "^3.5.0", + "@react-types/shared": "^3.26.0", + "@react-types/tooltip": "^3.4.13", + "@swc/helpers": "^0.5.0" + }, + "peerDependencies": { + "react": "^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1" + } + }, + "node_modules/@react-aria/utils": { + "version": "3.26.0", + "resolved": "https://registry.npmjs.org/@react-aria/utils/-/utils-3.26.0.tgz", + "integrity": "sha512-LkZouGSjjQ0rEqo4XJosS4L3YC/zzQkfRM3KoqK6fUOmUJ9t0jQ09WjiF+uOoG9u+p30AVg3TrZRUWmoTS+koQ==", + "dependencies": { + "@react-aria/ssr": "^3.9.7", + "@react-stately/utils": "^3.10.5", + "@react-types/shared": "^3.26.0", + "@swc/helpers": "^0.5.0", + "clsx": "^2.0.0" + }, + "peerDependencies": { + "react": "^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1" + } + }, + "node_modules/@react-aria/visually-hidden": { + "version": "3.8.18", + "resolved": "https://registry.npmjs.org/@react-aria/visually-hidden/-/visually-hidden-3.8.18.tgz", + "integrity": "sha512-l/0igp+uub/salP35SsNWq5mGmg3G5F5QMS1gDZ8p28n7CgjvzyiGhJbbca7Oxvaw1HRFzVl9ev+89I7moNnFQ==", + "dependencies": { + "@react-aria/interactions": "^3.22.5", + "@react-aria/utils": "^3.26.0", + "@react-types/shared": "^3.26.0", + "@swc/helpers": "^0.5.0" + }, + "peerDependencies": { + "react": "^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1" + } + }, + "node_modules/@react-stately/calendar": { + "version": "3.6.0", + "resolved": "https://registry.npmjs.org/@react-stately/calendar/-/calendar-3.6.0.tgz", + "integrity": "sha512-GqUtOtGnwWjtNrJud8nY/ywI4VBP5byToNVRTnxbMl+gYO1Qe/uc5NG7zjwMxhb2kqSBHZFdkF0DXVqG2Ul+BA==", + "dependencies": { + "@internationalized/date": "^3.6.0", + "@react-stately/utils": "^3.10.5", + "@react-types/calendar": "^3.5.0", + "@react-types/shared": "^3.26.0", + "@swc/helpers": "^0.5.0" + }, + "peerDependencies": { + "react": "^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1" + } + }, + "node_modules/@react-stately/checkbox": { + "version": "3.6.10", + "resolved": "https://registry.npmjs.org/@react-stately/checkbox/-/checkbox-3.6.10.tgz", + "integrity": "sha512-LHm7i4YI8A/RdgWAuADrnSAYIaYYpQeZqsp1a03Og0pJHAlZL0ymN3y2IFwbZueY0rnfM+yF+kWNXjJqbKrFEQ==", + "dependencies": { + "@react-stately/form": "^3.1.0", + "@react-stately/utils": "^3.10.5", + "@react-types/checkbox": "^3.9.0", + "@react-types/shared": "^3.26.0", + "@swc/helpers": "^0.5.0" + }, + "peerDependencies": { + "react": "^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1" + } + }, + "node_modules/@react-stately/collections": { + "version": "3.12.0", + "resolved": "https://registry.npmjs.org/@react-stately/collections/-/collections-3.12.0.tgz", + "integrity": "sha512-MfR9hwCxe5oXv4qrLUnjidwM50U35EFmInUeFf8i9mskYwWlRYS0O1/9PZ0oF1M0cKambaRHKEy98jczgb9ycA==", + "dependencies": { + "@react-types/shared": "^3.26.0", + "@swc/helpers": "^0.5.0" + }, + "peerDependencies": { + "react": "^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1" + } + }, + "node_modules/@react-stately/color": { + "version": "3.8.1", + "resolved": "https://registry.npmjs.org/@react-stately/color/-/color-3.8.1.tgz", + "integrity": "sha512-7eN7K+KJRu+rxK351eGrzoq2cG+yipr90i5b1cUu4lioYmcH4WdsfjmM5Ku6gypbafH+kTDfflvO6hiY1NZH+A==", + "dependencies": { + "@internationalized/number": "^3.6.0", + "@internationalized/string": "^3.2.5", + "@react-aria/i18n": "^3.12.4", + "@react-stately/form": "^3.1.0", + "@react-stately/numberfield": "^3.9.8", + "@react-stately/slider": "^3.6.0", + "@react-stately/utils": "^3.10.5", + "@react-types/color": "^3.0.1", + "@react-types/shared": "^3.26.0", + "@swc/helpers": "^0.5.0" + }, + "peerDependencies": { + "react": "^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1" + } + }, + "node_modules/@react-stately/combobox": { + "version": "3.10.1", + "resolved": "https://registry.npmjs.org/@react-stately/combobox/-/combobox-3.10.1.tgz", + "integrity": "sha512-Rso+H+ZEDGFAhpKWbnRxRR/r7YNmYVtt+Rn0eNDNIUp3bYaxIBCdCySyAtALs4I8RZXZQ9zoUznP7YeVwG3cLg==", + "dependencies": { + "@react-stately/collections": "^3.12.0", + "@react-stately/form": "^3.1.0", + "@react-stately/list": "^3.11.1", + "@react-stately/overlays": "^3.6.12", + "@react-stately/select": "^3.6.9", + "@react-stately/utils": "^3.10.5", + "@react-types/combobox": "^3.13.1", + "@react-types/shared": "^3.26.0", + "@swc/helpers": "^0.5.0" + }, + "peerDependencies": { + "react": "^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1" + } + }, + "node_modules/@react-stately/datepicker": { + "version": "3.11.0", + "resolved": "https://registry.npmjs.org/@react-stately/datepicker/-/datepicker-3.11.0.tgz", + "integrity": "sha512-d9MJF34A0VrhL5y5S8mAISA8uwfNCQKmR2k4KoQJm3De1J8SQeNzSjLviAwh1faDow6FXGlA6tVbTrHyDcBgBg==", + "dependencies": { + "@internationalized/date": "^3.6.0", + "@internationalized/string": "^3.2.5", + "@react-stately/form": "^3.1.0", + "@react-stately/overlays": "^3.6.12", + "@react-stately/utils": "^3.10.5", + "@react-types/datepicker": "^3.9.0", + "@react-types/shared": "^3.26.0", + "@swc/helpers": "^0.5.0" + }, + "peerDependencies": { + "react": "^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1" + } + }, + "node_modules/@react-stately/disclosure": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/@react-stately/disclosure/-/disclosure-3.0.0.tgz", + "integrity": "sha512-Z9+fi0/41ZXHjGopORQza7mk4lFEFslKhy65ehEo6O6j2GuIV0659ExIVDsmJoJSFjXCfGh0sX8oTSOlXi9gqg==", + "dependencies": { + "@react-stately/utils": "^3.10.5", + "@react-types/shared": "^3.26.0", + "@swc/helpers": "^0.5.0" + }, + "peerDependencies": { + "react": "^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1" + } + }, + "node_modules/@react-stately/dnd": { + "version": "3.5.0", + "resolved": "https://registry.npmjs.org/@react-stately/dnd/-/dnd-3.5.0.tgz", + "integrity": "sha512-ZcWFw1npEDnATiy3TEdzA1skQ3UEIyfbNA6VhPNO8yiSVLxoxBOaEaq8VVS72fRGAtxud6dgOy8BnsP9JwDClQ==", + "dependencies": { + "@react-stately/selection": "^3.18.0", + "@react-types/shared": "^3.26.0", + "@swc/helpers": "^0.5.0" + }, + "peerDependencies": { + "react": "^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1" + } + }, + "node_modules/@react-stately/flags": { + "version": "3.0.5", + "resolved": "https://registry.npmjs.org/@react-stately/flags/-/flags-3.0.5.tgz", + "integrity": "sha512-6wks4csxUwPCp23LgJSnkBRhrWpd9jGd64DjcCTNB2AHIFu7Ab1W59pJpUL6TW7uAxVxdNKjgn6D1hlBy8qWsA==", + "dependencies": { + "@swc/helpers": "^0.5.0" + } + }, + "node_modules/@react-stately/form": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/@react-stately/form/-/form-3.1.0.tgz", + "integrity": "sha512-E2wxNQ0QaTyDHD0nJFtTSnEH9A3bpJurwxhS4vgcUmESHgjFEMLlC9irUSZKgvOgb42GAq+fHoWBsgKeTp9Big==", + "dependencies": { + "@react-types/shared": "^3.26.0", + "@swc/helpers": "^0.5.0" + }, + "peerDependencies": { + "react": "^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1" + } + }, + "node_modules/@react-stately/grid": { + "version": "3.10.0", + "resolved": "https://registry.npmjs.org/@react-stately/grid/-/grid-3.10.0.tgz", + "integrity": "sha512-ii+DdsOBvCnHMgL0JvUfFwO1kiAPP19Bpdpl6zn/oOltk6F5TmnoyNrzyz+2///1hCiySI3FE1O7ujsAQs7a6Q==", + "dependencies": { + "@react-stately/collections": "^3.12.0", + "@react-stately/selection": "^3.18.0", + "@react-types/grid": "^3.2.10", + "@react-types/shared": "^3.26.0", + "@swc/helpers": "^0.5.0" + }, + "peerDependencies": { + "react": "^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1" + } + }, + "node_modules/@react-stately/list": { + "version": "3.11.1", + "resolved": "https://registry.npmjs.org/@react-stately/list/-/list-3.11.1.tgz", + "integrity": "sha512-UCOpIvqBOjwLtk7zVTYWuKU1m1Oe61Q5lNar/GwHaV1nAiSQ8/yYlhr40NkBEs9X3plEfsV28UIpzOrYnu1tPg==", + "dependencies": { + "@react-stately/collections": "^3.12.0", + "@react-stately/selection": "^3.18.0", + "@react-stately/utils": "^3.10.5", + "@react-types/shared": "^3.26.0", + "@swc/helpers": "^0.5.0" + }, + "peerDependencies": { + "react": "^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1" + } + }, + "node_modules/@react-stately/menu": { + "version": "3.9.0", + "resolved": "https://registry.npmjs.org/@react-stately/menu/-/menu-3.9.0.tgz", + "integrity": "sha512-++sm0fzZeUs9GvtRbj5RwrP+KL9KPANp9f4SvtI3s+MP+Y/X3X7LNNePeeccGeyikB5fzMsuyvd82bRRW9IhDQ==", + "dependencies": { + "@react-stately/overlays": "^3.6.12", + "@react-types/menu": "^3.9.13", + "@react-types/shared": "^3.26.0", + "@swc/helpers": "^0.5.0" + }, + "peerDependencies": { + "react": "^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1" + } + }, + "node_modules/@react-stately/numberfield": { + "version": "3.9.8", + "resolved": "https://registry.npmjs.org/@react-stately/numberfield/-/numberfield-3.9.8.tgz", + "integrity": "sha512-J6qGILxDNEtu7yvd3/y+FpbrxEaAeIODwlrFo6z1kvuDlLAm/KszXAc75yoDi0OtakFTCMP6/HR5VnHaQdMJ3w==", + "dependencies": { + "@internationalized/number": "^3.6.0", + "@react-stately/form": "^3.1.0", + "@react-stately/utils": "^3.10.5", + "@react-types/numberfield": "^3.8.7", + "@swc/helpers": "^0.5.0" + }, + "peerDependencies": { + "react": "^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1" + } + }, + "node_modules/@react-stately/overlays": { + "version": "3.6.12", + "resolved": "https://registry.npmjs.org/@react-stately/overlays/-/overlays-3.6.12.tgz", + "integrity": "sha512-QinvZhwZgj8obUyPIcyURSCjTZlqZYRRCS60TF8jH8ZpT0tEAuDb3wvhhSXuYA3Xo9EHLwvLjEf3tQKKdAQArw==", + "dependencies": { + "@react-stately/utils": "^3.10.5", + "@react-types/overlays": "^3.8.11", + "@swc/helpers": "^0.5.0" + }, + "peerDependencies": { + "react": "^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1" + } + }, + "node_modules/@react-stately/radio": { + "version": "3.10.9", + "resolved": "https://registry.npmjs.org/@react-stately/radio/-/radio-3.10.9.tgz", + "integrity": "sha512-kUQ7VdqFke8SDRCatw2jW3rgzMWbvw+n2imN2THETynI47NmNLzNP11dlGO2OllRtTrsLhmBNlYHa3W62pFpAw==", + "dependencies": { + "@react-stately/form": "^3.1.0", + "@react-stately/utils": "^3.10.5", + "@react-types/radio": "^3.8.5", + "@react-types/shared": "^3.26.0", + "@swc/helpers": "^0.5.0" + }, + "peerDependencies": { + "react": "^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1" + } + }, + "node_modules/@react-stately/searchfield": { + "version": "3.5.8", + "resolved": "https://registry.npmjs.org/@react-stately/searchfield/-/searchfield-3.5.8.tgz", + "integrity": "sha512-jtquvGadx1DmtQqPKaVO6Qg/xpBjNxsOd59ciig9xRxpxV+90i996EX1E2R6R+tGJdSM1pD++7PVOO4yE++HOg==", + "dependencies": { + "@react-stately/utils": "^3.10.5", + "@react-types/searchfield": "^3.5.10", + "@swc/helpers": "^0.5.0" + }, + "peerDependencies": { + "react": "^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1" + } + }, + "node_modules/@react-stately/select": { + "version": "3.6.9", + "resolved": "https://registry.npmjs.org/@react-stately/select/-/select-3.6.9.tgz", + "integrity": "sha512-vASUDv7FhEYQURzM+JIwcusPv7/x/l3zHc/oKJPvoCl3aa9pwS8hZwS82SC00o2iFnrDscfDJju4IE/cd4hucg==", + "dependencies": { + "@react-stately/form": "^3.1.0", + "@react-stately/list": "^3.11.1", + "@react-stately/overlays": "^3.6.12", + "@react-types/select": "^3.9.8", + "@react-types/shared": "^3.26.0", + "@swc/helpers": "^0.5.0" + }, + "peerDependencies": { + "react": "^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1" + } + }, + "node_modules/@react-stately/selection": { + "version": "3.18.0", + "resolved": "https://registry.npmjs.org/@react-stately/selection/-/selection-3.18.0.tgz", + "integrity": "sha512-6EaNNP3exxBhW2LkcRR4a3pg+3oDguZlBSqIVVR7lyahv/D8xXHRC4dX+m0mgGHJpsgjs7664Xx6c8v193TFxg==", + "dependencies": { + "@react-stately/collections": "^3.12.0", + "@react-stately/utils": "^3.10.5", + "@react-types/shared": "^3.26.0", + "@swc/helpers": "^0.5.0" + }, + "peerDependencies": { + "react": "^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1" + } + }, + "node_modules/@react-stately/slider": { + "version": "3.6.0", + "resolved": "https://registry.npmjs.org/@react-stately/slider/-/slider-3.6.0.tgz", + "integrity": "sha512-w5vJxVh267pmD1X+Ppd9S3ZzV1hcg0cV8q5P4Egr160b9WMcWlUspZPtsthwUlN7qQe/C8y5IAhtde4s29eNag==", + "dependencies": { + "@react-stately/utils": "^3.10.5", + "@react-types/shared": "^3.26.0", + "@react-types/slider": "^3.7.7", + "@swc/helpers": "^0.5.0" + }, + "peerDependencies": { + "react": "^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1" + } + }, + "node_modules/@react-stately/table": { + "version": "3.13.0", + "resolved": "https://registry.npmjs.org/@react-stately/table/-/table-3.13.0.tgz", + "integrity": "sha512-mRbNYrwQIE7xzVs09Lk3kPteEVFVyOc20vA8ph6EP54PiUf/RllJpxZe/WUYLf4eom9lUkRYej5sffuUBpxjCA==", + "dependencies": { + "@react-stately/collections": "^3.12.0", + "@react-stately/flags": "^3.0.5", + "@react-stately/grid": "^3.10.0", + "@react-stately/selection": "^3.18.0", + "@react-stately/utils": "^3.10.5", + "@react-types/grid": "^3.2.10", + "@react-types/shared": "^3.26.0", + "@react-types/table": "^3.10.3", + "@swc/helpers": "^0.5.0" + }, + "peerDependencies": { + "react": "^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1" + } + }, + "node_modules/@react-stately/tabs": { + "version": "3.7.0", + "resolved": "https://registry.npmjs.org/@react-stately/tabs/-/tabs-3.7.0.tgz", + "integrity": "sha512-ox4hTkfZCoR4Oyr3Op3rBlWNq2Wxie04vhEYpTZQ2hobR3l4fYaOkd7CPClILktJ3TC104j8wcb0knWxIBRx9w==", + "dependencies": { + "@react-stately/list": "^3.11.1", + "@react-types/shared": "^3.26.0", + "@react-types/tabs": "^3.3.11", + "@swc/helpers": "^0.5.0" + }, + "peerDependencies": { + "react": "^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1" + } + }, + "node_modules/@react-stately/toggle": { + "version": "3.8.0", + "resolved": "https://registry.npmjs.org/@react-stately/toggle/-/toggle-3.8.0.tgz", + "integrity": "sha512-pyt/k/J8BwE/2g6LL6Z6sMSWRx9HEJB83Sm/MtovXnI66sxJ2EfQ1OaXB7Su5PEL9OMdoQF6Mb+N1RcW3zAoPw==", + "dependencies": { + "@react-stately/utils": "^3.10.5", + "@react-types/checkbox": "^3.9.0", + "@react-types/shared": "^3.26.0", + "@swc/helpers": "^0.5.0" + }, + "peerDependencies": { + "react": "^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1" + } + }, + "node_modules/@react-stately/tooltip": { + "version": "3.5.0", + "resolved": "https://registry.npmjs.org/@react-stately/tooltip/-/tooltip-3.5.0.tgz", + "integrity": "sha512-+xzPNztJDd2XJD0X3DgWKlrgOhMqZpSzsIssXeJgO7uCnP8/Z513ESaipJhJCFC8fxj5caO/DK4Uu8hEtlB8cQ==", + "dependencies": { + "@react-stately/overlays": "^3.6.12", + "@react-types/tooltip": "^3.4.13", + "@swc/helpers": "^0.5.0" + }, + "peerDependencies": { + "react": "^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1" + } + }, + "node_modules/@react-stately/tree": { + "version": "3.8.6", + "resolved": "https://registry.npmjs.org/@react-stately/tree/-/tree-3.8.6.tgz", + "integrity": "sha512-lblUaxf1uAuIz5jm6PYtcJ+rXNNVkqyFWTIMx6g6gW/mYvm8GNx1G/0MLZE7E6CuDGaO9dkLSY2bB1uqyKHidA==", + "dependencies": { + "@react-stately/collections": "^3.12.0", + "@react-stately/selection": "^3.18.0", + "@react-stately/utils": "^3.10.5", + "@react-types/shared": "^3.26.0", + "@swc/helpers": "^0.5.0" + }, + "peerDependencies": { + "react": "^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1" + } + }, + "node_modules/@react-stately/utils": { + "version": "3.10.5", + "resolved": "https://registry.npmjs.org/@react-stately/utils/-/utils-3.10.5.tgz", + "integrity": "sha512-iMQSGcpaecghDIh3mZEpZfoFH3ExBwTtuBEcvZ2XnGzCgQjeYXcMdIUwAfVQLXFTdHUHGF6Gu6/dFrYsCzySBQ==", + "dependencies": { + "@swc/helpers": "^0.5.0" + }, + "peerDependencies": { + "react": "^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1" + } + }, + "node_modules/@react-types/breadcrumbs": { + "version": "3.7.9", + "resolved": "https://registry.npmjs.org/@react-types/breadcrumbs/-/breadcrumbs-3.7.9.tgz", + "integrity": "sha512-eARYJo8J+VfNV8vP4uw3L2Qliba9wLV2bx9YQCYf5Lc/OE5B/y4gaTLz+Y2P3Rtn6gBPLXY447zCs5i7gf+ICg==", + "dependencies": { + "@react-types/link": "^3.5.9", + "@react-types/shared": "^3.26.0" + }, + "peerDependencies": { + "react": "^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1" + } + }, + "node_modules/@react-types/button": { + "version": "3.10.1", + "resolved": "https://registry.npmjs.org/@react-types/button/-/button-3.10.1.tgz", + "integrity": "sha512-XTtap8o04+4QjPNAshFWOOAusUTxQlBjU2ai0BTVLShQEjHhRVDBIWsI2B2FKJ4KXT6AZ25llaxhNrreWGonmA==", + "dependencies": { + "@react-types/shared": "^3.26.0" + }, + "peerDependencies": { + "react": "^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1" + } + }, + "node_modules/@react-types/calendar": { + "version": "3.5.0", + "resolved": "https://registry.npmjs.org/@react-types/calendar/-/calendar-3.5.0.tgz", + "integrity": "sha512-O3IRE7AGwAWYnvJIJ80cOy7WwoJ0m8GtX/qSmvXQAjC4qx00n+b5aFNBYAQtcyc3RM5QpW6obs9BfwGetFiI8w==", + "dependencies": { + "@internationalized/date": "^3.6.0", + "@react-types/shared": "^3.26.0" }, - "bin": { - "vite": "bin/vite.js" + "peerDependencies": { + "react": "^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1" + } + }, + "node_modules/@react-types/checkbox": { + "version": "3.9.0", + "resolved": "https://registry.npmjs.org/@react-types/checkbox/-/checkbox-3.9.0.tgz", + "integrity": "sha512-9hbHx0Oo2Hp5a8nV8Q75LQR0DHtvOIJbFaeqESSopqmV9EZoYjtY/h0NS7cZetgahQgnqYWQi44XGooMDCsmxA==", + "dependencies": { + "@react-types/shared": "^3.26.0" }, - "engines": { - "node": "^14.18.0 || >=16.0.0" + "peerDependencies": { + "react": "^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1" + } + }, + "node_modules/@react-types/color": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/@react-types/color/-/color-3.0.1.tgz", + "integrity": "sha512-KemFziO3GbmT3HEKrgOGdqNA6Gsmy9xrwFO3f8qXSG7gVz6M27Ic4R9HVQv4iAjap5uti6W13/pk2bc/jLVcEA==", + "dependencies": { + "@react-types/shared": "^3.26.0", + "@react-types/slider": "^3.7.7" }, - "funding": { - "url": "https://github.com/vitejs/vite?sponsor=1" + "peerDependencies": { + "react": "^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1" + } + }, + "node_modules/@react-types/combobox": { + "version": "3.13.1", + "resolved": "https://registry.npmjs.org/@react-types/combobox/-/combobox-3.13.1.tgz", + "integrity": "sha512-7xr+HknfhReN4QPqKff5tbKTe2kGZvH+DGzPYskAtb51FAAiZsKo+WvnNAvLwg3kRoC9Rkn4TAiVBp/HgymRDw==", + "dependencies": { + "@react-types/shared": "^3.26.0" }, - "optionalDependencies": { - "fsevents": "~2.3.2" + "peerDependencies": { + "react": "^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1" + } + }, + "node_modules/@react-types/datepicker": { + "version": "3.9.0", + "resolved": "https://registry.npmjs.org/@react-types/datepicker/-/datepicker-3.9.0.tgz", + "integrity": "sha512-dbKL5Qsm2MQwOTtVQdOcKrrphcXAqDD80WLlSQrBLg+waDuuQ7H+TrvOT0thLKloNBlFUGnZZfXGRHINpih/0g==", + "dependencies": { + "@internationalized/date": "^3.6.0", + "@react-types/calendar": "^3.5.0", + "@react-types/overlays": "^3.8.11", + "@react-types/shared": "^3.26.0" }, "peerDependencies": { - "@types/node": ">= 14", - "less": "*", - "lightningcss": "^1.21.0", - "sass": "*", - "stylus": "*", - "sugarss": "*", - "terser": "^5.4.0" + "react": "^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1" + } + }, + "node_modules/@react-types/dialog": { + "version": "3.5.14", + "resolved": "https://registry.npmjs.org/@react-types/dialog/-/dialog-3.5.14.tgz", + "integrity": "sha512-OXWMjrALwrlgw8aHD8SeRm/s3tbAssdaEh2h73KUSeFau3fU3n5mfKv+WnFqsEaOtN261o48l7hTlS6615H9AA==", + "dependencies": { + "@react-types/overlays": "^3.8.11", + "@react-types/shared": "^3.26.0" }, - "peerDependenciesMeta": { - "@types/node": { - "optional": true - }, - "less": { - "optional": true - }, - "lightningcss": { - "optional": true - }, - "sass": { - "optional": true - }, - "stylus": { - "optional": true - }, - "sugarss": { - "optional": true - }, - "terser": { - "optional": true - } + "peerDependencies": { + "react": "^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1" } }, - "node_modules/@playwright/experimental-ct-react": { - "version": "1.40.1", - "resolved": "https://registry.npmjs.org/@playwright/experimental-ct-react/-/experimental-ct-react-1.40.1.tgz", - "integrity": "sha512-a2ubB04+pSswpWOgIwgBcSvvdvVNv4Cz8wud5ZLV5+4fcRqRACxFlGJPiVHw1zanhDSD+rH6H9+zaNm/o1iJHw==", - "dev": true, + "node_modules/@react-types/grid": { + "version": "3.2.10", + "resolved": "https://registry.npmjs.org/@react-types/grid/-/grid-3.2.10.tgz", + "integrity": "sha512-Z5cG0ITwqjUE4kWyU5/7VqiPl4wqMJ7kG/ZP7poAnLmwRsR8Ai0ceVn+qzp5nTA19cgURi8t3LsXn3Ar1FBoog==", "dependencies": { - "@playwright/experimental-ct-core": "1.40.1", - "@vitejs/plugin-react": "^4.0.0" + "@react-types/shared": "^3.26.0" }, - "bin": { - "playwright": "cli.js" + "peerDependencies": { + "react": "^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1" + } + }, + "node_modules/@react-types/link": { + "version": "3.5.9", + "resolved": "https://registry.npmjs.org/@react-types/link/-/link-3.5.9.tgz", + "integrity": "sha512-JcKDiDMqrq/5Vpn+BdWQEuXit4KN4HR/EgIi3yKnNbYkLzxBoeQZpQgvTaC7NEQeZnSqkyXQo3/vMUeX/ZNIKw==", + "dependencies": { + "@react-types/shared": "^3.26.0" }, - "engines": { - "node": ">=16" + "peerDependencies": { + "react": "^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1" } }, - "node_modules/@playwright/test": { - "version": "1.40.1", - "resolved": "https://registry.npmjs.org/@playwright/test/-/test-1.40.1.tgz", - "integrity": "sha512-EaaawMTOeEItCRvfmkI9v6rBkF1svM8wjl/YPRrg2N2Wmp+4qJYkWtJsbew1szfKKDm6fPLy4YAanBhIlf9dWw==", - "dev": true, + "node_modules/@react-types/listbox": { + "version": "3.5.3", + "resolved": "https://registry.npmjs.org/@react-types/listbox/-/listbox-3.5.3.tgz", + "integrity": "sha512-v1QXd9/XU3CCKr2Vgs7WLcTr6VMBur7CrxHhWZQQFExsf9bgJ/3wbUdjy4aThY/GsYHiaS38EKucCZFr1QAfqA==", "dependencies": { - "playwright": "1.40.1" + "@react-types/shared": "^3.26.0" }, - "bin": { - "playwright": "cli.js" + "peerDependencies": { + "react": "^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1" + } + }, + "node_modules/@react-types/menu": { + "version": "3.9.13", + "resolved": "https://registry.npmjs.org/@react-types/menu/-/menu-3.9.13.tgz", + "integrity": "sha512-7SuX6E2tDsqQ+HQdSvIda1ji/+ujmR86dtS9CUu5yWX91P25ufRjZ72EvLRqClWNQsj1Xl4+2zBDLWlceznAjw==", + "dependencies": { + "@react-types/overlays": "^3.8.11", + "@react-types/shared": "^3.26.0" }, - "engines": { - "node": ">=16" + "peerDependencies": { + "react": "^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1" } }, - "node_modules/@plotly/d3": { - "version": "3.8.1", - "resolved": "https://registry.npmjs.org/@plotly/d3/-/d3-3.8.1.tgz", - "integrity": "sha512-x49ThEu1FRA00kTso4Jdfyf2byaCPLBGmLjAYQz5OzaPyLUhHesX3/Nfv2OHEhynhdy2UB39DLXq6thYe2L2kg==", - "peer": true + "node_modules/@react-types/meter": { + "version": "3.4.5", + "resolved": "https://registry.npmjs.org/@react-types/meter/-/meter-3.4.5.tgz", + "integrity": "sha512-04w1lEtvP/c3Ep8ND8hhH2rwjz2MtQ8o8SNLhahen3u0rX3jKOgD4BvHujsyvXXTMjj1Djp74sGzNawb4Ppi9w==", + "dependencies": { + "@react-types/progress": "^3.5.8" + }, + "peerDependencies": { + "react": "^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1" + } }, - "node_modules/@plotly/d3-sankey": { - "version": "0.7.2", - "resolved": "https://registry.npmjs.org/@plotly/d3-sankey/-/d3-sankey-0.7.2.tgz", - "integrity": "sha512-2jdVos1N3mMp3QW0k2q1ph7Gd6j5PY1YihBrwpkFnKqO+cqtZq3AdEYUeSGXMeLsBDQYiqTVcihYfk8vr5tqhw==", - "peer": true, + "node_modules/@react-types/numberfield": { + "version": "3.8.7", + "resolved": "https://registry.npmjs.org/@react-types/numberfield/-/numberfield-3.8.7.tgz", + "integrity": "sha512-KccMPi39cLoVkB2T0V7HW6nsxQVAwt89WWCltPZJVGzsebv/k0xTQlPVAgrUake4kDLoE687e3Fr/Oe3+1bDhw==", "dependencies": { - "d3-array": "1", - "d3-collection": "1", - "d3-shape": "^1.2.0" + "@react-types/shared": "^3.26.0" + }, + "peerDependencies": { + "react": "^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1" } }, - "node_modules/@plotly/d3-sankey-circular": { - "version": "0.33.1", - "resolved": "https://registry.npmjs.org/@plotly/d3-sankey-circular/-/d3-sankey-circular-0.33.1.tgz", - "integrity": "sha512-FgBV1HEvCr3DV7RHhDsPXyryknucxtfnLwPtCKKxdolKyTFYoLX/ibEfX39iFYIL7DYbVeRtP43dbFcrHNE+KQ==", - "peer": true, + "node_modules/@react-types/overlays": { + "version": "3.8.11", + "resolved": "https://registry.npmjs.org/@react-types/overlays/-/overlays-3.8.11.tgz", + "integrity": "sha512-aw7T0rwVI3EuyG5AOaEIk8j7dZJQ9m34XAztXJVZ/W2+4pDDkLDbJ/EAPnuo2xGYRGhowuNDn4tDju01eHYi+w==", "dependencies": { - "d3-array": "^1.2.1", - "d3-collection": "^1.0.4", - "d3-shape": "^1.2.0", - "elementary-circuits-directed-graph": "^1.0.4" + "@react-types/shared": "^3.26.0" + }, + "peerDependencies": { + "react": "^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1" } }, - "node_modules/@plotly/d3-sankey-circular/node_modules/d3-array": { - "version": "1.2.4", - "resolved": "https://registry.npmjs.org/d3-array/-/d3-array-1.2.4.tgz", - "integrity": "sha512-KHW6M86R+FUPYGb3R5XiYjXPq7VzwxZ22buHhAEVG5ztoEcZZMLov530mmccaqA1GghZArjQV46fuc8kUqhhHw==", - "peer": true + "node_modules/@react-types/progress": { + "version": "3.5.8", + "resolved": "https://registry.npmjs.org/@react-types/progress/-/progress-3.5.8.tgz", + "integrity": "sha512-PR0rN5mWevfblR/zs30NdZr+82Gka/ba7UHmYOW9/lkKlWeD7PHgl1iacpd/3zl/jUF22evAQbBHmk1mS6Mpqw==", + "dependencies": { + "@react-types/shared": "^3.26.0" + }, + "peerDependencies": { + "react": "^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1" + } }, - "node_modules/@plotly/d3-sankey-circular/node_modules/d3-path": { - "version": "1.0.9", - "resolved": "https://registry.npmjs.org/d3-path/-/d3-path-1.0.9.tgz", - "integrity": "sha512-VLaYcn81dtHVTjEHd8B+pbe9yHWpXKZUC87PzoFmsFrJqgFwDe/qxfp5MlfsfM1V5E/iVt0MmEbWQ7FVIXh/bg==", - "peer": true + "node_modules/@react-types/radio": { + "version": "3.8.5", + "resolved": "https://registry.npmjs.org/@react-types/radio/-/radio-3.8.5.tgz", + "integrity": "sha512-gSImTPid6rsbJmwCkTliBIU/npYgJHOFaI3PNJo7Y0QTAnFelCtYeFtBiWrFodSArSv7ASqpLLUEj9hZu/rxIg==", + "dependencies": { + "@react-types/shared": "^3.26.0" + }, + "peerDependencies": { + "react": "^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1" + } }, - "node_modules/@plotly/d3-sankey-circular/node_modules/d3-shape": { - "version": "1.3.7", - "resolved": "https://registry.npmjs.org/d3-shape/-/d3-shape-1.3.7.tgz", - "integrity": "sha512-EUkvKjqPFUAZyOlhY5gzCxCeI0Aep04LwIRpsZ/mLFelJiUfnK56jo5JMDSE7yyP2kLSb6LtF+S5chMk7uqPqw==", - "peer": true, + "node_modules/@react-types/searchfield": { + "version": "3.5.10", + "resolved": "https://registry.npmjs.org/@react-types/searchfield/-/searchfield-3.5.10.tgz", + "integrity": "sha512-7wW4pJzbReawoGPu8a4l+CODTCDN088EN/ysUzl622ewim57PjArjix+lpO4+aEtJqS9HKpq8UEbjwo9axpcUA==", "dependencies": { - "d3-path": "1" + "@react-types/shared": "^3.26.0", + "@react-types/textfield": "^3.10.0" + }, + "peerDependencies": { + "react": "^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1" } }, - "node_modules/@plotly/d3-sankey/node_modules/d3-array": { - "version": "1.2.4", - "resolved": "https://registry.npmjs.org/d3-array/-/d3-array-1.2.4.tgz", - "integrity": "sha512-KHW6M86R+FUPYGb3R5XiYjXPq7VzwxZ22buHhAEVG5ztoEcZZMLov530mmccaqA1GghZArjQV46fuc8kUqhhHw==", - "peer": true + "node_modules/@react-types/select": { + "version": "3.9.8", + "resolved": "https://registry.npmjs.org/@react-types/select/-/select-3.9.8.tgz", + "integrity": "sha512-RGsYj2oFjXpLnfcvWMBQnkcDuKkwT43xwYWZGI214/gp/B64tJiIUgTM5wFTRAeGDX23EePkhCQF+9ctnqFd6g==", + "dependencies": { + "@react-types/shared": "^3.26.0" + }, + "peerDependencies": { + "react": "^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1" + } }, - "node_modules/@plotly/d3-sankey/node_modules/d3-path": { - "version": "1.0.9", - "resolved": "https://registry.npmjs.org/d3-path/-/d3-path-1.0.9.tgz", - "integrity": "sha512-VLaYcn81dtHVTjEHd8B+pbe9yHWpXKZUC87PzoFmsFrJqgFwDe/qxfp5MlfsfM1V5E/iVt0MmEbWQ7FVIXh/bg==", - "peer": true + "node_modules/@react-types/shared": { + "version": "3.26.0", + "resolved": "https://registry.npmjs.org/@react-types/shared/-/shared-3.26.0.tgz", + "integrity": "sha512-6FuPqvhmjjlpEDLTiYx29IJCbCNWPlsyO+ZUmCUXzhUv2ttShOXfw8CmeHWHftT/b2KweAWuzqSlfeXPR76jpw==", + "peerDependencies": { + "react": "^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1" + } }, - "node_modules/@plotly/d3-sankey/node_modules/d3-shape": { - "version": "1.3.7", - "resolved": "https://registry.npmjs.org/d3-shape/-/d3-shape-1.3.7.tgz", - "integrity": "sha512-EUkvKjqPFUAZyOlhY5gzCxCeI0Aep04LwIRpsZ/mLFelJiUfnK56jo5JMDSE7yyP2kLSb6LtF+S5chMk7uqPqw==", - "peer": true, + "node_modules/@react-types/slider": { + "version": "3.7.7", + "resolved": "https://registry.npmjs.org/@react-types/slider/-/slider-3.7.7.tgz", + "integrity": "sha512-lYTR9zXQV2fSEm/G3gwDENWiki1IXd/oorsgf0zu1DBi2SQDbOsLsGUXiwvD24Xy6OkUuhAqjLPPexezo7+u9g==", "dependencies": { - "d3-path": "1" + "@react-types/shared": "^3.26.0" + }, + "peerDependencies": { + "react": "^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1" } }, - "node_modules/@plotly/point-cluster": { - "version": "3.1.9", - "resolved": "https://registry.npmjs.org/@plotly/point-cluster/-/point-cluster-3.1.9.tgz", - "integrity": "sha512-MwaI6g9scKf68Orpr1pHZ597pYx9uP8UEFXLPbsCmuw3a84obwz6pnMXGc90VhgDNeNiLEdlmuK7CPo+5PIxXw==", - "peer": true, + "node_modules/@react-types/switch": { + "version": "3.5.7", + "resolved": "https://registry.npmjs.org/@react-types/switch/-/switch-3.5.7.tgz", + "integrity": "sha512-1IKiq510rPTHumEZuhxuazuXBa2Cuxz6wBIlwf3NCVmgWEvU+uk1ETG0sH2yymjwCqhtJDKXi+qi9HSgPEDwAg==", "dependencies": { - "array-bounds": "^1.0.1", - "binary-search-bounds": "^2.0.4", - "clamp": "^1.0.1", - "defined": "^1.0.0", - "dtype": "^2.0.0", - "flatten-vertex-data": "^1.0.2", - "is-obj": "^1.0.1", - "math-log2": "^1.0.1", - "parse-rect": "^1.2.0", - "pick-by-alias": "^1.2.0" + "@react-types/shared": "^3.26.0" + }, + "peerDependencies": { + "react": "^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1" } }, - "node_modules/@popperjs/core": { - "version": "2.11.8", - "resolved": "https://registry.npmjs.org/@popperjs/core/-/core-2.11.8.tgz", - "integrity": "sha512-P1st0aksCrn9sGZhp8GMYwBnQsbvAWsZAX44oXNNvLHGqAOcoVxmjZiohstwQ7SqKnbR47akdNi+uleWD8+g6A==", - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/popperjs" + "node_modules/@react-types/table": { + "version": "3.10.3", + "resolved": "https://registry.npmjs.org/@react-types/table/-/table-3.10.3.tgz", + "integrity": "sha512-Ac+W+m/zgRzlTU8Z2GEg26HkuJFswF9S6w26r+R3MHwr8z2duGPvv37XRtE1yf3dbpRBgHEAO141xqS2TqGwNg==", + "dependencies": { + "@react-types/grid": "^3.2.10", + "@react-types/shared": "^3.26.0" + }, + "peerDependencies": { + "react": "^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1" } }, - "node_modules/@probe.gl/env": { - "version": "4.0.9", - "resolved": "https://registry.npmjs.org/@probe.gl/env/-/env-4.0.9.tgz", - "integrity": "sha512-AOmVMD0/j78mX+k4+qX7ZhE0sY9H+EaJgIO6trik0BwV6VcrwxTGCGFAeuRsIGhETDnye06tkLXccYatYxAYwQ==" + "node_modules/@react-types/tabs": { + "version": "3.3.11", + "resolved": "https://registry.npmjs.org/@react-types/tabs/-/tabs-3.3.11.tgz", + "integrity": "sha512-BjF2TqBhZaIcC4lc82R5pDJd1F7kstj1K0Nokhz99AGYn8C0ITdp6lR+DPVY9JZRxKgP9R2EKfWGI90Lo7NQdA==", + "dependencies": { + "@react-types/shared": "^3.26.0" + }, + "peerDependencies": { + "react": "^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1" + } }, - "node_modules/@probe.gl/log": { - "version": "4.0.9", - "resolved": "https://registry.npmjs.org/@probe.gl/log/-/log-4.0.9.tgz", - "integrity": "sha512-ebuZaodSRE9aC+3bVC7cKRHT8garXeT1jTbj1R5tQRqQYc9iGeT3iemVOHx5bN9Q6gAs/0j54iPI+1DvWMAW4A==", + "node_modules/@react-types/textfield": { + "version": "3.10.0", + "resolved": "https://registry.npmjs.org/@react-types/textfield/-/textfield-3.10.0.tgz", + "integrity": "sha512-ShU3d6kLJGQjPXccVFjM3KOXdj3uyhYROqH9YgSIEVxgA9W6LRflvk/IVBamD9pJYTPbwmVzuP0wQkTDupfZ1w==", "dependencies": { - "@probe.gl/env": "4.0.9" + "@react-types/shared": "^3.26.0" + }, + "peerDependencies": { + "react": "^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1" } }, - "node_modules/@probe.gl/stats": { - "version": "4.0.9", - "resolved": "https://registry.npmjs.org/@probe.gl/stats/-/stats-4.0.9.tgz", - "integrity": "sha512-Q9Xt/sJUQaMsbjRKjOscv2t7wXIymTrOEJ4a3da4FTCn7bkKvcdxdyFAQySCrtPxE+YZ5I5lXpWPgv9BwmpE1g==" + "node_modules/@react-types/tooltip": { + "version": "3.4.13", + "resolved": "https://registry.npmjs.org/@react-types/tooltip/-/tooltip-3.4.13.tgz", + "integrity": "sha512-KPekFC17RTT8kZlk7ZYubueZnfsGTDOpLw7itzolKOXGddTXsrJGBzSB4Bb060PBVllaDO0MOrhPap8OmrIl1Q==", + "dependencies": { + "@react-types/overlays": "^3.8.11", + "@react-types/shared": "^3.26.0" + }, + "peerDependencies": { + "react": "^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1" + } }, "node_modules/@rollup/rollup-android-arm-eabi": { "version": "4.9.2", @@ -3649,6 +5235,14 @@ "integrity": "sha512-+Fj43pSMwJs4KRrH/938Uf+uAELIgVBmQzg/q1YG10djyfA3TnrU8N8XzqCh/okZdszqBQTZf96idMfE5lnwTA==", "dev": true }, + "node_modules/@swc/helpers": { + "version": "0.5.15", + "resolved": "https://registry.npmjs.org/@swc/helpers/-/helpers-0.5.15.tgz", + "integrity": "sha512-JQ5TuMi45Owi4/BIMAJBoSQoOJu12oOk/gADqlcUL9JEdHB8vyjUSsxqeNXnmXHjYKMi2WcYtezGEEhqUI/E2g==", + "dependencies": { + "tslib": "^2.8.0" + } + }, "node_modules/@tanstack/query-core": { "version": "5.17.19", "resolved": "https://registry.npmjs.org/@tanstack/query-core/-/query-core-5.17.19.tgz", @@ -3718,6 +5312,31 @@ "url": "https://github.com/sponsors/tannerlinsley" } }, + "node_modules/@tanstack/react-virtual": { + "version": "3.10.8", + "resolved": "https://registry.npmjs.org/@tanstack/react-virtual/-/react-virtual-3.10.8.tgz", + "integrity": "sha512-VbzbVGSsZlQktyLrP5nxE+vE1ZR+U0NFAWPbJLoG2+DKPwd2D7dVICTVIIaYlJqX1ZCEnYDbaOpmMwbsyhBoIA==", + "dependencies": { + "@tanstack/virtual-core": "3.10.8" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/tannerlinsley" + }, + "peerDependencies": { + "react": "^16.8.0 || ^17.0.0 || ^18.0.0", + "react-dom": "^16.8.0 || ^17.0.0 || ^18.0.0" + } + }, + "node_modules/@tanstack/virtual-core": { + "version": "3.10.8", + "resolved": "https://registry.npmjs.org/@tanstack/virtual-core/-/virtual-core-3.10.8.tgz", + "integrity": "sha512-PBu00mtt95jbKFi6Llk9aik8bnR3tR/oQP1o3TSi+iG//+Q2RTIzCEgKkHG8BB86kxMNW6O8wku+Lmi+QFR6jA==", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/tannerlinsley" + } + }, "node_modules/@trivago/prettier-plugin-sort-imports": { "version": "4.2.0", "resolved": "https://registry.npmjs.org/@trivago/prettier-plugin-sort-imports/-/prettier-plugin-sort-imports-4.2.0.tgz", @@ -7075,6 +8694,21 @@ "csstype": "^3.0.2" } }, + "node_modules/downshift": { + "version": "9.0.8", + "resolved": "https://registry.npmjs.org/downshift/-/downshift-9.0.8.tgz", + "integrity": "sha512-59BWD7+hSUQIM1DeNPLirNNnZIO9qMdIK5GQ/Uo8q34gT4B78RBlb9dhzgnh0HfQTJj4T/JKYD8KoLAlMWnTsA==", + "dependencies": { + "@babel/runtime": "^7.24.5", + "compute-scroll-into-view": "^3.1.0", + "prop-types": "^15.8.1", + "react-is": "18.2.0", + "tslib": "^2.6.2" + }, + "peerDependencies": { + "react": ">=16.12.0" + } + }, "node_modules/draco3d": { "version": "1.5.7", "resolved": "https://registry.npmjs.org/draco3d/-/draco3d-1.5.7.tgz", @@ -9272,6 +10906,17 @@ "node": ">=10.13.0" } }, + "node_modules/intl-messageformat": { + "version": "10.7.7", + "resolved": "https://registry.npmjs.org/intl-messageformat/-/intl-messageformat-10.7.7.tgz", + "integrity": "sha512-F134jIoeYMro/3I0h08D0Yt4N9o9pjddU/4IIxMMURqbAtI2wu70X8hvG1V48W49zXHXv3RKSF/po+0fDfsGjA==", + "dependencies": { + "@formatjs/ecma402-abstract": "2.2.4", + "@formatjs/fast-memoize": "2.2.3", + "@formatjs/icu-messageformat-parser": "2.9.4", + "tslib": "2" + } + }, "node_modules/is-array-buffer": { "version": "3.0.2", "resolved": "https://registry.npmjs.org/is-array-buffer/-/is-array-buffer-3.0.2.tgz", @@ -11856,6 +13501,56 @@ "node": ">=0.10.0" } }, + "node_modules/react-aria": { + "version": "3.36.0", + "resolved": "https://registry.npmjs.org/react-aria/-/react-aria-3.36.0.tgz", + "integrity": "sha512-AK5XyIhAN+e5HDlwlF+YwFrOrVI7RYmZ6kg/o7ZprQjkYqYKapXeUpWscmNm/3H2kDboE5Z4ymUnK6ZhobLqOw==", + "dependencies": { + "@internationalized/string": "^3.2.5", + "@react-aria/breadcrumbs": "^3.5.19", + "@react-aria/button": "^3.11.0", + "@react-aria/calendar": "^3.6.0", + "@react-aria/checkbox": "^3.15.0", + "@react-aria/color": "^3.0.2", + "@react-aria/combobox": "^3.11.0", + "@react-aria/datepicker": "^3.12.0", + "@react-aria/dialog": "^3.5.20", + "@react-aria/disclosure": "^3.0.0", + "@react-aria/dnd": "^3.8.0", + "@react-aria/focus": "^3.19.0", + "@react-aria/gridlist": "^3.10.0", + "@react-aria/i18n": "^3.12.4", + "@react-aria/interactions": "^3.22.5", + "@react-aria/label": "^3.7.13", + "@react-aria/link": "^3.7.7", + "@react-aria/listbox": "^3.13.6", + "@react-aria/menu": "^3.16.0", + "@react-aria/meter": "^3.4.18", + "@react-aria/numberfield": "^3.11.9", + "@react-aria/overlays": "^3.24.0", + "@react-aria/progress": "^3.4.18", + "@react-aria/radio": "^3.10.10", + "@react-aria/searchfield": "^3.7.11", + "@react-aria/select": "^3.15.0", + "@react-aria/selection": "^3.21.0", + "@react-aria/separator": "^3.4.4", + "@react-aria/slider": "^3.7.14", + "@react-aria/ssr": "^3.9.7", + "@react-aria/switch": "^3.6.10", + "@react-aria/table": "^3.16.0", + "@react-aria/tabs": "^3.9.8", + "@react-aria/tag": "^3.4.8", + "@react-aria/textfield": "^3.15.0", + "@react-aria/tooltip": "^3.7.10", + "@react-aria/utils": "^3.26.0", + "@react-aria/visually-hidden": "^3.8.18", + "@react-types/shared": "^3.26.0" + }, + "peerDependencies": { + "react": "^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1", + "react-dom": "^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1" + } + }, "node_modules/react-color": { "version": "2.19.3", "resolved": "https://registry.npmjs.org/react-color/-/react-color-2.19.3.tgz", @@ -13503,9 +15198,9 @@ } }, "node_modules/tslib": { - "version": "2.7.0", - "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.7.0.tgz", - "integrity": "sha512-gLXCKdN1/j47AiHiOkJN69hJmcbGTHI0ImLmbYLHykhgeN0jVGola9yVjFgzCUklsZQMW55o+dW7IXv3RCXDzA==" + "version": "2.8.1", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz", + "integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==" }, "node_modules/turf-jsts": { "version": "1.2.3", diff --git a/frontend/package.json b/frontend/package.json index 8836d81c5..27ef38466 100644 --- a/frontend/package.json +++ b/frontend/package.json @@ -19,6 +19,7 @@ "test:ct:ui": "playwright test -c playwright.ct.config.ts --ui" }, "dependencies": { + "@equinor/eds-core-react": "^0.42.5", "@equinor/esv-intersection": "^3.0.10", "@headlessui/react": "^1.7.8", "@mui/base": "^5.0.0-beta.3", @@ -28,9 +29,9 @@ "@tanstack/react-query-devtools": "^5.4.2", "@types/geojson": "^7946.0.14", "@webviz/group-tree-plot": "^1.1.14", - "@webviz/well-log-viewer": "^1.12.7", "@webviz/subsurface-viewer": "^1.1.1", "@webviz/well-completions-plot": "^1.5.11", + "@webviz/well-log-viewer": "^1.12.7", "animate.css": "^4.1.1", "axios": "^1.6.5", "culori": "^3.2.0", diff --git a/frontend/src/modules/2DViewer/interfaces.ts b/frontend/src/modules/2DViewer/interfaces.ts new file mode 100644 index 000000000..29ceee88e --- /dev/null +++ b/frontend/src/modules/2DViewer/interfaces.ts @@ -0,0 +1,23 @@ +import { InterfaceInitialization } from "@framework/UniDirectionalModuleComponentsInterface"; + +import { LayerManager } from "./layers/framework/LayerManager/LayerManager"; +import { layerManagerAtom, preferredViewLayoutAtom } from "./settings/atoms/baseAtoms"; +import { PreferredViewLayout } from "./types"; + +export type SettingsToViewInterface = { + layerManager: LayerManager | null; + preferredViewLayout: PreferredViewLayout; +}; + +export type Interfaces = { + settingsToView: SettingsToViewInterface; +}; + +export const settingsToViewInterfaceInitialization: InterfaceInitialization = { + layerManager: (get) => { + return get(layerManagerAtom); + }, + preferredViewLayout: (get) => { + return get(preferredViewLayoutAtom); + }, +}; diff --git a/frontend/src/modules/2DViewer/layers/LayersActions.tsx b/frontend/src/modules/2DViewer/layers/LayersActions.tsx new file mode 100644 index 000000000..f063c0a9a --- /dev/null +++ b/frontend/src/modules/2DViewer/layers/LayersActions.tsx @@ -0,0 +1,83 @@ +import React from "react"; + +import { Menu } from "@lib/components/Menu"; +import { MenuButton } from "@lib/components/MenuButton/menuButton"; +import { MenuDivider } from "@lib/components/MenuDivider"; +import { MenuHeading } from "@lib/components/MenuHeading"; +import { MenuItem } from "@lib/components/MenuItem"; +import { Dropdown } from "@mui/base"; +import { Add, ArrowDropDown } from "@mui/icons-material"; + +export type LayersAction = { + identifier: string; + icon?: React.ReactNode; + label: string; +}; + +export type LayersActionGroup = { + icon?: React.ReactNode; + label: string; + children: (LayersAction | LayersActionGroup)[]; +}; + +function isLayersActionGroup(action: LayersAction | LayersActionGroup): action is LayersActionGroup { + return (action as LayersActionGroup).children !== undefined; +} + +export type LayersActionsProps = { + layersActionGroups: LayersActionGroup[]; + onActionClick: (actionIdentifier: string) => void; +}; + +export function LayersActions(props: LayersActionsProps): React.ReactNode { + function makeContent( + layersActionGroups: (LayersActionGroup | LayersAction)[], + indentLevel: number = 0 + ): React.ReactNode[] { + const content: React.ReactNode[] = []; + for (const [index, item] of layersActionGroups.entries()) { + if (isLayersActionGroup(item)) { + if (index > 0) { + content.push(); + } + content.push( + + {item.icon} + {item.label} + + ); + content.push(makeContent(item.children, indentLevel + 1)); + } else { + content.push( + props.onActionClick(item.identifier)} + > + {item.icon} + {item.label} + + ); + } + } + return content; + } + + return ( + + + + Add + + + + {makeContent(props.layersActionGroups)} + + + ); +} diff --git a/frontend/src/modules/2DViewer/layers/delegates/GroupDelegate.ts b/frontend/src/modules/2DViewer/layers/delegates/GroupDelegate.ts new file mode 100644 index 000000000..3be0e82ef --- /dev/null +++ b/frontend/src/modules/2DViewer/layers/delegates/GroupDelegate.ts @@ -0,0 +1,254 @@ +import { ItemDelegateTopic } from "./ItemDelegate"; +import { PublishSubscribe, PublishSubscribeDelegate } from "./PublishSubscribeDelegate"; +import { UnsubscribeHandlerDelegate } from "./UnsubscribeHandlerDelegate"; + +import { LayerManagerTopic } from "../framework/LayerManager/LayerManager"; +import { SharedSetting } from "../framework/SharedSetting/SharedSetting"; +import { DeserializationFactory } from "../framework/utils/DeserializationFactory"; +import { Item, SerializedItem, instanceofGroup, instanceofLayer } from "../interfaces"; + +export enum GroupDelegateTopic { + CHILDREN = "CHILDREN", + TREE_REVISION_NUMBER = "TREE_REVISION_NUMBER", + CHILDREN_EXPANSION_STATES = "CHILDREN_EXPANSION_STATES", +} + +export type GroupDelegateTopicPayloads = { + [GroupDelegateTopic.CHILDREN]: Item[]; + [GroupDelegateTopic.TREE_REVISION_NUMBER]: number; + [GroupDelegateTopic.CHILDREN_EXPANSION_STATES]: { [id: string]: boolean }; +}; + +/* + * The GroupDelegate class is responsible for managing the children of a group item. + * It provides methods for adding, removing, and moving children, as well as for serializing and deserializing children. + * The class also provides methods for finding children and descendants based on a predicate. + */ +export class GroupDelegate implements PublishSubscribe { + private _owner: Item | null; + private _color: string | null = null; + private _children: Item[] = []; + private _publishSubscribeDelegate = new PublishSubscribeDelegate(); + private _unsubscribeHandlerDelegate = new UnsubscribeHandlerDelegate(); + private _treeRevisionNumber: number = 0; + private _deserializing = false; + + constructor(owner: Item | null) { + this._owner = owner; + } + + getColor(): string | null { + return this._color; + } + + setColor(color: string | null) { + this._color = color; + } + + prependChild(child: Item) { + this._children = [child, ...this._children]; + this.takeOwnershipOfChild(child); + } + + appendChild(child: Item) { + this._children = [...this._children, child]; + this.takeOwnershipOfChild(child); + } + + insertChild(child: Item, index: number) { + this._children = [...this._children.slice(0, index), child, ...this._children.slice(index)]; + this.takeOwnershipOfChild(child); + } + + removeChild(child: Item) { + this._children = this._children.filter((c) => c !== child); + this.disposeOwnershipOfChild(child); + this.incrementTreeRevisionNumber(); + } + + clearChildren() { + for (const child of this._children) { + this.disposeOwnershipOfChild(child); + } + this._children = []; + this.publishTopic(GroupDelegateTopic.CHILDREN); + this.incrementTreeRevisionNumber(); + } + + moveChild(child: Item, index: number) { + const currentIndex = this._children.indexOf(child); + if (currentIndex === -1) { + throw new Error("Child not found"); + } + + this._children = [...this._children.slice(0, currentIndex), ...this._children.slice(currentIndex + 1)]; + + this._children = [...this._children.slice(0, index), child, ...this._children.slice(index)]; + this.publishTopic(GroupDelegateTopic.CHILDREN); + this.incrementTreeRevisionNumber(); + } + + getChildren() { + return this._children; + } + + findChildren(predicate: (item: Item) => boolean): Item[] { + return this._children.filter(predicate); + } + + findDescendantById(id: string): Item | undefined { + for (const child of this._children) { + if (child.getItemDelegate().getId() === id) { + return child; + } + + if (instanceofGroup(child)) { + const descendant = child.getGroupDelegate().findDescendantById(id); + if (descendant) { + return descendant; + } + } + } + + return undefined; + } + + getAncestorAndSiblingItems(predicate: (item: Item) => boolean): Item[] { + const items: Item[] = []; + for (const child of this._children) { + if (predicate(child)) { + items.push(child); + } + } + const parentGroup = this._owner?.getItemDelegate().getParentGroup(); + if (parentGroup) { + items.push(...parentGroup.getAncestorAndSiblingItems(predicate)); + } + + return items; + } + + getDescendantItems(predicate: (item: Item) => boolean): Item[] { + const items: Item[] = []; + for (const child of this._children) { + if (predicate(child)) { + items.push(child); + } + + if (instanceofGroup(child)) { + items.push(...child.getGroupDelegate().getDescendantItems(predicate)); + } + } + + return items; + } + + makeSnapshotGetter(topic: T): () => GroupDelegateTopicPayloads[T] { + const snapshotGetter = (): any => { + if (topic === GroupDelegateTopic.CHILDREN) { + return this._children; + } + if (topic === GroupDelegateTopic.TREE_REVISION_NUMBER) { + return this._treeRevisionNumber; + } + if (topic === GroupDelegateTopic.CHILDREN_EXPANSION_STATES) { + const expansionState: { [id: string]: boolean } = {}; + for (const child of this._children) { + if (instanceofGroup(child)) { + expansionState[child.getItemDelegate().getId()] = child.getItemDelegate().isExpanded(); + } + } + return expansionState; + } + }; + + return snapshotGetter; + } + + getPublishSubscribeDelegate(): PublishSubscribeDelegate { + return this._publishSubscribeDelegate; + } + + serializeChildren(): SerializedItem[] { + return this._children.map((child) => child.serializeState()); + } + + deserializeChildren(children: SerializedItem[]) { + if (!this._owner) { + throw new Error("Owner not set"); + } + + this._deserializing = true; + const factory = new DeserializationFactory(this._owner.getItemDelegate().getLayerManager()); + for (const child of children) { + const item = factory.makeItem(child); + this.appendChild(item); + } + this._deserializing = false; + } + + private incrementTreeRevisionNumber() { + this._treeRevisionNumber++; + this.publishTopic(GroupDelegateTopic.TREE_REVISION_NUMBER); + } + + private takeOwnershipOfChild(child: Item) { + child.getItemDelegate().setParentGroup(this); + + this._unsubscribeHandlerDelegate.unsubscribe(child.getItemDelegate().getId()); + + if (instanceofLayer(child)) { + this._unsubscribeHandlerDelegate.registerUnsubscribeFunction( + child.getItemDelegate().getId(), + child + .getItemDelegate() + .getPublishSubscribeDelegate() + .makeSubscriberFunction(ItemDelegateTopic.EXPANDED)(() => { + this.publishTopic(GroupDelegateTopic.CHILDREN_EXPANSION_STATES); + }) + ); + } + + if (instanceofGroup(child)) { + this._unsubscribeHandlerDelegate.registerUnsubscribeFunction( + child.getItemDelegate().getId(), + child + .getGroupDelegate() + .getPublishSubscribeDelegate() + .makeSubscriberFunction(GroupDelegateTopic.TREE_REVISION_NUMBER)(() => { + this.incrementTreeRevisionNumber(); + }) + ); + this._unsubscribeHandlerDelegate.registerUnsubscribeFunction( + child.getItemDelegate().getId(), + child + .getGroupDelegate() + .getPublishSubscribeDelegate() + .makeSubscriberFunction(GroupDelegateTopic.CHILDREN_EXPANSION_STATES)(() => { + this.publishTopic(GroupDelegateTopic.CHILDREN_EXPANSION_STATES); + }) + ); + } + + this.publishTopic(GroupDelegateTopic.CHILDREN); + this.incrementTreeRevisionNumber(); + } + + private publishTopic(topic: GroupDelegateTopic) { + if (this._deserializing) { + return; + } + this._publishSubscribeDelegate.notifySubscribers(topic); + } + + private disposeOwnershipOfChild(child: Item) { + this._unsubscribeHandlerDelegate.unsubscribe(child.getItemDelegate().getId()); + child.getItemDelegate().setParentGroup(null); + + if (child instanceof SharedSetting) { + this._owner?.getItemDelegate().getLayerManager().publishTopic(LayerManagerTopic.SETTINGS_CHANGED); + } + + this.publishTopic(GroupDelegateTopic.CHILDREN); + } +} diff --git a/frontend/src/modules/2DViewer/layers/delegates/ItemDelegate.ts b/frontend/src/modules/2DViewer/layers/delegates/ItemDelegate.ts new file mode 100644 index 000000000..607a70195 --- /dev/null +++ b/frontend/src/modules/2DViewer/layers/delegates/ItemDelegate.ts @@ -0,0 +1,157 @@ +import { isEqual } from "lodash"; +import { v4 } from "uuid"; + +import { GroupDelegate } from "./GroupDelegate"; +import { PublishSubscribe, PublishSubscribeDelegate } from "./PublishSubscribeDelegate"; + +import { LayerManager, LayerManagerTopic } from "../framework/LayerManager/LayerManager"; +import { SerializedItem } from "../interfaces"; + +export enum ItemDelegateTopic { + NAME = "NAME", + VISIBILITY = "VISIBILITY", + EXPANDED = "EXPANDED", +} + +export type ItemDelegatePayloads = { + [ItemDelegateTopic.NAME]: string; + [ItemDelegateTopic.VISIBILITY]: boolean; + [ItemDelegateTopic.EXPANDED]: boolean; +}; + +/* + * The ItemDelegate class is responsible for managing the basic properties of an item. + * It provides methods for setting and getting the id, parent group, name, visibility, and expansion state of the item. + */ +export class ItemDelegate implements PublishSubscribe { + private _id: string; + private _name: string; + private _visible: boolean = true; + private _expanded: boolean = true; + private _parentGroup: GroupDelegate | null = null; + private _layerManager: LayerManager; + private _publishSubscribeDelegate = new PublishSubscribeDelegate(); + + constructor(name: string, layerManager: LayerManager) { + this._id = v4(); + this._layerManager = layerManager; + this._name = this.makeUniqueName(name); + } + + setId(id: string): void { + this._id = id; + } + + getId(): string { + return this._id; + } + + getName(): string { + return this._name; + } + + setName(name: string): void { + if (isEqual(this._name, name)) { + return; + } + + this._name = name; + this._publishSubscribeDelegate.notifySubscribers(ItemDelegateTopic.NAME); + if (this._layerManager) { + this._layerManager.publishTopic(LayerManagerTopic.LAYER_DATA_REVISION); + } + } + + getParentGroup(): GroupDelegate | null { + return this._parentGroup; + } + + setParentGroup(parentGroup: GroupDelegate | null): void { + this._parentGroup = parentGroup; + } + + getLayerManager(): LayerManager { + return this._layerManager; + } + + isVisible(): boolean { + return this._visible; + } + + setVisible(visible: boolean): void { + if (isEqual(this._visible, visible)) { + return; + } + + this._visible = visible; + this._publishSubscribeDelegate.notifySubscribers(ItemDelegateTopic.VISIBILITY); + if (this._layerManager) { + this._layerManager.publishTopic(LayerManagerTopic.LAYER_DATA_REVISION); + } + } + + isExpanded(): boolean { + return this._expanded; + } + + setExpanded(expanded: boolean): void { + if (isEqual(this._expanded, expanded)) { + return; + } + + this._expanded = expanded; + this._publishSubscribeDelegate.notifySubscribers(ItemDelegateTopic.EXPANDED); + } + + makeSnapshotGetter(topic: T): () => ItemDelegatePayloads[T] { + const snapshotGetter = (): any => { + if (topic === ItemDelegateTopic.NAME) { + return this._name; + } + if (topic === ItemDelegateTopic.VISIBILITY) { + return this._visible; + } + if (topic === ItemDelegateTopic.EXPANDED) { + return this._expanded; + } + }; + return snapshotGetter; + } + + getPublishSubscribeDelegate(): PublishSubscribeDelegate { + return this._publishSubscribeDelegate; + } + + serializeState(): Omit { + return { + id: this._id, + name: this._name, + visible: this._visible, + expanded: this._expanded, + }; + } + + deserializeState(state: Omit): void { + this._id = state.id; + this._name = state.name; + this._visible = state.visible; + this._expanded = state.expanded; + } + + private makeUniqueName(candidate: string): string { + const groupDelegate = this._layerManager?.getGroupDelegate(); + if (!groupDelegate) { + return candidate; + } + const existingNames = groupDelegate + .getDescendantItems(() => true) + .map((item) => item.getItemDelegate().getName()); + let uniqueName = candidate; + let counter = 1; + while (existingNames.includes(uniqueName)) { + uniqueName = `${candidate} (${counter})`; + counter++; + } + return uniqueName; + } +} diff --git a/frontend/src/modules/2DViewer/layers/delegates/LayerDelegate.ts b/frontend/src/modules/2DViewer/layers/delegates/LayerDelegate.ts new file mode 100644 index 000000000..2ba7c353a --- /dev/null +++ b/frontend/src/modules/2DViewer/layers/delegates/LayerDelegate.ts @@ -0,0 +1,336 @@ +import { StatusMessage } from "@framework/ModuleInstanceStatusController"; +import { ApiErrorHelper } from "@framework/utils/ApiErrorHelper"; +import { isDevMode } from "@lib/utils/devMode"; +import { QueryClient, isCancelledError } from "@tanstack/react-query"; + +import { PublishSubscribe, PublishSubscribeDelegate } from "./PublishSubscribeDelegate"; +import { SettingsContextDelegateTopic } from "./SettingsContextDelegate"; +import { UnsubscribeHandlerDelegate } from "./UnsubscribeHandlerDelegate"; + +import { LayerManager, LayerManagerTopic } from "../framework/LayerManager/LayerManager"; +import { SharedSetting } from "../framework/SharedSetting/SharedSetting"; +import { BoundingBox, Layer, SerializedLayer, SerializedType, Settings, SettingsContext } from "../interfaces"; + +export enum LayerDelegateTopic { + STATUS = "STATUS", + DATA = "DATA", + SUBORDINATED = "SUBORDINATED", +} + +export enum LayerColoringType { + NONE = "NONE", + COLORSCALE = "COLORSCALE", + COLORSET = "COLORSET", +} + +export enum LayerStatus { + IDLE = "IDLE", + LOADING = "LOADING", + ERROR = "ERROR", + SUCCESS = "SUCCESS", +} + +export type LayerDelegatePayloads = { + [LayerDelegateTopic.STATUS]: LayerStatus; + [LayerDelegateTopic.DATA]: TData; + [LayerDelegateTopic.SUBORDINATED]: boolean; +}; + +/* + * The LayerDelegate class is responsible for managing the state of a layer. + * It is responsible for (re-)fetching the data whenever changes to settings make it necessary. + * It also manages the status of the layer (loading, success, error). + */ +export class LayerDelegate + implements PublishSubscribe> +{ + private _owner: Layer; + private _settingsContext: SettingsContext; + private _layerManager: LayerManager; + private _unsubscribeHandler: UnsubscribeHandlerDelegate = new UnsubscribeHandlerDelegate(); + private _cancellationPending: boolean = false; + private _publishSubscribeDelegate = new PublishSubscribeDelegate(); + private _queryKeys: unknown[][] = []; + private _status: LayerStatus = LayerStatus.IDLE; + private _data: TData | null = null; + private _error: StatusMessage | string | null = null; + private _boundingBox: BoundingBox | null = null; + private _valueRange: [number, number] | null = null; + private _coloringType: LayerColoringType; + private _isSubordinated: boolean = false; + + constructor( + owner: Layer, + layerManager: LayerManager, + settingsContext: SettingsContext, + coloringType: LayerColoringType + ) { + this._owner = owner; + this._layerManager = layerManager; + this._settingsContext = settingsContext; + this._coloringType = coloringType; + + this._unsubscribeHandler.registerUnsubscribeFunction( + "settings-context", + this._settingsContext + .getDelegate() + .getPublishSubscribeDelegate() + .makeSubscriberFunction(SettingsContextDelegateTopic.SETTINGS_CHANGED)(() => { + this.handleSettingsChange(); + }) + ); + + this._unsubscribeHandler.registerUnsubscribeFunction( + "layer-manager", + layerManager + .getPublishSubscribeDelegate() + .makeSubscriberFunction(LayerManagerTopic.SHARED_SETTINGS_CHANGED)(() => { + this.handleSharedSettingsChanged(); + }) + ); + + this._unsubscribeHandler.registerUnsubscribeFunction( + "layer-manager", + layerManager.getPublishSubscribeDelegate().makeSubscriberFunction(LayerManagerTopic.ITEMS_CHANGED)(() => { + this.handleSharedSettingsChanged(); + }) + ); + } + + handleSettingsChange(): void { + this._cancellationPending = true; + this.maybeCancelQuery().then(() => { + this.maybeRefetchData(); + }); + } + + registerQueryKey(queryKey: unknown[]): void { + this._queryKeys.push(queryKey); + } + + getStatus(): LayerStatus { + return this._status; + } + + getData(): TData | null { + return this._data; + } + + getSettingsContext(): SettingsContext { + return this._settingsContext; + } + + getBoundingBox(): BoundingBox | null { + return this._boundingBox; + } + + getColoringType(): LayerColoringType { + return this._coloringType; + } + + isSubordinated(): boolean { + return this._isSubordinated; + } + + setIsSubordinated(isSubordinated: boolean): void { + if (this._isSubordinated === isSubordinated) { + return; + } + this._isSubordinated = isSubordinated; + this._publishSubscribeDelegate.notifySubscribers(LayerDelegateTopic.SUBORDINATED); + } + + getValueRange(): [number, number] | null { + return this._valueRange; + } + + handleSharedSettingsChanged(): void { + const parentGroup = this._owner.getItemDelegate().getParentGroup(); + if (parentGroup) { + const sharedSettings: SharedSetting[] = parentGroup.getAncestorAndSiblingItems( + (item) => item instanceof SharedSetting + ) as SharedSetting[]; + const overriddenSettings: { [K in keyof TSettings]: TSettings[K] } = {} as { + [K in keyof TSettings]: TSettings[K]; + }; + for (const sharedSetting of sharedSettings) { + const type = sharedSetting.getWrappedSetting().getType(); + const setting = this._settingsContext.getDelegate().getSettings()[type]; + if (setting && overriddenSettings[type] === undefined) { + if ( + sharedSetting.getWrappedSetting().getDelegate().isInitialized() && + sharedSetting.getWrappedSetting().getDelegate().isValueValid() + ) { + overriddenSettings[type] = sharedSetting.getWrappedSetting().getDelegate().getValue(); + } else { + overriddenSettings[type] = null; + } + } + } + this._settingsContext.getDelegate().setOverriddenSettings(overriddenSettings); + } + } + + getLayerManager(): LayerManager { + return this._layerManager; + } + + makeSnapshotGetter(topic: T): () => LayerDelegatePayloads[T] { + const snapshotGetter = (): any => { + if (topic === LayerDelegateTopic.STATUS) { + return this._status; + } + if (topic === LayerDelegateTopic.DATA) { + return this._data; + } + if (topic === LayerDelegateTopic.SUBORDINATED) { + return this._isSubordinated; + } + }; + + return snapshotGetter; + } + + getPublishSubscribeDelegate(): PublishSubscribeDelegate { + return this._publishSubscribeDelegate; + } + + getError(): StatusMessage | string | null { + if (!this._error) { + return null; + } + + const name = this._owner.getItemDelegate().getName(); + + if (typeof this._error === "string") { + return `${name}: ${this._error}`; + } + + return { + ...this._error, + message: `${name}: ${this._error.message}`, + }; + } + + async maybeRefetchData(): Promise { + const queryClient = this.getQueryClient(); + + if (!queryClient) { + return; + } + + if (this._cancellationPending) { + return; + } + + if (this._isSubordinated) { + return; + } + + this.setStatus(LayerStatus.LOADING); + this.invalidateBoundingBox(); + this.invalidateValueRange(); + + try { + this._data = await this._owner.fetchData(queryClient); + if (this._owner.makeBoundingBox) { + this._boundingBox = this._owner.makeBoundingBox(); + } + if (this._owner.makeValueRange) { + this._valueRange = this._owner.makeValueRange(); + } + if (this._queryKeys.length === null && isDevMode()) { + console.warn( + "Did you forget to use 'setQueryKeys' in your layer implementation of 'fetchData'? This will cause the queries to not be cancelled when settings change and might lead to undesired behaviour." + ); + } + this._queryKeys = []; + this._publishSubscribeDelegate.notifySubscribers(LayerDelegateTopic.DATA); + this._layerManager.publishTopic(LayerManagerTopic.LAYER_DATA_REVISION); + this.setStatus(LayerStatus.SUCCESS); + } catch (error: any) { + if (isCancelledError(error)) { + return; + } + const apiError = ApiErrorHelper.fromError(error); + if (apiError) { + this._error = apiError.makeStatusMessage(); + } else { + this._error = "An error occurred"; + } + this.setStatus(LayerStatus.ERROR); + } + } + + serializeState(): SerializedLayer { + const itemState = this._owner.getItemDelegate().serializeState(); + return { + ...itemState, + type: SerializedType.LAYER, + layerClass: this._owner.constructor.name, + settings: this._settingsContext.getDelegate().serializeSettings(), + }; + } + + deserializeState(serializedLayer: SerializedLayer): void { + this._owner.getItemDelegate().deserializeState(serializedLayer); + this._settingsContext.getDelegate().deserializeSettings(serializedLayer.settings); + } + + beforeDestroy(): void { + this._settingsContext.getDelegate().beforeDestroy(); + this._unsubscribeHandler.unsubscribeAll(); + } + + private setStatus(status: LayerStatus): void { + if (this._status === status) { + return; + } + + this._status = status; + this._layerManager.publishTopic(LayerManagerTopic.LAYER_DATA_REVISION); + this._publishSubscribeDelegate.notifySubscribers(LayerDelegateTopic.STATUS); + } + + private getQueryClient(): QueryClient | null { + return this._layerManager?.getQueryClient() ?? null; + } + + private invalidateBoundingBox(): void { + this._boundingBox = null; + } + + private invalidateValueRange(): void { + this._valueRange = null; + } + + private async maybeCancelQuery(): Promise { + const queryClient = this.getQueryClient(); + + if (!queryClient) { + return; + } + + if (this._queryKeys.length > 0) { + for (const queryKey of this._queryKeys) { + await queryClient.cancelQueries( + { + queryKey, + exact: true, + fetchStatus: "fetching", + type: "active", + }, + { + silent: true, + revert: true, + } + ); + await queryClient.invalidateQueries({ queryKey }); + queryClient.removeQueries({ queryKey }); + } + this._queryKeys = []; + } + + this._cancellationPending = false; + } +} diff --git a/frontend/src/modules/2DViewer/layers/delegates/PublishSubscribeDelegate.ts b/frontend/src/modules/2DViewer/layers/delegates/PublishSubscribeDelegate.ts new file mode 100644 index 000000000..567fd3bfc --- /dev/null +++ b/frontend/src/modules/2DViewer/layers/delegates/PublishSubscribeDelegate.ts @@ -0,0 +1,46 @@ +import React from "react"; + +export type TopicPayloads = Record; + +export interface PublishSubscribe> { + makeSnapshotGetter(topic: T): () => TTopicPayloads[T]; + getPublishSubscribeDelegate(): PublishSubscribeDelegate; +} + +export class PublishSubscribeDelegate { + private _subscribers = new Map void>>(); + + notifySubscribers(topic: TTopic): void { + const subscribers = this._subscribers.get(topic); + if (subscribers) { + subscribers.forEach((subscriber) => subscriber()); + } + } + + makeSubscriberFunction(topic: TTopic): (onStoreChangeCallback: () => void) => () => void { + // Using arrow function in order to keep "this" in context + const subscriber = (onStoreChangeCallback: () => void): (() => void) => { + const subscribers = this._subscribers.get(topic) || new Set(); + subscribers.add(onStoreChangeCallback); + this._subscribers.set(topic, subscribers); + + return () => { + subscribers.delete(onStoreChangeCallback); + }; + }; + + return subscriber; + } +} + +export function usePublishSubscribeTopicValue>( + publishSubscribe: PublishSubscribe, + topic: TTopic +): TTopicPayloads[TTopic] { + const value = React.useSyncExternalStore( + publishSubscribe.getPublishSubscribeDelegate().makeSubscriberFunction(topic), + publishSubscribe.makeSnapshotGetter(topic) + ); + + return value; +} diff --git a/frontend/src/modules/2DViewer/layers/delegates/SettingDelegate.ts b/frontend/src/modules/2DViewer/layers/delegates/SettingDelegate.ts new file mode 100644 index 000000000..a6048dffc --- /dev/null +++ b/frontend/src/modules/2DViewer/layers/delegates/SettingDelegate.ts @@ -0,0 +1,347 @@ +import { WorkbenchSession } from "@framework/WorkbenchSession"; +import { WorkbenchSettings } from "@framework/WorkbenchSettings"; + +import { isArray, isEqual } from "lodash"; +import { v4 } from "uuid"; + +import { PublishSubscribe, PublishSubscribeDelegate } from "./PublishSubscribeDelegate"; + +import { AvailableValuesType, Setting } from "../interfaces"; + +export enum SettingTopic { + VALUE_CHANGED = "VALUE_CHANGED", + VALIDITY_CHANGED = "VALIDITY_CHANGED", + AVAILABLE_VALUES_CHANGED = "AVAILABLE_VALUES_CHANGED", + OVERRIDDEN_CHANGED = "OVERRIDDEN_CHANGED", + LOADING_STATE_CHANGED = "LOADING_STATE_CHANGED", + INIT_STATE_CHANGED = "INIT_STATE_CHANGED", + PERSISTED_STATE_CHANGED = "PERSISTED_STATE_CHANGED", +} + +export type SettingTopicPayloads = { + [SettingTopic.VALUE_CHANGED]: TValue; + [SettingTopic.VALIDITY_CHANGED]: boolean; + [SettingTopic.AVAILABLE_VALUES_CHANGED]: Exclude[]; + [SettingTopic.OVERRIDDEN_CHANGED]: TValue | undefined; + [SettingTopic.LOADING_STATE_CHANGED]: boolean; + [SettingTopic.INIT_STATE_CHANGED]: boolean; + [SettingTopic.PERSISTED_STATE_CHANGED]: boolean; +}; + +/* + * The SettingDelegate class is responsible for managing a setting. + * + * It provides a method for setting available values, which are used to validate the setting value or applying a fixup if the value is invalid. + * It provides methods for setting and getting the value and its states, checking if the value is valid, and setting the value as overridden or persisted. + */ +export class SettingDelegate implements PublishSubscribe> { + private _id: string; + private _owner: Setting; + private _value: TValue; + private _isValueValid: boolean = false; + private _publishSubscribeDelegate = new PublishSubscribeDelegate(); + private _availableValues: AvailableValuesType = [] as unknown as AvailableValuesType; + private _overriddenValue: TValue | undefined = undefined; + private _loading: boolean = false; + private _initialized: boolean = false; + private _currentValueFromPersistence: TValue | null = null; + private _isStatic: boolean; + + constructor(value: TValue, owner: Setting, isStatic: boolean = false) { + this._id = v4(); + this._owner = owner; + this._value = value; + if (typeof value === "boolean") { + this._isValueValid = true; + } + this._isStatic = isStatic; + if (isStatic) { + this.setInitialized(); + } + } + + getId(): string { + return this._id; + } + + getValue(): TValue { + if (this._overriddenValue !== undefined) { + return this._overriddenValue; + } + + if (this._currentValueFromPersistence !== null) { + return this._currentValueFromPersistence; + } + + return this._value; + } + + isStatic(): boolean { + return this._isStatic; + } + + serializeValue(): string { + if (this._owner.serializeValue) { + return this._owner.serializeValue(this.getValue()); + } + + return JSON.stringify(this.getValue()); + } + + deserializeValue(serializedValue: string): void { + if (this._owner.deserializeValue) { + this._currentValueFromPersistence = this._owner.deserializeValue(serializedValue); + return; + } + + this._currentValueFromPersistence = JSON.parse(serializedValue); + } + + isValueValid(): boolean { + return this._isValueValid; + } + + isPersistedValue(): boolean { + return this._currentValueFromPersistence !== null; + } + + /* + * This method is used to set the value of the setting. + * It should only be called when a user is changing a setting. + */ + setValue(value: TValue): void { + if (isEqual(this._value, value)) { + return; + } + this._currentValueFromPersistence = null; + this._value = value; + + this.setValueValid(this.checkIfValueIsValid(this._value)); + + this._publishSubscribeDelegate.notifySubscribers(SettingTopic.VALUE_CHANGED); + } + + setValueValid(isValueValid: boolean): void { + if (this._isValueValid === isValueValid) { + return; + } + this._isValueValid = isValueValid; + this._publishSubscribeDelegate.notifySubscribers(SettingTopic.VALIDITY_CHANGED); + } + + setLoading(loading: boolean): void { + if (this._loading === loading) { + return; + } + this._loading = loading; + this._publishSubscribeDelegate.notifySubscribers(SettingTopic.LOADING_STATE_CHANGED); + } + + setInitialized(): void { + if (this._initialized) { + return; + } + this._initialized = true; + this._publishSubscribeDelegate.notifySubscribers(SettingTopic.INIT_STATE_CHANGED); + } + + isInitialized(): boolean { + return this._initialized; + } + + isLoading(): boolean { + return this._loading; + } + + valueToString(value: TValue, workbenchSession: WorkbenchSession, workbenchSettings: WorkbenchSettings): string { + if (this._owner.valueToString) { + return this._owner.valueToString({ value, workbenchSession, workbenchSettings }); + } + + if (typeof value === "boolean") { + return value ? "true" : "false"; + } + + if (typeof value === "string") { + return value; + } + + if (typeof value === "number") { + return value.toString(); + } + + return "Value has no string representation"; + } + + setOverriddenValue(overriddenValue: TValue | undefined): void { + if (isEqual(this._overriddenValue, overriddenValue)) { + return; + } + + const prevValue = this._overriddenValue; + this._overriddenValue = overriddenValue; + this._publishSubscribeDelegate.notifySubscribers(SettingTopic.OVERRIDDEN_CHANGED); + + if (overriddenValue === undefined) { + // Keep overridden value, if invalid fix it + if (prevValue !== undefined) { + this._value = prevValue; + } + this.maybeFixupValue(); + } + + this.setValueValid(this.checkIfValueIsValid(this.getValue())); + + if (prevValue === undefined && overriddenValue !== undefined && isEqual(this._value, overriddenValue)) { + return; + } + + if (prevValue !== undefined && overriddenValue === undefined && isEqual(this._value, prevValue)) { + return; + } + + this._publishSubscribeDelegate.notifySubscribers(SettingTopic.VALUE_CHANGED); + } + + makeSnapshotGetter(topic: T): () => SettingTopicPayloads[T] { + const snapshotGetter = (): any => { + if (topic === SettingTopic.VALUE_CHANGED) { + return this._value; + } + if (topic === SettingTopic.VALIDITY_CHANGED) { + return this._isValueValid; + } + if (topic === SettingTopic.AVAILABLE_VALUES_CHANGED) { + return this._availableValues; + } + if (topic === SettingTopic.OVERRIDDEN_CHANGED) { + return this._overriddenValue; + } + if (topic === SettingTopic.LOADING_STATE_CHANGED) { + return this._loading; + } + if (topic === SettingTopic.PERSISTED_STATE_CHANGED) { + return this.isPersistedValue(); + } + if (topic === SettingTopic.INIT_STATE_CHANGED) { + return this._initialized; + } + }; + + return snapshotGetter; + } + + getPublishSubscribeDelegate(): PublishSubscribeDelegate { + return this._publishSubscribeDelegate; + } + + getAvailableValues(): AvailableValuesType { + return this._availableValues; + } + + maybeResetPersistedValue(): boolean { + if (this._currentValueFromPersistence === null) { + return false; + } + + if (this._owner.isValueValid) { + if (this._owner.isValueValid(this._availableValues, this._currentValueFromPersistence)) { + this._value = this._currentValueFromPersistence; + this._currentValueFromPersistence = null; + return true; + } + return false; + } + + if (Array.isArray(this._currentValueFromPersistence)) { + const currentValueFromPersistence = this._currentValueFromPersistence as TValue[]; + if (currentValueFromPersistence.every((value) => this._availableValues.some((el) => isEqual(el, value)))) { + this._value = this._currentValueFromPersistence; + this._currentValueFromPersistence = null; + return true; + } + return false; + } + + if (this._availableValues.some((el) => isEqual(this._currentValueFromPersistence as TValue, el))) { + this._value = this._currentValueFromPersistence; + this._currentValueFromPersistence = null; + return true; + } + + return false; + } + + setAvailableValues(availableValues: AvailableValuesType): void { + if (isEqual(this._availableValues, availableValues) && this._initialized) { + return; + } + + this._availableValues = availableValues; + let valueChanged = false; + if ((!this.checkIfValueIsValid(this.getValue()) && this.maybeFixupValue()) || this.maybeResetPersistedValue()) { + valueChanged = true; + } + this.setValueValid(this.checkIfValueIsValid(this.getValue())); + this.setInitialized(); + const prevIsValid = this._isValueValid; + if (valueChanged || this._isValueValid !== prevIsValid) { + this._publishSubscribeDelegate.notifySubscribers(SettingTopic.VALUE_CHANGED); + } + this._publishSubscribeDelegate.notifySubscribers(SettingTopic.AVAILABLE_VALUES_CHANGED); + } + + private maybeFixupValue(): boolean { + if (this.checkIfValueIsValid(this._value)) { + return false; + } + + if (this.isPersistedValue()) { + return false; + } + + if (this._availableValues.length === 0) { + return false; + } + + if (this._availableValues.some((el) => isEqual(el, this._value))) { + return false; + } + + let candidate = this._value; + + if (this._owner.fixupValue) { + candidate = this._owner.fixupValue(this._availableValues, this._value); + } else if (Array.isArray(this._value)) { + candidate = [this._availableValues[0]] as TValue; + } else { + candidate = this._availableValues[0] as TValue; + } + + if (isEqual(candidate, this._value)) { + return false; + } + + this._value = candidate; + return true; + } + + private checkIfValueIsValid(value: TValue): boolean { + if (this._owner.isValueValid) { + return this._owner.isValueValid(this._availableValues, value); + } + if (typeof value === "boolean") { + return true; + } + if (this._availableValues.length === 0) { + return false; + } + if (this._availableValues.some((el) => isEqual(el, value))) { + return true; + } + if (isArray(value) && value.every((value) => this._availableValues.some((el) => isEqual(value, el)))) { + return true; + } + return false; + } +} diff --git a/frontend/src/modules/2DViewer/layers/delegates/SettingsContextDelegate.ts b/frontend/src/modules/2DViewer/layers/delegates/SettingsContextDelegate.ts new file mode 100644 index 000000000..402bd7139 --- /dev/null +++ b/frontend/src/modules/2DViewer/layers/delegates/SettingsContextDelegate.ts @@ -0,0 +1,388 @@ +import { PublishSubscribe, PublishSubscribeDelegate } from "./PublishSubscribeDelegate"; +import { SettingTopic } from "./SettingDelegate"; +import { UnsubscribeHandlerDelegate } from "./UnsubscribeHandlerDelegate"; +import { Dependency } from "./_utils/Dependency"; + +import { GlobalSettings, LayerManager, LayerManagerTopic } from "../framework/LayerManager/LayerManager"; +import { + AvailableValuesType, + EachAvailableValuesType, + SerializedSettingsState, + Setting, + Settings, + SettingsContext, + UpdateFunc, +} from "../interfaces"; + +export enum SettingsContextLoadingState { + LOADING = "LOADING", + LOADED = "LOADED", + FAILED = "FAILED", +} + +export enum SettingsContextDelegateTopic { + SETTINGS_CHANGED = "SETTINGS_CHANGED", + LAYER_MANAGER_CHANGED = "LAYER_MANAGER_CHANGED", + LOADING_STATE_CHANGED = "LOADING_STATE_CHANGED", +} + +export type SettingsContextDelegatePayloads = { + [SettingsContextDelegateTopic.SETTINGS_CHANGED]: void; + [SettingsContextDelegateTopic.LAYER_MANAGER_CHANGED]: void; + [SettingsContextDelegateTopic.LOADING_STATE_CHANGED]: SettingsContextLoadingState; +}; + +export interface FetchDataFunction { + (oldValues: { [K in TKey]: TSettings[K] }, newValues: { [K in TKey]: TSettings[K] }): void; +} + +export type SettingsContextDelegateState = { + values: { [K in TKey]: TSettings[K] }; +}; + +/* + * The SettingsContextDelegate class is responsible for giving the settings of a layer a common context as + * many settings are interdependent. + * + * It creates a dependency graph for all settings and implements dependencies between both themselves and global settings. + * It also takes care of overriding settings that are set by shared settings. + * It also takes care of notifying its subscribers (e.g. the respective layer delegate) when the settings change. + * + */ +export class SettingsContextDelegate + implements PublishSubscribe +{ + private _parentContext: SettingsContext; + private _layerManager: LayerManager; + private _settings: { [K in TKey]: Setting } = {} as { [K in TKey]: Setting }; + private _overriddenSettings: { [K in TKey]: TSettings[K] } = {} as { [K in TKey]: TSettings[K] }; + private _publishSubscribeDelegate = new PublishSubscribeDelegate(); + private _unsubscribeHandler: UnsubscribeHandlerDelegate = new UnsubscribeHandlerDelegate(); + private _loadingState: SettingsContextLoadingState = SettingsContextLoadingState.LOADING; + + constructor( + context: SettingsContext, + layerManager: LayerManager, + settings: { [K in TKey]: Setting } + ) { + this._parentContext = context; + this._layerManager = layerManager; + + for (const key in settings) { + this._unsubscribeHandler.registerUnsubscribeFunction( + "settings", + settings[key] + .getDelegate() + .getPublishSubscribeDelegate() + .makeSubscriberFunction(SettingTopic.VALUE_CHANGED)(() => { + this.handleSettingChanged(); + }) + ); + this._unsubscribeHandler.registerUnsubscribeFunction( + "settings", + settings[key] + .getDelegate() + .getPublishSubscribeDelegate() + .makeSubscriberFunction(SettingTopic.LOADING_STATE_CHANGED)(() => { + this.handleSettingsLoadingStateChanged(); + }) + ); + } + + this._settings = settings; + + this.createDependencies(); + } + + getLayerManager(): LayerManager { + return this._layerManager; + } + + getValues(): { [K in TKey]?: TSettings[K] } { + const settings: { [K in TKey]?: TSettings[K] } = {} as { [K in TKey]?: TSettings[K] }; + for (const key in this._settings) { + if (this._settings[key].getDelegate().isPersistedValue()) { + settings[key] = undefined; + continue; + } + settings[key] = this._settings[key].getDelegate().getValue(); + } + + return settings; + } + + setOverriddenSettings(overriddenSettings: { [K in TKey]: TSettings[K] }): void { + this._overriddenSettings = overriddenSettings; + for (const key in this._settings) { + if (Object.keys(this._overriddenSettings).includes(key)) { + this._settings[key].getDelegate().setOverriddenValue(this._overriddenSettings[key]); + } else { + this._settings[key].getDelegate().setOverriddenValue(undefined); + } + } + } + + areCurrentSettingsValid(): boolean { + for (const key in this._settings) { + if (!this._settings[key].getDelegate().isValueValid()) { + return false; + } + } + + if (!this._parentContext.areCurrentSettingsValid) { + return true; + } + + const settings: TSettings = {} as TSettings; + for (const key in this._settings) { + settings[key] = this._settings[key].getDelegate().getValue(); + } + + return this._parentContext.areCurrentSettingsValid(settings); + } + + areAllSettingsLoaded(): boolean { + for (const key in this._settings) { + if (this._settings[key].getDelegate().isLoading()) { + return false; + } + } + + return true; + } + + areAllSettingsInitialized(): boolean { + for (const key in this._settings) { + if ( + !this._settings[key].getDelegate().isInitialized() || + this._settings[key].getDelegate().isPersistedValue() + ) { + return false; + } + } + + return true; + } + + isSomePersistedSettingNotValid(): boolean { + for (const key in this._settings) { + if ( + !this._settings[key].getDelegate().isLoading() && + this._settings[key].getDelegate().isPersistedValue() && + !this._settings[key].getDelegate().isValueValid() && + this._settings[key].getDelegate().isInitialized() + ) { + return true; + } + } + + return false; + } + + getInvalidSettings(): string[] { + const invalidSettings: string[] = []; + for (const key in this._settings) { + if (!this._settings[key].getDelegate().isValueValid()) { + invalidSettings.push(this._settings[key].getLabel()); + } + } + + return invalidSettings; + } + + setAvailableValues(key: K, availableValues: AvailableValuesType): void { + const settingDelegate = this._settings[key].getDelegate(); + settingDelegate.setAvailableValues(availableValues); + + this.getLayerManager().publishTopic(LayerManagerTopic.AVAILABLE_SETTINGS_CHANGED); + } + + getSettings() { + return this._settings; + } + + makeSnapshotGetter(topic: T): () => SettingsContextDelegatePayloads[T] { + const snapshotGetter = (): any => { + if (topic === SettingsContextDelegateTopic.SETTINGS_CHANGED) { + return; + } + if (topic === SettingsContextDelegateTopic.LAYER_MANAGER_CHANGED) { + return; + } + if (topic === SettingsContextDelegateTopic.LOADING_STATE_CHANGED) { + return this._loadingState; + } + }; + + return snapshotGetter; + } + + getPublishSubscribeDelegate(): PublishSubscribeDelegate { + return this._publishSubscribeDelegate; + } + + serializeSettings(): SerializedSettingsState { + const serializedSettings: SerializedSettingsState = {} as SerializedSettingsState; + for (const key in this._settings) { + serializedSettings[key] = this._settings[key].getDelegate().serializeValue(); + } + return serializedSettings; + } + + deserializeSettings(serializedSettings: SerializedSettingsState): void { + for (const [key, value] of Object.entries(serializedSettings)) { + const settingDelegate = this._settings[key as TKey].getDelegate(); + settingDelegate.deserializeValue(value); + if (settingDelegate.isStatic()) { + settingDelegate.maybeResetPersistedValue(); + } + } + } + + createDependencies(): void { + this._unsubscribeHandler.unsubscribe("dependencies"); + + const makeSettingGetter = (key: K, handler: (value: TSettings[K]) => void) => { + const handleChange = (): void => { + handler(this._settings[key].getDelegate().getValue()); + }; + this._unsubscribeHandler.registerUnsubscribeFunction( + "dependencies", + this._settings[key] + .getDelegate() + .getPublishSubscribeDelegate() + .makeSubscriberFunction(SettingTopic.VALUE_CHANGED)(handleChange) + ); + + this._unsubscribeHandler.registerUnsubscribeFunction( + "dependencies", + this._settings[key] + .getDelegate() + .getPublishSubscribeDelegate() + .makeSubscriberFunction(SettingTopic.PERSISTED_STATE_CHANGED)(handleChange) + ); + + return handleChange; + }; + + const makeGlobalSettingGetter = ( + key: K, + handler: (value: GlobalSettings[K]) => void + ) => { + const handleChange = (): void => { + handler(this.getLayerManager.bind(this)().getGlobalSetting(key)); + }; + this._unsubscribeHandler.registerUnsubscribeFunction( + "dependencies", + this.getLayerManager() + .getPublishSubscribeDelegate() + .makeSubscriberFunction(LayerManagerTopic.GLOBAL_SETTINGS_CHANGED)(handleChange) + ); + + return handleChange; + }; + + const availableSettingsUpdater = ( + settingKey: K, + updateFunc: UpdateFunc, TSettings, K> + ): Dependency, TSettings, K> => { + const dependency = new Dependency, TSettings, K>( + this as unknown as SettingsContextDelegate, + updateFunc, + makeSettingGetter, + makeGlobalSettingGetter + ); + + dependency.subscribe((availableValues: AvailableValuesType | null) => { + if (availableValues === null) { + this.setAvailableValues(settingKey, [] as unknown as AvailableValuesType); + return; + } + this.setAvailableValues(settingKey, availableValues); + }); + + dependency.subscribeLoading((loading: boolean, hasDependencies: boolean) => { + this._settings[settingKey].getDelegate().setLoading(loading); + + if (!hasDependencies) { + this.handleSettingChanged(); + } + }); + + dependency.initialize(); + + return dependency; + }; + + const helperDependency = ( + update: (args: { + getLocalSetting: (settingName: T) => TSettings[T]; + getGlobalSetting: (settingName: T) => GlobalSettings[T]; + getHelperDependency: (dep: Dependency) => TDep | null; + abortSignal: AbortSignal; + }) => T + ) => { + const dependency = new Dependency( + this as unknown as SettingsContextDelegate, + update, + makeSettingGetter, + makeGlobalSettingGetter + ); + + dependency.initialize(); + + return dependency; + }; + + if (this._parentContext.defineDependencies) { + this._parentContext.defineDependencies({ + availableSettingsUpdater, + helperDependency, + workbenchSession: this.getLayerManager().getWorkbenchSession(), + workbenchSettings: this.getLayerManager().getWorkbenchSettings(), + queryClient: this.getLayerManager().getQueryClient(), + }); + } + } + + beforeDestroy(): void { + this._unsubscribeHandler.unsubscribeAll(); + } + + private setLoadingState(loadingState: SettingsContextLoadingState) { + if (this._loadingState === loadingState) { + return; + } + + this._loadingState = loadingState; + this._publishSubscribeDelegate.notifySubscribers(SettingsContextDelegateTopic.LOADING_STATE_CHANGED); + } + + private handleSettingChanged() { + // this.getLayerManager().publishTopic(LayerManagerTopic.SETTINGS_CHANGED); + + if (!this.areAllSettingsLoaded() || !this.areAllSettingsInitialized()) { + this.setLoadingState(SettingsContextLoadingState.LOADING); + return; + } + + if (this.isSomePersistedSettingNotValid() || !this.areCurrentSettingsValid()) { + this.setLoadingState(SettingsContextLoadingState.FAILED); + return; + } + + this.setLoadingState(SettingsContextLoadingState.LOADED); + this._publishSubscribeDelegate.notifySubscribers(SettingsContextDelegateTopic.SETTINGS_CHANGED); + } + + private handleSettingsLoadingStateChanged() { + for (const key in this._settings) { + if (this._settings[key].getDelegate().isLoading()) { + this.setLoadingState(SettingsContextLoadingState.LOADING); + return; + } + } + + this.setLoadingState(SettingsContextLoadingState.LOADED); + } +} diff --git a/frontend/src/modules/2DViewer/layers/delegates/UnsubscribeHandlerDelegate.ts b/frontend/src/modules/2DViewer/layers/delegates/UnsubscribeHandlerDelegate.ts new file mode 100644 index 000000000..c7d90d140 --- /dev/null +++ b/frontend/src/modules/2DViewer/layers/delegates/UnsubscribeHandlerDelegate.ts @@ -0,0 +1,37 @@ +/* + * This class is used to manage the unsubscribe functions of the subscriptions + * of the class instances related to the layers. + * + * It provides a method for registering one ore more unsubscribe function for a specific + * topic and two methods for unsubscribing from a specific topic or from all topics, respectively. + */ +export class UnsubscribeHandlerDelegate { + private _subscriptions: Map void>> = new Map(); + + registerUnsubscribeFunction(topic: string, callback: () => void): void { + let subscriptionsSet = this._subscriptions.get(topic); + if (!subscriptionsSet) { + subscriptionsSet = new Set(); + this._subscriptions.set(topic, subscriptionsSet); + } + subscriptionsSet.add(callback); + } + + unsubscribe(topic: string): void { + const subscriptionsSet = this._subscriptions.get(topic); + if (subscriptionsSet) { + for (const unsubscribeFunc of subscriptionsSet) { + unsubscribeFunc(); + } + this._subscriptions.delete(topic); + } + } + + unsubscribeAll(): void { + for (const subscriptionsSet of this._subscriptions.values()) { + for (const unsubscribeFunc of subscriptionsSet) { + unsubscribeFunc(); + } + } + } +} diff --git a/frontend/src/modules/2DViewer/layers/delegates/_utils/Dependency.ts b/frontend/src/modules/2DViewer/layers/delegates/_utils/Dependency.ts new file mode 100644 index 000000000..911d67df5 --- /dev/null +++ b/frontend/src/modules/2DViewer/layers/delegates/_utils/Dependency.ts @@ -0,0 +1,221 @@ +import { isCancelledError } from "@tanstack/react-query"; + +import { isEqual } from "lodash"; + +import { GlobalSettings } from "../../framework/LayerManager/LayerManager"; +import { Settings, UpdateFunc } from "../../interfaces"; +import { SettingsContextDelegate } from "../SettingsContextDelegate"; + +/* + * Dependency class is used to represent a node in the dependency graph of a layer settings context. + * It can be compared to an atom in Jotai. + * + * It can subscribe to both changes in settings (local and global) and other dependencies. + * Its value is calculated by an update function that is provided during initialization. + * The update function is called whenever any of the dependencies change. + * All entities that this dependency depends on are cached such that they are only updated when they change, + * not when they are accessed. + * The dependency can be subscribed to, and will notify its subscribers whenever its value changes. + */ +export class Dependency { + private _updateFunc: UpdateFunc; + private _dependencies: Set<(value: Awaited | null) => void> = new Set(); + private _loadingDependencies: Set<(loading: boolean, hasDependencies: boolean) => void> = new Set(); + + private _contextDelegate: SettingsContextDelegate; + + private _makeSettingGetter: (key: K, handler: (value: TSettings[K]) => void) => void; + private _makeGlobalSettingGetter: ( + key: K, + handler: (value: GlobalSettings[K]) => void + ) => void; + private _cachedSettingsMap: Map = new Map(); + private _cachedGlobalSettingsMap: Map = new Map(); + private _cachedDependenciesMap: Map, any> = new Map(); + private _cachedValue: Awaited | null = null; + private _abortController: AbortController | null = null; + private _isInitialized = false; + private _numParentDependencies = 0; + private _numChildDependencies = 0; + + constructor( + contextDelegate: SettingsContextDelegate, + updateFunc: UpdateFunc, + makeSettingGetter: (key: K, handler: (value: TSettings[K]) => void) => void, + makeGlobalSettingGetter: ( + key: K, + handler: (value: GlobalSettings[K]) => void + ) => void + ) { + this._contextDelegate = contextDelegate; + this._updateFunc = updateFunc; + this._makeSettingGetter = makeSettingGetter; + this._makeGlobalSettingGetter = makeGlobalSettingGetter; + + this.getGlobalSetting = this.getGlobalSetting.bind(this); + this.getLocalSetting = this.getLocalSetting.bind(this); + this.getHelperDependency = this.getHelperDependency.bind(this); + } + + hasChildDependencies(): boolean { + return this._numChildDependencies > 0; + } + + getValue(): Awaited | null { + return this._cachedValue; + } + + subscribe(callback: (value: Awaited | null) => void, childDependency: boolean = false): () => void { + this._dependencies.add(callback); + + if (childDependency) { + this._numChildDependencies++; + } + + return () => { + this._dependencies.delete(callback); + if (childDependency) { + this._numChildDependencies--; + } + }; + } + + subscribeLoading(callback: (loading: boolean, hasDependencies: boolean) => void): () => void { + this._loadingDependencies.add(callback); + + return () => { + this._loadingDependencies.delete(callback); + }; + } + + private getLocalSetting(settingName: K): TSettings[K] { + if (!this._isInitialized) { + this._numParentDependencies++; + } + + if (this._cachedSettingsMap.has(settingName as string)) { + return this._cachedSettingsMap.get(settingName as string); + } + + this._makeSettingGetter(settingName, (value) => { + this._cachedSettingsMap.set(settingName as string, value); + this.callUpdateFunc(); + }); + + this._cachedSettingsMap.set( + settingName as string, + this._contextDelegate.getSettings()[settingName].getDelegate().getValue() + ); + return this._cachedSettingsMap.get(settingName as string); + } + + private setLoadingState(loading: boolean) { + for (const callback of this._loadingDependencies) { + callback(loading, this.hasChildDependencies()); + } + } + + private getGlobalSetting(settingName: K): GlobalSettings[K] { + if (this._cachedGlobalSettingsMap.has(settingName as string)) { + return this._cachedGlobalSettingsMap.get(settingName as string); + } + + this._makeGlobalSettingGetter(settingName, (value) => { + this._cachedGlobalSettingsMap.set(settingName as string, value); + this.callUpdateFunc(); + }); + + this._cachedGlobalSettingsMap.set( + settingName as string, + this._contextDelegate.getLayerManager().getGlobalSetting(settingName) + ); + return this._cachedGlobalSettingsMap.get(settingName as string); + } + + private getHelperDependency(dep: Dependency): Awaited | null { + if (!this._isInitialized) { + this._numParentDependencies++; + } + + if (this._cachedDependenciesMap.has(dep)) { + return this._cachedDependenciesMap.get(dep); + } + + const value = dep.getValue(); + this._cachedDependenciesMap.set(dep, value); + + dep.subscribe((newValue) => { + this._cachedDependenciesMap.set(dep, newValue); + this.callUpdateFunc(); + }, true); + + dep.subscribeLoading((loading) => { + if (loading) { + this.setLoadingState(true); + } + // Not subscribing to loading state false as it will + // be set when this dependency is updated + // #Waterfall + }); + + return value; + } + + async initialize() { + this._abortController = new AbortController(); + + // Establishing subscriptions + await this._updateFunc({ + getLocalSetting: this.getLocalSetting, + getGlobalSetting: this.getGlobalSetting, + getHelperDependency: this.getHelperDependency, + abortSignal: this._abortController.signal, + }); + + // If there are no dependencies, we can call the update function + if (this._numParentDependencies === 0) { + await this.callUpdateFunc(); + } + + this._isInitialized = true; + } + + private async callUpdateFunc() { + if (this._abortController) { + this._abortController.abort(); + this._abortController = null; + } + + this._abortController = new AbortController(); + + this.setLoadingState(true); + + let newValue: Awaited | null = null; + try { + newValue = await this._updateFunc({ + getLocalSetting: this.getLocalSetting, + getGlobalSetting: this.getGlobalSetting, + getHelperDependency: this.getHelperDependency, + abortSignal: this._abortController.signal, + }); + } catch (e: any) { + if (!isCancelledError(e)) { + this.applyNewValue(null); + return; + } + return; + } + + this.applyNewValue(newValue); + } + + private applyNewValue(newValue: Awaited | null) { + this.setLoadingState(false); + if (!isEqual(newValue, this._cachedValue) || newValue === null) { + this._cachedValue = newValue; + for (const callback of this._dependencies) { + callback(newValue); + } + } + } +} diff --git a/frontend/src/modules/2DViewer/layers/framework/ColorScale/ColorScale.ts b/frontend/src/modules/2DViewer/layers/framework/ColorScale/ColorScale.ts new file mode 100644 index 000000000..39928f9b3 --- /dev/null +++ b/frontend/src/modules/2DViewer/layers/framework/ColorScale/ColorScale.ts @@ -0,0 +1,59 @@ +import { defaultContinuousSequentialColorPalettes } from "@framework/utils/colorPalettes"; +import { ColorScaleGradientType, ColorScaleType } from "@lib/utils/ColorScale"; +import { ColorScale as ColorScaleImpl } from "@lib/utils/ColorScale"; + +import { ItemDelegate } from "../../delegates/ItemDelegate"; +import { Item, SerializedColorScale, SerializedType } from "../../interfaces"; +import { LayerManager, LayerManagerTopic } from "../LayerManager/LayerManager"; + +export class ColorScale implements Item { + private _itemDelegate: ItemDelegate; + private _colorScale: ColorScaleImpl = new ColorScaleImpl({ + colorPalette: defaultContinuousSequentialColorPalettes[0], + gradientType: ColorScaleGradientType.Sequential, + type: ColorScaleType.Continuous, + steps: 10, + }); + private _areBoundariesUserDefined: boolean = false; + + constructor(name: string, layerManager: LayerManager) { + this._itemDelegate = new ItemDelegate(name, layerManager); + } + + getItemDelegate(): ItemDelegate { + return this._itemDelegate; + } + + getColorScale(): ColorScaleImpl { + return this._colorScale; + } + + setColorScale(colorScale: ColorScaleImpl): void { + this._colorScale = colorScale; + this.getItemDelegate().getLayerManager()?.publishTopic(LayerManagerTopic.LAYER_DATA_REVISION); + } + + getAreBoundariesUserDefined(): boolean { + return this._areBoundariesUserDefined; + } + + setAreBoundariesUserDefined(areBoundariesUserDefined: boolean): void { + this._areBoundariesUserDefined = areBoundariesUserDefined; + this.getItemDelegate().getLayerManager()?.publishTopic(LayerManagerTopic.LAYER_DATA_REVISION); + } + + serializeState(): SerializedColorScale { + return { + ...this._itemDelegate.serializeState(), + type: SerializedType.COLOR_SCALE, + colorScale: this._colorScale.serialize(), + userDefinedBoundaries: this._areBoundariesUserDefined, + }; + } + + deserializeState(serialized: SerializedColorScale): void { + this._itemDelegate.deserializeState(serialized); + this._colorScale = ColorScaleImpl.fromSerialized(serialized.colorScale); + this._areBoundariesUserDefined = serialized.userDefinedBoundaries; + } +} diff --git a/frontend/src/modules/2DViewer/layers/framework/ColorScale/ColorScaleComponent.tsx b/frontend/src/modules/2DViewer/layers/framework/ColorScale/ColorScaleComponent.tsx new file mode 100644 index 000000000..2e7529a2f --- /dev/null +++ b/frontend/src/modules/2DViewer/layers/framework/ColorScale/ColorScaleComponent.tsx @@ -0,0 +1,73 @@ +import React from "react"; + +import { Icon } from "@equinor/eds-core-react"; +import { color_palette } from "@equinor/eds-icons"; +import { DenseIconButton } from "@lib/components/DenseIconButton"; +import { SortableListItem } from "@lib/components/SortableList"; +import { ColorScale as ColorScaleImpl } from "@lib/utils/ColorScale"; +import { resolveClassNames } from "@lib/utils/resolveClassNames"; +import { ColorScaleSelector } from "@modules/_shared/components/ColorScaleSelector/colorScaleSelector"; +import { ExpandLess, ExpandMore } from "@mui/icons-material"; + +import { ColorScale } from "./ColorScale"; + +import { ItemDelegateTopic } from "../../delegates/ItemDelegate"; +import { usePublishSubscribeTopicValue } from "../../delegates/PublishSubscribeDelegate"; +import { RemoveItemButton } from "../utilityComponents/RemoveItemButton"; + +export type ColorScaleComponentProps = { + colorScale: ColorScale; +}; + +export function ColorScaleComponent(props: ColorScaleComponentProps): React.ReactNode { + const workbenchSettings = props.colorScale.getItemDelegate().getLayerManager()?.getWorkbenchSettings(); + const isExpanded = usePublishSubscribeTopicValue(props.colorScale.getItemDelegate(), ItemDelegateTopic.EXPANDED); + + function handleColorScaleChange(newColorScale: ColorScaleImpl, areBoundariesUserDefined: boolean): void { + props.colorScale.setColorScale(newColorScale); + props.colorScale.setAreBoundariesUserDefined(areBoundariesUserDefined); + } + + function makeColorScaleSelector(): React.ReactNode { + if (!workbenchSettings) { + return "No layer manager set."; + } + + return ( + + ); + } + + function handleToggleExpanded(): void { + props.colorScale.getItemDelegate().setExpanded(!isExpanded); + } + + return ( + Color scale} + startAdornment={ +
+ + {isExpanded ? : } + + +
+ } + endAdornment={} + > +
+ {makeColorScaleSelector()} +
+
+ ); +} diff --git a/frontend/src/modules/2DViewer/layers/framework/DeltaSurface/DeltaSurface.ts b/frontend/src/modules/2DViewer/layers/framework/DeltaSurface/DeltaSurface.ts new file mode 100644 index 000000000..01d66be31 --- /dev/null +++ b/frontend/src/modules/2DViewer/layers/framework/DeltaSurface/DeltaSurface.ts @@ -0,0 +1,84 @@ +import { GroupDelegate, GroupDelegateTopic } from "../../delegates/GroupDelegate"; +import { ItemDelegate } from "../../delegates/ItemDelegate"; +import { LayerDelegate } from "../../delegates/LayerDelegate"; +import { SettingsContextDelegateTopic } from "../../delegates/SettingsContextDelegate"; +import { UnsubscribeHandlerDelegate } from "../../delegates/UnsubscribeHandlerDelegate"; +import { Group, SerializedDeltaSurface, SerializedType, instanceofLayer } from "../../interfaces"; +import { LayerManager } from "../LayerManager/LayerManager"; + +export class DeltaSurface implements Group { + private _itemDelegate: ItemDelegate; + private _groupDelegate: GroupDelegate; + private _unsubscribeHandler: UnsubscribeHandlerDelegate = new UnsubscribeHandlerDelegate(); + private _childrenLayerDelegateSet: Set> = new Set(); + + constructor(name: string, layerManager: LayerManager) { + this._groupDelegate = new GroupDelegate(this); + + this._unsubscribeHandler.registerUnsubscribeFunction( + "children", + this._groupDelegate.getPublishSubscribeDelegate().makeSubscriberFunction(GroupDelegateTopic.CHILDREN)( + () => { + this.handleChildrenChange(); + } + ) + ); + + this._groupDelegate.setColor("rgb(220, 210, 180)"); + this._itemDelegate = new ItemDelegate(name, layerManager); + } + + private handleChildrenChange(): void { + this._unsubscribeHandler.unsubscribe("layer-delegates"); + + for (const layerDelegate of this._childrenLayerDelegateSet) { + layerDelegate.setIsSubordinated(false); + } + + this._childrenLayerDelegateSet.clear(); + + for (const child of this._groupDelegate.getChildren()) { + if (instanceofLayer(child)) { + child.getLayerDelegate().setIsSubordinated(true); + const layerDelegate = child.getLayerDelegate(); + this._childrenLayerDelegateSet.add(layerDelegate); + + this._unsubscribeHandler.registerUnsubscribeFunction( + "layer-delegates", + layerDelegate + .getSettingsContext() + .getDelegate() + .getPublishSubscribeDelegate() + .makeSubscriberFunction(SettingsContextDelegateTopic.SETTINGS_CHANGED)(() => { + this.handleSettingsChange(); + }) + ); + } + } + } + + private handleSettingsChange(): void { + console.debug("Settings changed - would refetch data"); + } + + getItemDelegate(): ItemDelegate { + return this._itemDelegate; + } + + getGroupDelegate(): GroupDelegate { + return this._groupDelegate; + } + + deserializeState(serialized: SerializedDeltaSurface): void { + this._itemDelegate.deserializeState(serialized); + this._groupDelegate.deserializeChildren(serialized.children); + } + + serializeState(): SerializedDeltaSurface { + return { + ...this._itemDelegate.serializeState(), + type: SerializedType.DELTA_SURFACE, + children: this.getGroupDelegate().serializeChildren(), + }; + } +} diff --git a/frontend/src/modules/2DViewer/layers/framework/DeltaSurface/DeltaSurfaceComponent.tsx b/frontend/src/modules/2DViewer/layers/framework/DeltaSurface/DeltaSurfaceComponent.tsx new file mode 100644 index 000000000..47fcbbfc7 --- /dev/null +++ b/frontend/src/modules/2DViewer/layers/framework/DeltaSurface/DeltaSurfaceComponent.tsx @@ -0,0 +1,78 @@ +import { SortableListGroup } from "@lib/components/SortableList"; + +import { DeltaSurface } from "./DeltaSurface"; + +import { LayersActionGroup, LayersActions } from "../../LayersActions"; +import { GroupDelegateTopic } from "../../delegates/GroupDelegate"; +import { ItemDelegateTopic } from "../../delegates/ItemDelegate"; +import { usePublishSubscribeTopicValue } from "../../delegates/PublishSubscribeDelegate"; +import { Group, Item, instanceofLayer } from "../../interfaces"; +import { EditName } from "../utilityComponents/EditName"; +import { EmptyContent } from "../utilityComponents/EmptyContent"; +import { ExpandCollapseAllButton } from "../utilityComponents/ExpandCollapseAllButton"; +import { RemoveItemButton } from "../utilityComponents/RemoveItemButton"; +import { VisibilityToggle } from "../utilityComponents/VisibilityToggle"; +import { makeSortableListItemComponent } from "../utils/makeSortableListItemComponent"; + +export type DeltaSurfaceComponentProps = { + deltaSurface: DeltaSurface; + actions?: LayersActionGroup[]; + onActionClick?: (actionIdentifier: string, group: Group) => void; +}; + +export function DeltaSurfaceComponent(props: DeltaSurfaceComponentProps): React.ReactNode { + const children = usePublishSubscribeTopicValue(props.deltaSurface.getGroupDelegate(), GroupDelegateTopic.CHILDREN); + const isExpanded = usePublishSubscribeTopicValue(props.deltaSurface.getItemDelegate(), ItemDelegateTopic.EXPANDED); + const color = props.deltaSurface.getGroupDelegate().getColor(); + + function handleActionClick(actionIdentifier: string) { + if (props.onActionClick) { + props.onActionClick(actionIdentifier, props.deltaSurface); + } + } + + function makeEndAdornment() { + const adornment: React.ReactNode[] = []; + if ( + props.actions && + props.deltaSurface.getGroupDelegate().findChildren((item) => instanceofLayer(item)).length < 2 + ) { + adornment.push( + + ); + } + adornment.push(); + adornment.push(); + return adornment; + } + + return ( + } + contentStyle={{ + backgroundColor: color ?? undefined, + }} + headerStyle={{ + backgroundColor: color ?? undefined, + }} + startAdornment={ +
+ +
+ } + endAdornment={<>{makeEndAdornment()}} + contentWhenEmpty={ + Drag two surface layers inside to calculate the difference between them. + } + expanded={isExpanded} + > + {children.map((child: Item) => makeSortableListItemComponent(child, props.actions, props.onActionClick))} +
+ ); +} diff --git a/frontend/src/modules/2DViewer/layers/framework/LayerManager/LayerManager.ts b/frontend/src/modules/2DViewer/layers/framework/LayerManager/LayerManager.ts new file mode 100644 index 000000000..98208330a --- /dev/null +++ b/frontend/src/modules/2DViewer/layers/framework/LayerManager/LayerManager.ts @@ -0,0 +1,219 @@ +import { Ensemble } from "@framework/Ensemble"; +import { + EnsembleRealizationFilterFunction, + WorkbenchSession, + WorkbenchSessionEvent, + createEnsembleRealizationFilterFuncForWorkbenchSession, +} from "@framework/WorkbenchSession"; +import { WorkbenchSettings } from "@framework/WorkbenchSettings"; +import { QueryClient } from "@tanstack/react-query"; + +import { isEqual } from "lodash"; + +import { GroupDelegate, GroupDelegateTopic } from "../../delegates/GroupDelegate"; +import { ItemDelegate } from "../../delegates/ItemDelegate"; +import { PublishSubscribe, PublishSubscribeDelegate } from "../../delegates/PublishSubscribeDelegate"; +import { UnsubscribeHandlerDelegate } from "../../delegates/UnsubscribeHandlerDelegate"; +import { Group, Item, SerializedLayerManager, SerializedType } from "../../interfaces"; + +export enum LayerManagerTopic { + ITEMS_CHANGED = "ITEMS_CHANGED", + SETTINGS_CHANGED = "SETTINGS_CHANGED", + AVAILABLE_SETTINGS_CHANGED = "AVAILABLE_SETTINGS_CHANGED", + LAYER_DATA_REVISION = "LAYER_DATA_REVISION", + GLOBAL_SETTINGS_CHANGED = "GLOBAL_SETTINGS_CHANGED", + SHARED_SETTINGS_CHANGED = "SHARED_SETTINGS_CHANGED", +} + +export type LayerManagerTopicPayload = { + [LayerManagerTopic.ITEMS_CHANGED]: Item[]; + [LayerManagerTopic.SETTINGS_CHANGED]: void; + [LayerManagerTopic.AVAILABLE_SETTINGS_CHANGED]: void; + [LayerManagerTopic.LAYER_DATA_REVISION]: number; + [LayerManagerTopic.GLOBAL_SETTINGS_CHANGED]: void; + [LayerManagerTopic.SHARED_SETTINGS_CHANGED]: void; +}; + +export type GlobalSettings = { + fieldId: string | null; + ensembles: readonly Ensemble[]; + realizationFilterFunction: EnsembleRealizationFilterFunction; +}; + +/* + * The LayerManager class is responsible for managing all items (layers, views, settings, etc.). + * It is the main ancestor of all items and provides a way to subscribe/publish messages to all descendants. + * Moreover, it is responsible for managing the global settings coming from the framework (e.g. ensembles, fieldId). + * It also holds the revision number of the layer data, which is used to notify subscribers when any layer data changes. + * This makes it possible to update the GUI accordingly. + * The LayerManager class is also responsible for serializing/deserializing the state of itself and all its descendants. + * It does also serve as a provider of the QueryClient and WorkbenchSession. + */ + +export class LayerManager implements Group, PublishSubscribe { + private _workbenchSession: WorkbenchSession; + private _workbenchSettings: WorkbenchSettings; + private _groupDelegate: GroupDelegate; + private _queryClient: QueryClient; + private _publishSubscribeDelegate = new PublishSubscribeDelegate(); + private _itemDelegate: ItemDelegate; + private _layerDataRevision: number = 0; + private _globalSettings: GlobalSettings; + private _subscriptionsHandler = new UnsubscribeHandlerDelegate(); + private _deserializing = false; + + constructor(workbenchSession: WorkbenchSession, workbenchSettings: WorkbenchSettings, queryClient: QueryClient) { + this._workbenchSession = workbenchSession; + this._workbenchSettings = workbenchSettings; + this._queryClient = queryClient; + this._itemDelegate = new ItemDelegate("LayerManager", this); + this._groupDelegate = new GroupDelegate(this); + + this._globalSettings = this.initializeGlobalSettings(); + + this._subscriptionsHandler.registerUnsubscribeFunction( + "workbenchSession", + this._workbenchSession.subscribe( + WorkbenchSessionEvent.EnsembleSetChanged, + this.handleEnsembleSetChanged.bind(this) + ) + ); + this._subscriptionsHandler.registerUnsubscribeFunction( + "workbenchSession", + this._workbenchSession.subscribe( + WorkbenchSessionEvent.RealizationFilterSetChanged, + this.handleRealizationFilterSetChanged.bind(this) + ) + ); + this._subscriptionsHandler.registerUnsubscribeFunction( + "groupDelegate", + this._groupDelegate + .getPublishSubscribeDelegate() + .makeSubscriberFunction(GroupDelegateTopic.TREE_REVISION_NUMBER)(() => { + this.publishTopic(LayerManagerTopic.LAYER_DATA_REVISION); + this.publishTopic(LayerManagerTopic.ITEMS_CHANGED); + }) + ); + } + + getItemDelegate(): ItemDelegate { + return this._itemDelegate; + } + + getGroupDelegate(): GroupDelegate { + return this._groupDelegate; + } + + updateGlobalSetting(key: T, value: GlobalSettings[T]): void { + if (isEqual(this._globalSettings[key], value)) { + return; + } + + this._globalSettings[key] = value; + this.publishTopic(LayerManagerTopic.GLOBAL_SETTINGS_CHANGED); + } + + getGlobalSetting(key: T): GlobalSettings[T] { + return this._globalSettings[key]; + } + + publishTopic(topic: LayerManagerTopic): void { + if (this._deserializing) { + return; + } + + if (topic === LayerManagerTopic.LAYER_DATA_REVISION) { + this._layerDataRevision++; + } + + this._publishSubscribeDelegate.notifySubscribers(topic); + } + + getWorkbenchSession(): WorkbenchSession { + return this._workbenchSession; + } + + getQueryClient(): QueryClient { + return this._queryClient; + } + + getWorkbenchSettings(): WorkbenchSettings { + return this._workbenchSettings; + } + + makeSnapshotGetter(topic: T): () => LayerManagerTopicPayload[T] { + const snapshotGetter = (): any => { + if (topic === LayerManagerTopic.ITEMS_CHANGED) { + return this._groupDelegate.getChildren(); + } + if (topic === LayerManagerTopic.SETTINGS_CHANGED) { + return; + } + if (topic === LayerManagerTopic.AVAILABLE_SETTINGS_CHANGED) { + return; + } + if (topic === LayerManagerTopic.LAYER_DATA_REVISION) { + return this._layerDataRevision; + } + if (topic === LayerManagerTopic.GLOBAL_SETTINGS_CHANGED) { + return this._globalSettings; + } + if (topic === LayerManagerTopic.SHARED_SETTINGS_CHANGED) { + return; + } + }; + + return snapshotGetter; + } + + getPublishSubscribeDelegate(): PublishSubscribeDelegate { + return this._publishSubscribeDelegate; + } + + beforeDestroy() { + this._subscriptionsHandler.unsubscribeAll(); + } + + serializeState(): SerializedLayerManager { + const itemState = this._itemDelegate.serializeState(); + return { + ...itemState, + type: SerializedType.LAYER_MANAGER, + children: this._groupDelegate.serializeChildren(), + }; + } + + deserializeState(serializedState: SerializedLayerManager): void { + this._deserializing = true; + this._itemDelegate.deserializeState(serializedState); + this._groupDelegate.deserializeChildren(serializedState.children); + this._deserializing = false; + + this.publishTopic(LayerManagerTopic.ITEMS_CHANGED); + this.publishTopic(LayerManagerTopic.GLOBAL_SETTINGS_CHANGED); + } + + private initializeGlobalSettings(): GlobalSettings { + const ensembles = this._workbenchSession.getEnsembleSet().getEnsembleArr(); + return { + fieldId: null, + ensembles, + realizationFilterFunction: createEnsembleRealizationFilterFuncForWorkbenchSession(this._workbenchSession), + }; + } + + private handleRealizationFilterSetChanged() { + this._globalSettings.realizationFilterFunction = createEnsembleRealizationFilterFuncForWorkbenchSession( + this._workbenchSession + ); + + this.publishTopic(LayerManagerTopic.GLOBAL_SETTINGS_CHANGED); + } + + private handleEnsembleSetChanged() { + const ensembles = this._workbenchSession.getEnsembleSet().getEnsembleArr(); + this._globalSettings.ensembles = ensembles; + + this.publishTopic(LayerManagerTopic.GLOBAL_SETTINGS_CHANGED); + } +} diff --git a/frontend/src/modules/2DViewer/layers/framework/LayerManager/LayerManagerComponent.tsx b/frontend/src/modules/2DViewer/layers/framework/LayerManager/LayerManagerComponent.tsx new file mode 100644 index 000000000..727375833 --- /dev/null +++ b/frontend/src/modules/2DViewer/layers/framework/LayerManager/LayerManagerComponent.tsx @@ -0,0 +1,158 @@ +import React from "react"; + +import { IsMoveAllowedArgs, SortableList } from "@lib/components/SortableList"; +import { useElementSize } from "@lib/hooks/useElementSize"; +import { convertRemToPixels } from "@lib/utils/screenUnitConversions"; +import { GroupDelegate, GroupDelegateTopic } from "@modules/2DViewer/layers/delegates/GroupDelegate"; +import { usePublishSubscribeTopicValue } from "@modules/2DViewer/layers/delegates/PublishSubscribeDelegate"; +import { Group, Item, instanceofGroup } from "@modules/2DViewer/layers/interfaces"; +import { Add } from "@mui/icons-material"; + +import { LayerManager } from "./LayerManager"; + +import { LayersActionGroup, LayersActions } from "../../LayersActions"; +import { ColorScale } from "../ColorScale/ColorScale"; +import { SharedSetting } from "../SharedSetting/SharedSetting"; +import { View } from "../View/View"; +import { ExpandCollapseAllButton } from "../utilityComponents/ExpandCollapseAllButton"; +import { makeSortableListItemComponent } from "../utils/makeSortableListItemComponent"; + +export type LayerManagerComponentProps = { + layerManager: LayerManager; + additionalHeaderComponents: React.ReactNode; + layerActions: LayersActionGroup[]; + onLayerAction: (identifier: string, groupDelegate: GroupDelegate) => void; + isMoveAllowed?: (movedItem: Item, destinationGroup: Group) => boolean; +}; + +export function LayerManagerComponent(props: LayerManagerComponentProps): React.ReactNode { + const layerListRef = React.useRef(null); + const layerListSize = useElementSize(layerListRef); + + const groupDelegate = props.layerManager.getGroupDelegate(); + const items = usePublishSubscribeTopicValue(groupDelegate, GroupDelegateTopic.CHILDREN); + + function handleLayerAction(identifier: string, group?: Group) { + let groupDelegate = props.layerManager.getGroupDelegate(); + if (group) { + groupDelegate = group.getGroupDelegate(); + } + + props.onLayerAction(identifier, groupDelegate); + } + + function checkIfItemMoveAllowed(args: IsMoveAllowedArgs): boolean { + const movedItem = groupDelegate.findDescendantById(args.movedItemId); + if (!movedItem) { + return false; + } + + const destinationItem = args.destinationId + ? groupDelegate.findDescendantById(args.destinationId) + : props.layerManager; + + if (!destinationItem || !instanceofGroup(destinationItem)) { + return false; + } + + if (movedItem instanceof View && destinationItem instanceof View) { + return false; + } + + if (props.isMoveAllowed) { + if (!props.isMoveAllowed(movedItem, destinationItem)) { + return false; + } + } + + const numSharedSettingsAndColorScales = + destinationItem.getGroupDelegate().findChildren((item) => { + return item instanceof SharedSetting || item instanceof ColorScale; + }).length ?? 0; + + if (!(movedItem instanceof SharedSetting || movedItem instanceof ColorScale)) { + if (args.position < numSharedSettingsAndColorScales) { + return false; + } + } else { + if (args.originId === args.destinationId) { + if (args.position >= numSharedSettingsAndColorScales) { + return false; + } + } else { + if (args.position > numSharedSettingsAndColorScales) { + return false; + } + } + } + + return true; + } + + function handleItemMoved( + movedItemId: string, + originId: string | null, + destinationId: string | null, + position: number + ) { + const movedItem = groupDelegate.findDescendantById(movedItemId); + if (!movedItem) { + return; + } + + let origin = props.layerManager.getGroupDelegate(); + if (originId) { + const candidate = groupDelegate.findDescendantById(originId); + if (candidate && instanceofGroup(candidate)) { + origin = candidate.getGroupDelegate(); + } + } + + let destination = props.layerManager.getGroupDelegate(); + if (destinationId) { + const candidate = groupDelegate.findDescendantById(destinationId); + if (candidate && instanceofGroup(candidate)) { + destination = candidate.getGroupDelegate(); + } + } + + if (origin === destination) { + origin.moveChild(movedItem, position); + return; + } + + origin.removeChild(movedItem); + destination.insertChild(movedItem, position); + } + + return ( +
+
+
+
Layers
+ + + {props.additionalHeaderComponents} +
+
+ + Click on to add a layer. +
+ } + > + {items.map((item: Item) => + makeSortableListItemComponent(item, props.layerActions, handleLayerAction) + )} + +
+
+ + ); +} diff --git a/frontend/src/modules/2DViewer/layers/framework/SettingsGroup/SettingsGroup.ts b/frontend/src/modules/2DViewer/layers/framework/SettingsGroup/SettingsGroup.ts new file mode 100644 index 000000000..84833c990 --- /dev/null +++ b/frontend/src/modules/2DViewer/layers/framework/SettingsGroup/SettingsGroup.ts @@ -0,0 +1,36 @@ +import { GroupDelegate } from "../../delegates/GroupDelegate"; +import { ItemDelegate } from "../../delegates/ItemDelegate"; +import { Group, SerializedSettingsGroup, SerializedType } from "../../interfaces"; +import { LayerManager } from "../LayerManager/LayerManager"; + +export class SettingsGroup implements Group { + private _itemDelegate: ItemDelegate; + private _groupDelegate: GroupDelegate; + + constructor(name: string, layerManager: LayerManager) { + this._groupDelegate = new GroupDelegate(this); + this._groupDelegate.setColor("rgb(196 181 253)"); + this._itemDelegate = new ItemDelegate(name, layerManager); + } + + getItemDelegate(): ItemDelegate { + return this._itemDelegate; + } + + getGroupDelegate(): GroupDelegate { + return this._groupDelegate; + } + + serializeState(): SerializedSettingsGroup { + return { + ...this._itemDelegate.serializeState(), + type: SerializedType.SETTINGS_GROUP, + children: this._groupDelegate.serializeChildren(), + }; + } + + deserializeState(serialized: SerializedSettingsGroup) { + this._itemDelegate.deserializeState(serialized); + this._groupDelegate.deserializeChildren(serialized.children); + } +} diff --git a/frontend/src/modules/2DViewer/layers/framework/SettingsGroup/SettingsGroupComponent.tsx b/frontend/src/modules/2DViewer/layers/framework/SettingsGroup/SettingsGroupComponent.tsx new file mode 100644 index 000000000..e121fb3f3 --- /dev/null +++ b/frontend/src/modules/2DViewer/layers/framework/SettingsGroup/SettingsGroupComponent.tsx @@ -0,0 +1,72 @@ +import { SortableListGroup } from "@lib/components/SortableList"; +import { SettingsApplications } from "@mui/icons-material"; + +import { LayersActionGroup, LayersActions } from "../../LayersActions"; +import { GroupDelegateTopic } from "../../delegates/GroupDelegate"; +import { ItemDelegateTopic } from "../../delegates/ItemDelegate"; +import { usePublishSubscribeTopicValue } from "../../delegates/PublishSubscribeDelegate"; +import { Group, Item } from "../../interfaces"; +import { EmptyContent } from "../utilityComponents/EmptyContent"; +import { ExpandCollapseAllButton } from "../utilityComponents/ExpandCollapseAllButton"; +import { RemoveItemButton } from "../utilityComponents/RemoveItemButton"; +import { makeSortableListItemComponent } from "../utils/makeSortableListItemComponent"; + +export type SettingsGroupComponentProps = { + group: Group; + actions?: LayersActionGroup[]; + onActionClick?: (actionIdentifier: string, group: Group) => void; +}; + +export function SettingsGroupComponent(props: SettingsGroupComponentProps): React.ReactNode { + const children = usePublishSubscribeTopicValue(props.group.getGroupDelegate(), GroupDelegateTopic.CHILDREN); + const isExpanded = usePublishSubscribeTopicValue(props.group.getItemDelegate(), ItemDelegateTopic.EXPANDED); + const color = props.group.getGroupDelegate().getColor(); + + function handleActionClick(actionIdentifier: string) { + if (props.onActionClick) { + props.onActionClick(actionIdentifier, props.group); + } + } + + function makeEndAdornment() { + const adornment: React.ReactNode[] = []; + if (props.actions) { + adornment.push( + + ); + } + adornment.push(); + adornment.push(); + return adornment; + } + + return ( + + {props.group.getItemDelegate().getName()} + + } + contentStyle={{ + backgroundColor: color ?? undefined, + }} + headerStyle={{ + backgroundColor: "rgb(196 181 253)", + }} + startAdornment={} + endAdornment={<>{makeEndAdornment()}} + contentWhenEmpty={ + Drag a layer or setting inside to add it to this settings group. + } + expanded={isExpanded} + > + {children.map((child: Item) => makeSortableListItemComponent(child, props.actions, props.onActionClick))} + + ); +} diff --git a/frontend/src/modules/2DViewer/layers/framework/SharedSetting/SharedSetting.ts b/frontend/src/modules/2DViewer/layers/framework/SharedSetting/SharedSetting.ts new file mode 100644 index 000000000..eaba26e6d --- /dev/null +++ b/frontend/src/modules/2DViewer/layers/framework/SharedSetting/SharedSetting.ts @@ -0,0 +1,118 @@ +import { ItemDelegate } from "../../delegates/ItemDelegate"; +import { SettingTopic } from "../../delegates/SettingDelegate"; +import { UnsubscribeHandlerDelegate } from "../../delegates/UnsubscribeHandlerDelegate"; +import { Item, Layer, SerializedSharedSetting, SerializedType, Setting, instanceofLayer } from "../../interfaces"; +import { LayerManager, LayerManagerTopic } from "../LayerManager/LayerManager"; + +export class SharedSetting implements Item { + private _wrappedSetting: Setting; + private _unsubscribeHandler: UnsubscribeHandlerDelegate = new UnsubscribeHandlerDelegate(); + private _itemDelegate: ItemDelegate; + + constructor(wrappedSetting: Setting, layerManager: LayerManager) { + this._wrappedSetting = wrappedSetting; + + this._unsubscribeHandler.registerUnsubscribeFunction( + "setting", + this._wrappedSetting + .getDelegate() + .getPublishSubscribeDelegate() + .makeSubscriberFunction(SettingTopic.VALUE_CHANGED)(() => { + this.publishValueChange(); + }) + ); + this._itemDelegate = new ItemDelegate(wrappedSetting.getLabel(), layerManager); + + this._unsubscribeHandler.registerUnsubscribeFunction( + "layer-manager", + layerManager.getPublishSubscribeDelegate().makeSubscriberFunction(LayerManagerTopic.ITEMS_CHANGED)(() => { + this.makeIntersectionOfAvailableValues(); + }) + ); + this._unsubscribeHandler.registerUnsubscribeFunction( + "layer-manager", + layerManager.getPublishSubscribeDelegate().makeSubscriberFunction(LayerManagerTopic.SETTINGS_CHANGED)( + () => { + this.makeIntersectionOfAvailableValues(); + } + ) + ); + this._unsubscribeHandler.registerUnsubscribeFunction( + "layer-manager", + layerManager + .getPublishSubscribeDelegate() + .makeSubscriberFunction(LayerManagerTopic.AVAILABLE_SETTINGS_CHANGED)(() => { + this.makeIntersectionOfAvailableValues(); + }) + ); + } + + getItemDelegate(): ItemDelegate { + return this._itemDelegate; + } + + publishValueChange(): void { + const layerManager = this._itemDelegate.getLayerManager(); + if (layerManager) { + layerManager.publishTopic(LayerManagerTopic.SHARED_SETTINGS_CHANGED); + } + } + + getWrappedSetting(): Setting { + return this._wrappedSetting; + } + + private makeIntersectionOfAvailableValues(): void { + const parentGroup = this._itemDelegate.getParentGroup(); + if (!parentGroup) { + return; + } + + const layers = parentGroup.getDescendantItems((item) => instanceofLayer(item)) as Layer[]; + let index = 0; + let availableValues: any[] = []; + for (const item of layers) { + const setting = item.getLayerDelegate().getSettingsContext().getDelegate().getSettings()[ + this._wrappedSetting.getType() + ]; + if (setting) { + if (setting.getDelegate().isLoading()) { + this._wrappedSetting.getDelegate().setLoading(true); + return; + } + if (index === 0) { + availableValues.push(...setting.getDelegate().getAvailableValues()); + } else { + availableValues = availableValues.filter((value) => + setting.getDelegate().getAvailableValues().includes(value) + ); + } + index++; + } + } + + this._wrappedSetting.getDelegate().setLoading(false); + + this._wrappedSetting.getDelegate().setAvailableValues(availableValues); + this.publishValueChange(); + } + + serializeState(): SerializedSharedSetting { + return { + ...this._itemDelegate.serializeState(), + type: SerializedType.SHARED_SETTING, + wrappedSettingClass: this._wrappedSetting.constructor.name, + settingType: this._wrappedSetting.getType(), + value: this._wrappedSetting.getDelegate().serializeValue(), + }; + } + + deserializeState(serialized: SerializedSharedSetting): void { + this._itemDelegate.deserializeState(serialized); + this._wrappedSetting.getDelegate().deserializeValue(serialized.value); + } + + beforeDestroy(): void { + this._unsubscribeHandler.unsubscribeAll(); + } +} diff --git a/frontend/src/modules/2DViewer/layers/framework/SharedSetting/SharedSettingComponent.tsx b/frontend/src/modules/2DViewer/layers/framework/SharedSetting/SharedSettingComponent.tsx new file mode 100644 index 000000000..0ad9c38af --- /dev/null +++ b/frontend/src/modules/2DViewer/layers/framework/SharedSetting/SharedSettingComponent.tsx @@ -0,0 +1,89 @@ +import React from "react"; + +import { DenseIconButton } from "@lib/components/DenseIconButton"; +import { DenseIconButtonColorScheme } from "@lib/components/DenseIconButton/denseIconButton"; +import { SortableListItem } from "@lib/components/SortableList"; +import { resolveClassNames } from "@lib/utils/resolveClassNames"; +import { Delete, ExpandLess, ExpandMore, Link } from "@mui/icons-material"; + +import { SharedSetting } from "./SharedSetting"; + +import { ItemDelegateTopic } from "../../delegates/ItemDelegate"; +import { usePublishSubscribeTopicValue } from "../../delegates/PublishSubscribeDelegate"; +import { SettingComponent } from "../../settings/SettingComponent"; + +export type SharedSettingComponentProps = { + sharedSetting: SharedSetting; +}; + +export function SharedSettingComponent(props: SharedSettingComponentProps): React.ReactNode { + const isExpanded = usePublishSubscribeTopicValue(props.sharedSetting.getItemDelegate(), ItemDelegateTopic.EXPANDED); + + const manager = props.sharedSetting.getItemDelegate().getLayerManager(); + if (!manager) { + return null; + } + + function handleToggleExpanded() { + props.sharedSetting.getItemDelegate().setExpanded(!isExpanded); + } + + return ( + + {props.sharedSetting.getItemDelegate().getName()} + + } + startAdornment={ +
+ + {isExpanded ? : } + + +
+ } + endAdornment={} + headerClassNames="!bg-teal-200" + > +
+ +
+
+ ); +} + +type ActionProps = { + sharedSetting: SharedSetting; +}; + +function Actions(props: ActionProps): React.ReactNode { + function handleRemove() { + props.sharedSetting.beforeDestroy(); + const parentGroup = props.sharedSetting.getItemDelegate().getParentGroup(); + if (parentGroup) { + parentGroup.removeChild(props.sharedSetting); + } + } + + return ( + <> + + + + + ); +} diff --git a/frontend/src/modules/2DViewer/layers/framework/View/View.ts b/frontend/src/modules/2DViewer/layers/framework/View/View.ts new file mode 100644 index 000000000..1be6fdd9a --- /dev/null +++ b/frontend/src/modules/2DViewer/layers/framework/View/View.ts @@ -0,0 +1,38 @@ +import { GroupDelegate } from "../../delegates/GroupDelegate"; +import { ItemDelegate } from "../../delegates/ItemDelegate"; +import { Group, SerializedType, SerializedView } from "../../interfaces"; +import { LayerManager } from "../LayerManager/LayerManager"; + +export class View implements Group { + private _itemDelegate: ItemDelegate; + private _groupDelegate: GroupDelegate; + + constructor(name: string, layerManager: LayerManager, color: string | null = null) { + this._groupDelegate = new GroupDelegate(this); + this._groupDelegate.setColor(color); + this._itemDelegate = new ItemDelegate(name, layerManager); + } + + getItemDelegate(): ItemDelegate { + return this._itemDelegate; + } + + getGroupDelegate(): GroupDelegate { + return this._groupDelegate; + } + + serializeState(): SerializedView { + return { + ...this._itemDelegate.serializeState(), + type: SerializedType.VIEW, + color: this._groupDelegate.getColor() ?? "", + children: this._groupDelegate.serializeChildren(), + }; + } + + deserializeState(serialized: SerializedView) { + this._itemDelegate.deserializeState(serialized); + this._groupDelegate.setColor(serialized.color); + this._groupDelegate.deserializeChildren(serialized.children); + } +} diff --git a/frontend/src/modules/2DViewer/layers/framework/View/ViewComponent.tsx b/frontend/src/modules/2DViewer/layers/framework/View/ViewComponent.tsx new file mode 100644 index 000000000..86817a450 --- /dev/null +++ b/frontend/src/modules/2DViewer/layers/framework/View/ViewComponent.tsx @@ -0,0 +1,76 @@ +import { SortableListGroup } from "@lib/components/SortableList"; + +import { LayersActionGroup, LayersActions } from "../../LayersActions"; +import { GroupDelegateTopic } from "../../delegates/GroupDelegate"; +import { ItemDelegateTopic } from "../../delegates/ItemDelegate"; +import { usePublishSubscribeTopicValue } from "../../delegates/PublishSubscribeDelegate"; +import { Group, Item } from "../../interfaces"; +import { EditName } from "../utilityComponents/EditName"; +import { EmptyContent } from "../utilityComponents/EmptyContent"; +import { ExpandCollapseAllButton } from "../utilityComponents/ExpandCollapseAllButton"; +import { RemoveItemButton } from "../utilityComponents/RemoveItemButton"; +import { VisibilityToggle } from "../utilityComponents/VisibilityToggle"; +import { makeSortableListItemComponent } from "../utils/makeSortableListItemComponent"; + +export type ViewComponentProps = { + group: Group; + actions?: LayersActionGroup[]; + onActionClick?: (actionIdentifier: string, group: Group) => void; +}; + +export function ViewComponent(props: ViewComponentProps): React.ReactNode { + const children = usePublishSubscribeTopicValue(props.group.getGroupDelegate(), GroupDelegateTopic.CHILDREN); + const isExpanded = usePublishSubscribeTopicValue(props.group.getItemDelegate(), ItemDelegateTopic.EXPANDED); + const color = props.group.getGroupDelegate().getColor(); + + function handleActionClick(actionIdentifier: string) { + if (props.onActionClick) { + props.onActionClick(actionIdentifier, props.group); + } + } + + function makeEndAdornment() { + const adornments: React.ReactNode[] = []; + if (props.actions) { + adornments.push( + + ); + } + adornments.push(); + adornments.push(); + return adornments; + } + + return ( + +
+
+ +
+
+ } + contentStyle={{ + backgroundColor: color ?? undefined, + }} + expanded={isExpanded} + startAdornment={} + endAdornment={<>{makeEndAdornment()}} + contentWhenEmpty={Drag a layer inside to add it to this view.} + > + {children.map((child: Item) => makeSortableListItemComponent(child, props.actions, props.onActionClick))} +
+ ); +} diff --git a/frontend/src/modules/2DViewer/layers/framework/utilityComponents/EditName.tsx b/frontend/src/modules/2DViewer/layers/framework/utilityComponents/EditName.tsx new file mode 100644 index 000000000..4e1a418e3 --- /dev/null +++ b/frontend/src/modules/2DViewer/layers/framework/utilityComponents/EditName.tsx @@ -0,0 +1,67 @@ +import React from "react"; + +import { Edit } from "@mui/icons-material"; + +import { ItemDelegateTopic } from "../../delegates/ItemDelegate"; +import { usePublishSubscribeTopicValue } from "../../delegates/PublishSubscribeDelegate"; +import { Item } from "../../interfaces"; + +type EditItemNameProps = { + item: Item; +}; + +export function EditName(props: EditItemNameProps): React.ReactNode { + const itemName = usePublishSubscribeTopicValue(props.item.getItemDelegate(), ItemDelegateTopic.NAME); + + const [editingName, setEditingName] = React.useState(false); + const [currentName, setCurrentName] = React.useState(itemName); + + function handleNameDoubleClick() { + setEditingName(true); + } + + function handleNameChange(e: React.ChangeEvent) { + setCurrentName(e.target.value); + } + + function handleBlur() { + setEditingName(false); + props.item.getItemDelegate().setName(currentName); + } + + function handleKeyDown(e: React.KeyboardEvent) { + if (e.key === "Enter") { + setEditingName(false); + props.item.getItemDelegate().setName(currentName); + } + } + + return ( +
+ {editingName ? ( + + ) : ( + <> +
{itemName}
+ + + )} +
+ ); +} diff --git a/frontend/src/modules/2DViewer/layers/framework/utilityComponents/EmptyContent.tsx b/frontend/src/modules/2DViewer/layers/framework/utilityComponents/EmptyContent.tsx new file mode 100644 index 000000000..ab8a3db27 --- /dev/null +++ b/frontend/src/modules/2DViewer/layers/framework/utilityComponents/EmptyContent.tsx @@ -0,0 +1,9 @@ +import React from "react"; + +export type EmptyContentProps = { + children?: React.ReactNode; +}; + +export function EmptyContent(props: EmptyContentProps): React.ReactNode { + return
{props.children}
; +} diff --git a/frontend/src/modules/2DViewer/layers/framework/utilityComponents/ExpandCollapseAllButton.tsx b/frontend/src/modules/2DViewer/layers/framework/utilityComponents/ExpandCollapseAllButton.tsx new file mode 100644 index 000000000..c2e430598 --- /dev/null +++ b/frontend/src/modules/2DViewer/layers/framework/utilityComponents/ExpandCollapseAllButton.tsx @@ -0,0 +1,37 @@ +import React from "react"; + +import { DenseIconButton } from "@lib/components/DenseIconButton"; +import { UnfoldLessDouble, UnfoldMoreDouble } from "@mui/icons-material"; + +import { Group } from "../../interfaces"; + +export type ExpandCollapseAllButtonProps = { + group: Group; +}; + +export function ExpandCollapseAllButton(props: ExpandCollapseAllButtonProps): React.ReactNode { + function expandAllChildren() { + const descendants = props.group.getGroupDelegate().getDescendantItems(() => true); + for (const child of descendants) { + child.getItemDelegate().setExpanded(true); + } + } + + function collapseAllChildren() { + const descendants = props.group.getGroupDelegate().getDescendantItems(() => true); + for (const child of descendants) { + child.getItemDelegate().setExpanded(false); + } + } + + return ( + <> + + + + + + + + ); +} diff --git a/frontend/src/modules/2DViewer/layers/framework/utilityComponents/RemoveItemButton.tsx b/frontend/src/modules/2DViewer/layers/framework/utilityComponents/RemoveItemButton.tsx new file mode 100644 index 000000000..0db7c0b75 --- /dev/null +++ b/frontend/src/modules/2DViewer/layers/framework/utilityComponents/RemoveItemButton.tsx @@ -0,0 +1,30 @@ +import { DenseIconButton } from "@lib/components/DenseIconButton"; +import { DenseIconButtonColorScheme } from "@lib/components/DenseIconButton/denseIconButton"; +import { Delete } from "@mui/icons-material"; + +import { Item, instanceofLayer } from "../../interfaces"; + +export type RemoveItemButtonProps = { + item: Item; +}; + +export function RemoveItemButton(props: RemoveItemButtonProps): React.ReactNode { + function handleRemove() { + const parentGroup = props.item.getItemDelegate().getParentGroup(); + if (parentGroup) { + parentGroup.removeChild(props.item); + } + + if (instanceofLayer(props.item)) { + props.item.getLayerDelegate().beforeDestroy(); + } + } + + return ( + <> + + + + + ); +} diff --git a/frontend/src/modules/2DViewer/layers/framework/utilityComponents/VisibilityToggle.tsx b/frontend/src/modules/2DViewer/layers/framework/utilityComponents/VisibilityToggle.tsx new file mode 100644 index 000000000..0f976b0be --- /dev/null +++ b/frontend/src/modules/2DViewer/layers/framework/utilityComponents/VisibilityToggle.tsx @@ -0,0 +1,24 @@ +import { DenseIconButton } from "@lib/components/DenseIconButton"; +import { Visibility, VisibilityOff } from "@mui/icons-material"; + +import { ItemDelegateTopic } from "../../delegates/ItemDelegate"; +import { usePublishSubscribeTopicValue } from "../../delegates/PublishSubscribeDelegate"; +import { Item } from "../../interfaces"; + +export type VisibilityToggleProps = { + item: Item; +}; + +export function VisibilityToggle(props: VisibilityToggleProps): React.ReactNode { + const isVisible = usePublishSubscribeTopicValue(props.item.getItemDelegate(), ItemDelegateTopic.VISIBILITY); + + function handleToggleLayerVisibility() { + props.item.getItemDelegate().setVisible(!isVisible); + } + + return ( + + {isVisible ? : } + + ); +} diff --git a/frontend/src/modules/2DViewer/layers/framework/utils/DeserializationFactory.ts b/frontend/src/modules/2DViewer/layers/framework/utils/DeserializationFactory.ts new file mode 100644 index 000000000..a2474f7d0 --- /dev/null +++ b/frontend/src/modules/2DViewer/layers/framework/utils/DeserializationFactory.ts @@ -0,0 +1,73 @@ +import { + Item, + SerializedColorScale, + SerializedItem, + SerializedLayer, + SerializedSettingsGroup, + SerializedSharedSetting, + SerializedType, + SerializedView, +} from "../../interfaces"; +import { LayerRegistry } from "../../layers/LayerRegistry"; +import { SettingRegistry } from "../../settings/SettingRegistry"; +import { ColorScale } from "../ColorScale/ColorScale"; +import { LayerManager } from "../LayerManager/LayerManager"; +import { SettingsGroup } from "../SettingsGroup/SettingsGroup"; +import { SharedSetting } from "../SharedSetting/SharedSetting"; +import { View } from "../View/View"; + +export class DeserializationFactory { + private _layerManager: LayerManager; + + constructor(layerManager: LayerManager) { + this._layerManager = layerManager; + } + + makeItem(serialized: SerializedItem): Item { + if (serialized.type === SerializedType.LAYER_MANAGER) { + throw new Error( + "Cannot deserialize a LayerManager in DeserializationFactory. A LayerManager can never be a descendant of a LayerManager." + ); + } + + if (serialized.type === SerializedType.LAYER) { + const serializedLayer = serialized as SerializedLayer; + const layer = LayerRegistry.makeLayer(serializedLayer.layerClass, this._layerManager); + layer.getLayerDelegate().deserializeState(serializedLayer); + layer.getItemDelegate().setId(serializedLayer.id); + layer.getItemDelegate().setName(serializedLayer.name); + return layer; + } + + if (serialized.type === SerializedType.VIEW) { + const serializedView = serialized as SerializedView; + const view = new View(serializedView.name, this._layerManager, serializedView.color); + view.deserializeState(serializedView); + return view; + } + + if (serialized.type === SerializedType.SETTINGS_GROUP) { + const serializedSettingsGroup = serialized as SerializedSettingsGroup; + const settingsGroup = new SettingsGroup(serializedSettingsGroup.name, this._layerManager); + settingsGroup.deserializeState(serializedSettingsGroup); + return settingsGroup; + } + + if (serialized.type === SerializedType.COLOR_SCALE) { + const serializedColorScale = serialized as SerializedColorScale; + const colorScale = new ColorScale(serializedColorScale.name, this._layerManager); + colorScale.deserializeState(serializedColorScale); + return colorScale; + } + + if (serialized.type === SerializedType.SHARED_SETTING) { + const serializedSharedSetting = serialized as SerializedSharedSetting; + const wrappedSetting = SettingRegistry.makeSetting(serializedSharedSetting.wrappedSettingClass); + const setting = new SharedSetting(wrappedSetting, this._layerManager); + setting.deserializeState(serializedSharedSetting); + return setting; + } + + throw new Error(`Unhandled serialized item type: ${serialized.type}`); + } +} diff --git a/frontend/src/modules/2DViewer/layers/framework/utils/makeSortableListItemComponent.tsx b/frontend/src/modules/2DViewer/layers/framework/utils/makeSortableListItemComponent.tsx new file mode 100644 index 000000000..19f4a674e --- /dev/null +++ b/frontend/src/modules/2DViewer/layers/framework/utils/makeSortableListItemComponent.tsx @@ -0,0 +1,91 @@ +import { LayersActionGroup } from "../../LayersActions"; +import { Group, Item, instanceofLayer } from "../../interfaces"; +import { LayerComponent } from "../../layers/LayerComponent"; +import { ColorScale } from "../ColorScale/ColorScale"; +import { ColorScaleComponent } from "../ColorScale/ColorScaleComponent"; +import { DeltaSurface } from "../DeltaSurface/DeltaSurface"; +import { DeltaSurfaceComponent } from "../DeltaSurface/DeltaSurfaceComponent"; +import { SettingsGroup } from "../SettingsGroup/SettingsGroup"; +import { SettingsGroupComponent } from "../SettingsGroup/SettingsGroupComponent"; +import { SharedSetting } from "../SharedSetting/SharedSetting"; +import { SharedSettingComponent } from "../SharedSetting/SharedSettingComponent"; +import { View } from "../View/View"; +import { ViewComponent } from "../View/ViewComponent"; + +export function makeSortableListItemComponent( + item: Item, + layerActions?: LayersActionGroup[], + onActionClick?: (identifier: string, group: Group) => void +): React.ReactElement { + if (instanceofLayer(item)) { + return ; + } + if (item instanceof SettingsGroup) { + return ( + + ); + } + if (item instanceof View) { + return ( + + ); + } + if (item instanceof DeltaSurface) { + return ( + + ); + } + if (item instanceof SharedSetting) { + return ; + } + if (item instanceof ColorScale) { + return ; + } + + throw new Error(`Unsupported item type: ${item.constructor.name}`); +} + +function filterAwayViewActions(actions: LayersActionGroup[]): LayersActionGroup[] { + return actions.map((group) => ({ + ...group, + children: group.children.filter((child) => child.label !== "View"), + })); +} + +function filterAwayNonSurfaceActions(actions: LayersActionGroup[]): LayersActionGroup[] { + const result: LayersActionGroup[] = []; + + for (const group of actions) { + if (group.label === "Shared Settings") { + result.push(group); + continue; + } + if (group.label !== "Layers") { + continue; + } + const children = group.children.filter((child) => child.label.includes("Surface")); + if (children.length > 0) { + result.push({ + ...group, + children, + }); + } + } + + return result; +} diff --git a/frontend/src/modules/2DViewer/layers/interfaces.ts b/frontend/src/modules/2DViewer/layers/interfaces.ts new file mode 100644 index 000000000..8a492e11b --- /dev/null +++ b/frontend/src/modules/2DViewer/layers/interfaces.ts @@ -0,0 +1,205 @@ +import { WorkbenchSession } from "@framework/WorkbenchSession"; +import { WorkbenchSettings } from "@framework/WorkbenchSettings"; +import { ColorScaleSerialization } from "@lib/utils/ColorScale"; +import { QueryClient } from "@tanstack/react-query"; + +import { GroupDelegate } from "./delegates/GroupDelegate"; +import { ItemDelegate } from "./delegates/ItemDelegate"; +import { LayerDelegate } from "./delegates/LayerDelegate"; +import { SettingDelegate } from "./delegates/SettingDelegate"; +import { SettingsContextDelegate } from "./delegates/SettingsContextDelegate"; +import { Dependency } from "./delegates/_utils/Dependency"; +import { GlobalSettings } from "./framework/LayerManager/LayerManager"; +import { SettingType } from "./settings/settingsTypes"; + +export enum SerializedType { + LAYER_MANAGER = "layer-manager", + VIEW = "view", + LAYER = "layer", + SETTINGS_GROUP = "settings-group", + COLOR_SCALE = "color-scale", + DELTA_SURFACE = "delta-surface", + SHARED_SETTING = "shared-setting", +} + +export interface SerializedItem { + id: string; + type: SerializedType; + name: string; + expanded: boolean; + visible: boolean; +} + +export type SerializedSettingsState = Record; + +export interface SerializedLayer extends SerializedItem { + type: SerializedType.LAYER; + layerClass: string; + settings: SerializedSettingsState; +} + +export interface SerializedView extends SerializedItem { + type: SerializedType.VIEW; + color: string; + children: SerializedItem[]; +} + +export interface SerializedSettingsGroup extends SerializedItem { + type: SerializedType.SETTINGS_GROUP; + children: SerializedItem[]; +} + +export interface SerializedColorScale extends SerializedItem { + type: SerializedType.COLOR_SCALE; + colorScale: ColorScaleSerialization; + userDefinedBoundaries: boolean; +} + +export interface SerializedSharedSetting extends SerializedItem { + type: SerializedType.SHARED_SETTING; + settingType: SettingType; + wrappedSettingClass: string; + value: string; +} + +export interface SerializedLayerManager extends SerializedItem { + type: SerializedType.LAYER_MANAGER; + children: SerializedItem[]; +} + +export interface SerializedDeltaSurface extends SerializedItem { + type: SerializedType.DELTA_SURFACE; + children: SerializedItem[]; +} + +export interface Item { + getItemDelegate(): ItemDelegate; + serializeState(): SerializedItem; + deserializeState(serialized: SerializedItem): void; +} + +export function instanceofItem(item: any): item is Item { + return (item as Item).getItemDelegate !== undefined; +} + +export interface Group extends Item { + getGroupDelegate(): GroupDelegate; +} + +export function instanceofGroup(item: Item): item is Group { + return (item as Group).getItemDelegate !== undefined && (item as Group).getGroupDelegate !== undefined; +} + +export type BoundingBox = { + x: [number, number]; + y: [number, number]; + z: [number, number]; +}; + +export enum FetchDataFunctionResult { + SUCCESS = "SUCCESS", + IN_PROGRESS = "IN_PROGRESS", + ERROR = "ERROR", + NO_CHANGE = "NO_CHANGE", +} +export interface FetchDataFunction { + ( + oldValues: { [K in TKey]?: TSettings[K] }, + newValues: { [K in TKey]?: TSettings[K] } + ): Promise; +} + +export interface Layer extends Item { + getLayerDelegate(): LayerDelegate; + doSettingsChangesRequireDataRefetch(prevSettings: TSettings, newSettings: TSettings): boolean; + fetchData(queryClient: QueryClient): Promise; + makeBoundingBox?(): BoundingBox | null; + makeValueRange?(): [number, number] | null; +} + +export function instanceofLayer(item: Item): item is Layer { + return ( + (item as Layer).getItemDelegate !== undefined && + (item as Layer).doSettingsChangesRequireDataRefetch !== undefined && + (item as Layer).fetchData !== undefined + ); +} + +export interface GetHelperDependency { + (dep: Dependency): Awaited | null; +} + +export interface UpdateFunc { + (args: { + getLocalSetting: (settingName: K) => TSettings[K]; + getGlobalSetting: (settingName: T) => GlobalSettings[T]; + getHelperDependency: GetHelperDependency; + abortSignal: AbortSignal; + }): TReturnValue; +} + +export interface DefineDependenciesArgs { + availableSettingsUpdater: ( + settingName: TKey, + update: UpdateFunc, TSettings, TKey> + ) => Dependency, TSettings, TKey>; + helperDependency: ( + update: (args: { + getLocalSetting: (settingName: T) => TSettings[T]; + getGlobalSetting: (settingName: T) => GlobalSettings[T]; + getHelperDependency: (helperDependency: Dependency) => TDep | null; + abortSignal: AbortSignal; + }) => T + ) => Dependency; + workbenchSession: WorkbenchSession; + workbenchSettings: WorkbenchSettings; + queryClient: QueryClient; +} + +export interface SettingsContext { + getDelegate(): SettingsContextDelegate; + areCurrentSettingsValid?: (settings: TSettings) => boolean; + defineDependencies(args: DefineDependenciesArgs): void; +} + +// Required when making "AvailableValuesType" for all settings in an object ("TSettings") +export type EachAvailableValuesType = T extends any ? AvailableValuesType : never; + +// Returns an array of "TValue" if the "TValue" itself is not already an array +export type AvailableValuesType = RemoveUnknownFromArray>; + +// "MakeArrayIfNotArray" yields "unknown[] | any[]" for "T = any" - we don't want "unknown[]" +type RemoveUnknownFromArray = T extends unknown[] | any[] ? any[] : T; +type MakeArrayIfNotArray = Exclude extends Array ? Array : Array>; + +export type SettingComponentProps = { + onValueChange: (newValue: TValue) => void; + value: TValue; + isValueValid: boolean; + overriddenValue: TValue | null; + isOverridden: boolean; + availableValues: AvailableValuesType; + workbenchSession: WorkbenchSession; + workbenchSettings: WorkbenchSettings; + globalSettings: GlobalSettings; +}; + +export type ValueToStringArgs = { + value: TValue; + workbenchSession: WorkbenchSession; + workbenchSettings: WorkbenchSettings; +}; + +export interface Setting { + getType(): SettingType; + getLabel(): string; + makeComponent(): (props: SettingComponentProps) => React.ReactNode; + getDelegate(): SettingDelegate; + fixupValue?: (availableValues: AvailableValuesType, currentValue: TValue) => TValue; + isValueValid?: (availableValues: AvailableValuesType, value: TValue) => boolean; + serializeValue?: (value: TValue) => string; + deserializeValue?: (serializedValue: string) => TValue; + valueToString?: (args: ValueToStringArgs) => string; +} + +export type Settings = { [key in SettingType]?: any }; diff --git a/frontend/src/modules/2DViewer/layers/layers/LayerComponent.tsx b/frontend/src/modules/2DViewer/layers/layers/LayerComponent.tsx new file mode 100644 index 000000000..db3ef4490 --- /dev/null +++ b/frontend/src/modules/2DViewer/layers/layers/LayerComponent.tsx @@ -0,0 +1,166 @@ +import React from "react"; + +import { StatusMessage } from "@framework/ModuleInstanceStatusController"; +import { CircularProgress } from "@lib/components/CircularProgress"; +import { DenseIconButton } from "@lib/components/DenseIconButton"; +import { SortableListItem } from "@lib/components/SortableList"; +import { resolveClassNames } from "@lib/utils/resolveClassNames"; +import { Block, CheckCircle, Difference, Error, ExpandLess, ExpandMore } from "@mui/icons-material"; + +import { ItemDelegateTopic } from "../delegates/ItemDelegate"; +import { LayerDelegateTopic, LayerStatus } from "../delegates/LayerDelegate"; +import { usePublishSubscribeTopicValue } from "../delegates/PublishSubscribeDelegate"; +import { SettingsContextDelegateTopic, SettingsContextLoadingState } from "../delegates/SettingsContextDelegate"; +import { EditName } from "../framework/utilityComponents/EditName"; +import { RemoveItemButton } from "../framework/utilityComponents/RemoveItemButton"; +import { VisibilityToggle } from "../framework/utilityComponents/VisibilityToggle"; +import { Layer, Setting } from "../interfaces"; +import { SettingComponent } from "../settings/SettingComponent"; + +export type LayerComponentProps = { + layer: Layer; +}; + +export function LayerComponent(props: LayerComponentProps): React.ReactNode { + const isExpanded = usePublishSubscribeTopicValue(props.layer.getItemDelegate(), ItemDelegateTopic.EXPANDED); + + function makeSetting(setting: Setting) { + const manager = props.layer.getItemDelegate().getLayerManager(); + if (!manager) { + return null; + } + return ( + + ); + } + + function makeSettings(settings: Record>): React.ReactNode[] { + const settingNodes: React.ReactNode[] = []; + for (const key of Object.keys(settings)) { + settingNodes.push(makeSetting(settings[key])); + } + return settingNodes; + } + + return ( + } + startAdornment={} + endAdornment={} + > +
+ {makeSettings(props.layer.getLayerDelegate().getSettingsContext().getDelegate().getSettings())} +
+
+ ); +} + +type StartActionProps = { + layer: Layer; +}; + +function StartActions(props: StartActionProps): React.ReactNode { + const isExpanded = usePublishSubscribeTopicValue(props.layer.getItemDelegate(), ItemDelegateTopic.EXPANDED); + + function handleToggleExpanded() { + props.layer.getItemDelegate().setExpanded(!isExpanded); + } + return ( +
+ + {isExpanded ? : } + + +
+ ); +} + +type EndActionProps = { + layer: Layer; +}; + +function EndActions(props: EndActionProps): React.ReactNode { + const status = usePublishSubscribeTopicValue(props.layer.getLayerDelegate(), LayerDelegateTopic.STATUS); + const settingsStatus = usePublishSubscribeTopicValue( + props.layer.getLayerDelegate().getSettingsContext().getDelegate(), + SettingsContextDelegateTopic.LOADING_STATE_CHANGED + ); + const isSubordinated = usePublishSubscribeTopicValue( + props.layer.getLayerDelegate(), + LayerDelegateTopic.SUBORDINATED + ); + + function makeStatus(): React.ReactNode { + if (isSubordinated) { + return ( +
+ +
+ ); + } + if (status === LayerStatus.LOADING) { + return ( +
+ +
+ ); + } + if (status === LayerStatus.ERROR) { + const error = props.layer.getLayerDelegate().getError(); + if (typeof error === "string") { + return ( +
+ +
+ ); + } else { + const statusMessage = error as StatusMessage; + return ( +
+ +
+ ); + } + } + if (status === LayerStatus.SUCCESS) { + return ( +
+ +
+ ); + } + if (settingsStatus === SettingsContextLoadingState.FAILED) { + return ( +
+ +
+ ); + } + return null; + } + + return ( + <> + {makeStatus()} + + + ); +} diff --git a/frontend/src/modules/2DViewer/layers/layers/LayerRegistry.ts b/frontend/src/modules/2DViewer/layers/layers/LayerRegistry.ts new file mode 100644 index 000000000..c27ed29b7 --- /dev/null +++ b/frontend/src/modules/2DViewer/layers/layers/LayerRegistry.ts @@ -0,0 +1,18 @@ +import { LayerManager } from "../framework/LayerManager/LayerManager"; +import { Layer } from "../interfaces"; + +export class LayerRegistry { + private static _registeredLayers: Map }> = new Map(); + + static registerLayer(ctor: { new (layerManager: LayerManager): Layer }): void { + this._registeredLayers.set(ctor.name, ctor); + } + + static makeLayer(layerName: string, layerManager: LayerManager): Layer { + const Layer = this._registeredLayers.get(layerName); + if (!Layer) { + throw new Error(`Layer ${layerName} not found`); + } + return new Layer(layerManager); + } +} diff --git a/frontend/src/modules/2DViewer/layers/layers/_utils/queryConstants.ts b/frontend/src/modules/2DViewer/layers/layers/_utils/queryConstants.ts new file mode 100644 index 000000000..3acac7d69 --- /dev/null +++ b/frontend/src/modules/2DViewer/layers/layers/_utils/queryConstants.ts @@ -0,0 +1,2 @@ +export const STALE_TIME = 60 * 1000; +export const CACHE_TIME = 60 * 1000; diff --git a/frontend/src/modules/2DViewer/layers/layers/_utils/utils.ts b/frontend/src/modules/2DViewer/layers/layers/_utils/utils.ts new file mode 100644 index 000000000..d9ea853b6 --- /dev/null +++ b/frontend/src/modules/2DViewer/layers/layers/_utils/utils.ts @@ -0,0 +1,22 @@ +import { CancelablePromise } from "@api"; +import { FetchQueryOptions, QueryClient } from "@tanstack/react-query"; + +export function cancelPromiseOnAbort(promise: CancelablePromise, abortSignal: AbortSignal): Promise { + abortSignal.addEventListener("abort", () => { + console.debug("Promise aborted"); + promise.cancel(); + }); + return promise; +} + +export async function cancelQueryOnAbort( + queryClient: QueryClient, + abortSignal: AbortSignal, + options: FetchQueryOptions +) { + abortSignal.addEventListener("abort", () => { + queryClient.cancelQueries({ queryKey: options.queryKey }); + }); + + return await queryClient.fetchQuery(options); +} diff --git a/frontend/src/modules/2DViewer/layers/layers/implementations/DrilledWellTrajectoriesLayer/DrilledWellTrajectoriesLayer.ts b/frontend/src/modules/2DViewer/layers/layers/implementations/DrilledWellTrajectoriesLayer/DrilledWellTrajectoriesLayer.ts new file mode 100644 index 000000000..65a1f0907 --- /dev/null +++ b/frontend/src/modules/2DViewer/layers/layers/implementations/DrilledWellTrajectoriesLayer/DrilledWellTrajectoriesLayer.ts @@ -0,0 +1,125 @@ +import { WellboreTrajectory_api } from "@api"; +import { apiService } from "@framework/ApiService"; +import { ItemDelegate } from "@modules/2DViewer/layers/delegates/ItemDelegate"; +import { LayerManager } from "@modules/2DViewer/layers/framework/LayerManager/LayerManager"; +import { LayerRegistry } from "@modules/2DViewer/layers/layers/LayerRegistry"; +import { SettingType } from "@modules/2DViewer/layers/settings/settingsTypes"; +import { QueryClient } from "@tanstack/react-query"; + +import { isEqual } from "lodash"; + +import { DrilledWellTrajectoriesSettingsContext } from "./DrilledWellTrajectoriesSettingsContext"; +import { DrilledWellTrajectoriesSettings } from "./types"; + +import { LayerColoringType, LayerDelegate } from "../../../delegates/LayerDelegate"; +import { BoundingBox, Layer, SerializedLayer } from "../../../interfaces"; + +export class DrilledWellTrajectoriesLayer implements Layer { + private _layerDelegate: LayerDelegate; + private _itemDelegate: ItemDelegate; + + constructor(layerManager: LayerManager) { + this._itemDelegate = new ItemDelegate("Drilled Wellbore trajectories", layerManager); + this._layerDelegate = new LayerDelegate( + this, + layerManager, + new DrilledWellTrajectoriesSettingsContext(layerManager), + LayerColoringType.NONE + ); + } + + getSettingsContext() { + return this._layerDelegate.getSettingsContext(); + } + + getItemDelegate(): ItemDelegate { + return this._itemDelegate; + } + + getLayerDelegate(): LayerDelegate { + return this._layerDelegate; + } + + doSettingsChangesRequireDataRefetch( + prevSettings: DrilledWellTrajectoriesSettings, + newSettings: DrilledWellTrajectoriesSettings + ): boolean { + return !isEqual(prevSettings, newSettings); + } + + makeBoundingBox(): BoundingBox | null { + const data = this._layerDelegate.getData(); + if (!data) { + return null; + } + + const bbox: BoundingBox = { + x: [Number.MAX_SAFE_INTEGER, Number.MIN_SAFE_INTEGER], + y: [Number.MAX_SAFE_INTEGER, Number.MIN_SAFE_INTEGER], + z: [Number.MAX_SAFE_INTEGER, Number.MIN_SAFE_INTEGER], + }; + + for (const trajectory of data) { + for (const point of trajectory.eastingArr) { + bbox.x[0] = Math.min(bbox.x[0], point); + bbox.x[1] = Math.max(bbox.x[1], point); + } + for (const point of trajectory.northingArr) { + bbox.y[0] = Math.min(bbox.y[0], point); + bbox.y[1] = Math.max(bbox.y[1], point); + } + for (const point of trajectory.tvdMslArr) { + bbox.z[0] = Math.min(bbox.z[0], point); + bbox.z[1] = Math.max(bbox.z[1], point); + } + } + + return bbox; + } + + fetchData(queryClient: QueryClient): Promise { + const workbenchSession = this.getSettingsContext().getDelegate().getLayerManager().getWorkbenchSession(); + const ensembleSet = workbenchSession.getEnsembleSet(); + const settings = this.getSettingsContext().getDelegate().getSettings(); + const ensembleIdent = settings[SettingType.ENSEMBLE].getDelegate().getValue(); + const selectedWellboreHeaders = settings[SettingType.SMDA_WELLBORE_HEADERS].getDelegate().getValue(); + let selectedWellboreUuids: string[] = []; + if (selectedWellboreHeaders) { + selectedWellboreUuids = selectedWellboreHeaders.map((header) => header.wellboreUuid); + } + + let fieldIdentifier: string | null = null; + if (ensembleIdent) { + const ensemble = ensembleSet.findEnsemble(ensembleIdent); + if (ensemble) { + fieldIdentifier = ensemble.getFieldIdentifier(); + } + } + + const queryKey = ["getWellTrajectories", fieldIdentifier]; + this._layerDelegate.registerQueryKey(queryKey); + + const promise = queryClient + .fetchQuery({ + queryKey, + queryFn: () => apiService.well.getWellTrajectories(fieldIdentifier ?? ""), + staleTime: 1800000, // TODO: Both stale and gcTime are set to 30 minutes for now since SMDA is quite slow for fields with many wells - this should be adjusted later + gcTime: 1800000, + }) + .then((response: WellboreTrajectory_api[]) => { + return response.filter((trajectory) => selectedWellboreUuids.includes(trajectory.wellboreUuid)); + }); + + return promise; + } + + serializeState(): SerializedLayer { + return this._layerDelegate.serializeState(); + } + + deserializeState(serializedState: SerializedLayer): void { + this._layerDelegate.deserializeState(serializedState); + } +} + +LayerRegistry.registerLayer(DrilledWellTrajectoriesLayer); diff --git a/frontend/src/modules/2DViewer/layers/layers/implementations/DrilledWellTrajectoriesLayer/DrilledWellTrajectoriesSettingsContext.ts b/frontend/src/modules/2DViewer/layers/layers/implementations/DrilledWellTrajectoriesLayer/DrilledWellTrajectoriesSettingsContext.ts new file mode 100644 index 000000000..29c7da0c9 --- /dev/null +++ b/frontend/src/modules/2DViewer/layers/layers/implementations/DrilledWellTrajectoriesLayer/DrilledWellTrajectoriesSettingsContext.ts @@ -0,0 +1,86 @@ +import { apiService } from "@framework/ApiService"; +import { SettingsContextDelegate } from "@modules/2DViewer/layers/delegates/SettingsContextDelegate"; +import { LayerManager } from "@modules/2DViewer/layers/framework/LayerManager/LayerManager"; +import { CACHE_TIME, STALE_TIME } from "@modules/2DViewer/layers/layers/_utils/queryConstants"; +import { cancelPromiseOnAbort } from "@modules/2DViewer/layers/layers/_utils/utils"; +import { SettingType } from "@modules/2DViewer/layers/settings/settingsTypes"; + +import { DrilledWellTrajectoriesSettings } from "./types"; + +import { DefineDependenciesArgs, SettingsContext } from "../../../interfaces"; +import { DrilledWellboresSetting } from "../../../settings/implementations/DrilledWellboresSetting"; +import { EnsembleSetting } from "../../../settings/implementations/EnsembleSetting"; + +export class DrilledWellTrajectoriesSettingsContext implements SettingsContext { + private _contextDelegate: SettingsContextDelegate; + + constructor(layerManager: LayerManager) { + this._contextDelegate = new SettingsContextDelegate< + DrilledWellTrajectoriesSettings, + keyof DrilledWellTrajectoriesSettings + >(this, layerManager, { + [SettingType.ENSEMBLE]: new EnsembleSetting(), + [SettingType.SMDA_WELLBORE_HEADERS]: new DrilledWellboresSetting(), + }); + } + + getDelegate(): SettingsContextDelegate { + return this._contextDelegate; + } + + getSettings() { + return this._contextDelegate.getSettings(); + } + + defineDependencies({ + helperDependency, + availableSettingsUpdater, + workbenchSession, + queryClient, + }: DefineDependenciesArgs) { + availableSettingsUpdater(SettingType.ENSEMBLE, ({ getGlobalSetting }) => { + const fieldIdentifier = getGlobalSetting("fieldId"); + const ensembles = getGlobalSetting("ensembles"); + + const ensembleIdents = ensembles + .filter((ensemble) => ensemble.getFieldIdentifier() === fieldIdentifier) + .map((ensemble) => ensemble.getIdent()); + + return ensembleIdents; + }); + + const wellboreHeadersDep = helperDependency(async function fetchData({ getLocalSetting, abortSignal }) { + const ensembleIdent = getLocalSetting(SettingType.ENSEMBLE); + + if (!ensembleIdent) { + return null; + } + + const ensembleSet = workbenchSession.getEnsembleSet(); + const ensemble = ensembleSet.findEnsemble(ensembleIdent); + + if (!ensemble) { + return null; + } + + const fieldIdentifier = ensemble.getFieldIdentifier(); + + return await queryClient.fetchQuery({ + queryKey: ["getDrilledWellboreHeaders", fieldIdentifier], + queryFn: () => + cancelPromiseOnAbort(apiService.well.getDrilledWellboreHeaders(fieldIdentifier), abortSignal), + staleTime: STALE_TIME, + gcTime: CACHE_TIME, + }); + }); + availableSettingsUpdater(SettingType.SMDA_WELLBORE_HEADERS, ({ getHelperDependency }) => { + const wellboreHeaders = getHelperDependency(wellboreHeadersDep); + + if (!wellboreHeaders) { + return []; + } + + return wellboreHeaders; + }); + } +} diff --git a/frontend/src/modules/2DViewer/layers/layers/implementations/DrilledWellTrajectoriesLayer/types.ts b/frontend/src/modules/2DViewer/layers/layers/implementations/DrilledWellTrajectoriesLayer/types.ts new file mode 100644 index 000000000..b8423380e --- /dev/null +++ b/frontend/src/modules/2DViewer/layers/layers/implementations/DrilledWellTrajectoriesLayer/types.ts @@ -0,0 +1,8 @@ +import { WellboreHeader_api } from "@api"; +import { EnsembleIdent } from "@framework/EnsembleIdent"; +import { SettingType } from "@modules/2DViewer/layers/settings/settingsTypes"; + +export type DrilledWellTrajectoriesSettings = { + [SettingType.ENSEMBLE]: EnsembleIdent | null; + [SettingType.SMDA_WELLBORE_HEADERS]: WellboreHeader_api[] | null; +}; diff --git a/frontend/src/modules/2DViewer/layers/layers/implementations/DrilledWellborePicksLayer/DrilledWellborePicksLayer.ts b/frontend/src/modules/2DViewer/layers/layers/implementations/DrilledWellborePicksLayer/DrilledWellborePicksLayer.ts new file mode 100644 index 000000000..fa06b0092 --- /dev/null +++ b/frontend/src/modules/2DViewer/layers/layers/implementations/DrilledWellborePicksLayer/DrilledWellborePicksLayer.ts @@ -0,0 +1,126 @@ +import { WellborePick_api } from "@api"; +import { apiService } from "@framework/ApiService"; +import { ItemDelegate } from "@modules/2DViewer/layers/delegates/ItemDelegate"; +import { LayerManager } from "@modules/2DViewer/layers/framework/LayerManager/LayerManager"; +import { LayerRegistry } from "@modules/2DViewer/layers/layers/LayerRegistry"; +import { SettingType } from "@modules/2DViewer/layers/settings/settingsTypes"; +import { QueryClient } from "@tanstack/react-query"; + +import { isEqual } from "lodash"; + +import { DrilledWellborePicksSettingsContext } from "./DrilledWellborePicksSettingsContext"; +import { DrilledWellborePicksSettings } from "./types"; + +import { LayerColoringType, LayerDelegate } from "../../../delegates/LayerDelegate"; +import { BoundingBox, Layer, SerializedLayer } from "../../../interfaces"; +import { CACHE_TIME, STALE_TIME } from "../../_utils/queryConstants"; + +export class DrilledWellborePicksLayer implements Layer { + private _layerDelegate: LayerDelegate; + private _itemDelegate: ItemDelegate; + + constructor(layerManager: LayerManager) { + this._itemDelegate = new ItemDelegate("Drilled Wellbore picks", layerManager); + this._layerDelegate = new LayerDelegate( + this, + layerManager, + new DrilledWellborePicksSettingsContext(layerManager), + LayerColoringType.NONE + ); + } + + getSettingsContext() { + return this._layerDelegate.getSettingsContext(); + } + + getItemDelegate(): ItemDelegate { + return this._itemDelegate; + } + + getLayerDelegate(): LayerDelegate { + return this._layerDelegate; + } + + doSettingsChangesRequireDataRefetch( + prevSettings: DrilledWellborePicksSettings, + newSettings: DrilledWellborePicksSettings + ): boolean { + return !isEqual(prevSettings, newSettings); + } + + makeBoundingBox(): BoundingBox | null { + const data = this._layerDelegate.getData(); + if (!data) { + return null; + } + + const bbox: BoundingBox = { + x: [Number.MAX_SAFE_INTEGER, Number.MIN_SAFE_INTEGER], + y: [Number.MAX_SAFE_INTEGER, Number.MIN_SAFE_INTEGER], + z: [Number.MAX_SAFE_INTEGER, Number.MIN_SAFE_INTEGER], + }; + + for (const trajectory of data) { + bbox.x[0] = Math.min(bbox.x[0], trajectory.easting); + bbox.x[1] = Math.max(bbox.x[1], trajectory.easting); + + bbox.y[0] = Math.min(bbox.y[0], trajectory.northing); + bbox.y[1] = Math.max(bbox.y[1], trajectory.northing); + + bbox.z[0] = Math.min(bbox.z[0], trajectory.tvdMsl); + bbox.z[1] = Math.max(bbox.z[1], trajectory.tvdMsl); + } + + return bbox; + } + + fetchData(queryClient: QueryClient): Promise { + const workbenchSession = this.getSettingsContext().getDelegate().getLayerManager().getWorkbenchSession(); + const ensembleSet = workbenchSession.getEnsembleSet(); + const settings = this.getSettingsContext().getDelegate().getSettings(); + const ensembleIdent = settings[SettingType.ENSEMBLE].getDelegate().getValue(); + const selectedWellboreHeaders = settings[SettingType.SMDA_WELLBORE_HEADERS].getDelegate().getValue(); + let selectedWellboreUuids: string[] = []; + if (selectedWellboreHeaders) { + selectedWellboreUuids = selectedWellboreHeaders.map((header) => header.wellboreUuid); + } + const selectedPickIdentifier = settings[SettingType.SURFACE_NAME].getDelegate().getValue(); + let fieldIdentifier: string | null = null; + if (ensembleIdent) { + const ensemble = ensembleSet.findEnsemble(ensembleIdent); + if (ensemble) { + fieldIdentifier = ensemble.getFieldIdentifier(); + } + } + + const queryKey = ["getWellborePicksForPickIdentifier", fieldIdentifier, selectedPickIdentifier]; + this._layerDelegate.registerQueryKey(queryKey); + + const promise = queryClient + .fetchQuery({ + queryKey, + queryFn: () => + apiService.well.getWellborePicksForPickIdentifier( + fieldIdentifier ?? "", + selectedPickIdentifier ?? "" + ), + staleTime: STALE_TIME, + gcTime: CACHE_TIME, + }) + .then((response: WellborePick_api[]) => { + return response.filter((trajectory) => selectedWellboreUuids.includes(trajectory.wellboreUuid)); + }); + + return promise; + } + + serializeState(): SerializedLayer { + return this._layerDelegate.serializeState(); + } + + deserializeState(state: SerializedLayer): void { + this._layerDelegate.deserializeState(state); + } +} + +LayerRegistry.registerLayer(DrilledWellborePicksLayer); diff --git a/frontend/src/modules/2DViewer/layers/layers/implementations/DrilledWellborePicksLayer/DrilledWellborePicksSettingsContext.ts b/frontend/src/modules/2DViewer/layers/layers/implementations/DrilledWellborePicksLayer/DrilledWellborePicksSettingsContext.ts new file mode 100644 index 000000000..e628151fc --- /dev/null +++ b/frontend/src/modules/2DViewer/layers/layers/implementations/DrilledWellborePicksLayer/DrilledWellborePicksSettingsContext.ts @@ -0,0 +1,136 @@ +import { apiService } from "@framework/ApiService"; +import { SettingsContextDelegate } from "@modules/2DViewer/layers/delegates/SettingsContextDelegate"; +import { LayerManager } from "@modules/2DViewer/layers/framework/LayerManager/LayerManager"; +import { SettingType } from "@modules/2DViewer/layers/settings/settingsTypes"; + +import { DrilledWellborePicksSettings } from "./types"; + +import { DefineDependenciesArgs, SettingsContext } from "../../../interfaces"; +import { DrilledWellboresSetting } from "../../../settings/implementations/DrilledWellboresSetting"; +import { EnsembleSetting } from "../../../settings/implementations/EnsembleSetting"; +import { SurfaceNameSetting } from "../../../settings/implementations/SurfaceNameSetting"; +import { CACHE_TIME, STALE_TIME } from "../../_utils/queryConstants"; +import { cancelPromiseOnAbort } from "../../_utils/utils"; + +export class DrilledWellborePicksSettingsContext implements SettingsContext { + private _contextDelegate: SettingsContextDelegate; + + constructor(layerManager: LayerManager) { + this._contextDelegate = new SettingsContextDelegate< + DrilledWellborePicksSettings, + keyof DrilledWellborePicksSettings + >(this, layerManager, { + [SettingType.ENSEMBLE]: new EnsembleSetting(), + [SettingType.SMDA_WELLBORE_HEADERS]: new DrilledWellboresSetting(), + [SettingType.SURFACE_NAME]: new SurfaceNameSetting(), + }); + } + + getDelegate(): SettingsContextDelegate { + return this._contextDelegate; + } + + getSettings() { + return this._contextDelegate.getSettings(); + } + + areCurrentSettingsValid(settings: DrilledWellborePicksSettings): boolean { + return ( + settings[SettingType.ENSEMBLE] !== null && + settings[SettingType.SMDA_WELLBORE_HEADERS] !== null && + settings[SettingType.SMDA_WELLBORE_HEADERS].length > 0 && + settings[SettingType.SURFACE_NAME] !== null + ); + } + + defineDependencies({ + helperDependency, + availableSettingsUpdater, + workbenchSession, + queryClient, + }: DefineDependenciesArgs) { + availableSettingsUpdater(SettingType.ENSEMBLE, ({ getGlobalSetting }) => { + const fieldIdentifier = getGlobalSetting("fieldId"); + const ensembles = getGlobalSetting("ensembles"); + + const ensembleIdents = ensembles + .filter((ensemble) => ensemble.getFieldIdentifier() === fieldIdentifier) + .map((ensemble) => ensemble.getIdent()); + + return ensembleIdents; + }); + + const wellboreHeadersDep = helperDependency(async function fetchData({ getLocalSetting, abortSignal }) { + const ensembleIdent = getLocalSetting(SettingType.ENSEMBLE); + + if (!ensembleIdent) { + return null; + } + + const ensembleSet = workbenchSession.getEnsembleSet(); + const ensemble = ensembleSet.findEnsemble(ensembleIdent); + + if (!ensemble) { + return null; + } + + const fieldIdentifier = ensemble.getFieldIdentifier(); + + return await queryClient.fetchQuery({ + queryKey: ["getDrilledWellboreHeaders", fieldIdentifier], + queryFn: () => + cancelPromiseOnAbort(apiService.well.getDrilledWellboreHeaders(fieldIdentifier), abortSignal), + staleTime: STALE_TIME, + gcTime: CACHE_TIME, + }); + }); + + const pickIdentifiersDep = helperDependency(async function fetchData({ getLocalSetting, abortSignal }) { + const ensembleIdent = getLocalSetting(SettingType.ENSEMBLE); + + if (!ensembleIdent) { + return null; + } + + const ensembleSet = workbenchSession.getEnsembleSet(); + const ensemble = ensembleSet.findEnsemble(ensembleIdent); + + if (!ensemble) { + return null; + } + + const stratColumnIdentifier = ensemble.getStratigraphicColumnIdentifier(); + + return await queryClient.fetchQuery({ + queryKey: ["getPickStratigraphy", stratColumnIdentifier], + queryFn: () => + cancelPromiseOnAbort( + apiService.well.getWellborePickIdentifiers(stratColumnIdentifier), + abortSignal + ), + staleTime: STALE_TIME, + gcTime: CACHE_TIME, + }); + }); + + availableSettingsUpdater(SettingType.SMDA_WELLBORE_HEADERS, ({ getHelperDependency }) => { + const wellboreHeaders = getHelperDependency(wellboreHeadersDep); + + if (!wellboreHeaders) { + return []; + } + + return wellboreHeaders; + }); + + availableSettingsUpdater(SettingType.SURFACE_NAME, ({ getHelperDependency }) => { + const pickIdentifiers = getHelperDependency(pickIdentifiersDep); + + if (!pickIdentifiers) { + return []; + } + + return pickIdentifiers; + }); + } +} diff --git a/frontend/src/modules/2DViewer/layers/layers/implementations/DrilledWellborePicksLayer/types.ts b/frontend/src/modules/2DViewer/layers/layers/implementations/DrilledWellborePicksLayer/types.ts new file mode 100644 index 000000000..1796ddad5 --- /dev/null +++ b/frontend/src/modules/2DViewer/layers/layers/implementations/DrilledWellborePicksLayer/types.ts @@ -0,0 +1,9 @@ +import { WellboreHeader_api } from "@api"; +import { EnsembleIdent } from "@framework/EnsembleIdent"; +import { SettingType } from "@modules/2DViewer/layers/settings/settingsTypes"; + +export type DrilledWellborePicksSettings = { + [SettingType.ENSEMBLE]: EnsembleIdent | null; + [SettingType.SMDA_WELLBORE_HEADERS]: WellboreHeader_api[] | null; + [SettingType.SURFACE_NAME]: string | null; +}; diff --git a/frontend/src/modules/2DViewer/layers/layers/implementations/ObservedSurfaceLayer/ObservedSurfaceLayer.ts b/frontend/src/modules/2DViewer/layers/layers/implementations/ObservedSurfaceLayer/ObservedSurfaceLayer.ts new file mode 100644 index 000000000..ebfdc7f0b --- /dev/null +++ b/frontend/src/modules/2DViewer/layers/layers/implementations/ObservedSurfaceLayer/ObservedSurfaceLayer.ts @@ -0,0 +1,124 @@ +import { SurfaceDataPng_api } from "@api"; +import { apiService } from "@framework/ApiService"; +import { ItemDelegate } from "@modules/2DViewer/layers/delegates/ItemDelegate"; +import { LayerManager } from "@modules/2DViewer/layers/framework/LayerManager/LayerManager"; +import { LayerRegistry } from "@modules/2DViewer/layers/layers/LayerRegistry"; +import { CACHE_TIME, STALE_TIME } from "@modules/2DViewer/layers/layers/_utils/queryConstants"; +import { SettingType } from "@modules/2DViewer/layers/settings/settingsTypes"; +import { FullSurfaceAddress, SurfaceAddressBuilder } from "@modules/_shared/Surface"; +import { SurfaceDataFloat_trans, transformSurfaceData } from "@modules/_shared/Surface/queryDataTransforms"; +import { encodeSurfAddrStr } from "@modules/_shared/Surface/surfaceAddress"; +import { QueryClient } from "@tanstack/react-query"; + +import { isEqual } from "lodash"; + +import { ObservedSurfaceSettingsContext } from "./ObservedSurfaceSettingsContext"; +import { ObservedSurfaceSettings } from "./types"; + +import { LayerColoringType, LayerDelegate } from "../../../delegates/LayerDelegate"; +import { BoundingBox, Layer, SerializedLayer } from "../../../interfaces"; + +export class ObservedSurfaceLayer + implements Layer +{ + private _layerDelegate: LayerDelegate; + private _itemDelegate: ItemDelegate; + + constructor(layerManager: LayerManager) { + this._itemDelegate = new ItemDelegate("Observed Surface", layerManager); + this._layerDelegate = new LayerDelegate( + this, + layerManager, + new ObservedSurfaceSettingsContext(layerManager), + LayerColoringType.COLORSCALE + ); + } + + getSettingsContext() { + return this._layerDelegate.getSettingsContext(); + } + + getItemDelegate(): ItemDelegate { + return this._itemDelegate; + } + + getLayerDelegate(): LayerDelegate { + return this._layerDelegate; + } + + doSettingsChangesRequireDataRefetch( + prevSettings: ObservedSurfaceSettings, + newSettings: ObservedSurfaceSettings + ): boolean { + return !isEqual(prevSettings, newSettings); + } + + makeBoundingBox(): BoundingBox | null { + const data = this._layerDelegate.getData(); + if (!data) { + return null; + } + + return { + x: [data.transformed_bbox_utm.min_x, data.transformed_bbox_utm.max_x], + y: [data.transformed_bbox_utm.min_y, data.transformed_bbox_utm.max_y], + z: [0, 0], + }; + } + + makeValueRange(): [number, number] | null { + const data = this._layerDelegate.getData(); + if (!data) { + return null; + } + + return [data.value_min, data.value_max]; + } + + fetchData(queryClient: QueryClient): Promise { + let surfaceAddress: FullSurfaceAddress | null = null; + const addrBuilder = new SurfaceAddressBuilder(); + + const settings = this.getSettingsContext().getDelegate().getSettings(); + const ensembleIdent = settings[SettingType.ENSEMBLE].getDelegate().getValue(); + const surfaceName = settings[SettingType.SURFACE_NAME].getDelegate().getValue(); + const attribute = settings[SettingType.SURFACE_ATTRIBUTE].getDelegate().getValue(); + const timeOrInterval = settings[SettingType.TIME_OR_INTERVAL].getDelegate().getValue(); + + if (ensembleIdent && surfaceName && attribute && timeOrInterval) { + addrBuilder.withEnsembleIdent(ensembleIdent); + addrBuilder.withName(surfaceName); + addrBuilder.withAttribute(attribute); + addrBuilder.withTimeOrInterval(timeOrInterval); + + surfaceAddress = addrBuilder.buildObservedAddress(); + } + + const surfAddrStr = surfaceAddress ? encodeSurfAddrStr(surfaceAddress) : null; + + const queryKey = ["getSurfaceData", surfAddrStr, null, "png"]; + + this._layerDelegate.registerQueryKey(queryKey); + + const promise = queryClient + .fetchQuery({ + queryKey, + queryFn: () => apiService.surface.getSurfaceData(surfAddrStr ?? "", "png", null), + staleTime: STALE_TIME, + gcTime: CACHE_TIME, + }) + .then((data) => transformSurfaceData(data)); + + return promise; + } + + serializeState(): SerializedLayer { + return this._layerDelegate.serializeState(); + } + + deserializeState(serializedState: SerializedLayer): void { + this._layerDelegate.deserializeState(serializedState); + } +} + +LayerRegistry.registerLayer(ObservedSurfaceLayer); diff --git a/frontend/src/modules/2DViewer/layers/layers/implementations/ObservedSurfaceLayer/ObservedSurfaceSettingsContext.ts b/frontend/src/modules/2DViewer/layers/layers/implementations/ObservedSurfaceLayer/ObservedSurfaceSettingsContext.ts new file mode 100644 index 000000000..7ea07106a --- /dev/null +++ b/frontend/src/modules/2DViewer/layers/layers/implementations/ObservedSurfaceLayer/ObservedSurfaceSettingsContext.ts @@ -0,0 +1,143 @@ +import { SurfaceTimeType_api } from "@api"; +import { apiService } from "@framework/ApiService"; +import { SettingsContextDelegate } from "@modules/2DViewer/layers/delegates/SettingsContextDelegate"; +import { LayerManager } from "@modules/2DViewer/layers/framework/LayerManager/LayerManager"; +import { CACHE_TIME, STALE_TIME } from "@modules/2DViewer/layers/layers/_utils/queryConstants"; +import { cancelPromiseOnAbort } from "@modules/2DViewer/layers/layers/_utils/utils"; +import { SettingType } from "@modules/2DViewer/layers/settings/settingsTypes"; + +import { ObservedSurfaceSettings } from "./types"; + +import { DefineDependenciesArgs, SettingsContext } from "../../../interfaces"; +import { EnsembleSetting } from "../../../settings/implementations/EnsembleSetting"; +import { SurfaceAttributeSetting } from "../../../settings/implementations/SurfaceAttributeSetting"; +import { SurfaceNameSetting } from "../../../settings/implementations/SurfaceNameSetting"; +import { TimeOrIntervalSetting } from "../../../settings/implementations/TimeOrIntervalSetting"; + +export class ObservedSurfaceSettingsContext implements SettingsContext { + private _contextDelegate: SettingsContextDelegate; + + constructor(layerManager: LayerManager) { + this._contextDelegate = new SettingsContextDelegate( + this, + layerManager, + { + [SettingType.ENSEMBLE]: new EnsembleSetting(), + [SettingType.SURFACE_ATTRIBUTE]: new SurfaceAttributeSetting(), + [SettingType.SURFACE_NAME]: new SurfaceNameSetting(), + [SettingType.TIME_OR_INTERVAL]: new TimeOrIntervalSetting(), + } + ); + } + + getDelegate(): SettingsContextDelegate { + return this._contextDelegate; + } + + getSettings() { + return this._contextDelegate.getSettings(); + } + defineDependencies({ + helperDependency, + availableSettingsUpdater, + workbenchSession, + queryClient, + }: DefineDependenciesArgs) { + availableSettingsUpdater(SettingType.ENSEMBLE, ({ getGlobalSetting }) => { + const fieldIdentifier = getGlobalSetting("fieldId"); + const ensembleSet = workbenchSession.getEnsembleSet(); + + const ensembleIdents = ensembleSet + .getEnsembleArr() + .filter((ensemble) => ensemble.getFieldIdentifier() === fieldIdentifier) + .map((ensemble) => ensemble.getIdent()); + + return ensembleIdents; + }); + + const observedSurfaceMetadataDep = helperDependency(async ({ getLocalSetting, abortSignal }) => { + const ensembleIdent = getLocalSetting(SettingType.ENSEMBLE); + + if (!ensembleIdent) { + return null; + } + + return await queryClient.fetchQuery({ + queryKey: ["getObservedSurfacesMetadata", ensembleIdent.getCaseUuid()], + queryFn: () => + cancelPromiseOnAbort( + apiService.surface.getObservedSurfacesMetadata(ensembleIdent.getCaseUuid()), + abortSignal + ), + staleTime: STALE_TIME, + gcTime: CACHE_TIME, + }); + }); + + availableSettingsUpdater(SettingType.SURFACE_ATTRIBUTE, ({ getHelperDependency }) => { + const data = getHelperDependency(observedSurfaceMetadataDep); + + if (!data) { + return []; + } + + const availableAttributes = [ + ...Array.from(new Set(data.surfaces.map((surface) => surface.attribute_name))), + ]; + + return availableAttributes; + }); + + availableSettingsUpdater(SettingType.SURFACE_NAME, ({ getHelperDependency, getLocalSetting }) => { + const attribute = getLocalSetting(SettingType.SURFACE_ATTRIBUTE); + const data = getHelperDependency(observedSurfaceMetadataDep); + + if (!attribute || !data) { + return []; + } + + const availableSurfaceNames = [ + ...Array.from( + new Set( + data.surfaces.filter((surface) => surface.attribute_name === attribute).map((el) => el.name) + ) + ), + ]; + + return availableSurfaceNames; + }); + + availableSettingsUpdater(SettingType.TIME_OR_INTERVAL, ({ getLocalSetting, getHelperDependency }) => { + const attribute = getLocalSetting(SettingType.SURFACE_ATTRIBUTE); + const surfaceName = getLocalSetting(SettingType.SURFACE_NAME); + const data = getHelperDependency(observedSurfaceMetadataDep); + + if (!attribute || !surfaceName || !data) { + return []; + } + + const availableTimeOrIntervals: string[] = []; + const availableTimeTypes = [ + ...Array.from( + new Set( + data.surfaces + .filter((surface) => surface.attribute_name === attribute && surface.name === surfaceName) + .map((el) => el.time_type) + ) + ), + ]; + + if (availableTimeTypes.includes(SurfaceTimeType_api.NO_TIME)) { + availableTimeOrIntervals.push(SurfaceTimeType_api.NO_TIME); + } + if (availableTimeTypes.includes(SurfaceTimeType_api.TIME_POINT)) { + availableTimeOrIntervals.push(...data.time_points_iso_str); + } + if (availableTimeTypes.includes(SurfaceTimeType_api.INTERVAL)) { + availableTimeOrIntervals.push(...data.time_intervals_iso_str); + } + + return availableTimeOrIntervals; + }); + } +} diff --git a/frontend/src/modules/2DViewer/layers/layers/implementations/ObservedSurfaceLayer/types.ts b/frontend/src/modules/2DViewer/layers/layers/implementations/ObservedSurfaceLayer/types.ts new file mode 100644 index 000000000..a4bc063f3 --- /dev/null +++ b/frontend/src/modules/2DViewer/layers/layers/implementations/ObservedSurfaceLayer/types.ts @@ -0,0 +1,9 @@ +import { EnsembleIdent } from "@framework/EnsembleIdent"; +import { SettingType } from "@modules/2DViewer/layers/settings/settingsTypes"; + +export type ObservedSurfaceSettings = { + [SettingType.ENSEMBLE]: EnsembleIdent | null; + [SettingType.SURFACE_ATTRIBUTE]: string | null; + [SettingType.SURFACE_NAME]: string | null; + [SettingType.TIME_OR_INTERVAL]: string | null; +}; diff --git a/frontend/src/modules/2DViewer/layers/layers/implementations/RealizationGridLayer/RealizationGridLayer.ts b/frontend/src/modules/2DViewer/layers/layers/implementations/RealizationGridLayer/RealizationGridLayer.ts new file mode 100644 index 000000000..efb001eda --- /dev/null +++ b/frontend/src/modules/2DViewer/layers/layers/implementations/RealizationGridLayer/RealizationGridLayer.ts @@ -0,0 +1,204 @@ +import { apiService } from "@framework/ApiService"; +import { ItemDelegate } from "@modules/2DViewer/layers/delegates/ItemDelegate"; +import { LayerManager } from "@modules/2DViewer/layers/framework/LayerManager/LayerManager"; +import { LayerRegistry } from "@modules/2DViewer/layers/layers/LayerRegistry"; +import { CACHE_TIME, STALE_TIME } from "@modules/2DViewer/layers/layers/_utils/queryConstants"; +import { SettingType } from "@modules/2DViewer/layers/settings/settingsTypes"; +import { + GridMappedProperty_trans, + GridSurface_trans, + transformGridMappedProperty, + transformGridSurface, +} from "@modules/3DViewer/view/queries/queryDataTransforms"; +import { QueryClient } from "@tanstack/react-query"; + +import { isEqual } from "lodash"; + +import { RealizationGridSettingsContext } from "./RealizationGridSettingsContext"; +import { RealizationGridSettings } from "./types"; + +import { LayerColoringType, LayerDelegate } from "../../../delegates/LayerDelegate"; +import { BoundingBox, Layer, SerializedLayer } from "../../../interfaces"; + +export class RealizationGridLayer + implements + Layer< + RealizationGridSettings, + { + gridSurfaceData: GridSurface_trans; + gridParameterData: GridMappedProperty_trans; + } + > +{ + private _layerDelegate: LayerDelegate< + RealizationGridSettings, + { + gridSurfaceData: GridSurface_trans; + gridParameterData: GridMappedProperty_trans; + } + >; + private _itemDelegate: ItemDelegate; + + constructor(layerManager: LayerManager) { + this._itemDelegate = new ItemDelegate("Realization Grid layer", layerManager); + this._layerDelegate = new LayerDelegate( + this, + layerManager, + new RealizationGridSettingsContext(layerManager), + LayerColoringType.COLORSCALE + ); + } + + getSettingsContext() { + return this._layerDelegate.getSettingsContext(); + } + + getItemDelegate(): ItemDelegate { + return this._itemDelegate; + } + + getLayerDelegate(): LayerDelegate< + RealizationGridSettings, + { + gridSurfaceData: GridSurface_trans; + gridParameterData: GridMappedProperty_trans; + } + > { + return this._layerDelegate; + } + + doSettingsChangesRequireDataRefetch( + prevSettings: RealizationGridSettings, + newSettings: RealizationGridSettings + ): boolean { + return !isEqual(prevSettings, newSettings); + } + + makeBoundingBox(): BoundingBox | null { + const data = this._layerDelegate.getData(); + if (!data) { + return null; + } + + return { + x: [ + data.gridSurfaceData.origin_utm_x + data.gridSurfaceData.xmin, + data.gridSurfaceData.origin_utm_x + data.gridSurfaceData.xmax, + ], + y: [ + data.gridSurfaceData.origin_utm_y + data.gridSurfaceData.ymin, + data.gridSurfaceData.origin_utm_y + data.gridSurfaceData.ymax, + ], + z: [data.gridSurfaceData.zmin, data.gridSurfaceData.zmax], + }; + } + + makeValueRange(): [number, number] | null { + const data = this._layerDelegate.getData(); + if (!data) { + return null; + } + + return [data.gridParameterData.min_grid_prop_value, data.gridParameterData.max_grid_prop_value]; + } + + fetchData(queryClient: QueryClient): Promise<{ + gridSurfaceData: GridSurface_trans; + gridParameterData: GridMappedProperty_trans; + }> { + const settings = this.getSettingsContext().getDelegate().getSettings(); + const ensembleIdent = settings[SettingType.ENSEMBLE].getDelegate().getValue(); + const realizationNum = settings[SettingType.REALIZATION].getDelegate().getValue(); + const gridName = settings[SettingType.GRID_NAME].getDelegate().getValue(); + const attribute = settings[SettingType.GRID_ATTRIBUTE].getDelegate().getValue(); + let timeOrInterval = settings[SettingType.TIME_OR_INTERVAL].getDelegate().getValue(); + if (timeOrInterval === "NO_TIME") { + timeOrInterval = null; + } + let availableDimensions = settings[SettingType.GRID_LAYER].getDelegate().getAvailableValues(); + if (!availableDimensions.length || availableDimensions[0] === null) { + availableDimensions = [0, 0, 0]; + } + const layerIndex = settings[SettingType.GRID_LAYER].getDelegate().getValue(); + const iMin = 0; + const iMax = availableDimensions[0] || 0; + const jMin = 0; + const jMax = availableDimensions[1] || 0; + const kMin = layerIndex || 0; + const kMax = layerIndex || 0; + const queryKey = [ + "gridParameter", + ensembleIdent, + gridName, + attribute, + timeOrInterval, + realizationNum, + iMin, + iMax, + jMin, + jMax, + kMin, + kMax, + ]; + this._layerDelegate.registerQueryKey(queryKey); + + const gridParameterPromise = queryClient + .fetchQuery({ + queryKey, + queryFn: () => + apiService.grid3D.gridParameter( + ensembleIdent?.getCaseUuid() ?? "", + ensembleIdent?.getEnsembleName() ?? "", + gridName ?? "", + attribute ?? "", + realizationNum ?? 0, + timeOrInterval, + iMin, + iMax - 1, + jMin, + jMax - 1, + kMin, + kMax + ), + staleTime: STALE_TIME, + gcTime: CACHE_TIME, + }) + .then(transformGridMappedProperty); + + const gridSurfacePromise = queryClient + .fetchQuery({ + queryKey: ["getGridData", ensembleIdent, gridName, realizationNum, iMin, iMax, jMin, jMax, kMin, kMax], + queryFn: () => + apiService.grid3D.gridSurface( + ensembleIdent?.getCaseUuid() ?? "", + ensembleIdent?.getEnsembleName() ?? "", + gridName ?? "", + realizationNum ?? 0, + iMin, + iMax - 1, + jMin, + jMax - 1, + kMin, + kMax + ), + staleTime: STALE_TIME, + gcTime: CACHE_TIME, + }) + .then(transformGridSurface); + + return Promise.all([gridSurfacePromise, gridParameterPromise]).then(([gridSurfaceData, gridParameterData]) => ({ + gridSurfaceData, + gridParameterData, + })); + } + + serializeState(): SerializedLayer { + return this._layerDelegate.serializeState(); + } + + deserializeState(serializedState: SerializedLayer): void { + this._layerDelegate.deserializeState(serializedState); + } +} + +LayerRegistry.registerLayer(RealizationGridLayer); diff --git a/frontend/src/modules/2DViewer/layers/layers/implementations/RealizationGridLayer/RealizationGridSettingsContext.ts b/frontend/src/modules/2DViewer/layers/layers/implementations/RealizationGridLayer/RealizationGridSettingsContext.ts new file mode 100644 index 000000000..bd53b1a0c --- /dev/null +++ b/frontend/src/modules/2DViewer/layers/layers/implementations/RealizationGridLayer/RealizationGridSettingsContext.ts @@ -0,0 +1,180 @@ +import { apiService } from "@framework/ApiService"; +import { SettingsContextDelegate } from "@modules/2DViewer/layers/delegates/SettingsContextDelegate"; +import { LayerManager } from "@modules/2DViewer/layers/framework/LayerManager/LayerManager"; +import { CACHE_TIME, STALE_TIME } from "@modules/2DViewer/layers/layers/_utils/queryConstants"; +import { cancelQueryOnAbort } from "@modules/2DViewer/layers/layers/_utils/utils"; +import { SettingType } from "@modules/2DViewer/layers/settings/settingsTypes"; + +import { RealizationGridSettings } from "./types"; + +import { DefineDependenciesArgs, SettingsContext } from "../../../interfaces"; +import { EnsembleSetting } from "../../../settings/implementations/EnsembleSetting"; +import { GridAttributeSetting } from "../../../settings/implementations/GridAttributeSetting"; +import { GridLayerSetting } from "../../../settings/implementations/GridLayerSetting"; +import { GridNameSetting } from "../../../settings/implementations/GridNameSetting"; +import { RealizationSetting } from "../../../settings/implementations/RealizationSetting"; +import { ShowGridLinesSetting } from "../../../settings/implementations/ShowGridLinesSetting"; +import { TimeOrIntervalSetting } from "../../../settings/implementations/TimeOrIntervalSetting"; + +export class RealizationGridSettingsContext implements SettingsContext { + private _contextDelegate: SettingsContextDelegate; + + constructor(layerManager: LayerManager) { + this._contextDelegate = new SettingsContextDelegate( + this, + layerManager, + { + [SettingType.ENSEMBLE]: new EnsembleSetting(), + [SettingType.REALIZATION]: new RealizationSetting(), + [SettingType.GRID_NAME]: new GridNameSetting(), + [SettingType.GRID_ATTRIBUTE]: new GridAttributeSetting(), + [SettingType.GRID_LAYER]: new GridLayerSetting(), + [SettingType.TIME_OR_INTERVAL]: new TimeOrIntervalSetting(), + [SettingType.SHOW_GRID_LINES]: new ShowGridLinesSetting(), + } + ); + } + + areCurrentSettingsValid(settings: RealizationGridSettings): boolean { + return ( + settings[SettingType.ENSEMBLE] !== null && + settings[SettingType.REALIZATION] !== null && + settings[SettingType.GRID_NAME] !== null && + settings[SettingType.GRID_ATTRIBUTE] !== null && + settings[SettingType.GRID_LAYER] !== null && + settings[SettingType.TIME_OR_INTERVAL] !== null + ); + } + + getDelegate(): SettingsContextDelegate { + return this._contextDelegate; + } + + getSettings() { + return this._contextDelegate.getSettings(); + } + + defineDependencies({ + helperDependency, + availableSettingsUpdater, + queryClient, + }: DefineDependenciesArgs) { + availableSettingsUpdater(SettingType.ENSEMBLE, ({ getGlobalSetting }) => { + const fieldIdentifier = getGlobalSetting("fieldId"); + const ensembles = getGlobalSetting("ensembles"); + + const ensembleIdents = ensembles + .filter((ensemble) => ensemble.getFieldIdentifier() === fieldIdentifier) + .map((ensemble) => ensemble.getIdent()); + + return ensembleIdents; + }); + + availableSettingsUpdater(SettingType.REALIZATION, ({ getLocalSetting, getGlobalSetting }) => { + const ensembleIdent = getLocalSetting(SettingType.ENSEMBLE); + const realizationFilterFunc = getGlobalSetting("realizationFilterFunction"); + + if (!ensembleIdent) { + return []; + } + + const realizations = realizationFilterFunc(ensembleIdent); + + return [...realizations]; + }); + const realizationGridDataDep = helperDependency(async ({ getLocalSetting, abortSignal }) => { + const ensembleIdent = getLocalSetting(SettingType.ENSEMBLE); + const realization = getLocalSetting(SettingType.REALIZATION); + + if (!ensembleIdent || realization === null) { + return null; + } + + return await cancelQueryOnAbort(queryClient, abortSignal, { + queryKey: ["getRealizationGridMetadata", ensembleIdent, realization], + queryFn: () => + apiService.grid3D.getGridModelsInfo( + ensembleIdent.getCaseUuid(), + ensembleIdent.getEnsembleName(), + realization + ), + staleTime: STALE_TIME, + gcTime: CACHE_TIME, + }); + }); + + availableSettingsUpdater(SettingType.GRID_NAME, ({ getHelperDependency }) => { + const data = getHelperDependency(realizationGridDataDep); + + if (!data) { + return []; + } + + const availableGridNames = [...Array.from(new Set(data.map((gridModelInfo) => gridModelInfo.grid_name)))]; + + return availableGridNames; + }); + + availableSettingsUpdater(SettingType.GRID_ATTRIBUTE, ({ getLocalSetting, getHelperDependency }) => { + const gridName = getLocalSetting(SettingType.GRID_NAME); + const data = getHelperDependency(realizationGridDataDep); + + if (!gridName || !data) { + return []; + } + + const gridAttributeArr = + data.find((gridModel) => gridModel.grid_name === gridName)?.property_info_arr ?? []; + + const availableGridAttributes = [ + ...Array.from(new Set(gridAttributeArr.map((gridAttribute) => gridAttribute.property_name))), + ]; + + return availableGridAttributes; + }); + + availableSettingsUpdater(SettingType.GRID_LAYER, ({ getLocalSetting, getHelperDependency }) => { + const gridName = getLocalSetting(SettingType.GRID_NAME); + const data = getHelperDependency(realizationGridDataDep); + + if (!gridName || !data) { + return []; + } + + const gridDimensions = data.find((gridModel) => gridModel.grid_name === gridName)?.dimensions ?? null; + const availableGridLayers: number[] = []; + if (gridDimensions) { + availableGridLayers.push(gridDimensions.i_count); + availableGridLayers.push(gridDimensions.j_count); + availableGridLayers.push(gridDimensions.k_count); + } + + return availableGridLayers; + }); + + availableSettingsUpdater(SettingType.TIME_OR_INTERVAL, ({ getLocalSetting, getHelperDependency }) => { + const gridName = getLocalSetting(SettingType.GRID_NAME); + const gridAttribute = getLocalSetting(SettingType.GRID_ATTRIBUTE); + const data = getHelperDependency(realizationGridDataDep); + + if (!gridName || !gridAttribute || !data) { + return []; + } + + const gridAttributeArr = + data.find((gridModel) => gridModel.grid_name === gridName)?.property_info_arr ?? []; + + const availableTimeOrIntervals = [ + ...Array.from( + new Set( + gridAttributeArr + .filter((attr) => attr.property_name === gridAttribute) + .map((gridAttribute) => gridAttribute.iso_date_or_interval ?? "NO_TIME") + ) + ), + ]; + + return availableTimeOrIntervals; + }); + } +} diff --git a/frontend/src/modules/2DViewer/layers/layers/implementations/RealizationGridLayer/types.ts b/frontend/src/modules/2DViewer/layers/layers/implementations/RealizationGridLayer/types.ts new file mode 100644 index 000000000..2ac9df1c2 --- /dev/null +++ b/frontend/src/modules/2DViewer/layers/layers/implementations/RealizationGridLayer/types.ts @@ -0,0 +1,12 @@ +import { EnsembleIdent } from "@framework/EnsembleIdent"; +import { SettingType } from "@modules/2DViewer/layers/settings/settingsTypes"; + +export type RealizationGridSettings = { + [SettingType.ENSEMBLE]: EnsembleIdent | null; + [SettingType.REALIZATION]: number | null; + [SettingType.GRID_ATTRIBUTE]: string | null; + [SettingType.GRID_NAME]: string | null; + [SettingType.GRID_LAYER]: number | null; + [SettingType.TIME_OR_INTERVAL]: string | null; + [SettingType.SHOW_GRID_LINES]: boolean; +}; diff --git a/frontend/src/modules/2DViewer/layers/layers/implementations/RealizationPolygonsLayer/RealizationPolygonsLayer.ts b/frontend/src/modules/2DViewer/layers/layers/implementations/RealizationPolygonsLayer/RealizationPolygonsLayer.ts new file mode 100644 index 000000000..7df1d8210 --- /dev/null +++ b/frontend/src/modules/2DViewer/layers/layers/implementations/RealizationPolygonsLayer/RealizationPolygonsLayer.ts @@ -0,0 +1,124 @@ +import { PolygonData_api } from "@api"; +import { apiService } from "@framework/ApiService"; +import { ItemDelegate } from "@modules/2DViewer/layers/delegates/ItemDelegate"; +import { LayerColoringType, LayerDelegate } from "@modules/2DViewer/layers/delegates/LayerDelegate"; +import { LayerManager } from "@modules/2DViewer/layers/framework/LayerManager/LayerManager"; +import { LayerRegistry } from "@modules/2DViewer/layers/layers/LayerRegistry"; +import { CACHE_TIME, STALE_TIME } from "@modules/2DViewer/layers/layers/_utils/queryConstants"; +import { SettingType } from "@modules/2DViewer/layers/settings/settingsTypes"; +import { QueryClient } from "@tanstack/react-query"; + +import { isEqual } from "lodash"; + +import { RealizationPolygonsSettingsContext } from "./RealizationPolygonsSettingsContext"; +import { RealizationPolygonsSettings } from "./types"; + +import { BoundingBox, Layer, SerializedLayer } from "../../../interfaces"; + +export class RealizationPolygonsLayer implements Layer { + private _layerDelegate: LayerDelegate; + private _itemDelegate: ItemDelegate; + + constructor(layerManager: LayerManager) { + this._itemDelegate = new ItemDelegate("Realization Polygons", layerManager); + this._layerDelegate = new LayerDelegate( + this, + layerManager, + new RealizationPolygonsSettingsContext(layerManager), + LayerColoringType.NONE + ); + } + + getSettingsContext() { + return this._layerDelegate.getSettingsContext(); + } + + getItemDelegate(): ItemDelegate { + return this._itemDelegate; + } + + getLayerDelegate(): LayerDelegate { + return this._layerDelegate; + } + + doSettingsChangesRequireDataRefetch( + prevSettings: RealizationPolygonsSettings, + newSettings: RealizationPolygonsSettings + ): boolean { + return !isEqual(prevSettings, newSettings); + } + + makeBoundingBox(): BoundingBox | null { + const data = this._layerDelegate.getData(); + if (!data) { + return null; + } + + const bbox: BoundingBox = { + x: [Number.POSITIVE_INFINITY, Number.NEGATIVE_INFINITY], + y: [Number.POSITIVE_INFINITY, Number.NEGATIVE_INFINITY], + z: [Number.POSITIVE_INFINITY, Number.NEGATIVE_INFINITY], + }; + + for (const polygon of data) { + for (const point of polygon.x_arr) { + bbox.x[0] = Math.min(bbox.x[0], point); + bbox.x[1] = Math.max(bbox.x[1], point); + } + for (const point of polygon.y_arr) { + bbox.y[0] = Math.min(bbox.y[0], point); + bbox.y[1] = Math.max(bbox.y[1], point); + } + for (const point of polygon.z_arr) { + bbox.z[0] = Math.min(bbox.z[0], point); + bbox.z[1] = Math.max(bbox.z[1], point); + } + } + + return bbox; + } + + fetchData(queryClient: QueryClient): Promise { + const settings = this.getSettingsContext().getDelegate().getSettings(); + const ensembleIdent = settings[SettingType.ENSEMBLE].getDelegate().getValue(); + const realizationNum = settings[SettingType.REALIZATION].getDelegate().getValue(); + const polygonsName = settings[SettingType.POLYGONS_NAME].getDelegate().getValue(); + const polygonsAttribute = settings[SettingType.POLYGONS_ATTRIBUTE].getDelegate().getValue(); + + const queryKey = [ + "getPolygonsData", + ensembleIdent?.getCaseUuid() ?? "", + ensembleIdent?.getEnsembleName() ?? "", + realizationNum ?? 0, + polygonsName ?? "", + polygonsAttribute ?? "", + ]; + this._layerDelegate.registerQueryKey(queryKey); + + const promise = queryClient.fetchQuery({ + queryKey, + queryFn: () => + apiService.polygons.getPolygonsData( + ensembleIdent?.getCaseUuid() ?? "", + ensembleIdent?.getEnsembleName() ?? "", + realizationNum ?? 0, + polygonsName ?? "", + polygonsAttribute ?? "" + ), + staleTime: STALE_TIME, + gcTime: CACHE_TIME, + }); + + return promise; + } + + serializeState(): SerializedLayer { + return this._layerDelegate.serializeState(); + } + + deserializeState(serializedState: SerializedLayer): void { + this._layerDelegate.deserializeState(serializedState); + } +} + +LayerRegistry.registerLayer(RealizationPolygonsLayer); diff --git a/frontend/src/modules/2DViewer/layers/layers/implementations/RealizationPolygonsLayer/RealizationPolygonsSettingsContext.ts b/frontend/src/modules/2DViewer/layers/layers/implementations/RealizationPolygonsLayer/RealizationPolygonsSettingsContext.ts new file mode 100644 index 000000000..e736b165d --- /dev/null +++ b/frontend/src/modules/2DViewer/layers/layers/implementations/RealizationPolygonsLayer/RealizationPolygonsSettingsContext.ts @@ -0,0 +1,123 @@ +import { apiService } from "@framework/ApiService"; +import { SettingsContextDelegate } from "@modules/2DViewer/layers/delegates/SettingsContextDelegate"; +import { LayerManager } from "@modules/2DViewer/layers/framework/LayerManager/LayerManager"; +import { CACHE_TIME, STALE_TIME } from "@modules/2DViewer/layers/layers/_utils/queryConstants"; +import { cancelPromiseOnAbort } from "@modules/2DViewer/layers/layers/_utils/utils"; +import { SettingType } from "@modules/2DViewer/layers/settings/settingsTypes"; + +import { RealizationPolygonsSettings } from "./types"; + +import { DefineDependenciesArgs, SettingsContext } from "../../../interfaces"; +import { EnsembleSetting } from "../../../settings/implementations/EnsembleSetting"; +import { PolygonsAttributeSetting } from "../../../settings/implementations/PolygonsAttributeSetting"; +import { PolygonsNameSetting } from "../../../settings/implementations/PolygonsNameSetting"; +import { RealizationSetting } from "../../../settings/implementations/RealizationSetting"; + +export class RealizationPolygonsSettingsContext implements SettingsContext { + private _contextDelegate: SettingsContextDelegate; + + constructor(layerManager: LayerManager) { + this._contextDelegate = new SettingsContextDelegate< + RealizationPolygonsSettings, + keyof RealizationPolygonsSettings + >(this, layerManager, { + [SettingType.ENSEMBLE]: new EnsembleSetting(), + [SettingType.REALIZATION]: new RealizationSetting(), + [SettingType.POLYGONS_ATTRIBUTE]: new PolygonsAttributeSetting(), + [SettingType.POLYGONS_NAME]: new PolygonsNameSetting(), + }); + } + + getDelegate(): SettingsContextDelegate { + return this._contextDelegate; + } + + getSettings() { + return this._contextDelegate.getSettings(); + } + + defineDependencies({ + helperDependency, + availableSettingsUpdater, + queryClient, + }: DefineDependenciesArgs) { + availableSettingsUpdater(SettingType.ENSEMBLE, ({ getGlobalSetting }) => { + const fieldIdentifier = getGlobalSetting("fieldId"); + const ensembles = getGlobalSetting("ensembles"); + + const ensembleIdents = ensembles + .filter((ensemble) => ensemble.getFieldIdentifier() === fieldIdentifier) + .map((ensemble) => ensemble.getIdent()); + + return ensembleIdents; + }); + + availableSettingsUpdater(SettingType.REALIZATION, ({ getLocalSetting, getGlobalSetting }) => { + const ensembleIdent = getLocalSetting(SettingType.ENSEMBLE); + const realizationFilterFunc = getGlobalSetting("realizationFilterFunction"); + + if (!ensembleIdent) { + return []; + } + + const realizations = realizationFilterFunc(ensembleIdent); + + return [...realizations]; + }); + + const realizationPolygonsMetadataDep = helperDependency(async ({ getLocalSetting, abortSignal }) => { + const ensembleIdent = getLocalSetting(SettingType.ENSEMBLE); + + if (!ensembleIdent) { + return null; + } + + return await queryClient.fetchQuery({ + queryKey: ["getRealizationPolygonsMetadata", ensembleIdent], + queryFn: () => + cancelPromiseOnAbort( + apiService.polygons.getPolygonsDirectory( + ensembleIdent.getCaseUuid(), + ensembleIdent.getEnsembleName() + ), + abortSignal + ), + staleTime: STALE_TIME, + gcTime: CACHE_TIME, + }); + }); + + availableSettingsUpdater(SettingType.POLYGONS_ATTRIBUTE, ({ getHelperDependency }) => { + const data = getHelperDependency(realizationPolygonsMetadataDep); + + if (!data) { + return []; + } + + const availableAttributes = [ + ...Array.from(new Set(data.map((polygonsMeta) => polygonsMeta.attribute_name))), + ]; + + return availableAttributes; + }); + + availableSettingsUpdater(SettingType.POLYGONS_NAME, ({ getHelperDependency, getLocalSetting }) => { + const attribute = getLocalSetting(SettingType.POLYGONS_ATTRIBUTE); + const data = getHelperDependency(realizationPolygonsMetadataDep); + + if (!attribute || !data) { + return []; + } + + const availableSurfaceNames = [ + ...Array.from( + new Set( + data.filter((polygonsMeta) => polygonsMeta.attribute_name === attribute).map((el) => el.name) + ) + ), + ]; + + return availableSurfaceNames; + }); + } +} diff --git a/frontend/src/modules/2DViewer/layers/layers/implementations/RealizationPolygonsLayer/types.ts b/frontend/src/modules/2DViewer/layers/layers/implementations/RealizationPolygonsLayer/types.ts new file mode 100644 index 000000000..1b79a705b --- /dev/null +++ b/frontend/src/modules/2DViewer/layers/layers/implementations/RealizationPolygonsLayer/types.ts @@ -0,0 +1,10 @@ +import { EnsembleIdent } from "@framework/EnsembleIdent"; + +import { SettingType } from "../../../settings/settingsTypes"; + +export type RealizationPolygonsSettings = { + [SettingType.ENSEMBLE]: EnsembleIdent | null; + [SettingType.REALIZATION]: number | null; + [SettingType.POLYGONS_ATTRIBUTE]: string | null; + [SettingType.POLYGONS_NAME]: string | null; +}; diff --git a/frontend/src/modules/2DViewer/layers/layers/implementations/RealizationSurfaceLayer/RealizationSurfaceLayer.ts b/frontend/src/modules/2DViewer/layers/layers/implementations/RealizationSurfaceLayer/RealizationSurfaceLayer.ts new file mode 100644 index 000000000..f5159e8b2 --- /dev/null +++ b/frontend/src/modules/2DViewer/layers/layers/implementations/RealizationSurfaceLayer/RealizationSurfaceLayer.ts @@ -0,0 +1,129 @@ +import { SurfaceDataPng_api, SurfaceTimeType_api } from "@api"; +import { apiService } from "@framework/ApiService"; +import { ItemDelegate } from "@modules/2DViewer/layers/delegates/ItemDelegate"; +import { LayerColoringType, LayerDelegate } from "@modules/2DViewer/layers/delegates/LayerDelegate"; +import { LayerManager } from "@modules/2DViewer/layers/framework/LayerManager/LayerManager"; +import { LayerRegistry } from "@modules/2DViewer/layers/layers/LayerRegistry"; +import { CACHE_TIME, STALE_TIME } from "@modules/2DViewer/layers/layers/_utils/queryConstants"; +import { SettingType } from "@modules/2DViewer/layers/settings/settingsTypes"; +import { FullSurfaceAddress, SurfaceAddressBuilder } from "@modules/_shared/Surface"; +import { SurfaceDataFloat_trans, transformSurfaceData } from "@modules/_shared/Surface/queryDataTransforms"; +import { encodeSurfAddrStr } from "@modules/_shared/Surface/surfaceAddress"; +import { QueryClient } from "@tanstack/react-query"; + +import { isEqual } from "lodash"; + +import { RealizationSurfaceSettingsContext } from "./RealizationSurfaceSettingsContext"; +import { RealizationSurfaceSettings } from "./types"; + +import { BoundingBox, Layer, SerializedLayer } from "../../../interfaces"; + +export class RealizationSurfaceLayer + implements Layer +{ + private _layerDelegate: LayerDelegate; + private _itemDelegate: ItemDelegate; + + constructor(layerManager: LayerManager) { + this._itemDelegate = new ItemDelegate("Realization Surface", layerManager); + this._layerDelegate = new LayerDelegate( + this, + layerManager, + new RealizationSurfaceSettingsContext(layerManager), + LayerColoringType.COLORSCALE + ); + } + + getSettingsContext() { + return this._layerDelegate.getSettingsContext(); + } + + getItemDelegate(): ItemDelegate { + return this._itemDelegate; + } + + getLayerDelegate(): LayerDelegate { + return this._layerDelegate; + } + + doSettingsChangesRequireDataRefetch( + prevSettings: RealizationSurfaceSettings, + newSettings: RealizationSurfaceSettings + ): boolean { + return !isEqual(prevSettings, newSettings); + } + + makeBoundingBox(): BoundingBox | null { + const data = this._layerDelegate.getData(); + if (!data) { + return null; + } + + return { + x: [data.transformed_bbox_utm.min_x, data.transformed_bbox_utm.max_x], + y: [data.transformed_bbox_utm.min_y, data.transformed_bbox_utm.max_y], + z: [0, 0], + }; + } + + makeValueRange(): [number, number] | null { + const data = this._layerDelegate.getData(); + if (!data) { + return null; + } + + return [data.value_min, data.value_max]; + } + + fetchData(queryClient: QueryClient): Promise { + let surfaceAddress: FullSurfaceAddress | null = null; + const addrBuilder = new SurfaceAddressBuilder(); + + const settings = this.getSettingsContext().getDelegate().getSettings(); + const ensembleIdent = settings[SettingType.ENSEMBLE].getDelegate().getValue(); + const realizationNum = settings[SettingType.REALIZATION].getDelegate().getValue(); + const surfaceName = settings[SettingType.SURFACE_NAME].getDelegate().getValue(); + const attribute = settings[SettingType.SURFACE_ATTRIBUTE].getDelegate().getValue(); + const timeOrInterval = settings[SettingType.TIME_OR_INTERVAL].getDelegate().getValue(); + + if (ensembleIdent && surfaceName && attribute && realizationNum !== null) { + addrBuilder.withEnsembleIdent(ensembleIdent); + addrBuilder.withName(surfaceName); + addrBuilder.withAttribute(attribute); + addrBuilder.withRealization(realizationNum); + + if (timeOrInterval !== SurfaceTimeType_api.NO_TIME) { + addrBuilder.withTimeOrInterval(timeOrInterval); + } + + surfaceAddress = addrBuilder.buildRealizationAddress(); + } + + const surfAddrStr = surfaceAddress ? encodeSurfAddrStr(surfaceAddress) : null; + + const queryKey = ["getSurfaceData", surfAddrStr, null, "png"]; + + this._layerDelegate.registerQueryKey(queryKey); + + const promise = queryClient + .fetchQuery({ + queryKey, + queryFn: () => apiService.surface.getSurfaceData(surfAddrStr ?? "", "png", null), + staleTime: STALE_TIME, + gcTime: CACHE_TIME, + }) + .then((data) => transformSurfaceData(data)); + + return promise; + } + + serializeState(): SerializedLayer { + return this._layerDelegate.serializeState(); + } + + deserializeState(serializedState: SerializedLayer): void { + this._layerDelegate.deserializeState(serializedState); + } +} + +LayerRegistry.registerLayer(RealizationSurfaceLayer); diff --git a/frontend/src/modules/2DViewer/layers/layers/implementations/RealizationSurfaceLayer/RealizationSurfaceSettingsContext.ts b/frontend/src/modules/2DViewer/layers/layers/implementations/RealizationSurfaceLayer/RealizationSurfaceSettingsContext.ts new file mode 100644 index 000000000..2322f86bd --- /dev/null +++ b/frontend/src/modules/2DViewer/layers/layers/implementations/RealizationSurfaceLayer/RealizationSurfaceSettingsContext.ts @@ -0,0 +1,159 @@ +import { SurfaceTimeType_api } from "@api"; +import { apiService } from "@framework/ApiService"; +import { SettingsContextDelegate } from "@modules/2DViewer/layers/delegates/SettingsContextDelegate"; +import { LayerManager } from "@modules/2DViewer/layers/framework/LayerManager/LayerManager"; +import { CACHE_TIME, STALE_TIME } from "@modules/2DViewer/layers/layers/_utils/queryConstants"; +import { cancelPromiseOnAbort } from "@modules/2DViewer/layers/layers/_utils/utils"; +import { SettingType } from "@modules/2DViewer/layers/settings/settingsTypes"; + +import { RealizationSurfaceSettings } from "./types"; + +import { DefineDependenciesArgs, SettingsContext } from "../../../interfaces"; +import { EnsembleSetting } from "../../../settings/implementations/EnsembleSetting"; +import { RealizationSetting } from "../../../settings/implementations/RealizationSetting"; +import { SurfaceAttributeSetting } from "../../../settings/implementations/SurfaceAttributeSetting"; +import { SurfaceNameSetting } from "../../../settings/implementations/SurfaceNameSetting"; +import { TimeOrIntervalSetting } from "../../../settings/implementations/TimeOrIntervalSetting"; + +export class RealizationSurfaceSettingsContext implements SettingsContext { + private _contextDelegate: SettingsContextDelegate; + + constructor(layerManager: LayerManager) { + this._contextDelegate = new SettingsContextDelegate< + RealizationSurfaceSettings, + keyof RealizationSurfaceSettings + >(this, layerManager, { + [SettingType.ENSEMBLE]: new EnsembleSetting(), + [SettingType.REALIZATION]: new RealizationSetting(), + [SettingType.SURFACE_ATTRIBUTE]: new SurfaceAttributeSetting(), + [SettingType.SURFACE_NAME]: new SurfaceNameSetting(), + [SettingType.TIME_OR_INTERVAL]: new TimeOrIntervalSetting(), + }); + } + + getDelegate(): SettingsContextDelegate { + return this._contextDelegate; + } + + getSettings() { + return this._contextDelegate.getSettings(); + } + + defineDependencies({ + helperDependency, + availableSettingsUpdater, + queryClient, + }: DefineDependenciesArgs) { + availableSettingsUpdater(SettingType.ENSEMBLE, ({ getGlobalSetting }) => { + const fieldIdentifier = getGlobalSetting("fieldId"); + const ensembles = getGlobalSetting("ensembles"); + + const ensembleIdents = ensembles + .filter((ensemble) => ensemble.getFieldIdentifier() === fieldIdentifier) + .map((ensemble) => ensemble.getIdent()); + + return ensembleIdents; + }); + + availableSettingsUpdater(SettingType.REALIZATION, ({ getLocalSetting, getGlobalSetting }) => { + const ensembleIdent = getLocalSetting(SettingType.ENSEMBLE); + const realizationFilterFunc = getGlobalSetting("realizationFilterFunction"); + + if (!ensembleIdent) { + return []; + } + + const realizations = realizationFilterFunc(ensembleIdent); + + return [...realizations]; + }); + + const realizationSurfaceMetadataDep = helperDependency(async ({ getLocalSetting, abortSignal }) => { + const ensembleIdent = getLocalSetting(SettingType.ENSEMBLE); + + if (!ensembleIdent) { + return null; + } + + return await queryClient.fetchQuery({ + queryKey: ["getRealizationSurfacesMetadata", ensembleIdent], + queryFn: () => + cancelPromiseOnAbort( + apiService.surface.getRealizationSurfacesMetadata( + ensembleIdent.getCaseUuid(), + ensembleIdent.getEnsembleName() + ), + abortSignal + ), + staleTime: STALE_TIME, + gcTime: CACHE_TIME, + }); + }); + + availableSettingsUpdater(SettingType.SURFACE_ATTRIBUTE, ({ getHelperDependency }) => { + const data = getHelperDependency(realizationSurfaceMetadataDep); + + if (!data) { + return []; + } + + const availableAttributes = [ + ...Array.from(new Set(data.surfaces.map((surface) => surface.attribute_name))), + ]; + + return availableAttributes; + }); + + availableSettingsUpdater(SettingType.SURFACE_NAME, ({ getHelperDependency, getLocalSetting }) => { + const attribute = getLocalSetting(SettingType.SURFACE_ATTRIBUTE); + const data = getHelperDependency(realizationSurfaceMetadataDep); + + if (!attribute || !data) { + return []; + } + + const availableSurfaceNames = [ + ...Array.from( + new Set( + data.surfaces.filter((surface) => surface.attribute_name === attribute).map((el) => el.name) + ) + ), + ]; + + return availableSurfaceNames; + }); + + availableSettingsUpdater(SettingType.TIME_OR_INTERVAL, ({ getLocalSetting, getHelperDependency }) => { + const attribute = getLocalSetting(SettingType.SURFACE_ATTRIBUTE); + const surfaceName = getLocalSetting(SettingType.SURFACE_NAME); + const data = getHelperDependency(realizationSurfaceMetadataDep); + + if (!attribute || !surfaceName || !data) { + return []; + } + + const availableTimeOrIntervals: string[] = []; + const availableTimeTypes = [ + ...Array.from( + new Set( + data.surfaces + .filter((surface) => surface.attribute_name === attribute && surface.name === surfaceName) + .map((el) => el.time_type) + ) + ), + ]; + + if (availableTimeTypes.includes(SurfaceTimeType_api.NO_TIME)) { + availableTimeOrIntervals.push(SurfaceTimeType_api.NO_TIME); + } + if (availableTimeTypes.includes(SurfaceTimeType_api.TIME_POINT)) { + availableTimeOrIntervals.push(...data.time_points_iso_str); + } + if (availableTimeTypes.includes(SurfaceTimeType_api.INTERVAL)) { + availableTimeOrIntervals.push(...data.time_intervals_iso_str); + } + + return availableTimeOrIntervals; + }); + } +} diff --git a/frontend/src/modules/2DViewer/layers/layers/implementations/RealizationSurfaceLayer/types.ts b/frontend/src/modules/2DViewer/layers/layers/implementations/RealizationSurfaceLayer/types.ts new file mode 100644 index 000000000..6477cdcc6 --- /dev/null +++ b/frontend/src/modules/2DViewer/layers/layers/implementations/RealizationSurfaceLayer/types.ts @@ -0,0 +1,11 @@ +import { EnsembleIdent } from "@framework/EnsembleIdent"; + +import { SettingType } from "../../../settings/settingsTypes"; + +export type RealizationSurfaceSettings = { + [SettingType.ENSEMBLE]: EnsembleIdent | null; + [SettingType.REALIZATION]: number | null; + [SettingType.SURFACE_ATTRIBUTE]: string | null; + [SettingType.SURFACE_NAME]: string | null; + [SettingType.TIME_OR_INTERVAL]: string | null; +}; diff --git a/frontend/src/modules/2DViewer/layers/layers/implementations/StatisticalSurfaceLayer/StatisticalSurfaceLayer.ts b/frontend/src/modules/2DViewer/layers/layers/implementations/StatisticalSurfaceLayer/StatisticalSurfaceLayer.ts new file mode 100644 index 000000000..d34862f1f --- /dev/null +++ b/frontend/src/modules/2DViewer/layers/layers/implementations/StatisticalSurfaceLayer/StatisticalSurfaceLayer.ts @@ -0,0 +1,155 @@ +import { SurfaceDataPng_api, SurfaceTimeType_api } from "@api"; +import { apiService } from "@framework/ApiService"; +import { ItemDelegate } from "@modules/2DViewer/layers/delegates/ItemDelegate"; +import { LayerColoringType, LayerDelegate } from "@modules/2DViewer/layers/delegates/LayerDelegate"; +import { LayerManager } from "@modules/2DViewer/layers/framework/LayerManager/LayerManager"; +import { LayerRegistry } from "@modules/2DViewer/layers/layers/LayerRegistry"; +import { CACHE_TIME, STALE_TIME } from "@modules/2DViewer/layers/layers/_utils/queryConstants"; +import { SettingType } from "@modules/2DViewer/layers/settings/settingsTypes"; +import { FullSurfaceAddress, SurfaceAddressBuilder } from "@modules/_shared/Surface"; +import { SurfaceDataFloat_trans, transformSurfaceData } from "@modules/_shared/Surface/queryDataTransforms"; +import { encodeSurfAddrStr } from "@modules/_shared/Surface/surfaceAddress"; +import { QueryClient } from "@tanstack/react-query"; + +import { isEqual } from "lodash"; + +import { StatisticalSurfaceSettingsContext } from "./StatisticalSurfaceSettingsContext"; +import { StatisticalSurfaceSettings } from "./types"; + +import { BoundingBox, Layer, SerializedLayer } from "../../../interfaces"; + +export class StatisticalSurfaceLayer + implements Layer +{ + private _itemDelegate: ItemDelegate; + private _layerDelegate: LayerDelegate; + + constructor(layerManager: LayerManager) { + this._itemDelegate = new ItemDelegate("Statistical Surface", layerManager); + this._layerDelegate = new LayerDelegate( + this, + layerManager, + new StatisticalSurfaceSettingsContext(layerManager), + LayerColoringType.COLORSCALE + ); + } + + getSettingsContext() { + return this._layerDelegate.getSettingsContext(); + } + + getItemDelegate(): ItemDelegate { + return this._itemDelegate; + } + + getLayerDelegate(): LayerDelegate { + return this._layerDelegate; + } + + doSettingsChangesRequireDataRefetch( + prevSettings: StatisticalSurfaceSettings, + newSettings: StatisticalSurfaceSettings + ): boolean { + return !isEqual(prevSettings, newSettings); + } + + makeBoundingBox(): BoundingBox | null { + const data = this._layerDelegate.getData(); + if (!data) { + return null; + } + + return { + x: [data.transformed_bbox_utm.min_x, data.transformed_bbox_utm.max_x], + y: [data.transformed_bbox_utm.min_y, data.transformed_bbox_utm.max_y], + z: [0, 0], + }; + } + + makeValueRange(): [number, number] | null { + const data = this._layerDelegate.getData(); + if (!data) { + return null; + } + + return [data.value_min, data.value_max]; + } + + fetchData(queryClient: QueryClient): Promise { + let surfaceAddress: FullSurfaceAddress | null = null; + const addrBuilder = new SurfaceAddressBuilder(); + const workbenchSession = this.getLayerDelegate().getLayerManager().getWorkbenchSession(); + const settings = this.getSettingsContext().getDelegate().getSettings(); + const ensembleIdent = settings[SettingType.ENSEMBLE].getDelegate().getValue(); + const surfaceName = settings[SettingType.SURFACE_NAME].getDelegate().getValue(); + const attribute = settings[SettingType.SURFACE_ATTRIBUTE].getDelegate().getValue(); + const timeOrInterval = settings[SettingType.TIME_OR_INTERVAL].getDelegate().getValue(); + const statisticFunction = settings[SettingType.STATISTIC_FUNCTION].getDelegate().getValue(); + const sensitivityNameCasePair = settings[SettingType.SENSITIVITY].getDelegate().getValue(); + + if (ensembleIdent && surfaceName && attribute) { + addrBuilder.withEnsembleIdent(ensembleIdent); + addrBuilder.withName(surfaceName); + addrBuilder.withAttribute(attribute); + + // Get filtered realizations from workbench + let filteredRealizations = workbenchSession + .getRealizationFilterSet() + .getRealizationFilterForEnsembleIdent(ensembleIdent) + .getFilteredRealizations(); + const currentEnsemble = workbenchSession.getEnsembleSet().findEnsemble(ensembleIdent); + + // If sensitivity is set, filter realizations further to only include the realizations that are in the sensitivity + if (sensitivityNameCasePair) { + const sensitivity = currentEnsemble + ?.getSensitivities() + ?.getCaseByName(sensitivityNameCasePair.sensitivityName, sensitivityNameCasePair.sensitivityCase); + + const sensitivityRealizations = sensitivity?.realizations ?? []; + + filteredRealizations = filteredRealizations.filter((realization) => + sensitivityRealizations.includes(realization) + ); + } + + // If realizations are filtered, update the address + const allRealizations = currentEnsemble?.getRealizations() ?? []; + if (!isEqual([...allRealizations], [...filteredRealizations])) { + addrBuilder.withStatisticRealizations([...filteredRealizations]); + } + + if (timeOrInterval !== SurfaceTimeType_api.NO_TIME) { + addrBuilder.withTimeOrInterval(timeOrInterval); + } + addrBuilder.withStatisticFunction(statisticFunction); + surfaceAddress = addrBuilder.buildStatisticalAddress(); + } + + const surfAddrStr = surfaceAddress ? encodeSurfAddrStr(surfaceAddress) : null; + + const queryKey = ["getSurfaceData", surfAddrStr, null, "png"]; + + this._layerDelegate.registerQueryKey(queryKey); + + const promise = queryClient + .fetchQuery({ + queryKey, + queryFn: () => apiService.surface.getSurfaceData(surfAddrStr ?? "", "png", null), + staleTime: STALE_TIME, + gcTime: CACHE_TIME, + }) + .then((data) => transformSurfaceData(data)); + + return promise; + } + + serializeState(): SerializedLayer { + return this._layerDelegate.serializeState(); + } + + deserializeState(serializedState: SerializedLayer): void { + this._layerDelegate.deserializeState(serializedState); + } +} + +LayerRegistry.registerLayer(StatisticalSurfaceLayer); diff --git a/frontend/src/modules/2DViewer/layers/layers/implementations/StatisticalSurfaceLayer/StatisticalSurfaceSettingsContext.ts b/frontend/src/modules/2DViewer/layers/layers/implementations/StatisticalSurfaceLayer/StatisticalSurfaceSettingsContext.ts new file mode 100644 index 000000000..c052e92db --- /dev/null +++ b/frontend/src/modules/2DViewer/layers/layers/implementations/StatisticalSurfaceLayer/StatisticalSurfaceSettingsContext.ts @@ -0,0 +1,173 @@ +import { SurfaceStatisticFunction_api, SurfaceTimeType_api } from "@api"; +import { apiService } from "@framework/ApiService"; +import { LayerManager } from "@modules/2DViewer/layers/framework/LayerManager/LayerManager"; +import { CACHE_TIME, STALE_TIME } from "@modules/2DViewer/layers/layers/_utils/queryConstants"; +import { cancelPromiseOnAbort } from "@modules/2DViewer/layers/layers/_utils/utils"; + +import { StatisticalSurfaceSettings } from "./types"; + +import { SettingsContextDelegate } from "../../../delegates/SettingsContextDelegate"; +import { DefineDependenciesArgs, SettingsContext } from "../../../interfaces"; +import { EnsembleSetting } from "../../../settings/implementations/EnsembleSetting"; +import { SensitivityNameCasePair, SensitivitySetting } from "../../../settings/implementations/SensitivitySetting"; +import { StatisticFunctionSetting } from "../../../settings/implementations/StatisticFunctionSetting"; +import { SurfaceAttributeSetting } from "../../../settings/implementations/SurfaceAttributeSetting"; +import { SurfaceNameSetting } from "../../../settings/implementations/SurfaceNameSetting"; +import { TimeOrIntervalSetting } from "../../../settings/implementations/TimeOrIntervalSetting"; +import { SettingType } from "../../../settings/settingsTypes"; + +export class StatisticalSurfaceSettingsContext implements SettingsContext { + private _contextDelegate: SettingsContextDelegate; + + constructor(layerManager: LayerManager) { + this._contextDelegate = new SettingsContextDelegate< + StatisticalSurfaceSettings, + keyof StatisticalSurfaceSettings + >(this, layerManager, { + [SettingType.ENSEMBLE]: new EnsembleSetting(), + [SettingType.STATISTIC_FUNCTION]: new StatisticFunctionSetting(), + [SettingType.SENSITIVITY]: new SensitivitySetting(), + [SettingType.SURFACE_ATTRIBUTE]: new SurfaceAttributeSetting(), + [SettingType.SURFACE_NAME]: new SurfaceNameSetting(), + [SettingType.TIME_OR_INTERVAL]: new TimeOrIntervalSetting(), + }); + } + + getDelegate(): SettingsContextDelegate { + return this._contextDelegate; + } + + getSettings() { + return this._contextDelegate.getSettings(); + } + + defineDependencies({ + helperDependency, + availableSettingsUpdater, + workbenchSession, + queryClient, + }: DefineDependenciesArgs) { + availableSettingsUpdater(SettingType.STATISTIC_FUNCTION, () => Object.values(SurfaceStatisticFunction_api)); + availableSettingsUpdater(SettingType.ENSEMBLE, ({ getGlobalSetting }) => { + const fieldIdentifier = getGlobalSetting("fieldId"); + const ensembles = getGlobalSetting("ensembles"); + + const ensembleIdents = ensembles + .filter((ensemble) => ensemble.getFieldIdentifier() === fieldIdentifier) + .map((ensemble) => ensemble.getIdent()); + + return ensembleIdents; + }); + availableSettingsUpdater(SettingType.SENSITIVITY, ({ getLocalSetting }) => { + const ensembleIdent = getLocalSetting(SettingType.ENSEMBLE); + + if (!ensembleIdent) { + return []; + } + + const ensembleSet = workbenchSession.getEnsembleSet(); + const currentEnsemble = ensembleSet.findEnsemble(ensembleIdent); + const sensitivities = currentEnsemble?.getSensitivities()?.getSensitivityArr() ?? []; + if (sensitivities.length === 0) { + return []; + } + const availableSensitivityPairs: SensitivityNameCasePair[] = []; + sensitivities.map((sensitivity) => + sensitivity.cases.map((sensitivityCase) => { + availableSensitivityPairs.push({ + sensitivityName: sensitivity.name, + sensitivityCase: sensitivityCase.name, + }); + }) + ); + return availableSensitivityPairs; + }); + + const surfaceMetadataDep = helperDependency(async ({ getLocalSetting, abortSignal }) => { + const ensembleIdent = getLocalSetting(SettingType.ENSEMBLE); + + if (!ensembleIdent) { + return null; + } + + return await queryClient.fetchQuery({ + queryKey: ["getRealizationSurfacesMetadata", ensembleIdent], + queryFn: () => + cancelPromiseOnAbort( + apiService.surface.getRealizationSurfacesMetadata( + ensembleIdent.getCaseUuid(), + ensembleIdent.getEnsembleName() + ), + abortSignal + ), + staleTime: STALE_TIME, + gcTime: CACHE_TIME, + }); + }); + + availableSettingsUpdater(SettingType.SURFACE_ATTRIBUTE, ({ getHelperDependency }) => { + const data = getHelperDependency(surfaceMetadataDep); + + if (!data) { + return []; + } + + const availableAttributes = [ + ...Array.from(new Set(data.surfaces.map((surface) => surface.attribute_name))), + ]; + + return availableAttributes; + }); + availableSettingsUpdater(SettingType.SURFACE_NAME, ({ getHelperDependency, getLocalSetting }) => { + const attribute = getLocalSetting(SettingType.SURFACE_ATTRIBUTE); + const data = getHelperDependency(surfaceMetadataDep); + + if (!attribute || !data) { + return []; + } + + const availableSurfaceNames = [ + ...Array.from( + new Set( + data.surfaces.filter((surface) => surface.attribute_name === attribute).map((el) => el.name) + ) + ), + ]; + + return availableSurfaceNames; + }); + + availableSettingsUpdater(SettingType.TIME_OR_INTERVAL, ({ getLocalSetting, getHelperDependency }) => { + const attribute = getLocalSetting(SettingType.SURFACE_ATTRIBUTE); + const surfaceName = getLocalSetting(SettingType.SURFACE_NAME); + const data = getHelperDependency(surfaceMetadataDep); + + if (!attribute || !surfaceName || !data) { + return []; + } + + const availableTimeOrIntervals: string[] = []; + const availableTimeTypes = [ + ...Array.from( + new Set( + data.surfaces + .filter((surface) => surface.attribute_name === attribute && surface.name === surfaceName) + .map((el) => el.time_type) + ) + ), + ]; + + if (availableTimeTypes.includes(SurfaceTimeType_api.NO_TIME)) { + availableTimeOrIntervals.push(SurfaceTimeType_api.NO_TIME); + } + if (availableTimeTypes.includes(SurfaceTimeType_api.TIME_POINT)) { + availableTimeOrIntervals.push(...data.time_points_iso_str); + } + if (availableTimeTypes.includes(SurfaceTimeType_api.INTERVAL)) { + availableTimeOrIntervals.push(...data.time_intervals_iso_str); + } + + return availableTimeOrIntervals; + }); + } +} diff --git a/frontend/src/modules/2DViewer/layers/layers/implementations/StatisticalSurfaceLayer/types.ts b/frontend/src/modules/2DViewer/layers/layers/implementations/StatisticalSurfaceLayer/types.ts new file mode 100644 index 000000000..639fc8253 --- /dev/null +++ b/frontend/src/modules/2DViewer/layers/layers/implementations/StatisticalSurfaceLayer/types.ts @@ -0,0 +1,14 @@ +import { SurfaceStatisticFunction_api } from "@api"; +import { EnsembleIdent } from "@framework/EnsembleIdent"; +import { SettingType } from "@modules/2DViewer/layers/settings/settingsTypes"; + +import { SensitivityNameCasePair } from "../../../settings/implementations/SensitivitySetting"; + +export type StatisticalSurfaceSettings = { + [SettingType.ENSEMBLE]: EnsembleIdent | null; + [SettingType.STATISTIC_FUNCTION]: SurfaceStatisticFunction_api; + [SettingType.SENSITIVITY]: SensitivityNameCasePair | null; + [SettingType.SURFACE_ATTRIBUTE]: string | null; + [SettingType.SURFACE_NAME]: string | null; + [SettingType.TIME_OR_INTERVAL]: string | null; +}; diff --git a/frontend/src/modules/2DViewer/layers/settings/SettingComponent.tsx b/frontend/src/modules/2DViewer/layers/settings/SettingComponent.tsx new file mode 100644 index 000000000..360bc91c2 --- /dev/null +++ b/frontend/src/modules/2DViewer/layers/settings/SettingComponent.tsx @@ -0,0 +1,113 @@ +import React from "react"; + +import { PendingWrapper } from "@lib/components/PendingWrapper"; +import { resolveClassNames } from "@lib/utils/resolveClassNames"; +import { Link, Warning } from "@mui/icons-material"; + +import { usePublishSubscribeTopicValue } from "../delegates/PublishSubscribeDelegate"; +import { SettingTopic } from "../delegates/SettingDelegate"; +import { LayerManager, LayerManagerTopic } from "../framework/LayerManager/LayerManager"; +import { Setting, SettingComponentProps as SettingComponentPropsInterface } from "../interfaces"; + +export type SettingComponentProps = { + setting: Setting; + manager: LayerManager; + sharedSetting: boolean; +}; + +export function SettingComponent(props: SettingComponentProps): React.ReactNode { + const componentRef = React.useRef<(props: SettingComponentPropsInterface) => React.ReactNode>( + props.setting.makeComponent() + ); + const value = usePublishSubscribeTopicValue(props.setting.getDelegate(), SettingTopic.VALUE_CHANGED); + const isValid = usePublishSubscribeTopicValue(props.setting.getDelegate(), SettingTopic.VALIDITY_CHANGED); + const isPersisted = usePublishSubscribeTopicValue( + props.setting.getDelegate(), + SettingTopic.PERSISTED_STATE_CHANGED + ); + const availableValues = usePublishSubscribeTopicValue( + props.setting.getDelegate(), + SettingTopic.AVAILABLE_VALUES_CHANGED + ); + const overriddenValue = usePublishSubscribeTopicValue(props.setting.getDelegate(), SettingTopic.OVERRIDDEN_CHANGED); + const isLoading = usePublishSubscribeTopicValue(props.setting.getDelegate(), SettingTopic.LOADING_STATE_CHANGED); + const isInitialized = usePublishSubscribeTopicValue(props.setting.getDelegate(), SettingTopic.INIT_STATE_CHANGED); + const globalSettings = usePublishSubscribeTopicValue(props.manager, LayerManagerTopic.GLOBAL_SETTINGS_CHANGED); + + let actuallyLoading = isLoading || !isInitialized; + if (!isLoading && isPersisted && !isValid) { + actuallyLoading = false; + } + + function handleValueChanged(newValue: TValue) { + props.setting.getDelegate().setValue(newValue); + } + + if (props.sharedSetting && availableValues.length === 0 && isInitialized) { + return ( + +
{props.setting.getLabel()}
+
Empty intersection
+
+ ); + } + + if (overriddenValue !== undefined) { + const valueAsString = props.setting + .getDelegate() + .valueToString(overriddenValue, props.manager.getWorkbenchSession(), props.manager.getWorkbenchSettings()); + return ( + +
+ {props.setting.getLabel()} + + + +
+
+ {isValid ? valueAsString : No valid shared setting value} +
+
+ ); + } + + return ( + +
{props.setting.getLabel()}
+
+ +
+
+ +
+ {isPersisted && !isLoading && isInitialized && ( + + + + Persisted value not valid. + + + )} +
+
+
+
+ ); +} diff --git a/frontend/src/modules/2DViewer/layers/settings/SettingRegistry.ts b/frontend/src/modules/2DViewer/layers/settings/SettingRegistry.ts new file mode 100644 index 000000000..ade7db21c --- /dev/null +++ b/frontend/src/modules/2DViewer/layers/settings/SettingRegistry.ts @@ -0,0 +1,13 @@ +import { Setting } from "../interfaces"; + +export class SettingRegistry { + private static _registeredSettings: Record }> = {}; + + static registerSetting(ctor: { new (): Setting }): void { + this._registeredSettings[ctor.name] = ctor; + } + + static makeSetting(settingName: string): Setting { + return new this._registeredSettings[settingName](); + } +} diff --git a/frontend/src/modules/2DViewer/layers/settings/implementations/DrilledWellboresSetting.tsx b/frontend/src/modules/2DViewer/layers/settings/implementations/DrilledWellboresSetting.tsx new file mode 100644 index 000000000..1bf1840a6 --- /dev/null +++ b/frontend/src/modules/2DViewer/layers/settings/implementations/DrilledWellboresSetting.tsx @@ -0,0 +1,107 @@ +import React from "react"; + +import { WellboreHeader_api } from "@api"; +import { DenseIconButton } from "@lib/components/DenseIconButton"; +import { Select, SelectOption } from "@lib/components/Select"; +import { Deselect, SelectAll } from "@mui/icons-material"; + +import { SettingDelegate } from "../../delegates/SettingDelegate"; +import { AvailableValuesType, Setting, SettingComponentProps } from "../../interfaces"; +import { SettingRegistry } from "../SettingRegistry"; +import { SettingType } from "../settingsTypes"; + +type ValueType = WellboreHeader_api[] | null; + +export class DrilledWellboresSetting implements Setting { + private _delegate: SettingDelegate; + + constructor() { + this._delegate = new SettingDelegate(null, this); + } + + getType(): SettingType { + return SettingType.SMDA_WELLBORE_HEADERS; + } + + getLabel(): string { + return "Drilled wellbores"; + } + + getDelegate(): SettingDelegate { + return this._delegate; + } + + fixupValue(availableValues: AvailableValuesType, currentValue: ValueType): ValueType { + if (!currentValue) { + return availableValues; + } + + const matchingValues = currentValue.filter((value) => + availableValues.some((availableValue) => availableValue.wellboreUuid === value.wellboreUuid) + ); + if (matchingValues.length === 0) { + return availableValues; + } + return matchingValues; + } + + makeComponent(): (props: SettingComponentProps) => React.ReactNode { + return function DrilledWellbores(props: SettingComponentProps) { + const options: SelectOption[] = React.useMemo( + () => + props.availableValues.map((ident) => ({ + value: ident.wellboreUuid, + label: ident.uniqueWellboreIdentifier, + })), + [props.availableValues] + ); + + function handleChange(selectedUuids: string[]) { + const selectedWellbores = props.availableValues.filter((ident) => + selectedUuids.includes(ident.wellboreUuid) + ); + props.onValueChange(selectedWellbores); + } + + function selectAll() { + const allUuids = props.availableValues.map((ident) => ident.wellboreUuid); + handleChange(allUuids); + } + + function selectNone() { + handleChange([]); + } + + const selectedValues = React.useMemo( + () => props.value?.map((ident) => ident.wellboreUuid) ?? [], + [props.value] + ); + + return ( +
+
+ + + Select all + + + + Clear selection + +
+