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;
问题分析与修复
核心问题
- 重复绑定
moveend事件:每次创建瓦片时都会绑定一次moveend,导致地图移动时多个瓦片的偏移逻辑同时执行,坐标计算混乱 - 基准边界错误:
bounds是瓦片加载时的初始地图边界,不是上一次移动后的边界,计算的偏移量delta完全不准确 - 画布状态未同步:直接修改画布位置和绘制坐标,但没有和地图实时的视图偏移保持一致
- 画布未清除:重绘时没有清除之前的内容,导致瓦片叠加显示异常
修复后的代码
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;
修复说明
- 统一事件绑定:只在
onAdd中绑定一次moveend和resize事件,避免重复触发逻辑 - 保存瓦片状态:用
loadedTiles存储已加载的瓦片,重绘时统一处理所有瓦片 - 实时计算坐标:每次重绘时基于当前地图的像素边界,计算每个瓦片的正确绘制位置
- 清除画布:重绘前先清除画布,避免瓦片叠加
- 适配窗口大小:监听
resize事件,动态调整画布尺寸,保证显示正常
内容的提问来源于stack exchange,提问作者Attato
相关产品推荐
相关产品推荐

