@mytec: iter2.2 ready for testing
This commit is contained in:
256
backend/app/api/routes/regions.py
Normal file
256
backend/app/api/routes/regions.py
Normal file
@@ -0,0 +1,256 @@
|
||||
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,
|
||||
},
|
||||
"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),
|
||||
}
|
||||
Reference in New Issue
Block a user