diff --git a/web/public/assets/js/app/main.js b/web/public/assets/js/app/main.js index 05f6519..d87dece 100644 --- a/web/public/assets/js/app/main.js +++ b/web/public/assets/js/app/main.js @@ -816,11 +816,39 @@ export function initializeApp(config) { function createOfflineTileLayer() { if (!hasLeaflet) return null; const offlineLayer = L.gridLayer({ className: 'map-tiles map-tiles-offline' }); + /** @type {HTMLElement|null} */ + let cachedOfflineFallbackTile = null; + + /** + * Provide a minimal placeholder tile when canvas rendering is not available. + * + * @param {number} size Pixel width and height of the tile. + * @returns {HTMLElement} Cloned fallback element ready for Leaflet consumption. + */ + function getOfflineFallbackTile(size) { + if (!cachedOfflineFallbackTile) { + const placeholder = document.createElement('div'); + placeholder.className = 'offline-tile-fallback'; + placeholder.style.width = `${size}px`; + placeholder.style.height = `${size}px`; + placeholder.style.backgroundColor = 'rgba(33, 66, 110, 0.92)'; + placeholder.style.display = 'flex'; + placeholder.style.alignItems = 'center'; + placeholder.style.justifyContent = 'center'; + placeholder.style.color = 'rgba(255, 255, 255, 0.6)'; + placeholder.style.font = 'bold 14px system-ui, sans-serif'; + placeholder.style.textTransform = 'uppercase'; + placeholder.textContent = 'Offline tile'; + cachedOfflineFallbackTile = placeholder; + } + return /** @type {HTMLElement} */ (cachedOfflineFallbackTile.cloneNode(true)); + } + /** * Render a placeholder tile for offline map usage. * * @param {{x: number, y: number, z: number}} coords Tile coordinates supplied by Leaflet. - * @returns {HTMLCanvasElement} Canvas element containing placeholder artwork. + * @returns {HTMLElement} Tile node containing placeholder artwork. */ offlineLayer.createTile = coords => { const size = 256; @@ -828,51 +856,60 @@ export function initializeApp(config) { canvas.width = size; canvas.height = size; const ctx = canvas.getContext('2d'); - const gradient = ctx.createLinearGradient(0, 0, size, size); - gradient.addColorStop(0, 'rgba(33, 66, 110, 0.92)'); - gradient.addColorStop(1, 'rgba(64, 98, 144, 0.92)'); - ctx.fillStyle = gradient; - ctx.fillRect(0, 0, size, size); - - ctx.strokeStyle = 'rgba(255,255,255,0.12)'; - ctx.lineWidth = 1; - const steps = 4; - for (let i = 1; i < steps; i++) { - const pos = (size / steps) * i; - ctx.beginPath(); - ctx.moveTo(pos, 0); - ctx.lineTo(pos, size); - ctx.stroke(); - ctx.beginPath(); - ctx.moveTo(0, pos); - ctx.lineTo(size, pos); - ctx.stroke(); + if (!ctx) { + console.warn('Canvas 2D context unavailable for offline tile rendering. Using fallback placeholder.'); + return getOfflineFallbackTile(size); } + try { + const gradient = ctx.createLinearGradient(0, 0, size, size); + gradient.addColorStop(0, 'rgba(33, 66, 110, 0.92)'); + gradient.addColorStop(1, 'rgba(64, 98, 144, 0.92)'); + ctx.fillStyle = gradient; + ctx.fillRect(0, 0, size, size); - const west = tileToLon(coords.x, coords.z); - const east = tileToLon(coords.x + 1, coords.z); - const north = tileToLat(coords.y, coords.z); - const south = tileToLat(coords.y + 1, coords.z); + ctx.strokeStyle = 'rgba(255,255,255,0.12)'; + ctx.lineWidth = 1; + const steps = 4; + for (let i = 1; i < steps; i++) { + const pos = (size / steps) * i; + ctx.beginPath(); + ctx.moveTo(pos, 0); + ctx.lineTo(pos, size); + ctx.stroke(); + ctx.beginPath(); + ctx.moveTo(0, pos); + ctx.lineTo(size, pos); + ctx.stroke(); + } - ctx.fillStyle = 'rgba(255,255,255,0.7)'; - ctx.font = '12px system-ui, sans-serif'; - ctx.textBaseline = 'top'; - ctx.fillText(`${west.toFixed(1)}°`, 8, 8); - ctx.textBaseline = 'bottom'; - ctx.fillText(`${east.toFixed(1)}°`, 8, size - 8); - ctx.textAlign = 'right'; - ctx.textBaseline = 'top'; - ctx.fillText(`${north.toFixed(1)}°`, size - 8, 8); - ctx.textBaseline = 'bottom'; - ctx.fillText(`${south.toFixed(1)}°`, size - 8, size - 8); + const west = tileToLon(coords.x, coords.z); + const east = tileToLon(coords.x + 1, coords.z); + const north = tileToLat(coords.y, coords.z); + const south = tileToLat(coords.y + 1, coords.z); - ctx.textAlign = 'center'; - ctx.textBaseline = 'middle'; - ctx.fillStyle = 'rgba(255,255,255,0.35)'; - ctx.font = 'bold 22px system-ui, sans-serif'; - ctx.fillText('PotatoMesh offline basemap', size / 2, size / 2); + ctx.fillStyle = 'rgba(255,255,255,0.7)'; + ctx.font = '12px system-ui, sans-serif'; + ctx.textBaseline = 'top'; + ctx.fillText(`${west.toFixed(1)}°`, 8, 8); + ctx.textBaseline = 'bottom'; + ctx.fillText(`${east.toFixed(1)}°`, 8, size - 8); + ctx.textAlign = 'right'; + ctx.textBaseline = 'top'; + ctx.fillText(`${north.toFixed(1)}°`, size - 8, 8); + ctx.textBaseline = 'bottom'; + ctx.fillText(`${south.toFixed(1)}°`, size - 8, size - 8); - return canvas; + ctx.textAlign = 'center'; + ctx.textBaseline = 'middle'; + ctx.fillStyle = 'rgba(255,255,255,0.35)'; + ctx.font = 'bold 22px system-ui, sans-serif'; + ctx.fillText('PotatoMesh offline basemap', size / 2, size / 2); + + return canvas; + } catch (error) { + console.error('Failed to render offline tile. Falling back to placeholder element.', error); + return getOfflineFallbackTile(size); + } }; return offlineLayer; }