Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: Hooks for multi-picking and cursor-tracking, changes to python directory to provide functionality in Dash #2398

Merged
merged 11 commits into from
Dec 27, 2024
390 changes: 196 additions & 194 deletions python/package-lock.json

Large diffs are not rendered by default.

9 changes: 5 additions & 4 deletions python/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,8 @@
"validate": "npm run typecheck && npm run lint"
},
"dependencies": {
"@deck.gl/core": "^8.9.35",
"@deck.gl/core": "^9.0.36",
"@deck.gl/react": "^9.0.36",
"@emerson-eps/color-tables": "^0.4.85",
"@equinor/eds-core-react": "0.33.0",
"@equinor/eds-icons": "^0.19.1",
Expand All @@ -45,8 +46,8 @@
"leaflet-draw": "^1.0.4",
"lodash": "^4.17.21",
"mathjs": "^9.4.2",
"react": "^18.2.0",
"react-dom": "^18.2.0",
"react": "^18.3.1",
"react-dom": "^18.3.1",
"react-dropdown-tree-select": "^2.8.0",
"react-redux": "^8.1.1",
"react-resize-detector": "^9.0.0"
Expand All @@ -61,7 +62,7 @@
"@types/leaflet": "^1.8.0",
"@types/leaflet-draw": "^1.0.8",
"@types/lodash": "^4.14.199",
"@types/react": "^18.2.7",
"@types/react": "^18.3.12",
"@types/react-dom": "^18.2.7",
"@typescript-eslint/eslint-plugin": "^6.4.0",
"@typescript-eslint/parser": "^6.4.0",
Expand Down

This file was deleted.

1 change: 0 additions & 1 deletion python/src/components/DashSubsurfaceViewer/index.ts

This file was deleted.

70 changes: 70 additions & 0 deletions python/src/components/ReadoutComponent/ReadoutComponent.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,70 @@
import React from "react";

import { PickingInfoPerView } from "@webviz/subsurface-viewer/src/hooks/useMultiViewPicking";

function ReadoutComponent(props: {
viewId: string;
pickingInfoPerView: PickingInfoPerView;
}): React.ReactNode {
return (
<div
style={{
position: "absolute",
bottom: 24,
left: 8,
background: "#fff",
padding: 8,
borderRadius: 4,
display: "grid",
gridTemplateColumns: "8rem auto",
border: "1px solid #ccc",
fontSize: "0.8rem",
zIndex: 10,
}}
>
<div>X:</div>
<div>
{roundToSignificant(
props.pickingInfoPerView[props.viewId]?.coordinates?.at(0)
)}
</div>
<div>Y:</div>
<div>
{roundToSignificant(
props.pickingInfoPerView[props.viewId]?.coordinates?.at(1)
)}
</div>
{props.pickingInfoPerView[props.viewId]?.layerPickingInfo.map(
(el) => (
<React.Fragment key={`${el.layerId}`}>
<div style={{ fontWeight: "bold" }}>{el.layerName}</div>
{el.properties.map((prop, i) => (
<React.Fragment key={`${el.layerId}-${i}}`}>
<div style={{ gridColumn: 1 }}>{prop.name}</div>
<div>
{typeof prop.value === "string"
? prop.value
: roundToSignificant(prop.value)}
</div>
</React.Fragment>
))}
</React.Fragment>
)
) ?? ""}
</div>
);
}

function roundToSignificant(num: number | undefined) {
if (num === undefined) {
return "-";
}
// Returns two significant figures (non-zero) for numbers with an absolute value less
// than 1, and two decimal places for numbers with an absolute value greater
// than 1.
return parseFloat(
num.toExponential(Math.max(1, 2 + Math.log10(Math.abs(num))))
);
}

export default ReadoutComponent;
1 change: 1 addition & 0 deletions python/src/components/ReadoutComponent/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export { default } from "./ReadoutComponent";
168 changes: 161 additions & 7 deletions python/src/components/SubsurfaceViewer/SubsurfaceViewer.tsx
Original file line number Diff line number Diff line change
@@ -1,21 +1,175 @@
import React from "react";
import { SubsurfaceViewerProps } from "@webviz/subsurface-viewer";
import {
MapMouseEvent,
SubsurfaceViewerProps,
ViewStateType,
} from "@webviz/subsurface-viewer";
import { DeckGLRef } from "@deck.gl/react";
import { useMultiViewPicking } from "@webviz/subsurface-viewer/src/hooks/useMultiViewPicking";
import { useMultiViewCursorTracking } from "@webviz/subsurface-viewer/src/hooks/useMultiViewCursorTracking";
import { isEqual } from "lodash";

const SubsurfaceViewerComponent = React.lazy(
() =>
import(
/* webpackChunkName: "webviz-subsurface-viewer" */ "@webviz/subsurface-viewer"
)
const SubsurfaceViewerComponent = React.lazy(() =>
import(
/* webpackChunkName: "webviz-subsurface-viewer" */ "@webviz/subsurface-viewer"
).then((module) => ({
default:
module.DashSubsurfaceViewer as unknown as React.ComponentType<SubsurfaceViewerProps>,
}))
);

const SubsurfaceViewer: React.FC<SubsurfaceViewerProps> = (props) => {
const { views, children, ...rest } = props;

if (!views) {
return (
<React.Suspense fallback={<div>Loading...</div>}>
<SubsurfaceViewerComponent {...rest}>
{props.children}
</SubsurfaceViewerComponent>
</React.Suspense>
);
}

return (
<React.Suspense fallback={<div>Loading...</div>}>
<SubsurfaceViewerComponent {...props} />
<MultiViewSubsurfaceViewer {...rest} views={views}>
{children}
</MultiViewSubsurfaceViewer>
</React.Suspense>
);
};

function MultiViewSubsurfaceViewer(
props: SubsurfaceViewerProps &
Required<Pick<SubsurfaceViewerProps, "views">>
) {
const { onMouseEvent, getCameraPosition } = props;

const deckGlRef = React.useRef<DeckGLRef>(null);

const [mouseHover, setMouseHover] = React.useState<boolean>(false);
const [cameraPosition, setCameraPosition] = React.useState<
ViewStateType | undefined
>(undefined);
const [prevCameraPosition, setPrevCameraPosition] = React.useState<
ViewStateType | undefined
>(undefined);

if (!isEqual(prevCameraPosition, props.cameraPosition)) {
setPrevCameraPosition(props.cameraPosition);
}

const { getPickingInfo, activeViewportId, pickingInfoPerView } =
useMultiViewPicking({
deckGlRef,
multiPicking: true,
pickDepth: 1,
});

const handleMouseEvent = React.useCallback(
function handleMouseEvent(event: MapMouseEvent) {
if (event.type === "hover") {
getPickingInfo(event);
}
onMouseEvent?.(event);
},
[getPickingInfo, onMouseEvent]
);

const handleCameraPositionChange = React.useCallback(
function handleCameraPositionChange(position: ViewStateType) {
setCameraPosition(position);
getCameraPosition?.(position);
},
[getCameraPosition]
);

const viewports = props.views?.viewports ?? [];
const layers = props.layers ?? [];

const { viewports: adjustedViewports, layers: adjustedLayers } =
useMultiViewCursorTracking({
activeViewportId,
worldCoordinates:
pickingInfoPerView[activeViewportId]?.coordinates ?? null,
viewports,
layers,
crosshairProps: {
color: [255, 255, 255, 255],
sizePx: 32,
visible: mouseHover,
},
});

const foundViewAnnotations: string[] = [];
const children = React.Children.map(props.children, (child) => {
// Child wrapped in DashWrapper
if (
React.isValidElement(child) &&
typeof child.props === "object" &&
Object.keys(child.props).includes("_dashprivate_layout") &&
child.props._dashprivate_layout.type === "ViewAnnotation"
) {
const id = child.props._dashprivate_layout.props.id;
const readout = adjustedViewports.find(
(viewport) => viewport.id === id
);
if (!readout) {
return child;
}
foundViewAnnotations.push(id);
const newChild = React.cloneElement(child, {
// @ts-expect-error - this is proven to be a valid prop in Dash components
_dashprivate_layout: {
...child.props._dashprivate_layout,
props: {
...child.props._dashprivate_layout.props,
children: [
...child.props._dashprivate_layout.props.children,
{
type: "ReadoutComponent",
props: {
viewId: id,
pickingInfoPerView,
},
namespace: "webviz_subsurface_components",
},
],
},
},
});
return newChild;
}
});

return (
<div
onMouseEnter={() => setMouseHover(true)}
onMouseLeave={() => setMouseHover(false)}
onBlur={() => setMouseHover(false)}
onFocus={() => setMouseHover(true)}
>
<SubsurfaceViewerComponent
{...props}
coords={{ visible: false }}
onMouseEvent={handleMouseEvent}
layers={adjustedLayers}
views={{
...props.views,
viewports: adjustedViewports,
layout: props.views.layout,
}}
cameraPosition={cameraPosition}
deckGlRef={deckGlRef}
getCameraPosition={handleCameraPositionChange}
>
{children}
</SubsurfaceViewerComponent>
</div>
);
}

SubsurfaceViewer.displayName = "SubsurfaceViewer";

export default SubsurfaceViewer;
10 changes: 2 additions & 8 deletions python/src/components/WellLogViewer/WellLogViewer.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -9,14 +9,8 @@ const WellLogViewerComponent = React.lazy(() =>
}))
);

