# 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