Skip to content

Commit c08d06d

Browse files
committed
Merge branch 'fix/ridden-roads-client-cache'
2 parents 1f089cf + 2efe832 commit c08d06d

5 files changed

Lines changed: 117 additions & 6 deletions

File tree

‎app/page.tsx‎

Lines changed: 27 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,7 @@ import { ElevationProfile } from '@/components/ElevationProfile';
2727
import pkg from '@/package.json';
2828
import { haversineM, toSemicircles } from '@/lib/geometry';
2929
import { shareOrDownloadGpx } from '@/lib/gpxShare';
30+
import { missingTiles as missingRoadTiles, bboxForTiles as roadBboxForTiles, tileKey as roadTileKey } from '@/lib/roadTiles';
3031

3132
export default function Home() {
3233
const [bbox, setBbox] = useState<{ south: number; west: number; north: number; east: number } | null>(null);
@@ -106,6 +107,8 @@ export default function Home() {
106107
const clickChainRef = useRef<Promise<void>>(Promise.resolve());
107108
const generateAbortControllerRef = useRef<AbortController | null>(null);
108109
const roadsAbortControllerRef = useRef<AbortController | null>(null);
110+
const fetchedRoadTilesRef = useRef<Set<string>>(new Set());
111+
const roadTileCacheRef = useRef<[number, number][][]>([]);
109112
const pointsRef = useRef<{ lat: number; lon: number; id: string; status?: 'pending' | 'snapped' }[]>([]);
110113
const manualRouteRef = useRef<[number, number][][]>([]);
111114
const historyRef = useRef<{ points: { lat: number; lon: number; id: string; status?: 'pending' | 'snapped' }[], route: [number, number][][], selectionBoxes: { north: number; south: number; east: number; west: number }[], preAreaPointCount: number | null }[]>([]);
@@ -363,25 +366,46 @@ export default function Home() {
363366
useEffect(() => {
364367
if (!bbox) return;
365368

369+
// Tile the viewport (plus a small buffer) at a coarse grid so panning only
370+
// fetches the newly-revealed tiles instead of re-downloading the whole
371+
// viewport's road geometry every time — this is the single biggest source
372+
// of Vercel "Fast Origin Transfer" usage, since /api/roads fires on every
373+
// pan/zoom (moveend).
374+
const missing = missingRoadTiles(bbox, fetchedRoadTilesRef.current);
375+
376+
if (missing.length === 0) {
377+
// Every tile in view has already been fetched — reuse the accumulated
378+
// cache, no network request needed.
379+
if (roadTileCacheRef.current.length > 0) setAllRoads(roadTileCacheRef.current);
380+
return;
381+
}
382+
366383
roadsAbortControllerRef.current?.abort();
367384
roadsAbortControllerRef.current = new AbortController();
368385
const signal = roadsAbortControllerRef.current.signal;
369386

387+
// Fetch only the bounding box covering the missing tiles, not the full viewport.
388+
const fetchBbox = roadBboxForTiles(missing);
389+
370390
const timer = setTimeout(() => {
371391
fetch('/api/roads', {
372392
method: 'POST',
373393
headers: { 'Content-Type': 'application/json' },
374-
body: JSON.stringify({ bbox }),
394+
body: JSON.stringify({ bbox: fetchBbox }),
375395
signal
376396
})
377397
.then(res => res.json())
378398
.then(data => {
379399
if (data.roads) {
380-
console.log(`[StreetSweep] Received ${data.roads.length} roadmap segments.`);
400+
console.log(`[StreetSweep] Received ${data.roads.length} roadmap segments for ${missing.length} new tiles.`);
381401
// Keep the previous road set if this fetch came back empty
382402
// (transient Overpass hiccup) so ridden-road snapping doesn't
383403
// fall back to raw GPS traces.
384-
if (data.roads.length > 0) setAllRoads(data.roads);
404+
if (data.roads.length > 0) {
405+
for (const t of missing) fetchedRoadTilesRef.current.add(roadTileKey(t));
406+
roadTileCacheRef.current = roadTileCacheRef.current.concat(data.roads);
407+
setAllRoads(roadTileCacheRef.current);
408+
}
385409
setServiceWarning(!!data.degraded);
386410
}
387411
})

‎lib/roadTiles.test.ts‎

Lines changed: 50 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,50 @@
1+
import { tilesForBBox, tileKey, missingTiles, bboxForTiles, ROAD_TILE } from './roadTiles';
2+
3+
describe('roadTiles', () => {
4+
const bbox = { south: 40.0, west: -74.0, north: 40.02, east: -73.98 };
5+
6+
it('tilesForBBox covers the requested area', () => {
7+
const tiles = tilesForBBox(bbox);
8+
expect(tiles.length).toBeGreaterThan(0);
9+
const box = bboxForTiles(tiles);
10+
expect(box.south).toBeLessThanOrEqual(bbox.south);
11+
expect(box.north).toBeGreaterThanOrEqual(bbox.north);
12+
expect(box.west).toBeLessThanOrEqual(bbox.west);
13+
expect(box.east).toBeGreaterThanOrEqual(bbox.east);
14+
});
15+
16+
it('missingTiles excludes already-fetched tiles', () => {
17+
const all = tilesForBBox(bbox);
18+
const fetched = new Set(all.slice(0, Math.floor(all.length / 2)).map(tileKey));
19+
const missing = missingTiles(bbox, fetched);
20+
expect(missing.length).toBe(all.length - fetched.size);
21+
for (const t of missing) expect(fetched.has(tileKey(t))).toBe(false);
22+
});
23+
24+
it('missingTiles returns empty once every tile is fetched', () => {
25+
const all = tilesForBBox(bbox);
26+
const fetched = new Set(all.map(tileKey));
27+
expect(missingTiles(bbox, fetched)).toEqual([]);
28+
});
29+
30+
it('a pan into an adjacent area only requests the new tiles', () => {
31+
const original = tilesForBBox(bbox);
32+
const fetched = new Set(original.map(tileKey));
33+
34+
const panned = { south: 40.0, west: -74.0 + ROAD_TILE, north: 40.02, east: -73.98 + ROAD_TILE };
35+
const missing = missingTiles(panned, fetched);
36+
37+
expect(missing.length).toBeGreaterThan(0);
38+
expect(missing.length).toBeLessThan(tilesForBBox(panned).length);
39+
});
40+
41+
it('bboxForTiles returns the bounding box of the given tiles', () => {
42+
const tiles = [{ ty: 1, tx: 2 }, { ty: 3, tx: 4 }];
43+
expect(bboxForTiles(tiles)).toEqual({
44+
south: 1 * ROAD_TILE,
45+
north: 4 * ROAD_TILE,
46+
west: 2 * ROAD_TILE,
47+
east: 5 * ROAD_TILE,
48+
});
49+
});
50+
});

‎lib/roadTiles.ts‎

Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,37 @@
1+
export const ROAD_TILE = 0.01; // ~1.1km cells
2+
export const ROAD_TILE_BUFFER = 0.002;
3+
4+
export interface BBox { south: number; west: number; north: number; east: number }
5+
export interface TileCoord { ty: number; tx: number }
6+
7+
// Tiles covering a bbox (plus a small buffer), at the ROAD_TILE grid.
8+
export function tilesForBBox(bbox: BBox): TileCoord[] {
9+
const minTy = Math.floor((bbox.south - ROAD_TILE_BUFFER) / ROAD_TILE);
10+
const maxTy = Math.ceil((bbox.north + ROAD_TILE_BUFFER) / ROAD_TILE);
11+
const minTx = Math.floor((bbox.west - ROAD_TILE_BUFFER) / ROAD_TILE);
12+
const maxTx = Math.ceil((bbox.east + ROAD_TILE_BUFFER) / ROAD_TILE);
13+
14+
const tiles: TileCoord[] = [];
15+
for (let ty = minTy; ty < maxTy; ty++) {
16+
for (let tx = minTx; tx < maxTx; tx++) tiles.push({ ty, tx });
17+
}
18+
return tiles;
19+
}
20+
21+
export const tileKey = (t: TileCoord) => `${t.ty},${t.tx}`;
22+
23+
// Given a set of already-fetched tile keys, return the tiles in bbox that
24+
// still need fetching.
25+
export function missingTiles(bbox: BBox, fetched: Set<string>): TileCoord[] {
26+
return tilesForBBox(bbox).filter(t => !fetched.has(tileKey(t)));
27+
}
28+
29+
// Smallest bbox (on the tile grid) covering a set of tiles.
30+
export function bboxForTiles(tiles: TileCoord[]): BBox {
31+
return {
32+
south: Math.min(...tiles.map(t => t.ty)) * ROAD_TILE,
33+
north: Math.max(...tiles.map(t => t.ty + 1)) * ROAD_TILE,
34+
west: Math.min(...tiles.map(t => t.tx)) * ROAD_TILE,
35+
east: Math.max(...tiles.map(t => t.tx + 1)) * ROAD_TILE,
36+
};
37+
}

‎package-lock.json‎

Lines changed: 2 additions & 2 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

‎package.json‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
{
22
"name": "street-sweep",
3-
"version": "0.6.44",
3+
"version": "0.6.45",
44
"private": true,
55
"scripts": {
66
"dev": "next dev -p 3888",

0 commit comments

Comments
 (0)