from fastapi import APIRouter, BackgroundTasks, HTTPException from pydantic import BaseModel from typing import Optional import asyncio import uuid router = APIRouter() # Predefined regions REGIONS = { "ukraine": { "name": "Ukraine", "bbox": [44.0, 22.0, 52.5, 40.5], # min_lat, min_lon, max_lat, max_lon "srtm_tiles": 120, "estimated_size_gb": 3.0, }, "ukraine_east": { "name": "Eastern Ukraine (Donbas)", "bbox": [47.0, 34.0, 50.5, 40.5], "srtm_tiles": 24, "estimated_size_gb": 0.6, }, "ukraine_central": { "name": "Central Ukraine", "bbox": [48.0, 30.0, 51.0, 36.0], "srtm_tiles": 18, "estimated_size_gb": 0.5, }, "ukraine_west": { "name": "Western Ukraine", "bbox": [48.0, 22.0, 51.0, 26.0], "srtm_tiles": 12, "estimated_size_gb": 0.3, }, "kyiv_region": { "name": "Kyiv Region", "bbox": [49.5, 29.5, 51.5, 32.5], "srtm_tiles": 6, "estimated_size_gb": 0.15, }, } # Download progress tracking (in-memory) _download_tasks: dict[str, dict] = {} class RegionInfo(BaseModel): id: str name: str bbox: list[float] srtm_tiles: int estimated_size_gb: float downloaded: bool = False download_progress: float = 0.0 class DownloadProgress(BaseModel): task_id: str region_id: str status: str # queued, downloading_terrain, downloading_osm, done, error progress: float # 0-100 current_step: str downloaded_mb: float error: Optional[str] = None @router.get("/available") async def list_regions() -> list[RegionInfo]: """List available regions for download""" from app.services.terrain_service import terrain_service cached_tiles = set(terrain_service.get_cached_tiles()) result = [] for region_id, info in REGIONS.items(): min_lat, min_lon, max_lat, max_lon = info["bbox"] needed_tiles = set() for lat in range(int(min_lat), int(max_lat) + 1): for lon in range(int(min_lon), int(max_lon) + 1): tile = terrain_service.get_tile_name(lat, lon) needed_tiles.add(tile) downloaded_tiles = needed_tiles & cached_tiles progress = len(downloaded_tiles) / len(needed_tiles) * 100 if needed_tiles else 0 result.append(RegionInfo( id=region_id, name=info["name"], bbox=info["bbox"], srtm_tiles=info["srtm_tiles"], estimated_size_gb=info["estimated_size_gb"], downloaded=progress >= 100, download_progress=progress )) return result @router.post("/download/{region_id}") async def start_download(region_id: str, background_tasks: BackgroundTasks) -> dict: """Start downloading a region in the background""" if region_id not in REGIONS: raise HTTPException(404, f"Region '{region_id}' not found") # Check if already downloading for task_id, task in _download_tasks.items(): if task["region_id"] == region_id and task["status"] not in ["done", "error"]: return {"task_id": task_id, "status": "already_downloading"} task_id = str(uuid.uuid4())[:8] _download_tasks[task_id] = { "region_id": region_id, "status": "queued", "progress": 0.0, "current_step": "Starting...", "downloaded_mb": 0.0, "error": None } background_tasks.add_task(_download_region_task, task_id, region_id) return {"task_id": task_id, "status": "started"} async def _download_region_task(task_id: str, region_id: str): """Background task to download region data""" from app.services.terrain_service import terrain_service from app.services.buildings_service import buildings_service from app.services.water_service import water_service from app.services.vegetation_service import vegetation_service task = _download_tasks[task_id] region = REGIONS[region_id] min_lat, min_lon, max_lat, max_lon = region["bbox"] try: # Phase 1: Download SRTM tiles (0-70%) task["status"] = "downloading_terrain" task["current_step"] = "Downloading terrain data..." # Count total tiles total_tiles = 0 for lat in range(int(min_lat), int(max_lat) + 1): for lon in range(int(min_lon), int(max_lon) + 1): total_tiles += 1 downloaded_count = 0 for lat in range(int(min_lat), int(max_lat) + 1): for lon in range(int(min_lon), int(max_lon) + 1): tile_name = terrain_service.get_tile_name(lat, lon) await terrain_service.download_tile(tile_name) downloaded_count += 1 task["progress"] = (downloaded_count / total_tiles) * 70.0 task["current_step"] = f"Terrain: {downloaded_count}/{total_tiles} tiles" task["downloaded_mb"] = terrain_service.get_cache_size_mb() # Phase 2: Pre-cache OSM data (70-100%) task["status"] = "downloading_osm" task["current_step"] = "Downloading building data..." total_chunks = 0 for lat in range(int(min_lat), int(max_lat) + 1): for lon in range(int(min_lon), int(max_lon) + 1): total_chunks += 1 done_chunks = 0 for lat in range(int(min_lat), int(max_lat) + 1): for lon in range(int(min_lon), int(max_lon) + 1): chunk_min_lat = float(lat) chunk_min_lon = float(lon) chunk_max_lat = float(lat + 1) chunk_max_lon = float(lon + 1) try: await buildings_service.fetch_buildings( chunk_min_lat, chunk_min_lon, chunk_max_lat, chunk_max_lon ) except Exception as e: print(f"[Region] Buildings chunk error: {e}") try: await water_service.fetch_water_bodies( chunk_min_lat, chunk_min_lon, chunk_max_lat, chunk_max_lon ) except Exception as e: print(f"[Region] Water chunk error: {e}") try: await vegetation_service.fetch_vegetation( chunk_min_lat, chunk_min_lon, chunk_max_lat, chunk_max_lon ) except Exception as e: print(f"[Region] Vegetation chunk error: {e}") done_chunks += 1 task["progress"] = 70 + (done_chunks / total_chunks) * 30 task["current_step"] = f"OSM data: {done_chunks}/{total_chunks} chunks" # Delay to avoid Overpass rate limiting await asyncio.sleep(1.0) task["status"] = "done" task["progress"] = 100.0 task["current_step"] = "Complete!" except Exception as e: task["status"] = "error" task["error"] = str(e) task["current_step"] = f"Error: {e}" @router.get("/download/{task_id}/progress") async def get_download_progress(task_id: str) -> DownloadProgress: """Get download progress for a task""" if task_id not in _download_tasks: raise HTTPException(404, "Task not found") task = _download_tasks[task_id] return DownloadProgress( task_id=task_id, region_id=task["region_id"], status=task["status"], progress=task["progress"], current_step=task["current_step"], downloaded_mb=task["downloaded_mb"], error=task["error"] ) @router.delete("/cache") async def clear_cache() -> dict: """Clear all OSM cached data (keeps SRTM terrain)""" from app.services.buildings_service import buildings_service from app.services.water_service import water_service from app.services.vegetation_service import vegetation_service buildings_service.cache.clear() water_service.cache.clear() vegetation_service.cache.clear() return {"status": "ok", "message": "OSM cache cleared"} @router.get("/cache/stats") async def get_cache_stats() -> dict: """Get cache statistics""" from app.services.terrain_service import terrain_service from app.services.buildings_service import buildings_service from app.services.water_service import water_service from app.services.vegetation_service import vegetation_service return { "terrain_mb": round(terrain_service.get_cache_size_mb(), 2), "terrain_tiles": len(terrain_service.get_cached_tiles()), "buildings_mb": round(buildings_service.cache.get_size_mb(), 2), "water_mb": round(water_service.cache.get_size_mb(), 2), "vegetation_mb": round(vegetation_service.cache.get_size_mb(), 2), }