From b2942cd686321dbf6d8b948a6bc4f1842db29de1 Mon Sep 17 00:00:00 2001 From: Anders Rantala Hunderi Date: Fri, 1 Nov 2024 16:36:44 +0100 Subject: [PATCH 01/13] FIXUP -- Well-log-viewer module v0.1: * Supports continuous curves from SSDL endpoint * Settings are persisted across reloads --- .../primary/routers/well/converters.py | 6 + .../primary/primary/routers/well/router.py | 2 + .../primary/primary/routers/well/schemas.py | 13 +- .../primary/services/ssdl_access/types.py | 28 +- .../services/ssdl_access/well_access.py | 6 +- frontend/package-lock.json | 31 ++ frontend/package.json | 1 + .../src/api/models/WellboreLogCurveData.ts | 11 +- .../src/api/models/WellboreLogCurveHeader.ts | 2 +- frontend/src/framework/utils/arrays.ts | 14 + .../src/modules/WellLogViewer/interfaces.ts | 37 ++ .../src/modules/WellLogViewer/loadModule.tsx | 13 + .../modules/WellLogViewer/registerModule.ts | 31 ++ .../WellLogViewer/settings/atoms/baseAtoms.ts | 6 + .../settings/atoms/derivedAtoms.ts | 107 +++++ .../settings/atoms/persistedAtoms.ts | 56 +++ .../settings/atoms/queryAtoms.ts | 66 +++ .../settings/components/AddItemButton.tsx | 62 +++ .../SortablePlotList.tsx | 228 +++++++++++ .../SortableTrackItem.tsx | 72 ++++ .../TemplateTrackSettings/TrackSettings.tsx | 83 ++++ .../TemplateTrackSettings/index.tsx | 181 ++++++++ .../components/ViewerSettings/index.tsx | 57 +++ .../settings/components/WellpickSelect.tsx | 92 +++++ .../WellLogViewer/settings/settings.tsx | 120 ++++++ .../src/modules/WellLogViewer/utils/atoms.ts | 54 +++ .../src/modules/WellLogViewer/utils/hooks.ts | 12 + .../WellLogViewer/utils/logViewerColors.ts | 165 ++++++++ .../WellLogViewer/utils/logViewerTemplate.ts | 156 +++++++ .../WellLogViewer/utils/queryDataTransform.ts | 213 ++++++++++ .../WellLogViewer/view/ReadoutWrapper.tsx | 66 +++ .../view/SubsurfaceLogViewerWrapper.tsx | 265 ++++++++++++ .../WellLogViewer/view/atoms/derivedAtoms.ts | 28 ++ .../WellLogViewer/view/atoms/queryAtoms.ts | 20 + .../WellLogViewer/view/queries/shared.ts | 7 + .../view/queries/wellLogQueries.ts | 19 + .../view/queries/wellboreQueries.ts | 13 + .../src/modules/WellLogViewer/view/view.tsx | 93 +++++ .../usePropagateApiErrorToStatusWriter.ts | 16 +- frontend/src/modules/registerAllModules.ts | 1 + .../WellLogViewer/logViewerTemplate.test.ts | 385 ++++++++++++++++++ .../WellLogViewer/queryDataTransform.test.ts | 255 ++++++++++++ 42 files changed, 3074 insertions(+), 19 deletions(-) create mode 100644 frontend/src/framework/utils/arrays.ts create mode 100644 frontend/src/modules/WellLogViewer/interfaces.ts create mode 100644 frontend/src/modules/WellLogViewer/loadModule.tsx create mode 100644 frontend/src/modules/WellLogViewer/registerModule.ts create mode 100644 frontend/src/modules/WellLogViewer/settings/atoms/baseAtoms.ts create mode 100644 frontend/src/modules/WellLogViewer/settings/atoms/derivedAtoms.ts create mode 100644 frontend/src/modules/WellLogViewer/settings/atoms/persistedAtoms.ts create mode 100644 frontend/src/modules/WellLogViewer/settings/atoms/queryAtoms.ts create mode 100644 frontend/src/modules/WellLogViewer/settings/components/AddItemButton.tsx create mode 100644 frontend/src/modules/WellLogViewer/settings/components/TemplateTrackSettings/SortablePlotList.tsx create mode 100644 frontend/src/modules/WellLogViewer/settings/components/TemplateTrackSettings/SortableTrackItem.tsx create mode 100644 frontend/src/modules/WellLogViewer/settings/components/TemplateTrackSettings/TrackSettings.tsx create mode 100644 frontend/src/modules/WellLogViewer/settings/components/TemplateTrackSettings/index.tsx create mode 100644 frontend/src/modules/WellLogViewer/settings/components/ViewerSettings/index.tsx create mode 100644 frontend/src/modules/WellLogViewer/settings/components/WellpickSelect.tsx create mode 100644 frontend/src/modules/WellLogViewer/settings/settings.tsx create mode 100644 frontend/src/modules/WellLogViewer/utils/atoms.ts create mode 100644 frontend/src/modules/WellLogViewer/utils/hooks.ts create mode 100644 frontend/src/modules/WellLogViewer/utils/logViewerColors.ts create mode 100644 frontend/src/modules/WellLogViewer/utils/logViewerTemplate.ts create mode 100644 frontend/src/modules/WellLogViewer/utils/queryDataTransform.ts create mode 100644 frontend/src/modules/WellLogViewer/view/ReadoutWrapper.tsx create mode 100644 frontend/src/modules/WellLogViewer/view/SubsurfaceLogViewerWrapper.tsx create mode 100644 frontend/src/modules/WellLogViewer/view/atoms/derivedAtoms.ts create mode 100644 frontend/src/modules/WellLogViewer/view/atoms/queryAtoms.ts create mode 100644 frontend/src/modules/WellLogViewer/view/queries/shared.ts create mode 100644 frontend/src/modules/WellLogViewer/view/queries/wellLogQueries.ts create mode 100644 frontend/src/modules/WellLogViewer/view/queries/wellboreQueries.ts create mode 100644 frontend/src/modules/WellLogViewer/view/view.tsx create mode 100644 frontend/tests/unit/WellLogViewer/logViewerTemplate.test.ts create mode 100644 frontend/tests/unit/WellLogViewer/queryDataTransform.test.ts diff --git a/backend_py/primary/primary/routers/well/converters.py b/backend_py/primary/primary/routers/well/converters.py index f6d01f078..d00be3646 100644 --- a/backend_py/primary/primary/routers/well/converters.py +++ b/backend_py/primary/primary/routers/well/converters.py @@ -120,6 +120,9 @@ def convert_wellbore_perforation_to_schema( def convert_wellbore_log_curve_header_to_schema( wellbore_log_curve_header: WellboreLogCurveHeader, ) -> schemas.WellboreLogCurveHeader: + if wellbore_log_curve_header.log_name is None: + raise AttributeError("Missing log name is not allowed") + return schemas.WellboreLogCurveHeader( logName=wellbore_log_curve_header.log_name, curveName=wellbore_log_curve_header.curve_name, @@ -131,6 +134,9 @@ def convert_wellbore_log_curve_data_to_schema( wellbore_log_curve_data: WellboreLogCurveData, ) -> schemas.WellboreLogCurveData: return schemas.WellboreLogCurveData( + name=wellbore_log_curve_data.name, + unit=wellbore_log_curve_data.unit, + curveUnitDesc=wellbore_log_curve_data.curve_unit_desc, indexMin=wellbore_log_curve_data.index_min, indexMax=wellbore_log_curve_data.index_max, minCurveValue=wellbore_log_curve_data.min_curve_value, diff --git a/backend_py/primary/primary/routers/well/router.py b/backend_py/primary/primary/routers/well/router.py index 9d8e6c415..fdc35af6a 100644 --- a/backend_py/primary/primary/routers/well/router.py +++ b/backend_py/primary/primary/routers/well/router.py @@ -216,6 +216,8 @@ async def get_wellbore_log_curve_headers( return [ converters.convert_wellbore_log_curve_header_to_schema(wellbore_log_curve_header) for wellbore_log_curve_header in wellbore_log_curve_headers + # Missing log name implies garbage data, so we simply drop them + if wellbore_log_curve_header.log_name is not None ] diff --git a/backend_py/primary/primary/routers/well/schemas.py b/backend_py/primary/primary/routers/well/schemas.py index 8427292d0..783a1ece4 100644 --- a/backend_py/primary/primary/routers/well/schemas.py +++ b/backend_py/primary/primary/routers/well/schemas.py @@ -103,16 +103,19 @@ class WellborePerforation(BaseModel): class WellboreLogCurveHeader(BaseModel): logName: str curveName: str - curveUnit: str + curveUnit: str | None class WellboreLogCurveData(BaseModel): + name: str indexMin: float indexMax: float minCurveValue: float maxCurveValue: float - dataPoints: list[list[float | None]] - curveAlias: str - curveDescription: str + curveAlias: str | None + curveDescription: str | None indexUnit: str - noDataValue: float + noDataValue: float | None + unit: str + curveUnitDesc: str | None + dataPoints: list[list[float | None]] diff --git a/backend_py/primary/primary/services/ssdl_access/types.py b/backend_py/primary/primary/services/ssdl_access/types.py index 7e0660d94..84f07a0cf 100644 --- a/backend_py/primary/primary/services/ssdl_access/types.py +++ b/backend_py/primary/primary/services/ssdl_access/types.py @@ -1,3 +1,4 @@ +from typing import Any from pydantic import BaseModel @@ -34,18 +35,33 @@ class WellborePerforation(BaseModel): class WellboreLogCurveHeader(BaseModel): - log_name: str + log_name: str | None curve_name: str - curve_unit: str + curve_unit: str | None + + # Defining a hash-function to facilitate usage in Sets + def __hash__(self) -> int: + # No globally unique field, but curve-name should be unique (per wellbore) + return hash(self.curve_name + (self.log_name or "N/A")) + + def __eq__(self, other: Any) -> bool: + if not isinstance(other, WellboreLogCurveHeader): + # delegate to the other item in the comparison + return NotImplemented + + return (self.curve_name, self.log_name) == (other.curve_name, other.log_name) class WellboreLogCurveData(BaseModel): + name: str index_min: float index_max: float min_curve_value: float max_curve_value: float - DataPoints: list[list[float | None]] - curve_alias: str - curve_description: str + curve_alias: str | None + curve_description: str | None index_unit: str - no_data_value: float + no_data_value: float | None + unit: str + curve_unit_desc: str | None + DataPoints: list[list[float | None]] diff --git a/backend_py/primary/primary/services/ssdl_access/well_access.py b/backend_py/primary/primary/services/ssdl_access/well_access.py index acdb4542f..da17dc28e 100644 --- a/backend_py/primary/primary/services/ssdl_access/well_access.py +++ b/backend_py/primary/primary/services/ssdl_access/well_access.py @@ -51,10 +51,12 @@ async def get_log_curve_headers_for_wellbore(self, wellbore_uuid: str) -> List[t endpoint = f"WellLog/{wellbore_uuid}" ssdl_data = await fetch_from_ssdl(access_token=self._ssdl_token, endpoint=endpoint, params=None) try: - result = [types.WellboreLogCurveHeader.model_validate(log_curve) for log_curve in ssdl_data] + # This endpoint is a bit weird, and MIGHT return duplicates which, as far as I can tell, are the exact same. Using a set to drop duplicates. See data model for comparator + result_set = {types.WellboreLogCurveHeader.model_validate(log_curve) for log_curve in ssdl_data} + except ValidationError as error: raise InvalidDataError(f"Invalid log curve headers for wellbore {wellbore_uuid}", Service.SSDL) from error - return result + return list(result_set) async def get_log_curve_headers_for_field(self, field_uuid: str) -> List[types.WellboreLogCurveHeader]: endpoint = f"WellLog/field/{field_uuid}" diff --git a/frontend/package-lock.json b/frontend/package-lock.json index e50e50853..795960ac5 100644 --- a/frontend/package-lock.json +++ b/frontend/package-lock.json @@ -19,6 +19,7 @@ "@webviz/group-tree-plot": "^1.1.14", "@webviz/subsurface-viewer": "^0.25.2", "@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", @@ -890,6 +891,19 @@ "@equinor/videx-linear-algebra": "^1.0.7" } }, + "node_modules/@equinor/videx-wellog": { + "version": "0.10.3", + "resolved": "https://registry.npmjs.org/@equinor/videx-wellog/-/videx-wellog-0.10.3.tgz", + "integrity": "sha512-twcQXeXDLcl5szLJ3MDDBbR2ehg0uTgHmK4FTqDzfAGP6y6jVowxoz2V3vOzS5nsk63QsySiEuWNxYMpLwQJmQ==", + "dependencies": { + "@equinor/videx-math": "^1.1.0", + "d3-array": "^3.2.0", + "d3-scale": "^4.0.2", + "d3-selection": "^3.0.0", + "d3-shape": "^3.1.0", + "d3-zoom": "^3.0.0" + } + }, "node_modules/@esbuild/aix-ppc64": { "version": "0.19.11", "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.19.11.tgz", @@ -5069,6 +5083,23 @@ "react-dom": "^18.0.0" } }, + "node_modules/@webviz/well-log-viewer": { + "version": "1.12.7", + "resolved": "https://registry.npmjs.org/@webviz/well-log-viewer/-/well-log-viewer-1.12.7.tgz", + "integrity": "sha512-evCrmtRRIBeQfHDSQmlQERBllCzXXR9mAVnK+sKwy4IH0i5enOEf6BpoVLmoA0eHLlum30H+tj3rEha2xT5ZFg==", + "dependencies": { + "@emerson-eps/color-tables": "^0.4.71", + "@equinor/videx-wellog": "^0.10.0", + "@webviz/wsc-common": "*", + "convert-units": "^2.3.4", + "d3": "^7.8.2" + }, + "peerDependencies": { + "@mui/material": "^5.11", + "react": "^17 || ^18", + "react-dom": "^17 || ^18" + } + }, "node_modules/@webviz/wsc-common": { "version": "0.0.1-alpha.1", "resolved": "https://registry.npmjs.org/@webviz/wsc-common/-/wsc-common-0.0.1-alpha.1.tgz", diff --git a/frontend/package.json b/frontend/package.json index 552408a48..591989345 100644 --- a/frontend/package.json +++ b/frontend/package.json @@ -29,6 +29,7 @@ "@types/geojson": "^7946.0.14", "@webviz/group-tree-plot": "^1.1.14", "@webviz/subsurface-viewer": "^0.25.2", + "@webviz/well-log-viewer": "^1.12.7", "@webviz/well-completions-plot": "^1.5.11", "animate.css": "^4.1.1", "axios": "^1.6.5", diff --git a/frontend/src/api/models/WellboreLogCurveData.ts b/frontend/src/api/models/WellboreLogCurveData.ts index d8d952bd4..81f8170df 100644 --- a/frontend/src/api/models/WellboreLogCurveData.ts +++ b/frontend/src/api/models/WellboreLogCurveData.ts @@ -3,14 +3,17 @@ /* tslint:disable */ /* eslint-disable */ export type WellboreLogCurveData = { + name: string; indexMin: number; indexMax: number; minCurveValue: number; maxCurveValue: number; - dataPoints: Array>; - curveAlias: string; - curveDescription: string; + curveAlias: (string | null); + curveDescription: (string | null); indexUnit: string; - noDataValue: number; + noDataValue: (number | null); + unit: string; + curveUnitDesc: (string | null); + dataPoints: Array>; }; diff --git a/frontend/src/api/models/WellboreLogCurveHeader.ts b/frontend/src/api/models/WellboreLogCurveHeader.ts index 12049f97e..3d804b915 100644 --- a/frontend/src/api/models/WellboreLogCurveHeader.ts +++ b/frontend/src/api/models/WellboreLogCurveHeader.ts @@ -5,6 +5,6 @@ export type WellboreLogCurveHeader = { logName: string; curveName: string; - curveUnit: string; + curveUnit: (string | null); }; diff --git a/frontend/src/framework/utils/arrays.ts b/frontend/src/framework/utils/arrays.ts new file mode 100644 index 000000000..1ee291e9a --- /dev/null +++ b/frontend/src/framework/utils/arrays.ts @@ -0,0 +1,14 @@ +/** + * Util method to do an immutable item move in an array + * @param array The array to move items in + * @param from The index of the first item being moved + * @param to The index the item(s) should be moved to + * @param moveAmt The amount of items (from the start-index) that should be moved + * @returns A copy of the original array, with it's items moved accordingly + */ +export function arrayMove(array: t[], from: number, to: number, moveAmt = 1): t[] { + const newArrray = [...array]; + const movedItems = newArrray.splice(from, moveAmt); + + return newArrray.toSpliced(to, 0, ...movedItems); +} diff --git a/frontend/src/modules/WellLogViewer/interfaces.ts b/frontend/src/modules/WellLogViewer/interfaces.ts new file mode 100644 index 000000000..da0c8e06f --- /dev/null +++ b/frontend/src/modules/WellLogViewer/interfaces.ts @@ -0,0 +1,37 @@ +import { WellboreHeader_api } from "@api"; +import { InterfaceInitialization } from "@framework/UniDirectionalModuleComponentsInterface"; +import { WellPicksLayerData } from "@modules/Intersection/utils/layers/WellpicksLayer"; +import { TemplateTrack } from "@webviz/well-log-viewer/dist/components/WellLogTemplateTypes"; + +import { + allSelectedWellLogCurvesAtom, + selectedFieldIdentifierAtom, + selectedWellboreAtom, + selectedWellborePicksAtom, + wellLogTemplateTracks, +} from "./settings/atoms/derivedAtoms"; +import { padDataWithEmptyRowsAtom, viewerHorizontalAtom } from "./settings/atoms/persistedAtoms"; + +export type InterfaceTypes = { + settingsToView: SettingsToViewInterface; +}; + +export type SettingsToViewInterface = { + selectedField: string | null; + wellboreHeader: WellboreHeader_api | null; + requiredDataCurves: string[]; + templateTracks: TemplateTrack[]; + viewerHorizontal: boolean; + padDataWithEmptyRows: boolean; + selectedWellborePicks: WellPicksLayerData; +}; + +export const settingsToViewInterfaceInitialization: InterfaceInitialization = { + selectedField: (get) => get(selectedFieldIdentifierAtom), + wellboreHeader: (get) => get(selectedWellboreAtom), + templateTracks: (get) => get(wellLogTemplateTracks), + requiredDataCurves: (get) => get(allSelectedWellLogCurvesAtom), + viewerHorizontal: (get) => get(viewerHorizontalAtom), + padDataWithEmptyRows: (get) => get(padDataWithEmptyRowsAtom), + selectedWellborePicks: (get) => get(selectedWellborePicksAtom), +}; diff --git a/frontend/src/modules/WellLogViewer/loadModule.tsx b/frontend/src/modules/WellLogViewer/loadModule.tsx new file mode 100644 index 000000000..3381cca53 --- /dev/null +++ b/frontend/src/modules/WellLogViewer/loadModule.tsx @@ -0,0 +1,13 @@ +import { ModuleRegistry } from "@framework/ModuleRegistry"; + +import { InterfaceTypes, 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.viewFC = View; +module.settingsFC = Settings; diff --git a/frontend/src/modules/WellLogViewer/registerModule.ts b/frontend/src/modules/WellLogViewer/registerModule.ts new file mode 100644 index 000000000..0798e75b7 --- /dev/null +++ b/frontend/src/modules/WellLogViewer/registerModule.ts @@ -0,0 +1,31 @@ +/** + * Well log viewer module. + * @author Anders Rantala Hunderi + * @since 08.14.2024 + */ +import { ModuleCategory, ModuleDevState } from "@framework/Module"; +import { ModuleRegistry } from "@framework/ModuleRegistry"; +import { SyncSettingKey } from "@framework/SyncSettings"; + +import { InterfaceTypes } from "./interfaces"; +import { clearStorageForInstance } from "./settings/atoms/persistedAtoms"; + +export const MODULE_NAME = "WellLogViewer"; +const MODULE_TITLE = "Well log Viewer"; +// TODO: Better description +const MODULE_DESCRIPTION = "Well log Viewer"; +// TODO: preview Icon + +ModuleRegistry.registerModule({ + moduleName: MODULE_NAME, + defaultTitle: MODULE_TITLE, + description: MODULE_DESCRIPTION, + + category: ModuleCategory.MAIN, + devState: ModuleDevState.DEV, + + syncableSettingKeys: [SyncSettingKey.INTERSECTION, SyncSettingKey.VERTICAL_SCALE], + onInstanceUnload(instanceId) { + clearStorageForInstance(instanceId); + }, +}); diff --git a/frontend/src/modules/WellLogViewer/settings/atoms/baseAtoms.ts b/frontend/src/modules/WellLogViewer/settings/atoms/baseAtoms.ts new file mode 100644 index 000000000..5e0f79f75 --- /dev/null +++ b/frontend/src/modules/WellLogViewer/settings/atoms/baseAtoms.ts @@ -0,0 +1,6 @@ +import { atom } from "jotai"; + +export const userSelectedFieldIdentifierAtom = atom(null); +export const userSelectedWellboreUuidAtom = atom(null); +export const userSelectedUnitWellpicksAtom = atom([]); +export const userSelectedNonUnitWellpicksAtom = atom([]); diff --git a/frontend/src/modules/WellLogViewer/settings/atoms/derivedAtoms.ts b/frontend/src/modules/WellLogViewer/settings/atoms/derivedAtoms.ts new file mode 100644 index 000000000..95e5393eb --- /dev/null +++ b/frontend/src/modules/WellLogViewer/settings/atoms/derivedAtoms.ts @@ -0,0 +1,107 @@ +import { WellboreHeader_api, WellboreLogCurveHeader_api } from "@api"; +import { EnsembleSetAtom } from "@framework/GlobalAtoms"; +import { WellPicksLayerData } from "@modules/Intersection/utils/layers/WellpicksLayer"; +import { TemplatePlot, TemplateTrack } from "@webviz/well-log-viewer/dist/components/WellLogTemplateTypes"; + +import { atom } from "jotai"; +import _, { Dictionary } from "lodash"; + +import { + userSelectedFieldIdentifierAtom, + userSelectedNonUnitWellpicksAtom, + userSelectedUnitWellpicksAtom, + userSelectedWellboreUuidAtom, +} from "./baseAtoms"; +import { logViewerTrackConfigs } from "./persistedAtoms"; +import { + drilledWellboreHeadersQueryAtom, + wellLogCurveHeadersQueryAtom, + wellborePicksAndStratigraphyQueryAtom, +} from "./queryAtoms"; + +export const selectedEnsembleSetAtom = atom((get) => { + const ensembleSetArr = get(EnsembleSetAtom).getEnsembleArr(); + const selectedFieldId = get(userSelectedFieldIdentifierAtom); + + if (ensembleSetArr.length < 1) { + return null; + } else { + const selectedEnsemble = ensembleSetArr.find((e) => e.getFieldIdentifier() === selectedFieldId); + + return selectedEnsemble ?? ensembleSetArr[0]; + } +}); + +export const selectedFieldIdentifierAtom = atom((get) => { + return get(selectedEnsembleSetAtom)?.getFieldIdentifier() ?? null; +}); + +export const selectedWellboreAtom = atom((get) => { + const availableWellboreHeaders = get(drilledWellboreHeadersQueryAtom)?.data; + const selectedWellboreId = get(userSelectedWellboreUuidAtom); + + return getSelectedWellboreHeader(selectedWellboreId, availableWellboreHeaders); +}); + +export const selectedWellborePicksAtom = atom((get) => { + const wellborePicks = get(wellborePicksAndStratigraphyQueryAtom)?.data; + const selectedUnitPicks = get(userSelectedUnitWellpicksAtom); + const selectedNonUnitPicks = get(userSelectedNonUnitWellpicksAtom); + + if (!wellborePicks) return { unitPicks: [], nonUnitPicks: [] }; + else { + const unitPicks = wellborePicks.unitPicks.filter((pick) => selectedUnitPicks.includes(pick.name)); + const nonUnitPicks = wellborePicks.nonUnitPicks.filter((pick) => + selectedNonUnitPicks.includes(pick.identifier) + ); + + return { unitPicks, nonUnitPicks }; + } +}); + +export const groupedCurveHeadersAtom = atom>((get) => { + const logCurveHeaders = get(wellLogCurveHeadersQueryAtom)?.data ?? []; + + return _.groupBy(logCurveHeaders, "logName"); +}); + +export const wellLogTemplateTracks = atom((get) => { + const templateTrackConfigs = get(logViewerTrackConfigs); + + return templateTrackConfigs.map((config): TemplateTrack => { + return { + ...config, + plots: config.plots.filter(({ _isValid }) => _isValid) as TemplatePlot[], + }; + }); +}); + +export const allSelectedWellLogCurvesAtom = atom((get) => { + const templateTracks = get(wellLogTemplateTracks); + + const curveNames = templateTracks.reduce((acc, trackCfg) => { + const usedCurves = _.flatMap(trackCfg.plots, ({ name, name2 }) => { + if (name2) return [name, name2]; + else return [name]; + }); + + return _.uniq([...acc, ...usedCurves]); + }, []); + + return curveNames; +}); + +function getSelectedWellboreHeader( + currentId: string | null, + wellboreHeaderSet: WellboreHeader_api[] | null | undefined +): WellboreHeader_api | null { + if (!wellboreHeaderSet || wellboreHeaderSet.length < 1) { + return null; + } + + if (!currentId) { + return wellboreHeaderSet[0]; + } + + return wellboreHeaderSet.find((wh) => wh.wellboreUuid === currentId) ?? wellboreHeaderSet[0]; +} diff --git a/frontend/src/modules/WellLogViewer/settings/atoms/persistedAtoms.ts b/frontend/src/modules/WellLogViewer/settings/atoms/persistedAtoms.ts new file mode 100644 index 000000000..b0be012bf --- /dev/null +++ b/frontend/src/modules/WellLogViewer/settings/atoms/persistedAtoms.ts @@ -0,0 +1,56 @@ +import { atomWithModuleInstanceStorage, clearModuleInstanceStorage } from "@modules/WellLogViewer/utils/atoms"; +import { TemplatePlot, TemplateTrack } from "@webviz/well-log-viewer/dist/components/WellLogTemplateTypes"; + +import { Getter, Setter, atom } from "jotai"; +import { Dictionary } from "lodash"; + +/** + * Extension of the template track type with additional fields used while editing + */ +export type TemplatePlotConfig = Partial & { + // Used for state updates + _id: string; + // Wether the config has all required fields for it's curve-type + _isValid: boolean; + // This is used as the value for dropdowns. Even if the curvename is supposed to be unique, In some rare cases, the curvename is duplicated across different well-logs. + _logAndName: `${string}::${string}`; + _logAndName2?: `${string}::${string}`; +}; +export type TemplateTrackConfig = Omit & { + // ID used to allow the settings-menu to drag-sort them + _id: string; + plots: TemplatePlotConfig[]; +}; + +const STORAGE_KEY = "moduleSettings"; +const moduleSettingsAtom = atomWithModuleInstanceStorage>(STORAGE_KEY, {}); + +function getPersistentModuleField(get: Getter, valueKey: string, defaultValue: any): typeof defaultValue { + return get(moduleSettingsAtom)[valueKey] ?? defaultValue; +} + +function setPersistentModuleField(get: Getter, set: Setter, valueKey: string, newValue: any) { + const storageCopy = { ...get(moduleSettingsAtom) }; + storageCopy[valueKey] = newValue; + + set(moduleSettingsAtom, storageCopy); +} + +export const logViewerTrackConfigs = atom( + (get) => getPersistentModuleField(get, "logViewerTrackConfigs", []), + (get, set, newVal) => setPersistentModuleField(get, set, "logViewerTrackConfigs", newVal) +); + +export const viewerHorizontalAtom = atom( + (get) => getPersistentModuleField(get, "viewerHorizontal", false), + (get, set, newVal) => setPersistentModuleField(get, set, "viewerHorizontal", newVal) +); + +export const padDataWithEmptyRowsAtom = atom( + (get) => getPersistentModuleField(get, "padDataWithEmptyRows", true), + (get, set, newVal) => setPersistentModuleField(get, set, "padDataWithEmptyRows", newVal) +); + +export function clearStorageForInstance(instanceId: string) { + clearModuleInstanceStorage(instanceId, STORAGE_KEY); +} diff --git a/frontend/src/modules/WellLogViewer/settings/atoms/queryAtoms.ts b/frontend/src/modules/WellLogViewer/settings/atoms/queryAtoms.ts new file mode 100644 index 000000000..809f7b0cc --- /dev/null +++ b/frontend/src/modules/WellLogViewer/settings/atoms/queryAtoms.ts @@ -0,0 +1,66 @@ +import { transformFormationData } from "@equinor/esv-intersection"; +import { apiService } from "@framework/ApiService"; +import { WellPicksLayerData } from "@modules/Intersection/utils/layers/WellpicksLayer"; + +import { atomWithQuery } from "jotai-tanstack-query"; +import _ from "lodash"; + +import { selectedEnsembleSetAtom, selectedFieldIdentifierAtom, selectedWellboreAtom } from "./derivedAtoms"; + +const STALE_TIME = 60 * 1000; +const CACHE_TIME = 60 * 1000; + +const SHARED_QUERY_OPTS = { + staleTime: STALE_TIME, + gcTime: CACHE_TIME, +}; + +export const drilledWellboreHeadersQueryAtom = atomWithQuery((get) => { + const fieldId = get(selectedFieldIdentifierAtom) ?? ""; + + return { + queryKey: ["getDrilledWellboreHeader", fieldId], + queryFn: () => apiService.well.getDrilledWellboreHeaders(fieldId), + enabled: Boolean(fieldId), + ...SHARED_QUERY_OPTS, + }; +}); + +/* ! Note + No logs are returned for any of the Drogon wells, afaik. Found a working set using in one of the TROLL ones. Some of them are still on the old system, so just click around until you find a working one + +*/ +export const wellLogCurveHeadersQueryAtom = atomWithQuery((get) => { + const wellboreId = get(selectedWellboreAtom)?.wellboreUuid; + + return { + queryKey: ["getWellboreLogCurveHeaders", wellboreId], + queryFn: () => apiService.well.getWellboreLogCurveHeaders(wellboreId ?? ""), + enabled: Boolean(wellboreId), + ...SHARED_QUERY_OPTS, + }; +}); + +export const wellborePicksAndStratigraphyQueryAtom = atomWithQuery((get) => { + const selectedEnsemble = get(selectedEnsembleSetAtom); + + const wellboreId = get(selectedWellboreAtom)?.wellboreUuid ?? ""; + const caseId = selectedEnsemble?.getIdent()?.getCaseUuid() ?? ""; + + return { + queryKey: ["getWellborePicksAndStratigraphicUnits", wellboreId, caseId], + enabled: Boolean(caseId && wellboreId), + queryFn: async () => { + const data = await apiService.well.getWellborePicksAndStratigraphicUnits(caseId, wellboreId); + + const transformedData = transformFormationData(data.wellbore_picks, data.stratigraphic_units as any); + + // ! Sometimes the transformation data returns duplicate entries, filtering them out + return { + nonUnitPicks: _.uniqBy(transformedData.nonUnitPicks, "identifier"), + unitPicks: _.uniqBy(transformedData.unitPicks, "name"), + }; + }, + ...SHARED_QUERY_OPTS, + }; +}); diff --git a/frontend/src/modules/WellLogViewer/settings/components/AddItemButton.tsx b/frontend/src/modules/WellLogViewer/settings/components/AddItemButton.tsx new file mode 100644 index 000000000..00b42001e --- /dev/null +++ b/frontend/src/modules/WellLogViewer/settings/components/AddItemButton.tsx @@ -0,0 +1,62 @@ +import React from "react"; + +import { Menu } from "@lib/components/Menu"; +import { MenuItem } from "@lib/components/MenuItem"; +import { SelectOption } from "@lib/components/Select"; +import { Button, Dropdown, MenuButton } from "@mui/base"; +import { Add, ArrowDropDown } from "@mui/icons-material"; + +export type AddItemButtonProps = { + buttonText: string; + options?: SelectOption[]; + onAddClicked?: () => void; + onOptionClicked?: (value: SelectOption["value"]) => void; +}; + +/** + * Generic add-button, for the top of sortable-lists. Uses a dropdown if there's more than 1 available options + */ +export function AddItemButton(props: AddItemButtonProps): React.ReactNode { + const { onOptionClicked, onAddClicked } = props; + + const handleOptionClicked = React.useCallback( + function handleOptionClicked(item: SelectOption) { + if (onOptionClicked) onOptionClicked(item.value); + }, + [onOptionClicked] + ); + + if (!props.options) { + return ( + + ); + } + + return ( + + + + + + + {props.options.map((entry) => ( + handleOptionClicked(entry)}> + {entry.label} + + ))} + + + ); +} + +function ButtonContent(props: { text: string; multiple?: boolean }) { + return ( +
+ + {props.text} + {props.multiple && } +
+ ); +} diff --git a/frontend/src/modules/WellLogViewer/settings/components/TemplateTrackSettings/SortablePlotList.tsx b/frontend/src/modules/WellLogViewer/settings/components/TemplateTrackSettings/SortablePlotList.tsx new file mode 100644 index 000000000..c2248225a --- /dev/null +++ b/frontend/src/modules/WellLogViewer/settings/components/TemplateTrackSettings/SortablePlotList.tsx @@ -0,0 +1,228 @@ +import React from "react"; + +import { WellboreLogCurveHeader_api } from "@api"; +import { arrayMove } from "@framework/utils/arrays"; +import { Dropdown, DropdownOption } from "@lib/components/Dropdown"; +import { Label } from "@lib/components/Label"; +import { SortableList, SortableListItem } from "@lib/components/SortableList"; +import { ColorSet } from "@lib/utils/ColorSet"; +import { CURVE_COLOR_PALETTE } from "@modules/WellLogViewer/utils/logViewerColors"; +import { PLOT_TYPE_OPTIONS, makeTrackPlot } from "@modules/WellLogViewer/utils/logViewerTemplate"; +import { Delete, SwapHoriz, Warning } from "@mui/icons-material"; +import { TemplatePlotTypes } from "@webviz/well-log-viewer/dist/components/WellLogTemplateTypes"; + +import { useAtomValue } from "jotai"; +import _ from "lodash"; + +import { allSelectedWellLogCurvesAtom } from "../../atoms/derivedAtoms"; +import { TemplatePlotConfig } from "../../atoms/persistedAtoms"; +import { AddItemButton } from "../AddItemButton"; + +export type SortablePlotListProps = { + availableCurveHeaders: WellboreLogCurveHeader_api[]; + plots: TemplatePlotConfig[]; + onUpdatePlots: (plots: TemplatePlotConfig[]) => void; +}; + +export function SortablePlotList(props: SortablePlotListProps): React.ReactNode { + const allSelectedWellLogCurves = useAtomValue(allSelectedWellLogCurvesAtom); + + const { onUpdatePlots } = props; + + const curveHeaderOptions = makeCurveNameOptions(props.availableCurveHeaders); + + // If the current selection does not exist, keep it in the selection, with a warning. This can happen when the user is importing a config, or swapping between wellbores + allSelectedWellLogCurves.forEach((curveName) => { + if (!curveHeaderOptions.some(({ value }) => value.endsWith("::" + curveName))) { + curveHeaderOptions.push(makeMissingCurveOption(curveName)); + } + }); + + // TODO, do an offsett or something, so they dont always start on the same color? + const colorSet = React.useRef(new ColorSet(CURVE_COLOR_PALETTE)); + + const addPlot = React.useCallback( + function addPlot(plotType: string) { + const plotConfig: TemplatePlotConfig = makeTrackPlot({ + color: colorSet.current.getNextColor(), + type: plotType as TemplatePlotTypes, + }); + + onUpdatePlots([...props.plots, plotConfig]); + }, + [onUpdatePlots, props.plots] + ); + + const removePlot = React.useCallback( + function removePlot(plot: TemplatePlotConfig) { + onUpdatePlots(props.plots.filter((p) => p._id !== plot._id)); + }, + [onUpdatePlots, props.plots] + ); + + const handlePlotUpdate = React.useCallback( + function handlePlotUpdate(newPlot: TemplatePlotConfig) { + const newPlots = props.plots.map((p) => (p._id === newPlot._id ? newPlot : p)); + + onUpdatePlots(newPlots); + }, + [onUpdatePlots, props.plots] + ); + + const handleTrackMove = React.useCallback( + function handleTrackMove( + movedItemId: string, + originId: string | null, + destinationId: string | null, + newPosition: number + ) { + // Skip update if the item was moved above or below itself, as this means no actual move happened + // TODO: This should probably be checked inside SortableList + const currentPosition = props.plots.findIndex((p) => p.name === movedItemId); + if (currentPosition === newPosition || currentPosition + 1 === newPosition) return; + + const newTrackCfg = arrayMove(props.plots, currentPosition, newPosition); + + onUpdatePlots(newTrackCfg); + }, + [onUpdatePlots, props.plots] + ); + + return ( +
+ + + + {props.plots.map((plot) => ( + + ))} + +
+ ); +} + +type SortablePlotItemProps = { + plot: TemplatePlotConfig; + curveHeaderOptions: DropdownOption[]; + onPlotUpdate: (plot: TemplatePlotConfig) => void; + onDeletePlot: (plot: TemplatePlotConfig) => void; +}; + +function SortablePlotItem(props: SortablePlotItemProps) { + const { onPlotUpdate } = props; + const secondCurveNeeded = props.plot.type === "differential"; + + const handlePlotChange = React.useCallback( + function handlePlotChange(changes: Partial) { + const newPlot = makeTrackPlot({ + ...props.plot, + ...changes, + }); + + onPlotUpdate(newPlot); + }, + [props.plot, onPlotUpdate] + ); + + const title = ( + <> + handlePlotChange({ _logAndName: v, name: v.split("::")[1] })} + /> + + ); + + const endAdornment = ( + <> + {secondCurveNeeded && ( + <> + + handlePlotChange({ _logAndName2: v, name2: v.split("::")[1] })} + /> + + )} +
+ handlePlotChange({ type: v as TemplatePlotTypes })} + /> +
+ + + + ); + + return ; +} +function sortStatLogsToTop(o: WellboreLogCurveHeader_api) { + if (o.logName.startsWith("STAT_")) return 0; + else return 1; +} + +function makeCurveNameOptions(curveHeaders: WellboreLogCurveHeader_api[]): DropdownOption[] { + // It's my understanding that the STAT logs are the main curves users' would care about, so sorting them to the top first + return _.chain(curveHeaders) + .sortBy([sortStatLogsToTop, "logName", "curveName"]) + .map((curveHeader): DropdownOption => { + return { + // ... surely they wont have log-names with :: in them, RIGHT? + value: `${curveHeader.logName}::${curveHeader.curveName}`, + label: curveHeader.curveName, + group: curveHeader.logName, + }; + }) + .value(); +} + +// Helper method to show a missing curve as a disabled option +function makeMissingCurveOption(curveAndLogName: string): DropdownOption { + return { + label: curveAndLogName.split("::")[0], + value: curveAndLogName, + group: "Unavailable curves!", + disabled: true, + adornment: ( + + + + ), + }; +} diff --git a/frontend/src/modules/WellLogViewer/settings/components/TemplateTrackSettings/SortableTrackItem.tsx b/frontend/src/modules/WellLogViewer/settings/components/TemplateTrackSettings/SortableTrackItem.tsx new file mode 100644 index 000000000..31bb5a01e --- /dev/null +++ b/frontend/src/modules/WellLogViewer/settings/components/TemplateTrackSettings/SortableTrackItem.tsx @@ -0,0 +1,72 @@ +import React from "react"; + +import { SettingsStatusWriter } from "@framework/StatusWriter"; +import { SortableListItem } from "@lib/components/SortableList"; +import { Delete, ExpandLess, ExpandMore, Settings, Warning } from "@mui/icons-material"; + +import { TrackSettings } from "./TrackSettings"; + +import { TemplateTrackConfig } from "../../atoms/persistedAtoms"; + +export type CurveTrackItemProps = { + trackConfig: TemplateTrackConfig; + statusWriter: SettingsStatusWriter; + onUpdateTrack: (newTrack: TemplateTrackConfig) => void; + onDeleteTrack: (track: TemplateTrackConfig) => void; +}; + +export function SortableTrackItem(props: CurveTrackItemProps) { + const [isExpanded, setIsExpanded] = React.useState(true); + + const itemEndAdornment = ( + props.onDeleteTrack(props.trackConfig)} + toggleExpanded={() => setIsExpanded(!isExpanded)} + /> + ); + + return ( + + + + ); +} + +type ListItemEndAdornmentProps = { + track: TemplateTrackConfig; + isExpanded: boolean; + onDeleteTrack?: () => void; + toggleExpanded?: () => void; +}; + +function ListItemEndAdornment(props: ListItemEndAdornmentProps) { + return ( + <> + {props.track.plots.length < 1 && ( + + + + )} + + + + + ); +} diff --git a/frontend/src/modules/WellLogViewer/settings/components/TemplateTrackSettings/TrackSettings.tsx b/frontend/src/modules/WellLogViewer/settings/components/TemplateTrackSettings/TrackSettings.tsx new file mode 100644 index 000000000..81d30881b --- /dev/null +++ b/frontend/src/modules/WellLogViewer/settings/components/TemplateTrackSettings/TrackSettings.tsx @@ -0,0 +1,83 @@ +import React from "react"; + +import { Dropdown } from "@lib/components/Dropdown"; +import { Input } from "@lib/components/Input"; +import { PendingWrapper } from "@lib/components/PendingWrapper"; +import { PLOT_SCALE_OPTIONS } from "@modules/WellLogViewer/utils/logViewerTemplate"; +import { usePropagateApiErrorToStatusWriter } from "@modules/_shared/hooks/usePropagateApiErrorToStatusWriter"; +import { TemplatePlotScaleTypes } from "@webviz/well-log-viewer/dist/components/WellLogTemplateTypes"; + +import { useAtomValue } from "jotai"; + +import { SortablePlotList } from "./SortablePlotList"; +import { CurveTrackItemProps } from "./SortableTrackItem"; + +import { TemplateTrackConfig } from "../../atoms/persistedAtoms"; +import { wellLogCurveHeadersQueryAtom } from "../../atoms/queryAtoms"; + +export type TrackSettingsProps = CurveTrackItemProps; +type ConfigChanges = Pick, "width" | "plots" | "scale" | "title">; + +const INPUT_DEBOUNCE_TIME = 500; + +export function TrackSettings(props: TrackSettingsProps): React.ReactNode { + const { onUpdateTrack } = props; + + const curveHeadersQuery = useAtomValue(wellLogCurveHeadersQueryAtom); + const curveHeadersErrorStatus = usePropagateApiErrorToStatusWriter(curveHeadersQuery, props.statusWriter) ?? ""; + + const updateTrackConfig = React.useCallback( + function updateTrackConfig(configChanges: ConfigChanges) { + onUpdateTrack({ ...props.trackConfig, ...configChanges }); + }, + [props.trackConfig, onUpdateTrack] + ); + + return ( +
+ + updateTrackConfig({ title: val })} + /> + + + updateTrackConfig({ width: Number(val) })} + /> + + + { + if (!val) updateTrackConfig({ scale: undefined }); + else updateTrackConfig({ scale: val as TemplatePlotScaleTypes }); + }} + /> + +
+ + updateTrackConfig({ plots: plots })} + /> + +
+
+ ); +} diff --git a/frontend/src/modules/WellLogViewer/settings/components/TemplateTrackSettings/index.tsx b/frontend/src/modules/WellLogViewer/settings/components/TemplateTrackSettings/index.tsx new file mode 100644 index 000000000..68723c319 --- /dev/null +++ b/frontend/src/modules/WellLogViewer/settings/components/TemplateTrackSettings/index.tsx @@ -0,0 +1,181 @@ +import React from "react"; + +import { SettingsStatusWriter } from "@framework/StatusWriter"; +import { arrayMove } from "@framework/utils/arrays"; +import { Menu } from "@lib/components/Menu"; +import { MenuItem } from "@lib/components/MenuItem"; +// import { MenuItem } from "@lib/components/MenuItem"; +import { SortableList } from "@lib/components/SortableList"; +import { transformToTrackConfigs } from "@modules/WellLogViewer/utils/logViewerTemplate"; +import { Dropdown, MenuButton } from "@mui/base"; +import { FileDownload, FileUpload, MoreVert } from "@mui/icons-material"; + +import { useAtom } from "jotai"; +import { v4 } from "uuid"; + +import { SortableTrackItem } from "./SortableTrackItem"; + +import { TemplateTrackConfig, logViewerTrackConfigs } from "../../atoms/persistedAtoms"; +import { AddItemButton } from "../AddItemButton"; + +interface TemplateTrackSettingsProps { + statusWriter: SettingsStatusWriter; +} + +export function TemplateTrackSettings(props: TemplateTrackSettingsProps): React.ReactNode { + const [trackConfigs, setTrackConfigs] = useAtom(logViewerTrackConfigs); + const jsonImportInputRef = React.useRef(null); + + const handleNewPlotTrack = React.useCallback( + function handleNewPlotTrack() { + const newConfig = createNewConfig(`Plot track #${trackConfigs.length + 1}`); + + setTrackConfigs([...trackConfigs, newConfig]); + }, + [setTrackConfigs, trackConfigs] + ); + + const handleDeleteTrack = React.useCallback( + function handleDeleteTrack(track: TemplateTrackConfig) { + setTrackConfigs(trackConfigs.filter((configs) => configs._id !== track._id)); + }, + [setTrackConfigs, trackConfigs] + ); + + const handleEditTrack = React.useCallback( + function handleEditTrack(updatedItem: TemplateTrackConfig) { + const newConfigs = trackConfigs.map((tc) => (tc._id === updatedItem._id ? updatedItem : tc)); + + setTrackConfigs(newConfigs); + }, + [setTrackConfigs, trackConfigs] + ); + + const handleTrackMove = React.useCallback( + function handleTrackMove( + movedItemId: string, + originId: string | null, + destinationId: string | null, + newPosition: number + ) { + // Skip update if the item was moved above or below itself, as this means no actual move happened + // TODO: This should probably be checked inside SortableList + const currentPosition = trackConfigs.findIndex((cfg) => cfg._id === movedItemId); + if (currentPosition === newPosition || currentPosition + 1 === newPosition) return; + + const newTrackCfg = arrayMove(trackConfigs, currentPosition, newPosition); + + setTrackConfigs(newTrackCfg); + }, + [setTrackConfigs, trackConfigs] + ); + + const encodedConfigJsonUrl = React.useMemo( + function generateConfigJsonDataString() { + if (trackConfigs.length === 0) return null; + + const configJSON = JSON.stringify(trackConfigs); + return `data:text/json;charset=utf-8,${encodeURIComponent(configJSON)}`; + }, + [trackConfigs] + ); + + const handleConfigJsonImport = React.useCallback( + async function readUploadedFile(evt: React.ChangeEvent) { + const file = evt.target.files?.item(0); + + if (!file) return console.warn("No file given"); + if (file.type !== "application/json") return console.warn("Invalid file extension"); + + try { + const fileData = await file.text(); + + const parsedData = JSON.parse(fileData); + const newConfig = transformToTrackConfigs(parsedData); + + setTrackConfigs(newConfig); + } catch (error) { + console.error(error); + console.warn("Invalid JSON content"); + } + }, + [setTrackConfigs] + ); + + return ( +
+
+ + +
Plot Tracks
+ + + + + + + + {/* No idea why this wouldnt play along with Typescript + + Export JSON + + */} + + + + Export JSON + + + jsonImportInputRef.current?.click()}> + Import JSON + + + +
+ + + {trackConfigs.map((config) => ( + + ))} + +
+ ); +} + +function createNewConfig(title: string): TemplateTrackConfig { + return { + _id: v4(), + plots: [], + scale: "linear", + width: 3, + title, + }; +} diff --git a/frontend/src/modules/WellLogViewer/settings/components/ViewerSettings/index.tsx b/frontend/src/modules/WellLogViewer/settings/components/ViewerSettings/index.tsx new file mode 100644 index 000000000..2d2099953 --- /dev/null +++ b/frontend/src/modules/WellLogViewer/settings/components/ViewerSettings/index.tsx @@ -0,0 +1,57 @@ +import React from "react"; + +import { SettingsStatusWriter } from "@framework/StatusWriter"; +import { Checkbox } from "@lib/components/Checkbox"; +import { Label } from "@lib/components/Label"; +import { PendingWrapper } from "@lib/components/PendingWrapper"; +import { usePropagateApiErrorToStatusWriter } from "@modules/_shared/hooks/usePropagateApiErrorToStatusWriter"; + +import { useAtom, useAtomValue } from "jotai"; + +import { userSelectedNonUnitWellpicksAtom, userSelectedUnitWellpicksAtom } from "../../atoms/baseAtoms"; +import { padDataWithEmptyRowsAtom, viewerHorizontalAtom } from "../../atoms/persistedAtoms"; +import { wellborePicksAndStratigraphyQueryAtom } from "../../atoms/queryAtoms"; +import { WellpickSelect } from "../WellpickSelect"; + +export type ViewerSettingsProps = { + statusWriter: SettingsStatusWriter; +}; + +export function ViewerSettings(props: ViewerSettingsProps): React.ReactNode { + // Well log selection + const [horizontal, setHorizontal] = useAtom(viewerHorizontalAtom); + const [padWithEmptyRows, setPadWithEmptyRows] = useAtom(padDataWithEmptyRowsAtom); + + // Wellpick selection + const borePicksAndStratQuery = useAtomValue(wellborePicksAndStratigraphyQueryAtom); + const availableWellPicks = borePicksAndStratQuery.data ?? { nonUnitPicks: [], unitPicks: [] }; + const wellpickErrorMsg = usePropagateApiErrorToStatusWriter(borePicksAndStratQuery, props.statusWriter) ?? ""; + + const [selectedNonUnitPicks, setSelectedNonUnitPicks] = useAtom(userSelectedNonUnitWellpicksAtom); + const [selectedUnitPicks, setSelectedUnitPicks] = useAtom(userSelectedUnitWellpicksAtom); + + return ( +
+ {/* TODO: Other settings, like, color, max cols, etc */} + + + + + +
+ ); +} diff --git a/frontend/src/modules/WellLogViewer/settings/components/WellpickSelect.tsx b/frontend/src/modules/WellLogViewer/settings/components/WellpickSelect.tsx new file mode 100644 index 000000000..d2f0d083c --- /dev/null +++ b/frontend/src/modules/WellLogViewer/settings/components/WellpickSelect.tsx @@ -0,0 +1,92 @@ +import React from "react"; + +import { Select, SelectOption, SelectProps } from "@lib/components/Select"; +import { WellPicksLayerData } from "@modules/Intersection/utils/layers/WellpicksLayer"; + +import _ from "lodash"; + +export type WellpickSelectProps = { + availableWellpicks: WellPicksLayerData; + selectedUnitPicks: string[]; + selectedNonUnitPicks: string[]; + onNonUnitPicksChange?: (value: string[]) => void; + onUnitPicksChange?: (value: string[]) => void; +} & Omit; + +export function WellpickSelect(props: WellpickSelectProps): React.ReactNode { + const { onNonUnitPicksChange, onUnitPicksChange } = props; + const groupedOptions = createWellpickOptions(props.availableWellpicks); + + const handleChangeUnitPicks = React.useCallback( + function handleChangeUnitPicks(value: string[]) { + if (!onUnitPicksChange) return; + + return _.isEqual(value, props.selectedUnitPicks) ? onUnitPicksChange([]) : onUnitPicksChange(value); + }, + [onUnitPicksChange, props.selectedUnitPicks] + ); + + const handleChangeNonUnitPicks = React.useCallback( + function handleChangeNonUnitPicks(value: string[]) { + if (!onNonUnitPicksChange) return; + + return _.isEqual(value, props.selectedNonUnitPicks) + ? onNonUnitPicksChange([]) + : onNonUnitPicksChange(value); + }, + [onNonUnitPicksChange, props.selectedNonUnitPicks] + ); + + return ( +
+ Unit picks + + +
+ ); +} + +type UnitPicks = WellPicksLayerData["unitPicks"]; +type NonUnitPicks = WellPicksLayerData["nonUnitPicks"]; + +type WellpickOptions = { + unitPicks: SelectOption[]; + nonUnitPicks: SelectOption[]; +}; + +function createWellpickOptions(groupedWellpicks: WellPicksLayerData): WellpickOptions { + return { + unitPicks: unitPickToSelectOptions(groupedWellpicks.unitPicks), + nonUnitPicks: nonUnitPickToSelectOptions(groupedWellpicks.nonUnitPicks), + }; +} + +function nonUnitPickToSelectOptions(picks: NonUnitPicks): SelectOption[] { + return picks.map((pick) => ({ + label: pick.identifier, + value: pick.identifier, + })); +} + +function unitPickToSelectOptions(picks: UnitPicks): SelectOption[] { + return picks.map((pick) => ({ + label: pick.name, + value: pick.name, + })); +} diff --git a/frontend/src/modules/WellLogViewer/settings/settings.tsx b/frontend/src/modules/WellLogViewer/settings/settings.tsx new file mode 100644 index 000000000..cc3826d40 --- /dev/null +++ b/frontend/src/modules/WellLogViewer/settings/settings.tsx @@ -0,0 +1,120 @@ +import React from "react"; + +import { WellboreHeader_api } from "@api"; +import { ModuleSettingsProps } from "@framework/Module"; +import { useSettingsStatusWriter } from "@framework/StatusWriter"; +import { SyncSettingKey, SyncSettingsHelper } from "@framework/SyncSettings"; +import { useEnsembleSet } from "@framework/WorkbenchSession"; +import { FieldDropdown } from "@framework/components/FieldDropdown"; +import { IntersectionType } from "@framework/types/intersection"; +import { CollapsibleGroup } from "@lib/components/CollapsibleGroup"; +import { Label } from "@lib/components/Label"; +import { PendingWrapper } from "@lib/components/PendingWrapper"; +import { Select, SelectOption } from "@lib/components/Select"; +import { usePropagateApiErrorToStatusWriter } from "@modules/_shared/hooks/usePropagateApiErrorToStatusWriter"; + +import { useAtomValue, useSetAtom } from "jotai"; + +import { userSelectedFieldIdentifierAtom, userSelectedWellboreUuidAtom } from "./atoms/baseAtoms"; +import { selectedFieldIdentifierAtom, selectedWellboreAtom } from "./atoms/derivedAtoms"; +import { drilledWellboreHeadersQueryAtom } from "./atoms/queryAtoms"; +import { TemplateTrackSettings } from "./components/TemplateTrackSettings"; +import { ViewerSettings } from "./components/ViewerSettings"; + +import { InterfaceTypes } from "../interfaces"; +import { useTrackedGlobalValue } from "../utils/hooks"; + +function useSyncedWellboreSetting( + syncHelper: SyncSettingsHelper +): [typeof selectedWellboreHeader, typeof setSelectedWellboreHeader] { + const localSetSelectedWellboreHeader = useSetAtom(userSelectedWellboreUuidAtom); + // Global syncronization + const globalIntersection = syncHelper.useValue(SyncSettingKey.INTERSECTION, "global.syncValue.intersection"); + useTrackedGlobalValue(globalIntersection, () => { + if (globalIntersection?.type === IntersectionType.WELLBORE) { + localSetSelectedWellboreHeader(globalIntersection.uuid); + } + }); + + function setSelectedWellboreHeader(wellboreUuid: string | null) { + localSetSelectedWellboreHeader(wellboreUuid); + + syncHelper.publishValue(SyncSettingKey.INTERSECTION, "global.syncValue.intersection", { + type: IntersectionType.WELLBORE, + uuid: wellboreUuid ?? "", + }); + } + // Leave AFTER checking global, othwise the select menu will highlight the wrong value + const selectedWellboreHeader = useAtomValue(selectedWellboreAtom); + + return [selectedWellboreHeader, setSelectedWellboreHeader]; +} + +export function Settings(props: ModuleSettingsProps) { + // Utilities + const syncedSettingKeys = props.settingsContext.useSyncedSettingKeys(); + const syncHelper = new SyncSettingsHelper(syncedSettingKeys, props.workbenchServices); + + // Ensemble selections + const ensembleSet = useEnsembleSet(props.workbenchSession); + + const selectedField = useAtomValue(selectedFieldIdentifierAtom); + const setSelectedField = useSetAtom(userSelectedFieldIdentifierAtom); + + // Wellbore selection + const wellboreHeaders = useAtomValue(drilledWellboreHeadersQueryAtom); + const [selectedWellboreHeader, setSelectedWellboreHeader] = useSyncedWellboreSetting(syncHelper); + + const handleWellboreSelectionChange = React.useCallback( + function handleWellboreSelectionChange(uuids: string[]) { + setSelectedWellboreHeader(uuids[0] ?? null); + }, + [setSelectedWellboreHeader] + ); + + // Error messages + const statusWriter = useSettingsStatusWriter(props.settingsContext); + const wellboreHeadersErrorStatus = usePropagateApiErrorToStatusWriter(wellboreHeaders, statusWriter) ?? ""; + + return ( +
+ + + +