diff --git a/frontend/src/modules/2DViewer/view/customDeckGlLayers/AdvancedWellsLayer.ts b/frontend/src/modules/2DViewer/view/customDeckGlLayers/AdvancedWellsLayer.ts index 112c3363b..968bcb821 100644 --- a/frontend/src/modules/2DViewer/view/customDeckGlLayers/AdvancedWellsLayer.ts +++ b/frontend/src/modules/2DViewer/view/customDeckGlLayers/AdvancedWellsLayer.ts @@ -1,22 +1,49 @@ -import { FilterContext, Layer, LayersList } from "@deck.gl/core"; -import { GeoJsonLayer } from "@deck.gl/layers"; +import { FilterContext, Layer, LayersList, UpdateParameters } from "@deck.gl/core"; +import { CollisionFilterExtension } from "@deck.gl/extensions"; +import { GeoJsonLayer, TextLayer } from "@deck.gl/layers"; import { WellsLayer } from "@webviz/subsurface-viewer/dist/layers"; +import { FeatureCollection, GeometryCollection } from "geojson"; + export class AdvancedWellsLayer extends WellsLayer { static layerName: string = "WellsLayer"; + // @ts-ignore + state!: { + labelCoords: WellboreLabelCoords[]; + }; + constructor(props: any) { super(props); } filterSubLayer(context: FilterContext): boolean { if (context.layer.id.includes("names")) { - return context.viewport.zoom > -2; + return context.viewport.zoom > -3; } return true; } + updateState(params: UpdateParameters): void { + super.updateState(params); + + if (!params.changeFlags.dataChanged) { + return; + } + + const labelCoords = precalculateLabelPositions(params.props.data as FeatureCollection); + this.setState({ labelCoords }); + } + + initializeState(): void { + super.initializeState(); + + this.setState({ + labelCoords: precalculateLabelPositions(this.props.data as FeatureCollection), + }); + } + renderLayers(): LayersList { const layers = super.renderLayers(); @@ -32,10 +59,22 @@ export class AdvancedWellsLayer extends WellsLayer { return layer.id.includes("colors"); }); + const textLayer = layers.find((layer) => { + if (!(layer instanceof TextLayer)) { + return false; + } + + return layer.id.includes("names"); + }); + if (!(colorsLayer instanceof GeoJsonLayer)) { return layers; } + if (!(textLayer instanceof TextLayer)) { + return layers; + } + const newColorsLayer = new GeoJsonLayer({ data: colorsLayer.props.data, pickable: true, @@ -62,6 +101,115 @@ export class AdvancedWellsLayer extends WellsLayer { onHover: () => {}, }); - return [newColorsLayer, ...layers.filter((layer) => layer !== colorsLayer)]; + const newTextLayer = new TextLayer({ + id: "names", + data: this.state.labelCoords, + getColor: [0, 0, 0], + getBackgroundColor: [255, 255, 255], + getBorderColor: [0, 173, 230], + getBorderWidth: 1, + getPosition: (d: WellboreLabelCoords) => d.coords, + getText: (d: WellboreLabelCoords) => d.name, + getSize: 16, + getAngle: (d: WellboreLabelCoords) => d.angle, + billboard: false, + background: true, + backgroundPadding: [4, 1], + fontFamily: "monospace", + collisionEnabled: true, + sizeUnits: "meters", + sizeMaxPixels: 20, + sizeMinPixels: 8, + extensions: [new CollisionFilterExtension()], + }); + + return [ + newColorsLayer, + ...layers.filter((layer) => layer !== colorsLayer && layer !== textLayer), + newTextLayer, + ]; } } + +type WellboreLabelCoords = { + wellboreUuid: string; + name: string; + coords: [number, number, number]; + angle: number; +}; + +function precalculateLabelPositions(data: FeatureCollection, minDistance: number = 1000): WellboreLabelCoords[] { + const labelCoords: WellboreLabelCoords[] = []; + + for (const feature of data.features) { + const name = feature.properties?.name; + const uuid = feature.properties?.uuid; + if (!uuid) { + continue; + } + + const collection = feature.geometry as GeometryCollection; + if (!collection.geometries) { + continue; + } + + for (const geometry of collection.geometries) { + if (geometry.type !== "LineString") { + continue; + } + + const coords = geometry.coordinates as [number, number, number][]; + if (coords.length < 2) { + continue; + } + + let lastCoordinates = coords[0]; + for (let i = 1; i < coords.length - 2; i++) { + const distance = Math.sqrt( + (coords[i][0] - lastCoordinates[0]) ** 2 + (coords[i][1] - lastCoordinates[1]) ** 2 + ); + + if (distance < minDistance) { + continue; + } + + if ( + labelCoords.some( + (label) => + label.coords[0] - minDistance / 5 <= coords[i][0] && + label.coords[0] + minDistance / 5 >= coords[i][0] && + label.coords[1] - minDistance / 5 <= coords[i][1] && + label.coords[1] + minDistance / 5 >= coords[i][1] + ) + ) { + continue; + } + + const current = coords[i]; + const prev = coords[i - 1]; + const next = coords[i + 1]; + + let angle = Math.atan2(prev[1] - next[1], prev[0] - next[0]) * (180 / Math.PI); + + if (angle < -90) { + angle += 180; + } + + if (angle > 90) { + angle -= 180; + } + + labelCoords.push({ + name, + wellboreUuid: uuid, + coords: [current[0], current[1], 0], + angle: angle, + }); + + lastCoordinates = coords[i]; + } + } + } + + return labelCoords; +} diff --git a/frontend/src/modules/2DViewer/view/customDeckGlLayers/LabelLayer.ts b/frontend/src/modules/2DViewer/view/customDeckGlLayers/LabelLayer.ts index 58716ac14..c4e8dacab 100644 --- a/frontend/src/modules/2DViewer/view/customDeckGlLayers/LabelLayer.ts +++ b/frontend/src/modules/2DViewer/view/customDeckGlLayers/LabelLayer.ts @@ -11,6 +11,8 @@ import { GeoJsonLayer, LineLayer, TextLayer } from "@deck.gl/layers"; import { Entity, ForceDirectedEntityPositioning } from "@lib/utils/ForceDirectedEntityPositioning"; import { EntityGroup, ProximityGrouping } from "@lib/utils/ProximityGrouping"; +import { Feature } from "geojson"; + type LabelData = { coordinates: [number, number, number]; name: string; @@ -36,11 +38,6 @@ export type LabelLayerProps = { sizeMaxPixels: number; }; -type BoundingBox2D = { - topLeft: number[]; - bottomRight: number[]; -}; - export type LabelPickingInfo = PickingInfo & { additionalText?: string; }; @@ -194,10 +191,32 @@ export class LabelLayer extends CompositeLayer { const { adjustedData } = this.state; const info = super.getPickingInfo(params) as LabelPickingInfo; const { index, sourceLayer } = info; - if (index >= 0 && sourceLayer) { + + console.debug(info); + + if (index < 0) { + return info; + } + + const reg = /(text|points)-zoom-([-\d\\.]+)/; + const match = info.sourceLayer?.id.match(reg); + + if (match) { + const zoomLevel = parseFloat(match[2]); + const labelGroup = this.state.labelGroups.get(zoomLevel); + if (!labelGroup) { + return info; + } + info.object.name = this.reduceNames([ + labelGroup[info.index].name, + ...labelGroup[info.index].entities.map((e) => e.name), + ]); + return info; + } + + if (sourceLayer) { const otherNames = adjustedData[index].otherNames; info.object.name = this.reduceNames([adjustedData[index].name, ...otherNames]); - console.debug(info.object.name); } return info; } @@ -205,12 +224,33 @@ export class LabelLayer extends CompositeLayer { onHover(info: LabelPickingInfo): boolean { const { adjustedData, hoveredId } = this.state; let newHoveredId: string | null; - if (info.index >= 0) { - newHoveredId = this.makeId(adjustedData[info.index]); - } else { + if (info.index < 0) { + if (hoveredId === null) { + return false; + } + newHoveredId = null; + this.setState({ ...this.state, hoveredId: newHoveredId }); + return false; + } + + const reg = /(text|points)-zoom-([-\d\\.]+)/; + const match = info.sourceLayer?.id.match(reg); + + if (match) { + const zoomLevel = parseFloat(match[2]); + const labelGroup = this.state.labelGroups.get(zoomLevel); + if (!labelGroup) { + return false; + } + newHoveredId = this.makeZoomLevelGroupId(zoomLevel, labelGroup[info.index]); + if (newHoveredId !== hoveredId) { + this.setState({ ...this.state, hoveredId: newHoveredId }); + } + return false; } + newHoveredId = this.makeId(adjustedData[info.index]); if (newHoveredId !== hoveredId) { this.setState({ ...this.state, hoveredId: newHoveredId }); } @@ -222,6 +262,10 @@ export class LabelLayer extends CompositeLayer { return `${labelData.name}-${labelData.coordinates.join(",")}`; } + private makeZoomLevelGroupId(zoomLevel: number, group: EntityGroup): string { + return `zoom-${zoomLevel}-${group.coordinates.join(",")}`; + } + private reduceNames(names: string[], maxNum: number = 5): string { const ellipsis = names.length > maxNum ? `\n... + ${names.length - maxNum} more` : ""; const newNames = names.slice(0, Math.min(maxNum, names.length)); @@ -239,7 +283,7 @@ export class LabelLayer extends CompositeLayer { const featureCollection = { type: "FeatureCollection", features: labelGroup.map((d) => ({ - id: d.coordinates.join(","), + id: this.makeZoomLevelGroupId(zoomLevel, d), type: "Feature", geometry: { type: "Point", @@ -256,10 +300,21 @@ export class LabelLayer extends CompositeLayer { id: `points-zoom-${zoomLevel}`, data: featureCollection, getRadius: 100 / 2 ** zoomLevel, - getFillColor: [155, 155, 155, 30], + getFillColor: (d: Feature) => { + if (hoveredId && d.id === hoveredId) { + return [20, 20, 255, 30]; + } + return [255, 255, 255, 30]; + }, stroked: false, pickable: true, pointRadiusUnits: "meters", + parameters: { + depthMask: false, + }, + updateTriggers: { + getFillColor: [hoveredId], + }, }) ) ); @@ -267,7 +322,7 @@ export class LabelLayer extends CompositeLayer { new TextLayer( this.getSubLayerProps({ id: `text-zoom-${zoomLevel}`, - data: labelGroups, + data: labelGroup, getPosition: (d: ExtendedLabelData) => d.coordinates, getText: (d: ExtendedLabelData) => `${d.name}`, getSize: 16,