You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

react-map-gl/maplibre+deck.gl加载LiDAR点云图层报错排查

问题:React全局地图加载LiDAR点云时出现"Invalid LngLat object: (Nan, Nan)"错误

背景

正在开发一款基于React的3D点云全局地图应用,初期阶段正在熟悉deck.gl、react-map-gl和Maplibre的交互逻辑。已通过loaders.gl的LASWorkerLoader成功解析用户上传的LiDAR测试数据,且数据已投影至OGC:WGS84坐标系(适配MapView展示),但加载数据集时丢失WebGL上下文,触发**"Invalid LngLat object: (Nan, Nan)"**错误,无法正常加载图层。

已做验证

已尝试验证数据解析结果及格式兼容性,解析数据日志符合loaders.gl输出规范。

代码示例

import 'maplibre-gl/dist/maplibre-gl.css';
import React, {useState, useEffect, useMemo} from 'react';
import {Map, NavigationControl, Popup, useControl, FullscreenControl} from 'react-map-gl/maplibre';
import {GeoJsonLayer, ArcLayer, PointCloudLayer, MapView, OrthographicView} from 'deck.gl';
import { COORDINATE_SYSTEM } from 'deck.gl';
import {MapboxOverlay as DeckOverlay} from '@deck.gl/mapbox';
import ControlPanel, {COLOURS} from './ControlPanel';

import {LASWorkerLoader} from '@loaders.gl/las'
import { load } from '@loaders.gl/core';


// source: Natural Earth
const AIR_PORTS =
  'https://d2ad6b4ur7yvpq.cloudfront.net/naturalearth-3.3.0/ne_10m_airports.geojson';

const INITIAL_VIEW_STATE = {
  longitude: 0,
  latitude: 0,
  zoom: 2,
  pitch: 0,
  bearing: 0
};

const MAP_STYLE = 'https://basemaps.cartocdn.com/gl/positron-gl-style/style.json';

function DeckGLOverlay(props) {
  const overlay = useControl(() => new DeckOverlay(props));
  overlay.setProps(props);
  return null;
}

function MapOverlay() {
  const [lasData, setLasData] = useState(null);
  const [viewState, setViewState] = useState(INITIAL_VIEW_STATE);
  const [error, setError] = useState(null);

  const handleFileChange = async (event) => {
    const file = event.target.files[0];
    if (!file) return;

    const reader = new FileReader();

    reader.onload = async () => {
      try {
        const arrayBuffer = reader.result;
        const parsedData = await load(arrayBuffer, LASWorkerLoader);
        console.log('Parsed LAS data:', parsedData);
        setLasData(parsedData);
        
        if (parsedData.header && parsedData.header.boundingBox) {
          const [minX, minY, , maxX, maxY] = parsedData.header.boundingBox;
          const centerX = (minX + maxX) / 2;
          const centerY = (minY + maxY) / 2;
          setViewState({
            ...INITIAL_VIEW_STATE,
            longitude: centerX,
            latitude: centerY,
          });
        }
      } catch (error) {
        console.error('Error loading LAS data:', error);
        setError('Failed to load LAS data. Please try a different file.');
      }
    };

    reader.readAsArrayBuffer(file);
  };

  const layers = useMemo(() => {
    if (!lasData) return [];

    return [
      new PointCloudLayer({
        id: 'lidar-point-cloud',
        data: lasData.attributes.POSITION.value,
        coordinateSystem: COORDINATE_SYSTEM.CARTESIAN,
        getPosition: d => [d[0], d[1], d[2]],
        getColor: d => {
          const intensity = lasData.attributes.intensity ? 
            lasData.attributes.intensity.value[Math.floor(d[3])] : 255;
          return [intensity, intensity, intensity, 255];
        },
        pointSize: 1,
        opacity: 0.5,
        pickable: false,
        onHover: info => console.log('Hovered:', info),
        coordinateOrigin: lasData.header ? [
          (lasData.header.boundingBox[0] + lasData.header.boundingBox[3]) / 2,
          (lasData.header.boundingBox[1] + lasData.header.boundingBox[4]) / 2,
          (lasData.header.boundingBox[2] + lasData.header.boundingBox[5]) / 2
        ] : [0, 0, 0]
      })
    ];
  }, [lasData]);

  if (error) {
    return <div className="error-message">{error}</div>;
  }

  return (
    <>
      <div>
        <input type="file" accept=".laz,.las" onChange={handleFileChange} />
      </div>
      <div className='map-container'>
        <Map 
          {...viewState} 
          onMove={evt => setViewState(evt.viewState)}
          mapStyle={MAP_STYLE}
        >
          <DeckGLOverlay layers={layers} />
          <NavigationControl position="top-right" />
          <FullscreenControl position='top-right'/>
        </Map>
        <ControlPanel/>
      </div>
    </>
  );
}

export default MapOverlay;

目标数据

使用的是USGS开源LAZ数据,为亚利桑那州科罗拉多河下游区域2018年LiDAR数据集。

