diff --git a/backend_py/primary/primary/routers/well/router.py b/backend_py/primary/primary/routers/well/router.py index b45361e42..9d8e6c415 100644 --- a/backend_py/primary/primary/routers/well/router.py +++ b/backend_py/primary/primary/routers/well/router.py @@ -27,14 +27,11 @@ async def get_drilled_wellbore_headers( # fmt:off authenticated_user: AuthenticatedUser = Depends(AuthHelper.get_authenticated_user), - case_uuid: str = Query(description="Sumo case uuid"), + field_identifier: str = Query(description="Sumo field identifier"), # Should be field identifier # fmt:on ) -> List[schemas.WellboreHeader]: """Get wellbore headers for all wells in the field""" - - case_inspector = CaseInspector.from_case_uuid(authenticated_user.get_sumo_access_token(), case_uuid) - field_identifier = (await case_inspector.get_field_identifiers_async())[0] well_access: Union[SmdaWellAccess, MockedSmdaWellAccess] if field_identifier == "DROGON": # Handle DROGON @@ -51,13 +48,11 @@ async def get_drilled_wellbore_headers( async def get_field_well_trajectories( # fmt:off authenticated_user: AuthenticatedUser = Depends(AuthHelper.get_authenticated_user), - case_uuid: str = Query(description="Sumo case uuid"), # Should be field identifier? + field_identifier: str = Query(description="Sumo field identifier"), unique_wellbore_identifiers:List[str] = Query(None, description="Optional subset of well names") # fmt:on ) -> List[schemas.WellboreTrajectory]: """Get well trajectories for field""" - case_inspector = CaseInspector.from_case_uuid(authenticated_user.get_sumo_access_token(), case_uuid) - field_identifier = (await case_inspector.get_field_identifiers_async())[0] well_access: Union[SmdaWellAccess, MockedSmdaWellAccess] if field_identifier == "DROGON": # Handle DROGON diff --git a/frontend/src/api/services/WellService.ts b/frontend/src/api/services/WellService.ts index b3be5892c..d61482d61 100644 --- a/frontend/src/api/services/WellService.ts +++ b/frontend/src/api/services/WellService.ts @@ -17,18 +17,18 @@ export class WellService { /** * Get Drilled Wellbore Headers * Get wellbore headers for all wells in the field - * @param caseUuid Sumo case uuid + * @param fieldIdentifier Sumo field identifier * @returns WellboreHeader Successful Response * @throws ApiError */ public getDrilledWellboreHeaders( - caseUuid: string, + fieldIdentifier: string, ): CancelablePromise> { return this.httpRequest.request({ method: 'GET', url: '/well/drilled_wellbore_headers/', query: { - 'case_uuid': caseUuid, + 'field_identifier': fieldIdentifier, }, errors: { 422: `Validation Error`, @@ -38,20 +38,20 @@ export class WellService { /** * Get Field Well Trajectories * Get well trajectories for field - * @param caseUuid Sumo case uuid + * @param fieldIdentifier Sumo field identifier * @param uniqueWellboreIdentifiers Optional subset of well names * @returns WellboreTrajectory Successful Response * @throws ApiError */ public getFieldWellTrajectories( - caseUuid: string, + fieldIdentifier: string, uniqueWellboreIdentifiers?: Array, ): CancelablePromise> { return this.httpRequest.request({ method: 'GET', url: '/well/field_well_trajectories/', query: { - 'case_uuid': caseUuid, + 'field_identifier': fieldIdentifier, 'unique_wellbore_identifiers': uniqueWellboreIdentifiers, }, errors: { diff --git a/frontend/src/framework/components/EsvIntersection/esvIntersection.tsx b/frontend/src/framework/components/EsvIntersection/esvIntersection.tsx index c9dee240a..9f7d0ce56 100644 --- a/frontend/src/framework/components/EsvIntersection/esvIntersection.tsx +++ b/frontend/src/framework/components/EsvIntersection/esvIntersection.tsx @@ -213,7 +213,6 @@ function isPixiLayer(layer: Layer): boolean { } export function EsvIntersection(props: EsvIntersectionProps): React.ReactNode { - console.debug("esv intersection render"); const { onReadout, onViewportChange } = props; const [prevAxesOptions, setPrevAxesOptions] = React.useState(undefined); diff --git a/frontend/src/framework/components/EsvIntersection/types/types.ts b/frontend/src/framework/components/EsvIntersection/types/types.ts index 104d40f19..4708187b6 100644 --- a/frontend/src/framework/components/EsvIntersection/types/types.ts +++ b/frontend/src/framework/components/EsvIntersection/types/types.ts @@ -130,7 +130,7 @@ export type LayerDataItem = { intersectionItem: IntersectionItem; }; -export enum AdditionalInformationKey { +export enum AdditionalInformationType { GLOBAL_POLYGON_INDEX = "polygon-index", IJK = "ijk", PROP_VALUE = "prop-value", @@ -147,36 +147,26 @@ export enum AdditionalInformationKey { R = "r", G = "g", B = "b", - LABEL = "label", + POI = "poi", } -export type PropValue = { - name: string; - unit: string; - value: number; +export type LineStyle = { + color: string; + alpha?: number; + dashSegments?: number[]; }; -export type SchematicInfo = { - label: string; - value: string | number; -}[]; +export type AreaStyle = { + fillColor: string; + alpha?: number; + strokeStyle?: LineStyle; +}; -export type AdditionalInformation = { - [AdditionalInformationKey.B]?: number; - [AdditionalInformationKey.G]?: number; - [AdditionalInformationKey.R]?: number; - [AdditionalInformationKey.X]?: number; - [AdditionalInformationKey.Y]?: number; - [AdditionalInformationKey.MEAN]?: number; - [AdditionalInformationKey.MIN]?: number; - [AdditionalInformationKey.MAX]?: number; - [AdditionalInformationKey.P10]?: number; - [AdditionalInformationKey.P90]?: number; - [AdditionalInformationKey.P50]?: number; - [AdditionalInformationKey.GLOBAL_POLYGON_INDEX]?: number; - [AdditionalInformationKey.IJK]?: [number, number, number]; - [AdditionalInformationKey.PROP_VALUE]?: PropValue; - [AdditionalInformationKey.MD]?: number; - [AdditionalInformationKey.LABEL]?: string; - [AdditionalInformationKey.SCHEMATIC_INFO]?: SchematicInfo; +export type AdditionalInformationItem = { + type: AdditionalInformationType; + label: string; + value: number | string | [number, number] | [number, number, number] | boolean; + lineStyle?: LineStyle; + areaStyle?: AreaStyle; + unit?: string; }; diff --git a/frontend/src/framework/components/EsvIntersection/utilityComponents/ReadoutBox.tsx b/frontend/src/framework/components/EsvIntersection/utilityComponents/ReadoutBox.tsx index 956d58779..ff3e07ef7 100644 --- a/frontend/src/framework/components/EsvIntersection/utilityComponents/ReadoutBox.tsx +++ b/frontend/src/framework/components/EsvIntersection/utilityComponents/ReadoutBox.tsx @@ -2,9 +2,9 @@ import React from "react"; import { Layer } from "@equinor/esv-intersection"; -import { AdditionalInformationKey, PropValue, ReadoutItem, SchematicInfo } from "../types"; +import { AdditionalInformationItem, AdditionalInformationType, ReadoutItem } from "../types"; import { getColorFromLayerData } from "../utils/intersectionConversion"; -import { getAdditionalInformationFromReadoutItem, getLabelFromLayerData } from "../utils/readoutItemUtils"; +import { getAdditionalInformationItemsFromReadoutItem, getLabelFromLayerData } from "../utils/readoutItemUtils"; export type ReadoutBoxProps = { readoutItems: ReadoutItem[]; @@ -12,130 +12,116 @@ export type ReadoutBoxProps = { makeLabelFromLayer?: (layer: Layer) => string | null; }; -function additionalInformationItemToReadableString( - key: string, - value: unknown -): { label: string; value: string } | { label: string; value: string }[] | null { - if (key === AdditionalInformationKey.IJK) { - return { - label: "IJK", - value: `${(value as [number, number, number])[0].toFixed(0)}, ${( - value as [number, number, number] - )[1].toFixed(0)}, ${(value as [number, number, number])[2].toFixed(0)}`, - }; - } - if (key === AdditionalInformationKey.PROP_VALUE) { - const propValue = value as PropValue; - return { - label: propValue.name, - value: `${propValue.value.toFixed(2)} ${propValue.unit}`, - }; - } - if (key === AdditionalInformationKey.MD) { - return { - label: "MD", - value: `${(value as number).toFixed(2)} m`, - }; - } - if (key === AdditionalInformationKey.MAX) { - return { - label: "Max", - value: `${(value as number).toFixed(2)}`, - }; - } - if (key === AdditionalInformationKey.MIN) { - return { - label: "Min", - value: `${(value as number).toFixed(2)}`, - }; - } - if (key === AdditionalInformationKey.P10) { - return { - label: "P10", - value: `${(value as number).toFixed(2)}`, - }; - } - if (key === AdditionalInformationKey.P90) { - return { - label: "P90", - value: `${(value as number).toFixed(2)}`, - }; +type InfoItem = { + adornment: React.ReactNode; + label: React.ReactNode; + value: string; + unit?: string; +}; + +function formatValue(value: number | string): string { + if (typeof value === "number") { + return (+value.toFixed(2)).toString(); } - if (key === AdditionalInformationKey.P50) { - return { - label: "P50", - value: `${(value as number).toFixed(2)}`, - }; + return value.toString(); +} + +function makeAdornment(item: AdditionalInformationItem): React.ReactNode { + if (item.lineStyle) { + return ( + + + + ); } - if (key === AdditionalInformationKey.MEAN) { - return { - label: "Mean", - value: `${(value as number).toFixed(2)}`, - }; + + if (item.areaStyle) { + return ( + + + + ); } - if (key === AdditionalInformationKey.SCHEMATIC_INFO) { - const schematicInfo = value as SchematicInfo; - const info: { label: string; value: string }[] = []; - - for (const el of schematicInfo) { - let val = el.value; - if (typeof val === "number") { - val = val.toFixed(2); - } - info.push({ - label: el.label, - value: val as string, - }); + + return null; +} + +function convertAdditionalInformationItemToInfoItem(item: AdditionalInformationItem): InfoItem { + let formattedValue: string = ""; + if (item.value instanceof Array) { + if (item.value.length === 3) { + formattedValue = item.value.map((el) => formatValue(el)).join(", "); + } else { + formattedValue = item.value.map((el) => formatValue(el)).join(" - "); } - return info; - } - if (key === AdditionalInformationKey.X) { - return { - label: "X", - value: `${(value as number).toFixed(2)} m`, - }; } - if (key === AdditionalInformationKey.Y) { - return { - label: "Y", - value: `${(value as number).toFixed(2)} m`, - }; + if (typeof item.value === "number" || typeof item.value === "string") { + formattedValue = formatValue(item.value); } - return null; -} -function makeAdditionalInformation(item: ReadoutItem): { label: string; value: string }[] { - const additionalInformation = getAdditionalInformationFromReadoutItem(item); - return Object.entries(additionalInformation) - .map(([key, value]) => { - return additionalInformationItemToReadableString(key, value); - }) - .filter((el): el is { label: string; value: string } => el !== null); + return { + label: item.label, + value: formattedValue, + unit: item.unit, + adornment: makeAdornment(item), + }; } -function convertAdditionalInformationToHtml(items: { label: string; value: string }[]): React.ReactNode { - function formatValue(value: number | string): string { - if (typeof value === "number") { - return value.toFixed(2); - } - return value.toString(); - } +function makeAdditionalInformation(item: ReadoutItem): InfoItem[] { + const additionalInformation = getAdditionalInformationItemsFromReadoutItem(item); + return additionalInformation + .filter((el) => !(el.type === AdditionalInformationType.SCHEMATIC_INFO && el.label === "ID")) + .map((el) => { + return convertAdditionalInformationItemToInfoItem(el); + }); +} + +function convertAdditionalInformationToHtml(items: InfoItem[]): React.ReactNode { return items.map((el, index) => { - if (Array.isArray(el)) { - return el.map((subEl, subIndex) => { - return ( -
-
{subEl.label}:
-
{formatValue(subEl.value)}
-
- ); - }); - } return (
-
{el.label}:
-
{formatValue(el.value)}
+
{el.adornment}
+
{el.label}:
+
{el.value}
+ {el.unit &&
{el.unit}
}
); }); diff --git a/frontend/src/framework/components/EsvIntersection/utils/readoutItemUtils.tsx b/frontend/src/framework/components/EsvIntersection/utils/readoutItemUtils.tsx index f3667c322..61c3af64c 100644 --- a/frontend/src/framework/components/EsvIntersection/utils/readoutItemUtils.tsx +++ b/frontend/src/framework/components/EsvIntersection/utils/readoutItemUtils.tsx @@ -12,7 +12,7 @@ import { isWellborepathLayer, } from "./layers"; -import { AdditionalInformation, AdditionalInformationKey, ReadoutItem } from "../types/types"; +import { AdditionalInformationItem, AdditionalInformationType, ReadoutItem } from "../types/types"; export function getLabelFromLayerData(readoutItem: ReadoutItem): string { const layer = readoutItem.layer; @@ -114,8 +114,8 @@ export function makeSchematicInfo return arr; } -export function getAdditionalInformationFromReadoutItem(readoutItem: ReadoutItem): AdditionalInformation { - const infoObject: AdditionalInformation = {}; +export function getAdditionalInformationItemsFromReadoutItem(readoutItem: ReadoutItem): AdditionalInformationItem[] { + const items: AdditionalInformationItem[] = []; const layer = readoutItem.layer; if (isPolylineIntersectionLayer(layer) && layer.data) { @@ -123,76 +123,121 @@ export function getAdditionalInformationFromReadoutItem(readoutItem: ReadoutItem const cellIndexOffset = layer.data.fenceMeshSections .slice(0, readoutItem.index) .reduce((acc, section) => acc + section.polySourceCellIndicesArr.length, 0); - infoObject[AdditionalInformationKey.GLOBAL_POLYGON_INDEX] = cellIndexOffset + readoutItem.polygonIndex; + + items.push({ + label: "Global polygon index", + type: AdditionalInformationType.GLOBAL_POLYGON_INDEX, + value: cellIndexOffset + readoutItem.polygonIndex, + }); + const cellIndex = layer.data.fenceMeshSections[readoutItem.index].polySourceCellIndicesArr[readoutItem.polygonIndex]; - infoObject[AdditionalInformationKey.IJK] = ijkFromCellIndex( - cellIndex, - layer.data.gridDimensions.cellCountI, - layer.data.gridDimensions.cellCountJ - ); + items.push({ + label: "IJK", + type: AdditionalInformationType.IJK, + value: ijkFromCellIndex( + cellIndex, + layer.data.gridDimensions.cellCountI, + layer.data.gridDimensions.cellCountJ + ), + }); const propValue = layer.data.fenceMeshSections[readoutItem.index].polyPropsArr[readoutItem.polygonIndex]; - infoObject[AdditionalInformationKey.PROP_VALUE] = { - name: layer.data.propertyName, - unit: layer.data.propertyUnit, + items.push({ + label: layer.data.propertyName, + type: AdditionalInformationType.PROP_VALUE, value: propValue, - }; + unit: layer.data.propertyUnit, + }); } } if (isWellborepathLayer(layer)) { - infoObject[AdditionalInformationKey.MD] = readoutItem.md ?? undefined; + if (readoutItem.md) { + items.push({ + label: "MD", + type: AdditionalInformationType.MD, + value: readoutItem.md, + unit: "m", + }); + } } if (isStatisticalFanchartsCanvasLayer(layer) && layer.data) { const fanchart = layer.data.fancharts[readoutItem.index]; if (fanchart && readoutItem.points) { - const keys = Object.keys(fanchart.data).filter((el) => { - if (el === "mean") { - return fanchart.visibility?.mean ?? true; - } - if (el === "min") { - return fanchart.visibility?.minMax ?? true; - } - if (el === "max") { - return fanchart.visibility?.minMax ?? true; - } - if (el === "p10") { - return fanchart.visibility?.p10p90 ?? true; - } - if (el === "p90") { - return fanchart.visibility?.p10p90 ?? true; - } - if (el === "p50") { - return fanchart.visibility?.p50 ?? true; - } - return false; - }); + if (fanchart.visibility?.mean ?? true) { + items.push({ + label: "Mean", + type: AdditionalInformationType.MEAN, + value: readoutItem.points[0][1], + unit: "m", + lineStyle: { + color: fanchart.color ?? "black", + }, + }); + } - for (const [index, point] of readoutItem.points.entries()) { - const key = keys[index] as keyof AdditionalInformation; - switch (key) { - case "mean": - infoObject[AdditionalInformationKey.MEAN] = point[1]; - break; - case "min": - infoObject[AdditionalInformationKey.MIN] = point[1]; - break; - case "max": - infoObject[AdditionalInformationKey.MAX] = point[1]; - break; - case "p10": - infoObject[AdditionalInformationKey.P10] = point[1]; - break; - case "p90": - infoObject[AdditionalInformationKey.P90] = point[1]; - break; - case "p50": - infoObject[AdditionalInformationKey.P50] = point[1]; - break; - } + if (fanchart.visibility?.p50 ?? true) { + items.push({ + label: "P50", + type: AdditionalInformationType.P50, + value: readoutItem.points[1][1], + unit: "m", + lineStyle: { + color: fanchart.color ?? "black", + dashSegments: [1, 1, 5, 1], + }, + }); + } + + if (fanchart.visibility?.minMax ?? true) { + items.push({ + label: "Min", + type: AdditionalInformationType.MIN, + value: readoutItem.points[2][1], + unit: "m", + areaStyle: { + fillColor: fanchart.color ?? "black", + alpha: 0.2, + }, + }); + + items.push({ + label: "Max", + type: AdditionalInformationType.MAX, + value: readoutItem.points[3][1], + unit: "m", + areaStyle: { + fillColor: fanchart.color ?? "black", + alpha: 0.2, + }, + }); + } + + if (fanchart.visibility?.p10p90 ?? true) { + items.push({ + label: "P10", + type: AdditionalInformationType.P10, + value: readoutItem.points[4][1], + unit: "m", + areaStyle: { + fillColor: fanchart.color ?? "black", + alpha: 0.6, + }, + }); + + items.push({ + label: "P90", + type: AdditionalInformationType.P90, + value: readoutItem.points[5][1], + unit: "m", + areaStyle: { + fillColor: fanchart.color ?? "black", + alpha: 0.6, + }, + }); } } } @@ -200,8 +245,16 @@ export function getAdditionalInformationFromReadoutItem(readoutItem: ReadoutItem if (isCalloutCanvasLayer(layer) && layer.data) { const md = layer.data[readoutItem.index].md; if (md) { - infoObject[AdditionalInformationKey.LABEL] = layer.data[readoutItem.index].label; - infoObject[AdditionalInformationKey.MD] = md; + items.push({ + label: "MD", + type: AdditionalInformationType.MD, + value: md, + }); + items.push({ + label: "Wellpick", + type: AdditionalInformationType.POI, + value: layer.data[readoutItem.index].label, + }); } } @@ -209,15 +262,125 @@ export function getAdditionalInformationFromReadoutItem(readoutItem: ReadoutItem if (layer.data) { const schematicType = readoutItem.schematicType; if (schematicType && layer.data[schematicType] && schematicType !== "symbols") { - infoObject[AdditionalInformationKey.SCHEMATIC_INFO] = makeSchematicInfo( - schematicType, - layer.data[schematicType][readoutItem.index] - ); + const item = layer.data[schematicType][readoutItem.index]; + if (schematicType === "casings") { + const casing = item as Casing; + items.push({ label: "ID", type: AdditionalInformationType.SCHEMATIC_INFO, value: casing.id }); + items.push({ + label: "Diameter", + type: AdditionalInformationType.SCHEMATIC_INFO, + value: casing.diameter, + unit: "m", + }); + items.push({ + label: "Inner diameter", + type: AdditionalInformationType.SCHEMATIC_INFO, + value: casing.innerDiameter, + unit: "m", + }); + items.push({ + label: "Has shoe", + type: AdditionalInformationType.SCHEMATIC_INFO, + value: casing.hasShoe, + }); + items.push({ + label: "MD range", + type: AdditionalInformationType.SCHEMATIC_INFO, + value: [casing.start, casing.end], + unit: "m", + }); + } else if (schematicType === "cements") { + const cement = item as Cement; + items.push({ label: "ID", type: AdditionalInformationType.SCHEMATIC_INFO, value: cement.id }); + items.push({ label: "TOC", type: AdditionalInformationType.SCHEMATIC_INFO, value: cement.toc }); // Unit? + } else if (schematicType === "completion") { + const completion = item as Completion; + items.push({ label: "ID", type: AdditionalInformationType.SCHEMATIC_INFO, value: completion.id }); + items.push({ + label: "Kind", + type: AdditionalInformationType.SCHEMATIC_INFO, + value: completion.kind, + }); + items.push({ + label: "Diameter", + type: AdditionalInformationType.SCHEMATIC_INFO, + value: completion.diameter, + unit: "m", + }); + items.push({ + label: "MD range", + type: AdditionalInformationType.SCHEMATIC_INFO, + value: [completion.start, completion.end], + unit: "m", + }); + } else if (schematicType === "holeSizes") { + const holeSize = item as HoleSize; + items.push({ label: "ID", type: AdditionalInformationType.SCHEMATIC_INFO, value: holeSize.id }); + items.push({ + label: "Diameter", + type: AdditionalInformationType.SCHEMATIC_INFO, + value: holeSize.diameter, + unit: "m", + }); + items.push({ + label: "MD range", + type: AdditionalInformationType.SCHEMATIC_INFO, + value: [holeSize.start, holeSize.end], + unit: "m", + }); + } else if (schematicType === "pAndA") { + const pAndA = item as PAndA; + items.push({ label: "ID", type: AdditionalInformationType.SCHEMATIC_INFO, value: pAndA.id }); + items.push({ label: "Kind", type: AdditionalInformationType.SCHEMATIC_INFO, value: pAndA.kind }); + if (pAndA.kind === "pAndASymbol") { + items.push({ + label: "Diameter", + type: AdditionalInformationType.SCHEMATIC_INFO, + value: pAndA.diameter, + unit: "m", + }); + } + items.push({ + label: "MD range", + type: AdditionalInformationType.SCHEMATIC_INFO, + value: [pAndA.start, pAndA.end], + unit: "m", + }); + } else if (schematicType === "perforations") { + const perforation = item as Perforation; + items.push({ label: "ID", type: AdditionalInformationType.SCHEMATIC_INFO, value: perforation.id }); + items.push({ + label: "Open", + type: AdditionalInformationType.SCHEMATIC_INFO, + value: perforation.isOpen, + }); + items.push({ + label: "Subkind", + type: AdditionalInformationType.SCHEMATIC_INFO, + value: perforation.subKind, + }); + items.push({ + label: "MD range", + type: AdditionalInformationType.SCHEMATIC_INFO, + value: [perforation.start, perforation.end], + unit: "m", + }); + } } } } else { - infoObject[AdditionalInformationKey.X] = readoutItem.point[0]; - infoObject[AdditionalInformationKey.Y] = readoutItem.point[1]; + items.push({ + label: "X", + type: AdditionalInformationType.X, + value: readoutItem.point[0], + unit: "m", + }); + items.push({ + label: "Y", + type: AdditionalInformationType.Y, + value: readoutItem.point[1], + unit: "m", + }); } if (isSeismicCanvasLayer(layer)) { @@ -233,9 +396,23 @@ export function getAdditionalInformationFromReadoutItem(readoutItem: ReadoutItem const imageY = transformedPoint.y; const imageData = ctx.getImageData(imageX, imageY, 1, 1); - infoObject[AdditionalInformationKey.R] = imageData.data[0]; - infoObject[AdditionalInformationKey.G] = imageData.data[1]; - infoObject[AdditionalInformationKey.B] = imageData.data[2]; + items.push({ + label: "R", + type: AdditionalInformationType.R, + value: imageData.data[0], + }); + + items.push({ + label: "G", + type: AdditionalInformationType.G, + value: imageData.data[1], + }); + + items.push({ + label: "B", + type: AdditionalInformationType.B, + value: imageData.data[2], + }); } } } @@ -257,13 +434,14 @@ export function getAdditionalInformationFromReadoutItem(readoutItem: ReadoutItem const index = traceNum * seismicData.numSamplesPerTrace + sampleNum; const value = seismicData.fenceTracesFloat32Array[index]; - infoObject[AdditionalInformationKey.PROP_VALUE] = { - name: seismicData.propertyName, + items.push({ + label: seismicData.propertyName, + type: AdditionalInformationType.PROP_VALUE, + value: value, unit: seismicData.propertyUnit, - value, - }; + }); } } - return infoObject; + return items; } diff --git a/frontend/src/framework/components/EsvIntersection/utils/surfaceStatisticalFancharts.ts b/frontend/src/framework/components/EsvIntersection/utils/surfaceStatisticalFancharts.ts index 5e8b5ea0d..9b640d913 100644 --- a/frontend/src/framework/components/EsvIntersection/utils/surfaceStatisticalFancharts.ts +++ b/frontend/src/framework/components/EsvIntersection/utils/surfaceStatisticalFancharts.ts @@ -26,7 +26,7 @@ export function makeSurfaceStatisticalFanchartFromRealizationSurface( realizationSamplePoints: number[][], cumulatedLength: number[], surfaceName: string, - stratColorMap: StratigraphyColorMap, + color: string, visibility?: { mean: boolean; minMax: boolean; @@ -36,12 +36,12 @@ export function makeSurfaceStatisticalFanchartFromRealizationSurface( ): SurfaceStatisticalFanchart { const numPoints = realizationSamplePoints[0]?.length || 0; - const mean = new Array(numPoints).fill(0); + const mean = new Array(numPoints).fill(undefined); const min = new Array(numPoints).fill(Infinity); const max = new Array(numPoints).fill(-Infinity); - const p10 = new Array(numPoints).fill(0); - const p50 = new Array(numPoints).fill(0); - const p90 = new Array(numPoints).fill(0); + const p10 = new Array(numPoints).fill(undefined); + const p50 = new Array(numPoints).fill(undefined); + const p90 = new Array(numPoints).fill(undefined); for (let i = 0; i < numPoints; i++) { const values = realizationSamplePoints.map((el) => el[i]); @@ -56,8 +56,6 @@ export function makeSurfaceStatisticalFanchartFromRealizationSurface( p90[i] = calcPercentile(values, 90); } - const color = stratColorMap[surfaceName] || "black"; - return { color, label: surfaceName, diff --git a/frontend/src/framework/components/FieldDropdown/fieldDropdown.tsx b/frontend/src/framework/components/FieldDropdown/fieldDropdown.tsx new file mode 100644 index 000000000..922387576 --- /dev/null +++ b/frontend/src/framework/components/FieldDropdown/fieldDropdown.tsx @@ -0,0 +1,30 @@ +import { EnsembleSet } from "@framework/EnsembleSet"; +import { Dropdown, DropdownOption, DropdownProps } from "@lib/components/Dropdown"; + +type FieldDropdownProps = { + ensembleSet: EnsembleSet; + value: string | null; + onChange: (fieldIdentifier: string | null) => void; +} & Omit; + +export function FieldDropdown(props: FieldDropdownProps): JSX.Element { + const { ensembleSet, value, onChange, ...rest } = props; + + function handleSelectionChanged(fieldIdentifier: string) { + onChange(fieldIdentifier); + } + + const optionsArr: DropdownOption[] = []; + for (const ens of ensembleSet.getEnsembleArr()) { + const fieldIdentifier = ens.getFieldIdentifier(); + if (optionsArr.some((option) => option.value === fieldIdentifier.toString())) { + continue; + } + optionsArr.push({ + value: fieldIdentifier.toString(), + label: fieldIdentifier.toString(), + }); + } + + return ; +} diff --git a/frontend/src/framework/components/FieldDropdown/index.ts b/frontend/src/framework/components/FieldDropdown/index.ts new file mode 100644 index 000000000..f4ed4a0b0 --- /dev/null +++ b/frontend/src/framework/components/FieldDropdown/index.ts @@ -0,0 +1 @@ +export { FieldDropdown } from "./fieldDropdown"; diff --git a/frontend/src/lib/components/Input/input.tsx b/frontend/src/lib/components/Input/input.tsx index dc34820c2..648a4ce3f 100644 --- a/frontend/src/lib/components/Input/input.tsx +++ b/frontend/src/lib/components/Input/input.tsx @@ -11,10 +11,19 @@ export type InputProps = InputUnstyledProps & { max?: number; rounded?: "all" | "left" | "right" | "none"; debounceTimeMs?: number; + onValueChange?: (value: string) => void; }; export const Input = React.forwardRef((props: InputProps, ref: React.ForwardedRef) => { - const { startAdornment, endAdornment, wrapperStyle, value: propsValue, onChange, debounceTimeMs, ...other } = props; + const { + startAdornment, + endAdornment, + wrapperStyle, + value: propsValue, + onValueChange, + debounceTimeMs, + ...other + } = props; const [value, setValue] = React.useState(propsValue); const [prevValue, setPrevValue] = React.useState(propsValue); @@ -49,50 +58,57 @@ export const Input = React.forwardRef((props: InputProps, ref: React.ForwardedRe event.stopPropagation(); }, []); - const handleInputChange = React.useCallback( - function handleInputChange(event: React.ChangeEvent) { - if (props.type === "number") { - let newValue = 0; - if (!isNaN(parseFloat(event.target.value))) { - newValue = parseFloat(event.target.value || "0"); - if (props.min !== undefined) { - newValue = Math.max(props.min, newValue); - } + function handleKeyUp(event: React.KeyboardEvent) { + if (event.key === "Enter") { + handleInputEditingDone(); + } + } - if (props.max !== undefined) { - newValue = Math.min(props.max, newValue); - } - } else { - setValue(event.target.value); - return; - } + function handleInputEditingDone() { + let adjustedValue: unknown = value; + if (props.type === "number") { + let newValue = 0; - setValue(newValue); + if (!isNaN(parseFloat(value as string))) { + newValue = parseFloat((value as string) || "0"); + if (props.min !== undefined) { + newValue = Math.max(props.min, newValue); + } - event.target.value = newValue.toString(); - } else { - setValue(event.target.value); + if (props.max !== undefined) { + newValue = Math.min(props.max, newValue); + } } - if (!onChange) { - return; - } + adjustedValue = newValue.toString(); + setValue(adjustedValue); + } - if (debounceTimerRef.current) { - clearTimeout(debounceTimerRef.current); - } + if (!onValueChange) { + return; + } - if (!debounceTimeMs) { - onChange(event); - return; - } + if (debounceTimerRef.current) { + clearTimeout(debounceTimerRef.current); + } - debounceTimerRef.current = setTimeout(() => { - onChange(event); - }, debounceTimeMs); - }, - [props.min, props.max, onChange, props.type, debounceTimeMs] - ); + if (!debounceTimeMs) { + onValueChange(`${adjustedValue}`); + return; + } + + debounceTimerRef.current = setTimeout(() => { + onValueChange(`${adjustedValue}`); + }, debounceTimeMs); + } + + function handleInputChange(event: React.ChangeEvent) { + setValue(event.target.value); + + if (props.onChange) { + props.onChange(event); + } + } return ( @@ -133,6 +149,8 @@ export const Input = React.forwardRef((props: InputProps, ref: React.ForwardedRe {...other} value={value} onChange={handleInputChange} + onBlur={handleInputEditingDone} + onKeyUp={handleKeyUp} ref={internalRef} slotProps={{ root: { diff --git a/frontend/src/modules/3DViewer/settings/atoms/queryAtoms.ts b/frontend/src/modules/3DViewer/settings/atoms/queryAtoms.ts index 27a7e9ec6..8b7c2b8df 100644 --- a/frontend/src/modules/3DViewer/settings/atoms/queryAtoms.ts +++ b/frontend/src/modules/3DViewer/settings/atoms/queryAtoms.ts @@ -1,4 +1,5 @@ import { apiService } from "@framework/ApiService"; +import { EnsembleSetAtom } from "@framework/GlobalAtoms"; import { selectedEnsembleIdentAtom } from "@modules/3DViewer/sharedAtoms/sharedAtoms"; import { atomWithQuery } from "jotai-tanstack-query"; @@ -26,14 +27,21 @@ export const gridModelInfosQueryAtom = atomWithQuery((get) => { export const drilledWellboreHeadersQueryAtom = atomWithQuery((get) => { const ensembleIdent = get(selectedEnsembleIdentAtom); + const ensembleSet = get(EnsembleSetAtom); - const caseUuid = ensembleIdent?.getCaseUuid() ?? ""; + let fieldIdentifier: string | null = null; + if (ensembleIdent) { + const ensemble = ensembleSet.findEnsemble(ensembleIdent); + if (ensemble) { + fieldIdentifier = ensemble.getFieldIdentifier(); + } + } return { - queryKey: ["getDrilledWellboreHeaders", caseUuid], - queryFn: () => apiService.well.getDrilledWellboreHeaders(caseUuid), + queryKey: ["getDrilledWellboreHeaders", fieldIdentifier], + queryFn: () => apiService.well.getDrilledWellboreHeaders(fieldIdentifier ?? ""), staleTime: STALE_TIME, gcTime: CACHE_TIME, - enabled: Boolean(caseUuid), + enabled: Boolean(fieldIdentifier), }; }); diff --git a/frontend/src/modules/3DViewer/sharedAtoms/sharedAtoms.ts b/frontend/src/modules/3DViewer/sharedAtoms/sharedAtoms.ts index 35224d922..97b851aef 100644 --- a/frontend/src/modules/3DViewer/sharedAtoms/sharedAtoms.ts +++ b/frontend/src/modules/3DViewer/sharedAtoms/sharedAtoms.ts @@ -1,3 +1,8 @@ +/* +Note that shared atoms is just a temporary solution to a use case that does not have a clear solution yet. +This is not how it should be done properly, communication between settings and view components should be done +through the use of interfaces. +*/ import { EnsembleIdent } from "@framework/EnsembleIdent"; import { EnsembleSetAtom } from "@framework/GlobalAtoms"; import { IntersectionType } from "@framework/types/intersection"; @@ -35,7 +40,7 @@ export const selectedHighlightedWellboreUuidAtom = atom((get) => { !userSelectedHighlightedWellboreUuid || !wellboreHeaders.data.some((el) => el.wellboreUuid === userSelectedHighlightedWellboreUuid) ) { - return wellboreHeaders.data[0].wellboreUuid ?? null; + return wellboreHeaders.data[0]?.wellboreUuid ?? null; } return userSelectedHighlightedWellboreUuid; diff --git a/frontend/src/modules/3DViewer/view/atoms/queryAtoms.ts b/frontend/src/modules/3DViewer/view/atoms/queryAtoms.ts index e8a5df8ff..5a1e62160 100644 --- a/frontend/src/modules/3DViewer/view/atoms/queryAtoms.ts +++ b/frontend/src/modules/3DViewer/view/atoms/queryAtoms.ts @@ -1,4 +1,5 @@ import { apiService } from "@framework/ApiService"; +import { EnsembleSetAtom } from "@framework/GlobalAtoms"; import { selectedEnsembleIdentAtom } from "@modules/3DViewer/sharedAtoms/sharedAtoms"; import { atomWithQuery } from "jotai-tanstack-query"; @@ -8,13 +9,21 @@ const CACHE_TIME = 60 * 1000; export const fieldWellboreTrajectoriesQueryAtom = atomWithQuery((get) => { const ensembleIdent = get(selectedEnsembleIdentAtom); - const caseUuid = ensembleIdent?.getCaseUuid(); + const ensembleSet = get(EnsembleSetAtom); + + let fieldIdentifier: string | null = null; + if (ensembleIdent) { + const ensemble = ensembleSet.findEnsemble(ensembleIdent); + if (ensemble) { + fieldIdentifier = ensemble.getFieldIdentifier(); + } + } return { - queryKey: ["getFieldWellboreTrajectories", caseUuid ?? ""], - queryFn: () => apiService.well.getFieldWellTrajectories(caseUuid ?? ""), + queryKey: ["getFieldWellboreTrajectories", fieldIdentifier ?? ""], + queryFn: () => apiService.well.getFieldWellTrajectories(fieldIdentifier ?? ""), staleTime: STALE_TIME, gcTime: CACHE_TIME, - enabled: caseUuid ? true : false, + enabled: Boolean(fieldIdentifier), }; }); diff --git a/frontend/src/modules/3DViewer/view/view.tsx b/frontend/src/modules/3DViewer/view/view.tsx index 63121f53e..a4dbfe7f6 100644 --- a/frontend/src/modules/3DViewer/view/view.tsx +++ b/frontend/src/modules/3DViewer/view/view.tsx @@ -93,7 +93,10 @@ export function View(props: ModuleViewProps): Re userSelectedCustomIntersectionPolylineIdAtom ); - const fieldWellboreTrajectoriesQuery = useFieldWellboreTrajectoriesQuery(ensembleIdent?.getCaseUuid() ?? undefined); + const fieldIdentifier = ensembleIdent + ? ensembleSet.findEnsemble(ensembleIdent)?.getFieldIdentifier() ?? null + : null; + const fieldWellboreTrajectoriesQuery = useFieldWellboreTrajectoriesQuery(fieldIdentifier ?? undefined); if (fieldWellboreTrajectoriesQuery.isError) { statusWriter.addError(fieldWellboreTrajectoriesQuery.error.message); diff --git a/frontend/src/modules/Intersection/settings/atoms/baseAtoms.ts b/frontend/src/modules/Intersection/settings/atoms/baseAtoms.ts index 10e136b81..71ae6f0b6 100644 --- a/frontend/src/modules/Intersection/settings/atoms/baseAtoms.ts +++ b/frontend/src/modules/Intersection/settings/atoms/baseAtoms.ts @@ -1,7 +1,16 @@ import { EnsembleIdent } from "@framework/EnsembleIdent"; +import { LayerManager } from "@modules/Intersection/utils/layers/LayerManager"; import { atom } from "jotai"; +export const addCustomIntersectionPolylineEditModeActiveAtom = atom(false); +export const editCustomIntersectionPolylineEditModeActiveAtom = atom(false); + +export const currentCustomIntersectionPolylineAtom = atom([]); + +export const userSelectedFieldIdentifierAtom = atom(null); export const userSelectedEnsembleIdentAtom = atom(null); export const userSelectedWellboreUuidAtom = atom(null); export const userSelectedCustomIntersectionPolylineIdAtom = atom(null); + +export const layerManagerBaseAtom = atom(new LayerManager()); diff --git a/frontend/src/modules/Intersection/settings/atoms/derivedAtoms.ts b/frontend/src/modules/Intersection/settings/atoms/derivedAtoms.ts index 7f4fd9e11..511d89b7f 100644 --- a/frontend/src/modules/Intersection/settings/atoms/derivedAtoms.ts +++ b/frontend/src/modules/Intersection/settings/atoms/derivedAtoms.ts @@ -1,13 +1,42 @@ import { EnsembleIdent } from "@framework/EnsembleIdent"; +import { EnsembleSet } from "@framework/EnsembleSet"; import { EnsembleRealizationFilterFunctionAtom, EnsembleSetAtom } from "@framework/GlobalAtoms"; import { IntersectionPolylinesAtom } from "@framework/userCreatedItems/IntersectionPolylines"; -import { selectedEnsembleIdentAtom } from "@modules/Intersection/sharedAtoms/sharedAtoms"; import { atom } from "jotai"; - -import { userSelectedCustomIntersectionPolylineIdAtom, userSelectedWellboreUuidAtom } from "./baseAtoms"; +import { queryClientAtom } from "jotai-tanstack-query"; + +import { + layerManagerBaseAtom, + userSelectedCustomIntersectionPolylineIdAtom, + userSelectedEnsembleIdentAtom, + userSelectedFieldIdentifierAtom, + userSelectedWellboreUuidAtom, +} from "./baseAtoms"; import { drilledWellboreHeadersQueryAtom } from "./queryAtoms"; +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)); +}); + +export const selectedFieldIdentifierAtom = atom((get) => { + const ensembleSet = get(EnsembleSetAtom); + const selectedFieldIdentifier = get(userSelectedFieldIdentifierAtom); + + if (selectedFieldIdentifier === null) { + return ensembleSet.getEnsembleArr()[0]?.getFieldIdentifier() || null; + } + + return selectedFieldIdentifier; +}); + export const availableRealizationsAtom = atom((get) => { const ensembleSet = get(EnsembleSetAtom); const selectedEnsembleIdent = get(selectedEnsembleIdentAtom); @@ -50,6 +79,17 @@ export const selectedCustomIntersectionPolylineIdAtom = atom((get) => { return userSelectedCustomIntersectionPolylineId; }); +export const selectedEnsembleIdentAtom = atom((get) => { + const ensembleSet = get(EnsembleSetAtom); + const userSelectedEnsembleIdent = get(userSelectedEnsembleIdentAtom); + + if (userSelectedEnsembleIdent === null || !ensembleSet.hasEnsemble(userSelectedEnsembleIdent)) { + return ensembleSet.getEnsembleArr()[0]?.getIdent() || null; + } + + return userSelectedEnsembleIdent; +}); + export const selectedWellboreAtom = atom((get) => { const userSelectedWellboreUuid = get(userSelectedWellboreUuidAtom); const wellboreHeaders = get(drilledWellboreHeadersQueryAtom); @@ -67,11 +107,23 @@ export const selectedWellboreAtom = atom((get) => { return { uuid: wellboreHeaders.data[0].wellboreUuid, identifier: wellboreHeaders.data[0].uniqueWellboreIdentifier, + depthReferencePoint: wellboreHeaders.data[0].depthReferencePoint, + depthReferenceElevation: wellboreHeaders.data[0].depthReferenceElevation, }; } return { uuid: userSelectedWellboreUuid, identifier: userSelectedWellboreHeader.uniqueWellboreIdentifier, + depthReferencePoint: userSelectedWellboreHeader.depthReferencePoint, + depthReferenceElevation: userSelectedWellboreHeader.depthReferenceElevation, }; }); + +export const layerManagerAtom = atom((get) => { + const queryClient = get(queryClientAtom); + const layerManager = get(layerManagerBaseAtom); + layerManager.setQueryClient(queryClient); + + return layerManager; +}); diff --git a/frontend/src/modules/Intersection/settings/atoms/layersAtoms.ts b/frontend/src/modules/Intersection/settings/atoms/layersAtoms.ts deleted file mode 100644 index fe2064162..000000000 --- a/frontend/src/modules/Intersection/settings/atoms/layersAtoms.ts +++ /dev/null @@ -1,128 +0,0 @@ -import { - LAYER_TYPE_TO_STRING_MAPPING, - LayerActionType, - LayerActions, - LayerType, -} from "@modules/Intersection/typesAndEnums"; -import { BaseLayer } from "@modules/Intersection/utils/layers/BaseLayer"; -import { GridLayer } from "@modules/Intersection/utils/layers/GridLayer"; -import { SeismicLayer } from "@modules/Intersection/utils/layers/SeismicLayer"; -import { SurfaceLayer } from "@modules/Intersection/utils/layers/SurfaceLayer"; -import { WellpicksLayer } from "@modules/Intersection/utils/layers/WellpicksLayer"; -import { QueryClient } from "@tanstack/query-core"; - -import { Getter, WritableAtom, atom } from "jotai"; -import { queryClientAtom } from "jotai-tanstack-query"; - -function makeUniqueLayerName(name: string, layers: BaseLayer[]): string { - let potentialName = name; - let i = 1; - while (layers.some((layer) => layer.getName() === potentialName)) { - potentialName = `${name} (${i})`; - i++; - } - return potentialName; -} - -function makeLayer(type: LayerType, name: string, queryClient: QueryClient): BaseLayer { - switch (type) { - case LayerType.GRID: - return new GridLayer(name, queryClient); - case LayerType.SEISMIC: - return new SeismicLayer(name, queryClient); - case LayerType.SURFACES: - return new SurfaceLayer(name, queryClient); - case LayerType.WELLPICKS: - return new WellpicksLayer(name, queryClient); - default: - throw new Error(`Layer type ${type} not supported`); - } -} - -export function atomWithReducerAndGetter( - initialValue: Value, - reducer: (value: Value, action: Action, get: Getter) => Value -): WritableAtom { - const valueAtom = atom(initialValue); - - return atom( - (get) => { - return get(valueAtom); - }, - (get, set, action) => { - const newValue = reducer(get(valueAtom), action, get); - set(valueAtom, newValue); - } - ); -} - -export const layersAtom = atomWithReducerAndGetter[], LayerActions>( - [], - (prev: BaseLayer[], action: LayerActions, get: Getter) => { - const queryClient = get(queryClientAtom); - if (action.type === LayerActionType.ADD_LAYER) { - return [ - ...prev, - makeLayer( - action.payload.type, - makeUniqueLayerName(LAYER_TYPE_TO_STRING_MAPPING[action.payload.type], prev), - queryClient - ), - ]; - } - if (action.type === LayerActionType.REMOVE_LAYER) { - return prev.filter((layer) => layer.getId() !== action.payload.id); - } - if (action.type === LayerActionType.TOGGLE_LAYER_VISIBILITY) { - const layer = prev.find((layer) => layer.getId() === action.payload.id); - if (!layer) { - return prev; - } - layer.setIsVisible(!layer.getIsVisible()); - return prev; - } - - if (action.type === LayerActionType.UPDATE_SETTING) { - const layer = prev.find((layer) => layer.getId() === action.payload.id); - if (!layer) { - return prev; - } - layer.maybeUpdateSettings(action.payload.settings); - return prev; - } - - if (action.type === LayerActionType.CHANGE_ORDER) { - return action.payload.orderedIds - .map((id) => prev.find((layer) => layer.getId() === id)) - .filter(Boolean) as BaseLayer[]; - } - - if (action.type === LayerActionType.MOVE_LAYER) { - const layer = prev.find((layer) => layer.getId() === action.payload.id); - if (!layer) { - return prev; - } - const index = prev.indexOf(layer); - const moveToIndex = action.payload.moveToIndex; - if (index === moveToIndex) { - return prev; - } - - if (moveToIndex <= 0) { - return [layer, ...prev.filter((el) => el.getId() !== action.payload.id)]; - } - - if (moveToIndex >= prev.length - 1) { - return [...prev.filter((el) => el.getId() !== action.payload.id), layer]; - } - - const newLayers = [...prev]; - newLayers.splice(index, 1); - newLayers.splice(moveToIndex, 0, layer); - - return newLayers; - } - - return prev; - } -); diff --git a/frontend/src/modules/Intersection/settings/atoms/queryAtoms.ts b/frontend/src/modules/Intersection/settings/atoms/queryAtoms.ts index bd75ffa1a..ac67ce9af 100644 --- a/frontend/src/modules/Intersection/settings/atoms/queryAtoms.ts +++ b/frontend/src/modules/Intersection/settings/atoms/queryAtoms.ts @@ -1,21 +1,20 @@ import { apiService } from "@framework/ApiService"; -import { selectedEnsembleIdentAtom } from "@modules/Intersection/sharedAtoms/sharedAtoms"; import { atomWithQuery } from "jotai-tanstack-query"; +import { selectedFieldIdentifierAtom } from "./derivedAtoms"; + const STALE_TIME = 60 * 1000; const CACHE_TIME = 60 * 1000; export const drilledWellboreHeadersQueryAtom = atomWithQuery((get) => { - const ensembleIdent = get(selectedEnsembleIdentAtom); - - const caseUuid = ensembleIdent?.getCaseUuid() ?? ""; + const fieldIdentifier = get(selectedFieldIdentifierAtom); return { - queryKey: ["getDrilledWellboreHeaders", caseUuid], - queryFn: () => apiService.well.getDrilledWellboreHeaders(caseUuid), + queryKey: ["getDrilledWellboreHeaders", fieldIdentifier], + queryFn: () => apiService.well.getDrilledWellboreHeaders(fieldIdentifier ?? ""), staleTime: STALE_TIME, gcTime: CACHE_TIME, - enabled: Boolean(caseUuid), + enabled: Boolean(fieldIdentifier), }; }); diff --git a/frontend/src/modules/Intersection/settings/components/layerSettings/surfacesUncertaintyLayer.tsx b/frontend/src/modules/Intersection/settings/components/layerSettings/surfacesUncertaintyLayer.tsx new file mode 100644 index 000000000..aad67b7ad --- /dev/null +++ b/frontend/src/modules/Intersection/settings/components/layerSettings/surfacesUncertaintyLayer.tsx @@ -0,0 +1,298 @@ +import React from "react"; + +import { SurfaceAttributeType_api, SurfaceMetaSet_api } from "@api"; +import { apiService } from "@framework/ApiService"; +import { EnsembleIdent } from "@framework/EnsembleIdent"; +import { EnsembleSet } from "@framework/EnsembleSet"; +import { WorkbenchSession, useEnsembleRealizationFilterFunc } from "@framework/WorkbenchSession"; +import { WorkbenchSettings } from "@framework/WorkbenchSettings"; +import { EnsembleDropdown } from "@framework/components/EnsembleDropdown"; +import { defaultColorPalettes } from "@framework/utils/colorPalettes"; +import { ColorPaletteSelector, ColorPaletteSelectorType } from "@lib/components/ColorPaletteSelector"; +import { Dropdown, DropdownOption } from "@lib/components/Dropdown"; +import { Input } from "@lib/components/Input"; +import { PendingWrapper } from "@lib/components/PendingWrapper"; +import { Select } from "@lib/components/Select"; +import { ColorPalette } from "@lib/utils/ColorPalette"; +import { ColorSet } from "@lib/utils/ColorSet"; +import { useLayerSettings } from "@modules/Intersection/utils/layers/BaseLayer"; +import { + SurfacesUncertaintyLayer, + SurfacesUncertaintyLayerSettings, +} from "@modules/Intersection/utils/layers/SurfacesUncertaintyLayer"; +import { SurfaceDirectory, SurfaceTimeType } from "@modules/_shared/Surface"; +import { UseQueryResult, useQuery } from "@tanstack/react-query"; + +import { cloneDeep, isEqual } from "lodash"; + +import { fixupSetting } from "./utils"; + +export type SurfacesUncertaintyLayerSettingsComponentProps = { + layer: SurfacesUncertaintyLayer; + ensembleSet: EnsembleSet; + workbenchSession: WorkbenchSession; + workbenchSettings: WorkbenchSettings; +}; + +export function SurfacesUncertaintyLayerSettingsComponent( + props: SurfacesUncertaintyLayerSettingsComponentProps +): React.ReactNode { + const settings = useLayerSettings(props.layer); + const [newSettings, setNewSettings] = React.useState(cloneDeep(settings)); + const [prevSettings, setPrevSettings] = React.useState(cloneDeep(settings)); + + if (!isEqual(settings, prevSettings)) { + setPrevSettings(settings); + setNewSettings(settings); + } + + const ensembleFilterFunc = useEnsembleRealizationFilterFunc(props.workbenchSession); + + const surfaceDirectoryQuery = useSurfaceDirectoryQuery( + newSettings.ensembleIdent?.getCaseUuid(), + newSettings.ensembleIdent?.getEnsembleName() + ); + + const fixupEnsembleIdent = fixupSetting( + "ensembleIdent", + props.ensembleSet.getEnsembleArr().map((el) => el.getIdent()), + newSettings + ); + if (!isEqual(fixupEnsembleIdent, newSettings.ensembleIdent)) { + setNewSettings((prev) => ({ ...prev, ensembleIdent: fixupEnsembleIdent })); + } + + if (fixupEnsembleIdent) { + const fixupRealizationNums = fixupRealizationNumsSetting( + newSettings.realizationNums, + ensembleFilterFunc(fixupEnsembleIdent) + ); + if (!isEqual(fixupRealizationNums, newSettings.realizationNums)) { + setNewSettings((prev) => ({ ...prev, realizationNums: fixupRealizationNums })); + } + } + + const availableAttributes: string[] = []; + const availableSurfaceNames: string[] = []; + const surfaceDirectory = surfaceDirectoryQuery.data + ? new SurfaceDirectory({ + useObservedSurfaces: false, + realizationMetaSet: surfaceDirectoryQuery.data, + timeType: SurfaceTimeType.None, + includeAttributeTypes: [SurfaceAttributeType_api.DEPTH], + }) + : null; + + if (surfaceDirectory) { + availableAttributes.push(...surfaceDirectory.getAttributeNames(null)); + + const fixupAttribute = fixupSetting("attribute", availableAttributes, newSettings); + if (!isEqual(fixupAttribute, newSettings.attribute)) { + setNewSettings((prev) => ({ ...prev, attribute: fixupAttribute })); + } + } + + if (surfaceDirectory && newSettings.attribute) { + availableSurfaceNames.push(...surfaceDirectory.getSurfaceNames(newSettings.attribute)); + + const fixupSurfaceNames = fixupSurfaceNamesSetting(newSettings.surfaceNames, availableSurfaceNames); + if (!isEqual(fixupSurfaceNames, newSettings.surfaceNames)) { + setNewSettings((prev) => ({ ...prev, surfaceNames: fixupSurfaceNames })); + } + + props.layer.maybeRefetchData(); + } + + React.useEffect( + function propagateSettingsChange() { + props.layer.maybeUpdateSettings(cloneDeep(newSettings)); + }, + [newSettings, props.layer] + ); + + React.useEffect( + function maybeRefetchData() { + props.layer.setIsSuspended(surfaceDirectoryQuery.isFetching); + if (!surfaceDirectoryQuery.isFetching) { + props.layer.maybeRefetchData(); + } + }, + [surfaceDirectoryQuery.isFetching, props.layer, newSettings] + ); + + function handleEnsembleChange(ensembleIdent: EnsembleIdent | null) { + setNewSettings((prev) => ({ ...prev, ensembleIdent })); + } + + function handleRealizationsChange(realizationNums: string[]) { + setNewSettings((prev) => ({ ...prev, realizationNums: realizationNums.map((el) => parseInt(el)) })); + } + + function handleAttributeChange(attribute: string) { + setNewSettings((prev) => ({ ...prev, attribute })); + } + + function handleSurfaceNamesChange(surfaceNames: string[]) { + setNewSettings((prev) => ({ ...prev, surfaceNames })); + } + + function handleResolutionChange(e: React.ChangeEvent) { + setNewSettings((prev) => ({ ...prev, resolution: parseFloat(e.target.value) })); + } + + function handleColorPaletteChange(colorPalette: ColorPalette) { + props.layer.setColorSet(new ColorSet(colorPalette)); + } + + const availableRealizations: number[] = []; + if (fixupEnsembleIdent) { + availableRealizations.push(...ensembleFilterFunc(fixupEnsembleIdent)); + } + + return ( +
+
+
Ensemble
+
+ +
+
+
+
Realizations
+
+ + +
+
+
+
Sample resolution
+
+ +
+
+
+
Color set
+
+ +
+
+
+ ); +} + +function makeRealizationOptions(realizations: readonly number[]): DropdownOption[] { + return realizations.map((realization) => ({ label: realization.toString(), value: realization.toString() })); +} + +function makeAttributeOptions(attributes: string[]): DropdownOption[] { + return attributes.map((attr) => ({ label: attr, value: attr })); +} + +function makeSurfaceNameOptions(surfaceNames: string[]): DropdownOption[] { + return surfaceNames.map((surfaceName) => ({ label: surfaceName, value: surfaceName })); +} + +const STALE_TIME = 60 * 1000; +const CACHE_TIME = 60 * 1000; + +export function useSurfaceDirectoryQuery( + caseUuid: string | undefined, + ensembleName: string | undefined +): UseQueryResult { + return useQuery({ + queryKey: ["getSurfaceDirectory", caseUuid, ensembleName], + queryFn: () => apiService.surface.getRealizationSurfacesMetadata(caseUuid ?? "", ensembleName ?? ""), + staleTime: STALE_TIME, + gcTime: CACHE_TIME, + enabled: Boolean(caseUuid && ensembleName), + }); +} + +function fixupRealizationNumsSetting( + currentRealizationNums: readonly number[], + validRealizationNums: readonly number[] +): number[] { + if (validRealizationNums.length === 0) { + return [...currentRealizationNums]; + } + + let adjustedRealizationNums = currentRealizationNums.filter((el) => validRealizationNums.includes(el)); + + if (adjustedRealizationNums.length === 0) { + adjustedRealizationNums = [...validRealizationNums]; + } + + return adjustedRealizationNums; +} + +function fixupSurfaceNamesSetting(currentSurfaceNames: string[], validSurfaceNames: string[]): string[] { + if (validSurfaceNames.length === 0) { + return currentSurfaceNames; + } + + let adjustedSurfaceNames = currentSurfaceNames.filter((el) => validSurfaceNames.includes(el)); + + if (adjustedSurfaceNames.length === 0) { + adjustedSurfaceNames = [validSurfaceNames[0]]; + } + + return adjustedSurfaceNames; +} diff --git a/frontend/src/modules/Intersection/settings/components/layers.tsx b/frontend/src/modules/Intersection/settings/components/layers.tsx index e864064fc..48f73a1ff 100644 --- a/frontend/src/modules/Intersection/settings/components/layers.tsx +++ b/frontend/src/modules/Intersection/settings/components/layers.tsx @@ -10,12 +10,6 @@ import { useElementBoundingRect } from "@lib/hooks/useElementBoundingRect"; import { createPortal } from "@lib/utils/createPortal"; import { MANHATTAN_LENGTH, Point2D, pointDistance, rectContainsPoint } from "@lib/utils/geometry"; import { resolveClassNames } from "@lib/utils/resolveClassNames"; -import { - LAYER_TYPE_TO_STRING_MAPPING, - LayerActionType, - LayerActions, - LayerType, -} from "@modules/Intersection/typesAndEnums"; import { BaseLayer, LayerStatus, @@ -24,9 +18,17 @@ import { useLayerStatus, } from "@modules/Intersection/utils/layers/BaseLayer"; import { isGridLayer } from "@modules/Intersection/utils/layers/GridLayer"; +import { LayerFactory } from "@modules/Intersection/utils/layers/LayerFactory"; +import { + LayerManager, + LayerManagerTopic, + useLayerManagerTopicValue, +} from "@modules/Intersection/utils/layers/LayerManager"; import { isSeismicLayer } from "@modules/Intersection/utils/layers/SeismicLayer"; import { isSurfaceLayer } from "@modules/Intersection/utils/layers/SurfaceLayer"; +import { isSurfacesUncertaintyLayer } from "@modules/Intersection/utils/layers/SurfacesUncertaintyLayer"; import { isWellpicksLayer } from "@modules/Intersection/utils/layers/WellpicksLayer"; +import { LAYER_TYPE_TO_STRING_MAPPING, LayerType } from "@modules/Intersection/utils/layers/types"; import { Dropdown, MenuButton } from "@mui/base"; import { Add, @@ -42,24 +44,23 @@ import { VisibilityOff, } from "@mui/icons-material"; -import { useAtom } from "jotai"; import { isEqual } from "lodash"; import { GridLayerSettingsComponent } from "./layerSettings/gridLayer"; import { SeismicLayerSettingsComponent } from "./layerSettings/seismicLayer"; import { SurfaceLayerSettingsComponent } from "./layerSettings/surfaceLayer"; +import { SurfacesUncertaintyLayerSettingsComponent } from "./layerSettings/surfacesUncertaintyLayer"; import { WellpicksLayerSettingsComponent } from "./layerSettings/wellpicksLayer"; -import { layersAtom } from "../atoms/layersAtoms"; - export type LayersProps = { ensembleSet: EnsembleSet; + layerManager: LayerManager; workbenchSession: WorkbenchSession; workbenchSettings: WorkbenchSettings; }; export function Layers(props: LayersProps): React.ReactNode { - const [layers, dispatch] = useAtom(layersAtom); + const layers = useLayerManagerTopicValue(props.layerManager, LayerManagerTopic.LAYERS_CHANGED); const [draggingLayerId, setDraggingLayerId] = React.useState(null); const [isDragging, setIsDragging] = React.useState(false); @@ -82,11 +83,11 @@ export function Layers(props: LayersProps): React.ReactNode { } function handleAddLayer(type: LayerType) { - dispatch({ type: LayerActionType.ADD_LAYER, payload: { type } }); + props.layerManager.addLayer(LayerFactory.makeLayer(type)); } function handleRemoveLayer(id: string) { - dispatch({ type: LayerActionType.REMOVE_LAYER, payload: { id } }); + props.layerManager.removeLayer(id); } React.useEffect( @@ -274,7 +275,7 @@ export function Layers(props: LayersProps): React.ReactNode { setDraggingLayerId(null); document.removeEventListener("pointermove", handlePointerMove); document.removeEventListener("pointerup", handlePointerUp); - dispatch({ type: LayerActionType.CHANGE_ORDER, payload: { orderedIds: newLayerOrder } }); + props.layerManager.changeOrder(newLayerOrder); } currentParentDivRef.addEventListener("pointerdown", handlePointerDown); @@ -287,7 +288,7 @@ export function Layers(props: LayersProps): React.ReactNode { setDraggingLayerId(null); }; }, - [dispatch, layers] + [layers, props.layerManager] ); function handleScroll(e: React.UIEvent) { @@ -354,7 +355,6 @@ export function Layers(props: LayersProps): React.ReactNode { workbenchSession={props.workbenchSession} workbenchSettings={props.workbenchSettings} onRemoveLayer={handleRemoveLayer} - dispatch={dispatch} isDragging={draggingLayerId === layer.getId()} dragPosition={dragPosition} /> @@ -380,7 +380,6 @@ type LayerItemProps = { isDragging: boolean; dragPosition: Point2D; onRemoveLayer: (id: string) => void; - dispatch: (action: LayerActions) => void; }; function LayerItem(props: LayerItemProps): React.ReactNode { @@ -447,6 +446,16 @@ function LayerItem(props: LayerItemProps): React.ReactNode { /> ); } + if (isSurfacesUncertaintyLayer(layer)) { + return ( + + ); + } return null; } @@ -460,7 +469,7 @@ function LayerItem(props: LayerItemProps): React.ReactNode { } if (status === LayerStatus.ERROR) { return ( -
+
); diff --git a/frontend/src/modules/Intersection/settings/settings.tsx b/frontend/src/modules/Intersection/settings/settings.tsx index e5dae6ab3..116b10b82 100644 --- a/frontend/src/modules/Intersection/settings/settings.tsx +++ b/frontend/src/modules/Intersection/settings/settings.tsx @@ -5,6 +5,7 @@ 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 { Intersection, IntersectionType } from "@framework/types/intersection"; import { IntersectionPolyline } from "@framework/userCreatedItems/IntersectionPolylines"; import { CollapsibleGroup } from "@lib/components/CollapsibleGroup"; @@ -18,10 +19,17 @@ import { resolveClassNames } from "@lib/utils/resolveClassNames"; import { useAtomValue, useSetAtom } from "jotai"; import { isEqual } from "lodash"; -import { userSelectedCustomIntersectionPolylineIdAtom, userSelectedWellboreUuidAtom } from "./atoms/baseAtoms"; +import { + userSelectedCustomIntersectionPolylineIdAtom, + userSelectedFieldIdentifierAtom, + userSelectedWellboreUuidAtom, +} from "./atoms/baseAtoms"; import { availableUserCreatedIntersectionPolylinesAtom, + filteredEnsembleSetAtom, + layerManagerAtom, selectedCustomIntersectionPolylineIdAtom, + selectedFieldIdentifierAtom, selectedWellboreAtom, } from "./atoms/derivedAtoms"; import { drilledWellboreHeadersQueryAtom } from "./atoms/queryAtoms"; @@ -35,8 +43,14 @@ export function Settings( props: ModuleSettingsProps, ViewAtoms> ): JSX.Element { const ensembleSet = useEnsembleSet(props.workbenchSession); + const filteredEnsembleSet = useAtomValue(filteredEnsembleSetAtom); const statusWriter = useSettingsStatusWriter(props.settingsContext); + const layerManager = useAtomValue(layerManagerAtom); + + const selectedField = useAtomValue(selectedFieldIdentifierAtom); + const setSelectedField = useSetAtom(userSelectedFieldIdentifierAtom); + const [intersectionExtensionLength, setIntersectionExtensionLength] = props.settingsContext.useSettingsToViewInterfaceState("intersectionExtensionLength"); @@ -78,6 +92,10 @@ export function Settings( wellHeadersErrorMessage = "Failed to load well headers"; } + function handleFieldIdentifierChange(fieldIdentifier: string | null) { + setSelectedField(fieldIdentifier); + } + function handleWellHeaderSelectionChange(wellHeader: string[]) { const uuid = wellHeader.at(0); setSelectedWellboreHeader(uuid ?? null); @@ -117,6 +135,13 @@ export function Settings(
+
diff --git a/frontend/src/modules/Intersection/settingsToViewInterface.ts b/frontend/src/modules/Intersection/settingsToViewInterface.ts index c4701387a..9c57f4c3f 100644 --- a/frontend/src/modules/Intersection/settingsToViewInterface.ts +++ b/frontend/src/modules/Intersection/settingsToViewInterface.ts @@ -3,10 +3,13 @@ import { InterfaceInitialization } from "@framework/UniDirectionalSettingsToView import { IntersectionType } from "@framework/types/intersection"; import { ColorScale } from "@lib/utils/ColorScale"; -import { selectedCustomIntersectionPolylineIdAtom, selectedWellboreAtom } from "./settings/atoms/derivedAtoms"; -import { layersAtom } from "./settings/atoms/layersAtoms"; -import { selectedEnsembleIdentAtom } from "./sharedAtoms/sharedAtoms"; -import { BaseLayer } from "./utils/layers/BaseLayer"; +import { + layerManagerAtom, + selectedCustomIntersectionPolylineIdAtom, + selectedEnsembleIdentAtom, + selectedWellboreAtom, +} from "./settings/atoms/derivedAtoms"; +import { LayerManager } from "./utils/layers/LayerManager"; export type SettingsToViewInterface = { baseStates: { @@ -21,8 +24,13 @@ export type SettingsToViewInterface = { derivedStates: { ensembleIdent: EnsembleIdent | null; selectedCustomIntersectionPolylineId: string | null; - layers: BaseLayer[]; - wellboreHeader: { uuid: string; identifier: string } | null; + layerManager: LayerManager; + wellboreHeader: { + uuid: string; + identifier: string; + depthReferencePoint: string; + depthReferenceElevation: number; + } | null; }; }; @@ -43,8 +51,8 @@ export const interfaceInitialization: InterfaceInitialization { return get(selectedCustomIntersectionPolylineIdAtom); }, - layers: (get) => { - return get(layersAtom); + layerManager: (get) => { + return get(layerManagerAtom); }, wellboreHeader: (get) => { return get(selectedWellboreAtom); diff --git a/frontend/src/modules/Intersection/sharedAtoms/sharedAtoms.ts b/frontend/src/modules/Intersection/sharedAtoms/sharedAtoms.ts deleted file mode 100644 index 1105acb37..000000000 --- a/frontend/src/modules/Intersection/sharedAtoms/sharedAtoms.ts +++ /dev/null @@ -1,51 +0,0 @@ -import { EnsembleIdent } from "@framework/EnsembleIdent"; -import { EnsembleSetAtom } from "@framework/GlobalAtoms"; - -import { atom } from "jotai"; - -import { userSelectedEnsembleIdentAtom, userSelectedWellboreUuidAtom } from "../settings/atoms/baseAtoms"; -import { drilledWellboreHeadersQueryAtom } from "../settings/atoms/queryAtoms"; - -export const selectedEnsembleIdentAtom = atom((get) => { - const ensembleSet = get(EnsembleSetAtom); - const userSelectedEnsembleIdent = get(userSelectedEnsembleIdentAtom); - - if (userSelectedEnsembleIdent === null || !ensembleSet.hasEnsemble(userSelectedEnsembleIdent)) { - return ensembleSet.getEnsembleArr()[0]?.getIdent() || null; - } - - return userSelectedEnsembleIdent; -}); - -export const selectedWellboreAtom = atom((get) => { - const userSelectedWellboreUuid = get(userSelectedWellboreUuidAtom); - const wellboreHeaders = get(drilledWellboreHeadersQueryAtom); - - if (!wellboreHeaders.data) { - return null; - } - - const userSelectedWellboreHeader = wellboreHeaders.data.find((el) => el.wellboreUuid === userSelectedWellboreUuid); - - if (!userSelectedWellboreUuid || !userSelectedWellboreHeader) { - if (wellboreHeaders.data.length === 0) { - return null; - } - return { - uuid: wellboreHeaders.data[0].wellboreUuid, - identifier: wellboreHeaders.data[0].uniqueWellboreIdentifier, - }; - } - - return { - uuid: userSelectedWellboreUuid, - identifier: userSelectedWellboreHeader.uniqueWellboreIdentifier, - depthReferencePoint: userSelectedWellboreHeader.depthReferencePoint, - depthReferenceElevation: userSelectedWellboreHeader.depthReferenceElevation, - }; -}); - -export const addCustomIntersectionPolylineEditModeActiveAtom = atom(false); -export const editCustomIntersectionPolylineEditModeActiveAtom = atom(false); - -export const currentCustomIntersectionPolylineAtom = atom([]); diff --git a/frontend/src/modules/Intersection/typesAndEnums.ts b/frontend/src/modules/Intersection/typesAndEnums.ts index 6dcc15c1e..b08010c66 100644 --- a/frontend/src/modules/Intersection/typesAndEnums.ts +++ b/frontend/src/modules/Intersection/typesAndEnums.ts @@ -1,46 +1 @@ -export const CURVE_FITTING_EPSILON = 5; // meters - -export enum LayerType { - GRID = "grid", - SEISMIC = "seismic", - SURFACES = "surfaces", - WELLPICKS = "wellpicks", -} - -export const LAYER_TYPE_TO_STRING_MAPPING = { - [LayerType.GRID]: "Grid", - [LayerType.SEISMIC]: "Seismic", - [LayerType.SURFACES]: "Surfaces", - [LayerType.WELLPICKS]: "Wellpicks", -}; - -export enum LayerActionType { - ADD_LAYER = "add-layer", - REMOVE_LAYER = "remove-layer", - TOGGLE_LAYER_VISIBILITY = "toggle-layer-visibility", - TOGGLE_LAYER_SETTINGS_VISIBILITY = "toggle-layer-settings-visibility", - UPDATE_SETTING = "update-settings", - MOVE_LAYER = "move-layer", - CHANGE_ORDER = "change-order", -} - -export type LayerActionPayloads = { - [LayerActionType.ADD_LAYER]: { type: LayerType }; - [LayerActionType.REMOVE_LAYER]: { id: string }; - [LayerActionType.TOGGLE_LAYER_VISIBILITY]: { id: string }; - [LayerActionType.TOGGLE_LAYER_SETTINGS_VISIBILITY]: { id: string }; - [LayerActionType.UPDATE_SETTING]: { id: string; settings: Record }; - [LayerActionType.MOVE_LAYER]: { id: string; moveToIndex: number }; - [LayerActionType.CHANGE_ORDER]: { orderedIds: string[] }; -}; - -export type LayerAction = { - type: T; - payload: LayerActionPayloads[T]; -}; - -export type LayerActions = { - [K in keyof LayerActionPayloads]: LayerActionPayloads[K] extends never - ? { type: K } - : { type: K; payload: LayerActionPayloads[K] }; -}[keyof LayerActionPayloads]; +export const CURVE_FITTING_EPSILON = 5; // meter diff --git a/frontend/src/modules/Intersection/utils/layers/BaseLayer.ts b/frontend/src/modules/Intersection/utils/layers/BaseLayer.ts index de76fc0e1..1863f8ba5 100644 --- a/frontend/src/modules/Intersection/utils/layers/BaseLayer.ts +++ b/frontend/src/modules/Intersection/utils/layers/BaseLayer.ts @@ -34,7 +34,7 @@ export type LayerSettings = { export class BaseLayer { private _subscribers: Map void>> = new Map(); - protected _queryClient: QueryClient; + protected _queryClient: QueryClient | null = null; protected _status: LayerStatus = LayerStatus.IDLE; private _id: string; private _name: string; @@ -45,13 +45,15 @@ export class BaseLayer { protected _settings: TSettings = {} as TSettings; private _lastDataFetchSettings: TSettings; private _queryKeys: unknown[][] = []; + private _error: string | null = null; + private _refetchingRequested: boolean = false; + private _cancellingPending: boolean = false; - constructor(name: string, settings: TSettings, queryClient: QueryClient) { + constructor(name: string, settings: TSettings) { this._id = v4(); this._name = name; this._settings = settings; this._lastDataFetchSettings = cloneDeep(settings); - this._queryClient = queryClient; } getId(): string { @@ -78,6 +80,10 @@ export class BaseLayer { this.notifySubscribers(LayerTopic.NAME); } + setQueryClient(queryClient: QueryClient): void { + this._queryClient = queryClient; + } + getBoundingBox(): BoundingBox | null { return this._boundingBox; } @@ -109,6 +115,7 @@ export class BaseLayer { } maybeUpdateSettings(updatedSettings: Partial): void { + this._cancellingPending = true; const patchesToApply: Partial = {}; for (const setting in updatedSettings) { if (!(setting in this._settings)) { @@ -120,23 +127,45 @@ export class BaseLayer { } } if (Object.keys(patchesToApply).length > 0) { - this._settings = { ...this._settings, ...patchesToApply }; - this.maybeCancelQuery(); + this.maybeCancelQuery().then(() => { + this._settings = { ...this._settings, ...patchesToApply }; + this.notifySubscribers(LayerTopic.SETTINGS); + if (this._refetchingRequested) { + this.maybeRefetchData(); + } + }); + } else { + this._cancellingPending = false; } - - this.notifySubscribers(LayerTopic.SETTINGS); } protected registerQueryKey(queryKey: unknown[]): void { this._queryKeys.push(queryKey); } - private maybeCancelQuery(): void { - if (this._queryKeys) { + private async maybeCancelQuery(): Promise { + if (!this._queryClient) { + return; + } + + if (this._queryKeys.length > 0) { for (const queryKey of this._queryKeys) { - this._queryClient.cancelQueries({ queryKey }); + await this._queryClient.cancelQueries( + { queryKey, exact: true, fetchStatus: "fetching", type: "active" }, + { + silent: true, + revert: true, + } + ); + await this._queryClient?.invalidateQueries({ queryKey }); + this._queryClient?.removeQueries({ queryKey }); } + this._queryKeys = []; + this._status = LayerStatus.IDLE; + this.notifySubscribers(LayerTopic.STATUS); } + + this._cancellingPending = false; } subscribe(topic: LayerTopic, callback: () => void): () => void { @@ -154,6 +183,10 @@ export class BaseLayer { this._subscribers.get(topic)?.delete(callback); } + getError(): string | null { + return this._error; + } + protected notifySubscribers(topic: LayerTopic): void { for (const callback of this._subscribers.get(topic) ?? []) { callback(); @@ -169,10 +202,19 @@ export class BaseLayer { } async maybeRefetchData(): Promise { + if (!this._queryClient) { + return; + } + if (this._isSuspended) { return; } + if (this._cancellingPending) { + this._refetchingRequested = true; + return; + } + if (!this.doSettingsChangesRequireDataRefetch(this._lastDataFetchSettings, this._settings)) { return; } @@ -187,7 +229,7 @@ export class BaseLayer { this._status = LayerStatus.LOADING; this.notifySubscribers(LayerTopic.STATUS); try { - this._data = await this.fetchData(); + this._data = await this.fetchData(this._queryClient); 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." @@ -196,14 +238,18 @@ export class BaseLayer { this._queryKeys = []; this.notifySubscribers(LayerTopic.DATA); this._status = LayerStatus.SUCCESS; - } catch (error) { - console.error("Error fetching data", error); + } catch (error: any) { + if (error.constructor?.name === "CancelledError") { + return; + } + this._error = `${error.message}`; this._status = LayerStatus.ERROR; } this.notifySubscribers(LayerTopic.STATUS); } - protected async fetchData(): Promise { + // eslint-disable-next-line @typescript-eslint/no-unused-vars + protected async fetchData(queryClient: QueryClient): Promise { throw new Error("Not implemented"); } } diff --git a/frontend/src/modules/Intersection/utils/layers/GridLayer.ts b/frontend/src/modules/Intersection/utils/layers/GridLayer.ts index 757959255..ec65f0b58 100644 --- a/frontend/src/modules/Intersection/utils/layers/GridLayer.ts +++ b/frontend/src/modules/Intersection/utils/layers/GridLayer.ts @@ -36,7 +36,7 @@ export class GridLayer extends BaseLayer(); private _defaultColorScale: ColorScale; - constructor(name: string, queryClient: QueryClient) { + constructor(name: string) { const defaultSettings = { ensembleIdent: null, gridModelName: null, @@ -50,7 +50,7 @@ export class GridLayer extends BaseLayer { + protected async fetchData(queryClient: QueryClient): Promise { super.setBoundingBox(null); const queryKey = ["getGridPolylineIntersection", ...Object.entries(this._settings)]; this.registerQueryKey(queryKey); - return this._queryClient + return queryClient .fetchQuery({ queryKey, queryFn: () => diff --git a/frontend/src/modules/Intersection/utils/layers/LayerFactory.ts b/frontend/src/modules/Intersection/utils/layers/LayerFactory.ts new file mode 100644 index 000000000..9e6af85f0 --- /dev/null +++ b/frontend/src/modules/Intersection/utils/layers/LayerFactory.ts @@ -0,0 +1,25 @@ +import { GridLayer } from "./GridLayer"; +import { SeismicLayer } from "./SeismicLayer"; +import { SurfaceLayer } from "./SurfaceLayer"; +import { SurfacesUncertaintyLayer } from "./SurfacesUncertaintyLayer"; +import { WellpicksLayer } from "./WellpicksLayer"; +import { LayerType } from "./types"; + +export class LayerFactory { + static makeLayer(layerType: LayerType) { + switch (layerType) { + case LayerType.GRID: + return new GridLayer("Grid"); + case LayerType.SEISMIC: + return new SeismicLayer("Seismic"); + case LayerType.SURFACES: + return new SurfaceLayer("Surfaces"); + case LayerType.WELLPICKS: + return new WellpicksLayer("Well picks"); + case LayerType.SURFACES_UNCERTAINTY: + return new SurfacesUncertaintyLayer("Surfaces uncertainty"); + default: + throw new Error("Unknown layer type"); + } + } +} diff --git a/frontend/src/modules/Intersection/utils/layers/LayerManager.ts b/frontend/src/modules/Intersection/utils/layers/LayerManager.ts new file mode 100644 index 000000000..1f6a238c6 --- /dev/null +++ b/frontend/src/modules/Intersection/utils/layers/LayerManager.ts @@ -0,0 +1,113 @@ +import React from "react"; + +import { QueryClient } from "@tanstack/query-core"; + +import { BaseLayer } from "./BaseLayer"; + +export enum LayerManagerTopic { + LAYERS_CHANGED = "layers-changed", +} + +export type LayerManagerTopicValueTypes = { + [LayerManagerTopic.LAYERS_CHANGED]: BaseLayer[]; +}; + +export class LayerManager { + private _queryClient: QueryClient | null = null; + private _layers: BaseLayer[] = []; + private _subscribers: Map void>> = new Map(); + + setQueryClient(queryClient: QueryClient): void { + this._queryClient = queryClient; + } + + addLayer(layer: BaseLayer): void { + if (!this._queryClient) { + throw new Error("Query client not set"); + } + layer.setName(this.makeUniqueLayerName(layer.getName())); + layer.setQueryClient(this._queryClient); + this._layers = [layer, ...this._layers]; + this.notifySubscribers(LayerManagerTopic.LAYERS_CHANGED); + } + + removeLayer(id: string): void { + this._layers = this._layers.filter((layer) => layer.getId() !== id); + this.notifySubscribers(LayerManagerTopic.LAYERS_CHANGED); + } + + getLayer(id: string): BaseLayer | undefined { + return this._layers.find((layer) => layer.getId() === id); + } + + getLayers(): BaseLayer[] { + return this._layers; + } + + changeOrder(order: string[]): void { + this._layers = order + .map((id) => this._layers.find((layer) => layer.getId() === id)) + .filter(Boolean) as BaseLayer[]; + this.notifySubscribers(LayerManagerTopic.LAYERS_CHANGED); + } + + subscribe(topic: LayerManagerTopic, subscriber: () => void): void { + const subscribers = this._subscribers.get(topic) ?? new Set(); + subscribers.add(subscriber); + this._subscribers.set(topic, subscribers); + } + + private notifySubscribers(topic: LayerManagerTopic): void { + const subscribers = this._subscribers.get(topic); + if (subscribers) { + subscribers.forEach((subscriber) => subscriber()); + } + } + + private makeUniqueLayerName(name: string): string { + let potentialName = name; + let i = 1; + while (this._layers.some((layer) => layer.getName() === potentialName)) { + potentialName = `${name} (${i})`; + i++; + } + return potentialName; + } + + makeSubscriberFunction(topic: LayerManagerTopic): (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; + } + + makeSnapshotGetter(topic: T): () => LayerManagerTopicValueTypes[T] { + const snapshotGetter = (): any => { + if (topic === LayerManagerTopic.LAYERS_CHANGED) { + return this.getLayers(); + } + }; + + return snapshotGetter; + } +} + +export function useLayerManagerTopicValue( + layerManager: LayerManager, + topic: T +): LayerManagerTopicValueTypes[T] { + const value = React.useSyncExternalStore( + layerManager.makeSubscriberFunction(topic), + layerManager.makeSnapshotGetter(topic) + ); + + return value; +} diff --git a/frontend/src/modules/Intersection/utils/layers/SeismicLayer.ts b/frontend/src/modules/Intersection/utils/layers/SeismicLayer.ts index 618f1130b..b524eb92a 100644 --- a/frontend/src/modules/Intersection/utils/layers/SeismicLayer.ts +++ b/frontend/src/modules/Intersection/utils/layers/SeismicLayer.ts @@ -53,7 +53,10 @@ const CACHE_TIME = 60 * 1000; export type SeismicLayerSettings = { ensembleIdent: EnsembleIdent | null; realizationNum: number | null; - polylineUtmXy: number[]; + polyline: { + polylineUtmXy: number[]; + actualSectionLengths: number[]; + }; extensionLength: number; surveyType: SeismicSurveyType; dataType: SeismicDataType; @@ -74,11 +77,14 @@ export class SeismicLayer extends BaseLayer = new Map(); private _useCustomColorScaleBoundariesParameterMap = new Map(); - constructor(name: string, queryClient: QueryClient) { + constructor(name: string) { const defaultSettings = { ensembleIdent: null, realizationNum: null, - polylineUtmXy: [], + polyline: { + polylineUtmXy: [], + actualSectionLengths: [], + }, extensionLength: 0, surveyType: SeismicSurveyType.THREE_D, dataType: SeismicDataType.SIMULATED, @@ -86,7 +92,7 @@ export class SeismicLayer extends BaseLayer 0 && + this._settings.polyline.polylineUtmXy.length > 0 && + this._settings.polyline.actualSectionLengths.length === + this._settings.polyline.polylineUtmXy.length / 2 - 1 && this._settings.attribute !== null && this._settings.dateOrInterval !== null && this._settings.extensionLength > 0 && @@ -206,7 +214,7 @@ export class SeismicLayer extends BaseLayer 0) { - const prevPoint = sampledPolyline[index - 1]; - u += pointDistance( - { - x: point[0], - y: point[1], - }, - { - x: prevPoint[0], - y: prevPoint[1], - } - ); + trajectoryFenceProjection.push([u, 0]); + for (let i = 2; i < polyline.length; i += 2) { + const distance = pointDistance( + { x: polyline[i], y: polyline[i + 1] }, + { x: polyline[i - 2], y: polyline[i - 1] } + ); + const actualDistance = this._settings.polyline.actualSectionLengths[i / 2 - 1]; + const numPoints = Math.floor(distance / this._settings.resolution) - 1; + const scale = actualDistance / distance; + + for (let p = 1; p <= numPoints; p++) { + u += this._settings.resolution * scale; + trajectoryFenceProjection.push([u, 0]); } - - // We don't care about depth/z when generatic the seismic image - trajectoryFenceProjection.push([u, 0]); } const options: SeismicSliceImageOptions = { @@ -284,7 +289,7 @@ export class SeismicLayer extends BaseLayer 0) { @@ -293,13 +298,13 @@ export class SeismicLayer extends BaseLayer { + protected async fetchData(queryClient: QueryClient): Promise { super.setBoundingBox(null); const sampledPolyline = this.samplePolyline(); @@ -332,7 +337,7 @@ export class SeismicLayer extends BaseLayer diff --git a/frontend/src/modules/Intersection/utils/layers/SurfaceLayer.ts b/frontend/src/modules/Intersection/utils/layers/SurfaceLayer.ts index 562970e17..fb1463738 100644 --- a/frontend/src/modules/Intersection/utils/layers/SurfaceLayer.ts +++ b/frontend/src/modules/Intersection/utils/layers/SurfaceLayer.ts @@ -16,7 +16,10 @@ const CACHE_TIME = 60 * 1000; export type SurfaceLayerSettings = { ensembleIdent: EnsembleIdent | null; realizationNum: number | null; - polylineUtmXy: number[]; + polyline: { + polylineUtmXy: number[]; + actualSectionLengths: number[]; + }; surfaceNames: string[]; attribute: string | null; extensionLength: number; @@ -26,17 +29,20 @@ export type SurfaceLayerSettings = { export class SurfaceLayer extends BaseLayer { private _colorSet: ColorSet; - constructor(name: string, queryClient: QueryClient) { + constructor(name: string) { const defaultSettings = { ensembleIdent: null, realizationNum: null, - polylineUtmXy: [], + polyline: { + polylineUtmXy: [], + actualSectionLengths: [], + }, surfaceNames: [], attribute: null, extensionLength: 0, resolution: 1, }; - super(name, defaultSettings, queryClient); + super(name, defaultSettings); this._colorSet = new ColorSet(defaultColorPalettes[0]); } @@ -92,7 +98,9 @@ export class SurfaceLayer extends BaseLayer 0 && this._settings.realizationNum !== null && - this._settings.polylineUtmXy.length > 0 && + this._settings.polyline.polylineUtmXy.length > 0 && + this._settings.polyline.actualSectionLengths.length === + this._settings.polyline.polylineUtmXy.length / 2 - 1 && this._settings.resolution > 0 ); } @@ -107,17 +115,17 @@ export class SurfaceLayer extends BaseLayer { + protected async fetchData(queryClient: QueryClient): Promise { const promises: Promise[] = []; super.setBoundingBox(null); - const polyline = this._settings.polylineUtmXy; + const polyline = this._settings.polyline.polylineUtmXy; const xPoints: number[] = []; const yPoints: number[] = []; @@ -129,7 +137,9 @@ export class SurfaceLayer extends BaseLayer apiService.surface.postGetSurfaceIntersection( diff --git a/frontend/src/modules/Intersection/utils/layers/SurfacesUncertaintyLayer.ts b/frontend/src/modules/Intersection/utils/layers/SurfacesUncertaintyLayer.ts new file mode 100644 index 000000000..2bec4a1ac --- /dev/null +++ b/frontend/src/modules/Intersection/utils/layers/SurfacesUncertaintyLayer.ts @@ -0,0 +1,252 @@ +import { SurfaceRealizationSampleValues_api } from "@api"; +import { apiService } from "@framework/ApiService"; +import { EnsembleIdent } from "@framework/EnsembleIdent"; +import { defaultColorPalettes } from "@framework/utils/colorPalettes"; +import { ColorSet } from "@lib/utils/ColorSet"; +import { Vector2D, pointDistance, vectorNormalize } from "@lib/utils/geometry"; +import { QueryClient } from "@tanstack/query-core"; + +import { isEqual } from "lodash"; + +import { BaseLayer, BoundingBox, LayerTopic } from "./BaseLayer"; + +const STALE_TIME = 60 * 1000; +const CACHE_TIME = 60 * 1000; + +export type SurfacesUncertaintyLayerSettings = { + ensembleIdent: EnsembleIdent | null; + realizationNums: number[]; + polyline: { + polylineUtmXy: number[]; + actualSectionLengths: number[]; + }; + surfaceNames: string[]; + attribute: string | null; + extensionLength: number; + resolution: number; +}; + +export type SurfaceUncertaintyData = { + surfaceName: string; + cumulatedLengths: number[]; + sampledValues: number[][]; +}; + +function transformData( + cumulatedLengths: number[], + surfaceName: string, + data: SurfaceRealizationSampleValues_api[] +): SurfaceUncertaintyData { + const sampledValues: number[][] = data.map((realization) => realization.sampled_values); + return { + surfaceName: surfaceName, + cumulatedLengths, + sampledValues, + }; +} + +export class SurfacesUncertaintyLayer extends BaseLayer { + private _colorSet: ColorSet; + + constructor(name: string) { + const defaultSettings = { + ensembleIdent: null, + realizationNums: [], + polyline: { + polylineUtmXy: [], + actualSectionLengths: [], + }, + surfaceNames: [], + attribute: null, + extensionLength: 0, + resolution: 1, + }; + super(name, defaultSettings); + + this._colorSet = new ColorSet(defaultColorPalettes[0]); + } + + getColorSet(): ColorSet { + return this._colorSet; + } + + setColorSet(colorSet: ColorSet): void { + this._colorSet = colorSet; + this.notifySubscribers(LayerTopic.DATA); + } + + private makeBoundingBox(): void { + if (!this._data) { + return; + } + + let minX = Number.MAX_VALUE; + let maxX = Number.MIN_VALUE; + let minY = Number.MAX_VALUE; + let maxY = Number.MIN_VALUE; + + for (const surface of this._data) { + let totalLength = 0; + for (let i = 2; i < this._settings.polyline.polylineUtmXy.length; i += 2) { + totalLength += pointDistance( + { + x: this._settings.polyline.polylineUtmXy[i], + y: this._settings.polyline.polylineUtmXy[i + 1], + }, + { + x: this._settings.polyline.polylineUtmXy[i - 2], + y: this._settings.polyline.polylineUtmXy[i - 1], + } + ); + } + minX = -this._settings.extensionLength; + maxX = Math.max(maxX, totalLength + this._settings.extensionLength); + for (const real of surface.sampledValues) { + for (const z of real) { + minY = Math.min(minY, z); + maxY = Math.max(maxY, z); + } + } + } + + super.setBoundingBox({ + x: [minX, maxX], + y: [minY, maxY], + z: [0, 0], + }); + } + + getBoundingBox(): BoundingBox | null { + const bbox = super.getBoundingBox(); + if (bbox) { + return bbox; + } + + this.makeBoundingBox(); + return super.getBoundingBox(); + } + + protected areSettingsValid(): boolean { + return ( + this._settings.ensembleIdent !== null && + this._settings.attribute !== null && + this._settings.surfaceNames.length > 0 && + this._settings.realizationNums.length > 0 && + this._settings.polyline.polylineUtmXy.length > 0 && + this._settings.polyline.actualSectionLengths.length === + this._settings.polyline.polylineUtmXy.length / 2 - 1 && + this._settings.resolution > 0 + ); + } + + protected doSettingsChangesRequireDataRefetch( + prevSettings: SurfacesUncertaintyLayerSettings, + newSettings: SurfacesUncertaintyLayerSettings + ): boolean { + return ( + !isEqual(prevSettings.surfaceNames, newSettings.surfaceNames) || + prevSettings.attribute !== newSettings.attribute || + !isEqual(prevSettings.realizationNums, newSettings.realizationNums) || + !isEqual(prevSettings.ensembleIdent, newSettings.ensembleIdent) || + prevSettings.extensionLength !== newSettings.extensionLength || + !isEqual(prevSettings.polyline.polylineUtmXy, newSettings.polyline.polylineUtmXy) || + prevSettings.resolution !== newSettings.resolution + ); + } + + protected async fetchData(queryClient: QueryClient): Promise { + const promises: Promise[] = []; + + super.setBoundingBox(null); + + const polyline = this._settings.polyline.polylineUtmXy; + + const xPoints: number[] = []; + const yPoints: number[] = []; + let cumulatedHorizontalPolylineLength = -this._settings.extensionLength; + const cumulatedHorizontalPolylineLengthArr: number[] = []; + for (let i = 0; i < polyline.length; i += 2) { + if (i > 0) { + const distance = pointDistance( + { x: polyline[i], y: polyline[i + 1] }, + { x: polyline[i - 2], y: polyline[i - 1] } + ); + const actualDistance = this._settings.polyline.actualSectionLengths[i / 2 - 1]; + const numPoints = Math.floor(distance / this._settings.resolution) - 1; + const scale = actualDistance / distance; + + for (let p = 1; p <= numPoints; p++) { + const vector: Vector2D = { + x: polyline[i] - polyline[i - 2], + y: polyline[i + 1] - polyline[i - 1], + }; + const normalizedVector = vectorNormalize(vector); + xPoints.push(polyline[i - 2] + normalizedVector.x * this._settings.resolution * p); + yPoints.push(polyline[i - 1] + normalizedVector.y * this._settings.resolution * p); + cumulatedHorizontalPolylineLength += this._settings.resolution * scale; + cumulatedHorizontalPolylineLengthArr.push(cumulatedHorizontalPolylineLength); + } + } + + xPoints.push(polyline[i]); + yPoints.push(polyline[i + 1]); + + if (i > 0) { + const distance = pointDistance( + { x: polyline[i], y: polyline[i + 1] }, + { x: xPoints[xPoints.length - 1], y: yPoints[yPoints.length - 1] } + ); + + cumulatedHorizontalPolylineLength += distance; + } + + cumulatedHorizontalPolylineLengthArr.push(cumulatedHorizontalPolylineLength); + } + + const queryBody = { + sample_points: { + x_points: xPoints, + y_points: yPoints, + }, + }; + + for (const surfaceName of this._settings.surfaceNames) { + const queryKey = [ + "getSurfaceIntersection", + this._settings.ensembleIdent?.getCaseUuid() ?? "", + this._settings.ensembleIdent?.getEnsembleName() ?? "", + this._settings.realizationNums, + surfaceName, + this._settings.attribute ?? "", + this._settings.polyline.polylineUtmXy, + this._settings.extensionLength, + this._settings.resolution, + ]; + this.registerQueryKey(queryKey); + + const promise = queryClient + .fetchQuery({ + queryKey, + queryFn: () => + apiService.surface.postSampleSurfaceInPoints( + this._settings.ensembleIdent?.getCaseUuid() ?? "", + this._settings.ensembleIdent?.getEnsembleName() ?? "", + surfaceName, + this._settings.attribute ?? "", + this._settings.realizationNums, + queryBody + ), + staleTime: STALE_TIME, + gcTime: CACHE_TIME, + }) + .then((data) => transformData(cumulatedHorizontalPolylineLengthArr, surfaceName, data)); + promises.push(promise); + } + + return Promise.all(promises); + } +} + +export function isSurfacesUncertaintyLayer(layer: BaseLayer): layer is SurfacesUncertaintyLayer { + return layer instanceof SurfacesUncertaintyLayer; +} diff --git a/frontend/src/modules/Intersection/utils/layers/WellpicksLayer.ts b/frontend/src/modules/Intersection/utils/layers/WellpicksLayer.ts index c98239aa4..3da88f844 100644 --- a/frontend/src/modules/Intersection/utils/layers/WellpicksLayer.ts +++ b/frontend/src/modules/Intersection/utils/layers/WellpicksLayer.ts @@ -21,7 +21,7 @@ export type WellpicksLayerSettings = { export type WellPicksLayerData = ReturnType; export class WellpicksLayer extends BaseLayer { - constructor(name: string, queryClient: QueryClient) { + constructor(name: string) { const defaultSettings = { ensembleIdent: null, wellboreUuid: null, @@ -29,7 +29,7 @@ export class WellpicksLayer extends BaseLayer { + protected async fetchData(queryClient: QueryClient): Promise { const queryKey = [ "getWellborePicksAndStratigraphicUnits", this._settings.ensembleIdent?.getCaseUuid(), @@ -72,7 +72,7 @@ export class WellpicksLayer extends BaseLayer diff --git a/frontend/src/modules/Intersection/utils/layers/types.ts b/frontend/src/modules/Intersection/utils/layers/types.ts new file mode 100644 index 000000000..491d50bdc --- /dev/null +++ b/frontend/src/modules/Intersection/utils/layers/types.ts @@ -0,0 +1,15 @@ +export enum LayerType { + GRID = "grid", + SEISMIC = "seismic", + SURFACES = "surfaces", + WELLPICKS = "wellpicks", + SURFACES_UNCERTAINTY = "surfaces-uncertainty", +} + +export const LAYER_TYPE_TO_STRING_MAPPING = { + [LayerType.GRID]: "Grid", + [LayerType.SEISMIC]: "Seismic", + [LayerType.SURFACES]: "Surfaces", + [LayerType.WELLPICKS]: "Wellpicks", + [LayerType.SURFACES_UNCERTAINTY]: "Surfaces Uncertainty", +}; diff --git a/frontend/src/modules/Intersection/view/atoms/atomDefinitions.ts b/frontend/src/modules/Intersection/view/atoms/atomDefinitions.ts index 4732e0afa..2bfc3c4d2 100644 --- a/frontend/src/modules/Intersection/view/atoms/atomDefinitions.ts +++ b/frontend/src/modules/Intersection/view/atoms/atomDefinitions.ts @@ -1,4 +1,6 @@ +import { WellboreTrajectory_api } from "@api"; import { IntersectionReferenceSystem } from "@equinor/esv-intersection"; +import { apiService } from "@framework/ApiService"; import { ModuleAtoms } from "@framework/Module"; import { UniDirectionalSettingsToViewInterface } from "@framework/UniDirectionalSettingsToViewInterface"; import { IntersectionType } from "@framework/types/intersection"; @@ -6,24 +8,22 @@ import { IntersectionPolylinesAtom } from "@framework/userCreatedItems/Intersect import { arrayPointToPoint2D, pointDistance } from "@lib/utils/geometry"; import { SettingsToViewInterface } from "@modules/Intersection/settingsToViewInterface"; import { CURVE_FITTING_EPSILON } from "@modules/Intersection/typesAndEnums"; -import { BaseLayer } from "@modules/Intersection/utils/layers/BaseLayer"; -import { isGridLayer } from "@modules/Intersection/utils/layers/GridLayer"; -import { isSeismicLayer } from "@modules/Intersection/utils/layers/SeismicLayer"; -import { isSurfaceLayer } from "@modules/Intersection/utils/layers/SurfaceLayer"; -import { isWellpicksLayer } from "@modules/Intersection/utils/layers/WellpicksLayer"; import { calcExtendedSimplifiedWellboreTrajectoryInXYPlane } from "@modules/_shared/utils/wellbore"; +import { QueryObserverResult } from "@tanstack/react-query"; import { atom } from "jotai"; +import { atomWithQuery } from "jotai-tanstack-query"; -import { wellboreTrajectoryQueryAtom } from "./queryAtoms"; +const STALE_TIME = 60 * 1000; +const CACHE_TIME = 60 * 1000; export type ViewAtoms = { - layers: BaseLayer[]; intersectionReferenceSystemAtom: IntersectionReferenceSystem | null; polylineAtom: { polylineUtmXy: number[]; actualSectionLengths: number[]; }; + wellboreTrajectoryQueryAtom: QueryObserverResult; }; export function viewAtomsInitialization( @@ -118,39 +118,22 @@ export function viewAtomsInitialization( }; }); - const layers = atom((get) => { - const layers = get(settingsToViewInterface.getAtom("layers")); - const ensembleIdent = get(settingsToViewInterface.getAtom("ensembleIdent")); + const wellboreTrajectoryQueryAtom = atomWithQuery((get) => { const wellbore = get(settingsToViewInterface.getAtom("wellboreHeader")); - const polyline = get(polylineAtom); - const extensionLength = get(settingsToViewInterface.getAtom("intersectionExtensionLength")); - const intersectionType = get(settingsToViewInterface.getAtom("intersectionType")); - for (const layer of layers) { - if (isGridLayer(layer)) { - layer.maybeUpdateSettings({ polyline, extensionLength }); - } - if (isSeismicLayer(layer)) { - layer.maybeUpdateSettings({ polylineUtmXy: polyline.polylineUtmXy, extensionLength }); - } - if (isSurfaceLayer(layer)) { - layer.maybeUpdateSettings({ polylineUtmXy: polyline.polylineUtmXy, extensionLength }); - } - if (isWellpicksLayer(layer)) { - layer.maybeUpdateSettings({ - ensembleIdent, - wellboreUuid: intersectionType === IntersectionType.WELLBORE ? wellbore?.uuid : null, - }); - } - layer.maybeRefetchData(); - } - - return layers; + return { + queryKey: ["getWellboreTrajectory", wellbore?.uuid ?? ""], + queryFn: () => apiService.well.getWellTrajectories(wellbore?.uuid ? [wellbore.uuid] : []), + staleTime: STALE_TIME, + gcTime: CACHE_TIME, + select: (data: WellboreTrajectory_api[]) => data[0], + enabled: wellbore?.uuid ? true : false, + }; }); return { - layers, intersectionReferenceSystemAtom, polylineAtom, + wellboreTrajectoryQueryAtom, }; } diff --git a/frontend/src/modules/Intersection/view/atoms/queryAtoms.ts b/frontend/src/modules/Intersection/view/atoms/queryAtoms.ts deleted file mode 100644 index b4c0aec07..000000000 --- a/frontend/src/modules/Intersection/view/atoms/queryAtoms.ts +++ /dev/null @@ -1,21 +0,0 @@ -import { WellboreTrajectory_api } from "@api"; -import { apiService } from "@framework/ApiService"; -import { selectedWellboreAtom } from "@modules/Intersection/sharedAtoms/sharedAtoms"; - -import { atomWithQuery } from "jotai-tanstack-query"; - -const STALE_TIME = 60 * 1000; -const CACHE_TIME = 60 * 1000; - -export const wellboreTrajectoryQueryAtom = atomWithQuery((get) => { - const wellbore = get(selectedWellboreAtom); - - return { - queryKey: ["getWellboreTrajectory", wellbore?.uuid ?? ""], - queryFn: () => apiService.well.getWellTrajectories(wellbore?.uuid ? [wellbore.uuid] : []), - staleTime: STALE_TIME, - gcTime: CACHE_TIME, - select: (data: WellboreTrajectory_api[]) => data[0], - enabled: wellbore?.uuid ? true : false, - }; -}); diff --git a/frontend/src/modules/Intersection/view/components/layersWrapper.tsx b/frontend/src/modules/Intersection/view/components/layersWrapper.tsx index 03d05202e..d14f3117b 100644 --- a/frontend/src/modules/Intersection/view/components/layersWrapper.tsx +++ b/frontend/src/modules/Intersection/view/components/layersWrapper.tsx @@ -6,6 +6,7 @@ import { IntersectionReferenceSystem, ReferenceLine, SurfaceData, + SurfaceLine, getPicksData, getSeismicOptions, } from "@equinor/esv-intersection"; @@ -13,6 +14,8 @@ import { ViewContext } from "@framework/ModuleContext"; import { WorkbenchServices } from "@framework/WorkbenchServices"; import { LayerItem, LayerType } from "@framework/components/EsvIntersection"; import { Viewport } from "@framework/components/EsvIntersection/esvIntersection"; +import { SurfaceStatisticalFanchart } from "@framework/components/EsvIntersection/layers/SurfaceStatisticalFanchartCanvasLayer"; +import { makeSurfaceStatisticalFanchartFromRealizationSurface } from "@framework/components/EsvIntersection/utils/surfaceStatisticalFancharts"; import { IntersectionType } from "@framework/types/intersection"; import { useElementBoundingRect } from "@lib/hooks/useElementBoundingRect"; import { SettingsToViewInterface } from "@modules/Intersection/settingsToViewInterface"; @@ -21,6 +24,7 @@ import { BaseLayer, LayerStatus, useLayers } from "@modules/Intersection/utils/l import { GridLayer, isGridLayer } from "@modules/Intersection/utils/layers/GridLayer"; import { SeismicLayer, isSeismicLayer } from "@modules/Intersection/utils/layers/SeismicLayer"; import { isSurfaceLayer } from "@modules/Intersection/utils/layers/SurfaceLayer"; +import { isSurfacesUncertaintyLayer } from "@modules/Intersection/utils/layers/SurfacesUncertaintyLayer"; import { isWellpicksLayer } from "@modules/Intersection/utils/layers/WellpicksLayer"; import { ColorLegendsContainer } from "@modules_shared/components/ColorLegendsContainer"; @@ -150,7 +154,10 @@ export function LayersWrapper(props: LayersWrapperProps): React.ReactNode { }; let boundsSetByLayer: boolean = false; - for (const [index, layer] of layers.toReversed().entries()) { + for (let i = layers.length - 1; i >= 0; i--) { + const layer = layers[i]; + const order = layers.length - i; + if (!layer.getIsVisible() || layer.getStatus() !== LayerStatus.SUCCESS) { continue; } @@ -211,7 +218,7 @@ export function LayersWrapper(props: LayersWrapperProps): React.ReactNode { propertyName: layer.getSettings().parameterName ?? "", propertyUnit: "", }, - order: index, + order, }, }); @@ -256,7 +263,7 @@ export function LayersWrapper(props: LayersWrapperProps): React.ReactNode { numTraces: data.seismicFenceData.num_traces, fenceTracesFloat32Array: data.seismicFenceData.fenceTracesFloat32Arr, }, - order: index, + order, layerOpacity: 1, }, hoverable: true, @@ -294,7 +301,7 @@ export function LayersWrapper(props: LayersWrapperProps): React.ReactNode { hoverable: true, options: { data: surfaceData, - order: index, + order, referenceSystem: props.referenceSystem ?? undefined, }, }); @@ -304,7 +311,63 @@ export function LayersWrapper(props: LayersWrapperProps): React.ReactNode { type: LayerType.GEOMODEL_LABELS, options: { data: surfaceData, - order: index, + order, + referenceSystem: props.referenceSystem ?? undefined, + }, + }); + } + + if (isSurfacesUncertaintyLayer(layer)) { + const surfaceLayer = layer; + const data = surfaceLayer.getData(); + + if (!data) { + continue; + } + + const colorSet = surfaceLayer.getColorSet(); + + let currentColor = colorSet.getFirstColor(); + const labelData: SurfaceLine[] = []; + const fancharts: SurfaceStatisticalFanchart[] = []; + + for (const surface of data) { + const fanchart = makeSurfaceStatisticalFanchartFromRealizationSurface( + surface.sampledValues, + surface.cumulatedLengths, + surface.surfaceName, + currentColor + ); + labelData.push({ + data: fanchart.data.mean, + color: currentColor, + label: surface.surfaceName, + }); + currentColor = colorSet.getNextColor(); + fancharts.push(fanchart); + } + + esvLayers.push({ + id: `${layer.getId()}-surfaces-uncertainty`, + type: LayerType.SURFACE_STATISTICAL_FANCHARTS_CANVAS, + hoverable: true, + options: { + data: { + fancharts, + }, + order, + referenceSystem: props.referenceSystem ?? undefined, + }, + }); + + esvLayers.push({ + id: `${layer.getId()}-surfaces-uncertainty-labels`, + type: LayerType.GEOMODEL_LABELS, + options: { + data: { + areas: [], + lines: labelData, + }, referenceSystem: props.referenceSystem ?? undefined, }, }); @@ -324,7 +387,7 @@ export function LayersWrapper(props: LayersWrapperProps): React.ReactNode { hoverable: false, options: { data: getPicksData(data), - order: index, + order, referenceSystem: props.referenceSystem ?? undefined, }, }); diff --git a/frontend/src/modules/Intersection/view/view.tsx b/frontend/src/modules/Intersection/view/view.tsx index e85bf2703..10fb4826b 100644 --- a/frontend/src/modules/Intersection/view/view.tsx +++ b/frontend/src/modules/Intersection/view/view.tsx @@ -7,17 +7,19 @@ import { IntersectionType } from "@framework/types/intersection"; import { CircularProgress } from "@lib/components/CircularProgress"; import { resolveClassNames } from "@lib/utils/resolveClassNames"; -import { useAtomValue } from "jotai"; - import { ViewAtoms } from "./atoms/atomDefinitions"; -import { wellboreTrajectoryQueryAtom } from "./atoms/queryAtoms"; import { LayersWrapper } from "./components/layersWrapper"; import { useWellboreCasingsQuery } from "./queries/wellboreSchematicsQueries"; import { SettingsToViewInterface } from "../settingsToViewInterface"; -import { selectedWellboreAtom } from "../sharedAtoms/sharedAtoms"; import { State } from "../state"; import { LayerStatus, useLayersStatuses } from "../utils/layers/BaseLayer"; +import { isGridLayer } from "../utils/layers/GridLayer"; +import { LayerManagerTopic, useLayerManagerTopicValue } from "../utils/layers/LayerManager"; +import { isSeismicLayer } from "../utils/layers/SeismicLayer"; +import { isSurfaceLayer } from "../utils/layers/SurfaceLayer"; +import { isSurfacesUncertaintyLayer } from "../utils/layers/SurfacesUncertaintyLayer"; +import { isWellpicksLayer } from "../utils/layers/WellpicksLayer"; export function View( props: ModuleViewProps, ViewAtoms> @@ -27,16 +29,59 @@ export function View( const ensembleIdent = props.viewContext.useSettingsToViewInterfaceValue("ensembleIdent"); const intersectionReferenceSystem = props.viewContext.useViewAtomValue("intersectionReferenceSystemAtom"); - const wellboreHeader = useAtomValue(selectedWellboreAtom); - const wellboreTrajectoryQuery = useAtomValue(wellboreTrajectoryQueryAtom); + const wellboreHeader = props.viewContext.useSettingsToViewInterfaceValue("wellboreHeader"); + const wellboreTrajectoryQuery = props.viewContext.useViewAtomValue("wellboreTrajectoryQueryAtom"); + const polyline = props.viewContext.useViewAtomValue("polylineAtom"); + const extensionLength = props.viewContext.useSettingsToViewInterfaceValue("intersectionExtensionLength"); + const wellbore = props.viewContext.useSettingsToViewInterfaceValue("wellboreHeader"); - const layers = props.viewContext.useViewAtomValue("layers"); + const layerManager = props.viewContext.useSettingsToViewInterfaceValue("layerManager"); + const layers = useLayerManagerTopicValue(layerManager, LayerManagerTopic.LAYERS_CHANGED); const layersStatuses = useLayersStatuses(layers); const intersectionExtensionLength = props.viewContext.useSettingsToViewInterfaceValue("intersectionExtensionLength"); const intersectionType = props.viewContext.useSettingsToViewInterfaceValue("intersectionType"); + React.useEffect( + function handleLayerSettingsChange() { + for (const layer of layers) { + if (isGridLayer(layer)) { + layer.maybeUpdateSettings({ polyline, extensionLength }); + layer.maybeRefetchData(); + } + if (isSeismicLayer(layer)) { + layer.maybeUpdateSettings({ polyline, extensionLength }); + layer.maybeRefetchData(); + } + if (isSurfaceLayer(layer)) { + layer.maybeUpdateSettings({ polyline, extensionLength }); + layer.maybeRefetchData(); + } + if (isSurfacesUncertaintyLayer(layer)) { + layer.maybeUpdateSettings({ polyline, extensionLength }); + layer.maybeRefetchData(); + } + } + }, + [polyline, extensionLength, layers] + ); + + React.useEffect( + function handleWellpicksLayerSettingsChange() { + for (const layer of layers) { + if (isWellpicksLayer(layer)) { + layer.maybeUpdateSettings({ + ensembleIdent, + wellboreUuid: intersectionType === IntersectionType.WELLBORE ? wellbore?.uuid : null, + }); + layer.maybeRefetchData(); + } + } + }, + [layers, wellbore, ensembleIdent, intersectionType] + ); + React.useEffect( function handleTitleChange() { let ensembleName = ""; diff --git a/frontend/src/modules/SubsurfaceMap/settings.tsx b/frontend/src/modules/SubsurfaceMap/settings.tsx index 8a56321fe..bd4340e5b 100644 --- a/frontend/src/modules/SubsurfaceMap/settings.tsx +++ b/frontend/src/modules/SubsurfaceMap/settings.tsx @@ -350,7 +350,15 @@ export function Settings({ settingsContext, workbenchSession, workbenchServices [show3D, settingsContext] ); - const wellHeadersQuery = useDrilledWellboreHeadersQuery(computedEnsembleIdent?.getCaseUuid()); + let fieldIdentifier: null | string = null; + if (computedEnsembleIdent) { + const ensembleIdent = new EnsembleIdent( + computedEnsembleIdent.getCaseUuid(), + computedEnsembleIdent.getEnsembleName() + ); + fieldIdentifier = ensembleSet.findEnsemble(ensembleIdent)?.getFieldIdentifier() ?? null; + } + const wellHeadersQuery = useDrilledWellboreHeadersQuery(fieldIdentifier ?? ""); let wellHeaderOptions: SelectOption[] = []; if (wellHeadersQuery.data) { diff --git a/frontend/src/modules/SubsurfaceMap/view.tsx b/frontend/src/modules/SubsurfaceMap/view.tsx index 5ad55e77d..c52835c78 100644 --- a/frontend/src/modules/SubsurfaceMap/view.tsx +++ b/frontend/src/modules/SubsurfaceMap/view.tsx @@ -2,8 +2,10 @@ import React from "react"; import { PolygonData_api, WellboreTrajectory_api } from "@api"; import { ContinuousLegend } from "@emerson-eps/color-tables"; +import { EnsembleIdent } from "@framework/EnsembleIdent"; import { ModuleViewProps } from "@framework/Module"; import { SyncSettingKey, SyncSettingsHelper } from "@framework/SyncSettings"; +import { useEnsembleSet } from "@framework/WorkbenchSession"; import { Wellbore } from "@framework/types/wellbore"; import { Button } from "@lib/components/Button"; import { CircularProgress } from "@lib/components/CircularProgress"; @@ -56,7 +58,7 @@ const updateViewPortBounds = ( return existingViewPortBounds; }; //----------------------------------------------------------------------------------------------------------- -export function View({ viewContext, workbenchSettings, workbenchServices }: ModuleViewProps) { +export function View({ viewContext, workbenchSettings, workbenchServices, workbenchSession }: ModuleViewProps) { const myInstanceIdStr = viewContext.getInstanceIdString(); console.debug(`${myInstanceIdStr} -- render TopographicMap view`); const viewIds = { @@ -66,6 +68,8 @@ export function View({ viewContext, workbenchSettings, workbenchServices }: Modu annotation3D: `${myInstanceIdStr} -- annotation3D`, }; + const ensembleSet = useEnsembleSet(workbenchSession); + const meshSurfAddr = viewContext.useStoreValue("meshSurfaceAddress"); const propertySurfAddr = viewContext.useStoreValue("propertySurfaceAddress"); const polygonsAddr = viewContext.useStoreValue("polygonsAddress"); @@ -88,7 +92,12 @@ export function View({ viewContext, workbenchSettings, workbenchServices }: Modu const hasMeshSurfData = meshSurfDataQuery?.data ? true : false; const propertySurfDataQuery = usePropertySurfaceDataByQueryAddress(meshSurfAddr, propertySurfAddr, hasMeshSurfData); - const wellTrajectoriesQuery = useFieldWellboreTrajectoriesQuery(meshSurfAddr?.caseUuid); + let fieldIdentifier: null | string = null; + if (meshSurfAddr) { + const ensembleIdent = new EnsembleIdent(meshSurfAddr.caseUuid, meshSurfAddr.ensemble); + fieldIdentifier = ensembleSet.findEnsemble(ensembleIdent)?.getFieldIdentifier() ?? null; + } + const wellTrajectoriesQuery = useFieldWellboreTrajectoriesQuery(fieldIdentifier ?? undefined); const polygonsQuery = usePolygonsDataQueryByAddress(polygonsAddr); const newLayers: Record[] = [createNorthArrowLayer()]; diff --git a/frontend/src/modules/_shared/WellBore/queryHooks.ts b/frontend/src/modules/_shared/WellBore/queryHooks.ts index 0e5882ef3..a110026d5 100644 --- a/frontend/src/modules/_shared/WellBore/queryHooks.ts +++ b/frontend/src/modules/_shared/WellBore/queryHooks.ts @@ -16,14 +16,14 @@ export function useDrilledWellboreHeadersQuery(caseUuid: string | undefined): Us } export function useFieldWellboreTrajectoriesQuery( - caseUuid: string | undefined + fieldIdentifier: string | undefined ): UseQueryResult { return useQuery({ - queryKey: ["getFieldWellsTrajectories", caseUuid], - queryFn: () => apiService.well.getFieldWellTrajectories(caseUuid ?? ""), + queryKey: ["getFieldWellsTrajectories", fieldIdentifier], + queryFn: () => apiService.well.getFieldWellTrajectories(fieldIdentifier ?? ""), staleTime: STALE_TIME, gcTime: CACHE_TIME, - enabled: caseUuid ? true : false, + enabled: fieldIdentifier ? true : false, }); } diff --git a/frontend/src/modules/_shared/components/ColorScaleSelector/colorScaleSelector.tsx b/frontend/src/modules/_shared/components/ColorScaleSelector/colorScaleSelector.tsx index 1b9e64c45..19a5c566b 100644 --- a/frontend/src/modules/_shared/components/ColorScaleSelector/colorScaleSelector.tsx +++ b/frontend/src/modules/_shared/components/ColorScaleSelector/colorScaleSelector.tsx @@ -445,8 +445,8 @@ function MinMaxDivMidPointSetter(props: MinMaxDivMidPointSetterProps): React.Rea [onChange, onChangePreview, min, max] ); - function handleMinChange(e: React.ChangeEvent) { - let newMin = parseFloat(e.target.value); + function handleMinChange(value: string) { + let newMin = parseFloat(value); let newDivMidPoint = divMidPoint; if (newMin >= max) { newMin = max - 0.000001; @@ -458,8 +458,8 @@ function MinMaxDivMidPointSetter(props: MinMaxDivMidPointSetterProps): React.Rea props.onChange(newMin, max, newDivMidPoint); } - function handleMaxChange(e: React.ChangeEvent) { - let newMax = parseFloat(e.target.value); + function handleMaxChange(value: string) { + let newMax = parseFloat(value); let newDivMidPoint = divMidPoint; if (newMax <= min) { newMax = min + 0.000001; @@ -471,8 +471,8 @@ function MinMaxDivMidPointSetter(props: MinMaxDivMidPointSetterProps): React.Rea props.onChange(min, newMax, newDivMidPoint); } - function handleDivMidPointChange(e: React.ChangeEvent) { - let newDivMidPoint = parseFloat(e.target.value); + function handleDivMidPointChange(value: string) { + let newDivMidPoint = parseFloat(value); if (newDivMidPoint <= min) { newDivMidPoint = min; } @@ -492,7 +492,7 @@ function MinMaxDivMidPointSetter(props: MinMaxDivMidPointSetterProps): React.Rea <> {isDragging && createPortal(
)} -
+
-
+