mirror of
https://github.com/mauriceboe/TREK.git
synced 2026-10-18 21:16:47 +00:00
fix(offline): cache osm-de and stadia tiles, prefetch down to the opening zoom (#2180)
This commit is contained in:
@@ -20,7 +20,10 @@ import type { Place } from '../types'
|
||||
|
||||
/** OpenFreeMap serves vector tiles up to z14 and overzooms from there. */
|
||||
const VECTOR_MAX_ZOOM = 14
|
||||
const VECTOR_MIN_ZOOM = 10
|
||||
// Zoom 0, not 10: MapLibre can only overzoom upward, so a wide trip opening at
|
||||
// its fitBounds zoom (z6–9) rendered nothing offline with a z10 floor (#2180).
|
||||
// The low zooms cost a few dozen tiles at most.
|
||||
const VECTOR_MIN_ZOOM = 0
|
||||
|
||||
/** Requests in flight, kept low for the same reason the raster side keeps it low. */
|
||||
const CONCURRENCY = 6
|
||||
|
||||
@@ -4,7 +4,10 @@
|
||||
*
|
||||
* Algorithm:
|
||||
* 1. Compute bbox from trip's place coordinates + padding.
|
||||
* 2. For zooms 10–16, enumerate tile XYZ coordinates within bbox.
|
||||
* 2. For zooms 0–16, enumerate tile XYZ coordinates within bbox. The low
|
||||
* zooms cost next to nothing (a handful of tiles) but are what the trip
|
||||
* map actually opens at for a multi-city bbox — a z10 floor left the
|
||||
* fitBounds view empty offline (#2180).
|
||||
* 3. Stop when cumulative tile estimate exceeds MAX_TILES (~50 MB).
|
||||
* 4. Fetch each tile URL so the Service Worker CacheFirst handler caches it,
|
||||
* at most TILE_CONCURRENCY at a time.
|
||||
@@ -228,7 +231,7 @@ async function openTileCache(): Promise<Cache | null> {
|
||||
export async function prefetchTiles(
|
||||
bbox: TileBbox,
|
||||
tileUrlTemplate: string,
|
||||
minZoom = 10,
|
||||
minZoom = 0,
|
||||
maxZoom = 16,
|
||||
cartoKey?: string,
|
||||
): Promise<number> {
|
||||
@@ -362,7 +365,7 @@ export async function prefetchTilesForTrip(
|
||||
// tile providers that don't send CORS headers. To stop the browser evicting
|
||||
// these tiles under the inflated quota, we request persistent storage at app
|
||||
// init instead (sync/persistentStorage.ts).
|
||||
const fetched = await prefetchTiles(bbox, template, 10, 16, cartoKey)
|
||||
const fetched = await prefetchTiles(bbox, template, 0, 16, cartoKey)
|
||||
|
||||
// Update syncMeta with bbox and tile count
|
||||
const meta = await offlineDb.syncMeta.get(tripId)
|
||||
|
||||
@@ -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);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -92,6 +92,28 @@ export default defineConfig(({ mode }) => ({
|
||||
cacheableResponse: { statuses: [0, 200] },
|
||||
},
|
||||
},
|
||||
{
|
||||
// OpenStreetMap DE — a shipped preset that matched no rule at all, so
|
||||
// "Store map tiles offline" fetched thousands of tiles and stored none
|
||||
// of them (#2180). Same cache, same limits as the rules above.
|
||||
urlPattern: /^https:\/\/tile\.openstreetmap\.de\/.*/i,
|
||||
handler: 'CacheFirst',
|
||||
options: {
|
||||
cacheName: 'map-tiles',
|
||||
expiration: { maxEntries: 12288, maxAgeSeconds: 30 * 24 * 60 * 60 },
|
||||
cacheableResponse: { statuses: [0, 200] },
|
||||
},
|
||||
},
|
||||
{
|
||||
// Stadia Smooth — the other shipped raster preset with the same hole (#2180).
|
||||
urlPattern: /^https:\/\/tiles\.stadiamaps\.com\/tiles\/.*/i,
|
||||
handler: 'CacheFirst',
|
||||
options: {
|
||||
cacheName: 'map-tiles',
|
||||
expiration: { maxEntries: 12288, maxAgeSeconds: 30 * 24 * 60 * 60 },
|
||||
cacheableResponse: { statuses: [0, 200] },
|
||||
},
|
||||
},
|
||||
{
|
||||
// The GL style DOCUMENT, for both providers. It has to be matched
|
||||
// before the tile rules below, because Workbox takes the first route
|
||||
|
||||
Reference in New Issue
Block a user