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

如何在Maplibre中对栅格瓦片图层进行着色?

栅格瓦片着色性能优化:解决大尺寸瓦片导致的浏览器崩溃

我尝试用以下代码给栅格瓦片着色,但当瓦片尺寸较大时,嵌套for循环的迭代次数过多会导致浏览器崩溃。Mapbox已推出栅格瓦片图层着色功能,但Maplibre目前尚未支持。

原代码思路是从Mapbox画布读取像素,基于灰度平均值逐像素绘制叠加画布并分配颜色:

const coloriseRasterTile = () => {
    if (!map.getLayer(`id-${id}`) && !map.getSource(`id-${id}`)) return;
    var customCanvas = document.getElementById('canvasID');
    var rasterTileBounds = [-76.76666666666668, 39.26666488571745, -76.73334678524573, 39.36666666666667];
    var canvasWidth = Math.abs(
        parseFloat(
            map.project([rasterTileBounds[0], rasterTileBounds[1]]).x -
                map.project([rasterTileBounds[2], rasterTileBounds[1]]).x,
            10,
        ),
    );
    var canvasHeight = Math.abs(
        parseFloat(
            map.project([rasterTileBounds[0], rasterTileBounds[1]]).y -
                map.project([rasterTileBounds[0], rasterTileBounds[3]]).y,
            10,
        ),
    );

    customCanvas.width = canvasWidth;
    customCanvas.height = canvasHeight;
    var overlayCtx = customCanvas.getContext('2d');
    if (overlayCtx) {
        customCanvas.style.display = 'none';
    }
    const canvas = map.getCanvas();
    const gl = canvas.getContext('webgl') || canvas.getContext('webgl2');
    const data = new Uint8Array(4);
    if (!gl) return;


     for (let x = 0; x < canvasWidth; x++) {
        for (let y = 0; y < canvasHeight; y++) {
            const canvasX = x - canvas.offsetLeft;
            const canvasY = canvas.height - y - canvas.offsetTop;
            const bufferX = ((gl.drawingBufferWidth / canvasWidth) * x).toFixed(0);
            const bufferY = ((gl.drawingBufferHeight / canvasHeight) * (canvasHeight - y)).toFixed(0);
            gl.readPixels(bufferX, bufferY, 1, 1, gl.RGBA, gl.UNSIGNED_BYTE, data);
            const [r, g, b, a] = data;
            const avg = r + g + b / 3;
            overlayCtx.fillStyle = avg < 125 ? 'green' : 'blue';
            overlayCtx.fillRect(x, y, 1, 1);
        }
    }
    map.addSource('canvas-source', {
        type: 'canvas',
        canvas: 'canvasID',
        coordinates: [
            [rasterTileBounds[0], rasterTileBounds[1]],
            [rasterTileBounds[2], rasterTileBounds[1]],
            [rasterTileBounds[2], rasterTileBounds[3]],
            [rasterTileBounds[0], rasterTileBounds[3]],
        ],
    });
    map.addLayer({
        id: 'canvas-layer',
        type: 'raster',
        source: 'canvas-source',
    });
 
};

优化方案:批量处理像素,消除性能瓶颈

逐像素调用gl.readPixels和fillRect是核心性能问题,每次调用都会产生上下文切换和DOM操作开销。优化后通过批量读取+批量绘制大幅降低迭代次数和API调用量:

