Files
bincio-activity/bincio/edit/cli.py
T
Davide Scaini 1940e2409b 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
2026-04-20 20:45:06 +02:00

115 lines
4.3 KiB
Python

"""bincio edit — local edit server for activity sidecar files."""
from __future__ import annotations
from pathlib import Path
from typing import Optional
import click
from rich.console import Console
console = Console()
@click.command()
@click.option("--data-dir", default=None,
help="BAS data store directory (output of bincio extract).")
@click.option("--port", default=4041, show_default=True,
help="Port for the edit server.")
@click.option("--site-url", default="http://localhost:4321", show_default=True,
help="URL of the Astro dev server (for the Back link).")
@click.option("--config", "config_path", default=None,
help="Path to extract_config.yaml (reads output.dir from it).")
@click.option("--strava-client-id", default=None, envvar="STRAVA_CLIENT_ID",
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,
site_url: str,
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.
Writes sidecar .md files to <data-dir>/edits/ which bincio render picks
up and applies at build time.
Run alongside the Astro dev server:
\b
bincio render --serve # port 4321 (or npm run dev)
bincio edit # port 4041
"""
try:
import uvicorn
except ImportError:
raise click.ClickException(
"uvicorn is required for the edit server.\n"
"Install with: uv sync --extra edit"
)
data = _resolve_data_dir(data_dir, config_path)
# Fall back to extract_config.yaml for Strava credentials
if not strava_client_id or not strava_client_secret:
cfg_strava = _load_config(config_path).get("import", {}).get("strava", {})
strava_client_id = strava_client_id or str(cfg_strava.get("client_id") or "")
strava_client_secret = strava_client_secret or str(cfg_strava.get("client_secret") or "")
console.print(f"Data dir: [cyan]{data}[/cyan]")
console.print(f"Edit UI: [cyan]http://localhost:{port}/edit/<activity-id>[/cyan]")
console.print(f"Site URL: [cyan]{site_url}[/cyan]")
console.print("Press [bold]Ctrl+C[/bold] to stop.\n")
import bincio.edit.server as srv
srv.data_dir = data
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")
def _load_config(config_path: Optional[str]) -> dict:
"""Load extract_config.yaml — explicit path first, then cwd auto-discovery."""
import yaml
for cfg in filter(None, [config_path and Path(config_path), Path("extract_config.yaml")]):
if Path(cfg).exists():
return yaml.safe_load(Path(cfg).read_text()) or {}
return {}
def _resolve_data_dir(explicit: Optional[str], config_path: Optional[str]) -> Path:
if explicit:
return Path(explicit).expanduser().resolve()
raw = _load_config(config_path)
out = raw.get("output", {}).get("dir")
if out:
return Path(out).expanduser().resolve()
default = Path.cwd() / "bincio_data"
if default.exists():
return default
raise click.UsageError(
"Could not find the BAS data directory. "
"Run `bincio extract` first, or pass --data-dir."
)