Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
97 changes: 87 additions & 10 deletions typescript/packages/subsurface-viewer/src/components/Map.tsx
Original file line number Diff line number Diff line change
@@ -1,4 +1,10 @@
import React, { useCallback, useEffect, useMemo, useState } from "react";
import React, {
useCallback,
useEffect,
useMemo,
useRef,
useState,
} from "react";

import type { Feature, FeatureCollection } from "geojson";
import { cloneDeep, isEmpty, isEqual } from "lodash";
Expand Down Expand Up @@ -148,12 +154,43 @@ function parseLights(lights?: LightsType): LightingEffect[] | undefined {
return effects;
}

type BoundingBoxMap = Record<string, BoundingBox3D>;

enum BoundingBoxActionType {
UPDATE = "update",
REMOVE = "remove",
}

type BoundingBoxAction =
| ({
type: BoundingBoxActionType.UPDATE;
layerId: string;
} & ReportBoundingBoxAction)
| {
type: BoundingBoxActionType.REMOVE;
layerIds: string[];
};

function mapBoundingBoxReducer(
mapBoundingBox: BoundingBox3D | undefined,
action: ReportBoundingBoxAction
): BoundingBox3D | undefined {
const union = boxUnion(mapBoundingBox, action.layerBoundingBox);
return isEqual(union, mapBoundingBox) ? mapBoundingBox : union;
state: BoundingBoxMap,
action: BoundingBoxAction
): BoundingBoxMap {
if (action.type === BoundingBoxActionType.UPDATE) {
const { layerId, layerBoundingBox } = action;
const current = state[layerId];
if (isEqual(current, layerBoundingBox)) return state;
return {
...state,
[layerId]: layerBoundingBox,
};
} else if (action.type === BoundingBoxActionType.REMOVE) {
const newState = { ...state };
for (const id of action.layerIds) {
delete newState[id];
}
return newState;
}
return state;
}

export type TooltipCallback = (
Expand Down Expand Up @@ -478,12 +515,21 @@ const Map: React.FC<MapProps> = ({
}
}, []);

// 3d bounding box computed from the layers
const [dataBoundingBox3d, dispatchBoundingBox] = React.useReducer(
// 3d bounding boxes stored by layer.
const [boundingBoxesByLayer, dispatchBoundingBox] = React.useReducer(
mapBoundingBoxReducer,
undefined
{}
);

// 3d bounding box computed from the layers
const dataBoundingBox3d = useMemo(() => {
const allBoxes = Object.values(boundingBoxesByLayer);
return allBoxes.reduce(
(acc, box) => boxUnion(acc, box),
undefined as BoundingBox3D | undefined
);
}, [boundingBoxesByLayer]);

// Get vertical scaling factor defined in viewports.
const viewportVerticalScale = useVerticalScale(views?.viewports);

Expand Down Expand Up @@ -716,14 +762,45 @@ const Map: React.FC<MapProps> = ({
// Inject "dispatchBoundingBox" function into layer for it to report back its respective bounding box.
// eslint-disable-next-line
// @ts-ignore
reportBoundingBox: dispatchBoundingBox,
reportBoundingBox: (action) => {
dispatchBoundingBox({
type: BoundingBoxActionType.UPDATE,
layerId: (item as Layer).id,
layerBoundingBox: action.layerBoundingBox,
});
},

// Modify "modelMatrix" to reflect correct z scaling.
modelMatrix: m,
});
});
}, [layers, zScale]);

const removeTimeoutRef = useRef<ReturnType<typeof setTimeout> | null>(null);

useEffect(() => {
if (!layers) return;

if (removeTimeoutRef.current) {
clearTimeout(removeTimeoutRef.current);
}

removeTimeoutRef.current = setTimeout(() => {
const currentIds = new Set(layers?.map((l) => (l as Layer).id));
const existingIds = new Set(Object.keys(boundingBoxesByLayer));

const removedIds = [...existingIds].filter(
(id) => !currentIds.has(id)
);
if (removedIds.length > 0) {
dispatchBoundingBox({
type: BoundingBoxActionType.REMOVE,
layerIds: removedIds,
});
}
}, 200);
}, [layers, boundingBoxesByLayer]);

const [loadingProgress, setLoadingProgress] = useState<number>(0);
const onAfterRender = useCallback(() => {
if (deckGLLayers) {
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,124 @@
import React from "react";
import { CompositeLayer } from "@deck.gl/core";
import type { BoundingBox3D } from "../../utils";
import type { Meta } from "@storybook/react";
import SubsurfaceViewer from "../../SubsurfaceViewer";
import { ScatterplotLayer } from "@deck.gl/layers";

export default {
component: SubsurfaceViewer,
title: "SubsurfaceViewer/Examples/BoundingBoxUpdate",
} as Meta;

type Props = {
id: string;
data: { position: [number, number, number] }[];
color?: [number, number, number];
reportBoundingBox?: (action: { layerBoundingBox: BoundingBox3D }) => void;
};

export class BoundingBoxReportingScatterplotLayer extends CompositeLayer<Props> {
static layerName = "BoundingBoxReportingScatterplotLayer";

initializeState() {
this.report();
}

updateState() {
this.report();
}

report() {
const { data, reportBoundingBox } = this.props;
if (!reportBoundingBox || !data.length) return;

const xs = data.map((d) => d.position[0]);
const ys = data.map((d) => d.position[1]);
const zs = data.map((d) => d.position[2] ?? 0);

const radius = 10; // assuming getRadius always returns 10 meters

const minX = Math.min(...xs) - radius;
const maxX = Math.max(...xs) + radius;
const minY = Math.min(...ys) - radius;
const maxY = Math.max(...ys) + radius;
const minZ = Math.min(...zs) - radius;
const maxZ = Math.max(...zs) + radius;

reportBoundingBox({
layerBoundingBox: [minX, minY, minZ, maxX, maxY, maxZ],
});
}

renderLayers() {
return new ScatterplotLayer({
id: `${this.props.id}-scatter`,
data: this.props.data,
getPosition: (d) => d.position,
getRadius: 10,
radiusUnits: "meters",
getFillColor: this.props.color ?? [255, 100, 100],
});
}
}

export const BoundingBoxAdjustsOnAddRemoveLayers = () => {
const [triggerHome, setTriggerHome] = React.useState(0);
const [layers, setLayers] = React.useState(() => [
new BoundingBoxReportingScatterplotLayer({
id: "layer-a",
data: [{ position: [0, 0, 0] }],
}),
]);

const toggleLayer = () => {
setLayers((prev) => {
const hasB = prev.some((l) => l.id === "layer-b");
if (hasB) {
return prev.filter((l) => l.id !== "layer-b");
} else {
return [
...prev,
new BoundingBoxReportingScatterplotLayer({
id: "layer-b",
data: [{ position: [250, 250, 10] }],
color: [100, 255, 100],
}),
];
}
});
};

return (
<div style={{ position: "relative", width: "100%", height: "500px" }}>
<div
style={{
position: "absolute",
top: 10,
right: 10,
zIndex: 100,
}}
>
<button onClick={toggleLayer}>Toggle Layer B</button>
<button onClick={() => setTriggerHome((x) => x + 1)}>
Trigger Home
</button>
</div>
<SubsurfaceViewer
id="bbox-toggle-map"
layers={layers}
triggerHome={triggerHome}
views={{
layout: [1, 1],
viewports: [
{
id: "main",
show3D: true,
layerIds: layers.map((l) => l.id),
},
],
}}
/>
</div>
);
};
Loading