export interface TileItem { key: string url: string x: number y: number leftPx: number topPx: number sizePx: number isCenter: boolean } const TILE_OVERLAP_PX = 2 export interface TileGridOptions { urlTemplate: string zoom: number centerTileX: number centerTileY: number viewportWidth: number viewportHeight: number tileSize: number overdraw: number } export function buildTileUrl(template: string, z: number, x: number, y: number): string { return template .replace('{z}', String(z)) .replace('{x}', String(x)) .replace('{y}', String(y)) } export function createTileGrid(options: TileGridOptions): TileItem[] { const tiles: TileItem[] = [] const halfWidth = options.viewportWidth / 2 const halfHeight = options.viewportHeight / 2 const horizontalRange = Math.ceil(halfWidth / options.tileSize) + options.overdraw const verticalRange = Math.ceil(halfHeight / options.tileSize) + options.overdraw for (let dy = -verticalRange; dy <= verticalRange; dy += 1) { for (let dx = -horizontalRange; dx <= horizontalRange; dx += 1) { const x = options.centerTileX + dx const y = options.centerTileY + dy const rawLeft = halfWidth + (dx - 0.5) * options.tileSize const rawTop = halfHeight + (dy - 0.5) * options.tileSize tiles.push({ key: `${options.zoom}-${x}-${y}`, url: buildTileUrl(options.urlTemplate, options.zoom, x, y), x, y, leftPx: Math.floor(rawLeft - TILE_OVERLAP_PX / 2), topPx: Math.floor(rawTop - TILE_OVERLAP_PX / 2), sizePx: Math.ceil(options.tileSize) + TILE_OVERLAP_PX, isCenter: dx === 0 && dy === 0, }) } } return tiles }