Tiles: heal blank tiles online (periodic OSM-reachability check + backfill)
The on-demand loader never retries a tile whose fetch failed, so holes from a bad signal stayed blank even after the network recovered. Add a healer: every 30s, when OSM is reachable, re-fetch MISSING tiles (current zoom) for the visible area and along the ridden route, cache them, and redraw. Gentle to OSM (one-tile probe, 300ms throttle, <=40 fetches/cycle); respects offline mode. Wired on record/detail/compare/roads maps, stopped on screen change.
This commit is contained in:
parent
d4dda3da0c
commit
3f092cced0
5 changed files with 148 additions and 6 deletions
|
|
@ -250,6 +250,7 @@ function renderScreen(name, fromPopstate) {
|
|||
// Release screen wake lock and compass listener when leaving recording screen
|
||||
if (window._screenLock) { try { window._screenLock.release(); } catch {} window._screenLock = null; }
|
||||
if (GPS.recording._compassCleanup) { GPS.recording._compassCleanup(); GPS.recording._compassCleanup = null; }
|
||||
try { TileCache.stopHealer(); } catch {} // leaving a map screen stops online tile-healing; map screens restart it
|
||||
|
||||
let prevScreen = state.screen;
|
||||
state.screen = name;
|
||||
|
|
@ -4632,7 +4633,9 @@ async function renderRecordingInline(container) {
|
|||
}, { passive: false });
|
||||
addZoomDisplay(map);
|
||||
let CachedLayer = TileCache.createLayer();
|
||||
new CachedLayer({ maxZoom: 19 }).addTo(map);
|
||||
let tileLayer = new CachedLayer({ maxZoom: 19 }).addTo(map);
|
||||
// Heal blank tiles along the ridden route whenever OSM is reachable.
|
||||
TileCache.startHealer(map, tileLayer, () => (GPS.recording.points || []).map(p => [p.lat, p.lon]));
|
||||
// Force recalc after flex layout settles — fixes black screen on init
|
||||
setTimeout(() => map.invalidateSize(), 200);
|
||||
|
||||
|
|
@ -5620,7 +5623,9 @@ async function showSessionDetail(sessionId) {
|
|||
}, { passive: false });
|
||||
addZoomDisplay(map);
|
||||
let CachedLayer = TileCache.createLayer();
|
||||
new CachedLayer({ maxZoom: 19 }).addTo(map);
|
||||
let tileLayer = new CachedLayer({ maxZoom: 19 }).addTo(map);
|
||||
// Heal blank tiles along this track's route when OSM is reachable.
|
||||
TileCache.startHealer(map, tileLayer, () => points.map(p => [p.lat, p.lon]));
|
||||
addDarkMapControl(map); // dark-tiles toggle on the track view
|
||||
POI.attach(map); // POI overlay + category filter
|
||||
{ let nl = null; let rn = async () => { nl = await drawNogoAreas(map, nl, rn); }; rn(); } // impassable-spot circles
|
||||
|
|
@ -6669,7 +6674,8 @@ async function showComparisonView(currentId, compareId, currentSession, currentP
|
|||
mapEl._leafletMap = map;
|
||||
addZoomDisplay(map);
|
||||
let CachedLayer = TileCache.createLayer();
|
||||
new CachedLayer({ maxZoom: 19 }).addTo(map);
|
||||
let tileLayer = new CachedLayer({ maxZoom: 19 }).addTo(map);
|
||||
TileCache.startHealer(map, tileLayer, null); // heal visible tiles when OSM is reachable
|
||||
|
||||
// Build speed lookup for older track
|
||||
let olderCoords = olderPoints.map(p => [p.lat, p.lon]);
|
||||
|
|
@ -8674,7 +8680,8 @@ async function showRoadsMap() {
|
|||
let map = L.map(mapEl, { preferCanvas: true, attributionControl: false }).setView([47.42, 40.08], 13); // canvas: many road polylines vs SVG compositor tile-memory limit
|
||||
addZoomDisplay(map);
|
||||
let CachedLayer = TileCache.createLayer();
|
||||
new CachedLayer({ maxZoom: 19 }).addTo(map);
|
||||
let tileLayer = new CachedLayer({ maxZoom: 19 }).addTo(map);
|
||||
TileCache.startHealer(map, tileLayer, null); // heal visible tiles when OSM is reachable
|
||||
addDarkMapControl(map); // dark-tiles toggle on the roads map
|
||||
POI.attach(map); // POI overlay + category filter
|
||||
|
||||
|
|
|
|||
|
|
@ -216,5 +216,113 @@ TileCache.importFromSqlite = async function(path, onProgress) {
|
|||
return imported;
|
||||
};
|
||||
|
||||
// ---- Online tile healer ---------------------------------------------------
|
||||
// The on-demand loader (createTile) never retries a tile whose fetch failed:
|
||||
// Leaflet marks it loaded-with-error and won't re-request until the tile is
|
||||
// re-created (pan out+back / zoom / redraw). So blanks from a bad moment stay
|
||||
// blank even after the network recovers. The healer fixes that: periodically,
|
||||
// when OSM is reachable, it re-fetches MISSING tiles (current zoom only) for the
|
||||
// visible area and — during recording / track review — along the ridden route,
|
||||
// caches them, and redraws so the holes fill. Respects offline mode. Gentle to
|
||||
// OSM (one-tile probe, throttled fetches, capped per cycle) per their tile usage
|
||||
// policy — we never bulk-download.
|
||||
let _healTimer = null, _healMap = null, _healLayer = null, _healPointsFn = null;
|
||||
let _healing = false, _healCursor = 0, _onlineHandler = null;
|
||||
const HEAL_INTERVAL_MS = 30000, HEAL_THROTTLE_MS = 300;
|
||||
const HEAL_MAX_FETCH = 40; // tiles fetched per cycle (≈1.3/s — modest)
|
||||
const HEAL_MAX_SCAN = 500; // IndexedDB lookups per cycle (bounds CPU on long routes)
|
||||
|
||||
function _sleep(ms) { return new Promise(r => setTimeout(r, ms)); }
|
||||
|
||||
// One small tile tells us OSM is reachable (navigator.onLine gates first — cheap).
|
||||
TileCache.probeOsm = async function() {
|
||||
if (!navigator.onLine) return false;
|
||||
try {
|
||||
const c = new AbortController();
|
||||
const t = setTimeout(() => c.abort(), 6000);
|
||||
const r = await fetch("https://a.tile.openstreetmap.org/0/0/0.png", { cache: "no-store", signal: c.signal });
|
||||
clearTimeout(t);
|
||||
return r.ok;
|
||||
} catch { return false; }
|
||||
};
|
||||
|
||||
function _lonLatToTile(lat, lon, z) {
|
||||
const n = Math.pow(2, z);
|
||||
return {
|
||||
x: Math.floor((lon + 180) / 360 * n),
|
||||
y: Math.floor((1 - Math.log(Math.tan(lat * Math.PI / 180) + 1 / Math.cos(lat * Math.PI / 180)) / Math.PI) / 2 * n),
|
||||
};
|
||||
}
|
||||
|
||||
// Tiles covering a polyline at zoom z (+1 ring around each point so the road's
|
||||
// tiles, not just the exact centre, are covered), de-duplicated.
|
||||
function _tileKeysForLatLngs(latlngs, z) {
|
||||
const seen = new Set();
|
||||
for (const ll of latlngs) {
|
||||
const lat = ll[0], lon = ll[1];
|
||||
if (lat == null || lon == null) continue;
|
||||
const t = _lonLatToTile(lat, lon, z);
|
||||
for (let dx = -1; dx <= 1; dx++) for (let dy = -1; dy <= 1; dy++) seen.add(`${z}/${t.x + dx}/${t.y + dy}`);
|
||||
}
|
||||
return [...seen];
|
||||
}
|
||||
|
||||
function _tileKeysForBounds(bounds, z) {
|
||||
const r = getTileRange(bounds, z);
|
||||
const keys = [];
|
||||
for (let x = r.xMin; x <= r.xMax; x++) for (let y = r.yMin; y <= r.yMax; y++) keys.push(`${z}/${x}/${y}`);
|
||||
return keys;
|
||||
}
|
||||
|
||||
async function _healCycle() {
|
||||
if (_healing || TileCache.offlineMode || !_healMap) return;
|
||||
_healing = true;
|
||||
try {
|
||||
if (!(await TileCache.probeOsm())) return;
|
||||
const z = _healMap.getZoom();
|
||||
// Visible tiles first (fills what you actually see), then the ridden route
|
||||
// from a rotating cursor so a long track trickles across cycles, not in a burst.
|
||||
let keys = _tileKeysForBounds(_healMap.getBounds(), z);
|
||||
const pts = _healPointsFn && _healPointsFn();
|
||||
if (pts && pts.length) {
|
||||
const routeKeys = _tileKeysForLatLngs(pts, z);
|
||||
if (_healCursor >= routeKeys.length) _healCursor = 0;
|
||||
keys = keys.concat(routeKeys.slice(_healCursor), routeKeys.slice(0, _healCursor));
|
||||
}
|
||||
let fetched = 0, scanned = 0, advanced = 0;
|
||||
for (const key of keys) {
|
||||
if (fetched >= HEAL_MAX_FETCH || scanned >= HEAL_MAX_SCAN) break;
|
||||
scanned++; advanced++;
|
||||
if (await getTile(key)) continue; // already cached
|
||||
try {
|
||||
const r = await fetch(`https://a.tile.openstreetmap.org/${key}.png`, { cache: "no-store" });
|
||||
if (r.ok) { await putTile(key, await r.blob()); fetched++; }
|
||||
} catch { /* leave for a later cycle */ }
|
||||
await _sleep(HEAL_THROTTLE_MS);
|
||||
}
|
||||
if (pts && pts.length) _healCursor += advanced; // continue the route next cycle
|
||||
if (fetched && _healLayer && _healLayer.redraw) _healLayer.redraw();
|
||||
} finally { _healing = false; }
|
||||
}
|
||||
|
||||
// Start healing for a map. layer = the tile layer to redraw once holes fill.
|
||||
// pointsFn() → [[lat,lon],…] of the ridden route (recording / track review), or
|
||||
// null to heal only the visible area (e.g. the roads screen).
|
||||
TileCache.startHealer = function(map, layer, pointsFn) {
|
||||
TileCache.stopHealer();
|
||||
_healMap = map; _healLayer = layer; _healPointsFn = pointsFn || null; _healCursor = 0;
|
||||
_healTimer = setInterval(_healCycle, HEAL_INTERVAL_MS);
|
||||
setTimeout(_healCycle, 3000); // first pass soon after the map opens
|
||||
_onlineHandler = () => _healCycle(); // and immediately when the link returns
|
||||
window.addEventListener("online", _onlineHandler);
|
||||
if (map && map.on) map.on("unload", TileCache.stopHealer);
|
||||
};
|
||||
|
||||
TileCache.stopHealer = function() {
|
||||
if (_healTimer) { clearInterval(_healTimer); _healTimer = null; }
|
||||
if (_onlineHandler) { window.removeEventListener("online", _onlineHandler); _onlineHandler = null; }
|
||||
_healMap = _healLayer = _healPointsFn = null; _healCursor = 0;
|
||||
};
|
||||
|
||||
window.TileCache = TileCache;
|
||||
console.log("[tilecache.js] window.TileCache ready, offline:", TileCache.offlineMode);
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue