explore: shard tracks into per-year files for progressive loading
bake_tracks now writes tracks_YYYY.json shards + tracks_index.json manifest
instead of a single monolithic tracks.json. API /api/me/tracks returns the
manifest; /api/me/tracks/{year} serves individual shards. Explore.svelte
fetches the two most recent years eagerly then streams the rest in the
background so the map renders immediately with recent data.
This commit is contained in:
@@ -15,6 +15,9 @@
|
||||
|
||||
let tracks: Track[] = [];
|
||||
let loading = true;
|
||||
let loadingYears = false; // background years still fetching
|
||||
let loadedCount = 0; // years fetched so far
|
||||
let totalYears = 0; // total years in manifest
|
||||
let error = '';
|
||||
|
||||
// Filters
|
||||
@@ -69,8 +72,8 @@
|
||||
$: allTypes = [...new Set(tracks.map(t => t.type))].sort();
|
||||
$: availableYears = [...new Set(tracks.map(t => t.date.slice(0,4)).filter(Boolean))].sort().reverse();
|
||||
|
||||
let typesInitialized = false;
|
||||
$: if (allTypes.length > 0 && !typesInitialized) { selectedTypes = new Set(allTypes); typesInitialized = true; }
|
||||
// Auto-select any type that hasn't been seen before (handles progressive loading)
|
||||
$: { const s = new Set(selectedTypes); let changed = false; for (const t of allTypes) { if (!s.has(t)) { s.add(t); changed = true; } } if (changed) selectedTypes = s; }
|
||||
|
||||
$: filteredTracks = tracks.filter(t => {
|
||||
if (!selectedTypes.has(t.type)) return false;
|
||||
@@ -199,15 +202,59 @@
|
||||
map.fitBounds([[w,s],[e,n]], { padding: 40, maxZoom: 14 });
|
||||
}
|
||||
|
||||
async function _loadYears(years: string[]) {
|
||||
for (const year of years) {
|
||||
try {
|
||||
const r = await fetch(`/api/me/tracks/${year}`, { credentials: 'include' });
|
||||
if (!r.ok) continue;
|
||||
const d = await r.json();
|
||||
const newTracks: Track[] = d.tracks ?? [];
|
||||
if (newTracks.length > 0) {
|
||||
tracks = [...tracks, ...newTracks];
|
||||
// reactive $: statement picks up the tracks change and updates the map
|
||||
}
|
||||
} catch { /* non-fatal — skip year */ }
|
||||
loadedCount += 1;
|
||||
}
|
||||
loadingYears = false;
|
||||
}
|
||||
|
||||
onMount(async () => {
|
||||
// Fetch manifest to discover available years
|
||||
let years: string[] = [];
|
||||
try {
|
||||
const r = await fetch('/api/me/tracks', { credentials: 'include' });
|
||||
if (!r.ok) { error = r.status === 404 ? 'No tracks baked yet — upload activities first.' : `Error ${r.status}`; loading = false; return; }
|
||||
const d = await r.json();
|
||||
tracks = d.tracks ?? [];
|
||||
years = d.years ?? [];
|
||||
} catch (e: any) { error = e.message ?? 'Failed to load'; loading = false; return; }
|
||||
|
||||
if (years.length === 0) { loading = false; return; }
|
||||
|
||||
totalYears = years.length;
|
||||
|
||||
// Load the two most recent years before showing the map
|
||||
const eager = years.slice(0, 2);
|
||||
const lazy = years.slice(2);
|
||||
|
||||
for (const year of eager) {
|
||||
try {
|
||||
const r = await fetch(`/api/me/tracks/${year}`, { credentials: 'include' });
|
||||
if (r.ok) {
|
||||
const d = await r.json();
|
||||
tracks = [...tracks, ...(d.tracks ?? [])];
|
||||
}
|
||||
} catch { /* skip */ }
|
||||
loadedCount += 1;
|
||||
}
|
||||
loading = false;
|
||||
|
||||
// Stream remaining years in the background
|
||||
if (lazy.length > 0) {
|
||||
loadingYears = true;
|
||||
_loadYears(lazy);
|
||||
}
|
||||
|
||||
map = new maplibregl.Map({
|
||||
container: mapEl,
|
||||
style: { version: 8,
|
||||
@@ -364,6 +411,7 @@
|
||||
</section>
|
||||
|
||||
{#if loading}<p class="status">Loading tracks…</p>{/if}
|
||||
{#if !loading && loadingYears}<p class="status">Loading history… {loadedCount}/{totalYears}</p>{/if}
|
||||
{#if error}<p class="status error">{error}</p>{/if}
|
||||
</aside>
|
||||
|
||||
|
||||
Reference in New Issue
Block a user