feat: DEM-based elevation recalculation via edit drawer button

Adds a "Recalculate from terrain map (DEM)" button to the activity edit
drawer. On click it queries an Open-Elevation-compatible API to replace
GPS altitude with SRTM terrain data, applies 5m hysteresis, and updates
the activity's elevation stats and timeseries chart in place.

- bincio/extract/dem.py: lookup_elevations() (batched HTTP POST) +
  recalculate_elevation() (subsample → DEM → interpolate → hysteresis →
  patch activity JSON, timeseries JSON, index.json)
- POST /api/activity/{id}/recalculate-elevation on both serve and edit
  servers; serve endpoint is auth-gated and triggers merge + rebuild
- --dem-url flag (also DEM_URL env var) on bincio serve and bincio edit;
  logged at startup; missing URL returns a clear 503 with setup instructions
- /api/me response gains dem_configured bool
- EditDrawer: button with loading state, shows new ↑/↓ values on success
This commit is contained in:
Davide Scaini
2026-04-20 20:45:06 +02:00
parent 872651f471
commit 1940e2409b
6 changed files with 340 additions and 1 deletions
+43
View File
@@ -24,6 +24,11 @@
let confirmDelete = false;
let deleting = false;
// Elevation recalculation from DEM
let recalculating = false;
let recalcStatus = '';
let recalcOk = false;
// Form state
let title = '';
let sport: Sport = 'cycling';
@@ -119,6 +124,26 @@
: [...hideStats, key];
}
async function recalculateElevation() {
recalculating = true;
recalcStatus = '';
recalcOk = false;
try {
const res = await fetch(`${api}/recalculate-elevation`, { method: 'POST' });
const d = await res.json();
if (!res.ok) throw new Error(d.detail ?? await res.text());
recalcOk = true;
const gain = d.elevation_gain_m != null ? `↑ ${Math.round(d.elevation_gain_m)} m` : '';
const loss = d.elevation_loss_m != null ? `↓ ${Math.round(d.elevation_loss_m)} m` : '';
recalcStatus = [gain, loss].filter(Boolean).join(' ');
} catch (e: any) {
recalcStatus = e.message;
recalcOk = false;
} finally {
recalculating = false;
}
}
async function deleteActivity() {
if (!confirmDelete) { confirmDelete = true; return; }
deleting = true;
@@ -264,6 +289,24 @@
</div>
</div>
<!-- Elevation recalculation -->
<div class="mb-4">
<p class="text-xs text-zinc-500 mb-2">Elevation</p>
<button
type="button"
class="w-full flex items-center justify-center gap-2 px-3 py-2 rounded-lg border border-zinc-700 text-xs text-zinc-400 hover:border-zinc-500 hover:text-zinc-200 transition-colors disabled:opacity-40"
disabled={recalculating}
on:click={recalculateElevation}
>
{recalculating ? 'Querying terrain data…' : '⛰ Recalculate from terrain map (DEM)'}
</button>
{#if recalcStatus}
<p class="text-xs mt-1.5 text-center" class:text-green-400={recalcOk} class:text-red-400={!recalcOk}>
{recalcStatus}
</p>
{/if}
</div>
<!-- Flags -->
<div class="flex gap-3 mb-2">
<button