263 lines
8.7 KiB
Python
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),
|
|
}
|