Unable to render ScatterplotLayer on selection of point in pointcloudlayer with react

91 views Asked by At

I'm currently working on a React component that uses DeckGL to display a point cloud. I've implemented a ScatterplotLayer for rendering selected points with a different color upon clicking. However, I'm encountering an issue where the ScatterplotLayer is not visible on the map even though the click handler seems to be working as expected. Please help me to understand what am I missing here.

I've implemented a ScatterplotLayer to render selected points with a red color upon clicking. The clickHandler function appears to be correctly updating the selected points and creating a new ScatterplotLayer with the selected points. However, the new layer is not visible on the map.

I've tried debugging and checked the DeckGL logs, which mention "Updated attributes for X instances in selected-point-markers in 0ms." It seems like the attributes are being updated, but the layer is not visible.

Code Snippet:

import { useEffect, useState } from "react";
import DeckGL from "@deck.gl/react/typed";
import { PointCloudLayer, ScatterplotLayer } from "@deck.gl/layers/typed";
import { COORDINATE_SYSTEM, FirstPersonView, FirstPersonController, PickingInfo, Position } from "@deck.gl/core/typed";
import { LASLoader } from "@loaders.gl/las";
import { useAppDispatch, useAppSelector } from "../../store/hooks";
import {
    getCurrentPanoramaIndex,
    getPanoramaHeading,
    getPanoramaImageMap,
    getPanoramaNameList,
    getPanoramaViewerRotation,
    setCurrentPanoramaLocalCoordinates,
    setPanoramaViewerRotation,
} from "../../store/reducers/panoramaSlice";
import { getCurrentTopdownMeta } from "../../store/reducers/topdownSlice";
import { deg2rad, extractNumber, rad2deg } from "../../services/common";
import { LocalCoordinates } from "../../store/reducers/type";
import "./PointcloudComponent.scss";
import { getQueryParams } from "../../services/queryParams";
import { getPointcloud } from "../../services/pointcloud";
import { getCurrentPointcloudUrl, setPointCoordinates } from "../../store/reducers/pointcloudSlice";
import { setPointcloudBusy } from "../../store/reducers/appSlice";
import store from "../../store/store";
import { deckGLStyles } from "./constant";
import { Panel } from "./components";
import { HDSIcon } from "@here/hds-react-components";

const INITIAL_VIEW_STATE = {
    longitude: 0,
    latitude: 0,
    position: [50, 50, 239.5],
    bearing: 0,
    pitch: 0,
    maxPitch: 0,
    minPitch: 0,
};

class MyFirstPersonController extends FirstPersonController {
    constructor(options = {}) {
        // TODO: remove this below ignore once the deck.gl updated version have type exposed for the class
        // @ts-ignore
        super(options);
    }
    handleEvent(event: any): boolean {
        if (event.type === "pan") {
            // Placeholder
        } else {
            super.handleEvent(event);
        }
        return true;
    }
}

interface CoordinateFactors {
    x: {
        lat: number;
        lng: number;
    };
    y: {
        lat: number;
        lng: number;
    };
}

const normalizeLng = (latIn: number): number => {
    const sign = Math.sign(latIn);
    latIn = Math.abs(latIn);
    if (latIn > 180) {
        latIn = 360 - latIn;
    }
    return sign * latIn;
};

