Files
rfcp/docs/devlog/gpu_supp/RFCP-3.9.1-Terra-Integration.md
2026-02-07 12:56:25 +02:00

247 lines
8.3 KiB
Markdown

# RFCP — Iteration 3.9.1: Terra Tile Server Integration
## Overview
Connect terrain_service.py to our SRTM tile server (terra.eliah.one) as primary download source, add terrain status API endpoint, and create a bulk pre-download utility. The `data/terrain/` directory already exists.
## Context
- terra.eliah.one is live and serving tiles via Caddy file_server
- SRTM3 (90m): 187 tiles, 515 MB — full Ukraine coverage (N44-N51, E018-E041)
- SRTM1 (30m): 160 tiles, 3.9 GB — same coverage area
- terrain_service.py already has bilinear interpolation (3.9.0)
- Backend runs on Windows with RTX 4060, tiles stored locally in `data/terrain/`
- Server is download source, NOT used during realtime calculations
## Changes Required
### 1. Update SRTM_SOURCES in terrain_service.py
**File:** `backend/app/services/terrain_service.py`
Replace current SRTM_SOURCES (lines 22-25):
```python
SRTM_SOURCES = [
"https://elevation-tiles-prod.s3.amazonaws.com/skadi/{lat_dir}/{tile_name}.hgt.gz",
"https://s3.amazonaws.com/elevation-tiles-prod/skadi/{lat_dir}/{tile_name}.hgt.gz",
]
```
With prioritized source list:
```python
SRTM_SOURCES = [
# Our tile server — SRTM1 (30m) preferred, uncompressed
{
"url": "https://terra.eliah.one/srtm1/{tile_name}.hgt",
"compressed": False,
"resolution": "srtm1",
},
# Our tile server — SRTM3 (90m) fallback
{
"url": "https://terra.eliah.one/srtm3/{tile_name}.hgt",
"compressed": False,
"resolution": "srtm3",
},
# Public AWS mirror — SRTM1, gzip compressed
{
"url": "https://elevation-tiles-prod.s3.amazonaws.com/skadi/{lat_dir}/{tile_name}.hgt.gz",
"compressed": True,
"resolution": "srtm1",
},
]
```
Update `download_tile()` to handle the new source format:
```python
async def download_tile(self, tile_name: str) -> bool:
"""Download SRTM tile from configured sources, preferring highest resolution."""
tile_path = self.get_tile_path(tile_name)
if tile_path.exists():
return True
lat_dir = tile_name[:3] # e.g., "N48"
async with httpx.AsyncClient(timeout=60.0, follow_redirects=True) as client:
for source in self.SRTM_SOURCES:
url = source["url"].format(lat_dir=lat_dir, tile_name=tile_name)
try:
response = await client.get(url)
if response.status_code == 200:
data = response.content
# Skip empty responses
if len(data) < 1000:
continue
if source["compressed"]:
if url.endswith('.gz'):
data = gzip.decompress(data)
elif url.endswith('.zip'):
with zipfile.ZipFile(io.BytesIO(data)) as zf:
for name in zf.namelist():
if name.endswith('.hgt'):
data = zf.read(name)
break
# Validate tile size
if len(data) not in (3601 * 3601 * 2, 1201 * 1201 * 2):
print(f"[Terrain] Invalid tile size {len(data)} from {url}")
continue
tile_path.write_bytes(data)
res = source["resolution"]
size_mb = len(data) / 1048576
print(f"[Terrain] Downloaded {tile_name} ({res}, {size_mb:.1f} MB)")
return True
except Exception as e:
print(f"[Terrain] Failed from {url}: {e}")
continue
print(f"[Terrain] Could not download {tile_name} from any source")
return False
```
### 2. Add Terrain Status API Endpoint
**File:** `backend/app/api/routes.py` (or wherever API routes are defined)
Add a new endpoint:
```python
@router.get("/api/terrain/status")
async def terrain_status():
"""Return terrain data availability info."""
from app.services.terrain_service import terrain_service
cached_tiles = terrain_service.get_cached_tiles()
cache_size = terrain_service.get_cache_size_mb()
# Categorize by resolution
srtm1_tiles = [t for t in cached_tiles
if (terrain_service.terrain_path / f"{t}.hgt").stat().st_size == 3601 * 3601 * 2]
srtm3_tiles = [t for t in cached_tiles if t not in srtm1_tiles]
return {
"total_tiles": len(cached_tiles),
"srtm1": {
"count": len(srtm1_tiles),
"resolution_m": 30,
"tiles": sorted(srtm1_tiles),
},
"srtm3": {
"count": len(srtm3_tiles),
"resolution_m": 90,
"tiles": sorted(srtm3_tiles),
},
"cache_size_mb": round(cache_size, 1),
"memory_cached": len(terrain_service._tile_cache),
"terra_server": "https://terra.eliah.one",
}
```
### 3. Add Bulk Pre-Download Endpoint
**File:** Same routes file
```python
@router.post("/api/terrain/download")
async def terrain_download(request: dict):
"""Pre-download tiles for a region.
Body: {"center_lat": 48.46, "center_lon": 35.04, "radius_km": 50}
Or: {"tiles": ["N48E034", "N48E035", "N47E034", "N47E035"]}
"""
from app.services.terrain_service import terrain_service
if "tiles" in request:
tile_list = request["tiles"]
else:
center_lat = request.get("center_lat", 48.46)
center_lon = request.get("center_lon", 35.04)
radius_km = request.get("radius_km", 50)
tile_list = terrain_service.get_required_tiles(center_lat, center_lon, radius_km)
missing = [t for t in tile_list if not terrain_service.get_tile_path(t).exists()]
if not missing:
return {"status": "ok", "message": "All tiles already cached", "count": len(tile_list)}
# Download missing tiles
downloaded = []
failed = []
for tile_name in missing:
success = await terrain_service.download_tile(tile_name)
if success:
downloaded.append(tile_name)
else:
failed.append(tile_name)
return {
"status": "ok",
"required": len(tile_list),
"already_cached": len(tile_list) - len(missing),
"downloaded": downloaded,
"failed": failed,
}
```
### 4. Add Tile Index Endpoint
**File:** Same routes file
```python
@router.get("/api/terrain/index")
async def terrain_index():
"""Fetch tile index from terra server."""
import httpx
try:
async with httpx.AsyncClient(timeout=10.0) as client:
resp = await client.get("https://terra.eliah.one/api/index")
if resp.status_code == 200:
return resp.json()
except Exception:
pass
return {"error": "Could not reach terra.eliah.one", "offline": True}
```
## Testing Checklist
- [ ] `GET /api/terrain/status` returns tile counts and sizes
- [ ] `POST /api/terrain/download {"center_lat": 48.46, "center_lon": 35.04, "radius_km": 10}` downloads missing tiles from terra.eliah.one
- [ ] Tiles downloaded from terra are valid HGT format (2,884,802 or 25,934,402 bytes)
- [ ] SRTM1 is preferred over SRTM3 when downloading
- [ ] Existing tiles are not re-downloaded
- [ ] Coverage calculation works with terrain data (test with Dnipro coordinates)
- [ ] `GET /api/terrain/index` returns terra server tile list
## Build & Deploy
```bash
cd D:\root\rfcp\backend
# No build needed — Python backend, just restart
# Kill existing uvicorn and restart:
python -m uvicorn app.main:app --host 0.0.0.0 --port 8000 --reload
```
## Commit Message
```
feat(terrain): integrate terra.eliah.one tile server
- Add terra.eliah.one as primary SRTM source (SRTM1 30m preferred)
- Keep AWS S3 as fallback source
- Add /api/terrain/status endpoint (tile inventory)
- Add /api/terrain/download endpoint (bulk pre-download)
- Add /api/terrain/index endpoint (terra server index)
- Validate tile size before saving
- Add follow_redirects=True to httpx client
```
## Success Criteria
1. terrain_service downloads from terra.eliah.one first
2. /api/terrain/status shows correct tile counts by resolution
3. /api/terrain/download fetches tiles for any Ukrainian coordinate
4. Offline mode works — no downloads attempted if tiles exist locally
5. Coverage calculation uses real elevation data instead of flat terrain