Files
rfcp/backend/app/api/routes/regions.py

263 lines
8.7 KiB
Python

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),
}