Skip to content

Commit b4673a1

Browse files
authored
fix: Subsurface viewer - Assorted fixes to wells layer and multi pick hook (#2491)
* Made the `useMultiViewPicking` hook able to return picks from multiple wells if hovering on overlapping wells * Generally makes it so the hook is able to pick multiple items from the same layer, whereas before it would only take the first * Added a new multi-pick example with wells to show it * Adds a more descriptive typing for the GeoJson feature we use in the WellsLayer data prop -- `WellFeatureCollection`. * Most notably, we now specify what should be in the properties object. * Made most utils take this new specified typing, and removed/clean up a buuunch of spots where we were just assuming validity and casting any-values * Exposed or specified some extra types that made sense to export as well * Updated the `colorMappingFunction` prop to match the `createColorMapFunction` from emerson table. * The only difference is that it now specifies a couple of extra (optional) params. We were actually using these params, so the typing should now reflect that * Refactored some messy code in the wells layer class
1 parent c00788c commit b4673a1

File tree

15 files changed

+480
-367
lines changed

15 files changed

+480
-367
lines changed

typescript/packages/subsurface-viewer/src/components/Map.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -51,7 +51,7 @@ import {
5151
getModelMatrixScale,
5252
getWellLayerByTypeAndSelectedWells,
5353
} from "../layers/utils/layerTools";
54-
import type { WellsPickInfo } from "../layers/wells/wellsLayer";
54+
import type { WellsPickInfo } from "../layers/wells/types";
5555
import type { BoundingBox2D, Point2D } from "../utils/BoundingBox2D";
5656
import { isEmpty as isEmptyBox2D } from "../utils/BoundingBox2D";
5757
import type { BoundingBox3D, Point3D } from "../utils/BoundingBox3D";

typescript/packages/subsurface-viewer/src/hooks/useMultiViewPicking/MultiViewPickingInfoAssembler.ts

Lines changed: 68 additions & 60 deletions
Original file line numberDiff line numberDiff line change
@@ -1,24 +1,26 @@
11
import type { PickingInfo, Viewport } from "@deck.gl/core";
22
import type { DeckGLRef } from "@deck.gl/react";
3+
4+
import _ from "lodash";
5+
36
import type {
47
ExtendedLayerProps,
58
MapMouseEvent,
69
PropertyDataType,
710
} from "../../";
11+
import { distance } from "mathjs";
812

913
export type LayerPickingInfo = {
1014
layerId: string;
1115
layerName: string;
1216
properties: PropertyDataType[];
1317
};
1418

15-
export type PickingInfoPerView = Record<
16-
string,
17-
{
18-
coordinates: number[] | null;
19-
layerPickingInfo: LayerPickingInfo[];
20-
}
21-
>;
19+
export type ViewportPickInfo = {
20+
coordinates: number[] | null;
21+
layerPickingInfo: LayerPickingInfo[];
22+
};
23+
export type PickingInfoPerView = Record<string, ViewportPickInfo>;
2224

2325
function hasPropertiesArray(
2426
// eslint-disable-next-line @typescript-eslint/no-explicit-any
@@ -34,6 +36,10 @@ function hasSingleProperty(obj: any): obj is { propertyValue: number } {
3436
);
3537
}
3638

39+
function getUniqueInfoPickId(pick: PickingInfo): string {
40+
return `${pick.index}::${pick.layer?.id}`;
41+
}
42+
3743
export type MultiViewPickingInfoAssemblerOptions = {
3844
multiPicking: boolean;
3945
pickDepth: number;
@@ -114,14 +120,43 @@ export class MultiViewPickingInfoAssembler {
114120
});
115121
}
116122

