diff --git a/typescript/packages/subsurface-viewer/src/extensions/collision-modifier-extension.ts b/typescript/packages/subsurface-viewer/src/extensions/collision-modifier-extension.ts new file mode 100644 index 0000000000..2a5f7cdfb4 --- /dev/null +++ b/typescript/packages/subsurface-viewer/src/extensions/collision-modifier-extension.ts @@ -0,0 +1,20 @@ +import type { Layer } from "@deck.gl/core"; +import { CollisionFilterExtension } from "@deck.gl/extensions"; + +const injectionVs = { + "vs:DECKGL_FILTER_COLOR": ` + if (collision_fade != 0.0) { + color.a = 1.0 / collision_fade; // Note: this will counteract the fading of the labels caused by deck.gl's CollisionFilterExtension + } + `, +}; + +export class CollisionModifierExtension extends CollisionFilterExtension { + getShaders(this: Layer) { + const superShaders = super.getShaders(); + return { + ...superShaders, + inject: injectionVs, + }; + } +} diff --git a/typescript/packages/subsurface-viewer/src/layers/wells/wellsLayer.ts b/typescript/packages/subsurface-viewer/src/layers/wells/wellsLayer.ts index a1b059d0dd..f49e16f724 100644 --- a/typescript/packages/subsurface-viewer/src/layers/wells/wellsLayer.ts +++ b/typescript/packages/subsurface-viewer/src/layers/wells/wellsLayer.ts @@ -7,12 +7,13 @@ import type { Position, UpdateParameters, } from "@deck.gl/core"; - +import { Vector2 } from "@math.gl/core"; +import { CollisionModifierExtension } from "../../extensions/collision-modifier-extension"; import { CompositeLayer, OrbitViewport } from "@deck.gl/core"; - import type { ExtendedLayerProps, LayerPickInfo, + Position3D, PropertyDataType, } from "../utils/layerTools"; @@ -53,6 +54,7 @@ import { invertPath, splineRefine, } from "./utils/spline"; +import { clamp } from "lodash"; type StyleAccessorFunction = ( object: Feature, @@ -112,9 +114,16 @@ export interface WellsLayerProps extends ExtendedLayerProps { colorMappingFunction: (x: number) => [number, number, number]; lineStyle: LineStyleAccessor; wellNameVisible: boolean; - wellNameAtTop: boolean; + /** It true place name at top, if false at bottom. + * If given as a number between 0 and 100, will place name at this percentage of trajectory from top. + */ + wellNameAtTop: boolean | number; wellNameSize: number; wellNameColor: Color; + /** If true will prevent well name cluttering by not displaying overlapping names. + * default false. + */ + hideOverlappingWellNames: boolean; isLog: boolean; depthTest: boolean; /** If true means that input z values are interpreted as depths. @@ -148,9 +157,10 @@ const defaultProps = { refine: false, visible: true, wellNameVisible: false, - wellNameAtTop: false, + wellNameAtTop: true, wellNameSize: 14, wellNameColor: [0, 0, 0, 255], + hideOverlappingWellNames: false, selectedWell: "@@#editedData.selectedWells", // used to get data from deckgl layer depthTest: true, ZIncreasingDownwards: true, @@ -299,9 +309,25 @@ export default class WellsLayer extends CompositeLayer { ...this.state, data, coarseData, + camChange: 0, }); } + shouldUpdateState({ changeFlags }: UpdateParameters): boolean { + if (changeFlags.viewportChanged) { + this.setState({ + ...this.state, + camChange: (this.state["camChange"] as number) + 1, + }); + } + + return ( + changeFlags.viewportChanged || + changeFlags.propsOrDataChanged || + typeof changeFlags.updateTriggersChanged === "object" + ); + } + updateState({ props, oldProps }: UpdateParameters): void { const needs_reload = !isEqual(props.data, oldProps.data) || @@ -348,14 +374,6 @@ export default class WellsLayer extends CompositeLayer { } } - shouldUpdateState({ changeFlags }: UpdateParameters): boolean { - return ( - changeFlags.viewportChanged || - changeFlags.propsOrDataChanged || - typeof changeFlags.updateTriggersChanged === "object" - ); - } - getLegendData( value: LogCurveDataType[] ): ContinuousLegendDataType | DiscreteLegendDataType | null { @@ -395,6 +413,96 @@ export default class WellsLayer extends CompositeLayer { if (data) this.setLegend(data); } + // return position for well name and icon + getAnnotationPosition( + well_data: Feature, + annotation_position: boolean | number, + view_is_3d: boolean, + color_accessor: ColorAccessor + ): Position | null { + const percentage = + Number(annotation_position) * + (typeof annotation_position === "boolean" ? 100 : 1); + clamp(percentage, 0, 100); + if (percentage > 0 && percentage < 100) { + // Return a pos "name_at_top" percent down the trajectory + const pos = this.getTrajMidPoint( + percentage, + well_data, + (this.props.data as unknown as FeatureCollection).features + )[1]; + + // using z=0 for orthographic view to keep label above other other layers + if (pos) return view_is_3d ? pos : [pos[0], pos[1], 0]; + } else if (percentage == 0) { + // Read top position from Point geometry, if not present, read it from LineString geometry + let top; + // Read top position from Point geometry, if not present, read it from LineString geometry + const well_head = getWellHeadPosition(well_data); + if (well_head) top = well_head; + else { + const trajectory = getTrajectory(well_data, color_accessor); + top = trajectory?.at(0); + } + + // using z=0 for orthographic view to keep label above other other layers + if (top) return view_is_3d ? top : [top[0], top[1], 0]; + } else { + let bot; + // if trajectory is not present, return top position from Point geometry + const trajectory = getTrajectory(well_data, color_accessor); + if (trajectory) bot = trajectory?.at(-1); + else bot = getWellHeadPosition(well_data); + + // using z=0 for orthographic view to keep label above other other layers + if (bot) return view_is_3d ? bot : [bot[0], bot[1], 0]; + } + return null; + } + + getTrajMidPoint( + percent: number, + well_data: Feature, + features: Feature[] + ): [number, Position3D] { + const wellName = well_data.properties?.["name"]; + const well_object = getWellObjectByName(features, wellName); + if (!well_object) { + return [0, [0, 0, 0]]; + } + + const well_xyz = getTrajectory(well_object, undefined); + const n = well_xyz?.length ?? 2; + if (well_xyz && n >= 2) { + const i = Math.min(Math.floor((percent / 100) * n), n - 2); + const pi1 = well_xyz[i]; + const pi2 = well_xyz[i + 1]; + const p1 = new Vector2(this.project(pi1 as number[])); + const p2 = new Vector2(this.project(pi2 as number[])); + const pMid: Position3D = [ + pi1[0] + (pi2[0] - pi1[0]) / 2, + pi1[1] + (pi2[1] - pi1[1]) / 2, + pi1?.[2] ?? 0 + (pi2?.[2] ?? 0 - (pi1?.[2] ?? 0)) / 2, + ]; + const v = new Vector2(p2[0] - p1[0], -(p2[1] - p1[1])); + v.normalize(); + const rad = Math.atan2(v[1], v[0]) as number; + const deg = rad * (180 / 3.14159); + let a = deg; + if (deg > 90) { + a = deg - 180; + } else if (deg < -90) { + a = deg + 180; + } + if (typeof percent == "boolean" || percent == 0 || percent == 100) { + // At top or bottom well names should be horizontal. + a = 0; + } + return [a, pMid]; + } + return [0, [0, 0, 0]]; + } + renderLayers(): LayersList { if (!(this.props.data as unknown as FeatureCollection).features) { return []; @@ -618,24 +726,70 @@ export default class WellsLayer extends CompositeLayer { }) ); - // well name + // Reduced cluttering properties + const clutterProps = { + background: true, + collisionEnabled: true, + getCollisionPriority: (d: Feature) => { + const labelSize = d.properties?.["name"].length ?? 1; + if (is3d) { + // In 3D prioritize according to label size. + return labelSize; + } else { + // In 2D prioritize according z height. + const labelPosition = this.getAnnotationPosition( + d, + this.props.wellNameAtTop, + true, + this.props.lineStyle?.color + ); + + const priority = labelPosition + ? (labelPosition?.[2] ?? 1) / 10 // priority must be in [-1000, 1000] + : labelSize; + return priority; + } + }, + collisionTestProps: { + sizeScale: 1, + }, + collisionGroup: "nobodys", + extensions: [new CollisionModifierExtension()], + }; + const namesLayer = new TextLayer( this.getSubLayerProps({ id: "names", data: data.features, getPosition: (d: Feature) => - getAnnotationPosition( + this.getAnnotationPosition( d, this.props.wellNameAtTop, is3d, this.props.lineStyle?.color ), + getAngle: (f: Feature) => { + const percentage = Math.min( + Math.max(0, Number(this.props.wellNameAtTop)), + 100 + ); + const a = this.getTrajMidPoint( + percentage, + f, + (this.props.data as unknown as FeatureCollection) + .features + ); + const text_angle = a[0]; + return text_angle; + }, + getText: (d: Feature) => d.properties?.["name"], getColor: this.props.wellNameColor, getAnchor: "start", getAlignmentBaseline: "bottom", getSize: this.props.wellNameSize, updateTriggers: { + getAngle: [this.state["camChange"]], getPosition: [ this.props.wellNameAtTop, is3d, @@ -644,6 +798,8 @@ export default class WellsLayer extends CompositeLayer { }, parameters, visible: this.props.wellNameVisible && !fastDrawing, + + ...(this.props.hideOverlappingWellNames ? clutterProps : {}), }) ); @@ -791,39 +947,6 @@ function isSelectedLogRun(d: LogCurveDataType, logrun_name: string): boolean { return d.header.name.toLowerCase() === logrun_name.toLowerCase(); } -// return position for well name and icon -function getAnnotationPosition( - well_data: Feature, - name_at_top: boolean, - view_is_3d: boolean, - color_accessor: ColorAccessor -): Position | null { - if (name_at_top) { - let top; - - // Read top position from Point geometry, if not present, read it from LineString geometry - const well_head = getWellHeadPosition(well_data); - if (well_data) top = well_head; - else { - const trajectory = getTrajectory(well_data, color_accessor); - top = trajectory?.at(0); - } - - // using z=0 for orthographic view to keep label above other other layers - if (top) return view_is_3d ? top : [top[0], top[1], 0]; - } else { - let bot; - // if trajectory is not present, return top position from Point geometry - const trajectory = getTrajectory(well_data, color_accessor); - if (trajectory) bot = trajectory?.at(-1); - else bot = getWellHeadPosition(well_data); - - // using z=0 for orthographic view to keep label above other other layers - if (bot) return view_is_3d ? bot : [bot[0], bot[1], 0]; - } - return null; -} - function getWellObjectByName( wells_data: Feature[], name: string diff --git a/typescript/packages/subsurface-viewer/src/storybook/layers/WellsLayer.stories.tsx b/typescript/packages/subsurface-viewer/src/storybook/layers/WellsLayer.stories.tsx index 26105bbc3a..b67141acc1 100644 --- a/typescript/packages/subsurface-viewer/src/storybook/layers/WellsLayer.stories.tsx +++ b/typescript/packages/subsurface-viewer/src/storybook/layers/WellsLayer.stories.tsx @@ -17,7 +17,11 @@ import { NativeSelect } from "@equinor/eds-core-react"; import type { SubsurfaceViewerProps } from "../../SubsurfaceViewer"; import SubsurfaceViewer from "../../SubsurfaceViewer"; -import type { MapMouseEvent } from "../../components/Map"; +import type { + MapMouseEvent, + Point3D, + BoundingBox2D, +} from "../../components/Map"; import AxesLayer from "../../layers/axes/axesLayer"; import WellsLayer from "../../layers/wells/wellsLayer"; @@ -560,7 +564,11 @@ export const WellsRefine: StoryObj = { export const Wells3d: StoryObj = { args: { ...defaultProps, - layers: [volveWellsFromResourcesLayer], + layers: [ + { + ...volveWellsFromResourcesLayer, + }, + ], views: default3DViews, }, parameters: { @@ -614,6 +622,7 @@ const SimplifiedRenderingComponent: React.FC = ( layers: [ new WellsLayer({ data: "./gullfaks.json", + wellNameAtTop: true, wellHeadStyle: { size: 4 }, refine: true, outline: true, @@ -652,6 +661,114 @@ export const SimplifiedRendering: StoryObj = { render: (args) => , }; +type ClutterProps = { + hideOverlappingWellNames: boolean; + wellNamePositionPercentage: boolean | number; +}; + +const ReducedWellNameClutterComponent: React.FC = ( + props: ClutterProps +) => { + const propsWithLayers = { + id: "clutter", + layers: [ + new WellsLayer({ + data: "./gullfaks.json", + wellNameVisible: true, + wellNameAtTop: props.wellNamePositionPercentage, + wellHeadStyle: { size: 4 }, + wellNameSize: 9, + hideOverlappingWellNames: props.hideOverlappingWellNames, + refine: true, + outline: true, + ZIncreasingDownwards: false, + }), + new AxesLayer({ + id: "axes-layer", + bounds: [450000, 6781000, 0, 464000, 6791000, 3500], + }), + ], + cameraPosition: { + rotationOrbit: 45, + rotationX: 45, + zoom: -4, + target: [ + (450000 + 464000) / 2, + (6781000 + 6791000) / 2, + -3500 / 2, + ] as Point3D, + }, + views: { + layout: [1, 1] as [number, number], + viewports: [ + { + id: "view_1", + show3D: true, + }, + ], + }, + }; + + return ; +}; + +export const ReducedWellNameClutter3D: StoryObj< + typeof ReducedWellNameClutterComponent +> = { + args: { + hideOverlappingWellNames: true, + wellNamePositionPercentage: 0, + }, + render: (args) => , +}; + +const ReducedWellNameClutterComponent2D: React.FC = ( + props: ClutterProps +) => { + const propsWithLayers = { + id: "clutter", + layers: [ + new WellsLayer({ + data: "./gullfaks.json", + wellNameVisible: true, + wellNameSize: 9, + wellNameAtTop: props.wellNamePositionPercentage, + wellHeadStyle: { size: 4 }, + hideOverlappingWellNames: props.hideOverlappingWellNames, + refine: true, + outline: true, + ZIncreasingDownwards: false, + }), + new AxesLayer({ + id: "axes-layer", + bounds: [450000, 6781000, 0, 464000, 6791000, 3500], + }), + ], + bounds: [450000, 6781000, 464000, 6791000] as BoundingBox2D, + views: { + layout: [1, 1] as [number, number], + viewports: [ + { + id: "view_1", + show3D: false, + }, + ], + }, + }; + + return ; +}; + +export const ReducedWellNameClutter2D: StoryObj< + typeof ReducedWellNameClutterComponent +> = { + args: { + hideOverlappingWellNames: true, + wellNamePositionPercentage: 0, + }, + render: (args) => , +}; + export const Wells3dDashed: StoryObj = { args: { ...defaultProps, diff --git a/typescript/packages/subsurface-viewer/src/storybook/layers/__image_snapshots__/subsurfaceviewer-map-layer--typed-array-input.png b/typescript/packages/subsurface-viewer/src/storybook/layers/__image_snapshots__/subsurfaceviewer-map-layer--typed-array-input.png index b797f62116..00d62231fa 100644 Binary files a/typescript/packages/subsurface-viewer/src/storybook/layers/__image_snapshots__/subsurfaceviewer-map-layer--typed-array-input.png and b/typescript/packages/subsurface-viewer/src/storybook/layers/__image_snapshots__/subsurfaceviewer-map-layer--typed-array-input.png differ diff --git a/typescript/packages/subsurface-viewer/src/storybook/layers/__image_snapshots__/subsurfaceviewer-wells-layer--custom-colored-wells.png b/typescript/packages/subsurface-viewer/src/storybook/layers/__image_snapshots__/subsurfaceviewer-wells-layer--custom-colored-wells.png index 47661d661d..b15ca901e6 100644 Binary files a/typescript/packages/subsurface-viewer/src/storybook/layers/__image_snapshots__/subsurfaceviewer-wells-layer--custom-colored-wells.png and b/typescript/packages/subsurface-viewer/src/storybook/layers/__image_snapshots__/subsurfaceviewer-wells-layer--custom-colored-wells.png differ diff --git a/typescript/packages/subsurface-viewer/src/storybook/layers/__image_snapshots__/subsurfaceviewer-wells-layer--reduced-well-name-clutter-2-d.png b/typescript/packages/subsurface-viewer/src/storybook/layers/__image_snapshots__/subsurfaceviewer-wells-layer--reduced-well-name-clutter-2-d.png new file mode 100644 index 0000000000..9735ca264c Binary files /dev/null and b/typescript/packages/subsurface-viewer/src/storybook/layers/__image_snapshots__/subsurfaceviewer-wells-layer--reduced-well-name-clutter-2-d.png differ diff --git a/typescript/packages/subsurface-viewer/src/storybook/layers/__image_snapshots__/subsurfaceviewer-wells-layer--reduced-well-name-clutter-3-d.png b/typescript/packages/subsurface-viewer/src/storybook/layers/__image_snapshots__/subsurfaceviewer-wells-layer--reduced-well-name-clutter-3-d.png new file mode 100644 index 0000000000..e328c12108 Binary files /dev/null and b/typescript/packages/subsurface-viewer/src/storybook/layers/__image_snapshots__/subsurfaceviewer-wells-layer--reduced-well-name-clutter-3-d.png differ diff --git a/typescript/packages/subsurface-viewer/src/storybook/layers/__image_snapshots__/subsurfaceviewer-wells-layer--simplified-rendering.png b/typescript/packages/subsurface-viewer/src/storybook/layers/__image_snapshots__/subsurfaceviewer-wells-layer--simplified-rendering.png index e160a789e6..1509e815a3 100644 Binary files a/typescript/packages/subsurface-viewer/src/storybook/layers/__image_snapshots__/subsurfaceviewer-wells-layer--simplified-rendering.png and b/typescript/packages/subsurface-viewer/src/storybook/layers/__image_snapshots__/subsurfaceviewer-wells-layer--simplified-rendering.png differ