const PointcloudComponent = ({ onLoad }: any): JSX.Element => {
    const [viewState, updateViewState] = useState(INITIAL_VIEW_STATE);
    const [showPanel, setShowPanel] = useState(false);
    const [layers, setLayers] = useState<any[]>([]);
    const [markerLayer, setMarkerLayer] = useState<ScatterplotLayer | null>(null);
    const [selectedPointsIndex, setSelectedPointsIndex] = useState<number[]>([]);
    const [coordFactors, setCoordFactors] = useState<CoordinateFactors>();
    const dispatch = useAppDispatch();
    const panoramaNameList = useAppSelector(getPanoramaNameList);
    const currentPanoramaIndex = useAppSelector(getCurrentPanoramaIndex);
    const panoramaPartId = extractNumber(panoramaNameList[currentPanoramaIndex]);
    const panoramaImageMap = useAppSelector(getPanoramaImageMap);
    const imageMap = panoramaImageMap[panoramaPartId];
    const currentPosition = [imageMap.xPc, imageMap.yPc, imageMap.zPc];
    const params = getQueryParams();
    const currentPointcloudUrl = useAppSelector(getCurrentPointcloudUrl);
    const currentHeading = useAppSelector(getPanoramaHeading);
    const viewerRotation = useAppSelector(getPanoramaViewerRotation);
    const currentTopdownMeta = useAppSelector(getCurrentTopdownMeta);
    const currentTopdownData = currentTopdownMeta?.data || undefined;

    const onDataLoad = (data: any) => {
        const { header } = data;
        console.log("on data load >>>>>>>", data);
        if (header.boundingBox) {
            // File contains bounding box info
            if (currentTopdownMeta && currentTopdownMeta.map) {
                if (currentTopdownMeta.map.source === "navvis") {
                    // Adjust position height as navvis panorama position is quite high
                    currentPosition[2] -= 0.6;
                }
            }
            updateViewState({
                ...INITIAL_VIEW_STATE,
                position: currentPosition,
                bearing: rad2deg(deg2rad(currentHeading) + viewerRotation),
            });
        }
        console.log("Pointcloud loaded, header", header);
        dispatch(setPointcloudBusy(false));

        if (onLoad) {
            onLoad({ count: header.vertexCount, progress: 1 });
        }
        if (currentTopdownData) {
            console.log("Topdowndata", currentTopdownData);
            const wgs84 = currentTopdownData!.cornerCoordsWGS84;
            const xDelta = Math.abs(currentTopdownData.xmin - currentTopdownData.xmax);
            const yDelta = Math.abs(currentTopdownData.ymin - currentTopdownData.ymax);
            const lngDeltaX = normalizeLng(wgs84.topRight.lng - wgs84.topLeft.lng);
            const lngDeltaY = normalizeLng(wgs84.bottomLeft.lng - wgs84.topLeft.lng);
            const xCoordScale = {
                lng: lngDeltaX / xDelta,
                lat: (wgs84.bottomLeft.lat - wgs84.bottomRight.lat) / xDelta,
            };
            const yCoordScale = { lng: lngDeltaY / yDelta, lat: (wgs84.topRight.lat - wgs84.bottomRight.lat) / yDelta };
            console.log("Coordscales", xCoordScale, yCoordScale);
            setCoordFactors({ x: xCoordScale, y: yCoordScale });
        }
        setSelectedPointsIndex([]);
    };

    const getScatterLayer = (positions: Position[]): ScatterplotLayer => {
        console.log("inside scatterplot >>>>>>>", positions);
        return new ScatterplotLayer({
            id: "selected-point-markers",
            data: positions,
            getPosition: (d: any) => d,
            // getPosition: (d: any) => positions[d.index],
            getFillColor: [255, 0, 0], // Red color for the markers
            radiusScale: 4,
            radiusMinPixels: 5,
            opacity: 1,
            updateTriggers: {
                getFillColor: selectedPointsIndex,
                getPosition: positions, // Re-render when the selected points change
            },
        });
    };

    const clickHandler = (object: any) => {
        console.log("Clicked object data", object);
        const index = object.index;
        const positions = layers[0]?.props.data.attributes.POSITION;
        // const selectedPosition = [
        //     positions.value[index * positions.size],
        //     positions.value[index * positions.size + 1],
        //     positions.value[index * positions.size + 2],
        // ] as Position;
        // Create a copy of the selectedPointsIndex array
        const updatedSelectedPoints = [...selectedPointsIndex];
        const selectedIndex = updatedSelectedPoints.indexOf(index);
        // setSelectedPointsIndex(index);
        if (selectedIndex !== -1) {
            // The point is already selected, so unselect it
            updatedSelectedPoints.splice(selectedIndex, 1);
        } else {
            // The point is not selected, so select it
            updatedSelectedPoints.push(index);
        }
        // viewState
        setSelectedPointsIndex(updatedSelectedPoints);
        if (object.layer && object.layer.props) {
            const oldCoordinates = store.getState().pointcloud.pointCoordinates;
            const position = object.layer.props.data.attributes.POSITION;
            const x = Number(position.value[index * position.size]);
            const y = Number(position.value[index * position.size + 1]);
            const z = Number(position.value[index * position.size + 2]);
            console.log("position:", x, y, z);
            if (oldCoordinates && JSON.stringify(oldCoordinates) === JSON.stringify([x, y, z])) {
                dispatch(setPointCoordinates(null));
            } else {
                dispatch(setPointCoordinates([x, y, z]));
            }
        }
        const newMarkerLayer = getScatterLayer(
            selectedPointsIndex.map(
                (idx) =>
                    [
                        positions.value[idx * positions.size],
                        positions.value[idx * positions.size + 1],
                        positions.value[idx * positions.size + 2],
                    ] as Position
            )
        );
        setMarkerLayer(newMarkerLayer);
    };

    // useEffect(() => {
    //     if (markerLayer) {
    //         // If the markerLayer is defined, set it as a new layer
    //         setLayers([...layers, markerLayer]);
    //     }
    // }, [markerLayer, selectedPointsIndex]);

    const getTooltipText = (object: any) => {
        const coordinates = store.getState().pointcloud.pointCoordinates;
        const index = object.index;
        let tooltipText = "";

        let x = "N/A";
        let y = "N/A";
        let z = "N/A";
        let x1 = 0;
        let y1 = 0;
        let z1 = 0;
        if (object.layer && object.layer.props) {
            const position = object.layer.props.data.attributes.POSITION;
            x = position.value[index * position.size];
            y = position.value[index * position.size + 1];
            z = position.value[index * position.size + 2];

            if (x && y && z) {
                x = Number(x).toFixed(2);
                y = Number(y).toFixed(2);
                z = Number(z).toFixed(2);
            }
            x1 = Number(x);
            y1 = Number(y);
            z1 = Number(z);
        }
        if (coordinates) {
            const x2 = coordinates[0];
            const y2 = coordinates[1];
            const z2 = coordinates[2];
            const distance = Math.sqrt((x2 - x1) ** 2 + (y2 - y1) ** 2 + (z2 - z1) ** 2);
            const height = Math.abs(z2 - z1);
            const width = Math.sqrt((x2 - x1) ** 2 + (y2 - y1) ** 2);
            tooltipText =
                "Distance:\n" +
                distance.toFixed(2) +
                " meters\n" +
                "Width:\n" +
                width.toFixed(2) +
                " meters\n" +
                "Height:\n" +
                height.toFixed(2) +
                " meters";
        } else {
            let lat = 0;
            let lng = 0;
            if (coordFactors && currentTopdownData) {
                const wgs84 = currentTopdownData!.cornerCoordsWGS84;
                lat = x1 * coordFactors.x.lat + y1 * coordFactors.y.lat + wgs84.bottomLeft.lat;
                lng = x1 * coordFactors.x.lng + y1 * coordFactors.y.lng + wgs84.bottomLeft.lng;
            }
            tooltipText =
                "X: " + x + "\nY: " + y + "\nZ: " + z + "\nlat: " + lat.toFixed(7) + "\nlng: " + lng.toFixed(7);
        }
        return tooltipText;
    };
    const layerUpdateTriggers = {
        getFillColor: selectedPointsIndex.length > 0, // This triggers a re-render when the selected point changes
    };

    const getColor = (d: any) => {
        console.log("inside getColor >>>>>>>>", d);
        if (selectedPointsIndex.includes(d.index)) {
            return [255, 0, 0]; // Red color for the selected point
        }
        return d.color;
    };

    useEffect(() => {
        if (currentPointcloudUrl !== "") {
            setLayers([
                new PointCloudLayer({
                    id: "point-cloud-layer",
                    data: currentPointcloudUrl,
                    onDataLoad,
                    visible: !!currentPointcloudUrl,
                    coordinateSystem: COORDINATE_SYSTEM.METER_OFFSETS,
                    getNormal: [0, 1, 0],
                    // getFillColor: (d: any) => {
                    //     console.log("d in getFillColor >>>>>>>", d);
                    //     return [255, 255, 0];
                    // },
                    // getIcon: (d: any) => "marker",
                    sizeScale: 15,
                    // getPosition: d => d.coordinates,
                    getFillColor: getColor,
                    rotationX: 0,
                    autoHighlight: true,
                    transitionDuration: 2400,
                    getPosition: (d: any) => d.position,
                    opacity: 1,
                    pointSize: 0.2,
                    sizeUnits: "meters",
                    getLineColor: [255, 255, 255],
                    pickable: true,
                    loaders: [LASLoader],
                    loadOptions: {
                        las: {
                            colorDepth: 16,
                        },
                    },
                    onClick: clickHandler,
                    updateTriggers: layerUpdateTriggers,
                }),
                markerLayer,
            ]);
        }
    }, [currentPointcloudUrl, JSON.stringify(selectedPointsIndex)]);

    useEffect(() => {
        console.log("Fetch pointcloud tile");
        dispatch(setPointcloudBusy(true));
        dispatch(getPointcloud(params, panoramaPartId.toString()));
    }, [dispatch]);

    const renderTooltip = (info: PickingInfo): any => {
        if (info && info.index !== -1) {
            return {
                text: getTooltipText(info),
                style: { ...deckGLStyles.pointToolTipStyles },
            };
        }
        return null; // Hide the tooltip when object.index is -1
    };
    console.log("render inside pointcloud >>>>>>>>>", selectedPointsIndex, layers, JSON.stringify(selectedPointsIndex));
    return (
        <div className="pointcloudComponent">
            <div className="panelContainer">
                <HDSIcon
                    name="show-menu"
                    category="core-ui"
                    size="16"
                    className="menuIcon"
                    onClick={() => setShowPanel(!showPanel)}
                />
                {showPanel && <Panel />}
            </div>
            <DeckGL
                views={[new FirstPersonView({ focalDistance: 100, fovy: 80, controller: MyFirstPersonController })]}
                viewState={viewState}
                onViewStateChange={(v) => {
                    updateViewState(v.viewState as typeof INITIAL_VIEW_STATE);
                    if (JSON.stringify(v.oldViewState?.position) !== JSON.stringify(v.viewState.position)) {
                        dispatch(
                            setCurrentPanoramaLocalCoordinates({
                                xPc: v.viewState.position[0],
                                yPc: v.viewState.position[1],
                                zPc: v.viewState.position[2],
                            } as LocalCoordinates)
                        );
                    }
                    if (v.viewState.bearing !== v.oldViewState?.bearing) {
                        // Convert point cloud bearing to panorama viewer rotation
                        dispatch(setPanoramaViewerRotation(deg2rad(v.viewState.bearing) - deg2rad(currentHeading)));
                    }
                }}
                controller={{ keyboard: { moveSpeed: 1 }, doubleClickZoom: false }}
                style={{ ...deckGLStyles.component }}
                layers={layers}
                parameters={{
                    clearColor: [0.93, 0.86, 0.81, 1],
                }}
                pickingRadius={10}
                getTooltip={renderTooltip}
                getCursor={(state): any => {
                    return state.isDragging ? "grabbing" : "pointer";
                }}
            />
        </div>
    );
};

export default PointcloudComponent;
1

There are 1 answers

0
RAVI singh On

My Pointcloudlayer was having coordinateSystem: COORDINATE_SYSTEM.METER_OFFSETS. I passed the same coordinateSystem to scatteredlayer and it worked as expected.