import L, { Util, Browser } from 'leaflet' interface SmartTileLayerCache { [key: string]: number | undefined } export const SmartTileLayer = L.TileLayer.extend({ cache: {} as SmartTileLayerCache, invalidTile: '', map: 0, getTileUrl(coords: { x: number; y: number; z: number }) { if (!this._map) return this.invalidTile let zoom try { zoom = this._getZoomForUrl() } catch { return this.invalidTile } return this.getTrueTileUrl(coords, zoom) }, getTrueTileUrl(coords: { x: number; y: number }, zoom: number) { const data: Record = { r: Browser.retina ? '@2x' : '', s: this._getSubdomain(coords), x: coords.x, y: coords.y, map: this.map, z: zoom, } if (this._map && !this._map.options.crs.infinite) { const invertedY = this._globalTileRange.max.y - coords.y if (this.options.tms) { data.y = invertedY } data['-y'] = invertedY } const cacheKey = `${data.map}:${data.x}:${data.y}:${data.z}` data.cache = this.cache[cacheKey] // Don't request tiles for invalid/unknown map (avoids 404 spam in console) const mapId = Number(data.map) if (data.map === undefined || data.map === null || mapId < 1) { return this.invalidTile } // Only use placeholder when server explicitly marks tile as invalid (-1) if (data.cache === -1) { return this.invalidTile } // Allow tile request when map is valid even if SSE snapshot hasn't arrived yet // (avoids empty map when proxy/SSE delays or drops first message) if (data.cache === undefined || data.cache === null) { data.cache = 0 } return Util.template(this._url, Util.extend(data, this.options)) }, refresh(x: number, y: number, z: number) { let zoom = z const maxZoom = this.options.maxZoom const zoomReverse = this.options.zoomReverse const zoomOffset = this.options.zoomOffset if (zoomReverse) { zoom = maxZoom - zoom } zoom += zoomOffset const key = `${x}:${y}:${zoom}` const tile = this._tiles[key] if (tile?.el) { tile.el.src = this.getTrueTileUrl({ x, y }, z) tile.el.classList.add('tile-fresh') const el = tile.el setTimeout(() => el.classList.remove('tile-fresh'), 600) } }, }) as unknown as new (urlTemplate: string, options?: L.TileLayerOptions) => L.TileLayer & { cache: SmartTileLayerCache invalidTile: string map: number getTrueTileUrl: (coords: { x: number; y: number }, zoom: number) => string refresh: (x: number, y: number, z: number) => void }