From b60aed13e22f3363168fa86fd42c4c78dcd5a97b Mon Sep 17 00:00:00 2001 From: Ruben Thoms Date: Fri, 12 Apr 2024 17:20:25 +0200 Subject: [PATCH] wip Co-authored-by: Hans Kallekleiv --- .../src/lib/components/Checkbox/checkbox.tsx | 4 +- .../src/lib/components/Overlay/overlay.tsx | 4 +- .../settings/atoms/baseAtoms.ts | 1 + .../Grid3DIntersection/settings/settings.tsx | 314 ++++++++++++- .../settingsToViewInterface.ts | 2 +- .../sharedAtoms/sharedAtoms.ts | 32 +- .../Grid3DIntersection/typesAndEnums.ts | 10 + .../view/atoms/derivedAtoms.ts | 63 ++- .../view/components/grid3d.tsx | 412 ++++++++++++++---- .../view/components/intersection.tsx | 12 +- .../modules/Grid3DIntersection/view/view.tsx | 56 ++- 11 files changed, 761 insertions(+), 149 deletions(-) diff --git a/frontend/src/lib/components/Checkbox/checkbox.tsx b/frontend/src/lib/components/Checkbox/checkbox.tsx index c59ae2a9a..6b1aa6649 100644 --- a/frontend/src/lib/components/Checkbox/checkbox.tsx +++ b/frontend/src/lib/components/Checkbox/checkbox.tsx @@ -27,7 +27,9 @@ export const Checkbox: React.FC = (props) => { const handleChange = React.useCallback( (event: React.ChangeEvent) => { - setChecked(event.target.checked); + if (props.checked === undefined) { + setChecked(event.target.checked); + } onChange && onChange(event, event.target.checked); }, [setChecked, onChange] diff --git a/frontend/src/lib/components/Overlay/overlay.tsx b/frontend/src/lib/components/Overlay/overlay.tsx index c493f9f04..a4cda67ca 100644 --- a/frontend/src/lib/components/Overlay/overlay.tsx +++ b/frontend/src/lib/components/Overlay/overlay.tsx @@ -1,11 +1,13 @@ import React from "react"; +import { createPortal } from "@lib/utils/createPortal"; + export type OverlayProps = { visible: boolean; }; export const Overlay: React.FC = (props: OverlayProps) => { - return ( + return createPortal(
(null); export const userSelectedGridModelParameterNameAtom = atom(null); export const userSelectedGridModelParameterDateOrIntervalAtom = atom(null); export const userSelectedWellboreUuidAtom = atom(null); +export const userSelectedCustomIntersectionPolylineIdAtom = atom(null); diff --git a/frontend/src/modules/Grid3DIntersection/settings/settings.tsx b/frontend/src/modules/Grid3DIntersection/settings/settings.tsx index d1297d411..4eaff609a 100644 --- a/frontend/src/modules/Grid3DIntersection/settings/settings.tsx +++ b/frontend/src/modules/Grid3DIntersection/settings/settings.tsx @@ -1,21 +1,29 @@ +import React from "react"; + import { Grid3dInfo_api, Grid3dPropertyInfo_api, WellboreHeader_api } from "@api"; import { EnsembleIdent } from "@framework/EnsembleIdent"; import { ModuleSettingsProps } from "@framework/Module"; import { useSettingsStatusWriter } from "@framework/StatusWriter"; import { EnsembleDropdown } from "@framework/components/EnsembleDropdown"; -import { CircularProgress } from "@lib/components/CircularProgress"; +import { Button } from "@lib/components/Button"; import { CollapsibleGroup } from "@lib/components/CollapsibleGroup"; +import { Dialog } from "@lib/components/Dialog"; import { Dropdown } from "@lib/components/Dropdown"; import { Input } from "@lib/components/Input"; import { Label } from "@lib/components/Label"; import { PendingWrapper } from "@lib/components/PendingWrapper"; -import { QueryStateWrapper } from "@lib/components/QueryStateWrapper"; +import { Radio } from "@lib/components/RadioGroup"; import { Select, SelectOption } from "@lib/components/Select"; import { Switch } from "@lib/components/Switch"; +import { TableSelect, TableSelectOption } from "@lib/components/TableSelect"; +import { resolveClassNames } from "@lib/utils/resolveClassNames"; +import { Check, Delete, Edit } from "@mui/icons-material"; -import { useAtomValue, useSetAtom } from "jotai"; +import { useAtom, useAtomValue, useSetAtom } from "jotai"; +import { v4 } from "uuid"; import { + userSelectedCustomIntersectionPolylineIdAtom, userSelectedEnsembleIdentAtom, userSelectedGridModelNameAtom, userSelectedGridModelParameterDateOrIntervalAtom, @@ -34,8 +42,19 @@ import { import { drilledWellboreHeadersQueryAtom, gridModelInfosQueryAtom } from "./atoms/queryAtoms"; import { SettingsToViewInterface } from "../settingsToViewInterface"; -import { selectedEnsembleIdentAtom, selectedWellboreUuidAtom } from "../sharedAtoms/sharedAtoms"; +import { + addCustomIntersectionPolylineEditModeActiveAtom, + currentCustomIntersectionPolylineAtom, + customIntersectionPolylinesAtom, + editCustomIntersectionPolylineEditModeActiveAtom, + intersectionTypeAtom, + selectedCustomIntersectionPolylineIdAtom, + selectedEnsembleIdentAtom, + selectedWellboreUuidAtom, +} from "../sharedAtoms/sharedAtoms"; import { State } from "../state"; +import { CustomIntersectionPolyline, IntersectionType } from "../typesAndEnums"; +import { selectedCustomIntersectionPolylineAtom } from "../view/atoms/derivedAtoms"; export function Settings(props: ModuleSettingsProps): JSX.Element { const ensembleSet = props.workbenchSession.getEnsembleSet(); @@ -46,6 +65,12 @@ export function Settings(props: ModuleSettingsProps(false); + const [currentCustomPolylineName, setCurrentCustomPolylineName] = React.useState(""); + const [currentCustomPolylineNameMessage, setCurrentCustomPolylineNameMessage] = React.useState(null); + + const polylineNameInputRef = React.useRef(null); + let gridModelErrorMessage = ""; if (gridModelInfos.isError) { statusWriter.addError("Failed to load grid model infos"); @@ -123,6 +165,143 @@ export function Settings(props: ModuleSettingsProps + prev.map((el) => { + if (el.id === selectedCustomIntersectionPolylineId) { + return { + ...el, + polyline: currentCustomIntersectionPolyline, + }; + } + return el; + }) + ); + setPolylineEditModeActive(false); + setCurrentCustomIntersectionPolyline([]); + } + + function handleCustomPolylineSelectionChange(customPolylineId: string[]) { + setSelectedCustomIntersectionPolylineId(customPolylineId.at(0) ?? null); + } + + function maybeSaveAndApplyCustomIntersectionPolyline() { + if (currentCustomPolylineName === "") { + setCurrentCustomPolylineNameMessage("Name must not be empty"); + return; + } + + if (availableCustomIntersectionPolylines.some((el) => el.name === currentCustomPolylineName)) { + setCurrentCustomPolylineNameMessage("A polyline with this name already exists"); + return; + } + + const uuid = v4(); + const newCustomIntersectionPolyline: CustomIntersectionPolyline = { + id: uuid, + name: currentCustomPolylineName, + polyline: currentCustomIntersectionPolyline, + }; + setSelectedCustomIntersectionPolylineId(uuid); + setAvailableCustomIntersectionPolylines([ + ...availableCustomIntersectionPolylines, + newCustomIntersectionPolyline, + ]); + setPolylineAddModeActive(false); + setPolylineEditModeActive(false); + setCurrentCustomPolylineName(""); + setCurrentCustomPolylineNameMessage(null); + setCurrentCustomIntersectionPolyline([]); + setShowDialog(false); + } + + function handleInputKeyDown(event: React.KeyboardEvent) { + if (event.key === "Enter") { + maybeSaveAndApplyCustomIntersectionPolyline(); + } + } + + function discardCustomIntersectionPolyline() { + setCurrentCustomIntersectionPolyline([]); + setPolylineAddModeActive(false); + setPolylineEditModeActive(false); + setCurrentCustomPolylineName(""); + setCurrentCustomPolylineNameMessage(null); + setShowDialog(false); + } + + function handleCurrentCustomPolylineNameChange(event: React.ChangeEvent) { + setCurrentCustomPolylineName(event.target.value); + } + + function handleRemoveCustomPolyline() { + setAvailableCustomIntersectionPolylines((prev) => + prev.filter((el) => el.id !== selectedCustomIntersectionPolylineId) + ); + setSelectedCustomIntersectionPolylineId(null); + setPolylineAddModeActive(false); + setPolylineEditModeActive(false); + setCurrentCustomIntersectionPolyline([]); + } + + React.useEffect(() => { + function handleKeyboardEvent(event: KeyboardEvent) { + if (!polylineAddModeActive && !polylineEditModeActive) { + return; + } + + if (event.key === "Escape") { + discardCustomIntersectionPolyline(); + } + + if (event.key === "Enter") { + if (polylineAddModeActive) { + setShowDialog(true); + } + if (polylineEditModeActive) { + handleEditPolylineModeChange(); + } + } + } + + document.addEventListener("keydown", handleKeyboardEvent); + + return () => { + document.removeEventListener("keydown", handleKeyboardEvent); + }; + }, [polylineAddModeActive, polylineEditModeActive]); + + React.useEffect( + function handleShowDialog() { + if (showDialog && polylineNameInputRef.current) { + polylineNameInputRef.current.getElementsByTagName("input")[0].focus(); + } + }, + [showDialog] + ); + const realizationOptions = makeRealizationOptions(availableRealizations); const gridModelInfo = gridModelInfos.data?.find((info) => info.grid_name === selectedGridModelName) ?? null; const datesOrIntervalsForSelectedParameter = @@ -196,34 +375,126 @@ export function Settings(props: ModuleSettingsProps
- - - + +
+ handleIntersectionTypeChange(IntersectionType.CUSTOM_POLYLINE)} + label={Use custom polyline} + /> + +
+ {polylineEditModeActive ? ( + + ) : ( + + )} +
+
+ +
+
+ )} + value={selectedCustomIntersectionPolylineId ? [selectedCustomIntersectionPolylineId] : []} + headerLabels={["Polyline name", "Actions"]} + onChange={handleCustomPolylineSelectionChange} + size={5} + columnSizesInPercent={[80, 20]} + disabled={intersectionType !== IntersectionType.CUSTOM_POLYLINE || polylineAddModeActive} + /> + + +
+ + Discard + , + , + ]} + modal + > + + ); } @@ -275,3 +546,14 @@ function makeWellHeaderOptions(wellHeaders: WellboreHeader_api[]): SelectOption[ label: wellHeader.unique_wellbore_identifier, })); } + +function makeCustomIntersectionPolylineOptions( + polylines: CustomIntersectionPolyline[], + selectedId: string | null, + actions: React.ReactNode +): TableSelectOption[] { + return polylines.map((polyline) => ({ + id: polyline.id, + values: [{ label: polyline.name }, { label: "", adornment: selectedId === polyline.id ? actions : undefined }], + })); +} diff --git a/frontend/src/modules/Grid3DIntersection/settingsToViewInterface.ts b/frontend/src/modules/Grid3DIntersection/settingsToViewInterface.ts index d926ac070..6fddaa41e 100644 --- a/frontend/src/modules/Grid3DIntersection/settingsToViewInterface.ts +++ b/frontend/src/modules/Grid3DIntersection/settingsToViewInterface.ts @@ -29,7 +29,7 @@ export type SettingsToViewInterface = { export const interfaceInitialization: InterfaceInitialization = { baseStates: { showGridlines: false, - gridLayer: -1, + gridLayer: 1, zFactor: 1, intersectionExtensionLength: 1000, }, diff --git a/frontend/src/modules/Grid3DIntersection/sharedAtoms/sharedAtoms.ts b/frontend/src/modules/Grid3DIntersection/sharedAtoms/sharedAtoms.ts index b6f929ea0..8dc8b69c3 100644 --- a/frontend/src/modules/Grid3DIntersection/sharedAtoms/sharedAtoms.ts +++ b/frontend/src/modules/Grid3DIntersection/sharedAtoms/sharedAtoms.ts @@ -3,8 +3,13 @@ import { EnsembleSetAtom } from "@framework/GlobalAtoms"; import { atom } from "jotai"; -import { userSelectedEnsembleIdentAtom, userSelectedWellboreUuidAtom } from "../settings/atoms/baseAtoms"; +import { + userSelectedCustomIntersectionPolylineIdAtom, + userSelectedEnsembleIdentAtom, + userSelectedWellboreUuidAtom, +} from "../settings/atoms/baseAtoms"; import { drilledWellboreHeadersQueryAtom } from "../settings/atoms/queryAtoms"; +import { CustomIntersectionPolyline, IntersectionType } from "../typesAndEnums"; export const selectedEnsembleIdentAtom = atom((get) => { const ensembleSet = get(EnsembleSetAtom); @@ -34,3 +39,28 @@ export const selectedWellboreUuidAtom = atom((get) => { return userSelectedWellboreUuid; }); + +export const intersectionTypeAtom = atom(IntersectionType.WELLBORE); +export const addCustomIntersectionPolylineEditModeActiveAtom = atom(false); +export const editCustomIntersectionPolylineEditModeActiveAtom = atom(false); + +export const currentCustomIntersectionPolylineAtom = atom([]); + +export const customIntersectionPolylinesAtom = atom([]); +export const selectedCustomIntersectionPolylineIdAtom = atom((get) => { + const userSelectedCustomIntersectionPolylineId = get(userSelectedCustomIntersectionPolylineIdAtom); + const customIntersectionPolylines = get(customIntersectionPolylinesAtom); + + if (!customIntersectionPolylines.length) { + return null; + } + + if ( + !userSelectedCustomIntersectionPolylineId || + !customIntersectionPolylines.some((el) => el.id === userSelectedCustomIntersectionPolylineId) + ) { + return customIntersectionPolylines[0].id; + } + + return userSelectedCustomIntersectionPolylineId; +}); diff --git a/frontend/src/modules/Grid3DIntersection/typesAndEnums.ts b/frontend/src/modules/Grid3DIntersection/typesAndEnums.ts index e69de29bb..35566d6d9 100644 --- a/frontend/src/modules/Grid3DIntersection/typesAndEnums.ts +++ b/frontend/src/modules/Grid3DIntersection/typesAndEnums.ts @@ -0,0 +1,10 @@ +export enum IntersectionType { + CUSTOM_POLYLINE = "custom-polyline", + WELLBORE = "wellbore", +} + +export type CustomIntersectionPolyline = { + id: string; + name: string; + polyline: number[][]; +}; diff --git a/frontend/src/modules/Grid3DIntersection/view/atoms/derivedAtoms.ts b/frontend/src/modules/Grid3DIntersection/view/atoms/derivedAtoms.ts index 2477456b6..14752c5bc 100644 --- a/frontend/src/modules/Grid3DIntersection/view/atoms/derivedAtoms.ts +++ b/frontend/src/modules/Grid3DIntersection/view/atoms/derivedAtoms.ts @@ -1,34 +1,63 @@ import { IntersectionReferenceSystem } from "@equinor/esv-intersection"; -import { selectedWellboreUuidAtom } from "@modules/Grid3DIntersection/sharedAtoms/sharedAtoms"; +import { + currentCustomIntersectionPolylineAtom, + customIntersectionPolylinesAtom, + intersectionTypeAtom, + selectedCustomIntersectionPolylineIdAtom, + selectedWellboreUuidAtom, +} from "@modules/Grid3DIntersection/sharedAtoms/sharedAtoms"; +import { IntersectionType } from "@modules/Grid3DIntersection/typesAndEnums"; import { atom } from "jotai"; import { fieldWellboreTrajectoriesQueryAtom } from "./queryAtoms"; +export const selectedCustomIntersectionPolylineAtom = atom((get) => { + const customIntersectionPolylineId = get(selectedCustomIntersectionPolylineIdAtom); + const customIntersectionPolylines = get(customIntersectionPolylinesAtom); + + return customIntersectionPolylines.find((el) => el.id === customIntersectionPolylineId); +}); + export const intersectionReferenceSystemAtom = atom((get) => { const fieldWellboreTrajectories = get(fieldWellboreTrajectoriesQueryAtom); const wellboreUuid = get(selectedWellboreUuidAtom); - if (!fieldWellboreTrajectories.data || !wellboreUuid) { - return null; - } + const customIntersectionPolyline = get(selectedCustomIntersectionPolylineAtom); + const intersectionType = get(intersectionTypeAtom); + + if (intersectionType === IntersectionType.WELLBORE) { + if (!fieldWellboreTrajectories.data || !wellboreUuid) { + return null; + } - const wellboreTrajectory = fieldWellboreTrajectories.data.find( - (wellbore) => wellbore.wellbore_uuid === wellboreUuid - ); + const wellboreTrajectory = fieldWellboreTrajectories.data.find( + (wellbore) => wellbore.wellbore_uuid === wellboreUuid + ); - if (wellboreTrajectory) { - const path: number[][] = []; - for (const [index, northing] of wellboreTrajectory.northing_arr.entries()) { - const easting = wellboreTrajectory.easting_arr[index]; - const tvd_msl = wellboreTrajectory.tvd_msl_arr[index]; + if (wellboreTrajectory) { + const path: number[][] = []; + for (const [index, northing] of wellboreTrajectory.northing_arr.entries()) { + const easting = wellboreTrajectory.easting_arr[index]; + const tvd_msl = wellboreTrajectory.tvd_msl_arr[index]; - path.push([easting, northing, tvd_msl]); - } - const offset = wellboreTrajectory.tvd_msl_arr[0]; + path.push([easting, northing, tvd_msl]); + } + const offset = wellboreTrajectory.tvd_msl_arr[0]; + + const referenceSystem = new IntersectionReferenceSystem(path); + referenceSystem.offset = offset; - const referenceSystem = new IntersectionReferenceSystem(path); - referenceSystem.offset = offset; + return referenceSystem; + } + } else if (intersectionType === IntersectionType.CUSTOM_POLYLINE && customIntersectionPolyline) { + if (customIntersectionPolyline.polyline.length < 2) { + return null; + } + const referenceSystem = new IntersectionReferenceSystem( + customIntersectionPolyline.polyline.map((point) => [point[0], point[1], 0]) + ); + referenceSystem.offset = 0; return referenceSystem; } diff --git a/frontend/src/modules/Grid3DIntersection/view/components/grid3d.tsx b/frontend/src/modules/Grid3DIntersection/view/components/grid3d.tsx index aedf3e42b..ac7c086e3 100644 --- a/frontend/src/modules/Grid3DIntersection/view/components/grid3d.tsx +++ b/frontend/src/modules/Grid3DIntersection/view/components/grid3d.tsx @@ -2,7 +2,7 @@ import React from "react"; import { BoundingBox3d_api, WellboreTrajectory_api } from "@api"; import { Layer } from "@deck.gl/core/typed"; -import { GeoJsonLayer } from "@deck.gl/layers/typed"; +import { ColumnLayer, SolidPolygonLayer } from "@deck.gl/layers/typed"; import { colorTablesObj } from "@emerson-eps/color-tables"; import { ColorScale } from "@lib/utils/ColorScale"; import SubsurfaceViewer, { @@ -19,6 +19,7 @@ import { } from "@webviz/subsurface-viewer/dist/layers"; import { Color, Rgb, parse } from "culori"; +import { isEqual } from "lodash"; import { FenceMeshSection_trans, @@ -32,13 +33,16 @@ export type Grid3DProps = { gridParameterData: GridMappedProperty_trans | null; fieldWellboreTrajectoriesData: WellboreTrajectory_api[] | null; polylineIntersectionData: PolylineIntersection_trans | null; + editCustomPolyline: number[][] | null; selectedWellboreUuid: string | null; boundingBox3d: BoundingBox3d_api | null; colorScale: ColorScale; showGridLines: boolean; zFactor: number; hoveredMdPoint3d: number[] | null; + editModeActive: boolean; onHoveredMdChange: (md: number | null) => void; + onEditPolylineChange: (polyline: number[][]) => void; }; type WorkingGrid3dLayer = { @@ -53,7 +57,27 @@ export function Grid3D(props: Grid3DProps): JSX.Element { const { onHoveredMdChange } = props; const [hoveredMdPoint, setHoveredMdPoint] = React.useState(null); - const [userPolyline, setUserPolyline] = React.useState([]); + const [editPolyline, setEditPolyline] = React.useState([]); + const [prevEditCustomPolyline, setPrevCustomPolyline] = React.useState(null); + const [prevEditModeActive, setPrevEditModeActive] = React.useState(false); + const [hoveredPolylineIndex, setHoveredPolylineIndex] = React.useState(null); + const [selectedPolylineIndex, setSelectedPolylineIndex] = React.useState(null); + const [userCameraInteractionActive, setUserCameraInteractionActive] = React.useState(true); + + if (!isEqual(props.editCustomPolyline, prevEditCustomPolyline)) { + setEditPolyline(props.editCustomPolyline ?? []); + setSelectedPolylineIndex(props.editCustomPolyline ? props.editCustomPolyline.length - 1 : null); + setPrevCustomPolyline(props.editCustomPolyline); + } + + if (!isEqual(props.editModeActive, prevEditModeActive)) { + setPrevEditModeActive(props.editModeActive); + if (!props.editModeActive) { + setSelectedPolylineIndex(null); + setHoveredPolylineIndex(null); + setEditPolyline([]); + } + } const bounds: [number, number, number, number] | undefined = props.boundingBox3d ? [props.boundingBox3d.xmin, props.boundingBox3d.ymin, props.boundingBox3d.xmax, props.boundingBox3d.ymax] @@ -70,6 +94,13 @@ export function Grid3D(props: Grid3DProps): JSX.Element { ] : [0, 0, 0, 100, 100, 100]; + let zMid = 0; + let zExtension = 0; + if (props.boundingBox3d) { + zMid = -(props.boundingBox3d.zmin + (props.boundingBox3d.zmax - props.boundingBox3d.zmin) / 2); + zExtension = Math.abs(props.boundingBox3d.zmax - props.boundingBox3d.zmin) + 100; + } + const colorTables = createContinuousColorScaleForMap(props.colorScale); const northArrowLayer = new NorthArrow3DLayer({ @@ -145,7 +176,6 @@ export function Grid3D(props: Grid3DProps): JSX.Element { minPropValue = Math.min(props.polylineIntersectionData.min_grid_prop_value, minPropValue); maxPropValue = Math.max(props.polylineIntersectionData.max_grid_prop_value, maxPropValue); } - console.log(`minMaxPropValue=${minPropValue <= maxPropValue ? `${minPropValue}, ${maxPropValue}` : "N/A"}`); if (props.gridSurfaceData && props.gridParameterData) { const offsetXyz = [props.gridSurfaceData.origin_utm_x, props.gridSurfaceData.origin_utm_y, 0]; @@ -201,127 +231,214 @@ export function Grid3D(props: Grid3DProps): JSX.Element { layers.push(pointsLayer); } - const userPolylinePointLayer = new PointsLayer({ - id: "user-polyline-point-layer", - pointsData: userPolyline.flat(), - color: [0, 255, 0, 255], - pointRadius: 10, - radiusUnits: "pixels", - ZIncreasingDownwards: false, - depthTest: false, - name: "User Polyline", - }); - layers.push(userPolylinePointLayer); + const currentlyEditedPolylineData = makePolylineData( + editPolyline, + zMid, + zExtension, + selectedPolylineIndex, + hoveredPolylineIndex, + [255, 255, 255, 255] + ); + + const userPolylinePolygonsData = currentlyEditedPolylineData.polygonData; + const userPolylineColumnsData = currentlyEditedPolylineData.columnData; - const userPolylineLineLayer = new GeoJsonLayer({ + const userPolylineLineLayer = new SolidPolygonLayer({ id: "user-polyline-line-layer", - data: { - type: "FeatureCollection", - features: [ - { - type: "Feature", - geometry: { - type: "LineString", - coordinates: userPolyline, - }, - properties: { - color: "green", - }, - }, - ], - }, - stroked: false, - filled: true, - lineWidthScale: 20, - lineWidthMinPixels: 2, - color: [0, 255, 0, 255], - widthUnits: "pixels", - ZIncreasingDownwards: false, - depthTest: false, - name: "User Polyline", + data: userPolylinePolygonsData, + getPolygon: (d) => d.polygon, + getFillColor: (d) => d.color, + getElevation: zExtension, + getLineColor: [255, 255, 255], + getLineWidth: 20, + lineWidthMinPixels: 1, + extruded: true, }); layers.push(userPolylineLineLayer); - console.debug(userPolyline); + const userPolylinePointLayer = new ColumnLayer({ + id: "user-polyline-point-layer", + data: userPolylineColumnsData, + getElevation: zExtension, + getPosition: (d) => d.centroid, + getFillColor: (d) => d.color, + extruded: true, + radius: 50, + radiusUnits: "pixels", + pickable: true, + onHover(pickingInfo) { + if (!props.editModeActive) { + return; + } + if (pickingInfo.object && pickingInfo.object.index < editPolyline.length) { + setHoveredPolylineIndex(pickingInfo.object.index); + } else { + setHoveredPolylineIndex(null); + } + }, + onClick(pickingInfo, event) { + if (!props.editModeActive) { + return; + } + + if (pickingInfo.object && pickingInfo.object.index < editPolyline.length) { + setSelectedPolylineIndex(pickingInfo.object.index); + event.stopPropagation(); + event.handled = true; + } else { + setSelectedPolylineIndex(null); + } + }, + onDragStart(pickingInfo) { + if (!props.editModeActive) { + return; + } + if (pickingInfo.object && selectedPolylineIndex === pickingInfo.object.index) { + setUserCameraInteractionActive(false); + } + }, + onDragEnd() { + setUserCameraInteractionActive(true); + }, + onDrag(pickingInfo) { + if (!props.editModeActive) { + return; + } + + if (pickingInfo.object) { + const index = pickingInfo.object.index; + if (!pickingInfo.coordinate) { + return; + } + setEditPolyline((prev) => { + const newPolyline = prev.reduce((acc, point, i) => { + if (i === index && pickingInfo.coordinate) { + return [...acc, [pickingInfo.coordinate[0], pickingInfo.coordinate[1]]]; + } + return [...acc, point]; + }, [] as number[][]); + + props.onEditPolylineChange(newPolyline); + return newPolyline; + }); + } + }, + }); + layers.push(userPolylinePointLayer); const handleMouseEvent = React.useCallback( function handleMouseEvent(event: MapMouseEvent) { - if (event.type === "click") { - let depth: number | null = null; + if (event.type === "click" && props.editModeActive) { if (event.x && event.y) { for (const info of event.infos) { - if ("layer" in info && info.layer?.id === "grid-3d-layer") { - if ("properties" in info) { - const properties = info.properties as Record[]; - for (const property of properties) { - if (property.name && property.name.startsWith("Depth") && property.value) { - depth = parseFloat(property.value); - break; - } - } - if (depth !== null) { - break; - } + if ("layer" in info && info.layer?.id === "user-polyline-point-layer") { + if (info.picked) { + return; } } } - if (depth !== null) { - const point = [event.x, event.y, depth]; - setUserPolyline((prev) => [...prev, point]); - } + const point = [event.x, event.y]; + setEditPolyline((prev) => { + let newPolyline: number[][] = []; + if (selectedPolylineIndex === null || selectedPolylineIndex === prev.length - 1) { + newPolyline = [...prev, point]; + setSelectedPolylineIndex(prev.length); + } else if (selectedPolylineIndex === 0) { + newPolyline = [point, ...prev]; + setSelectedPolylineIndex(0); + } else { + newPolyline = prev; + } + props.onEditPolylineChange(newPolyline); + return newPolyline; + }); } } - if (event.type !== "hover" || !props.fieldWellboreTrajectoriesData) { - return; - } - - let hoveredMd: number | null = null; - let coordinate: number[] | null = null; - for (const info of event.infos) { - if (!("layer" in info) || info.layer?.id !== "selected-well-layer") { - continue; - } - if ("object" in info && "properties" in info.object && "properties" in info) { - if ("uuid" in info.object.properties) { - if (info.object.properties.uuid === props.selectedWellboreUuid) { - const properties = info.properties as Record[]; - for (const property of properties) { - if (property.name && property.name.startsWith("MD") && property.value) { - hoveredMd = parseFloat(property.value.split(" ")[0]); - if ("coordinate" in info && info.coordinate) { - coordinate = [info.coordinate[0], info.coordinate[1], -info.coordinate[2]]; + if (event.type === "hover") { + if (props.fieldWellboreTrajectoriesData) { + let hoveredMd: number | null = null; + let coordinate: number[] | null = null; + for (const info of event.infos) { + if (!("layer" in info) || info.layer?.id !== "selected-well-layer") { + continue; + } + if ("object" in info && "properties" in info.object && "properties" in info) { + if ("uuid" in info.object.properties) { + if (info.object.properties.uuid === props.selectedWellboreUuid) { + const properties = info.properties as Record[]; + for (const property of properties) { + if (property.name && property.name.startsWith("MD") && property.value) { + hoveredMd = parseFloat(property.value.split(" ")[0]); + if ("coordinate" in info && info.coordinate) { + coordinate = [ + info.coordinate[0], + info.coordinate[1], + -info.coordinate[2], + ]; + break; + } + } + } + if (hoveredMd !== null) { break; } } } - if (hoveredMd !== null) { - break; - } } } - } - } - if (coordinate) { - setHoveredMdPoint(coordinate); - } else { - setHoveredMdPoint(null); + if (coordinate) { + setHoveredMdPoint(coordinate); + } else { + setHoveredMdPoint(null); + } + + if (hoveredMd !== null) { + onHoveredMdChange(hoveredMd); + return; + } + + onHoveredMdChange(null); + } } + }, + [ + onHoveredMdChange, + props.selectedWellboreUuid, + setHoveredMdPoint, + editPolyline, + props.editModeActive, + selectedPolylineIndex, + ] + ); - if (hoveredMd !== null) { - onHoveredMdChange(hoveredMd); + React.useEffect(() => { + function handleKeyboardEvent(event: KeyboardEvent) { + if (!props.editModeActive) { return; } + if (event.key === "Delete" && selectedPolylineIndex !== null) { + setSelectedPolylineIndex((prev) => (prev === null || prev === 0 ? null : prev - 1)); + setEditPolyline((prev) => { + const newPolyline = prev.filter((_, i) => i !== selectedPolylineIndex); + props.onEditPolylineChange(newPolyline); + return newPolyline; + }); + } + } - onHoveredMdChange(null); - }, - [onHoveredMdChange, props.selectedWellboreUuid, setHoveredMdPoint] - ); + document.addEventListener("keydown", handleKeyboardEvent); + + return () => { + document.removeEventListener("keydown", handleKeyboardEvent); + }; + }, [selectedPolylineIndex, setEditPolyline, setSelectedPolylineIndex, props.editModeActive]); return (
(undefined); const [cameraPosition, setCameraPosition] = React.useState(undefined); - const handleCameraChange = React.useCallback(function handleCameraChange(viewport: ViewStateType): void { - setCameraPosition(viewport); - }, []); + if (!isEqual(props.bounds, prevBounds)) { + setPrevBounds(props.bounds); + setCameraPosition(undefined); + } + + const handleCameraChange = React.useCallback( + function handleCameraChange(viewport: ViewStateType): void { + if (props.userCameraInteractionActive || props.userCameraInteractionActive === undefined) { + setCameraPosition(viewport); + } + }, + [props.userCameraInteractionActive] + ); return ; } @@ -494,3 +626,91 @@ export function createContinuousColorScaleForMap(colorScale: ColorScale): colorT return [{ name: "Continuous", discrete: false, colors: rgbArr }]; } + +function makePolylineData( + polyline: number[][], + zMid: number, + zExtension: number, + selectedPolylineIndex: number | null, + hoveredPolylineIndex: number | null, + color: [number, number, number, number] +): { + polygonData: { polygon: number[][]; color: number[] }[]; + columnData: { index: number; centroid: number[]; color: number[] }[]; +} { + const polygonData: { + polygon: number[][]; + color: number[]; + }[] = []; + + const columnData: { + index: number; + centroid: number[]; + color: number[]; + }[] = []; + + const width = 10; + for (let i = 0; i < polyline.length; i++) { + const startPoint = polyline[i]; + const endPoint = polyline[i + 1]; + + if (i < polyline.length - 1) { + const lineVector = [endPoint[0] - startPoint[0], endPoint[1] - startPoint[1], 0]; + const zVector = [0, 0, 1]; + const normalVector = [ + lineVector[1] * zVector[2] - lineVector[2] * zVector[1], + lineVector[2] * zVector[0] - lineVector[0] * zVector[2], + lineVector[0] * zVector[1] - lineVector[1] * zVector[0], + ]; + const normalizedNormalVector = [ + normalVector[0] / Math.sqrt(normalVector[0] ** 2 + normalVector[1] ** 2 + normalVector[2] ** 2), + normalVector[1] / Math.sqrt(normalVector[0] ** 2 + normalVector[1] ** 2 + normalVector[2] ** 2), + ]; + + const point1 = [ + startPoint[0] - (normalizedNormalVector[0] * width) / 2, + startPoint[1] - (normalizedNormalVector[1] * width) / 2, + zMid - zExtension / 2, + ]; + + const point2 = [ + endPoint[0] - (normalizedNormalVector[0] * width) / 2, + endPoint[1] - (normalizedNormalVector[1] * width) / 2, + zMid - zExtension / 2, + ]; + + const point3 = [ + endPoint[0] + (normalizedNormalVector[0] * width) / 2, + endPoint[1] + (normalizedNormalVector[1] * width) / 2, + zMid - zExtension / 2, + ]; + + const point4 = [ + startPoint[0] + (normalizedNormalVector[0] * width) / 2, + startPoint[1] + (normalizedNormalVector[1] * width) / 2, + zMid - zExtension / 2, + ]; + + const polygon: number[][] = [point1, point2, point3, point4]; + polygonData.push({ polygon, color: [color[0], color[1], color[2], color[3] / 2] }); + } + + let adjustedColor = color; + if (i === selectedPolylineIndex) { + if (i === 0 || i === polyline.length - 1) { + adjustedColor = [0, 255, 0, color[3]]; + } else { + adjustedColor = [0, 0, 255, color[3]]; + } + } else if (i === hoveredPolylineIndex) { + adjustedColor = [120, 120, 255, color[3]]; + } + columnData.push({ + index: i, + centroid: [startPoint[0], startPoint[1], zMid - zExtension / 2], + color: adjustedColor, + }); + } + + return { polygonData, columnData }; +} diff --git a/frontend/src/modules/Grid3DIntersection/view/components/intersection.tsx b/frontend/src/modules/Grid3DIntersection/view/components/intersection.tsx index eda59effd..3f4702c78 100644 --- a/frontend/src/modules/Grid3DIntersection/view/components/intersection.tsx +++ b/frontend/src/modules/Grid3DIntersection/view/components/intersection.tsx @@ -3,6 +3,7 @@ import React from "react"; import { BoundingBox3d_api, WellboreCasing_api } from "@api"; import { Casing, IntersectionReferenceSystem } from "@equinor/esv-intersection"; import { ColorScale } from "@lib/utils/ColorScale"; +import { IntersectionType } from "@modules/Grid3DIntersection/typesAndEnums"; import { EsvIntersection, EsvIntersectionReadoutEvent, @@ -29,14 +30,17 @@ export type IntersectionProps = { intersectionExtensionLength: number; hoveredMd: number | null; onReadout: (event: EsvIntersectionReadoutEvent) => void; + intersectionType: IntersectionType; }; export function Intersection(props: IntersectionProps): JSX.Element { const { onReadout } = props; const [readoutItems, setReadoutItems] = React.useState([]); - const layers: LayerItem[] = [ - { + const layers: LayerItem[] = []; + + if (props.intersectionType === IntersectionType.WELLBORE) { + layers.push({ id: "wellbore-path", type: LayerType.WELLBORE_PATH, hoverable: true, @@ -45,8 +49,8 @@ export function Intersection(props: IntersectionProps): JSX.Element { strokeWidth: "2", order: 6, }, - }, - ]; + }); + } if (props.polylineIntersectionData) { layers.push({ diff --git a/frontend/src/modules/Grid3DIntersection/view/view.tsx b/frontend/src/modules/Grid3DIntersection/view/view.tsx index 0ecc201bf..e581f9a78 100644 --- a/frontend/src/modules/Grid3DIntersection/view/view.tsx +++ b/frontend/src/modules/Grid3DIntersection/view/view.tsx @@ -9,9 +9,9 @@ import { useFieldWellboreTrajectoriesQuery } from "@modules/_shared/WellBore/que import { EsvIntersectionReadoutEvent } from "@modules/_shared/components/EsvIntersection"; import { isWellborepathLayer } from "@modules/_shared/components/EsvIntersection/utils/layers"; -import { useAtomValue } from "jotai"; +import { useAtom, useAtomValue } from "jotai"; -import { intersectionReferenceSystemAtom } from "./atoms/derivedAtoms"; +import { intersectionReferenceSystemAtom, selectedCustomIntersectionPolylineAtom } from "./atoms/derivedAtoms"; import { Grid3D } from "./components/grid3d"; import { Intersection } from "./components/intersection"; import { useGridParameterQuery, useGridSurfaceQuery } from "./queries/gridQueries"; @@ -19,8 +19,16 @@ import { useGridPolylineIntersection as useGridPolylineIntersectionQuery } from import { useWellboreCasingQuery } from "./queries/wellboreSchematicsQueries"; import { SettingsToViewInterface } from "../settingsToViewInterface"; -import { selectedEnsembleIdentAtom, selectedWellboreUuidAtom } from "../sharedAtoms/sharedAtoms"; +import { + addCustomIntersectionPolylineEditModeActiveAtom, + currentCustomIntersectionPolylineAtom, + editCustomIntersectionPolylineEditModeActiveAtom, + intersectionTypeAtom, + selectedEnsembleIdentAtom, + selectedWellboreUuidAtom, +} from "../sharedAtoms/sharedAtoms"; import { State } from "../state"; +import { IntersectionType } from "../typesAndEnums"; export function View(props: ModuleViewProps): JSX.Element { const statusWriter = useViewStatusWriter(props.viewContext); @@ -45,9 +53,14 @@ export function View(props: ModuleViewProps): JS const zFactor = props.viewContext.useSettingsToViewInterfaceValue("zFactor"); const intersectionExtensionLength = props.viewContext.useSettingsToViewInterfaceValue("intersectionExtensionLength"); + const addPolylineModeActive = useAtomValue(addCustomIntersectionPolylineEditModeActiveAtom); + const editPolylineModeActive = useAtomValue(editCustomIntersectionPolylineEditModeActiveAtom); + const intersectionType = useAtomValue(intersectionTypeAtom); const [hoveredMd, setHoveredMd] = React.useState(null); const [hoveredMd3dGrid, setHoveredMd3dGrid] = React.useState(null); + const [customIntersectionPolyline, setCustomIntersectionPolyline] = useAtom(currentCustomIntersectionPolylineAtom); + const selectedCustomIntersectionPolyline = useAtomValue(selectedCustomIntersectionPolylineAtom); const fieldWellboreTrajectoriesQuery = useFieldWellboreTrajectoriesQuery(ensembleIdent?.getCaseUuid() ?? undefined); @@ -59,14 +72,20 @@ export function View(props: ModuleViewProps): JS let hoveredMdPoint3d: number[] | null = null; if (intersectionReferenceSystem) { - const extendedTrajectory = intersectionReferenceSystem.getExtendedTrajectory( - 10, - intersectionExtensionLength, - intersectionExtensionLength - ); - - for (const point of extendedTrajectory.points) { - polylineUtmXy.push(point[0], point[1]); + if (intersectionType === IntersectionType.WELLBORE) { + const extendedTrajectory = intersectionReferenceSystem.getExtendedTrajectory( + 10, + intersectionExtensionLength, + intersectionExtensionLength + ); + + for (const point of extendedTrajectory.points) { + polylineUtmXy.push(point[0], point[1]); + } + } else if (intersectionType === IntersectionType.CUSTOM_POLYLINE && selectedCustomIntersectionPolyline) { + for (const point of selectedCustomIntersectionPolyline.polyline) { + polylineUtmXy.push(point[0], point[1]); + } } if (hoveredMd) { @@ -148,6 +167,13 @@ export function View(props: ModuleViewProps): JS setHoveredMd3dGrid(md); }, []); + const handleEditPolylineChange = React.useCallback(function handleEditPolylineChange(polyline: number[][]) { + setCustomIntersectionPolyline(polyline); + }, []); + + const potentialIntersectionExtensionLength = + intersectionType === IntersectionType.WELLBORE ? intersectionExtensionLength : 0; + return (
): JS fieldWellboreTrajectoriesData={fieldWellboreTrajectoriesQuery.data ?? null} selectedWellboreUuid={wellboreUuid} polylineIntersectionData={polylineIntersectionQuery.data ?? null} + editCustomPolyline={ + editPolylineModeActive ? selectedCustomIntersectionPolyline?.polyline ?? null : null + } boundingBox3d={gridModelBoundingBox3d} colorScale={colorScale} showGridLines={showGridLines} zFactor={zFactor} hoveredMdPoint3d={hoveredMdPoint3d} onHoveredMdChange={handleGrid3DMdChange} + onEditPolylineChange={handleEditPolylineChange} + editModeActive={addPolylineModeActive || editPolylineModeActive} /> ): JS colorScale={colorScale} showGridLines={showGridLines} zFactor={zFactor} - intersectionExtensionLength={intersectionExtensionLength} + intersectionExtensionLength={potentialIntersectionExtensionLength} hoveredMd={hoveredMd3dGrid} onReadout={handleReadout} + intersectionType={intersectionType} />
);