import { createTileGrid, type TileItem } from '../../utils/tile' import { getTileSizePx, type CameraState } from '../camera/camera' import { type MapScene } from '../renderer/mapRenderer' import { type TileStore } from '../tile/tileStore' import { type MapLayer, type LayerRenderContext } from './mapLayer' function buildGridKey(scene: MapScene, tileSize: number): string { return [ scene.tileSource, scene.zoom, scene.centerTileX, scene.centerTileY, scene.viewportWidth, scene.viewportHeight, tileSize, scene.overdraw, ].join('|') } export class TileLayer implements MapLayer { lastVisibleTileCount: number lastReadyTileCount: number cachedGridKey: string cachedTiles: TileItem[] constructor() { this.lastVisibleTileCount = 0 this.lastReadyTileCount = 0 this.cachedGridKey = '' this.cachedTiles = [] } prepareTiles(scene: MapScene, camera: CameraState, tileStore: TileStore): TileItem[] { const tileSize = getTileSizePx(camera) if (!tileSize) { this.lastVisibleTileCount = 0 this.lastReadyTileCount = 0 this.cachedGridKey = '' this.cachedTiles = [] return [] } const gridKey = buildGridKey(scene, tileSize) if (gridKey !== this.cachedGridKey) { this.cachedGridKey = gridKey this.cachedTiles = createTileGrid({ urlTemplate: scene.tileSource, zoom: scene.zoom, centerTileX: scene.centerTileX, centerTileY: scene.centerTileY, viewportWidth: scene.viewportWidth, viewportHeight: scene.viewportHeight, tileSize, overdraw: scene.overdraw, }) } tileStore.queueVisibleTiles(this.cachedTiles, scene, gridKey) this.lastVisibleTileCount = this.cachedTiles.length this.lastReadyTileCount = 0 return this.cachedTiles } draw(context: LayerRenderContext): void { const { ctx, scene, camera, tileStore } = context const tiles = this.prepareTiles(scene, camera, tileStore) for (const tile of tiles) { const entry = tileStore.getEntry(tile.url) const drawLeft = tile.leftPx + scene.translateX const drawTop = tile.topPx + scene.translateY if (entry && entry.status === 'ready' && entry.image) { ctx.drawImage(entry.image, drawLeft, drawTop, tile.sizePx, tile.sizePx) this.lastReadyTileCount += 1 continue } const parentFallback = tileStore.getParentFallbackSlice(tile, scene) let drewFallback = false if (parentFallback) { ctx.drawImage( parentFallback.entry.image, parentFallback.sourceX, parentFallback.sourceY, parentFallback.sourceWidth, parentFallback.sourceHeight, drawLeft, drawTop, tile.sizePx, tile.sizePx, ) drewFallback = true } const childFallback = tileStore.getChildFallback(tile, scene) if (childFallback) { const cellWidth = tile.sizePx / childFallback.division const cellHeight = tile.sizePx / childFallback.division for (const child of childFallback.children) { const childImageWidth = child.entry.image.width || 256 const childImageHeight = child.entry.image.height || 256 ctx.drawImage( child.entry.image, 0, 0, childImageWidth, childImageHeight, drawLeft + child.offsetX * cellWidth, drawTop + child.offsetY * cellHeight, cellWidth, cellHeight, ) } drewFallback = true } if (!drewFallback) { ctx.fillStyle = entry && entry.status === 'error' ? '#d9b2b2' : '#dbeed4' ctx.fillRect(drawLeft, drawTop, tile.sizePx, tile.sizePx) } } } }