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:
@@ -24,6 +24,8 @@ console = Console()
|
||||
help="Strava API client ID (enables Strava sync in the UI). Also reads STRAVA_CLIENT_ID env var.")
|
||||
@click.option("--strava-client-secret", default=None, envvar="STRAVA_CLIENT_SECRET",
|
||||
help="Strava API client secret. Also reads STRAVA_CLIENT_SECRET env var.")
|
||||
@click.option("--dem-url", default=None, envvar="DEM_URL",
|
||||
help="Base URL of an Open-Elevation-compatible API (enables 'Recalculate elevation' button).")
|
||||
def edit(
|
||||
data_dir: Optional[str],
|
||||
port: int,
|
||||
@@ -31,6 +33,7 @@ def edit(
|
||||
config_path: Optional[str],
|
||||
strava_client_id: Optional[str],
|
||||
strava_client_secret: Optional[str],
|
||||
dem_url: Optional[str],
|
||||
) -> None:
|
||||
"""Start a local web UI for editing activity sidecar files.
|
||||
|
||||
@@ -69,11 +72,16 @@ def edit(
|
||||
srv.site_url = site_url
|
||||
srv.strava_client_id = strava_client_id or ""
|
||||
srv.strava_client_secret = strava_client_secret or ""
|
||||
srv.dem_url = dem_url or ""
|
||||
|
||||
if strava_client_id:
|
||||
console.print(f"Strava sync: [green]enabled[/green] (client {strava_client_id})")
|
||||
else:
|
||||
console.print("Strava sync: [yellow]disabled[/yellow] (pass --strava-client-id to enable)")
|
||||
if dem_url:
|
||||
console.print(f"DEM: [green]enabled[/green] ({dem_url})")
|
||||
else:
|
||||
console.print("DEM: [yellow]disabled[/yellow] (pass --dem-url to enable elevation recalculation)")
|
||||
|
||||
uvicorn.run(srv.app, host="127.0.0.1", port=port, log_level="warning")
|
||||
|
||||
|
||||
@@ -20,6 +20,7 @@ data_dir: Path | None = None
|
||||
site_url: str = "http://localhost:4321"
|
||||
strava_client_id: str = ""
|
||||
strava_client_secret: str = ""
|
||||
dem_url: str = "" # Open-Elevation-compatible API base URL; empty = feature disabled
|
||||
|
||||
# In-memory CSRF state tokens for OAuth flows (token → True); cleared after use
|
||||
_oauth_states: set[str] = set()
|
||||
@@ -427,6 +428,32 @@ async def save_activity(activity_id: str, payload: dict[str, Any]) -> JSONRespon
|
||||
return JSONResponse({"ok": True, "sidecar": str(sidecar_path)})
|
||||
|
||||
|
||||
@app.post("/api/activity/{activity_id}/recalculate-elevation")
|
||||
async def recalculate_elevation_endpoint(activity_id: str) -> JSONResponse:
|
||||
"""Replace GPS altitude with DEM terrain elevation and recompute gain/loss.
|
||||
|
||||
Requires --dem-url to be set when starting bincio edit.
|
||||
"""
|
||||
if not dem_url:
|
||||
raise HTTPException(
|
||||
503,
|
||||
"DEM URL not configured. "
|
||||
"Pass --dem-url <api-url> to bincio edit (e.g. https://api.open-elevation.com).",
|
||||
)
|
||||
dd = _get_data_dir()
|
||||
_check_id(activity_id)
|
||||
try:
|
||||
from bincio.extract.dem import recalculate_elevation
|
||||
from bincio.render.merge import merge_one
|
||||
result = recalculate_elevation(dd, activity_id, dem_url)
|
||||
merge_one(dd, activity_id)
|
||||
return JSONResponse(result)
|
||||
except FileNotFoundError as e:
|
||||
raise HTTPException(404, str(e))
|
||||
except ValueError as e:
|
||||
raise HTTPException(422, str(e))
|
||||
|
||||
|
||||
@app.post("/api/activity/{activity_id}/images")
|
||||
async def upload_image(activity_id: str, file: UploadFile = File(...)) -> JSONResponse:
|
||||
dd = _get_data_dir()
|
||||
|
||||
Reference in New Issue
Block a user