fix(offline): cache osm-de and stadia tiles, prefetch down to the opening zoom (#2180)

This commit is contained in:
Maurice
2026-09-01 17:45:08 +02:00
committed by Maurice
parent 1e6f1fe95c
commit 5decdcfa13
5 changed files with 78 additions and 6 deletions
+4 -2
View File
@@ -139,7 +139,7 @@ describe('prefetchStyleAssets', () => {
describe('prefetchVectorForPlaces', () => {
const places = [buildPlace({ lat: 52.52, lng: 13.405 })];
it('FE-SYNC-GLPF-007: downloads the tiles covering the trip, z10 to z14', async () => {
it('FE-SYNC-GLPF-007: downloads the tiles covering the trip, z0 to z14', async () => {
const result = await prefetchVectorForPlaces(places, STYLE_URL);
expect(result.template).toBe(TILE_TEMPLATE);
expect(result.tiles).toBeGreaterThan(0);
@@ -151,7 +151,9 @@ describe('prefetchVectorForPlaces', () => {
);
// z14 is the last one OpenFreeMap serves; MapLibre scales it beyond that
// rather than asking for another, which is why this stops where raster could not.
expect([...zooms].sort((a, b) => a - b)).toEqual([10, 11, 12, 13, 14]);
// The floor is z0: MapLibre cannot overzoom downward, so the fitBounds view
// of a wide trip needs the low zooms cached too (#2180).
expect([...zooms].sort((a, b) => a - b)).toEqual([0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14]);
});
it('FE-SYNC-GLPF-008: does nothing offline', async () => {
@@ -519,3 +519,45 @@ describe('the GL style document is not served stale (#1924)', () => {
expect(config).toMatch(/cacheName:\s*'gl-map-styles'/);
});
});
describe('shipped raster presets reach the map-tiles cache (#2180)', () => {
const config = readFileSync(resolve(process.cwd(), 'vite.config.js'), 'utf8');
// First-match semantics, like the #1924 harness above, but capturing the
// cacheName too: a preset whose tiles land in some other cache is as offline-
// dead as one that matches nothing.
const rules = [...config.matchAll(/urlPattern:\s*(\/[^\n]*?\/i),\s*\n\s*handler:\s*'([A-Za-z]+)',\s*\n\s*options:\s*\{\s*\n\s*cacheName:\s*'([^']+)'/g)].map(m => ({
pattern: new RegExp(m[1].slice(1, m[1].lastIndexOf('/')), 'i'),
handler: m[2],
cacheName: m[3],
}));
const firstMatch = (url: string) => rules.find(r => r.pattern.test(url));
it('OpenStreetMap DE tiles are cached — the preset used to match no rule at all', () => {
const rule = firstMatch('https://tile.openstreetmap.de/13/4091/2722.png');
expect(rule?.handler).toBe('CacheFirst');
expect(rule?.cacheName).toBe('map-tiles');
});
it('Stadia tiles are cached the same way', () => {
const rule = firstMatch('https://tiles.stadiamaps.com/tiles/alidade_smooth/13/4091/2722.png');
expect(rule?.handler).toBe('CacheFirst');
expect(rule?.cacheName).toBe('map-tiles');
});
it('still refuses look-alike hosts', () => {
expect(firstMatch('https://tile.openstreetmap.de.example.com/13/4091/2722.png')).toBeUndefined();
});
});
describe('prefetch covers the opening zoom (#2180)', () => {
it('fetches the low zooms too, so a wide trip has tiles at its fitBounds view', async () => {
// A ~3° multi-city bbox opens around z7 — the old z10 floor left that view
// completely uncached offline.
const bbox: TileBbox = { minLat: 47.0, maxLat: 50.0, minLng: 6.0, maxLng: 9.0 };
await prefetchTiles(bbox, 'https://tile.example.com/{z}/{x}/{y}.png');
const urls = vi.mocked(fetch).mock.calls.map(c => String(c[0]));
expect(urls.some(u => u.includes('/0/'))).toBe(true);
expect(urls.some(u => u.includes('/7/'))).toBe(true);
});
});