const coloriseRasterTile = () => {
    const targetLayerId = `id-${id}`;
    const targetSourceId = `id-${id}`;
    if (!map.getLayer(targetLayerId) && !map.getSource(targetSourceId)) return;

    const customCanvas = document.getElementById('canvasID');
    const rasterTileBounds = [-76.76666666666668, 39.26666488571745, -76.73334678524573, 39.36666666666667];
    
    // 预计算地图投影坐标,减少重复调用
    const bottomLeft = map.project([rasterTileBounds[0], rasterTileBounds[1]]);
    const bottomRight = map.project([rasterTileBounds[2], rasterTileBounds[1]]);
    const topLeft = map.project([rasterTileBounds[0], rasterTileBounds[3]]);
    
    // 计算叠加画布尺寸
    const canvasWidth = Math.abs(bottomRight.x - bottomLeft.x);
    const canvasHeight = Math.abs(topLeft.y - bottomLeft.y);

    customCanvas.width = canvasWidth;
    customCanvas.height = canvasHeight;
    const overlayCtx = customCanvas.getContext('2d');
    if (!overlayCtx) return;
    customCanvas.style.display = 'none';

    const canvas = map.getCanvas();
    const gl = canvas.getContext('webgl') || canvas.getContext('webgl2');
    if (!gl) return;

    // 计算WebGL缓冲区中的目标区域范围
    const bufferScaleX = gl.drawingBufferWidth / canvas.clientWidth;
    const bufferScaleY = gl.drawingBufferHeight / canvas.clientHeight;
    const bufferX = Math.floor(bottomLeft.x * bufferScaleX);
    const bufferY = Math.floor((canvas.clientHeight - topLeft.y) * bufferScaleY);
    const bufferWidth = Math.floor(canvasWidth * bufferScaleX);
    const bufferHeight = Math.floor(canvasHeight * bufferScaleY);

    // 一次性读取整个瓦片区域的像素数据
    const pixelData = new Uint8Array(bufferWidth * bufferHeight * 4);
    gl.readPixels(bufferX, bufferY, bufferWidth, bufferHeight, gl.RGBA, gl.UNSIGNED_BYTE, pixelData);

    // 创建ImageData用于批量绘制
    const imageData = overlayCtx.createImageData(canvasWidth, canvasHeight);
    const outputData = imageData.data;

    // 批量处理像素颜色逻辑
    for (let i = 0; i < pixelData.length; i += 4) {
        const r = pixelData[i];
        const g = pixelData[i + 1];
        const b = pixelData[i + 2];
        // 修正原代码的灰度平均值计算错误
        const grayAvg = (r + g + b) / 3;
        // 分配目标颜色
        const [newR, newG, newB] = grayAvg < 125 ? [0, 255, 0] : [0, 0, 255];
        
        outputData[i] = newR;
        outputData[i + 1] = newG;
        outputData[i + 2] = newB;
        outputData[i + 3] = 255; // 设置完全不透明
    }

    // 一次性将处理后的像素绘制到叠加画布
    overlayCtx.putImageData(imageData, 0, 0);

    // 避免重复添加数据源和图层
    if (!map.getSource('canvas-source')) {
        map.addSource('canvas-source', {
            type: 'canvas',
            canvas: 'canvasID',
            coordinates: [
                [rasterTileBounds[0], rasterTileBounds[1]],
                [rasterTileBounds[2], rasterTileBounds[1]],
                [rasterTileBounds[2], rasterTileBounds[3]],
                [rasterTileBounds[0], rasterTileBounds[3]],
            ],
        });
    }

    if (!map.getLayer('canvas-layer')) {
        map.addLayer({
            id: 'canvas-layer',
            type: 'raster',
            source: 'canvas-source',
        });
    }
};

核心优化点说明:

  • 批量读取像素:用一次gl.readPixels读取整个瓦片区域,替代数万次逐像素读取,消除WebGL API调用开销。
  • 批量绘制:通过ImageData和putImageData一次性完成绘制,避免循环中反复调用fillRect带来的DOM操作损耗。
  • 修正逻辑错误:原代码中avg = r + g + b / 3计算错误,改为正确的灰度平均值公式(r + g + b) / 3。
  • 避免重复创建资源:添加数据源和图层前先检查是否已存在,防止重复初始化。
  • 减少重复计算:预计算地图投影坐标和缩放比例,避免循环内重复调用map.project和浮点数运算。

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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.07.11 19:05:41