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

使用Super Cluster在DeckGL中实现地图聚类功能失败

DeckGL + Super Cluster 聚类功能报错排查与解决

我基于DeckGL与Super Cluster实现了地图聚类功能,用到了IconLayer、GeoJsonLayer、ScatterplotLayer及TextLayer四种图层。addAssetMonitoringGeoJsonLayer()的数据已传递给addAssetMonitoringIconsLayer(),但renderLayers()无法获取到该数据,控制台抛出以下错误:

deck: update of CompositeLayer({id: 'icon-cluster'}): Cannot read properties of undefined (reading 'getClusters')

deck: update of CompositeLayer({id: 'icon-cluster'}): undefined is not iterable (cannot read property Symbol(Symbol.iterator))

相关实现代码如下:

addAssetMonitoringIconsLayer () {

        const getIconSize = (size) => {
            return Math.min(100, size) / 100 + 1;
        }

        class IconClusterLayer extends CompositeLayer {
            shouldUpdateState({changeFlags}) {
                return changeFlags.somethingChanged;
            }

            updateState({props, oldProps, changeFlags}) {
                const rebuildIndex = changeFlags.dataChanged || props.sizeScale !== oldProps.sizeScale;

                if (rebuildIndex) {
                    const index = new Supercluster({maxZoom: 16, radius: props.sizeScale});

                    index.load(
                        props.data.map(d => ({
                            geometry: {coordinates: props.getPosition(d)},
                            properties: d,
                        })
                    ));
                    this.setState({index});
                }

                const z = Math.floor(this.context.viewport.zoom);
                if (rebuildIndex || z !== this.state.z) {
                    this.setState({
                        data: this.state.index.getClusters([-180, -85, 180, 85], z),
                        z,
                    });
                }
            }

            renderLayers() {
                const {data} = this.state;

                return [

                    // No clustering -> render icons from the API -> onHover: show the asset name -> on Click: show the asset details
                    new IconLayer(
                        this.getSubLayerProps({
                            id: 'icon',
                            data,
                            getPosition: d => d.geometry.coordinates,
                            getIcon: d => d.properties.cluster ? '' : {url: (BASE_URL).substring(0, BASE_URL.length) + d.properties.icon_url, width: 38, height: 60, anchorY: 60},
                            transitions: {getPosition: { duration: 100 }},
                            onHover: (info, d) => {
                                if (!d.properties.cluster && (info.coordinate && info.object)){
                                    const marker = new window.google.maps.Marker({
                                        position: { lat: info.coordinate[1], lng: info.coordinate[0] },
                                        map: this.map.current.context[MAP]
                                    });
                                    marker.setOptions({ 'opacity': 0.0 });
                                    marker.setOptions({ 'title': `${info.object.asset_model__name}` });
                                        
                                    marker.addListener("click", () => {
                                        this.showMarkerInfo(info.object);
                                    });
                                }
                            },
                            onClick: (info, d) => {
                                if (!d.properties.cluster){
                                    this.showMarkerInfo(info.object);
                                }
                            },
                        })
                    ),

                    // when there is clustering show this and change it's size on zoom change -> onHover -> show total number of assets in the cluster
                    new ScatterplotLayer(
                        this.getSubLayerProps({
                            id: 'scatter-plot-cluster-layer',
                            data,
                            getPosition: d => d.geometry.coordinates,
                            getRadius: d => getIconSize(d.properties.cluster ? (d.properties.point_count * 200) : 0),
                            getFillColor: [255, 0, 0],
                            opacity: 0.5,
                            radiusScale: 6,
                            radiusMinPixels: 20,
                            radiusMaxPixels: 100,
                            transitions: {
                                getRadious: {
                                    duration: 1000,
                                    enter: t => t * 1.1,
                                }
                            },
                            onHover: (info, d) => {
                                if (!d.properties.cluster && (info.coordinate && info.object)){
                                    const marker = new window.google.maps.Marker({
                                        position: { lat: info.coordinate[1], lng: info.coordinate[0] },
                                        map: this.map.current.context[MAP]
                                    });
                                    marker.setOptions({ 'opacity': 0.0 });
                                    marker.setOptions({ 'title': `${d.properties.point_count}` });
                                }
                            }
                        })
                    ),

                    // when clustered, show to the total number of assets in the cluster on top of ScatterplotLayer
                    new TextLayer (
                        this.getSubLayerProps({
                            id: 'scatter-plot-cluster-layer',
                            data,
                            getText: d => d.properties.cluster ? d.properties.point_count : '',
                            getSize: 12,
                            getColor: [255, 255, 255],
                            characterSet: 'auto',
                        })
                    )
                ];
            }
        }

        // All the data that are necessary to create clustering and plotting point assets
        const layer = {
            id: "icon-cluster",
            data: this.props.assetMonitoringIconsData,
            pickable: true,
            getPosition: (d) => {
                if(d.geom && d.geom.coordinates) {
                    return d.geom.coordinates;
                }
                return d.geom;
            },
            sizeScale: 60,
            dataTransform: (data) => {
                return data.results.map(res => {
                    if(res.geom && res.geom_type === 'POINT' && res.geom) {
                        res.geom = {
                            "type": "Point",
                            "coordinates": [
                                res.geom.lng,
                                res.geom.lat
                            ]
                        };
                    }
                    return res;
                });
            }
        };

        const layers = this.overlay.current.props.layers
            ? [new IconClusterLayer(layer)].concat(this.overlay.current.props.layers.filter((ele) => ele.id !== layer.id))
            : [new IconClusterLayer(layer)];
        
        if (this.overlay.current) {
            this.overlay.current.setMap(this.map.current.context[MAP]);
            this.overlay.current.setProps({
                layers
            });
        }
    }

    addAssetMonitoringGeoJsonLayer () {
        const layer = {
            id: "custom-GeoJson-layer",
            data: this.props.assetMonitoringIconsData,
            pickable: true,
            stroked: true,
            filled: false,
            lineWidthScale: 20,
            getLineColor: [160, 160, 180, 200],
            getLineWidth: 1,
            wrapLongitude: true,
            dataTransform: (data) => {
                return data.results.map(res => {
                    if(res.geom && res.geom_type === 'LINESTRING') {
                        if (res.geom) {
                            res.geom = {
                                "type": "MultiLineString",
                                "coordinates": res.geom,
                            };
                        }
                    }
                    return res;
                });
            }
        };

        const layers = this.overlay.current.props.layers
            ? [new GeoJsonLayer(layer)].concat(this.overlay.current.props.layers.filter((ele) => ele.id !== layer.id))
            : [new GeoJsonLayer(layer)];
        
        if (this.overlay.current) {
            this.overlay.current.setMap(this.map.current.context[MAP]);
            this.overlay.current.setProps({
                layers
            });
        }
    }

