clarindasusan commited on
Commit
75b7527
Β·
verified Β·
1 Parent(s): 0825293

Update api.py

Browse files
Files changed (1) hide show
  1. api.py +260 -94
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: float
265
- longitude: float
266
- # Optional: override features if already calculated
267
- flood_features: Optional[Dict[str, float]] = None
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
268
 
269
 
270
  # ============================================================================
@@ -869,129 +1068,96 @@ def flood_map_from_geojson(req: GeoJSONMapRequest):
869
 
870
 
871
  @app.post("/evacuate/route")
872
- async def get_evacuation_route(req: EvacuationRequest):
873
  """
874
- 1. Takes user lat/lon
875
- 2. Reuses flood_predictor internally
876
- 3. If risk >= 70, calculates evacuation route
877
- 4. Returns route + safe zones
 
878
  """
 
 
879
 
880
- # Step 1: Build flood features from lat/lon
881
- # Use features from request or build from datasets
882
- features = req.flood_features or {
883
- "rainfall_mm": get_rainfall_for_location(req.latitude, req.longitude),
884
- "elevation_m": get_elevation_for_location(req.latitude, req.longitude),
885
- "soil_saturation_pct": 75.0,
886
- "dist_river": 1.2,
887
- "drainage_capacity_index": 0.4,
888
- "flow_accumulation": 0.6,
889
- "twi": 8.5,
890
- }
 
 
 
 
 
 
 
 
 
891
 
892
- # Step 2: Reuse existing flood_predictor (same instance!)
893
  errors = flood_predictor.validate_input(features)
894
  if errors:
895
  raise HTTPException(422, {"validation_errors": errors})
896
 
897
- result = flood_predictor.predict(features, n_mc_samples=50)
898
  risk_score = result.risk_score
899
  risk_tier = result.risk_tier.value
900
 
901
- # Step 3: Find nearest safe zones
902
  safe_zones = get_nearest_safe_zones(req.latitude, req.longitude)
903
 
904
- # Step 4: Only generate route if risk is HIGH
905
  if risk_score < 0.45:
906
  return {
907
- "latitude": req.latitude,
908
- "longitude": req.longitude,
909
- "risk_score": risk_score,
910
- "risk_tier": risk_tier,
 
 
911
  "evacuation_needed": False,
912
- "message": "No evacuation needed β€” risk is LOW",
913
- "safe_zones": safe_zones,
914
- "route": None
 
 
 
915
  }
916
 
917
- # Step 5: Run Dijkstra to nearest safe zone
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
- "risk_score": risk_score,
 
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
  # ============================================================================