From 310471767f3cce51fb7fb6964dc2bd72a9ca3d65 Mon Sep 17 00:00:00 2001 From: Ruben Thoms Date: Mon, 18 Nov 2024 16:32:41 +0100 Subject: [PATCH 1/8] Implemented new 2d viewer module Co-authored-by: Hans Kallekleiv --- frontend/src/modules/2DViewer/interfaces.ts | 23 + .../src/modules/2DViewer/layers/ColorScale.ts | 59 +++ .../modules/2DViewer/layers/DeltaSurface.ts | 85 ++++ .../src/modules/2DViewer/layers/Dependency.ts | 210 +++++++++ .../2DViewer/layers/DeserializationFactory.ts | 70 +++ .../modules/2DViewer/layers/LayerManager.ts | 209 ++++++++ .../modules/2DViewer/layers/LayerRegistry.ts | 18 + .../2DViewer/layers/SettingRegistry.ts | 13 + .../modules/2DViewer/layers/SettingsGroup.ts | 36 ++ .../modules/2DViewer/layers/SharedSetting.ts | 118 +++++ frontend/src/modules/2DViewer/layers/View.ts | 38 ++ .../layers/components/ColorScaleComponent.tsx | 73 +++ .../components/DeltaSurfaceComponent.tsx | 78 +++ .../2DViewer/layers/components/EditName.tsx | 67 +++ .../layers/components/EmptyContent.tsx | 9 + .../components/ExpandCollapseAllButton.tsx | 37 ++ .../layers/components/LayerComponent.tsx | 167 +++++++ .../layers/components/LayersActions.tsx | 83 ++++ .../layers/components/RemoveButton.tsx | 30 ++ .../layers/components/SettingComponent.tsx | 113 +++++ .../components/SettingsGroupComponent.tsx | 73 +++ .../components/SharedSettingComponent.tsx | 89 ++++ .../layers/components/ViewComponent.tsx | 77 +++ .../layers/components/VisibilityToggle.tsx | 24 + .../2DViewer/layers/components/utils.tsx | 93 ++++ .../layers/delegates/GroupDelegate.ts | 249 ++++++++++ .../2DViewer/layers/delegates/ItemDelegate.ts | 153 ++++++ .../layers/delegates/LayerDelegate.ts | 330 +++++++++++++ .../delegates/PublishSubscribeDelegate.ts | 46 ++ .../layers/delegates/SettingDelegate.ts | 341 ++++++++++++++ .../delegates/SettingsContextDelegate.ts | 379 +++++++++++++++ .../delegates/UnsubscribeHandlerDelegate.ts | 30 ++ .../DrilledWellTrajectoriesContext.ts | 86 ++++ .../DrilledWellTrajectoriesLayer.ts | 125 +++++ .../DrilledWellTrajectoriesLayer/types.ts | 8 + .../DrilledWellborePicksContext.ts | 137 ++++++ .../DrilledWellborePicksLayer.ts | 126 +++++ .../layers/DrilledWellborePicksLayer/types.ts | 9 + .../ObservedSurfaceContext.ts | 144 ++++++ .../ObservedSurfaceLayer.ts | 124 +++++ .../layers/ObservedSurfaceLayer/types.ts | 9 + .../RealizationGridContext.ts | 180 +++++++ .../RealizationGridLayer.ts | 204 ++++++++ .../layers/RealizationGridLayer/types.ts | 12 + .../RealizationPolygonsContext.ts | 126 +++++ .../RealizationPolygonsLayer.ts | 124 +++++ .../layers/RealizationPolygonsLayer/types.ts | 10 + .../RealizationSurfaceContext.ts | 159 +++++++ .../RealizationSurfaceLayer.ts | 129 +++++ .../layers/RealizationSurfaceLayer/types.ts | 11 + .../StatisticalSurfaceContext.ts | 173 +++++++ .../StatisticalSurfaceLayer.ts | 155 ++++++ .../layers/StatisticalSurfaceLayer/types.ts | 14 + .../settings/DrilledWellbores.tsx | 135 ++++++ .../implementations/settings/Ensemble.tsx | 67 +++ .../settings/GridAttribute.tsx | 54 +++ .../implementations/settings/GridLayer.tsx | 102 ++++ .../implementations/settings/GridName.tsx | 54 +++ .../settings/PolygonsAttribute.tsx | 54 +++ .../implementations/settings/PolygonsName.tsx | 54 +++ .../implementations/settings/Realization.tsx | 56 +++ .../implementations/settings/Sensitivity.tsx | 139 ++++++ .../settings/ShowGridLines.tsx | 47 ++ .../settings/StatisticFunction.tsx | 60 +++ .../settings/SurfaceAttribute.tsx | 54 +++ .../implementations/settings/SurfaceName.tsx | 54 +++ .../settings/TimeOrInterval.tsx | 84 ++++ .../implementations/settings/settingsTypes.ts | 16 + .../src/modules/2DViewer/layers/interfaces.ts | 204 ++++++++ .../modules/2DViewer/layers/queryConstants.ts | 2 + frontend/src/modules/2DViewer/layers/utils.ts | 22 + frontend/src/modules/2DViewer/loadModule.tsx | 13 + frontend/src/modules/2DViewer/preview.tsx | 8 + frontend/src/modules/2DViewer/preview.webp | Bin 0 -> 38828 bytes .../src/modules/2DViewer/registerModule.ts | 24 + .../2DViewer/settings/atoms/baseAtoms.ts | 8 + .../2DViewer/settings/atoms/derivedAtoms.ts | 31 ++ .../components/layerManagerComponent.tsx | 445 ++++++++++++++++++ .../modules/2DViewer/settings/settings.tsx | 146 ++++++ frontend/src/modules/2DViewer/types.ts | 38 ++ .../view/components/LayersWrapper.tsx | 154 ++++++ .../view/components/ReadoutBoxWrapper.tsx | 117 +++++ .../view/components/ReadoutWrapper.tsx | 76 +++ .../2DViewer/view/components/Toolbar.tsx | 21 + .../customDeckGlLayers/AdvancedWellsLayer.ts | 67 +++ .../customDeckGlLayers/PlaceholderLayer.ts | 21 + .../customDeckGlLayers/WellborePicksLayer.ts | 136 ++++++ .../2DViewer/view/utils/layerFactory.ts | 363 ++++++++++++++ .../2DViewer/view/utils/makeViewsAndLayers.ts | 179 +++++++ frontend/src/modules/2DViewer/view/view.tsx | 24 + .../_shared/components/Toolbar/index.ts | 3 + .../_shared/components/Toolbar/toolbar.tsx | 16 + .../components/Toolbar/toolbarDivider.tsx | 3 + frontend/src/modules/registerAllModules.ts | 4 +- 94 files changed, 8638 insertions(+), 2 deletions(-) create mode 100644 frontend/src/modules/2DViewer/interfaces.ts create mode 100644 frontend/src/modules/2DViewer/layers/ColorScale.ts create mode 100644 frontend/src/modules/2DViewer/layers/DeltaSurface.ts create mode 100644 frontend/src/modules/2DViewer/layers/Dependency.ts create mode 100644 frontend/src/modules/2DViewer/layers/DeserializationFactory.ts create mode 100644 frontend/src/modules/2DViewer/layers/LayerManager.ts create mode 100644 frontend/src/modules/2DViewer/layers/LayerRegistry.ts create mode 100644 frontend/src/modules/2DViewer/layers/SettingRegistry.ts create mode 100644 frontend/src/modules/2DViewer/layers/SettingsGroup.ts create mode 100644 frontend/src/modules/2DViewer/layers/SharedSetting.ts create mode 100644 frontend/src/modules/2DViewer/layers/View.ts create mode 100644 frontend/src/modules/2DViewer/layers/components/ColorScaleComponent.tsx create mode 100644 frontend/src/modules/2DViewer/layers/components/DeltaSurfaceComponent.tsx create mode 100644 frontend/src/modules/2DViewer/layers/components/EditName.tsx create mode 100644 frontend/src/modules/2DViewer/layers/components/EmptyContent.tsx create mode 100644 frontend/src/modules/2DViewer/layers/components/ExpandCollapseAllButton.tsx create mode 100644 frontend/src/modules/2DViewer/layers/components/LayerComponent.tsx create mode 100644 frontend/src/modules/2DViewer/layers/components/LayersActions.tsx create mode 100644 frontend/src/modules/2DViewer/layers/components/RemoveButton.tsx create mode 100644 frontend/src/modules/2DViewer/layers/components/SettingComponent.tsx create mode 100644 frontend/src/modules/2DViewer/layers/components/SettingsGroupComponent.tsx create mode 100644 frontend/src/modules/2DViewer/layers/components/SharedSettingComponent.tsx create mode 100644 frontend/src/modules/2DViewer/layers/components/ViewComponent.tsx create mode 100644 frontend/src/modules/2DViewer/layers/components/VisibilityToggle.tsx create mode 100644 frontend/src/modules/2DViewer/layers/components/utils.tsx create mode 100644 frontend/src/modules/2DViewer/layers/delegates/GroupDelegate.ts create mode 100644 frontend/src/modules/2DViewer/layers/delegates/ItemDelegate.ts create mode 100644 frontend/src/modules/2DViewer/layers/delegates/LayerDelegate.ts create mode 100644 frontend/src/modules/2DViewer/layers/delegates/PublishSubscribeDelegate.ts create mode 100644 frontend/src/modules/2DViewer/layers/delegates/SettingDelegate.ts create mode 100644 frontend/src/modules/2DViewer/layers/delegates/SettingsContextDelegate.ts create mode 100644 frontend/src/modules/2DViewer/layers/delegates/UnsubscribeHandlerDelegate.ts create mode 100644 frontend/src/modules/2DViewer/layers/implementations/layers/DrilledWellTrajectoriesLayer/DrilledWellTrajectoriesContext.ts create mode 100644 frontend/src/modules/2DViewer/layers/implementations/layers/DrilledWellTrajectoriesLayer/DrilledWellTrajectoriesLayer.ts create mode 100644 frontend/src/modules/2DViewer/layers/implementations/layers/DrilledWellTrajectoriesLayer/types.ts create mode 100644 frontend/src/modules/2DViewer/layers/implementations/layers/DrilledWellborePicksLayer/DrilledWellborePicksContext.ts create mode 100644 frontend/src/modules/2DViewer/layers/implementations/layers/DrilledWellborePicksLayer/DrilledWellborePicksLayer.ts create mode 100644 frontend/src/modules/2DViewer/layers/implementations/layers/DrilledWellborePicksLayer/types.ts create mode 100644 frontend/src/modules/2DViewer/layers/implementations/layers/ObservedSurfaceLayer/ObservedSurfaceContext.ts create mode 100644 frontend/src/modules/2DViewer/layers/implementations/layers/ObservedSurfaceLayer/ObservedSurfaceLayer.ts create mode 100644 frontend/src/modules/2DViewer/layers/implementations/layers/ObservedSurfaceLayer/types.ts create mode 100644 frontend/src/modules/2DViewer/layers/implementations/layers/RealizationGridLayer/RealizationGridContext.ts create mode 100644 frontend/src/modules/2DViewer/layers/implementations/layers/RealizationGridLayer/RealizationGridLayer.ts create mode 100644 frontend/src/modules/2DViewer/layers/implementations/layers/RealizationGridLayer/types.ts create mode 100644 frontend/src/modules/2DViewer/layers/implementations/layers/RealizationPolygonsLayer/RealizationPolygonsContext.ts create mode 100644 frontend/src/modules/2DViewer/layers/implementations/layers/RealizationPolygonsLayer/RealizationPolygonsLayer.ts create mode 100644 frontend/src/modules/2DViewer/layers/implementations/layers/RealizationPolygonsLayer/types.ts create mode 100644 frontend/src/modules/2DViewer/layers/implementations/layers/RealizationSurfaceLayer/RealizationSurfaceContext.ts create mode 100644 frontend/src/modules/2DViewer/layers/implementations/layers/RealizationSurfaceLayer/RealizationSurfaceLayer.ts create mode 100644 frontend/src/modules/2DViewer/layers/implementations/layers/RealizationSurfaceLayer/types.ts create mode 100644 frontend/src/modules/2DViewer/layers/implementations/layers/StatisticalSurfaceLayer/StatisticalSurfaceContext.ts create mode 100644 frontend/src/modules/2DViewer/layers/implementations/layers/StatisticalSurfaceLayer/StatisticalSurfaceLayer.ts create mode 100644 frontend/src/modules/2DViewer/layers/implementations/layers/StatisticalSurfaceLayer/types.ts create mode 100644 frontend/src/modules/2DViewer/layers/implementations/settings/DrilledWellbores.tsx create mode 100644 frontend/src/modules/2DViewer/layers/implementations/settings/Ensemble.tsx create mode 100644 frontend/src/modules/2DViewer/layers/implementations/settings/GridAttribute.tsx create mode 100644 frontend/src/modules/2DViewer/layers/implementations/settings/GridLayer.tsx create mode 100644 frontend/src/modules/2DViewer/layers/implementations/settings/GridName.tsx create mode 100644 frontend/src/modules/2DViewer/layers/implementations/settings/PolygonsAttribute.tsx create mode 100644 frontend/src/modules/2DViewer/layers/implementations/settings/PolygonsName.tsx create mode 100644 frontend/src/modules/2DViewer/layers/implementations/settings/Realization.tsx create mode 100644 frontend/src/modules/2DViewer/layers/implementations/settings/Sensitivity.tsx create mode 100644 frontend/src/modules/2DViewer/layers/implementations/settings/ShowGridLines.tsx create mode 100644 frontend/src/modules/2DViewer/layers/implementations/settings/StatisticFunction.tsx create mode 100644 frontend/src/modules/2DViewer/layers/implementations/settings/SurfaceAttribute.tsx create mode 100644 frontend/src/modules/2DViewer/layers/implementations/settings/SurfaceName.tsx create mode 100644 frontend/src/modules/2DViewer/layers/implementations/settings/TimeOrInterval.tsx create mode 100644 frontend/src/modules/2DViewer/layers/implementations/settings/settingsTypes.ts create mode 100644 frontend/src/modules/2DViewer/layers/interfaces.ts create mode 100644 frontend/src/modules/2DViewer/layers/queryConstants.ts create mode 100644 frontend/src/modules/2DViewer/layers/utils.ts create mode 100644 frontend/src/modules/2DViewer/loadModule.tsx create mode 100644 frontend/src/modules/2DViewer/preview.tsx create mode 100644 frontend/src/modules/2DViewer/preview.webp create mode 100644 frontend/src/modules/2DViewer/registerModule.ts create mode 100644 frontend/src/modules/2DViewer/settings/atoms/baseAtoms.ts create mode 100644 frontend/src/modules/2DViewer/settings/atoms/derivedAtoms.ts create mode 100644 frontend/src/modules/2DViewer/settings/components/layerManagerComponent.tsx create mode 100644 frontend/src/modules/2DViewer/settings/settings.tsx create mode 100644 frontend/src/modules/2DViewer/types.ts create mode 100644 frontend/src/modules/2DViewer/view/components/LayersWrapper.tsx create mode 100644 frontend/src/modules/2DViewer/view/components/ReadoutBoxWrapper.tsx create mode 100644 frontend/src/modules/2DViewer/view/components/ReadoutWrapper.tsx create mode 100644 frontend/src/modules/2DViewer/view/components/Toolbar.tsx create mode 100644 frontend/src/modules/2DViewer/view/customDeckGlLayers/AdvancedWellsLayer.ts create mode 100644 frontend/src/modules/2DViewer/view/customDeckGlLayers/PlaceholderLayer.ts create mode 100644 frontend/src/modules/2DViewer/view/customDeckGlLayers/WellborePicksLayer.ts create mode 100644 frontend/src/modules/2DViewer/view/utils/layerFactory.ts create mode 100644 frontend/src/modules/2DViewer/view/utils/makeViewsAndLayers.ts create mode 100644 frontend/src/modules/2DViewer/view/view.tsx create mode 100644 frontend/src/modules/_shared/components/Toolbar/index.ts create mode 100644 frontend/src/modules/_shared/components/Toolbar/toolbar.tsx create mode 100644 frontend/src/modules/_shared/components/Toolbar/toolbarDivider.tsx diff --git a/frontend/src/modules/2DViewer/interfaces.ts b/frontend/src/modules/2DViewer/interfaces.ts new file mode 100644 index 000000000..1c42c5ce3 --- /dev/null +++ b/frontend/src/modules/2DViewer/interfaces.ts @@ -0,0 +1,23 @@ +import { InterfaceInitialization } from "@framework/UniDirectionalModuleComponentsInterface"; + +import { LayerManager } from "./layers/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/ColorScale.ts b/frontend/src/modules/2DViewer/layers/ColorScale.ts new file mode 100644 index 000000000..6463ac651 --- /dev/null +++ b/frontend/src/modules/2DViewer/layers/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 { LayerManager, LayerManagerTopic } from "./LayerManager"; +import { ItemDelegate } from "./delegates/ItemDelegate"; +import { Item, SerializedColorScale } from "./interfaces"; + +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: "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/DeltaSurface.ts b/frontend/src/modules/2DViewer/layers/DeltaSurface.ts new file mode 100644 index 000000000..e7a9d5f8e --- /dev/null +++ b/frontend/src/modules/2DViewer/layers/DeltaSurface.ts @@ -0,0 +1,85 @@ +import { LayerManager } from "./LayerManager"; +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, instanceofLayer } from "./interfaces"; + +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"); + // Fetch 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: "delta-surface", + children: this.getGroupDelegate().serializeChildren(), + }; + } +} diff --git a/frontend/src/modules/2DViewer/layers/Dependency.ts b/frontend/src/modules/2DViewer/layers/Dependency.ts new file mode 100644 index 000000000..af2904299 --- /dev/null +++ b/frontend/src/modules/2DViewer/layers/Dependency.ts @@ -0,0 +1,210 @@ +import { isCancelledError } from "@tanstack/react-query"; + +import { isEqual } from "lodash"; + +import { GlobalSettings } from "./LayerManager"; +import { SettingsContextDelegate } from "./delegates/SettingsContextDelegate"; +import { Settings, UpdateFunc } from "./interfaces"; + +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; + } + + 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); + } + + 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/DeserializationFactory.ts b/frontend/src/modules/2DViewer/layers/DeserializationFactory.ts new file mode 100644 index 000000000..4f14b1477 --- /dev/null +++ b/frontend/src/modules/2DViewer/layers/DeserializationFactory.ts @@ -0,0 +1,70 @@ +import { ColorScale } from "./ColorScale"; +import { LayerManager } from "./LayerManager"; +import { LayerRegistry } from "./LayerRegistry"; +import { SettingRegistry } from "./SettingRegistry"; +import { SettingsGroup } from "./SettingsGroup"; +import { SharedSetting } from "./SharedSetting"; +import { View } from "./View"; +import { + Item, + SerializedColorScale, + SerializedItem, + SerializedLayer, + SerializedSettingsGroup, + SerializedSharedSetting, + SerializedView, +} from "./interfaces"; + +export class DeserializationFactory { + private _layerManager: LayerManager; + + constructor(layerManager: LayerManager) { + this._layerManager = layerManager; + } + + makeItem(serialized: SerializedItem): Item { + if (serialized.type === "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 === "view") { + const serializedView = serialized as SerializedView; + const view = new View(serializedView.name, this._layerManager, serializedView.color); + view.deserializeState(serializedView); + return view; + } + + if (serialized.type === "settings-group") { + const serializedSettingsGroup = serialized as SerializedSettingsGroup; + const settingsGroup = new SettingsGroup(serializedSettingsGroup.name, this._layerManager); + settingsGroup.deserializeState(serializedSettingsGroup); + return settingsGroup; + } + + if (serialized.type === "color-scale") { + const serializedColorScale = serialized as SerializedColorScale; + const colorScale = new ColorScale(serializedColorScale.name, this._layerManager); + colorScale.deserializeState(serializedColorScale); + return colorScale; + } + + if (serialized.type === "delta-surface") { + throw new Error("DeltaSurface deserialization not implemented"); + } + + if (serialized.type === "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(`Unknown serialized item type: ${serialized.type}`); + } +} diff --git a/frontend/src/modules/2DViewer/layers/LayerManager.ts b/frontend/src/modules/2DViewer/layers/LayerManager.ts new file mode 100644 index 000000000..dc9c7cfab --- /dev/null +++ b/frontend/src/modules/2DViewer/layers/LayerManager.ts @@ -0,0 +1,209 @@ +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 } 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; +}; + +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: "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/LayerRegistry.ts b/frontend/src/modules/2DViewer/layers/LayerRegistry.ts new file mode 100644 index 000000000..cb1455e94 --- /dev/null +++ b/frontend/src/modules/2DViewer/layers/LayerRegistry.ts @@ -0,0 +1,18 @@ +import { LayerManager } from "./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/SettingRegistry.ts b/frontend/src/modules/2DViewer/layers/SettingRegistry.ts new file mode 100644 index 000000000..b8e8b8a76 --- /dev/null +++ b/frontend/src/modules/2DViewer/layers/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/SettingsGroup.ts b/frontend/src/modules/2DViewer/layers/SettingsGroup.ts new file mode 100644 index 000000000..7ff819b2c --- /dev/null +++ b/frontend/src/modules/2DViewer/layers/SettingsGroup.ts @@ -0,0 +1,36 @@ +import { LayerManager } from "./LayerManager"; +import { GroupDelegate } from "./delegates/GroupDelegate"; +import { ItemDelegate } from "./delegates/ItemDelegate"; +import { Group, SerializedSettingsGroup } from "./interfaces"; + +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: "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/SharedSetting.ts b/frontend/src/modules/2DViewer/layers/SharedSetting.ts new file mode 100644 index 000000000..7446c0c42 --- /dev/null +++ b/frontend/src/modules/2DViewer/layers/SharedSetting.ts @@ -0,0 +1,118 @@ +import { LayerManager, LayerManagerTopic } from "./LayerManager"; +import { ItemDelegate } from "./delegates/ItemDelegate"; +import { SettingTopic } from "./delegates/SettingDelegate"; +import { UnsubscribeHandlerDelegate } from "./delegates/UnsubscribeHandlerDelegate"; +import { Item, Layer, SerializedSharedSetting, Setting, instanceofLayer } from "./interfaces"; + +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: "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/View.ts b/frontend/src/modules/2DViewer/layers/View.ts new file mode 100644 index 000000000..f39c308fa --- /dev/null +++ b/frontend/src/modules/2DViewer/layers/View.ts @@ -0,0 +1,38 @@ +import { LayerManager } from "./LayerManager"; +import { GroupDelegate } from "./delegates/GroupDelegate"; +import { ItemDelegate } from "./delegates/ItemDelegate"; +import { Group, SerializedView } from "./interfaces"; + +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: "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/components/ColorScaleComponent.tsx b/frontend/src/modules/2DViewer/layers/components/ColorScaleComponent.tsx new file mode 100644 index 000000000..91836a135 --- /dev/null +++ b/frontend/src/modules/2DViewer/layers/components/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 { RemoveButton } from "./RemoveButton"; + +import { ColorScale } from "../ColorScale"; +import { ItemDelegateTopic } from "../delegates/ItemDelegate"; +import { usePublishSubscribeTopicValue } from "../delegates/PublishSubscribeDelegate"; + +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/components/DeltaSurfaceComponent.tsx b/frontend/src/modules/2DViewer/layers/components/DeltaSurfaceComponent.tsx new file mode 100644 index 000000000..0dfa0eb61 --- /dev/null +++ b/frontend/src/modules/2DViewer/layers/components/DeltaSurfaceComponent.tsx @@ -0,0 +1,78 @@ +import { SortableListGroup } from "@lib/components/SortableList"; + +import { EditName } from "./EditName"; +import { EmptyContent } from "./EmptyContent"; +import { ExpandCollapseAllButton } from "./ExpandCollapseAllButton"; +import { LayersActionGroup, LayersActions } from "./LayersActions"; +import { RemoveButton } from "./RemoveButton"; +import { VisibilityToggle } from "./VisibilityToggle"; +import { makeComponent } from "./utils"; + +import { DeltaSurface } from "../DeltaSurface"; +import { GroupDelegateTopic } from "../delegates/GroupDelegate"; +import { ItemDelegateTopic } from "../delegates/ItemDelegate"; +import { usePublishSubscribeTopicValue } from "../delegates/PublishSubscribeDelegate"; +import { Group, Item, instanceofLayer } from "../interfaces"; + +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) => makeComponent(child, props.actions, props.onActionClick))} +
+ ); +} diff --git a/frontend/src/modules/2DViewer/layers/components/EditName.tsx b/frontend/src/modules/2DViewer/layers/components/EditName.tsx new file mode 100644 index 000000000..b4ec032ee --- /dev/null +++ b/frontend/src/modules/2DViewer/layers/components/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/components/EmptyContent.tsx b/frontend/src/modules/2DViewer/layers/components/EmptyContent.tsx new file mode 100644 index 000000000..ab8a3db27 --- /dev/null +++ b/frontend/src/modules/2DViewer/layers/components/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/components/ExpandCollapseAllButton.tsx b/frontend/src/modules/2DViewer/layers/components/ExpandCollapseAllButton.tsx new file mode 100644 index 000000000..49ecbfb5f --- /dev/null +++ b/frontend/src/modules/2DViewer/layers/components/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/components/LayerComponent.tsx b/frontend/src/modules/2DViewer/layers/components/LayerComponent.tsx new file mode 100644 index 000000000..05e3d3fdc --- /dev/null +++ b/frontend/src/modules/2DViewer/layers/components/LayerComponent.tsx @@ -0,0 +1,167 @@ +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 { EditName } from "./EditName"; +import { RemoveButton } from "./RemoveButton"; +import { SettingComponent } from "./SettingComponent"; +import { VisibilityToggle } from "./VisibilityToggle"; + +import { ItemDelegateTopic } from "../delegates/ItemDelegate"; +import { LayerDelegateTopic, LayerStatus } from "../delegates/LayerDelegate"; +import { usePublishSubscribeTopicValue } from "../delegates/PublishSubscribeDelegate"; +import { SettingsContextDelegateTopic, SettingsContextLoadingState } from "../delegates/SettingsContextDelegate"; +import { Layer, Setting } from "../interfaces"; + +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/components/LayersActions.tsx b/frontend/src/modules/2DViewer/layers/components/LayersActions.tsx new file mode 100644 index 000000000..f063c0a9a --- /dev/null +++ b/frontend/src/modules/2DViewer/layers/components/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/components/RemoveButton.tsx b/frontend/src/modules/2DViewer/layers/components/RemoveButton.tsx new file mode 100644 index 000000000..10a5a0caf --- /dev/null +++ b/frontend/src/modules/2DViewer/layers/components/RemoveButton.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 RemoveButtonProps = { + item: Item; +}; + +export function RemoveButton(props: RemoveButtonProps): 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/components/SettingComponent.tsx b/frontend/src/modules/2DViewer/layers/components/SettingComponent.tsx new file mode 100644 index 000000000..122fad595 --- /dev/null +++ b/frontend/src/modules/2DViewer/layers/components/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 { LayerManager, LayerManagerTopic } from "../LayerManager"; +import { usePublishSubscribeTopicValue } from "../delegates/PublishSubscribeDelegate"; +import { SettingTopic } from "../delegates/SettingDelegate"; +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/components/SettingsGroupComponent.tsx b/frontend/src/modules/2DViewer/layers/components/SettingsGroupComponent.tsx new file mode 100644 index 000000000..313ed2305 --- /dev/null +++ b/frontend/src/modules/2DViewer/layers/components/SettingsGroupComponent.tsx @@ -0,0 +1,73 @@ +import { SortableListGroup } from "@lib/components/SortableList"; +import { SettingsApplications } from "@mui/icons-material"; + +import { EmptyContent } from "./EmptyContent"; +import { ExpandCollapseAllButton } from "./ExpandCollapseAllButton"; +import { LayersActionGroup, LayersActions } from "./LayersActions"; +import { RemoveButton } from "./RemoveButton"; +import { makeComponent } from "./utils"; + +import { GroupDelegateTopic } from "../delegates/GroupDelegate"; +import { ItemDelegateTopic } from "../delegates/ItemDelegate"; +import { usePublishSubscribeTopicValue } from "../delegates/PublishSubscribeDelegate"; +import { Group, Item } from "../interfaces"; + +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) => makeComponent(child, props.actions, props.onActionClick))} + + ); +} diff --git a/frontend/src/modules/2DViewer/layers/components/SharedSettingComponent.tsx b/frontend/src/modules/2DViewer/layers/components/SharedSettingComponent.tsx new file mode 100644 index 000000000..27523b6d5 --- /dev/null +++ b/frontend/src/modules/2DViewer/layers/components/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 { SettingComponent } from "./SettingComponent"; + +import { SharedSetting } from "../SharedSetting"; +import { ItemDelegateTopic } from "../delegates/ItemDelegate"; +import { usePublishSubscribeTopicValue } from "../delegates/PublishSubscribeDelegate"; + +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/components/ViewComponent.tsx b/frontend/src/modules/2DViewer/layers/components/ViewComponent.tsx new file mode 100644 index 000000000..d91ae8ec8 --- /dev/null +++ b/frontend/src/modules/2DViewer/layers/components/ViewComponent.tsx @@ -0,0 +1,77 @@ +import { SortableListGroup } from "@lib/components/SortableList"; + +import { EditName } from "./EditName"; +import { EmptyContent } from "./EmptyContent"; +import { ExpandCollapseAllButton } from "./ExpandCollapseAllButton"; +import { LayersActionGroup, LayersActions } from "./LayersActions"; +import { RemoveButton } from "./RemoveButton"; +import { VisibilityToggle } from "./VisibilityToggle"; +import { makeComponent } from "./utils"; + +import { GroupDelegateTopic } from "../delegates/GroupDelegate"; +import { ItemDelegateTopic } from "../delegates/ItemDelegate"; +import { usePublishSubscribeTopicValue } from "../delegates/PublishSubscribeDelegate"; +import { Group, Item } from "../interfaces"; + +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) => makeComponent(child, props.actions, props.onActionClick))} +
+ ); +} diff --git a/frontend/src/modules/2DViewer/layers/components/VisibilityToggle.tsx b/frontend/src/modules/2DViewer/layers/components/VisibilityToggle.tsx new file mode 100644 index 000000000..366c17874 --- /dev/null +++ b/frontend/src/modules/2DViewer/layers/components/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/components/utils.tsx b/frontend/src/modules/2DViewer/layers/components/utils.tsx new file mode 100644 index 000000000..0f1f3f31b --- /dev/null +++ b/frontend/src/modules/2DViewer/layers/components/utils.tsx @@ -0,0 +1,93 @@ +import { SortableListItemProps } from "@lib/components/SortableList"; + +import { ColorScaleComponent } from "./ColorScaleComponent"; +import { DeltaSurfaceComponent } from "./DeltaSurfaceComponent"; +import { LayerComponent } from "./LayerComponent"; +import { LayersActionGroup } from "./LayersActions"; +import { SettingsGroupComponent } from "./SettingsGroupComponent"; +import { SharedSettingComponent } from "./SharedSettingComponent"; +import { ViewComponent } from "./ViewComponent"; + +import { ColorScale } from "../ColorScale"; +import { DeltaSurface } from "../DeltaSurface"; +import { SettingsGroup } from "../SettingsGroup"; +import { SharedSetting } from "../SharedSetting"; +import { View } from "../View"; +import { Group, Item, instanceofGroup, instanceofLayer } from "../interfaces"; + +export function makeComponent( + item: Item, + layerActions?: LayersActionGroup[], + onActionClick?: (identifier: string, group: Group) => void +): React.ReactElement { + if (instanceofLayer(item)) { + return ; + } + if (instanceofGroup(item)) { + if (item instanceof SettingsGroup) { + return ( + + ); + } else if (item instanceof View) { + return ( + + ); + } else if (item instanceof DeltaSurface) { + return ( + + ); + } + } + if (item instanceof SharedSetting) { + return ; + } + if (item instanceof ColorScale) { + return ; + } + throw new Error("Not implemented"); +} + +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/delegates/GroupDelegate.ts b/frontend/src/modules/2DViewer/layers/delegates/GroupDelegate.ts new file mode 100644 index 000000000..613be3e8b --- /dev/null +++ b/frontend/src/modules/2DViewer/layers/delegates/GroupDelegate.ts @@ -0,0 +1,249 @@ +import { ItemDelegateTopic } from "./ItemDelegate"; +import { PublishSubscribe, PublishSubscribeDelegate } from "./PublishSubscribeDelegate"; +import { UnsubscribeHandlerDelegate } from "./UnsubscribeHandlerDelegate"; + +import { DeserializationFactory } from "../DeserializationFactory"; +import { LayerManagerTopic } from "../LayerManager"; +import { SharedSetting } from "../SharedSetting"; +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 }; +}; + +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..0967d611e --- /dev/null +++ b/frontend/src/modules/2DViewer/layers/delegates/ItemDelegate.ts @@ -0,0 +1,153 @@ +import { isEqual } from "lodash"; +import { v4 } from "uuid"; + +import { GroupDelegate } from "./GroupDelegate"; +import { PublishSubscribe, PublishSubscribeDelegate } from "./PublishSubscribeDelegate"; + +import { LayerManager, LayerManagerTopic } from "../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; +}; + +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..3d9dde990 --- /dev/null +++ b/frontend/src/modules/2DViewer/layers/delegates/LayerDelegate.ts @@ -0,0 +1,330 @@ +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 "../LayerManager"; +import { SharedSetting } from "../SharedSetting"; +import { BoundingBox, Layer, SerializedLayer, 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; +}; +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.fechData(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: "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..e20285b14 --- /dev/null +++ b/frontend/src/modules/2DViewer/layers/delegates/SettingDelegate.ts @@ -0,0 +1,341 @@ +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; +}; + +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..5a0f21b15 --- /dev/null +++ b/frontend/src/modules/2DViewer/layers/delegates/SettingsContextDelegate.ts @@ -0,0 +1,379 @@ +import { PublishSubscribe, PublishSubscribeDelegate } from "./PublishSubscribeDelegate"; +import { SettingTopic } from "./SettingDelegate"; +import { UnsubscribeHandlerDelegate } from "./UnsubscribeHandlerDelegate"; + +import { Dependency } from "../Dependency"; +import { GlobalSettings, LayerManager, LayerManagerTopic } from "../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] }; +}; + +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..067c8d830 --- /dev/null +++ b/frontend/src/modules/2DViewer/layers/delegates/UnsubscribeHandlerDelegate.ts @@ -0,0 +1,30 @@ +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/implementations/layers/DrilledWellTrajectoriesLayer/DrilledWellTrajectoriesContext.ts b/frontend/src/modules/2DViewer/layers/implementations/layers/DrilledWellTrajectoriesLayer/DrilledWellTrajectoriesContext.ts new file mode 100644 index 000000000..4b9e466de --- /dev/null +++ b/frontend/src/modules/2DViewer/layers/implementations/layers/DrilledWellTrajectoriesLayer/DrilledWellTrajectoriesContext.ts @@ -0,0 +1,86 @@ +import { apiService } from "@framework/ApiService"; +import { LayerManager } from "@modules/2DViewer/layers/LayerManager"; +import { SettingsContextDelegate } from "@modules/2DViewer/layers/delegates/SettingsContextDelegate"; +import { SettingType } from "@modules/2DViewer/layers/implementations/settings/settingsTypes"; +import { CACHE_TIME, STALE_TIME } from "@modules/2DViewer/layers/queryConstants"; +import { cancelPromiseOnAbort } from "@modules/2DViewer/layers/utils"; + +import { DrilledWellTrajectoriesSettings } from "./types"; + +import { DefineDependenciesArgs, SettingsContext } from "../../../interfaces"; +import { DrilledWellbores } from "../../settings/DrilledWellbores"; +import { Ensemble } from "../../settings/Ensemble"; + +export class DrilledWellTrajectoriesContext implements SettingsContext { + private _contextDelegate: SettingsContextDelegate; + + constructor(layerManager: LayerManager) { + this._contextDelegate = new SettingsContextDelegate< + DrilledWellTrajectoriesSettings, + keyof DrilledWellTrajectoriesSettings + >(this, layerManager, { + [SettingType.ENSEMBLE]: new Ensemble(), + [SettingType.SMDA_WELLBORE_HEADERS]: new DrilledWellbores(), + }); + } + + 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/implementations/layers/DrilledWellTrajectoriesLayer/DrilledWellTrajectoriesLayer.ts b/frontend/src/modules/2DViewer/layers/implementations/layers/DrilledWellTrajectoriesLayer/DrilledWellTrajectoriesLayer.ts new file mode 100644 index 000000000..961b1f4e1 --- /dev/null +++ b/frontend/src/modules/2DViewer/layers/implementations/layers/DrilledWellTrajectoriesLayer/DrilledWellTrajectoriesLayer.ts @@ -0,0 +1,125 @@ +import { WellboreTrajectory_api } from "@api"; +import { apiService } from "@framework/ApiService"; +import { LayerManager } from "@modules/2DViewer/layers/LayerManager"; +import { LayerRegistry } from "@modules/2DViewer/layers/LayerRegistry"; +import { ItemDelegate } from "@modules/2DViewer/layers/delegates/ItemDelegate"; +import { SettingType } from "@modules/2DViewer/layers/implementations/settings/settingsTypes"; +import { QueryClient } from "@tanstack/react-query"; + +import { isEqual } from "lodash"; + +import { DrilledWellTrajectoriesContext } from "./DrilledWellTrajectoriesContext"; +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 DrilledWellTrajectoriesContext(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; + } + + fechData(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 + 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/implementations/layers/DrilledWellTrajectoriesLayer/types.ts b/frontend/src/modules/2DViewer/layers/implementations/layers/DrilledWellTrajectoriesLayer/types.ts new file mode 100644 index 000000000..f4877d5b6 --- /dev/null +++ b/frontend/src/modules/2DViewer/layers/implementations/layers/DrilledWellTrajectoriesLayer/types.ts @@ -0,0 +1,8 @@ +import { WellboreHeader_api } from "@api"; +import { EnsembleIdent } from "@framework/EnsembleIdent"; +import { SettingType } from "@modules/2DViewer/layers/implementations/settings/settingsTypes"; + +export type DrilledWellTrajectoriesSettings = { + [SettingType.ENSEMBLE]: EnsembleIdent | null; + [SettingType.SMDA_WELLBORE_HEADERS]: WellboreHeader_api[] | null; +}; diff --git a/frontend/src/modules/2DViewer/layers/implementations/layers/DrilledWellborePicksLayer/DrilledWellborePicksContext.ts b/frontend/src/modules/2DViewer/layers/implementations/layers/DrilledWellborePicksLayer/DrilledWellborePicksContext.ts new file mode 100644 index 000000000..e2e7fb5ff --- /dev/null +++ b/frontend/src/modules/2DViewer/layers/implementations/layers/DrilledWellborePicksLayer/DrilledWellborePicksContext.ts @@ -0,0 +1,137 @@ +import { apiService } from "@framework/ApiService"; +import { LayerManager } from "@modules/2DViewer/layers/LayerManager"; +import { SettingsContextDelegate } from "@modules/2DViewer/layers/delegates/SettingsContextDelegate"; +import { SettingType } from "@modules/2DViewer/layers/implementations/settings/settingsTypes"; +import { CACHE_TIME, STALE_TIME } from "@modules/2DViewer/layers/queryConstants"; +import { cancelPromiseOnAbort } from "@modules/2DViewer/layers/utils"; + +import { DrilledWellborePicksSettings } from "./types"; + +import { DefineDependenciesArgs, SettingsContext } from "../../../interfaces"; +import { DrilledWellbores } from "../../settings/DrilledWellbores"; +import { Ensemble } from "../../settings/Ensemble"; +import { SurfaceName } from "../../settings/SurfaceName"; + +export class DrilledWellborePicksContext implements SettingsContext { + private _contextDelegate: SettingsContextDelegate; + + constructor(layerManager: LayerManager) { + this._contextDelegate = new SettingsContextDelegate< + DrilledWellborePicksSettings, + keyof DrilledWellborePicksSettings + >(this, layerManager, { + [SettingType.ENSEMBLE]: new Ensemble(), + [SettingType.SMDA_WELLBORE_HEADERS]: new DrilledWellbores(), + [SettingType.SURFACE_NAME]: new SurfaceName(), + }); + } + + 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 fieldIdentifier = ensemble.getFieldIdentifier(); + const stratColumnIdentifier = ensemble.getStratigraphicColumnIdentifier(); + + return await queryClient.fetchQuery({ + queryKey: ["getPickStratigraphy", fieldIdentifier, stratColumnIdentifier], + queryFn: () => + cancelPromiseOnAbort( + apiService.well.getWellborePickIdentifiers(fieldIdentifier, 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/implementations/layers/DrilledWellborePicksLayer/DrilledWellborePicksLayer.ts b/frontend/src/modules/2DViewer/layers/implementations/layers/DrilledWellborePicksLayer/DrilledWellborePicksLayer.ts new file mode 100644 index 000000000..27fb63e4a --- /dev/null +++ b/frontend/src/modules/2DViewer/layers/implementations/layers/DrilledWellborePicksLayer/DrilledWellborePicksLayer.ts @@ -0,0 +1,126 @@ +import { WellborePick_api } from "@api"; +import { apiService } from "@framework/ApiService"; +import { LayerManager } from "@modules/2DViewer/layers/LayerManager"; +import { LayerRegistry } from "@modules/2DViewer/layers/LayerRegistry"; +import { ItemDelegate } from "@modules/2DViewer/layers/delegates/ItemDelegate"; +import { SettingType } from "@modules/2DViewer/layers/implementations/settings/settingsTypes"; +import { CACHE_TIME, STALE_TIME } from "@modules/2DViewer/layers/queryConstants"; +import { QueryClient } from "@tanstack/react-query"; + +import { isEqual } from "lodash"; + +import { DrilledWellborePicksContext } from "./DrilledWellborePicksContext"; +import { DrilledWellborePicksSettings } from "./types"; + +import { LayerColoringType, LayerDelegate } from "../../../delegates/LayerDelegate"; +import { BoundingBox, Layer, SerializedLayer } from "../../../interfaces"; + +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 DrilledWellborePicksContext(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; + } + + fechData(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/implementations/layers/DrilledWellborePicksLayer/types.ts b/frontend/src/modules/2DViewer/layers/implementations/layers/DrilledWellborePicksLayer/types.ts new file mode 100644 index 000000000..ace87b9b8 --- /dev/null +++ b/frontend/src/modules/2DViewer/layers/implementations/layers/DrilledWellborePicksLayer/types.ts @@ -0,0 +1,9 @@ +import { WellboreHeader_api } from "@api"; +import { EnsembleIdent } from "@framework/EnsembleIdent"; +import { SettingType } from "@modules/2DViewer/layers/implementations/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/implementations/layers/ObservedSurfaceLayer/ObservedSurfaceContext.ts b/frontend/src/modules/2DViewer/layers/implementations/layers/ObservedSurfaceLayer/ObservedSurfaceContext.ts new file mode 100644 index 000000000..8888954eb --- /dev/null +++ b/frontend/src/modules/2DViewer/layers/implementations/layers/ObservedSurfaceLayer/ObservedSurfaceContext.ts @@ -0,0 +1,144 @@ +import { SurfaceMetaSet_api, SurfaceTimeType_api } from "@api"; +import { apiService } from "@framework/ApiService"; +import { LayerManager } from "@modules/2DViewer/layers/LayerManager"; +import { SettingsContextDelegate } from "@modules/2DViewer/layers/delegates/SettingsContextDelegate"; +import { SettingType } from "@modules/2DViewer/layers/implementations/settings/settingsTypes"; +import { CACHE_TIME, STALE_TIME } from "@modules/2DViewer/layers/queryConstants"; +import { cancelPromiseOnAbort } from "@modules/2DViewer/layers/utils"; + +import { ObservedSurfaceSettings } from "./types"; + +import { DefineDependenciesArgs, SettingsContext } from "../../../interfaces"; +import { Ensemble } from "../../settings/Ensemble"; +import { SurfaceAttribute } from "../../settings/SurfaceAttribute"; +import { SurfaceName } from "../../settings/SurfaceName"; +import { TimeOrInterval } from "../../settings/TimeOrInterval"; + +export class ObservedSurfaceContext implements SettingsContext { + private _contextDelegate: SettingsContextDelegate; + private _fetchDataCache: SurfaceMetaSet_api | null = null; + + constructor(layerManager: LayerManager) { + this._contextDelegate = new SettingsContextDelegate( + this, + layerManager, + { + [SettingType.ENSEMBLE]: new Ensemble(), + [SettingType.SURFACE_ATTRIBUTE]: new SurfaceAttribute(), + [SettingType.SURFACE_NAME]: new SurfaceName(), + [SettingType.TIME_OR_INTERVAL]: new TimeOrInterval(), + } + ); + } + + 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/implementations/layers/ObservedSurfaceLayer/ObservedSurfaceLayer.ts b/frontend/src/modules/2DViewer/layers/implementations/layers/ObservedSurfaceLayer/ObservedSurfaceLayer.ts new file mode 100644 index 000000000..67ef98914 --- /dev/null +++ b/frontend/src/modules/2DViewer/layers/implementations/layers/ObservedSurfaceLayer/ObservedSurfaceLayer.ts @@ -0,0 +1,124 @@ +import { SurfaceDataPng_api } from "@api"; +import { apiService } from "@framework/ApiService"; +import { LayerManager } from "@modules/2DViewer/layers/LayerManager"; +import { LayerRegistry } from "@modules/2DViewer/layers/LayerRegistry"; +import { ItemDelegate } from "@modules/2DViewer/layers/delegates/ItemDelegate"; +import { SettingType } from "@modules/2DViewer/layers/implementations/settings/settingsTypes"; +import { CACHE_TIME, STALE_TIME } from "@modules/2DViewer/layers/queryConstants"; +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 { ObservedSurfaceContext } from "./ObservedSurfaceContext"; +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 ObservedSurfaceContext(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]; + } + + fechData(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/implementations/layers/ObservedSurfaceLayer/types.ts b/frontend/src/modules/2DViewer/layers/implementations/layers/ObservedSurfaceLayer/types.ts new file mode 100644 index 000000000..26d641e44 --- /dev/null +++ b/frontend/src/modules/2DViewer/layers/implementations/layers/ObservedSurfaceLayer/types.ts @@ -0,0 +1,9 @@ +import { EnsembleIdent } from "@framework/EnsembleIdent"; +import { SettingType } from "@modules/2DViewer/layers/implementations/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/implementations/layers/RealizationGridLayer/RealizationGridContext.ts b/frontend/src/modules/2DViewer/layers/implementations/layers/RealizationGridLayer/RealizationGridContext.ts new file mode 100644 index 000000000..e768409b5 --- /dev/null +++ b/frontend/src/modules/2DViewer/layers/implementations/layers/RealizationGridLayer/RealizationGridContext.ts @@ -0,0 +1,180 @@ +import { apiService } from "@framework/ApiService"; +import { LayerManager } from "@modules/2DViewer/layers/LayerManager"; +import { SettingsContextDelegate } from "@modules/2DViewer/layers/delegates/SettingsContextDelegate"; +import { SettingType } from "@modules/2DViewer/layers/implementations/settings/settingsTypes"; +import { CACHE_TIME, STALE_TIME } from "@modules/2DViewer/layers/queryConstants"; +import { cancelQueryOnAbort } from "@modules/2DViewer/layers/utils"; + +import { RealizationGridSettings } from "./types"; + +import { DefineDependenciesArgs, SettingsContext } from "../../../interfaces"; +import { Ensemble } from "../../settings/Ensemble"; +import { GridAttribute } from "../../settings/GridAttribute"; +import { GridLayer } from "../../settings/GridLayer"; +import { GridName } from "../../settings/GridName"; +import { Realization } from "../../settings/Realization"; +import { ShowGridLines } from "../../settings/ShowGridLines"; +import { TimeOrInterval } from "../../settings/TimeOrInterval"; + +export class RealizationGridContext implements SettingsContext { + private _contextDelegate: SettingsContextDelegate; + + constructor(layerManager: LayerManager) { + this._contextDelegate = new SettingsContextDelegate( + this, + layerManager, + { + [SettingType.ENSEMBLE]: new Ensemble(), + [SettingType.REALIZATION]: new Realization(), + [SettingType.GRID_NAME]: new GridName(), + [SettingType.GRID_ATTRIBUTE]: new GridAttribute(), + [SettingType.GRID_LAYER]: new GridLayer(), + [SettingType.TIME_OR_INTERVAL]: new TimeOrInterval(), + [SettingType.SHOW_GRID_LINES]: new ShowGridLines(), + } + ); + } + + 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, + 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; + }); + + 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/implementations/layers/RealizationGridLayer/RealizationGridLayer.ts b/frontend/src/modules/2DViewer/layers/implementations/layers/RealizationGridLayer/RealizationGridLayer.ts new file mode 100644 index 000000000..4ff662e3e --- /dev/null +++ b/frontend/src/modules/2DViewer/layers/implementations/layers/RealizationGridLayer/RealizationGridLayer.ts @@ -0,0 +1,204 @@ +import { apiService } from "@framework/ApiService"; +import { LayerManager } from "@modules/2DViewer/layers/LayerManager"; +import { LayerRegistry } from "@modules/2DViewer/layers/LayerRegistry"; +import { ItemDelegate } from "@modules/2DViewer/layers/delegates/ItemDelegate"; +import { SettingType } from "@modules/2DViewer/layers/implementations/settings/settingsTypes"; +import { CACHE_TIME, STALE_TIME } from "@modules/2DViewer/layers/queryConstants"; +import { + GridMappedProperty_trans, + GridSurface_trans, + transformGridMappedProperty, + transformGridSurface, +} from "@modules/3DViewer/view/queries/queryDataTransforms"; +import { QueryClient } from "@tanstack/react-query"; + +import { isEqual } from "lodash"; + +import { RealizationGridContext } from "./RealizationGridContext"; +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 RealizationGridContext(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]; + } + + fechData(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/implementations/layers/RealizationGridLayer/types.ts b/frontend/src/modules/2DViewer/layers/implementations/layers/RealizationGridLayer/types.ts new file mode 100644 index 000000000..8dbbeb749 --- /dev/null +++ b/frontend/src/modules/2DViewer/layers/implementations/layers/RealizationGridLayer/types.ts @@ -0,0 +1,12 @@ +import { EnsembleIdent } from "@framework/EnsembleIdent"; +import { SettingType } from "@modules/2DViewer/layers/implementations/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/implementations/layers/RealizationPolygonsLayer/RealizationPolygonsContext.ts b/frontend/src/modules/2DViewer/layers/implementations/layers/RealizationPolygonsLayer/RealizationPolygonsContext.ts new file mode 100644 index 000000000..d1a5c73ed --- /dev/null +++ b/frontend/src/modules/2DViewer/layers/implementations/layers/RealizationPolygonsLayer/RealizationPolygonsContext.ts @@ -0,0 +1,126 @@ +import { PolygonsMeta_api } from "@api"; +import { apiService } from "@framework/ApiService"; +import { LayerManager } from "@modules/2DViewer/layers/LayerManager"; +import { SettingsContextDelegate } from "@modules/2DViewer/layers/delegates/SettingsContextDelegate"; +import { SettingType } from "@modules/2DViewer/layers/implementations/settings/settingsTypes"; +import { CACHE_TIME, STALE_TIME } from "@modules/2DViewer/layers/queryConstants"; +import { cancelPromiseOnAbort } from "@modules/2DViewer/layers/utils"; + +import { RealizationPolygonsSettings } from "./types"; + +import { DefineDependenciesArgs, SettingsContext } from "../../../interfaces"; +import { Ensemble } from "../../settings/Ensemble"; +import { PolygonsAttribute } from "../../settings/PolygonsAttribute"; +import { PolygonsName } from "../../settings/PolygonsName"; +import { Realization } from "../../settings/Realization"; + +export class RealizationPolygonsContext implements SettingsContext { + private _contextDelegate: SettingsContextDelegate; + private _fetchDataCache: PolygonsMeta_api[] | null = null; + + constructor(layerManager: LayerManager) { + this._contextDelegate = new SettingsContextDelegate< + RealizationPolygonsSettings, + keyof RealizationPolygonsSettings + >(this, layerManager, { + [SettingType.ENSEMBLE]: new Ensemble(), + [SettingType.REALIZATION]: new Realization(), + [SettingType.POLYGONS_ATTRIBUTE]: new PolygonsAttribute(), + [SettingType.POLYGONS_NAME]: new PolygonsName(), + }); + } + + 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; + }); + + 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/implementations/layers/RealizationPolygonsLayer/RealizationPolygonsLayer.ts b/frontend/src/modules/2DViewer/layers/implementations/layers/RealizationPolygonsLayer/RealizationPolygonsLayer.ts new file mode 100644 index 000000000..7cf7e6753 --- /dev/null +++ b/frontend/src/modules/2DViewer/layers/implementations/layers/RealizationPolygonsLayer/RealizationPolygonsLayer.ts @@ -0,0 +1,124 @@ +import { PolygonData_api } from "@api"; +import { apiService } from "@framework/ApiService"; +import { LayerManager } from "@modules/2DViewer/layers/LayerManager"; +import { LayerRegistry } from "@modules/2DViewer/layers/LayerRegistry"; +import { ItemDelegate } from "@modules/2DViewer/layers/delegates/ItemDelegate"; +import { LayerColoringType, LayerDelegate } from "@modules/2DViewer/layers/delegates/LayerDelegate"; +import { SettingType } from "@modules/2DViewer/layers/implementations/settings/settingsTypes"; +import { CACHE_TIME, STALE_TIME } from "@modules/2DViewer/layers/queryConstants"; +import { QueryClient } from "@tanstack/react-query"; + +import { isEqual } from "lodash"; + +import { RealizationPolygonsContext } from "./RealizationPolygonsContext"; +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 RealizationPolygonsContext(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; + } + + fechData(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/implementations/layers/RealizationPolygonsLayer/types.ts b/frontend/src/modules/2DViewer/layers/implementations/layers/RealizationPolygonsLayer/types.ts new file mode 100644 index 000000000..cc5807a1e --- /dev/null +++ b/frontend/src/modules/2DViewer/layers/implementations/layers/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/implementations/layers/RealizationSurfaceLayer/RealizationSurfaceContext.ts b/frontend/src/modules/2DViewer/layers/implementations/layers/RealizationSurfaceLayer/RealizationSurfaceContext.ts new file mode 100644 index 000000000..19cbdc478 --- /dev/null +++ b/frontend/src/modules/2DViewer/layers/implementations/layers/RealizationSurfaceLayer/RealizationSurfaceContext.ts @@ -0,0 +1,159 @@ +import { SurfaceTimeType_api } from "@api"; +import { apiService } from "@framework/ApiService"; +import { LayerManager } from "@modules/2DViewer/layers/LayerManager"; +import { SettingsContextDelegate } from "@modules/2DViewer/layers/delegates/SettingsContextDelegate"; +import { SettingType } from "@modules/2DViewer/layers/implementations/settings/settingsTypes"; +import { CACHE_TIME, STALE_TIME } from "@modules/2DViewer/layers/queryConstants"; +import { cancelPromiseOnAbort } from "@modules/2DViewer/layers/utils"; + +import { RealizationSurfaceSettings } from "./types"; + +import { DefineDependenciesArgs, SettingsContext } from "../../../interfaces"; +import { Ensemble } from "../../settings/Ensemble"; +import { Realization } from "../../settings/Realization"; +import { SurfaceAttribute } from "../../settings/SurfaceAttribute"; +import { SurfaceName } from "../../settings/SurfaceName"; +import { TimeOrInterval } from "../../settings/TimeOrInterval"; + +export class RealizationSurfaceContext implements SettingsContext { + private _contextDelegate: SettingsContextDelegate; + + constructor(layerManager: LayerManager) { + this._contextDelegate = new SettingsContextDelegate< + RealizationSurfaceSettings, + keyof RealizationSurfaceSettings + >(this, layerManager, { + [SettingType.ENSEMBLE]: new Ensemble(), + [SettingType.REALIZATION]: new Realization(), + [SettingType.SURFACE_ATTRIBUTE]: new SurfaceAttribute(), + [SettingType.SURFACE_NAME]: new SurfaceName(), + [SettingType.TIME_OR_INTERVAL]: new TimeOrInterval(), + }); + } + + 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/implementations/layers/RealizationSurfaceLayer/RealizationSurfaceLayer.ts b/frontend/src/modules/2DViewer/layers/implementations/layers/RealizationSurfaceLayer/RealizationSurfaceLayer.ts new file mode 100644 index 000000000..3dec51ed0 --- /dev/null +++ b/frontend/src/modules/2DViewer/layers/implementations/layers/RealizationSurfaceLayer/RealizationSurfaceLayer.ts @@ -0,0 +1,129 @@ +import { SurfaceDataPng_api, SurfaceTimeType_api } from "@api"; +import { apiService } from "@framework/ApiService"; +import { LayerManager } from "@modules/2DViewer/layers/LayerManager"; +import { LayerRegistry } from "@modules/2DViewer/layers/LayerRegistry"; +import { ItemDelegate } from "@modules/2DViewer/layers/delegates/ItemDelegate"; +import { LayerColoringType, LayerDelegate } from "@modules/2DViewer/layers/delegates/LayerDelegate"; +import { SettingType } from "@modules/2DViewer/layers/implementations/settings/settingsTypes"; +import { CACHE_TIME, STALE_TIME } from "@modules/2DViewer/layers/queryConstants"; +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 { RealizationSurfaceContext } from "./RealizationSurfaceContext"; +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 RealizationSurfaceContext(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]; + } + + fechData(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/implementations/layers/RealizationSurfaceLayer/types.ts b/frontend/src/modules/2DViewer/layers/implementations/layers/RealizationSurfaceLayer/types.ts new file mode 100644 index 000000000..4eb3e2c98 --- /dev/null +++ b/frontend/src/modules/2DViewer/layers/implementations/layers/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/implementations/layers/StatisticalSurfaceLayer/StatisticalSurfaceContext.ts b/frontend/src/modules/2DViewer/layers/implementations/layers/StatisticalSurfaceLayer/StatisticalSurfaceContext.ts new file mode 100644 index 000000000..22832de4b --- /dev/null +++ b/frontend/src/modules/2DViewer/layers/implementations/layers/StatisticalSurfaceLayer/StatisticalSurfaceContext.ts @@ -0,0 +1,173 @@ +import { SurfaceStatisticFunction_api, SurfaceTimeType_api } from "@api"; +import { apiService } from "@framework/ApiService"; +import { LayerManager } from "@modules/2DViewer/layers/LayerManager"; +import { CACHE_TIME, STALE_TIME } from "@modules/2DViewer/layers/queryConstants"; +import { cancelPromiseOnAbort } from "@modules/2DViewer/layers/utils"; + +import { StatisticalSurfaceSettings } from "./types"; + +import { SettingsContextDelegate } from "../../../delegates/SettingsContextDelegate"; +import { DefineDependenciesArgs, SettingsContext } from "../../../interfaces"; +import { Ensemble } from "../../settings/Ensemble"; +import { Sensitivity, SensitivityNameCasePair } from "../../settings/Sensitivity"; +import { StatisticFunction } from "../../settings/StatisticFunction"; +import { SurfaceAttribute } from "../../settings/SurfaceAttribute"; +import { SurfaceName } from "../../settings/SurfaceName"; +import { TimeOrInterval } from "../../settings/TimeOrInterval"; +import { SettingType } from "../../settings/settingsTypes"; + +export class StatisticalSurfaceContext implements SettingsContext { + private _contextDelegate: SettingsContextDelegate; + + constructor(layerManager: LayerManager) { + this._contextDelegate = new SettingsContextDelegate< + StatisticalSurfaceSettings, + keyof StatisticalSurfaceSettings + >(this, layerManager, { + [SettingType.ENSEMBLE]: new Ensemble(), + [SettingType.STATISTIC_FUNCTION]: new StatisticFunction(), + [SettingType.SENSITIVITY]: new Sensitivity(), + [SettingType.SURFACE_ATTRIBUTE]: new SurfaceAttribute(), + [SettingType.SURFACE_NAME]: new SurfaceName(), + [SettingType.TIME_OR_INTERVAL]: new TimeOrInterval(), + }); + } + + 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/implementations/layers/StatisticalSurfaceLayer/StatisticalSurfaceLayer.ts b/frontend/src/modules/2DViewer/layers/implementations/layers/StatisticalSurfaceLayer/StatisticalSurfaceLayer.ts new file mode 100644 index 000000000..a06c7d16a --- /dev/null +++ b/frontend/src/modules/2DViewer/layers/implementations/layers/StatisticalSurfaceLayer/StatisticalSurfaceLayer.ts @@ -0,0 +1,155 @@ +import { SurfaceDataPng_api, SurfaceTimeType_api } from "@api"; +import { apiService } from "@framework/ApiService"; +import { LayerManager } from "@modules/2DViewer/layers/LayerManager"; +import { LayerRegistry } from "@modules/2DViewer/layers/LayerRegistry"; +import { ItemDelegate } from "@modules/2DViewer/layers/delegates/ItemDelegate"; +import { LayerColoringType, LayerDelegate } from "@modules/2DViewer/layers/delegates/LayerDelegate"; +import { SettingType } from "@modules/2DViewer/layers/implementations/settings/settingsTypes"; +import { CACHE_TIME, STALE_TIME } from "@modules/2DViewer/layers/queryConstants"; +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 { StatisticalSurfaceContext } from "./StatisticalSurfaceContext"; +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 StatisticalSurfaceContext(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]; + } + + fechData(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/implementations/layers/StatisticalSurfaceLayer/types.ts b/frontend/src/modules/2DViewer/layers/implementations/layers/StatisticalSurfaceLayer/types.ts new file mode 100644 index 000000000..22bb5154f --- /dev/null +++ b/frontend/src/modules/2DViewer/layers/implementations/layers/StatisticalSurfaceLayer/types.ts @@ -0,0 +1,14 @@ +import { SurfaceStatisticFunction_api } from "@api"; +import { EnsembleIdent } from "@framework/EnsembleIdent"; +import { SettingType } from "@modules/2DViewer/layers/implementations/settings/settingsTypes"; + +import { SensitivityNameCasePair } from "../../settings/Sensitivity"; + +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/implementations/settings/DrilledWellbores.tsx b/frontend/src/modules/2DViewer/layers/implementations/settings/DrilledWellbores.tsx new file mode 100644 index 000000000..2f69789f5 --- /dev/null +++ b/frontend/src/modules/2DViewer/layers/implementations/settings/DrilledWellbores.tsx @@ -0,0 +1,135 @@ +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 { SettingType } from "./settingsTypes"; + +import { SettingRegistry } from "../../SettingRegistry"; +import { SettingDelegate } from "../../delegates/SettingDelegate"; +import { AvailableValuesType, Setting, SettingComponentProps } from "../../interfaces"; + +type ValueType = WellboreHeader_api[] | null; + +export class DrilledWellbores 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 + +
+ + ); +} + +SettingRegistry.registerSetting(DrilledWellbores); diff --git a/frontend/src/modules/2DViewer/layers/implementations/settings/Ensemble.tsx b/frontend/src/modules/2DViewer/layers/implementations/settings/Ensemble.tsx new file mode 100644 index 000000000..404033a6c --- /dev/null +++ b/frontend/src/modules/2DViewer/layers/implementations/settings/Ensemble.tsx @@ -0,0 +1,67 @@ +import React from "react"; + +import { EnsembleIdent } from "@framework/EnsembleIdent"; +import { EnsembleDropdown } from "@framework/components/EnsembleDropdown"; + +import { SettingType } from "./settingsTypes"; + +import { SettingRegistry } from "../../SettingRegistry"; +import { SettingDelegate } from "../../delegates/SettingDelegate"; +import { Setting, SettingComponentProps, ValueToStringArgs } from "../../interfaces"; + +export class Ensemble implements Setting { + private _delegate: SettingDelegate; + + constructor() { + this._delegate = new SettingDelegate(null, this); + } + + getType(): SettingType { + return SettingType.ENSEMBLE; + } + + getLabel(): string { + return "Ensemble"; + } + + getDelegate(): SettingDelegate { + return this._delegate; + } + + serializeValue(value: EnsembleIdent | null): string { + return value?.toString() ?? ""; + } + + deserializeValue(serializedValue: string): EnsembleIdent | null { + return serializedValue !== "" ? EnsembleIdent.fromString(serializedValue) : null; + } + + makeComponent(): (props: SettingComponentProps) => React.ReactNode { + return function Ensemble(props: SettingComponentProps) { + const ensembles = props.globalSettings.ensembles.filter((ensemble) => + props.availableValues.includes(ensemble.getIdent()) + ); + + return ( + + ); + }; + } + + valueToString(args: ValueToStringArgs): string { + const { value, workbenchSession } = args; + if (value === null) { + return "-"; + } + + return workbenchSession.getEnsembleSet().findEnsemble(value)?.getDisplayName() ?? "-"; + } +} + +SettingRegistry.registerSetting(Ensemble); diff --git a/frontend/src/modules/2DViewer/layers/implementations/settings/GridAttribute.tsx b/frontend/src/modules/2DViewer/layers/implementations/settings/GridAttribute.tsx new file mode 100644 index 000000000..9be760c67 --- /dev/null +++ b/frontend/src/modules/2DViewer/layers/implementations/settings/GridAttribute.tsx @@ -0,0 +1,54 @@ +import React from "react"; + +import { Dropdown, DropdownOption } from "@lib/components/Dropdown"; + +import { SettingType } from "./settingsTypes"; + +import { SettingRegistry } from "../../SettingRegistry"; +import { SettingDelegate } from "../../delegates/SettingDelegate"; +import { Setting, SettingComponentProps } from "../../interfaces"; + +type ValueType = string | null; + +export class GridAttribute implements Setting { + private _delegate: SettingDelegate; + + constructor() { + this._delegate = new SettingDelegate(null, this); + } + + getType(): SettingType { + return SettingType.GRID_ATTRIBUTE; + } + + getLabel(): string { + return "Grid attribute"; + } + + getDelegate(): SettingDelegate { + return this._delegate; + } + + makeComponent(): (props: SettingComponentProps) => React.ReactNode { + return function Ensemble(props: SettingComponentProps) { + const options: DropdownOption[] = props.availableValues.map((value) => { + return { + value: value.toString(), + label: value === null ? "None" : value.toString(), + }; + }); + + return ( + + ); + }; + } +} + +SettingRegistry.registerSetting(GridAttribute); diff --git a/frontend/src/modules/2DViewer/layers/implementations/settings/GridLayer.tsx b/frontend/src/modules/2DViewer/layers/implementations/settings/GridLayer.tsx new file mode 100644 index 000000000..7c05849d2 --- /dev/null +++ b/frontend/src/modules/2DViewer/layers/implementations/settings/GridLayer.tsx @@ -0,0 +1,102 @@ +import React from "react"; + +import { Dropdown, DropdownOption } from "@lib/components/Dropdown"; + +import { SettingType } from "./settingsTypes"; + +import { SettingRegistry } from "../../SettingRegistry"; +import { SettingDelegate } from "../../delegates/SettingDelegate"; +import { AvailableValuesType, Setting, SettingComponentProps } from "../../interfaces"; + +type ValueType = number | null; + +export class GridLayer implements Setting { + private _delegate: SettingDelegate; + + constructor() { + this._delegate = new SettingDelegate(null, this); + } + + getType(): SettingType { + return SettingType.GRID_LAYER; + } + + getLabel(): string { + return "Grid layer"; + } + + getDelegate(): SettingDelegate { + return this._delegate; + } + + isValueValid(availableValues: AvailableValuesType, value: ValueType): boolean { + if (value === null) { + return false; + } + + if (availableValues.length < 3) { + return false; + } + + const min = 0; + const max = availableValues[2]; + + if (max === null) { + return false; + } + + return value >= min && value <= max; + } + + fixupValue(availableValues: AvailableValuesType, currentValue: ValueType): ValueType { + if (availableValues.length < 3) { + return null; + } + + const min = 0; + const max = availableValues[2]; + + if (max === null) { + return null; + } + + if (currentValue === null) { + return min; + } + + if (currentValue < min) { + return min; + } + + if (currentValue > max) { + return max; + } + + return currentValue; + } + + makeComponent(): (props: SettingComponentProps) => React.ReactNode { + return function Ensemble(props: SettingComponentProps) { + const kRange = props.availableValues ? Array.from({ length: props.availableValues[2] }, (_, i) => i) : []; + + const options: DropdownOption[] = kRange.map((value) => { + return { + value: value.toString(), + label: value === null ? "None" : value.toString(), + }; + }); + + return ( + props.onValueChange(parseInt(val))} + disabled={props.isOverridden} + showArrows + /> + ); + }; + } +} + +SettingRegistry.registerSetting(GridLayer); diff --git a/frontend/src/modules/2DViewer/layers/implementations/settings/GridName.tsx b/frontend/src/modules/2DViewer/layers/implementations/settings/GridName.tsx new file mode 100644 index 000000000..160da8a10 --- /dev/null +++ b/frontend/src/modules/2DViewer/layers/implementations/settings/GridName.tsx @@ -0,0 +1,54 @@ +import React from "react"; + +import { Dropdown, DropdownOption } from "@lib/components/Dropdown"; + +import { SettingType } from "./settingsTypes"; + +import { SettingRegistry } from "../../SettingRegistry"; +import { SettingDelegate } from "../../delegates/SettingDelegate"; +import { Setting, SettingComponentProps } from "../../interfaces"; + +type ValueType = string | null; + +export class GridName implements Setting { + private _delegate: SettingDelegate; + + constructor() { + this._delegate = new SettingDelegate(null, this); + } + + getType(): SettingType { + return SettingType.GRID_NAME; + } + + getLabel(): string { + return "Grid name"; + } + + getDelegate(): SettingDelegate { + return this._delegate; + } + + makeComponent(): (props: SettingComponentProps) => React.ReactNode { + return function Ensemble(props: SettingComponentProps) { + const options: DropdownOption[] = props.availableValues.map((value) => { + return { + value: value.toString(), + label: value === null ? "None" : value.toString(), + }; + }); + + return ( + + ); + }; + } +} + +SettingRegistry.registerSetting(GridName); diff --git a/frontend/src/modules/2DViewer/layers/implementations/settings/PolygonsAttribute.tsx b/frontend/src/modules/2DViewer/layers/implementations/settings/PolygonsAttribute.tsx new file mode 100644 index 000000000..43c6617c6 --- /dev/null +++ b/frontend/src/modules/2DViewer/layers/implementations/settings/PolygonsAttribute.tsx @@ -0,0 +1,54 @@ +import React from "react"; + +import { Dropdown, DropdownOption } from "@lib/components/Dropdown"; + +import { SettingType } from "./settingsTypes"; + +import { SettingRegistry } from "../../SettingRegistry"; +import { SettingDelegate } from "../../delegates/SettingDelegate"; +import { Setting, SettingComponentProps } from "../../interfaces"; + +type ValueType = string | null; + +export class PolygonsAttribute implements Setting { + private _delegate: SettingDelegate; + + constructor() { + this._delegate = new SettingDelegate(null, this); + } + + getType(): SettingType { + return SettingType.POLYGONS_ATTRIBUTE; + } + + getLabel(): string { + return "Polygons attribute"; + } + + getDelegate(): SettingDelegate { + return this._delegate; + } + + makeComponent(): (props: SettingComponentProps) => React.ReactNode { + return function Ensemble(props: SettingComponentProps) { + const options: DropdownOption[] = props.availableValues.map((value) => { + return { + value: value.toString(), + label: value === null ? "None" : value.toString(), + }; + }); + + return ( + + ); + }; + } +} + +SettingRegistry.registerSetting(PolygonsAttribute); diff --git a/frontend/src/modules/2DViewer/layers/implementations/settings/PolygonsName.tsx b/frontend/src/modules/2DViewer/layers/implementations/settings/PolygonsName.tsx new file mode 100644 index 000000000..9bd3f6ff6 --- /dev/null +++ b/frontend/src/modules/2DViewer/layers/implementations/settings/PolygonsName.tsx @@ -0,0 +1,54 @@ +import React from "react"; + +import { Dropdown, DropdownOption } from "@lib/components/Dropdown"; + +import { SettingType } from "./settingsTypes"; + +import { SettingRegistry } from "../../SettingRegistry"; +import { SettingDelegate } from "../../delegates/SettingDelegate"; +import { Setting, SettingComponentProps } from "../../interfaces"; + +type ValueType = string | null; + +export class PolygonsName implements Setting { + private _delegate: SettingDelegate; + + constructor() { + this._delegate = new SettingDelegate(null, this); + } + + getType(): SettingType { + return SettingType.POLYGONS_NAME; + } + + getLabel(): string { + return "Polygons name"; + } + + getDelegate(): SettingDelegate { + return this._delegate; + } + + makeComponent(): (props: SettingComponentProps) => React.ReactNode { + return function FaultPolygons(props: SettingComponentProps) { + const options: DropdownOption[] = props.availableValues.map((value) => { + return { + value: value.toString(), + label: value === null ? "None" : value.toString(), + }; + }); + + return ( + + ); + }; + } +} + +SettingRegistry.registerSetting(PolygonsName); diff --git a/frontend/src/modules/2DViewer/layers/implementations/settings/Realization.tsx b/frontend/src/modules/2DViewer/layers/implementations/settings/Realization.tsx new file mode 100644 index 000000000..1decc851d --- /dev/null +++ b/frontend/src/modules/2DViewer/layers/implementations/settings/Realization.tsx @@ -0,0 +1,56 @@ +import React from "react"; + +import { Dropdown, DropdownOption } from "@lib/components/Dropdown"; + +import { SettingType } from "./settingsTypes"; + +import { SettingRegistry } from "../../SettingRegistry"; +import { SettingDelegate } from "../../delegates/SettingDelegate"; +import { Setting, SettingComponentProps } from "../../interfaces"; + +export class Realization implements Setting { + private _delegate: SettingDelegate; + + constructor() { + this._delegate = new SettingDelegate(null, this); + } + + getType(): SettingType { + return SettingType.REALIZATION; + } + + getLabel(): string { + return "Realization"; + } + + getDelegate(): SettingDelegate { + return this._delegate; + } + + makeComponent(): (props: SettingComponentProps) => React.ReactNode { + return function Realization(props: SettingComponentProps) { + function handleSelectionChange(selectedValue: string) { + const newValue = parseInt(selectedValue); + props.onValueChange(newValue); + } + + const options: DropdownOption[] = props.availableValues.map((value) => { + return { + value: value.toString(), + label: value === null ? "None" : value.toString(), + }; + }); + return ( + + ); + }; + } +} + +SettingRegistry.registerSetting(Realization); diff --git a/frontend/src/modules/2DViewer/layers/implementations/settings/Sensitivity.tsx b/frontend/src/modules/2DViewer/layers/implementations/settings/Sensitivity.tsx new file mode 100644 index 000000000..c45071bac --- /dev/null +++ b/frontend/src/modules/2DViewer/layers/implementations/settings/Sensitivity.tsx @@ -0,0 +1,139 @@ +import React from "react"; + +import { Dropdown } from "@lib/components/Dropdown"; + +import { SettingType } from "./settingsTypes"; + +import { SettingRegistry } from "../../SettingRegistry"; +import { SettingDelegate } from "../../delegates/SettingDelegate"; +import { AvailableValuesType, Setting, SettingComponentProps } from "../../interfaces"; + +export type SensitivityNameCasePair = { + sensitivityName: string; + sensitivityCase: string; +}; + +export class Sensitivity implements Setting { + private _delegate: SettingDelegate; + + constructor() { + this._delegate = new SettingDelegate(null, this); + } + + getType(): SettingType { + return SettingType.STATISTIC_FUNCTION; + } + + getLabel(): string { + return "Sensitivity"; + } + + getDelegate(): SettingDelegate { + return this._delegate; + } + + isValueValid( + availableValues: AvailableValuesType, + value: SensitivityNameCasePair | null + ): boolean { + if (availableValues.length === 0) { + return true; + } + if (!value) { + return false; + } + return availableValues + .filter((el) => el !== null) + .some( + (sensitivity) => + sensitivity?.sensitivityName === value.sensitivityName && + sensitivity?.sensitivityCase === value.sensitivityCase + ); + } + + makeComponent(): (props: SettingComponentProps) => React.ReactNode { + return function Sensitivity(props: SettingComponentProps) { + const availableSensitivityNames: string[] = [ + ...Array.from(new Set(props.availableValues.map((sensitivity) => sensitivity.sensitivityName))), + ]; + + const currentSensitivityName = props.value?.sensitivityName; + const availableSensitiveCases = props.availableValues + .filter((sensitivity) => sensitivity.sensitivityName === currentSensitivityName) + .map((sensitivity) => sensitivity.sensitivityCase); + + const currentSensitivityCase = fixupSensitivityCase( + props.value?.sensitivityCase || null, + availableSensitiveCases + ); + + const sensitivityNameOptions = availableSensitivityNames.map((sensitivityName) => ({ + value: sensitivityName, + label: sensitivityName, + })); + + const sensitivityCaseOptions = availableSensitiveCases.map((sensitivityCase) => ({ + value: sensitivityCase, + label: sensitivityCase, + })); + + if (!currentSensitivityName || !currentSensitivityCase) { + props.onValueChange(null); + } else if (currentSensitivityCase !== props.value?.sensitivityCase) { + props.onValueChange({ + sensitivityName: currentSensitivityName, + sensitivityCase: currentSensitivityCase, + }); + } + + function handleSensitivityNameChange(selectedValue: string) { + const availableSensitiveCases = props.availableValues + .filter((sensitivity) => sensitivity.sensitivityName === selectedValue) + .map((sensitivity) => sensitivity.sensitivityCase); + + const currentSensitivityCase = fixupSensitivityCase(null, availableSensitiveCases); + if (!currentSensitivityCase) { + props.onValueChange(null); + } else { + props.onValueChange({ + sensitivityName: selectedValue, + sensitivityCase: currentSensitivityCase, + }); + } + } + function handleSensitivityCaseChange(selectedValue: string) { + props.onValueChange({ + sensitivityName: props.value?.sensitivityName ?? "", + sensitivityCase: selectedValue, + }); + } + if (props.availableValues.length === 0) { + return "No sensitivities available"; + } + return ( +
+ + +
+ ); + }; + } +} + +function fixupSensitivityCase(currentSensitivityCase: string | null, availableSensitiveCases: string[]): string | null { + if (!currentSensitivityCase || !availableSensitiveCases.includes(currentSensitivityCase)) { + return availableSensitiveCases[0] ?? null; + } + + return currentSensitivityCase; +} + +SettingRegistry.registerSetting(Sensitivity as unknown as new () => Setting); diff --git a/frontend/src/modules/2DViewer/layers/implementations/settings/ShowGridLines.tsx b/frontend/src/modules/2DViewer/layers/implementations/settings/ShowGridLines.tsx new file mode 100644 index 000000000..17a9456f3 --- /dev/null +++ b/frontend/src/modules/2DViewer/layers/implementations/settings/ShowGridLines.tsx @@ -0,0 +1,47 @@ +import React, { ChangeEvent } from "react"; + +import { Switch } from "@lib/components/Switch"; + +import { SettingType } from "./settingsTypes"; + +import { SettingRegistry } from "../../SettingRegistry"; +import { SettingDelegate } from "../../delegates/SettingDelegate"; +import { Setting, SettingComponentProps } from "../../interfaces"; + +type ValueType = boolean; + +export class ShowGridLines implements Setting { + private _delegate: SettingDelegate; + + constructor() { + this._delegate = new SettingDelegate(false, this, true); + } + + getType(): SettingType { + return SettingType.ENSEMBLE; + } + + getLabel(): string { + return "Show grid lines"; + } + + isValueValid(): boolean { + return true; + } + + getDelegate(): SettingDelegate { + return this._delegate; + } + + makeComponent(): (props: SettingComponentProps) => React.ReactNode { + return function ShowGridLines(props: SettingComponentProps) { + function handleChange(e: ChangeEvent) { + props.onValueChange(e.target.checked); + } + + return ; + }; + } +} + +SettingRegistry.registerSetting(ShowGridLines); diff --git a/frontend/src/modules/2DViewer/layers/implementations/settings/StatisticFunction.tsx b/frontend/src/modules/2DViewer/layers/implementations/settings/StatisticFunction.tsx new file mode 100644 index 000000000..029e7544e --- /dev/null +++ b/frontend/src/modules/2DViewer/layers/implementations/settings/StatisticFunction.tsx @@ -0,0 +1,60 @@ +import React from "react"; + +import { SurfaceStatisticFunction_api } from "@api"; +import { Dropdown, DropdownOption } from "@lib/components/Dropdown"; + +import { SettingType } from "./settingsTypes"; + +import { SettingRegistry } from "../../SettingRegistry"; +import { SettingDelegate } from "../../delegates/SettingDelegate"; +import { Setting, SettingComponentProps } from "../../interfaces"; + +export class StatisticFunction implements Setting { + private _delegate: SettingDelegate; + + constructor() { + this._delegate = new SettingDelegate(SurfaceStatisticFunction_api.MEAN, this); + } + + getType(): SettingType { + return SettingType.STATISTIC_FUNCTION; + } + + getLabel(): string { + return "Statistic function"; + } + + getDelegate(): SettingDelegate { + return this._delegate; + } + + isValueValid(): boolean { + return true; + } + + makeComponent(): (props: SettingComponentProps) => React.ReactNode { + const itemArr: DropdownOption[] = [ + { value: SurfaceStatisticFunction_api.MEAN, label: "Mean" }, + { value: SurfaceStatisticFunction_api.STD, label: "Std" }, + { value: SurfaceStatisticFunction_api.MIN, label: "Min" }, + { value: SurfaceStatisticFunction_api.MAX, label: "Max" }, + { value: SurfaceStatisticFunction_api.P10, label: "P10" }, + { value: SurfaceStatisticFunction_api.P90, label: "P90" }, + { value: SurfaceStatisticFunction_api.P50, label: "P50" }, + ]; + + return function StatisticFunction(props: SettingComponentProps) { + return ( + props.onValueChange(newVal as SurfaceStatisticFunction_api)} + disabled={props.isOverridden} + showArrows + /> + ); + }; + } +} + +SettingRegistry.registerSetting(StatisticFunction); diff --git a/frontend/src/modules/2DViewer/layers/implementations/settings/SurfaceAttribute.tsx b/frontend/src/modules/2DViewer/layers/implementations/settings/SurfaceAttribute.tsx new file mode 100644 index 000000000..2220606fd --- /dev/null +++ b/frontend/src/modules/2DViewer/layers/implementations/settings/SurfaceAttribute.tsx @@ -0,0 +1,54 @@ +import React from "react"; + +import { Dropdown, DropdownOption } from "@lib/components/Dropdown"; + +import { SettingType } from "./settingsTypes"; + +import { SettingRegistry } from "../../SettingRegistry"; +import { SettingDelegate } from "../../delegates/SettingDelegate"; +import { Setting, SettingComponentProps } from "../../interfaces"; + +type ValueType = string | null; + +export class SurfaceAttribute implements Setting { + private _delegate: SettingDelegate; + + constructor() { + this._delegate = new SettingDelegate(null, this); + } + + getType(): SettingType { + return SettingType.SURFACE_ATTRIBUTE; + } + + getLabel(): string { + return "Surface attribute"; + } + + getDelegate(): SettingDelegate { + return this._delegate; + } + + makeComponent(): (props: SettingComponentProps) => React.ReactNode { + return function Ensemble(props: SettingComponentProps) { + const options: DropdownOption[] = props.availableValues.map((value) => { + return { + value: value.toString(), + label: value === null ? "None" : value.toString(), + }; + }); + + return ( + + ); + }; + } +} + +SettingRegistry.registerSetting(SurfaceAttribute); diff --git a/frontend/src/modules/2DViewer/layers/implementations/settings/SurfaceName.tsx b/frontend/src/modules/2DViewer/layers/implementations/settings/SurfaceName.tsx new file mode 100644 index 000000000..63e714a0e --- /dev/null +++ b/frontend/src/modules/2DViewer/layers/implementations/settings/SurfaceName.tsx @@ -0,0 +1,54 @@ +import React from "react"; + +import { Dropdown, DropdownOption } from "@lib/components/Dropdown"; + +import { SettingType } from "./settingsTypes"; + +import { SettingRegistry } from "../../SettingRegistry"; +import { SettingDelegate } from "../../delegates/SettingDelegate"; +import { Setting, SettingComponentProps } from "../../interfaces"; + +type ValueType = string | null; + +export class SurfaceName implements Setting { + private _delegate: SettingDelegate; + + constructor() { + this._delegate = new SettingDelegate(null, this); + } + + getType(): SettingType { + return SettingType.SURFACE_NAME; + } + + getLabel(): string { + return "Surface name"; + } + + getDelegate(): SettingDelegate { + return this._delegate; + } + + makeComponent(): (props: SettingComponentProps) => React.ReactNode { + return function Ensemble(props: SettingComponentProps) { + const options: DropdownOption[] = props.availableValues.map((value) => { + return { + value: value.toString(), + label: value === null ? "None" : value.toString(), + }; + }); + + return ( + + ); + }; + } +} + +SettingRegistry.registerSetting(SurfaceName); diff --git a/frontend/src/modules/2DViewer/layers/implementations/settings/TimeOrInterval.tsx b/frontend/src/modules/2DViewer/layers/implementations/settings/TimeOrInterval.tsx new file mode 100644 index 000000000..5096481b2 --- /dev/null +++ b/frontend/src/modules/2DViewer/layers/implementations/settings/TimeOrInterval.tsx @@ -0,0 +1,84 @@ +import React from "react"; + +import { SurfaceTimeType_api } from "@api"; +import { Dropdown, DropdownOption } from "@lib/components/Dropdown"; + +import { SettingType } from "./settingsTypes"; + +import { SettingRegistry } from "../../SettingRegistry"; +import { SettingDelegate } from "../../delegates/SettingDelegate"; +import { Setting, SettingComponentProps, ValueToStringArgs } from "../../interfaces"; + +type ValueType = string | null; + +export class TimeOrInterval implements Setting { + private _delegate: SettingDelegate; + + constructor() { + this._delegate = new SettingDelegate(null, this); + } + + getType(): SettingType { + return SettingType.TIME_OR_INTERVAL; + } + + getLabel(): string { + return "Date"; + } + + getDelegate(): SettingDelegate { + return this._delegate; + } + + makeComponent(): (props: SettingComponentProps) => React.ReactNode { + return function Ensemble(props: SettingComponentProps) { + const options: DropdownOption[] = props.availableValues.map((value) => { + return { + value: value.toString(), + label: timeTypeToLabel(value), + }; + }); + + return ( + + ); + }; + } + + valueToString(args: ValueToStringArgs): string { + const { value } = args; + if (value === null) { + return "-"; + } + return timeTypeToLabel(value); + } +} + +function timeTypeToLabel(input: string): string { + if (input === SurfaceTimeType_api.NO_TIME) { + return "Initial / No date"; + } + const [start, end] = input.split("/"); + if (end) { + return isoIntervalStringToDateLabel(start, end); + } + return isoStringToDateLabel(start); +} +function isoStringToDateLabel(isoDatestring: string): string { + const date = isoDatestring.split("T")[0]; + return `${date}`; +} + +function isoIntervalStringToDateLabel(startIsoDateString: string, endIsoDateString: string): string { + const startDate = startIsoDateString.split("T")[0]; + const endDate = endIsoDateString.split("T")[0]; + return `${startDate}/${endDate}`; +} + +SettingRegistry.registerSetting(TimeOrInterval); diff --git a/frontend/src/modules/2DViewer/layers/implementations/settings/settingsTypes.ts b/frontend/src/modules/2DViewer/layers/implementations/settings/settingsTypes.ts new file mode 100644 index 000000000..f7430c181 --- /dev/null +++ b/frontend/src/modules/2DViewer/layers/implementations/settings/settingsTypes.ts @@ -0,0 +1,16 @@ +export enum SettingType { + ENSEMBLE = "ensemble", + REALIZATION = "realization", + STATISTIC_FUNCTION = "statisticFunction", + SENSITIVITY = "sensitivity", + SURFACE_NAME = "surfaceName", + SURFACE_ATTRIBUTE = "surfaceAttribute", + TIME_OR_INTERVAL = "timeOrInterval", + POLYGONS_ATTRIBUTE = "polygonsAttribute", + POLYGONS_NAME = "polygonsName", + SMDA_WELLBORE_HEADERS = "smdaWellboreHeaders", + GRID_NAME = "gridName", + GRID_ATTRIBUTE = "gridAttribute", + GRID_LAYER = "gridLayer", + SHOW_GRID_LINES = "showGridLines", +} diff --git a/frontend/src/modules/2DViewer/layers/interfaces.ts b/frontend/src/modules/2DViewer/layers/interfaces.ts new file mode 100644 index 000000000..1020cea33 --- /dev/null +++ b/frontend/src/modules/2DViewer/layers/interfaces.ts @@ -0,0 +1,204 @@ +import { WorkbenchSession } from "@framework/WorkbenchSession"; +import { WorkbenchSettings } from "@framework/WorkbenchSettings"; +import { ColorScaleSerialization } from "@lib/utils/ColorScale"; +import { QueryClient } from "@tanstack/react-query"; + +import { Dependency } from "./Dependency"; +import { GlobalSettings } from "./LayerManager"; +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 { SettingType } from "./implementations/settings/settingsTypes"; + +export type SerializedType = + | "layer-manager" + | "view" + | "layer" + | "settings-group" + | "color-scale" + | "delta-surface" + | "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: "layer"; + layerClass: string; + settings: SerializedSettingsState; +} + +export interface SerializedView extends SerializedItem { + type: "view"; + color: string; + children: SerializedItem[]; +} + +export interface SerializedSettingsGroup extends SerializedItem { + type: "settings-group"; + children: SerializedItem[]; +} + +export interface SerializedColorScale extends SerializedItem { + type: "color-scale"; + colorScale: ColorScaleSerialization; + userDefinedBoundaries: boolean; +} + +export interface SerializedSharedSetting extends SerializedItem { + type: "shared-setting"; + settingType: SettingType; + wrappedSettingClass: string; + value: string; +} + +export interface SerializedLayerManager extends SerializedItem { + type: "layer-manager"; + children: SerializedItem[]; +} + +export interface SerializedDeltaSurface extends SerializedItem { + type: "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; + fechData(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).fechData !== 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/queryConstants.ts b/frontend/src/modules/2DViewer/layers/queryConstants.ts new file mode 100644 index 000000000..3acac7d69 --- /dev/null +++ b/frontend/src/modules/2DViewer/layers/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/utils.ts b/frontend/src/modules/2DViewer/layers/utils.ts new file mode 100644 index 000000000..d9ea853b6 --- /dev/null +++ b/frontend/src/modules/2DViewer/layers/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/loadModule.tsx b/frontend/src/modules/2DViewer/loadModule.tsx new file mode 100644 index 000000000..dfcfa27de --- /dev/null +++ b/frontend/src/modules/2DViewer/loadModule.tsx @@ -0,0 +1,13 @@ +import { ModuleRegistry } from "@framework/ModuleRegistry"; + +import { Interfaces, settingsToViewInterfaceInitialization } from "./interfaces"; +import { MODULE_NAME } from "./registerModule"; +import { Settings } from "./settings/settings"; +import { View } from "./view/view"; + +const module = ModuleRegistry.initModule(MODULE_NAME, { + settingsToViewInterfaceInitialization, +}); + +module.settingsFC = Settings; +module.viewFC = View; diff --git a/frontend/src/modules/2DViewer/preview.tsx b/frontend/src/modules/2DViewer/preview.tsx new file mode 100644 index 000000000..2a9746ab7 --- /dev/null +++ b/frontend/src/modules/2DViewer/preview.tsx @@ -0,0 +1,8 @@ +import { DrawPreviewFunc } from "@framework/Preview"; +import previewImg from "./preview.webp"; + +export const preview: DrawPreviewFunc = function (width: number, height: number) { + return ( + + ); +}; diff --git a/frontend/src/modules/2DViewer/preview.webp b/frontend/src/modules/2DViewer/preview.webp new file mode 100644 index 0000000000000000000000000000000000000000..0b82963c8d6bbacf99350d170c51036c1c0e84ae GIT binary patch literal 38828 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 Xp{Yv8e@maqfBgIV-~aQc|MUL={RDJ4 literal 0 HcmV?d00001 diff --git a/frontend/src/modules/2DViewer/registerModule.ts b/frontend/src/modules/2DViewer/registerModule.ts new file mode 100644 index 000000000..2e29217a9 --- /dev/null +++ b/frontend/src/modules/2DViewer/registerModule.ts @@ -0,0 +1,24 @@ +import { ModuleCategory, ModuleDevState } from "@framework/Module"; +import { ModuleDataTagId } from "@framework/ModuleDataTags"; +import { ModuleRegistry } from "@framework/ModuleRegistry"; + +import { Interfaces } from "./interfaces"; +import { preview } from "./preview"; + +export const MODULE_NAME: string = "2DViewer"; + +ModuleRegistry.registerModule({ + moduleName: MODULE_NAME, + category: ModuleCategory.MAIN, + devState: ModuleDevState.DEV, + defaultTitle: "2D Viewer", + preview, + description: "Generic 2D viewer for co-visualization of spatial data.", + dataTagIds: [ + ModuleDataTagId.SURFACE, + ModuleDataTagId.DRILLED_WELLS, + ModuleDataTagId.SEISMIC, + ModuleDataTagId.GRID3D, + ModuleDataTagId.POLYGONS, + ], +}); diff --git a/frontend/src/modules/2DViewer/settings/atoms/baseAtoms.ts b/frontend/src/modules/2DViewer/settings/atoms/baseAtoms.ts new file mode 100644 index 000000000..c67693fd1 --- /dev/null +++ b/frontend/src/modules/2DViewer/settings/atoms/baseAtoms.ts @@ -0,0 +1,8 @@ +import { LayerManager } from "@modules/2DViewer/layers/LayerManager"; +import { PreferredViewLayout } from "@modules/2DViewer/types"; + +import { atom } from "jotai"; + +export const userSelectedFieldIdentifierAtom = atom(null); +export const layerManagerAtom = atom(null); +export const preferredViewLayoutAtom = atom(PreferredViewLayout.VERTICAL); diff --git a/frontend/src/modules/2DViewer/settings/atoms/derivedAtoms.ts b/frontend/src/modules/2DViewer/settings/atoms/derivedAtoms.ts new file mode 100644 index 000000000..7d4115816 --- /dev/null +++ b/frontend/src/modules/2DViewer/settings/atoms/derivedAtoms.ts @@ -0,0 +1,31 @@ +import { EnsembleSet } from "@framework/EnsembleSet"; +import { EnsembleSetAtom } from "@framework/GlobalAtoms"; + +import { atom } from "jotai"; + +import { userSelectedFieldIdentifierAtom } from "./baseAtoms"; + +export const selectedFieldIdentifierAtom = atom((get) => { + const ensembleSet = get(EnsembleSetAtom); + const userSelectedField = get(userSelectedFieldIdentifierAtom); + + if ( + !userSelectedField || + !ensembleSet.getEnsembleArr().some((ens) => ens.getFieldIdentifier() === userSelectedField) + ) { + return ensembleSet.getEnsembleArr().at(0)?.getFieldIdentifier() ?? null; + } + + return userSelectedField; +}); + +export const filteredEnsembleSetAtom = atom((get) => { + const ensembleSet = get(EnsembleSetAtom); + const fieldIdentifier = get(userSelectedFieldIdentifierAtom); + + if (fieldIdentifier === null) { + return ensembleSet; + } + + return new EnsembleSet(ensembleSet.getEnsembleArr().filter((el) => el.getFieldIdentifier() === fieldIdentifier)); +}); diff --git a/frontend/src/modules/2DViewer/settings/components/layerManagerComponent.tsx b/frontend/src/modules/2DViewer/settings/components/layerManagerComponent.tsx new file mode 100644 index 000000000..559bb309f --- /dev/null +++ b/frontend/src/modules/2DViewer/settings/components/layerManagerComponent.tsx @@ -0,0 +1,445 @@ +import React from "react"; + +import { Icon } from "@equinor/eds-core-react"; +import { color_palette, fault, grid_layer, settings, surface_layer, wellbore } from "@equinor/eds-icons"; +import { WorkbenchSession } from "@framework/WorkbenchSession"; +import { WorkbenchSettings } from "@framework/WorkbenchSettings"; +import { Menu } from "@lib/components/Menu"; +import { MenuButton } from "@lib/components/MenuButton"; +import { MenuHeading } from "@lib/components/MenuHeading"; +import { MenuItem } from "@lib/components/MenuItem"; +import { IsMoveAllowedArgs, SortableList } from "@lib/components/SortableList"; +import { useElementSize } from "@lib/hooks/useElementSize"; +import { convertRemToPixels } from "@lib/utils/screenUnitConversions"; +import { ColorScale } from "@modules/2DViewer/layers/ColorScale"; +import { DeltaSurface } from "@modules/2DViewer/layers/DeltaSurface"; +import { LayerManager } from "@modules/2DViewer/layers/LayerManager"; +import { SettingsGroup } from "@modules/2DViewer/layers/SettingsGroup"; +import { SharedSetting } from "@modules/2DViewer/layers/SharedSetting"; +import { View } from "@modules/2DViewer/layers/View"; +import { ExpandCollapseAllButton } from "@modules/2DViewer/layers/components/ExpandCollapseAllButton"; +import { LayersActionGroup, LayersActions } from "@modules/2DViewer/layers/components/LayersActions"; +import { makeComponent } from "@modules/2DViewer/layers/components/utils"; +import { GroupDelegateTopic } from "@modules/2DViewer/layers/delegates/GroupDelegate"; +import { usePublishSubscribeTopicValue } from "@modules/2DViewer/layers/delegates/PublishSubscribeDelegate"; +import { DrilledWellTrajectoriesLayer } from "@modules/2DViewer/layers/implementations/layers/DrilledWellTrajectoriesLayer/DrilledWellTrajectoriesLayer"; +import { DrilledWellborePicksLayer } from "@modules/2DViewer/layers/implementations/layers/DrilledWellborePicksLayer/DrilledWellborePicksLayer"; +import { ObservedSurfaceLayer } from "@modules/2DViewer/layers/implementations/layers/ObservedSurfaceLayer/ObservedSurfaceLayer"; +import { RealizationGridLayer } from "@modules/2DViewer/layers/implementations/layers/RealizationGridLayer/RealizationGridLayer"; +import { RealizationPolygonsLayer } from "@modules/2DViewer/layers/implementations/layers/RealizationPolygonsLayer/RealizationPolygonsLayer"; +import { RealizationSurfaceLayer } from "@modules/2DViewer/layers/implementations/layers/RealizationSurfaceLayer/RealizationSurfaceLayer"; +import { StatisticalSurfaceLayer } from "@modules/2DViewer/layers/implementations/layers/StatisticalSurfaceLayer/StatisticalSurfaceLayer"; +import { Ensemble } from "@modules/2DViewer/layers/implementations/settings/Ensemble"; +import { Realization } from "@modules/2DViewer/layers/implementations/settings/Realization"; +import { SurfaceAttribute } from "@modules/2DViewer/layers/implementations/settings/SurfaceAttribute"; +import { SurfaceName } from "@modules/2DViewer/layers/implementations/settings/SurfaceName"; +import { TimeOrInterval } from "@modules/2DViewer/layers/implementations/settings/TimeOrInterval"; +import { Group, Item, instanceofGroup, instanceofLayer } from "@modules/2DViewer/layers/interfaces"; +import { PreferredViewLayout } from "@modules/2DViewer/types"; +import { Dropdown } from "@mui/base"; +import { + Add, + Check, + Panorama, + SettingsApplications, + Settings as SettingsIcon, + TableRowsOutlined, + ViewColumnOutlined, +} from "@mui/icons-material"; + +import { useAtom } from "jotai"; + +import { preferredViewLayoutAtom } from "../atoms/baseAtoms"; + +export type LayerManagerComponentProps = { + layerManager: LayerManager; + workbenchSession: WorkbenchSession; + workbenchSettings: WorkbenchSettings; +}; + +export function LayerManagerComponent(props: LayerManagerComponentProps): React.ReactNode { + const layerListRef = React.useRef(null); + const colorSet = props.workbenchSettings.useColorSet(); + const layerListSize = useElementSize(layerListRef); + + const [preferredViewLayout, setPreferredViewLayout] = useAtom(preferredViewLayoutAtom); + + 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(); + } + + const numSharedSettings = groupDelegate.findChildren((item) => { + return item instanceof SharedSetting; + }).length; + + const numViews = groupDelegate.getDescendantItems((item) => item instanceof View).length; + + switch (identifier) { + case "view": + groupDelegate.appendChild( + new View(numViews > 0 ? `View (${numViews})` : "View", props.layerManager, colorSet.getNextColor()) + ); + return; + case "delta-surface": + groupDelegate.insertChild(new DeltaSurface("Delta surface", props.layerManager), numSharedSettings); + return; + case "settings-group": + groupDelegate.insertChild(new SettingsGroup("Settings group", props.layerManager), numSharedSettings); + return; + case "color-scale": + groupDelegate.prependChild(new ColorScale("Color scale", props.layerManager)); + return; + case "observed-surface": + groupDelegate.insertChild(new ObservedSurfaceLayer(props.layerManager), numSharedSettings); + return; + case "statistical-surface": + groupDelegate.insertChild(new StatisticalSurfaceLayer(props.layerManager), numSharedSettings); + return; + case "realization-surface": + groupDelegate.insertChild(new RealizationSurfaceLayer(props.layerManager), numSharedSettings); + return; + case "realization-polygons": + groupDelegate.insertChild(new RealizationPolygonsLayer(props.layerManager), numSharedSettings); + return; + case "drilled-wellbore-trajectories": + groupDelegate.insertChild(new DrilledWellTrajectoriesLayer(props.layerManager), numSharedSettings); + return; + case "drilled-wellbore-picks": + groupDelegate.insertChild(new DrilledWellborePicksLayer(props.layerManager), numSharedSettings); + return; + case "realization-grid": + groupDelegate.insertChild(new RealizationGridLayer(props.layerManager), numSharedSettings); + return; + case "ensemble": + groupDelegate.prependChild(new SharedSetting(new Ensemble(), props.layerManager)); + return; + case "realization": + groupDelegate.prependChild(new SharedSetting(new Realization(), props.layerManager)); + return; + case "surface-name": + groupDelegate.prependChild(new SharedSetting(new SurfaceName(), props.layerManager)); + return; + case "surface-attribute": + groupDelegate.prependChild(new SharedSetting(new SurfaceAttribute(), props.layerManager)); + return; + case "Date": + groupDelegate.prependChild(new SharedSetting(new TimeOrInterval(), props.layerManager)); + return; + } + } + + 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 (destinationItem instanceof DeltaSurface) { + if ( + instanceofLayer(movedItem) && + !( + movedItem instanceof RealizationSurfaceLayer || + movedItem instanceof StatisticalSurfaceLayer || + movedItem instanceof ObservedSurfaceLayer + ) + ) { + return false; + } + + if (instanceofGroup(movedItem)) { + return false; + } + + if (destinationItem.getGroupDelegate().findChildren((item) => instanceofLayer(item)).length >= 2) { + 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); + } + + const hasView = groupDelegate.getDescendantItems((item) => item instanceof View).length > 0; + const adjustedLayerActions = hasView ? LAYER_ACTIONS : INITIAL_LAYER_ACTIONS; + + return ( +
+
+
+
Layers
+ + + + + + + + Preferred view layout + setPreferredViewLayout(PreferredViewLayout.HORIZONTAL)} + > + Horizontal + + setPreferredViewLayout(PreferredViewLayout.VERTICAL)} + > + Vertical + + + +
+
+ + Click on to add a layer. +
+ } + > + {items.map((item: Item) => makeComponent(item, LAYER_ACTIONS, handleLayerAction))} + +
+
+
+ ); +} + +type ViewLayoutMenuItemProps = { + checked: boolean; + onClick: () => void; + children: React.ReactNode; +}; + +function ViewLayoutMenuItem(props: ViewLayoutMenuItemProps): React.ReactNode { + return ( + +
+
{props.checked && }
+
{props.children}
+
+
+ ); +} + +const INITIAL_LAYER_ACTIONS: LayersActionGroup[] = [ + { + label: "Groups", + children: [ + { + identifier: "view", + icon: , + label: "View", + }, + { + identifier: "settings-group", + icon: , + label: "Settings group", + }, + ], + }, +]; + +const LAYER_ACTIONS: LayersActionGroup[] = [ + { + label: "Groups", + children: [ + { + identifier: "view", + icon: , + label: "View", + }, + { + identifier: "settings-group", + icon: , + label: "Settings group", + }, + /* + { + identifier: "delta-surface", + icon: , + label: "Delta Surface", + }, + */ + ], + }, + { + label: "Layers", + children: [ + { + label: "Surfaces", + children: [ + { + identifier: "observed-surface", + icon: , + label: "Observed Surface", + }, + { + identifier: "statistical-surface", + icon: , + label: "Statistical Surface", + }, + { + identifier: "realization-surface", + icon: , + label: "Realization Surface", + }, + ], + }, + { + label: "Wells", + children: [ + { + identifier: "drilled-wellbore-trajectories", + icon: , + label: "Drilled Wellbore Trajectories", + }, + { + identifier: "drilled-wellbore-picks", + icon: , + label: "Drilled Wellbore Picks", + }, + ], + }, + { + label: "Polygons", + children: [ + { + identifier: "realization-polygons", + icon: , + label: "Realization Polygons", + }, + ], + }, + { + label: "Others", + children: [ + { + identifier: "realization-grid", + icon: , + label: "Realization Grid", + }, + ], + }, + ], + }, + { + label: "Shared Settings", + children: [ + { + identifier: "ensemble", + icon: , + label: "Ensemble", + }, + { + identifier: "realization", + icon: , + label: "Realization", + }, + { + identifier: "surface-name", + icon: , + label: "Surface Name", + }, + { + identifier: "surface-attribute", + icon: , + label: "Surface Attribute", + }, + { + identifier: "Date", + icon: , + label: "Date", + }, + ], + }, + { + label: "Utilities", + children: [ + { + identifier: "color-scale", + icon: , + label: "Color scale", + }, + ], + }, +]; diff --git a/frontend/src/modules/2DViewer/settings/settings.tsx b/frontend/src/modules/2DViewer/settings/settings.tsx new file mode 100644 index 000000000..b2c17d128 --- /dev/null +++ b/frontend/src/modules/2DViewer/settings/settings.tsx @@ -0,0 +1,146 @@ +import React from "react"; + +import { ModuleSettingsProps } from "@framework/Module"; +import { useEnsembleSet } from "@framework/WorkbenchSession"; +import { FieldDropdown } from "@framework/components/FieldDropdown"; +import { CollapsibleGroup } from "@lib/components/CollapsibleGroup"; +import { useQueryClient } from "@tanstack/react-query"; + +import { useAtom, useAtomValue, useSetAtom } from "jotai"; + +import { layerManagerAtom, preferredViewLayoutAtom, userSelectedFieldIdentifierAtom } from "./atoms/baseAtoms"; +import { selectedFieldIdentifierAtom } from "./atoms/derivedAtoms"; +import { LayerManagerComponent } from "./components/layerManagerComponent"; + +import { LayerManager, LayerManagerTopic } from "../layers/LayerManager"; +import { GroupDelegateTopic } from "../layers/delegates/GroupDelegate"; + +export function Settings(props: ModuleSettingsProps): React.ReactNode { + const ensembleSet = useEnsembleSet(props.workbenchSession); + const queryClient = useQueryClient(); + + const [layerManager, setLayerManager] = useAtom(layerManagerAtom); + + const fieldIdentifier = useAtomValue(selectedFieldIdentifierAtom); + const setFieldIdentifier = useSetAtom(userSelectedFieldIdentifierAtom); + const [preferredViewLayout, setPreferredViewLayout] = useAtom(preferredViewLayoutAtom); + + const persistState = React.useCallback( + function persistLayerManagerState() { + if (!layerManager) { + return; + } + + const serializedState = { + layerManager: layerManager.serializeState(), + fieldIdentifier, + preferredViewLayout, + }; + window.localStorage.setItem( + `${props.settingsContext.getInstanceIdString()}-settings`, + JSON.stringify(serializedState) + ); + }, + [layerManager, fieldIdentifier, preferredViewLayout, props.settingsContext] + ); + + const applyPersistedState = React.useCallback( + function applyPersistedState(layerManager: LayerManager) { + const serializedState = window.localStorage.getItem( + `${props.settingsContext.getInstanceIdString()}-settings` + ); + if (!serializedState) { + return; + } + + const parsedState = JSON.parse(serializedState); + if (parsedState.fieldIdentifier) { + setFieldIdentifier(parsedState.fieldIdentifier); + } + if (parsedState.preferredViewLayout) { + setPreferredViewLayout(parsedState.preferredViewLayout); + } + + if (parsedState.layerManager) { + if (!layerManager) { + return; + } + layerManager.deserializeState(parsedState.layerManager); + } + }, + [setFieldIdentifier, setPreferredViewLayout, props.settingsContext] + ); + + React.useEffect( + function onMountEffect() { + const newLayerManager = new LayerManager(props.workbenchSession, props.workbenchSettings, queryClient); + setLayerManager(newLayerManager); + + applyPersistedState(newLayerManager); + + return function onUnmountEffect() { + newLayerManager.beforeDestroy(); + }; + }, + [setLayerManager, props.workbenchSession, props.workbenchSettings, queryClient, applyPersistedState] + ); + + React.useEffect( + function onLayerManagerChangeEffect() { + if (!layerManager) { + return; + } + + persistState(); + + const unsubscribeDataRev = layerManager + .getPublishSubscribeDelegate() + .makeSubscriberFunction(LayerManagerTopic.LAYER_DATA_REVISION)(persistState); + + const unsubscribeExpands = layerManager + .getGroupDelegate() + .getPublishSubscribeDelegate() + .makeSubscriberFunction(GroupDelegateTopic.CHILDREN_EXPANSION_STATES)(persistState); + + return function onUnmountEffect() { + layerManager.beforeDestroy(); + unsubscribeDataRev(); + unsubscribeExpands(); + }; + }, + [layerManager, props.workbenchSession, props.workbenchSettings, persistState] + ); + + React.useEffect( + function onFieldIdentifierChangedEffect() { + if (!layerManager) { + return; + } + layerManager.updateGlobalSetting("fieldId", fieldIdentifier); + }, + [fieldIdentifier, layerManager] + ); + + function handleFieldChange(fieldId: string | null) { + setFieldIdentifier(fieldId); + if (!layerManager) { + return; + } + layerManager.updateGlobalSetting("fieldId", fieldId); + } + + return ( +
+ + + + {layerManager && ( + + )} +
+ ); +} diff --git a/frontend/src/modules/2DViewer/types.ts b/frontend/src/modules/2DViewer/types.ts new file mode 100644 index 000000000..4a9b727f7 --- /dev/null +++ b/frontend/src/modules/2DViewer/types.ts @@ -0,0 +1,38 @@ +export enum LayerType { + OBSERVED_SURFACE = "observedSurface", + STATISTICAL_SURFACE = "statisticalSurface", + REALIZATION_SURFACE = "realizationSurface", + REALIZATION_GRID = "realizationGrid", + REALIZATION_POLYGONS = "realizationPolygons", + DRILLED_WELLBORE_TRAJECTORIES = "drilledWellTrajectories", + DRILLED_WELLBORE_PICKS = "drilledWellPicks", +} + +export const LAYER_TYPE_TO_STRING_MAPPING: Record = { + [LayerType.OBSERVED_SURFACE]: "Observed Surface", + [LayerType.STATISTICAL_SURFACE]: "Statistical Surface", + [LayerType.REALIZATION_SURFACE]: "Realization Surface", + [LayerType.REALIZATION_GRID]: "Realization Grid Layer", + [LayerType.REALIZATION_POLYGONS]: "Realization Polygons", + [LayerType.DRILLED_WELLBORE_TRAJECTORIES]: "Drilled Well Trajectories", + [LayerType.DRILLED_WELLBORE_PICKS]: "Drilled Well Picks", +}; + +export enum SharedSettingType { + ENSEMBLE = "ensemble", + REALIZATION = "realization", + SURFACE_ATTRIBUTE = "surfaceAttribute", + SURFACE_NAME = "surfaceName", +} + +export const SHARED_SETTING_TYPE_TO_STRING_MAPPING: Record = { + [SharedSettingType.ENSEMBLE]: "Ensemble", + [SharedSettingType.REALIZATION]: "Realization", + [SharedSettingType.SURFACE_ATTRIBUTE]: "Surface Attribute", + [SharedSettingType.SURFACE_NAME]: "Surface Name", +}; + +export enum PreferredViewLayout { + HORIZONTAL = "horizontal", + VERTICAL = "vertical", +} diff --git a/frontend/src/modules/2DViewer/view/components/LayersWrapper.tsx b/frontend/src/modules/2DViewer/view/components/LayersWrapper.tsx new file mode 100644 index 000000000..ce4a63968 --- /dev/null +++ b/frontend/src/modules/2DViewer/view/components/LayersWrapper.tsx @@ -0,0 +1,154 @@ +import React from "react"; + +import { View as DeckGlView } from "@deck.gl/core"; +import { ViewContext } from "@framework/ModuleContext"; +import { useViewStatusWriter } from "@framework/StatusWriter"; +import { PendingWrapper } from "@lib/components/PendingWrapper"; +import { useElementSize } from "@lib/hooks/useElementSize"; +import { Rect2D, outerRectContainsInnerRect } from "@lib/utils/geometry"; +import { Interfaces } from "@modules/2DViewer/interfaces"; +import { LayerManager, LayerManagerTopic } from "@modules/2DViewer/layers/LayerManager"; +import { usePublishSubscribeTopicValue } from "@modules/2DViewer/layers/delegates/PublishSubscribeDelegate"; +import { BoundingBox } from "@modules/2DViewer/layers/interfaces"; +import { PreferredViewLayout } from "@modules/2DViewer/types"; +import { ColorLegendsContainer } from "@modules/_shared/components/ColorLegendsContainer"; +import { ColorScaleWithId } from "@modules/_shared/components/ColorLegendsContainer/colorLegendsContainer"; +import { ViewportType } from "@webviz/subsurface-viewer"; +import { ViewsType } from "@webviz/subsurface-viewer/dist/SubsurfaceViewer"; + +import { ReadoutWrapper } from "./ReadoutWrapper"; + +import { PlaceholderLayer } from "../customDeckGlLayers/PlaceholderLayer"; +import { DeckGlLayerWithPosition, recursivelyMakeViewsAndLayers } from "../utils/makeViewsAndLayers"; + +export type LayersWrapperProps = { + layerManager: LayerManager; + preferredViewLayout: PreferredViewLayout; + viewContext: ViewContext; +}; + +export function LayersWrapper(props: LayersWrapperProps): React.ReactNode { + const [prevBoundingBox, setPrevBoundingBox] = React.useState(null); + + const mainDivRef = React.useRef(null); + const mainDivSize = useElementSize(mainDivRef); + const statusWriter = useViewStatusWriter(props.viewContext); + + usePublishSubscribeTopicValue(props.layerManager, LayerManagerTopic.LAYER_DATA_REVISION); + + const viewports: ViewportType[] = []; + const viewerLayers: DeckGlLayerWithPosition[] = []; + const viewportAnnotations: React.ReactNode[] = []; + const globalColorScales: ColorScaleWithId[] = []; + + const views: ViewsType = { + layout: [1, 1], + viewports: viewports, + showLabel: false, + }; + + let numCols = 0; + let numRows = 0; + + let numLoadingLayers = 0; + + const viewsAndLayers = recursivelyMakeViewsAndLayers(props.layerManager); + + numCols = Math.ceil(Math.sqrt(viewsAndLayers.views.length)); + numRows = Math.ceil(viewsAndLayers.views.length / numCols); + + if (props.preferredViewLayout === PreferredViewLayout.HORIZONTAL) { + [numCols, numRows] = [numRows, numCols]; + } + + views.layout = [numCols, numRows]; + + viewerLayers.push(...viewsAndLayers.layers); + globalColorScales.push(...viewsAndLayers.colorScales); + const globalLayerIds = viewsAndLayers.layers.map((layer) => layer.layer.id); + + for (const view of viewsAndLayers.views) { + viewports.push({ + id: view.id, + name: view.name, + isSync: true, + layerIds: [...globalLayerIds, ...view.layers.map((layer) => layer.layer.id), "placeholder"], + }); + viewerLayers.push(...view.layers); + + viewportAnnotations.push( + // eslint-disable-next-line @typescript-eslint/ban-ts-comment + /* @ts-expect-error */ + + +
+
+
+
{view.name}
+
+
+ + ); + } + + if (viewsAndLayers.boundingBox !== null) { + if (prevBoundingBox !== null) { + const oldBoundingRect: Rect2D | null = { + x: prevBoundingBox.x[0], + y: prevBoundingBox.y[0], + width: prevBoundingBox.x[1] - prevBoundingBox.x[0], + height: prevBoundingBox.y[1] - prevBoundingBox.y[0], + }; + + const newBoundingRect: Rect2D = { + x: viewsAndLayers.boundingBox.x[0], + y: viewsAndLayers.boundingBox.y[0], + width: viewsAndLayers.boundingBox.x[1] - viewsAndLayers.boundingBox.x[0], + height: viewsAndLayers.boundingBox.y[1] - viewsAndLayers.boundingBox.y[0], + }; + + if (!outerRectContainsInnerRect(oldBoundingRect, newBoundingRect)) { + setPrevBoundingBox(viewsAndLayers.boundingBox); + } + } else { + setPrevBoundingBox(viewsAndLayers.boundingBox); + } + } + + numLoadingLayers = viewsAndLayers.numLoadingLayers; + statusWriter.setLoading(viewsAndLayers.numLoadingLayers > 0); + + for (const message of viewsAndLayers.errorMessages) { + statusWriter.addError(message); + } + + let bounds: [number, number, number, number] | undefined = undefined; + if (prevBoundingBox) { + bounds = [prevBoundingBox.x[0], prevBoundingBox.y[0], prevBoundingBox.x[1], prevBoundingBox.y[1]]; + } + + const layers = viewerLayers.toSorted((a, b) => b.position - a.position).map((layer) => layer.layer); + layers.push(new PlaceholderLayer({ id: "placeholder" })); + + return ( +
+ 0}> +
+ +
+
+
+ ); +} diff --git a/frontend/src/modules/2DViewer/view/components/ReadoutBoxWrapper.tsx b/frontend/src/modules/2DViewer/view/components/ReadoutBoxWrapper.tsx new file mode 100644 index 000000000..70a488c0b --- /dev/null +++ b/frontend/src/modules/2DViewer/view/components/ReadoutBoxWrapper.tsx @@ -0,0 +1,117 @@ +import React from "react"; + +import { ReadoutBox, ReadoutItem } from "@modules/_shared/components/ReadoutBox"; +import { ExtendedLayerProps, LayerPickInfo } from "@webviz/subsurface-viewer"; + +import { isEqual } from "lodash"; + +// Needs extra distance for the left side; this avoids overlapping with legend elements +const READOUT_EDGE_DISTANCE_REM = { left: 6 }; + +function makePositionReadout(layerPickInfo: LayerPickInfo): ReadoutItem | null { + if (layerPickInfo.coordinate === undefined || layerPickInfo.coordinate.length < 2) { + return null; + } + return { + label: "Position", + info: [ + { + name: "x", + value: layerPickInfo.coordinate[0], + unit: "m", + }, + { + name: "y", + value: layerPickInfo.coordinate[1], + unit: "m", + }, + ], + }; +} + +export type ReadoutBoxWrapperProps = { + layerPickInfo: LayerPickInfo[]; + maxNumItems?: number; + visible?: boolean; +}; + +export function ReadoutBoxWrapper(props: ReadoutBoxWrapperProps): React.ReactNode { + const [infoData, setInfoData] = React.useState([]); + const [prevLayerPickInfo, setPrevLayerPickInfo] = React.useState([]); + + if (!isEqual(props.layerPickInfo, prevLayerPickInfo)) { + setPrevLayerPickInfo(props.layerPickInfo); + const newReadoutItems: ReadoutItem[] = []; + + if (props.layerPickInfo.length === 0) { + setInfoData([]); + return; + } + + const positionReadout = makePositionReadout(props.layerPickInfo[0]); + if (!positionReadout) { + return; + } + newReadoutItems.push(positionReadout); + + for (const layerPickInfo of props.layerPickInfo) { + const layerName = (layerPickInfo.layer?.props as unknown as ExtendedLayerProps)?.name; + const layerProps = layerPickInfo.properties; + + // pick info can have 2 types of properties that can be displayed on the info card + // 1. defined as propertyValue, used for general layer info (now using for positional data) + // 2. Another defined as array of property object described by type PropertyDataType + + const layerReadout = newReadoutItems.find((item) => item.label === layerName); + + // collecting card data for 1st type + const zValue = (layerPickInfo as LayerPickInfo).propertyValue; + if (zValue !== undefined) { + if (layerReadout) { + layerReadout.info.push({ + name: "Property value", + value: zValue, + }); + } else { + newReadoutItems.push({ + label: layerName ?? "Unknown layer", + info: [ + { + name: "Property value", + value: zValue, + }, + ], + }); + } + } + + // collecting card data for 2nd type + if (!layerProps || layerProps.length === 0) { + continue; + } + if (layerReadout) { + layerProps?.forEach((prop) => { + const property = layerReadout.info?.find((item) => item.name === prop.name); + if (property) { + property.value = prop.value; + } else { + layerReadout.info.push(prop); + } + }); + } else { + newReadoutItems.push({ + label: layerName ?? "Unknown layer", + info: layerProps, + }); + } + } + + setInfoData(newReadoutItems); + } + + if (!props.visible) { + return null; + } + + return ; +} diff --git a/frontend/src/modules/2DViewer/view/components/ReadoutWrapper.tsx b/frontend/src/modules/2DViewer/view/components/ReadoutWrapper.tsx new file mode 100644 index 000000000..042573389 --- /dev/null +++ b/frontend/src/modules/2DViewer/view/components/ReadoutWrapper.tsx @@ -0,0 +1,76 @@ +import React from "react"; + +import { Layer as DeckGlLayer } from "@deck.gl/core"; +import { SubsurfaceViewerWithCameraState } from "@modules/_shared/components/SubsurfaceViewerWithCameraState"; +import { LayerPickInfo, MapMouseEvent, ViewStateType, ViewsType } from "@webviz/subsurface-viewer"; + +import { ReadoutBoxWrapper } from "./ReadoutBoxWrapper"; +import { Toolbar } from "./Toolbar"; + +export type ReadooutWrapperProps = { + views: ViewsType; + viewportAnnotations: React.ReactNode[]; + layers: DeckGlLayer[]; + bounds?: [number, number, number, number]; +}; + +export function ReadoutWrapper(props: ReadooutWrapperProps): React.ReactNode { + const id = React.useId(); + + const [cameraPositionSetByAction, setCameraPositionSetByAction] = React.useState(null); + const [triggerHomeCounter, setTriggerHomeCounter] = React.useState(0); + const [layerPickingInfo, setLayerPickingInfo] = React.useState([]); + + function handleFitInViewClick() { + setTriggerHomeCounter((prev) => prev + 1); + } + + function handleMouseHover(event: MapMouseEvent): void { + setLayerPickingInfo(event.infos); + } + + function handleMouseEvent(event: MapMouseEvent): void { + if (event.type === "hover") { + handleMouseHover(event); + } + } + + return ( + <> + + + setCameraPositionSetByAction(null)} + onMouseEvent={handleMouseEvent} + layers={props.layers} + scale={{ + visible: true, + incrementValue: 100, + widthPerUnit: 100, + cssStyle: { + right: 10, + top: 10, + }, + }} + coords={{ + visible: false, + multiPicking: true, + pickDepth: 2, + }} + triggerHome={triggerHomeCounter} + pickingRadius={5} + > + {props.viewportAnnotations} + + {props.views.viewports.length === 0 && ( +
+ Please add views and layers in the settings panel. +
+ )} + + ); +} diff --git a/frontend/src/modules/2DViewer/view/components/Toolbar.tsx b/frontend/src/modules/2DViewer/view/components/Toolbar.tsx new file mode 100644 index 000000000..08527e27d --- /dev/null +++ b/frontend/src/modules/2DViewer/view/components/Toolbar.tsx @@ -0,0 +1,21 @@ +import { Button } from "@lib/components/Button"; +import { Toolbar as GenericToolbar } from "@modules/_shared/components/Toolbar"; +import { FilterCenterFocus } from "@mui/icons-material"; + +export type ToolbarProps = { + onFitInView: () => void; +}; + +export function Toolbar(props: ToolbarProps): React.ReactNode { + function handleFitInViewClick() { + props.onFitInView(); + } + + return ( + + + + ); +} diff --git a/frontend/src/modules/2DViewer/view/customDeckGlLayers/AdvancedWellsLayer.ts b/frontend/src/modules/2DViewer/view/customDeckGlLayers/AdvancedWellsLayer.ts new file mode 100644 index 000000000..112c3363b --- /dev/null +++ b/frontend/src/modules/2DViewer/view/customDeckGlLayers/AdvancedWellsLayer.ts @@ -0,0 +1,67 @@ +import { FilterContext, Layer, LayersList } from "@deck.gl/core"; +import { GeoJsonLayer } from "@deck.gl/layers"; +import { WellsLayer } from "@webviz/subsurface-viewer/dist/layers"; + +export class AdvancedWellsLayer extends WellsLayer { + static layerName: string = "WellsLayer"; + + constructor(props: any) { + super(props); + } + + filterSubLayer(context: FilterContext): boolean { + if (context.layer.id.includes("names")) { + return context.viewport.zoom > -2; + } + + return true; + } + + renderLayers(): LayersList { + const layers = super.renderLayers(); + + if (!Array.isArray(layers)) { + return layers; + } + + const colorsLayer = layers.find((layer) => { + if (!(layer instanceof Layer)) { + return false; + } + + return layer.id.includes("colors"); + }); + + if (!(colorsLayer instanceof GeoJsonLayer)) { + return layers; + } + + const newColorsLayer = new GeoJsonLayer({ + data: colorsLayer.props.data, + pickable: true, + stroked: false, + positionFormat: colorsLayer.props.positionFormat, + pointRadiusUnits: "meters", + lineWidthUnits: "meters", + pointRadiusScale: this.props.pointRadiusScale, + lineWidthScale: this.props.lineWidthScale, + getLineWidth: colorsLayer.props.getLineWidth, + getPointRadius: colorsLayer.props.getPointRadius, + lineBillboard: true, + pointBillboard: true, + parameters: colorsLayer.props.parameters, + visible: colorsLayer.props.visible, + id: "colors", + lineWidthMinPixels: 1, + lineWidthMaxPixels: 5, + extensions: colorsLayer.props.extensions, + getDashArray: colorsLayer.props.getDashArray, + getLineColor: colorsLayer.props.getLineColor, + getFillColor: colorsLayer.props.getFillColor, + autoHighlight: true, + onHover: () => {}, + }); + + return [newColorsLayer, ...layers.filter((layer) => layer !== colorsLayer)]; + } +} diff --git a/frontend/src/modules/2DViewer/view/customDeckGlLayers/PlaceholderLayer.ts b/frontend/src/modules/2DViewer/view/customDeckGlLayers/PlaceholderLayer.ts new file mode 100644 index 000000000..b146c9fea --- /dev/null +++ b/frontend/src/modules/2DViewer/view/customDeckGlLayers/PlaceholderLayer.ts @@ -0,0 +1,21 @@ +import { Layer } from "@deck.gl/core"; + +type PlaceholderLayerProps = { + id: string; +}; + +export class PlaceholderLayer extends Layer { + static layerName: string = "PlaceholderLayer"; + + constructor(props: PlaceholderLayerProps) { + super(props); + } + + initializeState(): void { + return; + } + + render() { + return null; + } +} diff --git a/frontend/src/modules/2DViewer/view/customDeckGlLayers/WellborePicksLayer.ts b/frontend/src/modules/2DViewer/view/customDeckGlLayers/WellborePicksLayer.ts new file mode 100644 index 000000000..c58047a25 --- /dev/null +++ b/frontend/src/modules/2DViewer/view/customDeckGlLayers/WellborePicksLayer.ts @@ -0,0 +1,136 @@ +import { CompositeLayer, CompositeLayerProps, FilterContext, Layer, UpdateParameters } from "@deck.gl/core"; +import { GeoJsonLayer, TextLayer } from "@deck.gl/layers"; + +import type { Feature, FeatureCollection } from "geojson"; + +export type WellBorePickLayerData = { + easting: number; + northing: number; + wellBoreUwi: string; + tvdMsl: number; + md: number; + slotName: string; +}; + +type TextLayerData = { + coordinates: [number, number, number]; + name: string; +}; + +export type WellBorePicksLayerProps = { + id: string; + data: WellBorePickLayerData[]; +}; + +export class WellborePicksLayer extends CompositeLayer { + static layerName: string = "WellborePicksLayer"; + private _textData: TextLayerData[] = []; + private _pointsData: FeatureCollection | null = null; + + filterSubLayer(context: FilterContext): boolean { + if (context.layer.id.includes("text")) { + return context.viewport.zoom > -4; + } + + return true; + } + + updateState(params: UpdateParameters>>): void { + const features: Feature[] = params.props.data.map((wellPick) => { + return { + type: "Feature", + geometry: { + type: "Point", + coordinates: [wellPick.easting, wellPick.northing], + }, + properties: { + name: `${wellPick.wellBoreUwi}, TVD_MSL: ${wellPick.tvdMsl}, MD: ${wellPick.md}`, + color: [100, 100, 100, 100], + }, + }; + }); + + const pointsData: FeatureCollection = { + type: "FeatureCollection", + features: features, + }; + + const textData: TextLayerData[] = this.props.data.map((wellPick) => { + return { + coordinates: [wellPick.easting, wellPick.northing, wellPick.tvdMsl], + name: wellPick.wellBoreUwi, + }; + }); + + this._pointsData = pointsData; + this._textData = textData; + } + + renderLayers() { + const fontSize = 16; + const sizeMinPixels = 16; + const sizeMaxPixels = 16; + + return [ + new GeoJsonLayer( + this.getSubLayerProps({ + id: "points", + data: this._pointsData ?? undefined, + // pointType: 'circle+text', + filled: true, + lineWidthMinPixels: 5, + lineWidthMaxPixels: 5, + lineWidthUnits: "meters", + parameters: { + depthTest: false, + }, + getLineWidth: 1, + depthTest: false, + pickable: true, + getText: (d: Feature) => d.properties?.wellBoreUwi, + getLineColor: [50, 50, 50], + // extensions: [new CollisionFilterExtension()], + // collisionGroup: "wellbore-picks", + }) + ), + + new TextLayer( + this.getSubLayerProps({ + id: "text", + data: this._textData, + // depthTest: true, + pickable: true, + getColor: [255, 255, 255], + fontWeight: 800, + fontSettings: { + fontSize: fontSize * 2, + sdf: true, + }, + outlineColor: [0, 0, 0], + outlineWidth: 2, + getSize: 12, + sdf: true, + sizeScale: fontSize, + sizeUnits: "meters", + sizeMinPixels: sizeMinPixels, + sizeMaxPixels: sizeMaxPixels, + getAlignmentBaseline: "top", + getTextAnchor: "middle", + getPosition: (d: TextLayerData) => d.coordinates, + getText: (d: TextLayerData) => d.name, + // maxWidth: 64 * 12, + /* + // extensions: [new CollisionFilterExtension()], + collisionGroup: "wellbore-picks", + + collisionTestProps: { + sizeScale: fontSize, + sizeMaxPixels: sizeMaxPixels * 2, + sizeMinPixels: sizeMinPixels * 2, + }, + */ + }) + ), + ]; + } +} diff --git a/frontend/src/modules/2DViewer/view/utils/layerFactory.ts b/frontend/src/modules/2DViewer/view/utils/layerFactory.ts new file mode 100644 index 000000000..fc03f2de5 --- /dev/null +++ b/frontend/src/modules/2DViewer/view/utils/layerFactory.ts @@ -0,0 +1,363 @@ +import { PolygonData_api, SurfaceDataPng_api, SurfaceDef_api, WellborePick_api, WellboreTrajectory_api } from "@api"; +import { Layer } from "@deck.gl/core"; +import { GeoJsonLayer } from "@deck.gl/layers"; +import { defaultColorPalettes } from "@framework/utils/colorPalettes"; +import { ColorScaleGradientType, ColorScaleType } from "@lib/utils/ColorScale"; +import { Vec2, rotatePoint2Around } from "@lib/utils/vec2"; +import { GridMappedProperty_trans, GridSurface_trans } from "@modules/3DViewer/view/queries/queryDataTransforms"; +import { ColorScaleWithName } from "@modules/_shared/utils/ColorScaleWithName"; +import { ColormapLayer, Grid3DLayer, WellsLayer } from "@webviz/subsurface-viewer/dist/layers"; + +import { Rgb, parse } from "culori"; +import { Feature, FeatureCollection, GeoJsonProperties, Geometry } from "geojson"; + +import { DrilledWellTrajectoriesLayer } from "../../layers/implementations/layers/DrilledWellTrajectoriesLayer/DrilledWellTrajectoriesLayer"; +import { DrilledWellborePicksLayer } from "../../layers/implementations/layers/DrilledWellborePicksLayer/DrilledWellborePicksLayer"; +import { ObservedSurfaceLayer } from "../../layers/implementations/layers/ObservedSurfaceLayer/ObservedSurfaceLayer"; +import { RealizationGridLayer } from "../../layers/implementations/layers/RealizationGridLayer/RealizationGridLayer"; +import { RealizationPolygonsLayer } from "../../layers/implementations/layers/RealizationPolygonsLayer/RealizationPolygonsLayer"; +import { RealizationSurfaceLayer } from "../../layers/implementations/layers/RealizationSurfaceLayer/RealizationSurfaceLayer"; +import { StatisticalSurfaceLayer } from "../../layers/implementations/layers/StatisticalSurfaceLayer/StatisticalSurfaceLayer"; +import { Layer as LayerInterface } from "../../layers/interfaces"; +import { AdvancedWellsLayer } from "../customDeckGlLayers/AdvancedWellsLayer"; +import { WellBorePickLayerData, WellborePicksLayer } from "../customDeckGlLayers/WellborePicksLayer"; + +export function makeLayer(layer: LayerInterface, colorScale?: ColorScaleWithName): Layer | null { + const data = layer.getLayerDelegate().getData(); + + if (colorScale === undefined) { + colorScale = new ColorScaleWithName({ + colorPalette: defaultColorPalettes[0], + gradientType: ColorScaleGradientType.Sequential, + name: "Default", + type: ColorScaleType.Continuous, + steps: 10, + }); + } + + if (!data) { + return null; + } + if (layer instanceof ObservedSurfaceLayer) { + return createMapImageLayer( + data, + layer.getItemDelegate().getId(), + layer.getItemDelegate().getName(), + colorScale + ); + } + if (layer instanceof RealizationSurfaceLayer) { + return createMapImageLayer( + data, + layer.getItemDelegate().getId(), + layer.getItemDelegate().getName(), + colorScale + ); + } + if (layer instanceof StatisticalSurfaceLayer) { + return createMapImageLayer( + data, + layer.getItemDelegate().getId(), + layer.getItemDelegate().getName(), + colorScale + ); + } + if (layer instanceof RealizationPolygonsLayer) { + return createPolygonsLayer(data, layer.getItemDelegate().getId()); + } + if (layer instanceof DrilledWellTrajectoriesLayer) { + return makeWellsLayer(data, layer.getItemDelegate().getId(), null); + } + if (layer instanceof DrilledWellborePicksLayer) { + return createWellPicksLayer(data, layer.getItemDelegate().getId()); + } + if (layer instanceof RealizationGridLayer) { + return makeGrid3DLayer( + layer.getItemDelegate().getId(), + data.gridSurfaceData, + data.gridParameterData, + layer.getSettingsContext().getDelegate().getSettings().showGridLines.getDelegate().getValue(), + colorScale + ); + } + return null; +} +function createWellPicksLayer(wellPicksDataApi: WellborePick_api[], id: string): WellborePicksLayer { + const wellPicksData: WellBorePickLayerData[] = wellPicksDataApi.map((wellPick) => { + return { + easting: wellPick.easting, + northing: wellPick.northing, + wellBoreUwi: wellPick.uniqueWellboreIdentifier, + tvdMsl: wellPick.tvdMsl, + md: wellPick.md, + pickable: true, + slotName: "", + }; + }); + return new WellborePicksLayer({ + id: id, + data: wellPicksData, + pickable: true, + }); +} + +/* +function createMapFloatLayer(layerData: SurfaceDataFloat_trans, id: string): MapLayer { + return new MapLayer({ + id: id, + meshData: layerData.valuesFloat32Arr, + typedArraySupport: true, + frame: { + origin: [layerData.surface_def.origin_utm_x, layerData.surface_def.origin_utm_y], + count: [layerData.surface_def.npoints_x, layerData.surface_def.npoints_y], + increment: [layerData.surface_def.inc_x, layerData.surface_def.inc_y], + rotDeg: layerData.surface_def.rot_deg, + }, + contours: [0, 100], + isContoursDepth: true, + gridLines: false, + material: true, + smoothShading: true, + colorMapName: "Physics", + parameters: { + depthTest: false, + }, + depthTest: false, + }); +} +*/ + +function createMapImageLayer( + layerData: SurfaceDataPng_api, + id: string, + name: string, + colorScale?: ColorScaleWithName +): ColormapLayer { + return new ColormapLayer({ + id: id, + name: name, + image: `data:image/png;base64,${layerData.png_image_base64}`, + bounds: _calcBoundsForRotationAroundUpperLeftCorner(layerData.surface_def), + rotDeg: layerData.surface_def.rot_deg, + valueRange: [layerData.value_min, layerData.value_max], + colorMapRange: [layerData.value_min, layerData.value_max], + colorMapName: "Physics", + parameters: { + depthWriteEnabled: false, + }, + colorMapFunction: makeColorMapFunction(colorScale, layerData.value_min, layerData.value_max), + }); +} + +function _calcBoundsForRotationAroundUpperLeftCorner(surfDef: SurfaceDef_api): [number, number, number, number] { + const width = (surfDef.npoints_x - 1) * surfDef.inc_x; + const height = (surfDef.npoints_y - 1) * surfDef.inc_y; + const orgRotPoint: Vec2 = { x: surfDef.origin_utm_x, y: surfDef.origin_utm_y }; + const orgTopLeft: Vec2 = { x: surfDef.origin_utm_x, y: surfDef.origin_utm_y + height }; + + const transTopLeft: Vec2 = rotatePoint2Around(orgTopLeft, orgRotPoint, (surfDef.rot_deg * Math.PI) / 180); + const tLeft = transTopLeft.x; + const tBottom = transTopLeft.y - height; + const tRight = transTopLeft.x + width; + const tTop = transTopLeft.y; + + const bounds: [number, number, number, number] = [tLeft, tBottom, tRight, tTop]; + + return bounds; +} + +function createPolygonsLayer(polygonsData: PolygonData_api[], id: string): GeoJsonLayer { + const features: Feature[] = polygonsData.map((polygon) => { + return polygonsToGeojson(polygon); + }); + const data: FeatureCollection = { + type: "FeatureCollection", + features: features, + }; + return new GeoJsonLayer({ + id: id, + data: data, + // opacity: 0.5, + filled: false, + lineWidthMinPixels: 2, + parameters: { + depthTest: false, + }, + + pickable: true, + }); +} +function polygonsToGeojson(polygons: PolygonData_api): Feature { + const data: Feature = { + type: "Feature", + geometry: { + type: "Polygon", + coordinates: [zipCoords(polygons.x_arr, polygons.y_arr, polygons.z_arr)], + }, + properties: { name: polygons.poly_id, color: [0, 0, 0, 255] }, + }; + return data; +} + +export function makeWellsLayer( + fieldWellboreTrajectoriesData: WellboreTrajectory_api[], + id: string, + selectedWellboreUuid: string | null +): WellsLayer { + const tempWorkingWellsData = fieldWellboreTrajectoriesData.filter( + (el) => el.uniqueWellboreIdentifier !== "NO 34/4-K-3 AH" + ); + const wellLayerDataFeatures = tempWorkingWellsData.map((well) => + wellTrajectoryToGeojson(well, selectedWellboreUuid) + ); + + function getLineStyleWidth(object: Feature): number { + if (object.properties && "lineWidth" in object.properties) { + return object.properties.lineWidth as number; + } + return 2; + } + + function getWellHeadStyleWidth(object: Feature): number { + if (object.properties && "wellHeadSize" in object.properties) { + return object.properties.wellHeadSize as number; + } + return 1; + } + + function getColor(object: Feature): [number, number, number, number] { + if (object.properties && "color" in object.properties) { + return object.properties.color as [number, number, number, number]; + } + return [50, 50, 50, 100]; + } + + const wellsLayer = new AdvancedWellsLayer({ + id: id, + data: { + type: "FeatureCollection", + unit: "m", + features: wellLayerDataFeatures, + }, + refine: false, + lineStyle: { width: getLineStyleWidth, color: getColor }, + wellHeadStyle: { size: getWellHeadStyleWidth, color: getColor }, + wellNameVisible: true, + pickable: true, + ZIncreasingDownwards: false, + outline: false, + lineWidthScale: 2, + }); + + return wellsLayer; +} + +export function wellTrajectoryToGeojson( + wellTrajectory: WellboreTrajectory_api, + selectedWellboreUuid: string | null +): Record { + const point: Record = { + type: "Point", + coordinates: [wellTrajectory.eastingArr[0], wellTrajectory.northingArr[0], -wellTrajectory.tvdMslArr[0]], + }; + const coordinates: Record = { + type: "LineString", + coordinates: zipCoords(wellTrajectory.eastingArr, wellTrajectory.northingArr, wellTrajectory.tvdMslArr), + }; + + let color = [100, 100, 100]; + let lineWidth = 2; + let wellHeadSize = 1; + if (wellTrajectory.wellboreUuid === selectedWellboreUuid) { + color = [255, 0, 0]; + lineWidth = 5; + wellHeadSize = 10; + } + + const geometryCollection: Record = { + type: "Feature", + geometry: { + type: "GeometryCollection", + geometries: [point, coordinates], + }, + properties: { + uuid: wellTrajectory.wellboreUuid, + name: wellTrajectory.uniqueWellboreIdentifier, + uwi: wellTrajectory.uniqueWellboreIdentifier, + color, + md: [wellTrajectory.mdArr], + lineWidth, + wellHeadSize, + }, + }; + + return geometryCollection; +} + +function zipCoords(x_arr: number[], y_arr: number[], z_arr: number[]): number[][] { + const coords: number[][] = []; + for (let i = 0; i < x_arr.length; i++) { + coords.push([x_arr[i], y_arr[i], -z_arr[i]]); + } + + return coords; +} +type WorkingGrid3dLayer = { + pointsData: Float32Array; + polysData: Uint32Array; + propertiesData: Float32Array; + colorMapName: string; + ZIncreasingDownwards: boolean; +} & Layer; + +export function makeGrid3DLayer( + id: string, + gridSurfaceData: GridSurface_trans, + gridParameterData: GridMappedProperty_trans, + showGridLines: boolean, + colorScale?: ColorScaleWithName + // colorScale: ColorScale +): WorkingGrid3dLayer { + const offsetXyz = [gridSurfaceData.origin_utm_x, gridSurfaceData.origin_utm_y, 0]; + const pointsNumberArray = gridSurfaceData.pointsFloat32Arr.map((val, i) => val + offsetXyz[i % 3]); + const polysNumberArray = gridSurfaceData.polysUint32Arr; + const grid3dLayer = new Grid3DLayer({ + id: id, + pointsData: pointsNumberArray, + polysData: polysNumberArray, + propertiesData: gridParameterData.polyPropsFloat32Arr, + ZIncreasingDownwards: false, + gridLines: showGridLines, + material: { ambient: 0.4, diffuse: 0.7, shininess: 8, specularColor: [25, 25, 25] }, + pickable: true, + colorMapName: "Physics", + colorMapClampColor: true, + colorMapRange: [gridParameterData.min_grid_prop_value, gridParameterData.max_grid_prop_value], + colorMapFunction: makeColorMapFunction( + colorScale, + gridParameterData.min_grid_prop_value, + gridParameterData.max_grid_prop_value + ), + }); + return grid3dLayer as unknown as WorkingGrid3dLayer; +} + +function makeColorMapFunction( + colorScale: ColorScaleWithName | undefined, + valueMin: number, + valueMax: number +): ((value: number) => [number, number, number]) | undefined { + if (!colorScale) { + return undefined; + } + + return (value: number) => { + const nonNormalizedValue = value * (valueMax - valueMin) + valueMin; + const interpolatedColor = colorScale.getColorForValue(nonNormalizedValue); + const color = parse(interpolatedColor) as Rgb; + if (color === undefined) { + return [0, 0, 0]; + } + return [color.r * 255, color.g * 255, color.b * 255]; + }; +} diff --git a/frontend/src/modules/2DViewer/view/utils/makeViewsAndLayers.ts b/frontend/src/modules/2DViewer/view/utils/makeViewsAndLayers.ts new file mode 100644 index 000000000..d0e69341e --- /dev/null +++ b/frontend/src/modules/2DViewer/view/utils/makeViewsAndLayers.ts @@ -0,0 +1,179 @@ +import { Layer as DeckGlLayer } from "@deck.gl/core"; +import { StatusMessage } from "@framework/ModuleInstanceStatusController"; +import { defaultContinuousSequentialColorPalettes } from "@framework/utils/colorPalettes"; +import { ColorScaleGradientType, ColorScaleType } from "@lib/utils/ColorScale"; +import { ColorScale } from "@modules/2DViewer/layers/ColorScale"; +import { DeltaSurface } from "@modules/2DViewer/layers/DeltaSurface"; +import { View } from "@modules/2DViewer/layers/View"; +import { LayerStatus } from "@modules/2DViewer/layers/delegates/LayerDelegate"; +import { BoundingBox, Group, Layer, instanceofGroup, instanceofLayer } from "@modules/2DViewer/layers/interfaces"; +import { ColorScaleWithId } from "@modules/_shared/components/ColorLegendsContainer/colorLegendsContainer"; +import { ColorScaleWithName } from "@modules/_shared/utils/ColorScaleWithName"; + +import { makeLayer } from "./layerFactory"; + +export type DeckGlLayerWithPosition = { + layer: DeckGlLayer; + position: number; +}; + +export type DeckGlView = { + id: string; + color: string | null; + name: string; + layers: DeckGlLayerWithPosition[]; + colorScales: ColorScaleWithId[]; +}; + +export type DeckGlViewsAndLayers = { + views: DeckGlView[]; + layers: DeckGlLayerWithPosition[]; + errorMessages: (StatusMessage | string)[]; + boundingBox: BoundingBox | null; + colorScales: ColorScaleWithId[]; + numLoadingLayers: number; +}; + +export function recursivelyMakeViewsAndLayers(group: Group, numCollectedLayers: number = 0): DeckGlViewsAndLayers { + const collectedViews: DeckGlView[] = []; + const collectedLayers: DeckGlLayerWithPosition[] = []; + const collectedColorScales: ColorScaleWithId[] = []; + const collectedErrorMessages: (StatusMessage | string)[] = []; + let collectedNumLoadingLayers = 0; + let globalBoundingBox: BoundingBox | null = null; + + const children = group.getGroupDelegate().getChildren(); + + const maybeApplyBoundingBox = (boundingBox: BoundingBox | null) => { + if (boundingBox) { + globalBoundingBox = + globalBoundingBox === null ? boundingBox : makeNewBoundingBox(boundingBox, globalBoundingBox); + } + }; + + for (const child of children) { + if (!child.getItemDelegate().isVisible()) { + continue; + } + + if (instanceofGroup(child) && !(child instanceof DeltaSurface)) { + const { views, layers, boundingBox, colorScales, numLoadingLayers, errorMessages } = + recursivelyMakeViewsAndLayers(child, numCollectedLayers + collectedLayers.length); + + collectedErrorMessages.push(...errorMessages); + collectedNumLoadingLayers += numLoadingLayers; + maybeApplyBoundingBox(boundingBox); + + if (child instanceof View) { + const view: DeckGlView = { + id: child.getItemDelegate().getId(), + color: child.getGroupDelegate().getColor(), + name: child.getItemDelegate().getName(), + layers: layers, + colorScales, + }; + + collectedViews.push(view); + continue; + } + + collectedLayers.push(...layers); + collectedViews.push(...views); + } + + if (instanceofLayer(child)) { + if (child.getLayerDelegate().getStatus() === LayerStatus.LOADING) { + collectedNumLoadingLayers++; + } + + if (child.getLayerDelegate().getStatus() !== LayerStatus.SUCCESS) { + if (child.getLayerDelegate().getStatus() === LayerStatus.ERROR) { + const error = child.getLayerDelegate().getError(); + if (error) { + collectedErrorMessages.push(error); + } + } + continue; + } + + const colorScale = findColorScale(child); + + const layer = makeLayer(child, colorScale?.colorScale ?? undefined); + + if (!layer) { + continue; + } + + if (colorScale) { + collectedColorScales.push(colorScale); + } + + const boundingBox = child.getLayerDelegate().getBoundingBox(); + maybeApplyBoundingBox(boundingBox); + collectedLayers.push({ layer, position: numCollectedLayers + collectedLayers.length }); + } + } + + return { + views: collectedViews, + layers: collectedLayers, + errorMessages: collectedErrorMessages, + boundingBox: globalBoundingBox, + colorScales: collectedColorScales, + numLoadingLayers: collectedNumLoadingLayers, + }; +} + +function findColorScale(layer: Layer): { id: string; colorScale: ColorScaleWithName } | null { + if (layer.getLayerDelegate().getColoringType() !== "COLORSCALE") { + return null; + } + + let colorScaleWithName = new ColorScaleWithName({ + colorPalette: defaultContinuousSequentialColorPalettes[0], + gradientType: ColorScaleGradientType.Sequential, + name: layer.getItemDelegate().getName(), + type: ColorScaleType.Continuous, + steps: 10, + }); + + const range = layer.getLayerDelegate().getValueRange(); + if (range) { + colorScaleWithName.setRangeAndMidPoint(range[0], range[1], (range[0] + range[1]) / 2); + } + + const colorScaleItemArr = layer + .getItemDelegate() + .getParentGroup() + ?.getAncestorAndSiblingItems((item) => item instanceof ColorScale); + + if (colorScaleItemArr && colorScaleItemArr.length > 0) { + const colorScaleItem = colorScaleItemArr[0]; + if (colorScaleItem instanceof ColorScale) { + colorScaleWithName = ColorScaleWithName.fromColorScale( + colorScaleItem.getColorScale(), + layer.getItemDelegate().getName() + ); + + if (!colorScaleItem.getAreBoundariesUserDefined()) { + const range = layer.getLayerDelegate().getValueRange(); + if (range) { + colorScaleWithName.setRangeAndMidPoint(range[0], range[1], (range[0] + range[1]) / 2); + } + } + } + } + + return { + id: layer.getItemDelegate().getId(), + colorScale: colorScaleWithName, + }; +} + +function makeNewBoundingBox(newBoundingBox: BoundingBox, oldBoundingBox: BoundingBox): BoundingBox { + return { + x: [Math.min(newBoundingBox.x[0], oldBoundingBox.x[0]), Math.max(newBoundingBox.x[1], oldBoundingBox.x[1])], + y: [Math.min(newBoundingBox.y[0], oldBoundingBox.y[0]), Math.max(newBoundingBox.y[1], oldBoundingBox.y[1])], + z: [Math.min(newBoundingBox.z[0], oldBoundingBox.z[0]), Math.max(newBoundingBox.z[1], oldBoundingBox.z[1])], + }; +} diff --git a/frontend/src/modules/2DViewer/view/view.tsx b/frontend/src/modules/2DViewer/view/view.tsx new file mode 100644 index 000000000..bc4022d72 --- /dev/null +++ b/frontend/src/modules/2DViewer/view/view.tsx @@ -0,0 +1,24 @@ +import React from "react"; + +import { ModuleViewProps } from "@framework/Module"; + +import { LayersWrapper } from "./components/LayersWrapper"; + +import { Interfaces } from "../interfaces"; + +export function View(props: ModuleViewProps): React.ReactNode { + const preferredViewLayout = props.viewContext.useSettingsToViewInterfaceValue("preferredViewLayout"); + const layerManager = props.viewContext.useSettingsToViewInterfaceValue("layerManager"); + + if (!layerManager) { + return null; + } + + return ( + + ); +} diff --git a/frontend/src/modules/_shared/components/Toolbar/index.ts b/frontend/src/modules/_shared/components/Toolbar/index.ts new file mode 100644 index 000000000..92e32c985 --- /dev/null +++ b/frontend/src/modules/_shared/components/Toolbar/index.ts @@ -0,0 +1,3 @@ +export { Toolbar } from "./toolbar"; +export { ToolBarDivider } from "./toolbarDivider"; +export type { ToolbarProps } from "./toolbar"; diff --git a/frontend/src/modules/_shared/components/Toolbar/toolbar.tsx b/frontend/src/modules/_shared/components/Toolbar/toolbar.tsx new file mode 100644 index 000000000..3ca04a388 --- /dev/null +++ b/frontend/src/modules/_shared/components/Toolbar/toolbar.tsx @@ -0,0 +1,16 @@ +export type ToolbarProps = { + hidden?: boolean; + children: React.ReactNode; +}; + +export function Toolbar(props: ToolbarProps): React.ReactNode { + if (props.hidden) { + return null; + } + + return ( +
+ {props.children} +
+ ); +} diff --git a/frontend/src/modules/_shared/components/Toolbar/toolbarDivider.tsx b/frontend/src/modules/_shared/components/Toolbar/toolbarDivider.tsx new file mode 100644 index 000000000..c3c141c7f --- /dev/null +++ b/frontend/src/modules/_shared/components/Toolbar/toolbarDivider.tsx @@ -0,0 +1,3 @@ +export function ToolBarDivider(): React.ReactNode { + return
; +} diff --git a/frontend/src/modules/registerAllModules.ts b/frontend/src/modules/registerAllModules.ts index f16f380d4..e4609ba35 100644 --- a/frontend/src/modules/registerAllModules.ts +++ b/frontend/src/modules/registerAllModules.ts @@ -1,5 +1,6 @@ import { isDevMode } from "@lib/utils/devMode"; +import "./2DViewer/registerModule"; import "./3DViewer/registerModule"; import "./DistributionPlot/registerModule"; import "./FlowNetwork/registerModule"; @@ -14,9 +15,8 @@ import "./SimulationTimeSeries/registerModule"; import "./SimulationTimeSeriesSensitivity/registerModule"; import "./SubsurfaceMap/registerModule"; import "./TornadoChart/registerModule"; -import "./WellCompletions/registerModule"; import "./Vfp/registerModule"; - +import "./WellCompletions/registerModule"; if (isDevMode()) { await import("./MyModule/registerModule"); From c72e12ea840505065f606d6e260d880180a01e85 Mon Sep 17 00:00:00 2001 From: Ruben Thoms Date: Fri, 6 Dec 2024 13:22:54 +0100 Subject: [PATCH 2/8] Added missing package --- frontend/package-lock.json | 2001 +++++++++++++++++++++++++++++++++--- frontend/package.json | 3 +- 2 files changed, 1850 insertions(+), 154 deletions(-) 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", From 15046c877c492d0bfc344a73008d2d0cf9c89790 Mon Sep 17 00:00:00 2001 From: Ruben Thoms Date: Fri, 6 Dec 2024 13:28:22 +0100 Subject: [PATCH 3/8] removed unused variables --- .../layers/RealizationGridLayer/RealizationGridContext.ts | 2 +- .../RealizationPolygonsLayer/RealizationPolygonsContext.ts | 1 - 2 files changed, 1 insertion(+), 2 deletions(-) diff --git a/frontend/src/modules/2DViewer/layers/implementations/layers/RealizationGridLayer/RealizationGridContext.ts b/frontend/src/modules/2DViewer/layers/implementations/layers/RealizationGridLayer/RealizationGridContext.ts index e768409b5..6163be4d8 100644 --- a/frontend/src/modules/2DViewer/layers/implementations/layers/RealizationGridLayer/RealizationGridContext.ts +++ b/frontend/src/modules/2DViewer/layers/implementations/layers/RealizationGridLayer/RealizationGridContext.ts @@ -53,10 +53,10 @@ export class RealizationGridContext implements SettingsContext) { availableSettingsUpdater(SettingType.ENSEMBLE, ({ getGlobalSetting }) => { diff --git a/frontend/src/modules/2DViewer/layers/implementations/layers/RealizationPolygonsLayer/RealizationPolygonsContext.ts b/frontend/src/modules/2DViewer/layers/implementations/layers/RealizationPolygonsLayer/RealizationPolygonsContext.ts index d1a5c73ed..433ba563c 100644 --- a/frontend/src/modules/2DViewer/layers/implementations/layers/RealizationPolygonsLayer/RealizationPolygonsContext.ts +++ b/frontend/src/modules/2DViewer/layers/implementations/layers/RealizationPolygonsLayer/RealizationPolygonsContext.ts @@ -41,7 +41,6 @@ export class RealizationPolygonsContext implements SettingsContext) { availableSettingsUpdater(SettingType.ENSEMBLE, ({ getGlobalSetting }) => { From 525bb984f0b64502002a98983869c4422268ad22 Mon Sep 17 00:00:00 2001 From: Ruben Thoms Date: Fri, 6 Dec 2024 13:30:05 +0100 Subject: [PATCH 4/8] other fixes --- .../src/modules/2DViewer/view/utils/layerFactory.ts | 12 +++++------- 1 file changed, 5 insertions(+), 7 deletions(-) diff --git a/frontend/src/modules/2DViewer/view/utils/layerFactory.ts b/frontend/src/modules/2DViewer/view/utils/layerFactory.ts index fc03f2de5..bb2af37d6 100644 --- a/frontend/src/modules/2DViewer/view/utils/layerFactory.ts +++ b/frontend/src/modules/2DViewer/view/utils/layerFactory.ts @@ -137,7 +137,7 @@ function createMapImageLayer( id: id, name: name, image: `data:image/png;base64,${layerData.png_image_base64}`, - bounds: _calcBoundsForRotationAroundUpperLeftCorner(layerData.surface_def), + bounds: calcBoundsForRotationAroundUpperLeftCorner(layerData.surface_def), rotDeg: layerData.surface_def.rot_deg, valueRange: [layerData.value_min, layerData.value_max], colorMapRange: [layerData.value_min, layerData.value_max], @@ -149,7 +149,7 @@ function createMapImageLayer( }); } -function _calcBoundsForRotationAroundUpperLeftCorner(surfDef: SurfaceDef_api): [number, number, number, number] { +function calcBoundsForRotationAroundUpperLeftCorner(surfDef: SurfaceDef_api): [number, number, number, number] { const width = (surfDef.npoints_x - 1) * surfDef.inc_x; const height = (surfDef.npoints_y - 1) * surfDef.inc_y; const orgRotPoint: Vec2 = { x: surfDef.origin_utm_x, y: surfDef.origin_utm_y }; @@ -177,7 +177,6 @@ function createPolygonsLayer(polygonsData: PolygonData_api[], id: string): GeoJs return new GeoJsonLayer({ id: id, data: data, - // opacity: 0.5, filled: false, lineWidthMinPixels: 2, parameters: { @@ -294,10 +293,10 @@ export function wellTrajectoryToGeojson( return geometryCollection; } -function zipCoords(x_arr: number[], y_arr: number[], z_arr: number[]): number[][] { +function zipCoords(xArr: number[], yArr: number[], zArr: number[]): number[][] { const coords: number[][] = []; - for (let i = 0; i < x_arr.length; i++) { - coords.push([x_arr[i], y_arr[i], -z_arr[i]]); + for (let i = 0; i < xArr.length; i++) { + coords.push([xArr[i], yArr[i], -zArr[i]]); } return coords; @@ -316,7 +315,6 @@ export function makeGrid3DLayer( gridParameterData: GridMappedProperty_trans, showGridLines: boolean, colorScale?: ColorScaleWithName - // colorScale: ColorScale ): WorkingGrid3dLayer { const offsetXyz = [gridSurfaceData.origin_utm_x, gridSurfaceData.origin_utm_y, 0]; const pointsNumberArray = gridSurfaceData.pointsFloat32Arr.map((val, i) => val + offsetXyz[i % 3]); From 37e145c8b4597fffb249cf0173b04791fcae091c Mon Sep 17 00:00:00 2001 From: Ruben Thoms Date: Thu, 12 Dec 2024 15:37:40 +0100 Subject: [PATCH 5/8] Adjusted according to review comments --- .../src/modules/2DViewer/layers/ColorScale.ts | 4 +- .../modules/2DViewer/layers/DeltaSurface.ts | 5 +- .../src/modules/2DViewer/layers/Dependency.ts | 15 +++- .../2DViewer/layers/DeserializationFactory.ts | 23 ++++--- .../modules/2DViewer/layers/LayerManager.ts | 14 +++- .../modules/2DViewer/layers/SettingsGroup.ts | 4 +- .../modules/2DViewer/layers/SharedSetting.ts | 4 +- frontend/src/modules/2DViewer/layers/View.ts | 4 +- .../layers/components/ColorScaleComponent.tsx | 4 +- .../components/DeltaSurfaceComponent.tsx | 8 +-- .../layers/components/LayerComponent.tsx | 4 +- ...{RemoveButton.tsx => RemoveItemButton.tsx} | 4 +- .../components/SettingsGroupComponent.tsx | 8 +-- .../layers/components/ViewComponent.tsx | 8 +-- .../2DViewer/layers/components/utils.tsx | 69 +++++++++---------- .../layers/delegates/GroupDelegate.ts | 5 ++ .../2DViewer/layers/delegates/ItemDelegate.ts | 4 ++ .../layers/delegates/LayerDelegate.ts | 12 +++- .../layers/delegates/SettingDelegate.ts | 6 ++ .../delegates/SettingsContextDelegate.ts | 9 +++ .../delegates/UnsubscribeHandlerDelegate.ts | 7 ++ .../DrilledWellTrajectoriesLayer.ts | 6 +- ...DrilledWellTrajectoriesSettingsContext.ts} | 10 +-- .../DrilledWellborePicksLayer.ts | 6 +- ...=> DrilledWellborePicksSettingsContext.ts} | 19 +++-- .../ObservedSurfaceLayer.ts | 6 +- ...t.ts => ObservedSurfaceSettingsContext.ts} | 21 +++--- .../RealizationGridLayer.ts | 6 +- ...t.ts => RealizationGridSettingsContext.ts} | 32 ++++----- .../RealizationPolygonsLayer.ts | 6 +- ... => RealizationPolygonsSettingsContext.ts} | 20 +++--- .../RealizationSurfaceLayer.ts | 6 +- ...s => RealizationSurfaceSettingsContext.ts} | 22 +++--- .../StatisticalSurfaceLayer.ts | 6 +- ...s => StatisticalSurfaceSettingsContext.ts} | 26 +++---- .../layers/StatisticalSurfaceLayer/types.ts | 2 +- ...lbores.tsx => DrilledWellboresSetting.tsx} | 31 +-------- .../{Ensemble.tsx => EnsembleSetting.tsx} | 4 +- ...Attribute.tsx => GridAttributeSetting.tsx} | 4 +- .../{GridLayer.tsx => GridLayerSetting.tsx} | 4 +- .../{GridName.tsx => GridNameSetting.tsx} | 4 +- ...ibute.tsx => PolygonsAttributeSetting.tsx} | 4 +- ...lygonsName.tsx => PolygonsNameSetting.tsx} | 4 +- ...Realization.tsx => RealizationSetting.tsx} | 4 +- ...Sensitivity.tsx => SensitivitySetting.tsx} | 4 +- ...GridLines.tsx => ShowGridLinesSetting.tsx} | 4 +- ...ction.tsx => StatisticFunctionSetting.tsx} | 4 +- ...ribute.tsx => SurfaceAttributeSetting.tsx} | 4 +- ...SurfaceName.tsx => SurfaceNameSetting.tsx} | 4 +- ...Interval.tsx => TimeOrIntervalSetting.tsx} | 4 +- .../src/modules/2DViewer/layers/interfaces.ts | 35 +++++----- .../components/layerManagerComponent.tsx | 26 +++---- frontend/src/modules/2DViewer/types.ts | 34 --------- .../view/components/LayersWrapper.tsx | 6 +- .../view/components/ReadoutWrapper.tsx | 4 +- .../customDeckGlLayers/WellborePicksLayer.ts | 15 ---- .../2DViewer/view/utils/layerFactory.ts | 34 ++------- .../2DViewer/view/utils/makeViewsAndLayers.ts | 20 +++--- 58 files changed, 316 insertions(+), 356 deletions(-) rename frontend/src/modules/2DViewer/layers/components/{RemoveButton.tsx => RemoveItemButton.tsx} (87%) rename frontend/src/modules/2DViewer/layers/implementations/layers/DrilledWellTrajectoriesLayer/{DrilledWellTrajectoriesContext.ts => DrilledWellTrajectoriesSettingsContext.ts} (90%) rename frontend/src/modules/2DViewer/layers/implementations/layers/DrilledWellborePicksLayer/{DrilledWellborePicksContext.ts => DrilledWellborePicksSettingsContext.ts} (88%) rename frontend/src/modules/2DViewer/layers/implementations/layers/ObservedSurfaceLayer/{ObservedSurfaceContext.ts => ObservedSurfaceSettingsContext.ts} (89%) rename frontend/src/modules/2DViewer/layers/implementations/layers/RealizationGridLayer/{RealizationGridContext.ts => RealizationGridSettingsContext.ts} (87%) rename frontend/src/modules/2DViewer/layers/implementations/layers/RealizationPolygonsLayer/{RealizationPolygonsContext.ts => RealizationPolygonsSettingsContext.ts} (87%) rename frontend/src/modules/2DViewer/layers/implementations/layers/RealizationSurfaceLayer/{RealizationSurfaceContext.ts => RealizationSurfaceSettingsContext.ts} (88%) rename frontend/src/modules/2DViewer/layers/implementations/layers/StatisticalSurfaceLayer/{StatisticalSurfaceContext.ts => StatisticalSurfaceSettingsContext.ts} (88%) rename frontend/src/modules/2DViewer/layers/implementations/settings/{DrilledWellbores.tsx => DrilledWellboresSetting.tsx} (81%) rename frontend/src/modules/2DViewer/layers/implementations/settings/{Ensemble.tsx => EnsembleSetting.tsx} (94%) rename frontend/src/modules/2DViewer/layers/implementations/settings/{GridAttribute.tsx => GridAttributeSetting.tsx} (92%) rename frontend/src/modules/2DViewer/layers/implementations/settings/{GridLayer.tsx => GridLayerSetting.tsx} (95%) rename frontend/src/modules/2DViewer/layers/implementations/settings/{GridName.tsx => GridNameSetting.tsx} (93%) rename frontend/src/modules/2DViewer/layers/implementations/settings/{PolygonsAttribute.tsx => PolygonsAttributeSetting.tsx} (92%) rename frontend/src/modules/2DViewer/layers/implementations/settings/{PolygonsName.tsx => PolygonsNameSetting.tsx} (92%) rename frontend/src/modules/2DViewer/layers/implementations/settings/{Realization.tsx => RealizationSetting.tsx} (93%) rename frontend/src/modules/2DViewer/layers/implementations/settings/{Sensitivity.tsx => SensitivitySetting.tsx} (96%) rename frontend/src/modules/2DViewer/layers/implementations/settings/{ShowGridLines.tsx => ShowGridLinesSetting.tsx} (90%) rename frontend/src/modules/2DViewer/layers/implementations/settings/{StatisticFunction.tsx => StatisticFunctionSetting.tsx} (93%) rename frontend/src/modules/2DViewer/layers/implementations/settings/{SurfaceAttribute.tsx => SurfaceAttributeSetting.tsx} (92%) rename frontend/src/modules/2DViewer/layers/implementations/settings/{SurfaceName.tsx => SurfaceNameSetting.tsx} (93%) rename frontend/src/modules/2DViewer/layers/implementations/settings/{TimeOrInterval.tsx => TimeOrIntervalSetting.tsx} (95%) diff --git a/frontend/src/modules/2DViewer/layers/ColorScale.ts b/frontend/src/modules/2DViewer/layers/ColorScale.ts index 6463ac651..56ee21247 100644 --- a/frontend/src/modules/2DViewer/layers/ColorScale.ts +++ b/frontend/src/modules/2DViewer/layers/ColorScale.ts @@ -4,7 +4,7 @@ import { ColorScale as ColorScaleImpl } from "@lib/utils/ColorScale"; import { LayerManager, LayerManagerTopic } from "./LayerManager"; import { ItemDelegate } from "./delegates/ItemDelegate"; -import { Item, SerializedColorScale } from "./interfaces"; +import { Item, SerializedColorScale, SerializedType } from "./interfaces"; export class ColorScale implements Item { private _itemDelegate: ItemDelegate; @@ -45,7 +45,7 @@ export class ColorScale implements Item { serializeState(): SerializedColorScale { return { ...this._itemDelegate.serializeState(), - type: "color-scale", + type: SerializedType.COLOR_SCALE, colorScale: this._colorScale.serialize(), userDefinedBoundaries: this._areBoundariesUserDefined, }; diff --git a/frontend/src/modules/2DViewer/layers/DeltaSurface.ts b/frontend/src/modules/2DViewer/layers/DeltaSurface.ts index e7a9d5f8e..00caa36d3 100644 --- a/frontend/src/modules/2DViewer/layers/DeltaSurface.ts +++ b/frontend/src/modules/2DViewer/layers/DeltaSurface.ts @@ -4,7 +4,7 @@ import { ItemDelegate } from "./delegates/ItemDelegate"; import { LayerDelegate } from "./delegates/LayerDelegate"; import { SettingsContextDelegateTopic } from "./delegates/SettingsContextDelegate"; import { UnsubscribeHandlerDelegate } from "./delegates/UnsubscribeHandlerDelegate"; -import { Group, SerializedDeltaSurface, instanceofLayer } from "./interfaces"; +import { Group, SerializedDeltaSurface, SerializedType, instanceofLayer } from "./interfaces"; export class DeltaSurface implements Group { private _itemDelegate: ItemDelegate; @@ -59,7 +59,6 @@ export class DeltaSurface implements Group { private handleSettingsChange(): void { console.debug("Settings changed - would refetch data"); - // Fetch data } getItemDelegate(): ItemDelegate { @@ -78,7 +77,7 @@ export class DeltaSurface implements Group { serializeState(): SerializedDeltaSurface { return { ...this._itemDelegate.serializeState(), - type: "delta-surface", + type: SerializedType.DELTA_SURFACE, children: this.getGroupDelegate().serializeChildren(), }; } diff --git a/frontend/src/modules/2DViewer/layers/Dependency.ts b/frontend/src/modules/2DViewer/layers/Dependency.ts index af2904299..7e2228ba2 100644 --- a/frontend/src/modules/2DViewer/layers/Dependency.ts +++ b/frontend/src/modules/2DViewer/layers/Dependency.ts @@ -6,6 +6,17 @@ import { GlobalSettings } from "./LayerManager"; import { SettingsContextDelegate } from "./delegates/SettingsContextDelegate"; import { Settings, UpdateFunc } from "./interfaces"; +/* + * 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(); @@ -169,7 +180,7 @@ export class Dependency | null) { + private applyNewValue(newValue: Awaited | null) { this.setLoadingState(false); if (!isEqual(newValue, this._cachedValue) || newValue === null) { this._cachedValue = newValue; diff --git a/frontend/src/modules/2DViewer/layers/DeserializationFactory.ts b/frontend/src/modules/2DViewer/layers/DeserializationFactory.ts index 4f14b1477..bdddd8191 100644 --- a/frontend/src/modules/2DViewer/layers/DeserializationFactory.ts +++ b/frontend/src/modules/2DViewer/layers/DeserializationFactory.ts @@ -12,6 +12,7 @@ import { SerializedLayer, SerializedSettingsGroup, SerializedSharedSetting, + SerializedType, SerializedView, } from "./interfaces"; @@ -23,7 +24,13 @@ export class DeserializationFactory { } makeItem(serialized: SerializedItem): Item { - if (serialized.type === "layer") { + 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); @@ -32,32 +39,28 @@ export class DeserializationFactory { return layer; } - if (serialized.type === "view") { + 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 === "settings-group") { + 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 === "color-scale") { + 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 === "delta-surface") { - throw new Error("DeltaSurface deserialization not implemented"); - } - - if (serialized.type === "shared-setting") { + if (serialized.type === SerializedType.SHARED_SETTING) { const serializedSharedSetting = serialized as SerializedSharedSetting; const wrappedSetting = SettingRegistry.makeSetting(serializedSharedSetting.wrappedSettingClass); const setting = new SharedSetting(wrappedSetting, this._layerManager); @@ -65,6 +68,6 @@ export class DeserializationFactory { return setting; } - throw new Error(`Unknown serialized item type: ${serialized.type}`); + throw new Error(`Unhandled serialized item type: ${serialized.type}`); } } diff --git a/frontend/src/modules/2DViewer/layers/LayerManager.ts b/frontend/src/modules/2DViewer/layers/LayerManager.ts index dc9c7cfab..b96b529b3 100644 --- a/frontend/src/modules/2DViewer/layers/LayerManager.ts +++ b/frontend/src/modules/2DViewer/layers/LayerManager.ts @@ -14,7 +14,7 @@ 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 } from "./interfaces"; +import { Group, Item, SerializedLayerManager, SerializedType } from "./interfaces"; export enum LayerManagerTopic { ITEMS_CHANGED = "ITEMS_CHANGED", @@ -40,6 +40,16 @@ export type GlobalSettings = { 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; @@ -168,7 +178,7 @@ export class LayerManager implements Group, PublishSubscribe; @@ -100,7 +100,7 @@ export class SharedSetting implements Item { serializeState(): SerializedSharedSetting { return { ...this._itemDelegate.serializeState(), - type: "shared-setting", + type: SerializedType.SHARED_SETTING, wrappedSettingClass: this._wrappedSetting.constructor.name, settingType: this._wrappedSetting.getType(), value: this._wrappedSetting.getDelegate().serializeValue(), diff --git a/frontend/src/modules/2DViewer/layers/View.ts b/frontend/src/modules/2DViewer/layers/View.ts index f39c308fa..74f7f5119 100644 --- a/frontend/src/modules/2DViewer/layers/View.ts +++ b/frontend/src/modules/2DViewer/layers/View.ts @@ -1,7 +1,7 @@ import { LayerManager } from "./LayerManager"; import { GroupDelegate } from "./delegates/GroupDelegate"; import { ItemDelegate } from "./delegates/ItemDelegate"; -import { Group, SerializedView } from "./interfaces"; +import { Group, SerializedType, SerializedView } from "./interfaces"; export class View implements Group { private _itemDelegate: ItemDelegate; @@ -24,7 +24,7 @@ export class View implements Group { serializeState(): SerializedView { return { ...this._itemDelegate.serializeState(), - type: "view", + type: SerializedType.VIEW, color: this._groupDelegate.getColor() ?? "", children: this._groupDelegate.serializeChildren(), }; diff --git a/frontend/src/modules/2DViewer/layers/components/ColorScaleComponent.tsx b/frontend/src/modules/2DViewer/layers/components/ColorScaleComponent.tsx index 91836a135..b1bb786df 100644 --- a/frontend/src/modules/2DViewer/layers/components/ColorScaleComponent.tsx +++ b/frontend/src/modules/2DViewer/layers/components/ColorScaleComponent.tsx @@ -9,7 +9,7 @@ import { resolveClassNames } from "@lib/utils/resolveClassNames"; import { ColorScaleSelector } from "@modules/_shared/components/ColorScaleSelector/colorScaleSelector"; import { ExpandLess, ExpandMore } from "@mui/icons-material"; -import { RemoveButton } from "./RemoveButton"; +import { RemoveItemButton } from "./RemoveItemButton"; import { ColorScale } from "../ColorScale"; import { ItemDelegateTopic } from "../delegates/ItemDelegate"; @@ -63,7 +63,7 @@ export function ColorScaleComponent(props: ColorScaleComponentProps): React.Reac
} - endAdornment={} + endAdornment={} >
{makeColorScaleSelector()} diff --git a/frontend/src/modules/2DViewer/layers/components/DeltaSurfaceComponent.tsx b/frontend/src/modules/2DViewer/layers/components/DeltaSurfaceComponent.tsx index 0dfa0eb61..58139e921 100644 --- a/frontend/src/modules/2DViewer/layers/components/DeltaSurfaceComponent.tsx +++ b/frontend/src/modules/2DViewer/layers/components/DeltaSurfaceComponent.tsx @@ -4,9 +4,9 @@ import { EditName } from "./EditName"; import { EmptyContent } from "./EmptyContent"; import { ExpandCollapseAllButton } from "./ExpandCollapseAllButton"; import { LayersActionGroup, LayersActions } from "./LayersActions"; -import { RemoveButton } from "./RemoveButton"; +import { RemoveItemButton } from "./RemoveItemButton"; import { VisibilityToggle } from "./VisibilityToggle"; -import { makeComponent } from "./utils"; +import { makeSortableListItemComponent } from "./utils"; import { DeltaSurface } from "../DeltaSurface"; import { GroupDelegateTopic } from "../delegates/GroupDelegate"; @@ -46,7 +46,7 @@ export function DeltaSurfaceComponent(props: DeltaSurfaceComponentProps): React. ); } adornment.push(); - adornment.push(); + adornment.push(); return adornment; } @@ -72,7 +72,7 @@ export function DeltaSurfaceComponent(props: DeltaSurfaceComponentProps): React. } expanded={isExpanded} > - {children.map((child: Item) => makeComponent(child, props.actions, props.onActionClick))} + {children.map((child: Item) => makeSortableListItemComponent(child, props.actions, props.onActionClick))} ); } diff --git a/frontend/src/modules/2DViewer/layers/components/LayerComponent.tsx b/frontend/src/modules/2DViewer/layers/components/LayerComponent.tsx index 05e3d3fdc..8345551ac 100644 --- a/frontend/src/modules/2DViewer/layers/components/LayerComponent.tsx +++ b/frontend/src/modules/2DViewer/layers/components/LayerComponent.tsx @@ -8,7 +8,7 @@ import { resolveClassNames } from "@lib/utils/resolveClassNames"; import { Block, CheckCircle, Difference, Error, ExpandLess, ExpandMore } from "@mui/icons-material"; import { EditName } from "./EditName"; -import { RemoveButton } from "./RemoveButton"; +import { RemoveItemButton } from "./RemoveItemButton"; import { SettingComponent } from "./SettingComponent"; import { VisibilityToggle } from "./VisibilityToggle"; @@ -161,7 +161,7 @@ function EndActions(props: EndActionProps): React.ReactNode { return ( <> {makeStatus()} - + ); } diff --git a/frontend/src/modules/2DViewer/layers/components/RemoveButton.tsx b/frontend/src/modules/2DViewer/layers/components/RemoveItemButton.tsx similarity index 87% rename from frontend/src/modules/2DViewer/layers/components/RemoveButton.tsx rename to frontend/src/modules/2DViewer/layers/components/RemoveItemButton.tsx index 10a5a0caf..805d740b9 100644 --- a/frontend/src/modules/2DViewer/layers/components/RemoveButton.tsx +++ b/frontend/src/modules/2DViewer/layers/components/RemoveItemButton.tsx @@ -4,11 +4,11 @@ import { Delete } from "@mui/icons-material"; import { Item, instanceofLayer } from "../interfaces"; -export type RemoveButtonProps = { +export type RemoveItemButtonProps = { item: Item; }; -export function RemoveButton(props: RemoveButtonProps): React.ReactNode { +export function RemoveItemButton(props: RemoveItemButtonProps): React.ReactNode { function handleRemove() { const parentGroup = props.item.getItemDelegate().getParentGroup(); if (parentGroup) { diff --git a/frontend/src/modules/2DViewer/layers/components/SettingsGroupComponent.tsx b/frontend/src/modules/2DViewer/layers/components/SettingsGroupComponent.tsx index 313ed2305..8d8c6060d 100644 --- a/frontend/src/modules/2DViewer/layers/components/SettingsGroupComponent.tsx +++ b/frontend/src/modules/2DViewer/layers/components/SettingsGroupComponent.tsx @@ -4,8 +4,8 @@ import { SettingsApplications } from "@mui/icons-material"; import { EmptyContent } from "./EmptyContent"; import { ExpandCollapseAllButton } from "./ExpandCollapseAllButton"; import { LayersActionGroup, LayersActions } from "./LayersActions"; -import { RemoveButton } from "./RemoveButton"; -import { makeComponent } from "./utils"; +import { RemoveItemButton } from "./RemoveItemButton"; +import { makeSortableListItemComponent } from "./utils"; import { GroupDelegateTopic } from "../delegates/GroupDelegate"; import { ItemDelegateTopic } from "../delegates/ItemDelegate"; @@ -41,7 +41,7 @@ export function SettingsGroupComponent(props: SettingsGroupComponentProps): Reac ); } adornment.push(); - adornment.push(); + adornment.push(); return adornment; } @@ -67,7 +67,7 @@ export function SettingsGroupComponent(props: SettingsGroupComponentProps): Reac } expanded={isExpanded} > - {children.map((child: Item) => makeComponent(child, props.actions, props.onActionClick))} + {children.map((child: Item) => makeSortableListItemComponent(child, props.actions, props.onActionClick))} ); } diff --git a/frontend/src/modules/2DViewer/layers/components/ViewComponent.tsx b/frontend/src/modules/2DViewer/layers/components/ViewComponent.tsx index d91ae8ec8..682cf14de 100644 --- a/frontend/src/modules/2DViewer/layers/components/ViewComponent.tsx +++ b/frontend/src/modules/2DViewer/layers/components/ViewComponent.tsx @@ -4,9 +4,9 @@ import { EditName } from "./EditName"; import { EmptyContent } from "./EmptyContent"; import { ExpandCollapseAllButton } from "./ExpandCollapseAllButton"; import { LayersActionGroup, LayersActions } from "./LayersActions"; -import { RemoveButton } from "./RemoveButton"; +import { RemoveItemButton } from "./RemoveItemButton"; import { VisibilityToggle } from "./VisibilityToggle"; -import { makeComponent } from "./utils"; +import { makeSortableListItemComponent } from "./utils"; import { GroupDelegateTopic } from "../delegates/GroupDelegate"; import { ItemDelegateTopic } from "../delegates/ItemDelegate"; @@ -42,7 +42,7 @@ export function ViewComponent(props: ViewComponentProps): React.ReactNode { ); } adornments.push(); - adornments.push(); + adornments.push(); return adornments; } @@ -71,7 +71,7 @@ export function ViewComponent(props: ViewComponentProps): React.ReactNode { endAdornment={<>{makeEndAdornment()}} contentWhenEmpty={Drag a layer inside to add it to this view.} > - {children.map((child: Item) => makeComponent(child, props.actions, props.onActionClick))} + {children.map((child: Item) => makeSortableListItemComponent(child, props.actions, props.onActionClick))} ); } diff --git a/frontend/src/modules/2DViewer/layers/components/utils.tsx b/frontend/src/modules/2DViewer/layers/components/utils.tsx index 0f1f3f31b..f6991d95e 100644 --- a/frontend/src/modules/2DViewer/layers/components/utils.tsx +++ b/frontend/src/modules/2DViewer/layers/components/utils.tsx @@ -1,5 +1,3 @@ -import { SortableListItemProps } from "@lib/components/SortableList"; - import { ColorScaleComponent } from "./ColorScaleComponent"; import { DeltaSurfaceComponent } from "./DeltaSurfaceComponent"; import { LayerComponent } from "./LayerComponent"; @@ -13,45 +11,45 @@ import { DeltaSurface } from "../DeltaSurface"; import { SettingsGroup } from "../SettingsGroup"; import { SharedSetting } from "../SharedSetting"; import { View } from "../View"; -import { Group, Item, instanceofGroup, instanceofLayer } from "../interfaces"; +import { Group, Item, instanceofLayer } from "../interfaces"; -export function makeComponent( +export function makeSortableListItemComponent( item: Item, layerActions?: LayersActionGroup[], onActionClick?: (identifier: string, group: Group) => void -): React.ReactElement { +): React.ReactElement { if (instanceofLayer(item)) { return ; } - if (instanceofGroup(item)) { - if (item instanceof SettingsGroup) { - return ( - - ); - } else if (item instanceof View) { - return ( - - ); - } else if (item instanceof DeltaSurface) { - return ( - - ); - } + if (item instanceof SettingsGroup) { + return ( + + ); + } + if (item instanceof View) { + return ( + + ); + } + if (item instanceof DeltaSurface) { + return ( + + ); } if (item instanceof SharedSetting) { return ; @@ -59,7 +57,8 @@ export function makeComponent( if (item instanceof ColorScale) { return ; } - throw new Error("Not implemented"); + + throw new Error(`Unsupported item type: ${item.constructor.name}`); } function filterAwayViewActions(actions: LayersActionGroup[]): LayersActionGroup[] { diff --git a/frontend/src/modules/2DViewer/layers/delegates/GroupDelegate.ts b/frontend/src/modules/2DViewer/layers/delegates/GroupDelegate.ts index 613be3e8b..3ef7673db 100644 --- a/frontend/src/modules/2DViewer/layers/delegates/GroupDelegate.ts +++ b/frontend/src/modules/2DViewer/layers/delegates/GroupDelegate.ts @@ -19,6 +19,11 @@ export type GroupDelegateTopicPayloads = { [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; diff --git a/frontend/src/modules/2DViewer/layers/delegates/ItemDelegate.ts b/frontend/src/modules/2DViewer/layers/delegates/ItemDelegate.ts index 0967d611e..f494a5b5c 100644 --- a/frontend/src/modules/2DViewer/layers/delegates/ItemDelegate.ts +++ b/frontend/src/modules/2DViewer/layers/delegates/ItemDelegate.ts @@ -19,6 +19,10 @@ export type ItemDelegatePayloads = { [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; diff --git a/frontend/src/modules/2DViewer/layers/delegates/LayerDelegate.ts b/frontend/src/modules/2DViewer/layers/delegates/LayerDelegate.ts index 3d9dde990..096652901 100644 --- a/frontend/src/modules/2DViewer/layers/delegates/LayerDelegate.ts +++ b/frontend/src/modules/2DViewer/layers/delegates/LayerDelegate.ts @@ -9,7 +9,7 @@ import { UnsubscribeHandlerDelegate } from "./UnsubscribeHandlerDelegate"; import { LayerManager, LayerManagerTopic } from "../LayerManager"; import { SharedSetting } from "../SharedSetting"; -import { BoundingBox, Layer, SerializedLayer, Settings, SettingsContext } from "../interfaces"; +import { BoundingBox, Layer, SerializedLayer, SerializedType, Settings, SettingsContext } from "../interfaces"; export enum LayerDelegateTopic { STATUS = "STATUS", @@ -35,6 +35,12 @@ export type LayerDelegatePayloads = { [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> { @@ -226,7 +232,7 @@ export class LayerDelegate this.invalidateValueRange(); try { - this._data = await this._owner.fechData(queryClient); + this._data = await this._owner.fetchData(queryClient); if (this._owner.makeBoundingBox) { this._boundingBox = this._owner.makeBoundingBox(); } @@ -260,7 +266,7 @@ export class LayerDelegate const itemState = this._owner.getItemDelegate().serializeState(); return { ...itemState, - type: "layer", + type: SerializedType.LAYER, layerClass: this._owner.constructor.name, settings: this._settingsContext.getDelegate().serializeSettings(), }; diff --git a/frontend/src/modules/2DViewer/layers/delegates/SettingDelegate.ts b/frontend/src/modules/2DViewer/layers/delegates/SettingDelegate.ts index e20285b14..a6048dffc 100644 --- a/frontend/src/modules/2DViewer/layers/delegates/SettingDelegate.ts +++ b/frontend/src/modules/2DViewer/layers/delegates/SettingDelegate.ts @@ -28,6 +28,12 @@ export type SettingTopicPayloads = { [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; diff --git a/frontend/src/modules/2DViewer/layers/delegates/SettingsContextDelegate.ts b/frontend/src/modules/2DViewer/layers/delegates/SettingsContextDelegate.ts index 5a0f21b15..c1cf38c51 100644 --- a/frontend/src/modules/2DViewer/layers/delegates/SettingsContextDelegate.ts +++ b/frontend/src/modules/2DViewer/layers/delegates/SettingsContextDelegate.ts @@ -40,6 +40,15 @@ export type SettingsContextDelegateState implements PublishSubscribe { diff --git a/frontend/src/modules/2DViewer/layers/delegates/UnsubscribeHandlerDelegate.ts b/frontend/src/modules/2DViewer/layers/delegates/UnsubscribeHandlerDelegate.ts index 067c8d830..c7d90d140 100644 --- a/frontend/src/modules/2DViewer/layers/delegates/UnsubscribeHandlerDelegate.ts +++ b/frontend/src/modules/2DViewer/layers/delegates/UnsubscribeHandlerDelegate.ts @@ -1,3 +1,10 @@ +/* + * 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(); diff --git a/frontend/src/modules/2DViewer/layers/implementations/layers/DrilledWellTrajectoriesLayer/DrilledWellTrajectoriesLayer.ts b/frontend/src/modules/2DViewer/layers/implementations/layers/DrilledWellTrajectoriesLayer/DrilledWellTrajectoriesLayer.ts index 961b1f4e1..d89976ce1 100644 --- a/frontend/src/modules/2DViewer/layers/implementations/layers/DrilledWellTrajectoriesLayer/DrilledWellTrajectoriesLayer.ts +++ b/frontend/src/modules/2DViewer/layers/implementations/layers/DrilledWellTrajectoriesLayer/DrilledWellTrajectoriesLayer.ts @@ -8,7 +8,7 @@ import { QueryClient } from "@tanstack/react-query"; import { isEqual } from "lodash"; -import { DrilledWellTrajectoriesContext } from "./DrilledWellTrajectoriesContext"; +import { DrilledWellTrajectoriesSettingsContext } from "./DrilledWellTrajectoriesSettingsContext"; import { DrilledWellTrajectoriesSettings } from "./types"; import { LayerColoringType, LayerDelegate } from "../../../delegates/LayerDelegate"; @@ -23,7 +23,7 @@ export class DrilledWellTrajectoriesLayer implements Layer { + fetchData(queryClient: QueryClient): Promise { const workbenchSession = this.getSettingsContext().getDelegate().getLayerManager().getWorkbenchSession(); const ensembleSet = workbenchSession.getEnsembleSet(); const settings = this.getSettingsContext().getDelegate().getSettings(); diff --git a/frontend/src/modules/2DViewer/layers/implementations/layers/DrilledWellTrajectoriesLayer/DrilledWellTrajectoriesContext.ts b/frontend/src/modules/2DViewer/layers/implementations/layers/DrilledWellTrajectoriesLayer/DrilledWellTrajectoriesSettingsContext.ts similarity index 90% rename from frontend/src/modules/2DViewer/layers/implementations/layers/DrilledWellTrajectoriesLayer/DrilledWellTrajectoriesContext.ts rename to frontend/src/modules/2DViewer/layers/implementations/layers/DrilledWellTrajectoriesLayer/DrilledWellTrajectoriesSettingsContext.ts index 4b9e466de..9889366dc 100644 --- a/frontend/src/modules/2DViewer/layers/implementations/layers/DrilledWellTrajectoriesLayer/DrilledWellTrajectoriesContext.ts +++ b/frontend/src/modules/2DViewer/layers/implementations/layers/DrilledWellTrajectoriesLayer/DrilledWellTrajectoriesSettingsContext.ts @@ -8,10 +8,10 @@ import { cancelPromiseOnAbort } from "@modules/2DViewer/layers/utils"; import { DrilledWellTrajectoriesSettings } from "./types"; import { DefineDependenciesArgs, SettingsContext } from "../../../interfaces"; -import { DrilledWellbores } from "../../settings/DrilledWellbores"; -import { Ensemble } from "../../settings/Ensemble"; +import { DrilledWellboresSetting } from "../../settings/DrilledWellboresSetting"; +import { EnsembleSetting } from "../../settings/EnsembleSetting"; -export class DrilledWellTrajectoriesContext implements SettingsContext { +export class DrilledWellTrajectoriesSettingsContext implements SettingsContext { private _contextDelegate: SettingsContextDelegate; constructor(layerManager: LayerManager) { @@ -19,8 +19,8 @@ export class DrilledWellTrajectoriesContext implements SettingsContext(this, layerManager, { - [SettingType.ENSEMBLE]: new Ensemble(), - [SettingType.SMDA_WELLBORE_HEADERS]: new DrilledWellbores(), + [SettingType.ENSEMBLE]: new EnsembleSetting(), + [SettingType.SMDA_WELLBORE_HEADERS]: new DrilledWellboresSetting(), }); } diff --git a/frontend/src/modules/2DViewer/layers/implementations/layers/DrilledWellborePicksLayer/DrilledWellborePicksLayer.ts b/frontend/src/modules/2DViewer/layers/implementations/layers/DrilledWellborePicksLayer/DrilledWellborePicksLayer.ts index 27fb63e4a..8cce2eb4e 100644 --- a/frontend/src/modules/2DViewer/layers/implementations/layers/DrilledWellborePicksLayer/DrilledWellborePicksLayer.ts +++ b/frontend/src/modules/2DViewer/layers/implementations/layers/DrilledWellborePicksLayer/DrilledWellborePicksLayer.ts @@ -9,7 +9,7 @@ import { QueryClient } from "@tanstack/react-query"; import { isEqual } from "lodash"; -import { DrilledWellborePicksContext } from "./DrilledWellborePicksContext"; +import { DrilledWellborePicksSettingsContext } from "./DrilledWellborePicksSettingsContext"; import { DrilledWellborePicksSettings } from "./types"; import { LayerColoringType, LayerDelegate } from "../../../delegates/LayerDelegate"; @@ -24,7 +24,7 @@ export class DrilledWellborePicksLayer implements Layer { + fetchData(queryClient: QueryClient): Promise { const workbenchSession = this.getSettingsContext().getDelegate().getLayerManager().getWorkbenchSession(); const ensembleSet = workbenchSession.getEnsembleSet(); const settings = this.getSettingsContext().getDelegate().getSettings(); diff --git a/frontend/src/modules/2DViewer/layers/implementations/layers/DrilledWellborePicksLayer/DrilledWellborePicksContext.ts b/frontend/src/modules/2DViewer/layers/implementations/layers/DrilledWellborePicksLayer/DrilledWellborePicksSettingsContext.ts similarity index 88% rename from frontend/src/modules/2DViewer/layers/implementations/layers/DrilledWellborePicksLayer/DrilledWellborePicksContext.ts rename to frontend/src/modules/2DViewer/layers/implementations/layers/DrilledWellborePicksLayer/DrilledWellborePicksSettingsContext.ts index e2e7fb5ff..df55f46b5 100644 --- a/frontend/src/modules/2DViewer/layers/implementations/layers/DrilledWellborePicksLayer/DrilledWellborePicksContext.ts +++ b/frontend/src/modules/2DViewer/layers/implementations/layers/DrilledWellborePicksLayer/DrilledWellborePicksSettingsContext.ts @@ -8,11 +8,11 @@ import { cancelPromiseOnAbort } from "@modules/2DViewer/layers/utils"; import { DrilledWellborePicksSettings } from "./types"; import { DefineDependenciesArgs, SettingsContext } from "../../../interfaces"; -import { DrilledWellbores } from "../../settings/DrilledWellbores"; -import { Ensemble } from "../../settings/Ensemble"; -import { SurfaceName } from "../../settings/SurfaceName"; +import { DrilledWellboresSetting } from "../../settings/DrilledWellboresSetting"; +import { EnsembleSetting } from "../../settings/EnsembleSetting"; +import { SurfaceNameSetting } from "../../settings/SurfaceNameSetting"; -export class DrilledWellborePicksContext implements SettingsContext { +export class DrilledWellborePicksSettingsContext implements SettingsContext { private _contextDelegate: SettingsContextDelegate; constructor(layerManager: LayerManager) { @@ -20,9 +20,9 @@ export class DrilledWellborePicksContext implements SettingsContext(this, layerManager, { - [SettingType.ENSEMBLE]: new Ensemble(), - [SettingType.SMDA_WELLBORE_HEADERS]: new DrilledWellbores(), - [SettingType.SURFACE_NAME]: new SurfaceName(), + [SettingType.ENSEMBLE]: new EnsembleSetting(), + [SettingType.SMDA_WELLBORE_HEADERS]: new DrilledWellboresSetting(), + [SettingType.SURFACE_NAME]: new SurfaceNameSetting(), }); } @@ -99,14 +99,13 @@ export class DrilledWellborePicksContext implements SettingsContext cancelPromiseOnAbort( - apiService.well.getWellborePickIdentifiers(fieldIdentifier, stratColumnIdentifier), + apiService.well.getWellborePickIdentifiers(stratColumnIdentifier), abortSignal ), staleTime: STALE_TIME, diff --git a/frontend/src/modules/2DViewer/layers/implementations/layers/ObservedSurfaceLayer/ObservedSurfaceLayer.ts b/frontend/src/modules/2DViewer/layers/implementations/layers/ObservedSurfaceLayer/ObservedSurfaceLayer.ts index 67ef98914..2cce9a964 100644 --- a/frontend/src/modules/2DViewer/layers/implementations/layers/ObservedSurfaceLayer/ObservedSurfaceLayer.ts +++ b/frontend/src/modules/2DViewer/layers/implementations/layers/ObservedSurfaceLayer/ObservedSurfaceLayer.ts @@ -12,7 +12,7 @@ import { QueryClient } from "@tanstack/react-query"; import { isEqual } from "lodash"; -import { ObservedSurfaceContext } from "./ObservedSurfaceContext"; +import { ObservedSurfaceSettingsContext } from "./ObservedSurfaceSettingsContext"; import { ObservedSurfaceSettings } from "./types"; import { LayerColoringType, LayerDelegate } from "../../../delegates/LayerDelegate"; @@ -29,7 +29,7 @@ export class ObservedSurfaceLayer this._layerDelegate = new LayerDelegate( this, layerManager, - new ObservedSurfaceContext(layerManager), + new ObservedSurfaceSettingsContext(layerManager), LayerColoringType.COLORSCALE ); } @@ -75,7 +75,7 @@ export class ObservedSurfaceLayer return [data.value_min, data.value_max]; } - fechData(queryClient: QueryClient): Promise { + fetchData(queryClient: QueryClient): Promise { let surfaceAddress: FullSurfaceAddress | null = null; const addrBuilder = new SurfaceAddressBuilder(); diff --git a/frontend/src/modules/2DViewer/layers/implementations/layers/ObservedSurfaceLayer/ObservedSurfaceContext.ts b/frontend/src/modules/2DViewer/layers/implementations/layers/ObservedSurfaceLayer/ObservedSurfaceSettingsContext.ts similarity index 89% rename from frontend/src/modules/2DViewer/layers/implementations/layers/ObservedSurfaceLayer/ObservedSurfaceContext.ts rename to frontend/src/modules/2DViewer/layers/implementations/layers/ObservedSurfaceLayer/ObservedSurfaceSettingsContext.ts index 8888954eb..08faa47b3 100644 --- a/frontend/src/modules/2DViewer/layers/implementations/layers/ObservedSurfaceLayer/ObservedSurfaceContext.ts +++ b/frontend/src/modules/2DViewer/layers/implementations/layers/ObservedSurfaceLayer/ObservedSurfaceSettingsContext.ts @@ -1,4 +1,4 @@ -import { SurfaceMetaSet_api, SurfaceTimeType_api } from "@api"; +import { SurfaceTimeType_api } from "@api"; import { apiService } from "@framework/ApiService"; import { LayerManager } from "@modules/2DViewer/layers/LayerManager"; import { SettingsContextDelegate } from "@modules/2DViewer/layers/delegates/SettingsContextDelegate"; @@ -9,24 +9,23 @@ import { cancelPromiseOnAbort } from "@modules/2DViewer/layers/utils"; import { ObservedSurfaceSettings } from "./types"; import { DefineDependenciesArgs, SettingsContext } from "../../../interfaces"; -import { Ensemble } from "../../settings/Ensemble"; -import { SurfaceAttribute } from "../../settings/SurfaceAttribute"; -import { SurfaceName } from "../../settings/SurfaceName"; -import { TimeOrInterval } from "../../settings/TimeOrInterval"; +import { EnsembleSetting } from "../../settings/EnsembleSetting"; +import { SurfaceAttributeSetting } from "../../settings/SurfaceAttributeSetting"; +import { SurfaceNameSetting } from "../../settings/SurfaceNameSetting"; +import { TimeOrIntervalSetting } from "../../settings/TimeOrIntervalSetting"; -export class ObservedSurfaceContext implements SettingsContext { +export class ObservedSurfaceSettingsContext implements SettingsContext { private _contextDelegate: SettingsContextDelegate; - private _fetchDataCache: SurfaceMetaSet_api | null = null; constructor(layerManager: LayerManager) { this._contextDelegate = new SettingsContextDelegate( this, layerManager, { - [SettingType.ENSEMBLE]: new Ensemble(), - [SettingType.SURFACE_ATTRIBUTE]: new SurfaceAttribute(), - [SettingType.SURFACE_NAME]: new SurfaceName(), - [SettingType.TIME_OR_INTERVAL]: new TimeOrInterval(), + [SettingType.ENSEMBLE]: new EnsembleSetting(), + [SettingType.SURFACE_ATTRIBUTE]: new SurfaceAttributeSetting(), + [SettingType.SURFACE_NAME]: new SurfaceNameSetting(), + [SettingType.TIME_OR_INTERVAL]: new TimeOrIntervalSetting(), } ); } diff --git a/frontend/src/modules/2DViewer/layers/implementations/layers/RealizationGridLayer/RealizationGridLayer.ts b/frontend/src/modules/2DViewer/layers/implementations/layers/RealizationGridLayer/RealizationGridLayer.ts index 4ff662e3e..5b9f64894 100644 --- a/frontend/src/modules/2DViewer/layers/implementations/layers/RealizationGridLayer/RealizationGridLayer.ts +++ b/frontend/src/modules/2DViewer/layers/implementations/layers/RealizationGridLayer/RealizationGridLayer.ts @@ -14,7 +14,7 @@ import { QueryClient } from "@tanstack/react-query"; import { isEqual } from "lodash"; -import { RealizationGridContext } from "./RealizationGridContext"; +import { RealizationGridSettingsContext } from "./RealizationGridSettingsContext"; import { RealizationGridSettings } from "./types"; import { LayerColoringType, LayerDelegate } from "../../../delegates/LayerDelegate"; @@ -44,7 +44,7 @@ export class RealizationGridLayer this._layerDelegate = new LayerDelegate( this, layerManager, - new RealizationGridContext(layerManager), + new RealizationGridSettingsContext(layerManager), LayerColoringType.COLORSCALE ); } @@ -102,7 +102,7 @@ export class RealizationGridLayer return [data.gridParameterData.min_grid_prop_value, data.gridParameterData.max_grid_prop_value]; } - fechData(queryClient: QueryClient): Promise<{ + fetchData(queryClient: QueryClient): Promise<{ gridSurfaceData: GridSurface_trans; gridParameterData: GridMappedProperty_trans; }> { diff --git a/frontend/src/modules/2DViewer/layers/implementations/layers/RealizationGridLayer/RealizationGridContext.ts b/frontend/src/modules/2DViewer/layers/implementations/layers/RealizationGridLayer/RealizationGridSettingsContext.ts similarity index 87% rename from frontend/src/modules/2DViewer/layers/implementations/layers/RealizationGridLayer/RealizationGridContext.ts rename to frontend/src/modules/2DViewer/layers/implementations/layers/RealizationGridLayer/RealizationGridSettingsContext.ts index 6163be4d8..0fe62f597 100644 --- a/frontend/src/modules/2DViewer/layers/implementations/layers/RealizationGridLayer/RealizationGridContext.ts +++ b/frontend/src/modules/2DViewer/layers/implementations/layers/RealizationGridLayer/RealizationGridSettingsContext.ts @@ -8,15 +8,15 @@ import { cancelQueryOnAbort } from "@modules/2DViewer/layers/utils"; import { RealizationGridSettings } from "./types"; import { DefineDependenciesArgs, SettingsContext } from "../../../interfaces"; -import { Ensemble } from "../../settings/Ensemble"; -import { GridAttribute } from "../../settings/GridAttribute"; -import { GridLayer } from "../../settings/GridLayer"; -import { GridName } from "../../settings/GridName"; -import { Realization } from "../../settings/Realization"; -import { ShowGridLines } from "../../settings/ShowGridLines"; -import { TimeOrInterval } from "../../settings/TimeOrInterval"; - -export class RealizationGridContext implements SettingsContext { +import { EnsembleSetting } from "../../settings/EnsembleSetting"; +import { GridAttributeSetting } from "../../settings/GridAttributeSetting"; +import { GridLayerSetting } from "../../settings/GridLayerSetting"; +import { GridNameSetting } from "../../settings/GridNameSetting"; +import { RealizationSetting } from "../../settings/RealizationSetting"; +import { ShowGridLinesSetting } from "../../settings/ShowGridLinesSetting"; +import { TimeOrIntervalSetting } from "../../settings/TimeOrIntervalSetting"; + +export class RealizationGridSettingsContext implements SettingsContext { private _contextDelegate: SettingsContextDelegate; constructor(layerManager: LayerManager) { @@ -24,13 +24,13 @@ export class RealizationGridContext implements SettingsContext { + fetchData(queryClient: QueryClient): Promise { const settings = this.getSettingsContext().getDelegate().getSettings(); const ensembleIdent = settings[SettingType.ENSEMBLE].getDelegate().getValue(); const realizationNum = settings[SettingType.REALIZATION].getDelegate().getValue(); diff --git a/frontend/src/modules/2DViewer/layers/implementations/layers/RealizationPolygonsLayer/RealizationPolygonsContext.ts b/frontend/src/modules/2DViewer/layers/implementations/layers/RealizationPolygonsLayer/RealizationPolygonsSettingsContext.ts similarity index 87% rename from frontend/src/modules/2DViewer/layers/implementations/layers/RealizationPolygonsLayer/RealizationPolygonsContext.ts rename to frontend/src/modules/2DViewer/layers/implementations/layers/RealizationPolygonsLayer/RealizationPolygonsSettingsContext.ts index 433ba563c..6da787736 100644 --- a/frontend/src/modules/2DViewer/layers/implementations/layers/RealizationPolygonsLayer/RealizationPolygonsContext.ts +++ b/frontend/src/modules/2DViewer/layers/implementations/layers/RealizationPolygonsLayer/RealizationPolygonsSettingsContext.ts @@ -1,4 +1,3 @@ -import { PolygonsMeta_api } from "@api"; import { apiService } from "@framework/ApiService"; import { LayerManager } from "@modules/2DViewer/layers/LayerManager"; import { SettingsContextDelegate } from "@modules/2DViewer/layers/delegates/SettingsContextDelegate"; @@ -9,24 +8,23 @@ import { cancelPromiseOnAbort } from "@modules/2DViewer/layers/utils"; import { RealizationPolygonsSettings } from "./types"; import { DefineDependenciesArgs, SettingsContext } from "../../../interfaces"; -import { Ensemble } from "../../settings/Ensemble"; -import { PolygonsAttribute } from "../../settings/PolygonsAttribute"; -import { PolygonsName } from "../../settings/PolygonsName"; -import { Realization } from "../../settings/Realization"; +import { EnsembleSetting } from "../../settings/EnsembleSetting"; +import { PolygonsAttributeSetting } from "../../settings/PolygonsAttributeSetting"; +import { PolygonsNameSetting } from "../../settings/PolygonsNameSetting"; +import { RealizationSetting } from "../../settings/RealizationSetting"; -export class RealizationPolygonsContext implements SettingsContext { +export class RealizationPolygonsSettingsContext implements SettingsContext { private _contextDelegate: SettingsContextDelegate; - private _fetchDataCache: PolygonsMeta_api[] | null = null; constructor(layerManager: LayerManager) { this._contextDelegate = new SettingsContextDelegate< RealizationPolygonsSettings, keyof RealizationPolygonsSettings >(this, layerManager, { - [SettingType.ENSEMBLE]: new Ensemble(), - [SettingType.REALIZATION]: new Realization(), - [SettingType.POLYGONS_ATTRIBUTE]: new PolygonsAttribute(), - [SettingType.POLYGONS_NAME]: new PolygonsName(), + [SettingType.ENSEMBLE]: new EnsembleSetting(), + [SettingType.REALIZATION]: new RealizationSetting(), + [SettingType.POLYGONS_ATTRIBUTE]: new PolygonsAttributeSetting(), + [SettingType.POLYGONS_NAME]: new PolygonsNameSetting(), }); } diff --git a/frontend/src/modules/2DViewer/layers/implementations/layers/RealizationSurfaceLayer/RealizationSurfaceLayer.ts b/frontend/src/modules/2DViewer/layers/implementations/layers/RealizationSurfaceLayer/RealizationSurfaceLayer.ts index 3dec51ed0..0af3bf577 100644 --- a/frontend/src/modules/2DViewer/layers/implementations/layers/RealizationSurfaceLayer/RealizationSurfaceLayer.ts +++ b/frontend/src/modules/2DViewer/layers/implementations/layers/RealizationSurfaceLayer/RealizationSurfaceLayer.ts @@ -13,7 +13,7 @@ import { QueryClient } from "@tanstack/react-query"; import { isEqual } from "lodash"; -import { RealizationSurfaceContext } from "./RealizationSurfaceContext"; +import { RealizationSurfaceSettingsContext } from "./RealizationSurfaceSettingsContext"; import { RealizationSurfaceSettings } from "./types"; import { BoundingBox, Layer, SerializedLayer } from "../../../interfaces"; @@ -29,7 +29,7 @@ export class RealizationSurfaceLayer this._layerDelegate = new LayerDelegate( this, layerManager, - new RealizationSurfaceContext(layerManager), + new RealizationSurfaceSettingsContext(layerManager), LayerColoringType.COLORSCALE ); } @@ -75,7 +75,7 @@ export class RealizationSurfaceLayer return [data.value_min, data.value_max]; } - fechData(queryClient: QueryClient): Promise { + fetchData(queryClient: QueryClient): Promise { let surfaceAddress: FullSurfaceAddress | null = null; const addrBuilder = new SurfaceAddressBuilder(); diff --git a/frontend/src/modules/2DViewer/layers/implementations/layers/RealizationSurfaceLayer/RealizationSurfaceContext.ts b/frontend/src/modules/2DViewer/layers/implementations/layers/RealizationSurfaceLayer/RealizationSurfaceSettingsContext.ts similarity index 88% rename from frontend/src/modules/2DViewer/layers/implementations/layers/RealizationSurfaceLayer/RealizationSurfaceContext.ts rename to frontend/src/modules/2DViewer/layers/implementations/layers/RealizationSurfaceLayer/RealizationSurfaceSettingsContext.ts index 19cbdc478..c59a67635 100644 --- a/frontend/src/modules/2DViewer/layers/implementations/layers/RealizationSurfaceLayer/RealizationSurfaceContext.ts +++ b/frontend/src/modules/2DViewer/layers/implementations/layers/RealizationSurfaceLayer/RealizationSurfaceSettingsContext.ts @@ -9,13 +9,13 @@ import { cancelPromiseOnAbort } from "@modules/2DViewer/layers/utils"; import { RealizationSurfaceSettings } from "./types"; import { DefineDependenciesArgs, SettingsContext } from "../../../interfaces"; -import { Ensemble } from "../../settings/Ensemble"; -import { Realization } from "../../settings/Realization"; -import { SurfaceAttribute } from "../../settings/SurfaceAttribute"; -import { SurfaceName } from "../../settings/SurfaceName"; -import { TimeOrInterval } from "../../settings/TimeOrInterval"; +import { EnsembleSetting } from "../../settings/EnsembleSetting"; +import { RealizationSetting } from "../../settings/RealizationSetting"; +import { SurfaceAttributeSetting } from "../../settings/SurfaceAttributeSetting"; +import { SurfaceNameSetting } from "../../settings/SurfaceNameSetting"; +import { TimeOrIntervalSetting } from "../../settings/TimeOrIntervalSetting"; -export class RealizationSurfaceContext implements SettingsContext { +export class RealizationSurfaceSettingsContext implements SettingsContext { private _contextDelegate: SettingsContextDelegate; constructor(layerManager: LayerManager) { @@ -23,11 +23,11 @@ export class RealizationSurfaceContext implements SettingsContext(this, layerManager, { - [SettingType.ENSEMBLE]: new Ensemble(), - [SettingType.REALIZATION]: new Realization(), - [SettingType.SURFACE_ATTRIBUTE]: new SurfaceAttribute(), - [SettingType.SURFACE_NAME]: new SurfaceName(), - [SettingType.TIME_OR_INTERVAL]: new TimeOrInterval(), + [SettingType.ENSEMBLE]: new EnsembleSetting(), + [SettingType.REALIZATION]: new RealizationSetting(), + [SettingType.SURFACE_ATTRIBUTE]: new SurfaceAttributeSetting(), + [SettingType.SURFACE_NAME]: new SurfaceNameSetting(), + [SettingType.TIME_OR_INTERVAL]: new TimeOrIntervalSetting(), }); } diff --git a/frontend/src/modules/2DViewer/layers/implementations/layers/StatisticalSurfaceLayer/StatisticalSurfaceLayer.ts b/frontend/src/modules/2DViewer/layers/implementations/layers/StatisticalSurfaceLayer/StatisticalSurfaceLayer.ts index a06c7d16a..db7910e60 100644 --- a/frontend/src/modules/2DViewer/layers/implementations/layers/StatisticalSurfaceLayer/StatisticalSurfaceLayer.ts +++ b/frontend/src/modules/2DViewer/layers/implementations/layers/StatisticalSurfaceLayer/StatisticalSurfaceLayer.ts @@ -13,7 +13,7 @@ import { QueryClient } from "@tanstack/react-query"; import { isEqual } from "lodash"; -import { StatisticalSurfaceContext } from "./StatisticalSurfaceContext"; +import { StatisticalSurfaceSettingsContext } from "./StatisticalSurfaceSettingsContext"; import { StatisticalSurfaceSettings } from "./types"; import { BoundingBox, Layer, SerializedLayer } from "../../../interfaces"; @@ -29,7 +29,7 @@ export class StatisticalSurfaceLayer this._layerDelegate = new LayerDelegate( this, layerManager, - new StatisticalSurfaceContext(layerManager), + new StatisticalSurfaceSettingsContext(layerManager), LayerColoringType.COLORSCALE ); } @@ -75,7 +75,7 @@ export class StatisticalSurfaceLayer return [data.value_min, data.value_max]; } - fechData(queryClient: QueryClient): Promise { + fetchData(queryClient: QueryClient): Promise { let surfaceAddress: FullSurfaceAddress | null = null; const addrBuilder = new SurfaceAddressBuilder(); const workbenchSession = this.getLayerDelegate().getLayerManager().getWorkbenchSession(); diff --git a/frontend/src/modules/2DViewer/layers/implementations/layers/StatisticalSurfaceLayer/StatisticalSurfaceContext.ts b/frontend/src/modules/2DViewer/layers/implementations/layers/StatisticalSurfaceLayer/StatisticalSurfaceSettingsContext.ts similarity index 88% rename from frontend/src/modules/2DViewer/layers/implementations/layers/StatisticalSurfaceLayer/StatisticalSurfaceContext.ts rename to frontend/src/modules/2DViewer/layers/implementations/layers/StatisticalSurfaceLayer/StatisticalSurfaceSettingsContext.ts index 22832de4b..f849997de 100644 --- a/frontend/src/modules/2DViewer/layers/implementations/layers/StatisticalSurfaceLayer/StatisticalSurfaceContext.ts +++ b/frontend/src/modules/2DViewer/layers/implementations/layers/StatisticalSurfaceLayer/StatisticalSurfaceSettingsContext.ts @@ -8,15 +8,15 @@ import { StatisticalSurfaceSettings } from "./types"; import { SettingsContextDelegate } from "../../../delegates/SettingsContextDelegate"; import { DefineDependenciesArgs, SettingsContext } from "../../../interfaces"; -import { Ensemble } from "../../settings/Ensemble"; -import { Sensitivity, SensitivityNameCasePair } from "../../settings/Sensitivity"; -import { StatisticFunction } from "../../settings/StatisticFunction"; -import { SurfaceAttribute } from "../../settings/SurfaceAttribute"; -import { SurfaceName } from "../../settings/SurfaceName"; -import { TimeOrInterval } from "../../settings/TimeOrInterval"; +import { EnsembleSetting } from "../../settings/EnsembleSetting"; +import { SensitivityNameCasePair, SensitivitySetting } from "../../settings/SensitivitySetting"; +import { StatisticFunctionSetting } from "../../settings/StatisticFunctionSetting"; +import { SurfaceAttributeSetting } from "../../settings/SurfaceAttributeSetting"; +import { SurfaceNameSetting } from "../../settings/SurfaceNameSetting"; +import { TimeOrIntervalSetting } from "../../settings/TimeOrIntervalSetting"; import { SettingType } from "../../settings/settingsTypes"; -export class StatisticalSurfaceContext implements SettingsContext { +export class StatisticalSurfaceSettingsContext implements SettingsContext { private _contextDelegate: SettingsContextDelegate; constructor(layerManager: LayerManager) { @@ -24,12 +24,12 @@ export class StatisticalSurfaceContext implements SettingsContext(this, layerManager, { - [SettingType.ENSEMBLE]: new Ensemble(), - [SettingType.STATISTIC_FUNCTION]: new StatisticFunction(), - [SettingType.SENSITIVITY]: new Sensitivity(), - [SettingType.SURFACE_ATTRIBUTE]: new SurfaceAttribute(), - [SettingType.SURFACE_NAME]: new SurfaceName(), - [SettingType.TIME_OR_INTERVAL]: new TimeOrInterval(), + [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(), }); } diff --git a/frontend/src/modules/2DViewer/layers/implementations/layers/StatisticalSurfaceLayer/types.ts b/frontend/src/modules/2DViewer/layers/implementations/layers/StatisticalSurfaceLayer/types.ts index 22bb5154f..6d6b08349 100644 --- a/frontend/src/modules/2DViewer/layers/implementations/layers/StatisticalSurfaceLayer/types.ts +++ b/frontend/src/modules/2DViewer/layers/implementations/layers/StatisticalSurfaceLayer/types.ts @@ -2,7 +2,7 @@ import { SurfaceStatisticFunction_api } from "@api"; import { EnsembleIdent } from "@framework/EnsembleIdent"; import { SettingType } from "@modules/2DViewer/layers/implementations/settings/settingsTypes"; -import { SensitivityNameCasePair } from "../../settings/Sensitivity"; +import { SensitivityNameCasePair } from "../../settings/SensitivitySetting"; export type StatisticalSurfaceSettings = { [SettingType.ENSEMBLE]: EnsembleIdent | null; diff --git a/frontend/src/modules/2DViewer/layers/implementations/settings/DrilledWellbores.tsx b/frontend/src/modules/2DViewer/layers/implementations/settings/DrilledWellboresSetting.tsx similarity index 81% rename from frontend/src/modules/2DViewer/layers/implementations/settings/DrilledWellbores.tsx rename to frontend/src/modules/2DViewer/layers/implementations/settings/DrilledWellboresSetting.tsx index 2f69789f5..dbac02115 100644 --- a/frontend/src/modules/2DViewer/layers/implementations/settings/DrilledWellbores.tsx +++ b/frontend/src/modules/2DViewer/layers/implementations/settings/DrilledWellboresSetting.tsx @@ -13,7 +13,7 @@ import { AvailableValuesType, Setting, SettingComponentProps } from "../../inter type ValueType = WellboreHeader_api[] | null; -export class DrilledWellbores implements Setting { +export class DrilledWellboresSetting implements Setting { private _delegate: SettingDelegate; constructor() { @@ -105,31 +105,4 @@ export class DrilledWellbores implements Setting { } } -type WellboreHeaderSelectorProps = { - wellboreHeaders: WellboreHeader_api[]; - selectedWellboreUuids: string[]; - onChange: (selectedWellboreUuids: string[]) => void; -}; - -export function WellboreHeaderSelector(props: WellboreHeaderSelectorProps): React.ReactNode { - const options: SelectOption[] = props.wellboreHeaders.map((ident) => ({ - value: ident.wellboreUuid, - label: ident.uniqueWellboreIdentifier, - })); - - const handleChange = (selectedUuids: string[]) => { - props.onChange(selectedUuids); - }; - - return ( -