diff --git a/frontend/src/framework/GlobalAtoms.ts b/frontend/src/framework/GlobalAtoms.ts index 1dfc282ce..a042ae586 100644 --- a/frontend/src/framework/GlobalAtoms.ts +++ b/frontend/src/framework/GlobalAtoms.ts @@ -3,9 +3,25 @@ import { EnsembleSet } from "@framework/EnsembleSet"; import { atom } from "jotai"; import { isEqual } from "lodash"; +import { EnsembleIdent } from "./EnsembleIdent"; +import { RealizationFilterSet } from "./RealizationFilterSet"; +import { UserCreatedItems } from "./UserCreatedItems"; import { EnsembleRealizationFilterFunction } from "./WorkbenchSession"; import { atomWithCompare } from "./utils/atomUtils"; export const EnsembleSetAtom = atomWithCompare(new EnsembleSet([]), isEqual); -export const EnsembleRealizationFilterFunctionAtom = atom(null); +export const EnsembleRealizationFilterFunctionAtom = atom((get) => { + const realizationFilterSet = get(RealizationFilterSetAtom); + + if (!realizationFilterSet) { + return null; + } + + return (ensembleIdent: EnsembleIdent) => + realizationFilterSet.getRealizationFilterForEnsembleIdent(ensembleIdent).getFilteredRealizations(); +}); + +export const UserCreatedItemsAtom = atomWithCompare(new UserCreatedItems(), (a, b) => a.isEqual(b)); + +export const RealizationFilterSetAtom = atomWithCompare(null, isEqual); diff --git a/frontend/src/framework/UserCreatedItems.ts b/frontend/src/framework/UserCreatedItems.ts new file mode 100644 index 000000000..5dae6fb55 --- /dev/null +++ b/frontend/src/framework/UserCreatedItems.ts @@ -0,0 +1,23 @@ +import { AtomStoreMaster } from "./AtomStoreMaster"; +import { IntersectionPolylines } from "./userCreatedItems/IntersectionPolylines"; + +export interface UserCreatedItemSet { + serialize(): string; + populateFromData(data: string): void; +} + +export class UserCreatedItems { + private _intersectionPolylines: IntersectionPolylines; + + constructor() { + this._intersectionPolylines = new IntersectionPolylines(); + } + + getIntersectionPolylines(): IntersectionPolylines { + return this._intersectionPolylines; + } + + isEqual(other: UserCreatedItems): boolean { + return this._intersectionPolylines.serialize() === other._intersectionPolylines.serialize(); + } +} diff --git a/frontend/src/framework/WorkbenchSession.ts b/frontend/src/framework/WorkbenchSession.ts index 0b323a9d5..8181adac4 100644 --- a/frontend/src/framework/WorkbenchSession.ts +++ b/frontend/src/framework/WorkbenchSession.ts @@ -4,6 +4,7 @@ import { Ensemble } from "./Ensemble"; import { EnsembleIdent } from "./EnsembleIdent"; import { EnsembleSet } from "./EnsembleSet"; import { RealizationFilterSet } from "./RealizationFilterSet"; +import { UserCreatedItems } from "./UserCreatedItems"; export type EnsembleRealizationFilterFunction = (ensembleIdent: EnsembleIdent) => readonly number[]; @@ -23,6 +24,7 @@ export class WorkbenchSession { private _subscribersMap: Map void>> = new Map(); protected _ensembleSet: EnsembleSet = new EnsembleSet([]); protected _realizationFilterSet = new RealizationFilterSet(); + protected _userCreatedItems: UserCreatedItems = new UserCreatedItems(); getEnsembleSet(): EnsembleSet { return this._ensembleSet; @@ -32,6 +34,10 @@ export class WorkbenchSession { return this._realizationFilterSet; } + getUserCreatedItems(): UserCreatedItems { + return this._userCreatedItems; + } + subscribe>( event: T, cb: () => void diff --git a/frontend/src/framework/internal/WorkbenchSessionPrivate.ts b/frontend/src/framework/internal/WorkbenchSessionPrivate.ts index b1927c8a3..a3e164617 100644 --- a/frontend/src/framework/internal/WorkbenchSessionPrivate.ts +++ b/frontend/src/framework/internal/WorkbenchSessionPrivate.ts @@ -1,8 +1,9 @@ import { AtomStoreMaster } from "@framework/AtomStoreMaster"; import { EnsembleIdent } from "@framework/EnsembleIdent"; +import { UserCreatedItems } from "@framework/UserCreatedItems"; import { EnsembleSet } from "../EnsembleSet"; -import { EnsembleRealizationFilterFunctionAtom, EnsembleSetAtom } from "../GlobalAtoms"; +import { EnsembleSetAtom, RealizationFilterSetAtom, UserCreatedItemsAtom } from "../GlobalAtoms"; import { WorkbenchSession, WorkbenchSessionEvent } from "../WorkbenchSession"; export class WorkbenchSessionPrivate extends WorkbenchSession { @@ -11,6 +12,8 @@ export class WorkbenchSessionPrivate extends WorkbenchSession { constructor(atomStoreMaster: AtomStoreMaster) { super(); this._atomStoreMaster = atomStoreMaster; + this._atomStoreMaster.setAtomValue(UserCreatedItemsAtom, this._userCreatedItems); + this._atomStoreMaster.setAtomValue(RealizationFilterSetAtom, this._realizationFilterSet); } setEnsembleSetLoadingState(isLoading: boolean): void { @@ -21,11 +24,6 @@ export class WorkbenchSessionPrivate extends WorkbenchSession { this._ensembleSet = newEnsembleSet; this._realizationFilterSet.synchronizeWithEnsembleSet(this._ensembleSet); this._atomStoreMaster.setAtomValue(EnsembleSetAtom, newEnsembleSet); - this._atomStoreMaster.setAtomValue( - EnsembleRealizationFilterFunctionAtom, - () => (ensembleIdent: EnsembleIdent) => - this._realizationFilterSet.getRealizationFilterForEnsembleIdent(ensembleIdent).getFilteredRealizations() - ); this.notifySubscribers(WorkbenchSessionEvent.EnsembleSetChanged); this.notifySubscribers(WorkbenchSessionEvent.RealizationFilterSetChanged); } diff --git a/frontend/src/framework/userCreatedItems/IntersectionPolylines.ts b/frontend/src/framework/userCreatedItems/IntersectionPolylines.ts new file mode 100644 index 000000000..f4d26e979 --- /dev/null +++ b/frontend/src/framework/userCreatedItems/IntersectionPolylines.ts @@ -0,0 +1,51 @@ +import { UserCreatedItemSet } from "@framework/UserCreatedItems"; + +import { v4 } from "uuid"; + +export type IntersectionPolyline = { + id: string; + name: string; + points: number[][]; +}; + +export type IntersectionPolylineWithoutId = Omit; + +export class IntersectionPolylines implements UserCreatedItemSet { + private _polylines: IntersectionPolyline[] = []; + + serialize(): string { + return JSON.stringify(this._polylines); + } + + populateFromData(data: string): void { + this._polylines = JSON.parse(data); + } + + add(polyline: IntersectionPolylineWithoutId): void { + const id = v4(); + this._polylines.push({ + id, + ...polyline, + }); + } + + remove(id: string): void { + this._polylines = this._polylines.filter((polyline) => polyline.id !== id); + } + + getPolylines(): IntersectionPolyline[] { + return this._polylines; + } + + getPolyline(id: string): IntersectionPolyline | undefined { + return this._polylines.find((polyline) => polyline.id === id); + } + + updatePolyline(id: string, polyline: IntersectionPolylineWithoutId): void { + const index = this._polylines.findIndex((polyline) => polyline.id === id); + this._polylines[index] = { + id, + ...polyline, + }; + } +} diff --git a/frontend/src/modules/Grid3DIntersection/settings/atoms/derivedAtoms.ts b/frontend/src/modules/Grid3DIntersection/settings/atoms/derivedAtoms.ts index 9d9744cf4..3fa59fb93 100644 --- a/frontend/src/modules/Grid3DIntersection/settings/atoms/derivedAtoms.ts +++ b/frontend/src/modules/Grid3DIntersection/settings/atoms/derivedAtoms.ts @@ -1,5 +1,5 @@ import { EnsembleIdent } from "@framework/EnsembleIdent"; -import { EnsembleRealizationFilterFunctionAtom, EnsembleSetAtom } from "@framework/GlobalAtoms"; +import { EnsembleRealizationFilterFunctionAtom, EnsembleSetAtom, UserCreatedItemsAtom } from "@framework/GlobalAtoms"; import { selectedEnsembleIdentAtom } from "@modules/Grid3DIntersection/sharedAtoms/sharedAtoms"; import { atom } from "jotai"; @@ -158,3 +158,8 @@ export const selectedGridModelParameterDateOrIntervalAtom = atom((get) => { return userSelectedGridModelParameterDateOrInterval; }); + +export const availableUserCreatedIntersectionPolylinesAtom = atom((get) => { + const userCreatedItems = get(UserCreatedItemsAtom); + return userCreatedItems.getIntersectionPolylines().getPolylines(); +}); diff --git a/frontend/src/modules/Grid3DIntersection/settings/settings.tsx b/frontend/src/modules/Grid3DIntersection/settings/settings.tsx index 4eaff609a..5af7f5c9d 100644 --- a/frontend/src/modules/Grid3DIntersection/settings/settings.tsx +++ b/frontend/src/modules/Grid3DIntersection/settings/settings.tsx @@ -2,9 +2,11 @@ import React from "react"; import { Grid3dInfo_api, Grid3dPropertyInfo_api, WellboreHeader_api } from "@api"; import { EnsembleIdent } from "@framework/EnsembleIdent"; +import { UserCreatedItemsAtom } from "@framework/GlobalAtoms"; import { ModuleSettingsProps } from "@framework/Module"; import { useSettingsStatusWriter } from "@framework/StatusWriter"; import { EnsembleDropdown } from "@framework/components/EnsembleDropdown"; +import { IntersectionPolyline } from "@framework/userCreatedItems/IntersectionPolylines"; import { Button } from "@lib/components/Button"; import { CollapsibleGroup } from "@lib/components/CollapsibleGroup"; import { Dialog } from "@lib/components/Dialog"; @@ -33,6 +35,7 @@ import { } from "./atoms/baseAtoms"; import { availableRealizationsAtom, + availableUserCreatedIntersectionPolylinesAtom, gridModelDimensionsAtom, selectedGridModelNameAtom, selectedGridModelParameterDateOrIntervalAtom, @@ -45,7 +48,6 @@ import { SettingsToViewInterface } from "../settingsToViewInterface"; import { addCustomIntersectionPolylineEditModeActiveAtom, currentCustomIntersectionPolylineAtom, - customIntersectionPolylinesAtom, editCustomIntersectionPolylineEditModeActiveAtom, intersectionTypeAtom, selectedCustomIntersectionPolylineIdAtom, @@ -96,9 +98,7 @@ export function Settings(props: ModuleSettingsProps el.name === currentCustomPolylineName)) { + if (availableUserCreatedIntersectionPolylines.some((el) => el.name === currentCustomPolylineName)) { setCurrentCustomPolylineNameMessage("A polyline with this name already exists"); return; } @@ -225,10 +227,12 @@ export function Settings(props: ModuleSettingsProps - prev.filter((el) => el.id !== selectedCustomIntersectionPolylineId) - ); - setSelectedCustomIntersectionPolylineId(null); - setPolylineAddModeActive(false); - setPolylineEditModeActive(false); - setCurrentCustomIntersectionPolyline([]); - } - + /* React.useEffect(() => { function handleKeyboardEvent(event: KeyboardEvent) { if (!polylineAddModeActive && !polylineEditModeActive) { @@ -292,6 +287,7 @@ export function Settings(props: ModuleSettingsProps + <> + /*
-
+ + */ )} value={selectedCustomIntersectionPolylineId ? [selectedCustomIntersectionPolylineId] : []} headerLabels={["Polyline name", "Actions"]} @@ -548,7 +546,7 @@ function makeWellHeaderOptions(wellHeaders: WellboreHeader_api[]): SelectOption[ } function makeCustomIntersectionPolylineOptions( - polylines: CustomIntersectionPolyline[], + polylines: IntersectionPolyline[], selectedId: string | null, actions: React.ReactNode ): TableSelectOption[] { diff --git a/frontend/src/modules/Grid3DIntersection/sharedAtoms/sharedAtoms.ts b/frontend/src/modules/Grid3DIntersection/sharedAtoms/sharedAtoms.ts index 8dc8b69c3..511d74797 100644 --- a/frontend/src/modules/Grid3DIntersection/sharedAtoms/sharedAtoms.ts +++ b/frontend/src/modules/Grid3DIntersection/sharedAtoms/sharedAtoms.ts @@ -1,5 +1,5 @@ import { EnsembleIdent } from "@framework/EnsembleIdent"; -import { EnsembleSetAtom } from "@framework/GlobalAtoms"; +import { EnsembleSetAtom, UserCreatedItemsAtom } from "@framework/GlobalAtoms"; import { atom } from "jotai"; @@ -46,10 +46,9 @@ export const editCustomIntersectionPolylineEditModeActiveAtom = atom(fa export const currentCustomIntersectionPolylineAtom = atom([]); -export const customIntersectionPolylinesAtom = atom([]); export const selectedCustomIntersectionPolylineIdAtom = atom((get) => { const userSelectedCustomIntersectionPolylineId = get(userSelectedCustomIntersectionPolylineIdAtom); - const customIntersectionPolylines = get(customIntersectionPolylinesAtom); + const customIntersectionPolylines = get(UserCreatedItemsAtom).getIntersectionPolylines().getPolylines(); if (!customIntersectionPolylines.length) { return null; diff --git a/frontend/src/modules/Grid3DIntersection/utils/IntersectionBacksampling.ts b/frontend/src/modules/Grid3DIntersection/utils/IntersectionBacksampling.ts new file mode 100644 index 000000000..bb7de0a60 --- /dev/null +++ b/frontend/src/modules/Grid3DIntersection/utils/IntersectionBacksampling.ts @@ -0,0 +1,15 @@ +import { PolylineIntersection_trans } from "../view/queries/queryDataTransforms"; + +export function backsampleIntersectionPolyline( + completeWellborePath: number[][], + polylineIntersection: PolylineIntersection_trans +) { + /* + const backsampledIntersectionPolyline: number[][] = []; + for (const point of polylineIntersection) { + const closestPoint = findClosestPoint(completeWellborePath, point); + backsampledIntersectionPolyline.push(closestPoint); + } + return backsampledIntersectionPolyline; + */ +} diff --git a/frontend/src/modules/Grid3DIntersection/view/atoms/derivedAtoms.ts b/frontend/src/modules/Grid3DIntersection/view/atoms/derivedAtoms.ts index 14752c5bc..cad5f978e 100644 --- a/frontend/src/modules/Grid3DIntersection/view/atoms/derivedAtoms.ts +++ b/frontend/src/modules/Grid3DIntersection/view/atoms/derivedAtoms.ts @@ -1,7 +1,6 @@ import { IntersectionReferenceSystem } from "@equinor/esv-intersection"; +import { UserCreatedItemsAtom } from "@framework/GlobalAtoms"; import { - currentCustomIntersectionPolylineAtom, - customIntersectionPolylinesAtom, intersectionTypeAtom, selectedCustomIntersectionPolylineIdAtom, selectedWellboreUuidAtom, @@ -14,7 +13,7 @@ import { fieldWellboreTrajectoriesQueryAtom } from "./queryAtoms"; export const selectedCustomIntersectionPolylineAtom = atom((get) => { const customIntersectionPolylineId = get(selectedCustomIntersectionPolylineIdAtom); - const customIntersectionPolylines = get(customIntersectionPolylinesAtom); + const customIntersectionPolylines = get(UserCreatedItemsAtom).getIntersectionPolylines().getPolylines(); return customIntersectionPolylines.find((el) => el.id === customIntersectionPolylineId); }); @@ -51,11 +50,11 @@ export const intersectionReferenceSystemAtom = atom((get) => { return referenceSystem; } } else if (intersectionType === IntersectionType.CUSTOM_POLYLINE && customIntersectionPolyline) { - if (customIntersectionPolyline.polyline.length < 2) { + if (customIntersectionPolyline.points.length < 2) { return null; } const referenceSystem = new IntersectionReferenceSystem( - customIntersectionPolyline.polyline.map((point) => [point[0], point[1], 0]) + customIntersectionPolyline.points.map((point) => [point[0], point[1], 0]) ); referenceSystem.offset = 0; diff --git a/frontend/src/modules/Grid3DIntersection/view/components/SubsurfaceViewerWrapper.tsx b/frontend/src/modules/Grid3DIntersection/view/components/SubsurfaceViewerWrapper.tsx new file mode 100644 index 000000000..dc74b4655 --- /dev/null +++ b/frontend/src/modules/Grid3DIntersection/view/components/SubsurfaceViewerWrapper.tsx @@ -0,0 +1,442 @@ +import React from "react"; + +import { Layer, PickingInfo } from "@deck.gl/core/typed"; +import { ColumnLayer, SolidPolygonLayer } from "@deck.gl/layers/typed"; +import { IntersectionPolyline } from "@framework/userCreatedItems/IntersectionPolylines"; +import { Button } from "@lib/components/Button"; +import { Select, SelectOption } from "@lib/components/Select"; +import { SubsurfaceViewerProps, ViewStateType } from "@webviz/subsurface-viewer"; +import SubsurfaceViewer, { MapMouseEvent, colorTablesArray } from "@webviz/subsurface-viewer/dist/SubsurfaceViewer"; + +import { isEqual } from "lodash"; + +export type BoundingBox3D = { + xmin: number; + ymin: number; + zmin: number; + xmax: number; + ymax: number; + zmax: number; +}; + +export type BoundingBox2D = { + xmin: number; + ymin: number; + xmax: number; + ymax: number; +}; + +export type SubsurfaceViewerWrapperProps = { + boundingBox: BoundingBox2D | BoundingBox3D; + layers: Layer[]; + show3D?: boolean; + colorTables: colorTablesArray; + enableIntersectionPolylineEditing?: boolean; + onAddIntersectionPolyline?: (intersectionPolyline: IntersectionPolyline) => void; +} & ( + | { + onIntersectionPolylineChange: (intersectionPolyline: IntersectionPolyline) => void; + intersectionPolyline: IntersectionPolyline; + } + | { + onIntersectionPolylineChange?: never; + intersectionPolyline?: never; + } +); + +type IntersectionZValues = { + zMid: number; + zExtension: number; +}; + +export function SubsurfaceViewerWrapper(props: SubsurfaceViewerWrapperProps): React.ReactNode { + const [intersectionZValues, setIntersectionZValues] = React.useState(undefined); + const [polylineEditingActive, setPolylineEditingActive] = React.useState(false); + const [currentlyEditedPolyline, setCurrentlyEditedPolyline] = React.useState([]); + const [selectedPolylinePointIndex, setSelectedPolylinePointIndex] = React.useState(null); + const [hoveredPolylinePointIndex, setHoveredPolylinePointIndex] = React.useState(null); + const [userCameraInteractionActive, setUserCameraInteractionActive] = React.useState(true); + + const [prevBoundingBox, setPrevBoundingBox] = React.useState(undefined); + + if (!isEqual(props.boundingBox, prevBoundingBox)) { + setPrevBoundingBox(props.boundingBox); + let zMid = 0; + let zExtension = 10; + if ("zmin" in props.boundingBox && "zmax" in props.boundingBox) { + zMid = -(props.boundingBox.zmin + (props.boundingBox.zmax - props.boundingBox.zmin) / 2); + zExtension = Math.abs(props.boundingBox.zmax - props.boundingBox.zmin) + 100; + } + setIntersectionZValues({ + zMid, + zExtension, + }); + } + + const layers: Layer[] = []; + const layerIds: string[] = []; + + if (props.layers) { + layers.push(...props.layers); + layerIds.push(...props.layers.map((layer) => layer.id)); + } + + if (props.enableIntersectionPolylineEditing && polylineEditingActive) { + const zMid = intersectionZValues?.zMid || 0; + const zExtension = intersectionZValues?.zExtension || 10; + + const currentlyEditedPolylineData = makePolylineData( + currentlyEditedPolyline, + zMid, + zExtension, + selectedPolylinePointIndex, + hoveredPolylinePointIndex, + [255, 255, 255, 255] + ); + + const userPolylinePolygonsData = currentlyEditedPolylineData.polygonData; + const userPolylineColumnsData = currentlyEditedPolylineData.columnData; + + const userPolylineLineLayer = new SolidPolygonLayer({ + id: "user-polyline-line-layer", + 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); + layerIds.push(userPolylineLineLayer.id); + + function handleHover(pickingInfo: PickingInfo): void { + if (pickingInfo.object && pickingInfo.object.index < currentlyEditedPolyline.length) { + setHoveredPolylinePointIndex(pickingInfo.object.index); + } else { + setHoveredPolylinePointIndex(null); + } + } + + function handleClick(pickingInfo: PickingInfo, event: any): void { + if (pickingInfo.object && pickingInfo.object.index < currentlyEditedPolyline.length) { + setSelectedPolylinePointIndex(pickingInfo.object.index); + event.stopPropagation(); + event.handled = true; + } else { + setSelectedPolylinePointIndex(null); + } + } + + function handleDragStart(pickingInfo: PickingInfo): void { + if (pickingInfo.object && selectedPolylinePointIndex === pickingInfo.object.index) { + setUserCameraInteractionActive(false); + } + } + + function handleDragEnd(): void { + setUserCameraInteractionActive(true); + } + + function handleDrag(pickingInfo: PickingInfo): void { + if (pickingInfo.object) { + const index = pickingInfo.object.index; + if (!pickingInfo.coordinate) { + return; + } + setCurrentlyEditedPolyline((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[][]); + + if (props.onIntersectionPolylineChange) { + // props.onIntersectionPolylineChange(newPolyline); + } + return newPolyline; + }); + } + } + + 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: handleHover, + onClick: handleClick, + onDragStart: handleDragStart, + onDragEnd: handleDragEnd, + onDrag: handleDrag, + }); + layers.push(userPolylinePointLayer); + layerIds.push(userPolylinePointLayer.id); + } + + function handleMouseClick(event: MapMouseEvent): void { + if (!props.enableIntersectionPolylineEditing) { + return; + } + + if (!event.x || !event.y) { + return; + } + + // Do not create new polyline point when clicking on an already existing point + for (const info of event.infos) { + if ("layer" in info && info.layer?.id === "user-polyline-point-layer") { + if (info.picked) { + return; + } + } + } + + const newPoint = [event.x, event.y]; + setCurrentlyEditedPolyline((prev) => { + let newPolyline: number[][] = []; + if (selectedPolylinePointIndex === null || selectedPolylinePointIndex === prev.length - 1) { + newPolyline = [...prev, newPoint]; + setSelectedPolylinePointIndex(prev.length); + } else if (selectedPolylinePointIndex === 0) { + newPolyline = [newPoint, ...prev]; + setSelectedPolylinePointIndex(0); + } else { + newPolyline = prev; + } + // props.onEditPolylineChange(newPolyline); + return newPolyline; + }); + } + + function handleMouseEvent(event: MapMouseEvent): void { + if (event.type === "click") { + handleMouseClick(event); + } + } + + React.useEffect(() => { + function handleKeyboardEvent(event: KeyboardEvent) { + if (!props.enableIntersectionPolylineEditing) { + return; + } + if (event.key === "Delete" && selectedPolylinePointIndex !== null) { + setSelectedPolylinePointIndex((prev) => (prev === null || prev === 0 ? null : prev - 1)); + setCurrentlyEditedPolyline((prev) => { + const newPolyline = prev.filter((_, i) => i !== selectedPolylinePointIndex); + // props.onEditPolylineChange(newPolyline); + return newPolyline; + }); + } + } + + document.addEventListener("keydown", handleKeyboardEvent); + + return () => { + document.removeEventListener("keydown", handleKeyboardEvent); + }; + }, [ + selectedPolylinePointIndex, + setCurrentlyEditedPolyline, + setSelectedPolylinePointIndex, + props.enableIntersectionPolylineEditing, + ]); + + return ( +
+
+ {props.enableIntersectionPolylineEditing && ( + + )} +
+ {props.enableIntersectionPolylineEditing && polylineEditingActive && ( + + )} + +
+ ); +} + +type PolylineEditingPanelProps = { + currentlyEditedPolyline: number[][]; + selectedPolylineIndex: number | null; + hoveredPolylineIndex: number | null; + onPolylinePointSelectionChange: (index: number | null) => void; + onPolylineChange: (polyline: number[][]) => void; +}; + +export function PolylineEditingPanel(props: PolylineEditingPanelProps): React.ReactNode { + function handlePolylinePointSelectionChange(values: string[]): void { + if (values.length === 0) { + props.onPolylinePointSelectionChange(null); + } else { + props.onPolylinePointSelectionChange(parseInt(values[0], 10)); + } + } + + return ( +
+
Polyline editing
+