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

Leaflet中CanvasTileLayer移动后位置错误,如何正确偏移Canvas?

问题描述

当moveend事件触发时,我移动了大画布并计算了瓦片左上角的坐标,画布内绘制的所有内容都需要随之移动。我计划通过左上角新旧位置的差值来偏移画布,但执行以下代码后,地图跑到了错误的位置:

import L from 'leaflet';

class CanvasTileLayer extends L.TileLayer {
    tileSize: L.Point;
    canvas: HTMLCanvasElement;
    ctx: CanvasRenderingContext2D | null;

    constructor(urlTemplate: string, options?: L.TileLayerOptions) {
        super(urlTemplate, options);
        this.tileSize = this.getTileSize();
        this.canvas = L.DomUtil.create('canvas', 'leaflet-tile-pane');
        this.ctx = this.canvas.getContext('2d');

        this.canvas.width = window.innerWidth;
        this.canvas.height = window.innerHeight;
    }

    createTile(coords: L.Coords, done: L.DoneCallback): HTMLElement {
        const tile = super.createTile(coords, done);
        const url = this.getTileUrl(coords);

        this.canvasRedraw(tile, url, coords);
        return tile;
    }

    private canvasRedraw(tile: HTMLElement, url: string, coords: L.Coords) {
        const map: L.Map = this._map;
        const bounds = map.getPixelBounds();

        // @ts-ignore
        const pos = this._getTilePos(coords);

        tile.onload = () => {
            // delete the original tile that was created with createTile
            this.removeTileElement(tile);

            this.ctx?.drawImage(
                // @ts-ignore
                tile,
                // Need to find out the difference in coordinates after onmoveend and then subtract it from pos
                pos.x,
                pos.y,
                this.tileSize.x,
                this.tileSize.y
            );
        };

        map.on('moveend', () => {
            const newBounds = map.getPixelBounds();

            // @ts-ignore
            const deltaX = Math.floor(newBounds.min.x - bounds.min.x);
            // @ts-ignore
            const deltaY = Math.floor(newBounds.min.y - bounds.min.y);

            console.log(pos.subtract([deltaX, deltaY]));

            L.DomUtil.setPosition(this.canvas, pos.subtract([deltaX, deltaY]));

            this.ctx?.drawImage(
                // @ts-ignore
                tile,
                pos.x - deltaX,
                pos.y - deltaY,
                this.tileSize.x,
                this.tileSize.y
            );
        });

        tile.onerror = () => {
            console.log(`Failed to load tile: ${url}`);
        };
    }

    removeTileElement(tile: HTMLElement) {
        tile.parentNode?.removeChild(tile);
    }

    onAdd(map: L.Map): this {
        super.onAdd(map);

        this.getPane()?.appendChild(this.canvas);

        return this;
    }
}

export default CanvasTileLayer;
问题分析与修复

核心问题

  1. 重复绑定moveend事件:每次创建瓦片时都会绑定一次moveend,导致地图移动时多个瓦片的偏移逻辑同时执行,坐标计算混乱
  2. 基准边界错误:bounds是瓦片加载时的初始地图边界,不是上一次移动后的边界,计算的偏移量delta完全不准确
  3. 画布状态未同步:直接修改画布位置和绘制坐标,但没有和地图实时的视图偏移保持一致
  4. 画布未清除:重绘时没有清除之前的内容,导致瓦片叠加显示异常

修复后的代码

import L from 'leaflet';

class CanvasTileLayer extends L.TileLayer {
    tileSize: L.Point;
    canvas: HTMLCanvasElement;
    ctx: CanvasRenderingContext2D | null;
    // 保存已加载的瓦片信息:坐标 -> 图片元素
    loadedTiles: Map<string, HTMLImageElement> = new Map();

    constructor(urlTemplate: string, options?: L.TileLayerOptions) {
        super(urlTemplate, options);
        this.tileSize = this.getTileSize();
        this.canvas = L.DomUtil.create('canvas', 'leaflet-tile-pane');
        this.ctx = this.canvas.getContext('2d');

        // 画布尺寸设为地图容器大小,避免窗口 resize 问题
        const mapContainer = this._map?.getContainer();
        if (mapContainer) {
            this.canvas.width = mapContainer.clientWidth;
            this.canvas.height = mapContainer.clientHeight;
        }
    }

    createTile(coords: L.Coords, done: L.DoneCallback): HTMLElement {
        const tile = super.createTile(coords, done) as HTMLImageElement;
        const tileKey = `${coords.x},${coords.y},${coords.z}`;

        tile.onload = () => {
            // 保存加载完成的瓦片
            this.loadedTiles.set(tileKey, tile);
            // 移除原生瓦片元素
            this.removeTileElement(tile);
            // 立即重绘画布
            this.redrawCanvas();
        };

        tile.onerror = () => {
            console.log(`Failed to load tile: ${this.getTileUrl(coords)}`);
            done(null, tile);
        };

        return tile;
    }

    private redrawCanvas() {
        if (!this.ctx || !this._map) return;

        // 清除画布内容
        this.ctx.clearRect(0, 0, this.canvas.width, this.canvas.height);
        const mapBounds = this._map.getPixelBounds();
        const tileSize = this.tileSize;

        // 遍历所有已加载的瓦片,计算当前应该绘制的位置
        this.loadedTiles.forEach((tile, key) => {
            const [x, y, z] = key.split(',').map(Number);
            // 获取瓦片的原始像素位置
            const tilePos = this._getTilePos({ x, y, z } as L.Coords);
            // 计算相对于当前地图边界的绘制坐标
            const drawX = tilePos.x - mapBounds.min.x;
            const drawY = tilePos.y - mapBounds.min.y;

            // 只绘制在画布可见范围内的瓦片
            if (drawX + tileSize.x > 0 && drawX < this.canvas.width && drawY + tileSize.y > 0 && drawY < this.canvas.height) {
                this.ctx.drawImage(tile, drawX, drawY, tileSize.x, tileSize.y);
            }
        });

        // 设置画布的位置,使其与地图视图对齐
        L.DomUtil.setPosition(this.canvas, L.point(-mapBounds.min.x, -mapBounds.min.y));
    }

    removeTileElement(tile: HTMLElement) {
        tile.parentNode?.removeChild(tile);
    }

    onAdd(map: L.Map): this {
        super.onAdd(map);
        this.getPane()?.appendChild(this.canvas);

        // 只绑定一次 moveend 和 resize 事件
        map.on('moveend resize', () => {
            // 更新画布尺寸(resize 时)
            const container = map.getContainer();
            this.canvas.width = container.clientWidth;
            this.canvas.height = container.clientHeight;
            // 重绘画布
            this.redrawCanvas();
        });

        return this;
    }

    onRemove(map: L.Map): this {
        // 移除事件监听,避免内存泄漏
        map.off('moveend resize');
        this.loadedTiles.clear();
        return super.onRemove(map);
    }
}

export default CanvasTileLayer;

修复说明

  1. 统一事件绑定:只在onAdd中绑定一次moveend和resize事件,避免重复触发逻辑
  2. 保存瓦片状态:用loadedTiles存储已加载的瓦片,重绘时统一处理所有瓦片
  3. 实时计算坐标:每次重绘时基于当前地图的像素边界,计算每个瓦片的正确绘制位置
  4. 清除画布:重绘前先清除画布,避免瓦片叠加
  5. 适配窗口大小:监听resize事件,动态调整画布尺寸,保证显示正常

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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.07.28 14:47:55