目前不清楚如何正确访问解析后的数据,也不确定是否需要进行额外的坐标转换或数据处理。


解决方案

1. 修正坐标系统匹配问题

当前设置coordinateSystem: COORDINATE_SYSTEM.CARTESIAN,但MapView默认使用WGS84地理坐标系(经纬度)。USGS的LAZ数据通常采用UTM投影坐标系,直接用笛卡尔坐标系会导致坐标超出地图范围,触发NaN错误。

  • 将coordinateSystem改为COORDINATE_SYSTEM.LNGLAT,适配地图的经纬度坐标系。
  • 必须将UTM投影坐标转换为WGS84经纬度,可使用proj4库实现转换。

2. 修复数据访问逻辑

解析后的LAS数据POSITION是一维FlatArray,当前的getPosition和getColor存在索引访问错误:

  • getPosition需按3个一组取点,因为一维数组中每3个元素对应一个点的X/Y/Z:
getPosition: (_, index) => [
  lasData.attributes.POSITION.value[index * 3],
  lasData.attributes.POSITION.value[index * 3 + 1],
  lasData.attributes.POSITION.value[index * 3 + 2]
],
  • 强度值需通过索引直接访问,而非d[3](当前data是一维数组,每个元素是单个数值):
getColor: (_, index) => {
  const intensity = lasData.attributes.intensity?.value[index] || 255;
  return [intensity, intensity, intensity, 255];
},

3. 修正视图中心计算

原始数据的boundingBox是投影坐标,直接赋值给longitude/latitude会导致无效坐标,需先转换为经纬度:

// 安装proj4:npm install proj4
import proj4 from 'proj4';

// 在handleFileChange中添加转换逻辑
if (parsedData.header && parsedData.header.boundingBox) {
  const [minX, minY, , maxX, maxY] = parsedData.header.boundingBox;
  // 定义UTM 12N(对应目标数据区域)转WGS84的规则
  proj4.defs('EPSG:32612', '+proj=utm +zone=12 +datum=WGS84 +units=m +no_defs');
  // 转换中心点坐标
  const [centerLon, centerLat] = proj4('EPSG:32612', 'EPSG:4326').forward([(minX+maxX)/2, (minY+maxY)/2]);
  setViewState({
    ...INITIAL_VIEW_STATE,
    longitude: centerLon,
    latitude: centerLat,
    zoom: 14 // 调整到合适的缩放级别
  });
}

4. 避免WebGL上下文丢失

添加updateTriggers控制图层更新时机,减少不必要的WebGL资源重建:

new PointCloudLayer({
  // ...其他属性
  updateTriggers: {
    getPosition: [lasData.attributes.POSITION.value],
    getColor: [lasData.attributes.intensity?.value]
  }
})

完整修改后的核心代码片段

// 引入proj4
import proj4 from 'proj4';

// handleFileChange中的坐标转换
const handleFileChange = async (event) => {
  const file = event.target.files[0];
  if (!file) return;

  const reader = new FileReader();

  reader.onload = async () => {
    try {
      const arrayBuffer = reader.result;
      const parsedData = await load(arrayBuffer, LASWorkerLoader);
      console.log('Parsed LAS data:', parsedData);
      setLasData(parsedData);
      
      if (parsedData.header && parsedData.header.boundingBox) {
        const [minX, minY, , maxX, maxY] = parsedData.header.boundingBox;
        proj4.defs('EPSG:32612', '+proj=utm +zone=12 +datum=WGS84 +units=m +no_defs');
        const [centerLon, centerLat] = proj4('EPSG:32612', 'EPSG:4326').forward([(minX+maxX)/2, (minY+maxY)/2]);
        setViewState({
          ...INITIAL_VIEW_STATE,
          longitude: centerLon,
          latitude: centerLat,
          zoom: 14
        });
      }
    } catch (error) {
      console.error('Error loading LAS data:', error);
      setError('Failed to load LAS data. Please try a different file.');
    }
  };

  reader.readAsArrayBuffer(file);
};

// layers中的PointCloudLayer配置
const layers = useMemo(() => {
  if (!lasData) return [];

  return [
    new PointCloudLayer({
      id: 'lidar-point-cloud',
      data: lasData.attributes.POSITION.value,
      coordinateSystem: COORDINATE_SYSTEM.LNGLAT,
      getPosition: (_, index) => [
        lasData.attributes.POSITION.value[index * 3],
        lasData.attributes.POSITION.value[index * 3 + 1],
        lasData.attributes.POSITION.value[index * 3 + 2]
      ],
      getColor: (_, index) => {
        const intensity = lasData.attributes.intensity?.value[index] || 255;
        return [intensity, intensity, intensity, 255];
      },
      pointSize: 1,
      opacity: 0.5,
      pickable: false,
      updateTriggers: {
        getPosition: [lasData.attributes.POSITION.value],
        getColor: [lasData.attributes.intensity?.value]
      }
    })
  ];
}, [lasData]);

内容的提问来源于stack exchange,提问作者Luke Booysen

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.06.18 13:29:52