Spaces:
Running
Running
Update api.py
Browse files
api.py
CHANGED
|
@@ -57,6 +57,8 @@ from src.live_data_fetcher import (
|
|
| 57 |
# In api.py β update the imports at the top
|
| 58 |
from datetime import datetime
|
| 59 |
from pydantic import BaseModel
|
|
|
|
|
|
|
| 60 |
|
| 61 |
|
| 62 |
|
|
@@ -261,10 +263,207 @@ def prediction_result_to_dict(result: PredictionResult) -> dict:
|
|
| 261 |
"feature_memberships": result.feature_memberships,
|
| 262 |
}
|
| 263 |
class EvacuationRequest(BaseModel):
|
| 264 |
-
latitude:
|
| 265 |
-
longitude:
|
| 266 |
-
|
| 267 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 268 |
|
| 269 |
|
| 270 |
# ============================================================================
|
|
@@ -869,129 +1068,96 @@ def flood_map_from_geojson(req: GeoJSONMapRequest):
|
|
| 869 |
|
| 870 |
|
| 871 |
@app.post("/evacuate/route")
|
| 872 |
-
|
| 873 |
"""
|
| 874 |
-
1. Takes user lat/lon
|
| 875 |
-
2.
|
| 876 |
-
3.
|
| 877 |
-
4.
|
|
|
|
| 878 |
"""
|
|
|
|
|
|
|
| 879 |
|
| 880 |
-
# Step 1: Build flood features
|
| 881 |
-
|
| 882 |
-
|
| 883 |
-
|
| 884 |
-
|
| 885 |
-
"
|
| 886 |
-
|
| 887 |
-
|
| 888 |
-
|
| 889 |
-
|
| 890 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 891 |
|
| 892 |
-
# Step 2:
|
| 893 |
errors = flood_predictor.validate_input(features)
|
| 894 |
if errors:
|
| 895 |
raise HTTPException(422, {"validation_errors": errors})
|
| 896 |
|
| 897 |
-
result
|
| 898 |
risk_score = result.risk_score
|
| 899 |
risk_tier = result.risk_tier.value
|
| 900 |
|
| 901 |
-
# Step 3:
|
| 902 |
safe_zones = get_nearest_safe_zones(req.latitude, req.longitude)
|
| 903 |
|
| 904 |
-
# Step 4:
|
| 905 |
if risk_score < 0.45:
|
| 906 |
return {
|
| 907 |
-
"latitude":
|
| 908 |
-
"longitude":
|
| 909 |
-
"
|
| 910 |
-
"
|
|
|
|
|
|
|
| 911 |
"evacuation_needed": False,
|
| 912 |
-
"message":
|
| 913 |
-
"
|
| 914 |
-
"
|
|
|
|
|
|
|
|
|
|
| 915 |
}
|
| 916 |
|
| 917 |
-
# Step 5:
|
| 918 |
nearest_zone = safe_zones[0]
|
| 919 |
route = dijkstra_route(
|
| 920 |
start_lat=req.latitude,
|
| 921 |
start_lon=req.longitude,
|
| 922 |
end_lat=nearest_zone["lat"],
|
| 923 |
-
end_lon=nearest_zone["lon"]
|
| 924 |
)
|
| 925 |
|
| 926 |
return {
|
| 927 |
"latitude": req.latitude,
|
| 928 |
"longitude": req.longitude,
|
| 929 |
-
"
|
|
|
|
| 930 |
"risk_tier": risk_tier,
|
| 931 |
-
"uncertainty": result.uncertainty,
|
|
|
|
|
|
|
|
|
|
|
|
|
| 932 |
"evacuation_needed": True,
|
|
|
|
|
|
|
|
|
|
| 933 |
"nearest_safe_zone": nearest_zone,
|
| 934 |
"all_safe_zones": safe_zones,
|
| 935 |
"route": route,
|
| 936 |
-
"timestamp": datetime.now().isoformat()
|
| 937 |
-
}
|
| 938 |
-
|
| 939 |
-
|
| 940 |
-
# ββ Helper: Safe zones near India βββββββββββββββββββββββββββββββββββββββββ
|
| 941 |
-
def get_nearest_safe_zones(lat: float, lon: float) -> list:
|
| 942 |
-
import math
|
| 943 |
-
|
| 944 |
-
safe_zones = [
|
| 945 |
-
{"name": "Government Relief Camp β Tambaram", "lat": 12.9249, "lon": 80.1000, "capacity": 500},
|
| 946 |
-
{"name": "Flood Shelter β Anna Nagar", "lat": 13.0850, "lon": 80.2101, "capacity": 800},
|
| 947 |
-
{"name": "NDRF Base β Sholinganallur", "lat": 12.9010, "lon": 80.2279, "capacity": 300},
|
| 948 |
-
{"name": "Community Hall β Velachery", "lat": 12.9815, "lon": 80.2180, "capacity": 400},
|
| 949 |
-
{"name": "Higher Ground β Guindy", "lat": 13.0067, "lon": 80.2206, "capacity": 600},
|
| 950 |
-
]
|
| 951 |
-
|
| 952 |
-
def haversine(la1, lo1, la2, lo2):
|
| 953 |
-
R = 6371
|
| 954 |
-
dlat = math.radians(la2 - la1)
|
| 955 |
-
dlon = math.radians(lo2 - lo1)
|
| 956 |
-
a = math.sin(dlat/2)*2 + math.cos(math.radians(la1)) * math.cos(math.radians(la2)) * math.sin(dlon/2)*2
|
| 957 |
-
return R * 2 * math.atan2(math.sqrt(a), math.sqrt(1-a))
|
| 958 |
-
|
| 959 |
-
for zone in safe_zones:
|
| 960 |
-
zone["distance_km"] = round(haversine(lat, lon, zone["lat"], zone["lon"]), 2)
|
| 961 |
-
|
| 962 |
-
return sorted(safe_zones, key=lambda z: z["distance_km"])
|
| 963 |
-
|
| 964 |
-
|
| 965 |
-
# ββ Helper: Simple Dijkstra ββββββββββββββββββββββββββββββββββββββββββββββββ
|
| 966 |
-
def dijkstra_route(start_lat, start_lon, end_lat, end_lon) -> dict:
|
| 967 |
-
import math
|
| 968 |
-
|
| 969 |
-
def haversine(la1, lo1, la2, lo2):
|
| 970 |
-
R = 6371
|
| 971 |
-
dlat = math.radians(la2 - la1)
|
| 972 |
-
dlon = math.radians(lo2 - lo1)
|
| 973 |
-
a = math.sin(dlat/2)*2 + math.cos(math.radians(la1)) * math.cos(math.radians(la2)) * math.sin(dlon/2)*2
|
| 974 |
-
return R * 2 * math.atan2(math.sqrt(a), math.sqrt(1-a))
|
| 975 |
-
|
| 976 |
-
distance_km = haversine(start_lat, start_lon, end_lat, end_lon)
|
| 977 |
-
eta_minutes = round((distance_km / 40) * 60) # assume 40 km/h
|
| 978 |
-
|
| 979 |
-
return {
|
| 980 |
-
"algorithm": "Dijkstra Shortest Path",
|
| 981 |
-
"start": {"lat": start_lat, "lon": start_lon},
|
| 982 |
-
"end": {"lat": end_lat, "lon": end_lon},
|
| 983 |
-
"distance_km": round(distance_km, 2),
|
| 984 |
-
"eta_minutes": eta_minutes,
|
| 985 |
-
"waypoints": [
|
| 986 |
-
[start_lat, start_lon],
|
| 987 |
-
[end_lat, end_lon]
|
| 988 |
-
],
|
| 989 |
-
"instructions": [
|
| 990 |
-
f"Head towards safe zone ({distance_km:.1f} km away)",
|
| 991 |
-
"Avoid low-lying roads and underpasses",
|
| 992 |
-
"Follow elevated roads",
|
| 993 |
-
"Arrive at safe zone"
|
| 994 |
-
]
|
| 995 |
}
|
| 996 |
|
| 997 |
# ============================================================================
|
|
|
|
| 57 |
# In api.py β update the imports at the top
|
| 58 |
from datetime import datetime
|
| 59 |
from pydantic import BaseModel
|
| 60 |
+
from typing import Optional, Dict
|
| 61 |
+
import math
|
| 62 |
|
| 63 |
|
| 64 |
|
|
|
|
| 263 |
"feature_memberships": result.feature_memberships,
|
| 264 |
}
|
| 265 |
class EvacuationRequest(BaseModel):
|
| 266 |
+
latitude: float = Field(..., ge=5.0, le=37.0, example=19.0760)
|
| 267 |
+
longitude: float = Field(..., ge=68.0, le=97.0, example=72.8777)
|
| 268 |
+
flood_features: Optional[Dict[str, float]] = Field(
|
| 269 |
+
None,
|
| 270 |
+
description="Optional β if not provided, features are auto-derived from lat/lon"
|
| 271 |
+
)
|
| 272 |
+
n_mc_samples: int = Field(50, ge=1, le=200)
|
| 273 |
+
|
| 274 |
+
SAFE_ZONES_DB = {
|
| 275 |
+
"Mumbai": [
|
| 276 |
+
{"name": "NDRF Camp β Bandra Kurla Complex", "lat": 19.0596, "lon": 72.8656, "capacity": 1000},
|
| 277 |
+
{"name": "Elevated Shelter β Sion", "lat": 19.0390, "lon": 72.8619, "capacity": 800},
|
| 278 |
+
{"name": "Relief Centre β Powai", "lat": 19.1197, "lon": 72.9058, "capacity": 600},
|
| 279 |
+
{"name": "Higher Ground β Vikhroli", "lat": 19.1086, "lon": 72.9300, "capacity": 500},
|
| 280 |
+
],
|
| 281 |
+
"Chennai": [
|
| 282 |
+
{"name": "Government Relief Camp β Tambaram", "lat": 12.9249, "lon": 80.1000, "capacity": 500},
|
| 283 |
+
{"name": "Flood Shelter β Anna Nagar", "lat": 13.0850, "lon": 80.2101, "capacity": 800},
|
| 284 |
+
{"name": "NDRF Base β Sholinganallur", "lat": 12.9010, "lon": 80.2279, "capacity": 300},
|
| 285 |
+
{"name": "Community Hall β Velachery", "lat": 12.9815, "lon": 80.2180, "capacity": 400},
|
| 286 |
+
{"name": "Higher Ground β Guindy", "lat": 13.0067, "lon": 80.2206, "capacity": 600},
|
| 287 |
+
],
|
| 288 |
+
"Kolkata": [
|
| 289 |
+
{"name": "Relief Camp β Salt Lake", "lat": 22.5800, "lon": 88.4100, "capacity": 700},
|
| 290 |
+
{"name": "Elevated Shelter β Ballygunge", "lat": 22.5262, "lon": 88.3639, "capacity": 500},
|
| 291 |
+
{"name": "NDRF Base β Ultadanga", "lat": 22.5900, "lon": 88.3900, "capacity": 600},
|
| 292 |
+
],
|
| 293 |
+
"Delhi": [
|
| 294 |
+
{"name": "Relief Camp β Pragati Maidan", "lat": 28.6187, "lon": 77.2410, "capacity": 2000},
|
| 295 |
+
{"name": "Flood Shelter β Yamuna Sports Complex","lat": 28.6448, "lon": 77.2637, "capacity": 1000},
|
| 296 |
+
{"name": "Higher Ground β Saket", "lat": 28.5244, "lon": 77.2066, "capacity": 800},
|
| 297 |
+
],
|
| 298 |
+
"Hyderabad": [
|
| 299 |
+
{"name": "Relief Camp β Hitec City", "lat": 17.4435, "lon": 78.3772, "capacity": 600},
|
| 300 |
+
{"name": "Shelter β Secunderabad", "lat": 17.4399, "lon": 78.4983, "capacity": 500},
|
| 301 |
+
{"name": "Higher Ground β Banjara Hills", "lat": 17.4156, "lon": 78.4347, "capacity": 400},
|
| 302 |
+
],
|
| 303 |
+
"Bangalore": [
|
| 304 |
+
{"name": "Relief Camp β Whitefield", "lat": 12.9698, "lon": 77.7500, "capacity": 600},
|
| 305 |
+
{"name": "Shelter β Koramangala", "lat": 12.9279, "lon": 77.6271, "capacity": 500},
|
| 306 |
+
{"name": "NDRF Base β Hebbal", "lat": 13.0350, "lon": 77.5970, "capacity": 400},
|
| 307 |
+
],
|
| 308 |
+
"Bhubaneswar": [
|
| 309 |
+
{"name": "Cyclone Shelter β Chandrasekharpur","lat": 20.3200, "lon": 85.8100, "capacity": 800},
|
| 310 |
+
{"name": "Relief Camp β Patia", "lat": 20.3500, "lon": 85.8200, "capacity": 600},
|
| 311 |
+
{"name": "Higher Ground β Nayapalli", "lat": 20.2800, "lon": 85.8100, "capacity": 500},
|
| 312 |
+
],
|
| 313 |
+
"Patna": [
|
| 314 |
+
{"name": "Flood Relief Camp β Gandhi Maidan", "lat": 25.6069, "lon": 85.1348, "capacity": 1500},
|
| 315 |
+
{"name": "Elevated Shelter β Kankarbagh", "lat": 25.5900, "lon": 85.1500, "capacity": 700},
|
| 316 |
+
{"name": "NDRF Base β Danapur", "lat": 25.6200, "lon": 85.0500, "capacity": 500},
|
| 317 |
+
],
|
| 318 |
+
"Guwahati": [
|
| 319 |
+
{"name": "Flood Shelter β Dispur", "lat": 26.1433, "lon": 91.7898, "capacity": 600},
|
| 320 |
+
{"name": "Relief Camp β Jalukbari", "lat": 26.1600, "lon": 91.6900, "capacity": 500},
|
| 321 |
+
{"name": "Higher Ground β Bhangagarh", "lat": 26.1800, "lon": 91.7500, "capacity": 400},
|
| 322 |
+
],
|
| 323 |
+
"Kochi": [
|
| 324 |
+
{"name": "Relief Camp β Kakkanad", "lat": 10.0159, "lon": 76.3419, "capacity": 700},
|
| 325 |
+
{"name": "Flood Shelter β Aluva", "lat": 10.1004, "lon": 76.3570, "capacity": 600},
|
| 326 |
+
{"name": "Higher Ground β Edapally", "lat": 10.0261, "lon": 76.3083, "capacity": 500},
|
| 327 |
+
],
|
| 328 |
+
}
|
| 329 |
+
|
| 330 |
+
def _haversine(la1: float, lo1: float, la2: float, lo2: float) -> float:
|
| 331 |
+
R = 6371
|
| 332 |
+
dlat = math.radians(la2 - la1)
|
| 333 |
+
dlon = math.radians(lo2 - lo1)
|
| 334 |
+
a = (math.sin(dlat / 2) ** 2 +
|
| 335 |
+
math.cos(math.radians(la1)) * math.cos(math.radians(la2)) *
|
| 336 |
+
math.sin(dlon / 2) ** 2)
|
| 337 |
+
return R * 2 * math.atan2(math.sqrt(a), math.sqrt(1 - a))
|
| 338 |
+
|
| 339 |
+
|
| 340 |
+
def _nearest_city(lat: float, lon: float) -> str:
|
| 341 |
+
"""Find which monitored city is closest to the given coordinates."""
|
| 342 |
+
CITY_COORDS = {
|
| 343 |
+
"Mumbai": (19.0760, 72.8777),
|
| 344 |
+
"Chennai": (13.0827, 80.2707),
|
| 345 |
+
"Kolkata": (22.5726, 88.3639),
|
| 346 |
+
"Delhi": (28.6139, 77.2090),
|
| 347 |
+
"Hyderabad": (17.3850, 78.4867),
|
| 348 |
+
"Bangalore": (12.9716, 77.5946),
|
| 349 |
+
"Bhubaneswar": (20.2961, 85.8245),
|
| 350 |
+
"Patna": (25.5941, 85.1376),
|
| 351 |
+
"Guwahati": (26.1445, 91.7362),
|
| 352 |
+
"Kochi": ( 9.9312, 76.2673),
|
| 353 |
+
}
|
| 354 |
+
return min(CITY_COORDS, key=lambda c: _haversine(lat, lon, *CITY_COORDS[c]))
|
| 355 |
+
|
| 356 |
+
|
| 357 |
+
def get_rainfall_for_location(lat: float, lon: float) -> float:
|
| 358 |
+
"""
|
| 359 |
+
Get live rainfall for the nearest monitored city.
|
| 360 |
+
Falls back to 0.0 if IMD APIs are unavailable.
|
| 361 |
+
"""
|
| 362 |
+
city = _nearest_city(lat, lon)
|
| 363 |
+
station_id = {
|
| 364 |
+
"Mumbai": "42941", "Chennai": "43279", "Kolkata": "42809",
|
| 365 |
+
"Delhi": "42182", "Hyderabad": "43128", "Bangalore": "43295",
|
| 366 |
+
"Bhubaneswar": "42971", "Patna": "42492",
|
| 367 |
+
"Guwahati": "42410", "Kochi": "43371",
|
| 368 |
+
}.get(city, "42182")
|
| 369 |
+
|
| 370 |
+
obs = flood_fetcher.fetch_city_rainfall(city, station_id)
|
| 371 |
+
return float(obs.get("rainfall_mm") or 0.0)
|
| 372 |
+
|
| 373 |
+
|
| 374 |
+
def get_elevation_for_location(lat: float, lon: float) -> float:
|
| 375 |
+
"""
|
| 376 |
+
Return elevation for nearest city from static dataset.
|
| 377 |
+
"""
|
| 378 |
+
CITY_ELEVATION = {
|
| 379 |
+
"Mumbai": 11, "Chennai": 6, "Kolkata": 9, "Delhi": 216,
|
| 380 |
+
"Hyderabad": 542, "Bangalore": 920, "Bhubaneswar": 45,
|
| 381 |
+
"Patna": 53, "Guwahati": 55, "Kochi": 3,
|
| 382 |
+
}
|
| 383 |
+
city = _nearest_city(lat, lon)
|
| 384 |
+
return float(CITY_ELEVATION.get(city, 50))
|
| 385 |
+
|
| 386 |
+
|
| 387 |
+
def get_static_features_for_location(lat: float, lon: float) -> dict:
|
| 388 |
+
"""
|
| 389 |
+
Return all static flood features for the nearest monitored city.
|
| 390 |
+
"""
|
| 391 |
+
CITY_STATIC = {
|
| 392 |
+
"Mumbai": {"elevation_m": 11, "drainage_capacity_index": 0.35,
|
| 393 |
+
"flow_accumulation": 0.70, "twi": 12.0, "dist_river": 0.8},
|
| 394 |
+
"Chennai": {"elevation_m": 6, "drainage_capacity_index": 0.40,
|
| 395 |
+
"flow_accumulation": 0.65, "twi": 11.5, "dist_river": 1.2},
|
| 396 |
+
"Kolkata": {"elevation_m": 9, "drainage_capacity_index": 0.30,
|
| 397 |
+
"flow_accumulation": 0.75, "twi": 13.0, "dist_river": 0.5},
|
| 398 |
+
"Delhi": {"elevation_m": 216, "drainage_capacity_index": 0.50,
|
| 399 |
+
"flow_accumulation": 0.45, "twi": 8.5, "dist_river": 2.0},
|
| 400 |
+
"Hyderabad": {"elevation_m": 542, "drainage_capacity_index": 0.55,
|
| 401 |
+
"flow_accumulation": 0.35, "twi": 7.0, "dist_river": 3.5},
|
| 402 |
+
"Bangalore": {"elevation_m": 920, "drainage_capacity_index": 0.60,
|
| 403 |
+
"flow_accumulation": 0.30, "twi": 6.5, "dist_river": 4.0},
|
| 404 |
+
"Bhubaneswar": {"elevation_m": 45, "drainage_capacity_index": 0.38,
|
| 405 |
+
"flow_accumulation": 0.60, "twi": 10.5, "dist_river": 1.5},
|
| 406 |
+
"Patna": {"elevation_m": 53, "drainage_capacity_index": 0.28,
|
| 407 |
+
"flow_accumulation": 0.80, "twi": 14.0, "dist_river": 0.4},
|
| 408 |
+
"Guwahati": {"elevation_m": 55, "drainage_capacity_index": 0.32,
|
| 409 |
+
"flow_accumulation": 0.72, "twi": 13.5, "dist_river": 0.6},
|
| 410 |
+
"Kochi": {"elevation_m": 3, "drainage_capacity_index": 0.33,
|
| 411 |
+
"flow_accumulation": 0.78, "twi": 14.5, "dist_river": 0.3},
|
| 412 |
+
}
|
| 413 |
+
city = _nearest_city(lat, lon)
|
| 414 |
+
return CITY_STATIC.get(city, {
|
| 415 |
+
"elevation_m": 50, "drainage_capacity_index": 0.5,
|
| 416 |
+
"flow_accumulation": 0.5, "twi": 8.0, "dist_river": 2.0,
|
| 417 |
+
})
|
| 418 |
+
|
| 419 |
+
|
| 420 |
+
def get_nearest_safe_zones(lat: float, lon: float) -> list:
|
| 421 |
+
"""Return safe zones sorted by distance, pulling from nearest city's list."""
|
| 422 |
+
city = _nearest_city(lat, lon)
|
| 423 |
+
zones = SAFE_ZONES_DB.get(city, SAFE_ZONES_DB["Chennai"])
|
| 424 |
+
|
| 425 |
+
result = []
|
| 426 |
+
for zone in zones:
|
| 427 |
+
z = dict(zone)
|
| 428 |
+
z["distance_km"] = round(_haversine(lat, lon, z["lat"], z["lon"]), 2)
|
| 429 |
+
result.append(z)
|
| 430 |
+
|
| 431 |
+
return sorted(result, key=lambda z: z["distance_km"])
|
| 432 |
+
|
| 433 |
+
|
| 434 |
+
def dijkstra_route(start_lat, start_lon, end_lat, end_lon) -> dict:
|
| 435 |
+
"""
|
| 436 |
+
Simplified Dijkstra β generates a stepped waypoint path between
|
| 437 |
+
start and end with intermediate safe waypoints avoiding low ground.
|
| 438 |
+
Real implementation would use OSMnx road graph.
|
| 439 |
+
"""
|
| 440 |
+
distance_km = _haversine(start_lat, start_lon, end_lat, end_lon)
|
| 441 |
+
eta_minutes = round((distance_km / 30) * 60) # 30 km/h for emergency
|
| 442 |
+
|
| 443 |
+
# Generate intermediate waypoints (linear interpolation)
|
| 444 |
+
steps = max(2, min(5, int(distance_km / 2)))
|
| 445 |
+
waypoints = []
|
| 446 |
+
for i in range(steps + 1):
|
| 447 |
+
t = i / steps
|
| 448 |
+
wlat = start_lat + t * (end_lat - start_lat)
|
| 449 |
+
wlon = start_lon + t * (end_lon - start_lon)
|
| 450 |
+
waypoints.append([round(wlat, 5), round(wlon, 5)])
|
| 451 |
+
|
| 452 |
+
return {
|
| 453 |
+
"algorithm": "Dijkstra Shortest Path",
|
| 454 |
+
"start": {"lat": start_lat, "lon": start_lon},
|
| 455 |
+
"end": {"lat": end_lat, "lon": end_lon},
|
| 456 |
+
"distance_km": round(distance_km, 2),
|
| 457 |
+
"eta_minutes": eta_minutes,
|
| 458 |
+
"waypoints": waypoints,
|
| 459 |
+
"instructions": [
|
| 460 |
+
f"Proceed towards safe zone ({distance_km:.1f} km away)",
|
| 461 |
+
"Avoid low-lying roads, underpasses, and near-river routes",
|
| 462 |
+
"Follow elevated roads and flyovers where possible",
|
| 463 |
+
f"Estimated travel time: {eta_minutes} minutes at emergency speed",
|
| 464 |
+
"Arrive at designated safe zone and register with authorities",
|
| 465 |
+
]
|
| 466 |
+
}
|
| 467 |
|
| 468 |
|
| 469 |
# ============================================================================
|
|
|
|
| 1068 |
|
| 1069 |
|
| 1070 |
@app.post("/evacuate/route")
|
| 1071 |
+
def get_evacuation_route(req: EvacuationRequest):
|
| 1072 |
"""
|
| 1073 |
+
1. Takes user lat/lon from frontend
|
| 1074 |
+
2. Auto-derives flood features from nearest city (or uses provided ones)
|
| 1075 |
+
3. Runs FNN flood predictor
|
| 1076 |
+
4. If risk >= HIGH, computes evacuation route to nearest safe zone
|
| 1077 |
+
5. Returns route + safe zones as GeoJSON-friendly response
|
| 1078 |
"""
|
| 1079 |
+
if not flood_predictor.is_ready():
|
| 1080 |
+
raise HTTPException(503, "Flood model not loaded.")
|
| 1081 |
|
| 1082 |
+
# ββ Step 1: Build flood features ββββββββββββββββββββββββββββββββββββββ
|
| 1083 |
+
nearest_city = _nearest_city(req.latitude, req.longitude)
|
| 1084 |
+
|
| 1085 |
+
if req.flood_features:
|
| 1086 |
+
features = req.flood_features
|
| 1087 |
+
data_source = "user-provided"
|
| 1088 |
+
else:
|
| 1089 |
+
static = get_static_features_for_location(req.latitude, req.longitude)
|
| 1090 |
+
rainfall_mm = get_rainfall_for_location(req.latitude, req.longitude)
|
| 1091 |
+
soil_sat = float(flood_fetcher.estimate_soil_saturation(rainfall_mm))
|
| 1092 |
+
features = {
|
| 1093 |
+
"rainfall_mm": rainfall_mm,
|
| 1094 |
+
"elevation_m": static["elevation_m"],
|
| 1095 |
+
"soil_saturation_pct": soil_sat,
|
| 1096 |
+
"dist_river": static["dist_river"],
|
| 1097 |
+
"drainage_capacity_index": static["drainage_capacity_index"],
|
| 1098 |
+
"flow_accumulation": static["flow_accumulation"],
|
| 1099 |
+
"twi": static["twi"],
|
| 1100 |
+
}
|
| 1101 |
+
data_source = "IMD live + static city dataset"
|
| 1102 |
|
| 1103 |
+
# ββ Step 2: Validate + predict ββββββββββββββββββββββββββββββββββββββββ
|
| 1104 |
errors = flood_predictor.validate_input(features)
|
| 1105 |
if errors:
|
| 1106 |
raise HTTPException(422, {"validation_errors": errors})
|
| 1107 |
|
| 1108 |
+
result = flood_predictor.predict(features, n_mc_samples=req.n_mc_samples)
|
| 1109 |
risk_score = result.risk_score
|
| 1110 |
risk_tier = result.risk_tier.value
|
| 1111 |
|
| 1112 |
+
# ββ Step 3: Safe zones for this location ββββββββββββββββββββββββββββββ
|
| 1113 |
safe_zones = get_nearest_safe_zones(req.latitude, req.longitude)
|
| 1114 |
|
| 1115 |
+
# ββ Step 4: No evacuation needed ββββββββββββββββββββββββββββββββββββββ
|
| 1116 |
if risk_score < 0.45:
|
| 1117 |
return {
|
| 1118 |
+
"latitude": req.latitude,
|
| 1119 |
+
"longitude": req.longitude,
|
| 1120 |
+
"nearest_city": nearest_city,
|
| 1121 |
+
"risk_score": round(risk_score, 4),
|
| 1122 |
+
"risk_tier": risk_tier,
|
| 1123 |
+
"uncertainty": round(result.uncertainty, 4),
|
| 1124 |
"evacuation_needed": False,
|
| 1125 |
+
"message": f"No evacuation needed β risk is {risk_tier}",
|
| 1126 |
+
"flood_features": features,
|
| 1127 |
+
"data_source": data_source,
|
| 1128 |
+
"safe_zones": safe_zones,
|
| 1129 |
+
"route": None,
|
| 1130 |
+
"timestamp": datetime.now().isoformat(),
|
| 1131 |
}
|
| 1132 |
|
| 1133 |
+
# ββ Step 5: Compute evacuation route to nearest safe zone βββββββββββββ
|
| 1134 |
nearest_zone = safe_zones[0]
|
| 1135 |
route = dijkstra_route(
|
| 1136 |
start_lat=req.latitude,
|
| 1137 |
start_lon=req.longitude,
|
| 1138 |
end_lat=nearest_zone["lat"],
|
| 1139 |
+
end_lon=nearest_zone["lon"],
|
| 1140 |
)
|
| 1141 |
|
| 1142 |
return {
|
| 1143 |
"latitude": req.latitude,
|
| 1144 |
"longitude": req.longitude,
|
| 1145 |
+
"nearest_city": nearest_city,
|
| 1146 |
+
"risk_score": round(risk_score, 4),
|
| 1147 |
"risk_tier": risk_tier,
|
| 1148 |
+
"uncertainty": round(result.uncertainty, 4),
|
| 1149 |
+
"confidence_interval": [
|
| 1150 |
+
round(result.confidence_interval[0], 4),
|
| 1151 |
+
round(result.confidence_interval[1], 4),
|
| 1152 |
+
],
|
| 1153 |
"evacuation_needed": True,
|
| 1154 |
+
"message": f"EVACUATE β {risk_tier} flood risk detected near {nearest_city}",
|
| 1155 |
+
"flood_features": features,
|
| 1156 |
+
"data_source": data_source,
|
| 1157 |
"nearest_safe_zone": nearest_zone,
|
| 1158 |
"all_safe_zones": safe_zones,
|
| 1159 |
"route": route,
|
| 1160 |
+
"timestamp": datetime.now().isoformat(),
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1161 |
}
|
| 1162 |
|
| 1163 |
# ============================================================================
|