import type { ColorMapFunction } from "../components/ColorMapFunction";
import type { WellPickProps } from "../components/WellLogView";

// react-docgen / dash-generate-components/extract-meta.js does not properly parse
// the imported WellLogViewerProps. Hence, we have to recreate them here.
/**
* WellLogView additional options
*/
type WellLogViewOptions = {
/** The maximum zoom value */
maxContentZoom?: number;
Expand Down Expand Up @@ -56,7 +50,7 @@ type WellLogViewerProps = {
template: object;

/** Prop containing color function/table array */
colorMapFunctions: ColorMapFunction[];
colorMapFunctions: unknown;

/** Orientation of the track plots on the screen. Default is false */
horizontal?: boolean;
Expand All @@ -68,7 +62,7 @@ type WellLogViewerProps = {
selection?: number[];

/** Well picks data */
wellpick?: WellPickProps;
wellpick?: unknown;

/** Primary axis id: " md", "tvd", "time"... */
primaryAxis?: string;
Expand Down
5 changes: 3 additions & 2 deletions python/src/index.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,3 @@
import DashSubsurfaceViewer from "./components/DashSubsurfaceViewer";
import SubsurfaceViewer from "./components/SubsurfaceViewer";
import { GroupTree } from "./components/GroupTree";
import HistoryMatch from "./components/HistoryMatch";
Expand All @@ -10,6 +9,7 @@ import VectorSelector from "./components/VectorSelector";
import { WellCompletions } from "./components/WellCompletions";
import { VectorCalculator } from "./components/VectorCalculator";
import WellLogViewer from "./components/WellLogViewer";
import ReadoutComponent from "./components/ReadoutComponent";
import SyncLogViewer from "./components/WellLogViewer";
import WebVizContinuousLegend from "./components/ColorLegends/WebVizContinuousLegend";
import WebVizDiscreteLegend from "./components/ColorLegends/WebVizDiscreteLegend";
Expand All @@ -30,12 +30,13 @@ export {
* @deprecated Use the {@link SubsurfaceViewer} component instead.
*/
SubsurfaceViewer as DeckGLMap, // For backwards compatibility
DashSubsurfaceViewer,
SubsurfaceViewer as DashSubsurfaceViewer, // For backwards compatibility
VectorSelector,
WellCompletions,
VectorCalculator,
GroupTree,
WellLogViewer,
ReadoutComponent,
SyncLogViewer,
WebVizContinuousLegend,
WebVizDiscreteLegend,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,10 +2,24 @@ import React from "react";
import type { SubsurfaceViewerProps } from "./SubsurfaceViewer";
import SubsurfaceViewer from "./SubsurfaceViewer";
import { View } from "@deck.gl/core";
import { ViewAnnotation } from "./components/ViewAnnotation";

function mapAnnotation(annotationContainers: React.ReactNode) {
return React.Children.map(annotationContainers, (annotationContainer) => {
const viewId = (annotationContainer as React.ReactElement).key;
let viewId = (annotationContainer as React.ReactElement).props.id;
if (
React.isValidElement(annotationContainer) &&
(annotationContainer.type === ViewAnnotation ||
(annotationContainer.props instanceof Object &&
Object.keys(annotationContainer.props).includes(
"_dashprivate_layout"
)))
) {
viewId = annotationContainer.props._dashprivate_layout.props.id;
}
if (!viewId) {
return null;
}
return (
// @ts-expect-error This is proven to work in JavaScript
<View key={viewId} id={viewId}>
Expand Down
Loading
Loading