123+
private pickAtCoordinate(x: number, y: number): PickingInfo[] {
124+
const deck = this._deckGl?.deck;
125+
if (!deck) return [];
126+
127+
if (this._options.multiPicking) {
128+
// ! For some reason, multi-pick pads the array up to pick-depth length, repeating the last element
129+
const multPickResult = deck.pickMultipleObjects({
130+
depth: this._options.pickDepth,
131+
unproject3D: true,
132+
x,
133+
y,
134+
});
135+
136+
// Ensure the top-most element is processed first by sorting based on distance to camera
137+
return _.sortBy(multPickResult, (pick) => {
138+
if (!pick.viewport?.cameraPosition) return -1;
139+
if (!pick.coordinate) return -1;
140+
141+
return distance(pick.coordinate, pick.viewport.cameraPosition);
142+
});
143+
} else {
144+
const obj = deck.pickObject({
145+
unproject3D: true,
146+
x,
147+
y,
148+
});
149+
return obj ? [obj] : [];
150+
}
151+
}
152+
117153
private async assembleMultiViewPickingInfo(
118154
eventScreenCoordinate: [number, number],
119155
activeViewportId: string,
120156
viewports: Viewport[]
121157
): Promise<PickingInfoPerView> {
122158
return new Promise((resolve, reject) => {
123-
const deck = this._deckGl?.deck;
124-
if (!deck) {
159+
if (!this._deckGl?.deck) {
125160
reject("DeckGL not initialized");
126161
return;
127162
}
@@ -143,80 +178,53 @@ export class MultiViewPickingInfoAssembler {
143178
);
144179

145180
const collectedPickingInfo: PickingInfoPerView = {};
181+
146182
for (const viewport of viewports) {
147183
const [relativeScreenX, relativeScreenY] =
148184
viewport.project(worldCoordinate);
149185

150-
let pickingInfo: PickingInfo[] = [];
151-
if (this._options.multiPicking) {
152-
pickingInfo = deck.pickMultipleObjects({
153-
x: relativeScreenX + viewport.x,
154-
y: relativeScreenY + viewport.y,
155-
depth: this._options.pickDepth,
156-
unproject3D: true,
157-
});
158-
} else {
159-
const obj = deck.pickObject({
160-
x: relativeScreenX + viewport.x,
161-
y: relativeScreenY + viewport.y,
162-
unproject3D: true,
163-
});
164-
pickingInfo = obj ? [obj] : [];
165-
}
186+
const pickingInfo = this.pickAtCoordinate(
187+
relativeScreenX + viewport.x,
188+
relativeScreenY + viewport.y
189+
);
166190

167191
if (pickingInfo) {
168-
const collectedLayerPickingInfo: LayerPickingInfo[] = [];
192+
const layerInfoDict: Record<string, LayerPickingInfo> = {};
193+
const processedPickIds: string[] = [];
194+
169195
for (const info of pickingInfo) {
196+
const uniquePickId = getUniqueInfoPickId(info);
170197
const hasMultipleProperties = hasPropertiesArray(info);
171198
const hasOneProperty = hasSingleProperty(info);
172199

173-
if (!hasMultipleProperties && !hasOneProperty) {
174-
continue;
175-
}
176-
177-
if (!info.layer) {
178-
continue;
179-
}
200+
// General guard clauses
201+
if (!info.layer) continue;
202+
if (processedPickIds.includes(uniquePickId)) continue;
203+
if (!hasMultipleProperties && !hasOneProperty) continue;
180204

181-
if (
182-
collectedLayerPickingInfo.find(
183-
(el) => el.layerId === info.layer?.id
184-
)
185-
) {
186-
continue;
187-
}
205+
processedPickIds.push(uniquePickId);
188206

189207
const layerId = info.layer.id;
190-
const layerName = (
191-
info.layer.props as unknown as ExtendedLayerProps
192-
).name;
208+
const layerProps = info.layer
209+
.props as unknown as ExtendedLayerProps;
210+
const layerName = layerProps.name;
193211

194-
let layerPickingInfo = collectedLayerPickingInfo.find(
195-
(el) => el.layerId === layerId
196-
);
197-
198-
if (!layerPickingInfo) {
199-
collectedLayerPickingInfo.push({
212+
if (!layerInfoDict[layerId]) {
213+
layerInfoDict[layerId] = {
200214
layerId,
201215
layerName,
202216
properties: [],
203-
});
204-
layerPickingInfo =
205-
collectedLayerPickingInfo[
206-
collectedLayerPickingInfo.length - 1
207-
];
217+
};
208218
}
219+
const layerPickingInfo = layerInfoDict[layerId];
209220

210221
if (hasOneProperty) {
211222
layerPickingInfo.properties.push({
212223
name: "Value",
213224
value: info.propertyValue,
214225
color: undefined,
215226
});
216-
continue;
217-
}
218-
219-
if (hasMultipleProperties) {
227+
} else if (hasMultipleProperties) {
220228
const properties = info.properties;
221229

222230
for (const property of properties) {
@@ -231,11 +239,11 @@ export class MultiViewPickingInfoAssembler {
231239

232240
collectedPickingInfo[viewport.id] = {
233241
coordinates: worldCoordinate,
234-
layerPickingInfo: collectedLayerPickingInfo,
242+
layerPickingInfo: Object.values(layerInfoDict),
235243
};
236244
} else {
237245
collectedPickingInfo[viewport.id] = {
238-
coordinates: null,
246+
coordinates: worldCoordinate,
239247
layerPickingInfo: [],
240248
};
241249
}

typescript/packages/subsurface-viewer/src/layers/types.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@ export type NumberPair = [number, number];
55

66
type StyleData = NumberPair | Color | number;
77

8-
export type StyleAccessorFunction = (
9-
object: Feature,
8+
export type StyleAccessorFunction<TFeature extends Feature = Feature> = (
9+
object: TFeature,
1010
objectInfo?: Record<string, unknown>
1111
) => StyleData;

typescript/packages/subsurface-viewer/src/layers/utils/layerTools.ts

Lines changed: 7 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,9 @@
11
import type { PickingInfo } from "@deck.gl/core";
22
import type { Color } from "@deck.gl/core";
3-
import type { colorTablesArray } from "@emerson-eps/color-tables/";
3+
import type {
4+
colorTablesArray,
5+
createColorMapFunction,
6+
} from "@emerson-eps/color-tables/";
47
import { rgbValues } from "@emerson-eps/color-tables/";
58
import { createDefaultContinuousColorScale } from "@emerson-eps/color-tables/dist/component/Utils/legendCommonFunction";
69

@@ -20,7 +23,7 @@ import type DrawingLayer from "../drawing/drawingLayer";
2023
export type Position3D = [number, number, number];
2124

2225
/** Type of functions returning a color from a value in the [0,1] range. */
23-
export type ColorMapFunctionType = (x: number) => [number, number, number];
26+
export type ColorMapFunctionType = ReturnType<typeof createColorMapFunction>;
2427
/** @deprecated Use ColorMapFunctionType instead. */
2528
export type colorMapFunctionType = ColorMapFunctionType;
2629

@@ -44,7 +47,8 @@ export interface PropertyDataType {
4447
}
4548

4649
// Layer pick info can have multiple properties
47-
export interface LayerPickInfo extends PickingInfo {
50+
// eslint-disable-next-line @typescript-eslint/no-explicit-any -- We use "any" to match picking-info default
51+
export interface LayerPickInfo<T = any> extends PickingInfo<T> {
4852
propertyValue?: number; // for single property
4953
properties?: PropertyDataType[]; // for multiple properties
5054
}

typescript/packages/subsurface-viewer/src/layers/wells/layers/wellLabelLayer.ts

Lines changed: 11 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -1,18 +1,14 @@
1-
import {
2-
type DefaultProps,
3-
type Position,
4-
type UpdateParameters,
5-
} from "@deck.gl/core";
1+
import { type DefaultProps, type UpdateParameters } from "@deck.gl/core";
62
import type { TextLayerProps } from "@deck.gl/layers";
73
import { TextLayer } from "@deck.gl/layers";
8-
import type { Feature, GeoJsonProperties, Geometry } from "geojson";
4+
import type { Feature, Position } from "geojson";
95
import _ from "lodash";
106
import { Vector2, Vector3 } from "math.gl";
117
import type { Position3D } from "../../utils/layerTools";
128
import { getTrajectory } from "../utils/trajectory";
9+
import type { WellFeature } from "../types";
1310

14-
type WellLabelLayerData = Feature<Geometry, GeoJsonProperties>;
15-
11+
type WellLabelLayerData = WellFeature;
1612
/**
1713
* Enum representing the orientation of well labels.
1814
*/
@@ -58,8 +54,8 @@ const DEFAULT_PROPS: DefaultProps<WellLabelLayerProps> = {
5854
id: "well-label-layer",
5955
autoPosition: false,
6056
getPositionAlongPath: 0,
61-
getText: (d: Feature) => {
62-
const name = d.properties?.["name"];
57+
getText: (d: WellFeature) => {
58+
const name = d.properties.name;
6359
return name;
6460
},
6561
getAlignmentBaseline: "bottom",
@@ -123,13 +119,13 @@ export class WellLabelLayer extends TextLayer<
123119

124120
const newProps = {
125121
...sublayerProps,
126-
getPosition: (d: Feature) => {
122+
getPosition: (d: WellFeature) => {
127123
return this.getAnnotationPosition(
128124
d,
129125
this.props.getPositionAlongPath
130126
);
131127
},
132-
getAngle: (d: Feature) => this.getLabelAngle(d),
128+
getAngle: (d: WellFeature) => this.getLabelAngle(d),
133129
updateTriggers: {
134130
...sublayerProps?.updateTriggers,
135131
getAngle: [
@@ -160,7 +156,7 @@ export class WellLabelLayer extends TextLayer<
160156
return super.shouldUpdateState(params);
161157
}
162158

163-
protected getLabelAngle(well: Feature) {
159+
protected getLabelAngle(well: WellFeature) {
164160
if (this.props.orientation === "horizontal") {
165161
return 0;
166162
}
@@ -178,7 +174,7 @@ export class WellLabelLayer extends TextLayer<
178174
* @returns world position of the annotation
179175
*/
180176
protected getAnnotationPosition(
181-
well_data: Feature,
177+
well_data: WellFeature,
182178
annotationPosition: WellLabelLayerProps["getPositionAlongPath"]
183179
): Position {
184180
let fraction = Number(annotationPosition);
@@ -236,7 +232,7 @@ export class WellLabelLayer extends TextLayer<
236232
*/
237233
protected getVectorAlongTrajectory(
238234
fraction: number,
239-
wellData: Feature
235+
wellData: WellFeature
240236
): [number, Position3D] {
241237
if (!wellData) {
242238
return [0, [0, 0, 0]];
Lines changed: 58 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,58 @@
1+
import type { Color } from "@deck.gl/core";
2+
import type { Feature, GeometryCollection, FeatureCollection } from "geojson";
3+
import type { LayerPickInfo } from "../utils/layerTools";
4+
import type { NumberPair, StyleAccessorFunction } from "../types";
5+
6+
export type GeoJsonWellProperties = {
7+
name: string;
8+
md: number[][];
9+
color?: Color;
10+
};
11+
12+
export type WellFeature = Feature<GeometryCollection, GeoJsonWellProperties>;
13+
export type WellFeatureCollection = FeatureCollection<
14+
GeometryCollection,
15+
GeoJsonWellProperties
16+
>;
17+
18+
export interface WellsPickInfo extends LayerPickInfo<WellFeature> {
19+
featureType?: string;
20+
logName: string;
21+
}
22+
23+
export interface LogCurveDataType {
24+
header: {
25+
name: string;
26+
well: string;
27+
};
28+
curves: {
29+
name: string;
30+
description: string;
31+
}[];
32+
data: number[][];
33+
metadata_discrete: Record<
34+
string,
35+
{
36+
attributes: unknown;
37+
objects: Record<string, [Color, number]>;
38+
}
39+
>;
40+
}
41+
42+
export type ColorAccessor = Color | StyleAccessorFunction | undefined;
43+
export type SizeAccessor = number | StyleAccessorFunction | undefined;
44+
export type DashAccessor =
45+
| boolean
46+
| NumberPair
47+
| StyleAccessorFunction
48+
| undefined;
49+
50+
export type LineStyleAccessor = {
51+
color?: ColorAccessor;
52+
dash?: DashAccessor;
53+
width?: SizeAccessor;
54+
};
55+
export type WellHeadStyleAccessor = {
56+
color?: ColorAccessor;
57+
size?: SizeAccessor;
58+
};

0 commit comments

Comments
 (0)