""" OpenStreetMap and local simulation fallback for emergency resource intelligence. Traffic-aware ETA simulation, nearest services, and hexagonal coverage scoring. """ from __future__ import annotations import logging import math import os import requests from datetime import datetime, timezone from typing import Any logger = logging.getLogger(__name__) def _maps_api_key() -> str: return "" def _get_gmaps_client(): return None def maps_configured() -> bool: return False def maps_status_detail() -> dict[str, Any]: return { "configured": False, "provider": "simulated_osm", "key_present": False, "error": "Google Maps integration disabled. Using local OSM intelligence.", } SERVICE_TYPES: dict[str, dict[str, Any]] = { "hospital": { "place_type": "hospital", "keyword": "hospital", "size_filter": True, "min_ratings": 50, "icon": "H", "label": "Hospital", }, "fire_station": { "place_type": "fire_station", "keyword": "fire station", "size_filter": False, "min_ratings": 0, "icon": "F", "label": "Fire Station", }, "police": { "place_type": "police", "keyword": "police station", "size_filter": False, "min_ratings": 0, "icon": "P", "label": "Police Station", }, "ambulance": { "place_type": "establishment", "keyword": "ambulance service emergency", "size_filter": False, "min_ratings": 0, "icon": "A", "label": "Ambulance Service", }, "emergency_supplies": { "place_type": "pharmacy", "keyword": "pharmacy medical supply first aid", "size_filter": False, "min_ratings": 10, "icon": "S", "label": "Emergency Supplies", }, } _OSM_AMENITY_TO_SERVICE = { "hospital": "hospital", "fire_station": "fire_station", "police": "police", "ambulance_station": "ambulance", "clinic": "ambulance", "pharmacy": "emergency_supplies", "medical_supply": "emergency_supplies", } def _haversine_km(lat1: float, lon1: float, lat2: float, lon2: float) -> float: r = 6371 dlat = math.radians(lat2 - lat1) dlon = math.radians(lon2 - lon1) a = ( math.sin(dlat / 2) ** 2 + math.cos(math.radians(lat1)) * math.cos(math.radians(lat2)) * math.sin(dlon / 2) ** 2 ) return r * 2 * math.asin(math.sqrt(a)) def _fetch_emergency_nearby_sync(lat: float, lon: float, radius_m: int = 15000) -> list[dict[str, Any]]: """Query OSM Overpass API synchronously for emergency resources.""" query = f""" [out:json][timeout:15]; ( node["amenity"="hospital"](around:{radius_m},{lat},{lon}); node["amenity"="fire_station"](around:{radius_m},{lat},{lon}); node["amenity"="police"](around:{radius_m},{lat},{lon}); node["emergency"="ambulance_station"](around:{radius_m},{lat},{lon}); node["amenity"="clinic"]["emergency"="yes"](around:{radius_m},{lat},{lon}); node["amenity"="pharmacy"](around:{radius_m},{lat},{lon}); node["shop"="medical_supply"](around:{radius_m},{lat},{lon}); ); out body 80; """ mirrors = [ "https://overpass-api.de/api/interpreter", "https://overpass.kumi.systems/api/interpreter", ] for url in mirrors: try: resp = requests.post( url, data={"data": query}, headers={"User-Agent": "CepheusEmergencyConsole/1.0"}, timeout=12 ) if resp.status_code == 200: return resp.json().get("elements", []) except Exception as exc: logger.warning("Overpass sync mirror %s failed: %s", url, exc) logger.warning("All Overpass sync mirrors failed, generating synthetic mock data.") import random mock_elements = [] # Simplified mock generation for agent use for _ in range(5): dlat = (random.random() - 0.5) * 0.05 dlon = (random.random() - 0.5) * 0.05 mock_elements.append({ "type": "node", "id": random.randint(10000, 99999), "lat": lat + dlat, "lon": lon + dlon, "tags": { "name": f"Local Medical Center {random.randint(1, 100)}", "amenity": "hospital", "emergency": "yes", "phone": f"+1-555-{random.randint(1000, 9999)}" } }) return mock_elements def find_nearest_services( lat: float, lon: float, radius_m: int = 10000, top_n: int = 3, ) -> dict[str, Any]: """Nearest emergency services fetched via OSM Overpass with locally simulated driving metrics.""" elements = _fetch_emergency_nearby_sync(lat, lon, radius_m) results: dict[str, list[dict[str, Any]]] = {k: [] for k in SERVICE_TYPES} for el in elements: tags = el.get("tags", {}) amenity = tags.get("amenity") emergency_tag = tags.get("emergency") shop = tags.get("shop") service_key = _OSM_AMENITY_TO_SERVICE.get(amenity) if not service_key and emergency_tag == "ambulance_station": service_key = "ambulance" if not service_key and shop == "medical_supply": service_key = "emergency_supplies" if not service_key or service_key not in SERVICE_TYPES: continue plat = el.get("lat") plng = el.get("lon") if plat is None or plng is None: continue config = SERVICE_TYPES[service_key] dist_km = _haversine_km(lat, lon, plat, plng) # Simulate driving metrics drive_distance_m = int(dist_km * 1250) # driving distance usually ~25% longer than straight line duration_normal_s = int((drive_distance_m / 11) + 60) # average speed 40km/h + 1 min startup overhead duration_traffic_s = int(duration_normal_s * 1.18) # simulate moderate traffic (18% slower) delay_s = duration_traffic_s - duration_normal_s # Build enriched data results[service_key].append({ "place_id": f"osm-{el.get('id')}", "name": tags.get("name") or config["label"], "address": tags.get("addr:full") or ", ".join(filter(None, [tags.get("addr:street"), tags.get("addr:city")])) or tags.get("operator", "") or "Address unknown", "lat": plat, "lng": plng, "straight_line_km": round(dist_km, 2), "drive_distance_m": drive_distance_m, "drive_distance_text": f"{round(drive_distance_m / 1000, 1)} km", "duration_normal_s": duration_normal_s, "duration_normal_text": f"{max(1, duration_normal_s // 60)} mins", "duration_traffic_s": duration_traffic_s, "duration_traffic_text": f"{max(1, duration_traffic_s // 60)} mins", "rating": 4.5, "user_ratings_total": 20, "open_now": True, "phone": tags.get("phone") or tags.get("contact:phone") or "", "icon": config["icon"], "service_type": service_key, "label": config["label"], "traffic_delay_s": delay_s, "traffic_delay_text": f"+{delay_s // 60} min delay" if delay_s > 60 else "No significant delay", "traffic_severity": "heavy" if delay_s > 300 else "moderate" if delay_s > 120 else "light", }) for k in results: results[k].sort(key=lambda x: x["duration_traffic_s"]) results[k] = results[k][:top_n] return { "origin": {"lat": lat, "lng": lon}, "fetched_at": datetime.now(timezone.utc).isoformat(), "services": results, "source": "osm_simulation", } def get_directions_with_traffic( origin_lat: float, origin_lng: float, dest_lat: float, dest_lng: float, place_name: str = "", ) -> dict[str, Any]: """Generates simulated driving route step-by-step and driving metrics.""" dist_km = _haversine_km(origin_lat, origin_lng, dest_lat, dest_lng) drive_distance_m = int(dist_km * 1250) duration_normal_s = int((drive_distance_m / 11) + 60) duration_traffic_s = int(duration_normal_s * 1.18) steps = [ { "instruction": "Head toward main road", "distance": "200 m", "duration": "1 min", }, { "instruction": f"Drive along primary route for {round(dist_km, 1)} km", "distance": f"{round(drive_distance_m / 1000, 1)} km", "duration": f"{duration_normal_s // 60} mins", }, { "instruction": f"Turn into destination: {place_name or 'Emergency Service'}", "distance": "100 m", "duration": "1 min", } ] return { "destination_name": place_name, "distance": f"{round(drive_distance_m / 1000, 1)} km", "duration_normal": f"{duration_normal_s // 60} mins", "duration_traffic": f"{duration_traffic_s // 60} mins", "start_address": f"Location ({origin_lat:.4f}, {origin_lng:.4f})", "end_address": f"Location ({dest_lat:.4f}, {dest_lng:.4f})", "steps": steps, "overview_polyline": "", "maps_url": f"https://www.openstreetmap.org/directions?engine=fossgis_osrm_car&route={origin_lat},{origin_lng};{dest_lat},{dest_lng}", } def get_hexagonal_coverage_data( center_lat: float, center_lng: float, radius_km: float = 5.0, ) -> dict[str, Any]: """Calculates coverage grids dynamically using simulated OSM locations.""" sample_points: list[tuple[float, float]] = [] steps = 8 for i in range(-steps, steps + 1): for j in range(-steps, steps + 1): dlat = (i / steps) * (radius_km / 111) dlng = (j / steps) * (radius_km / (111 * math.cos(math.radians(center_lat)))) pt_lat = center_lat + dlat pt_lng = center_lng + dlng if _haversine_km(center_lat, center_lng, pt_lat, pt_lng) <= radius_km: sample_points.append((pt_lat, pt_lng)) services_data = find_nearest_services( center_lat, center_lng, radius_m=int(radius_km * 1000 * 2), top_n=3 ) all_service_locations: list[tuple[float, float, str]] = [] for svc_list in services_data.get("services", {}).values(): if isinstance(svc_list, list): for svc in svc_list: all_service_locations.append((svc["lat"], svc["lng"], svc["service_type"])) if not all_service_locations: return {"cells": [], "services": services_data, "center": {"lat": center_lat, "lng": center_lng}} scored_cells: list[dict[str, Any]] = [] for pt_lat, pt_lng in sample_points[:40]: reachable = 0 unique_types = set() for slat, slng, stype in all_service_locations: dist = _haversine_km(pt_lat, pt_lng, slat, slng) drive_m = dist * 1250 dur_s = (drive_m / 11) + 60 if dur_s <= 600: # reachable under 10 mins reachable += 1 unique_types.add(stype) coverage_score = min(100, (reachable * 10) + (len(unique_types) * 15)) scored_cells.append( { "lat": pt_lat, "lng": pt_lng, "coverage_score": coverage_score, "color": ( "#22c55e" if coverage_score >= 70 else "#eab308" if coverage_score >= 40 else "#ef4444" ), } ) return { "center": {"lat": center_lat, "lng": center_lng}, "cells": scored_cells, "services": services_data, "fetched_at": datetime.now(timezone.utc).isoformat(), } def recommend_emergency_dispatch( lat: float, lng: float, emergency_type: str, severity: str = "medium", ) -> dict[str, Any]: priority_map = { "fire": ["fire_station", "hospital", "ambulance"], "medical": ["ambulance", "hospital", "emergency_supplies"], "security": ["police", "ambulance", "hospital"], "crowd": ["police", "ambulance", "hospital"], "general": ["hospital", "police", "fire_station", "ambulance"], } data = find_nearest_services(lat, lng, top_n=3) services = data.get("services", {}) priority_types = priority_map.get(emergency_type, priority_map["general"]) recommendation: dict[str, Any] = { "emergency_type": emergency_type, "severity": severity, "primary_dispatch": [], "backup_dispatch": [], "total_response_time_estimate": None, } for svc_type in priority_types: svc_list = services.get(svc_type, []) if not svc_list or not isinstance(svc_list, list): continue best = svc_list[0] recommendation["primary_dispatch"].append( { "service_type": svc_type, "name": best["name"], "eta_with_traffic": best["duration_traffic_text"], "distance": best["drive_distance_text"], "traffic_severity": best["traffic_severity"], "address": best["address"], "lat": best["lat"], "lng": best["lng"], } ) if len(svc_list) > 1: backup = svc_list[1] recommendation["backup_dispatch"].append( { "service_type": svc_type, "name": backup["name"], "eta_with_traffic": backup["duration_traffic_text"], } ) max_eta_s = 0 for pt in priority_types: lst = services.get(pt) if lst and isinstance(lst, list) and lst: max_eta_s = max(max_eta_s, lst[0].get("duration_traffic_s", 0)) if max_eta_s: recommendation["total_response_time_estimate"] = f"{max_eta_s // 60} minutes" return recommendation