@mytec: stack done, rust next
This commit is contained in:
246
docs/devlog/gpu_supp/RFCP-3.9.1-Terra-Integration.md
Normal file
246
docs/devlog/gpu_supp/RFCP-3.9.1-Terra-Integration.md
Normal file
@@ -0,0 +1,246 @@
|
||||
# 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
|
||||
Reference in New Issue
Block a user