问题原因分析

  • this.state.index未初始化:updateState中仅在rebuildIndex为true时创建Supercluster实例,组件首次渲染或rebuildIndex为false时,this.state.index为undefined,调用getClusters直接报错。
  • 数据有效性未校验:props.data可能为空或未正确加载,导致props.data.map触发“undefined is not iterable”错误。
  • 图层ID重复:ScatterplotLayer和TextLayer使用同一ID scatter-plot-cluster-layer,引发DeckGL图层管理冲突。
  • 属性名拼写错误:ScatterplotLayer的transitions中getRadious应为getRadius,导致过渡逻辑失效并干扰状态更新。

解决方案

1. 初始化Supercluster实例

在IconClusterLayer中添加initializeState方法,提前初始化index和数据:

class IconClusterLayer extends CompositeLayer {
  initializeState() {
    this.setState({
      index: new Supercluster({maxZoom: 16, radius: this.props.sizeScale}),
      data: [],
      z: 0
    });
  }

  // 其余代码保持不变
}

2. 校验数据有效性

在updateState中先校验数据,避免空值迭代:

updateState({props, oldProps, changeFlags}) {
  const rebuildIndex = changeFlags.dataChanged || props.sizeScale !== oldProps.sizeScale;

  if (rebuildIndex) {
    const index = new Supercluster({maxZoom: 16, radius: props.sizeScale});
    
    // 确保数据有效后再处理
    const validData = props.data?.results || [];
    index.load(
      validData.map(d => ({
        geometry: {coordinates: props.getPosition(d)},
        properties: d,
      }))
    );
    this.setState({index});
  }

  const z = Math.floor(this.context.viewport?.zoom || 0);
  // 确认index存在后再调用getClusters
  if ((rebuildIndex || z !== this.state.z) && this.state.index) {
    this.setState({
      data: this.state.index.getClusters([-180, -85, 180, 85], z),
      z,
    });
  }
}

3. 修复重复图层ID

给TextLayer设置唯一ID:

new TextLayer (
  this.getSubLayerProps({
    id: 'cluster-text-layer',
    data,
    getText: d => d.properties.cluster ? d.properties.point_count : '',
    getSize: 12,
    getColor: [255, 255, 255],
    characterSet: 'auto',
  })
)

4. 修正属性名拼写错误

将getRadious改为正确的getRadius:

transitions: {
  getRadius: {
    duration: 1000,
    enter: t => t * 1.1,
  }
}

5. 确保数据时序正确

在组件更新时监听数据变化,再触发图层添加:

// 类组件示例
componentDidUpdate(prevProps) {
  if (prevProps.assetMonitoringIconsData !== this.props.assetMonitoringIconsData) {
    this.addAssetMonitoringGeoJsonLayer();
    this.addAssetMonitoringIconsLayer();
  }
}

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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.08.10 16:31:17