Handle offline tile rendering failures (#306)

This commit is contained in:
l5y
2025-10-13 09:26:49 +02:00
committed by GitHub
parent 874c8fd73c
commit 51e6479ab6
+78 -41
View File
@@ -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;
}