""" Simulador AIS - Tráfico marítimo zona Miami / Port of Miami Genera barcos y ayudas a la navegación ficticios para pruebas. """ import asyncio import random import math from datetime import datetime # Ayudas a la navegación reales zona Miami (posiciones aproximadas) MIAMI_AIDS = [ {"id": "BOYA_MIAMI_01", "nombre": "Boya Canal Principal N", "tipo": "BOYA_LATERAL", "categoria": "FLOTANTE", "tipo_ais": "AIS_NORMAL", "mmsi": "338001001", "lat_nominal": 25.7730, "lon_nominal": -80.1320, "radio_borneo_m": 10.0}, {"id": "BOYA_MIAMI_02", "nombre": "Boya Canal Principal S", "tipo": "BOYA_LATERAL", "categoria": "FLOTANTE", "tipo_ais": "ATON_21", "mmsi": "338001002", "lat_nominal": 25.7650, "lon_nominal": -80.1290, "radio_borneo_m": 10.0}, {"id": "BOYA_MIAMI_03", "nombre": "Boya Cardinal Este", "tipo": "BOYA_CARDINAL", "categoria": "FLOTANTE", "tipo_ais": "SIN_AIS", "mmsi": None, "lat_nominal": 25.7800, "lon_nominal": -80.1200, "radio_borneo_m": 10.0}, {"id": "FARO_MIAMI_01", "nombre": "Faro Cape Florida", "tipo": "FARO", "categoria": "FIJA", "tipo_ais": "SIN_AIS", "mmsi": None, "lat_nominal": 25.6664, "lon_nominal": -80.1572, "radio_borneo_m": 0.0, "caracteristica_luz": "Fl W 15s", "alcance_nm": 12}, {"id": "FAROL_MIAMI_01", "nombre": "Farol Entrada Puerto", "tipo": "FAROL", "categoria": "FIJA", "tipo_ais": "SIN_AIS", "mmsi": None, "lat_nominal": 25.7743, "lon_nominal": -80.1337, "radio_borneo_m": 0.0, "caracteristica_luz": "Fl(2) R 10s", "alcance_nm": 6}, ] # Barcos simulados en zona Miami. # Dimensiones (length × beam, to_bow/to_stern/to_port/to_starboard) según AIS Type 5/24. # Reference point = posición de la antena AIS. MIAMI_VESSELS = [ {"mmsi": "636015000", "nombre": "CARNIVAL MAGIC", "tipo": 60, "destino": "MIAMI", "lat": 25.7800, "lon": -80.1900, "sog": 0.2, "cog": 90, "heading": 90, "length": 306, "beam": 37, "to_bow": 200, "to_stern": 106, "to_port": 18, "to_starboard": 19, "imo": "9378474", "callsign": "3FOF8", "bandera": "PA", "draught": 8.2, "eta": "05-15 14:00", "nav_status": 5, "fix_type": 1, "rot": 0.0}, {"mmsi": "353984000", "nombre": "MSC SEASHORE", "tipo": 60, "destino": "NASSAU", "lat": 25.7600, "lon": -80.2100, "sog": 14.5, "cog": 125, "heading": 124, "length": 339, "beam": 41, "to_bow": 220, "to_stern": 119, "to_port": 20, "to_starboard": 21, "imo": "9839348", "callsign": "3EUX9", "bandera": "PA", "draught": 8.5, "eta": "05-12 08:00", "nav_status": 0, "fix_type": 1, "rot": 0.5}, {"mmsi": "244820000", "nombre": "BISCAYNE BAY TUG", "tipo": 52, "destino": "PORT MIAMI", "lat": 25.7720, "lon": -80.1600, "sog": 5.2, "cog": 270, "heading": 268, "length": 30, "beam": 11, "to_bow": 18, "to_stern": 12, "to_port": 5, "to_starboard": 6, "imo": "9412104", "callsign": "WDF8493", "bandera": "US", "draught": 4.0, "eta": "05-10 18:30", "nav_status": 0, "fix_type": 1, "rot": 0.0}, {"mmsi": "367123450", "nombre": "SEA HUNTER", "tipo": 30, "destino": "FISHING", "lat": 25.7400, "lon": -80.1100, "sog": 3.1, "cog": 45, "heading": 47, "length": 18, "beam": 6, "to_bow": 12, "to_stern": 6, "to_port": 3, "to_starboard": 3, "imo": None, "callsign": "WDA3047", "bandera": "US", "draught": 1.8, "eta": None, "nav_status": 7, "fix_type": 1, "rot": 0.0}, {"mmsi": "311001234", "nombre": "BAHAMAS EXPRESS", "tipo": 69, "destino": "FREEPORT", "lat": 25.7900, "lon": -80.1800, "sog": 18.0, "cog": 65, "heading": 65, "length": 80, "beam": 14, "to_bow": 60, "to_stern": 20, "to_port": 7, "to_starboard": 7, "imo": "9242315", "callsign": "C6BX2", "bandera": "BS", "draught": 3.6, "eta": "05-11 06:00", "nav_status": 0, "fix_type": 1, "rot": 0.0}, ] # Ship-type table — AIS spec, message type 5 cargo type field (rangos parcial). SHIP_TYPE_NAMES = { 0: "Not available", 20: "WIG", 21: "WIG hazardous A", 22: "WIG hazardous B", 23: "WIG hazardous C", 24: "WIG hazardous D", 30: "Fishing", 31: "Towing", 32: "Towing >200m", 33: "Dredging", 34: "Diving", 35: "Military", 36: "Sailing", 37: "Pleasure craft", 40: "High speed craft", 50: "Pilot vessel", 51: "Search and rescue", 52: "Tug", 53: "Port tender", 54: "Anti-pollution", 55: "Law enforcement", 58: "Medical transport", 60: "Passenger", 61: "Passenger hazardous A", 62: "Passenger hazardous B", 69: "Passenger (other)", 70: "Cargo", 71: "Cargo hazardous A", 72: "Cargo hazardous B", 73: "Cargo hazardous C", 74: "Cargo hazardous D", 79: "Cargo (other)", 80: "Tanker", 81: "Tanker hazardous A", 82: "Tanker hazardous B", 83: "Tanker hazardous C", 84: "Tanker hazardous D", 89: "Tanker (other)", 90: "Other", 99: "Other (no info)", } # AIS navigation status codes (Type 1/2/3 nav_status field) NAV_STATUS_NAMES = { 0: "Under way using engine", 1: "At anchor", 2: "Not under command", 3: "Restricted manoeuvrability", 4: "Constrained by draught", 5: "Moored", 6: "Aground", 7: "Engaged in fishing", 8: "Under way sailing", 9: "Reserved (HSC)", 10: "Reserved (WIG)", 11: "Power-driven towing astern", 12: "Power-driven pushing ahead", 13: "Reserved", 14: "AIS-SART/MOB/EPIRB", 15: "Undefined / default", } def estimate_dims_from_type(ship_type: int) -> tuple[int, int]: """Fallback length/beam (m) for vessels reporting position (Type 1/18) but no Type 5/24 with real dimensions yet. Conservative averages.""" t = ship_type or 0 if 80 <= t < 90: return (220, 32) # tanker if 70 <= t < 80: return (180, 28) # cargo if 60 <= t < 70: return (200, 32) # passenger if t == 36: return (12, 4) # sailing if t == 30: return (18, 6) # fishing if t == 31 or t == 32 or t == 52: return (30, 10) # tug / dredger if t == 37: return (15, 5) # pleasure return (50, 10) def haversine_distance(lat1, lon1, lat2, lon2): R = 6371000 phi1, phi2 = math.radians(lat1), math.radians(lat2) dphi = math.radians(lat2 - lat1) dlambda = math.radians(lon2 - lon1) a = math.sin(dphi/2)**2 + math.cos(phi1)*math.cos(phi2)*math.sin(dlambda/2)**2 return R * 2 * math.atan2(math.sqrt(a), math.sqrt(1-a)) def move_vessel(vessel, dt_seconds=5): if vessel["sog"] < 0.5: return vessel speed_ms = vessel["sog"] * 0.514444 distance_m = speed_ms * dt_seconds cog_rad = math.radians(vessel["cog"]) dlat = (distance_m * math.cos(cog_rad)) / 111320 dlon = (distance_m * math.sin(cog_rad)) / (111320 * math.cos(math.radians(vessel["lat"]))) vessel["lat"] += dlat vessel["lon"] += dlon vessel["cog"] += random.uniform(-1.5, 1.5) vessel["cog"] %= 360 vessel["heading"] = vessel["cog"] return vessel def simulate_aid_borneo(aid): if aid["radio_borneo_m"] == 0 or aid["tipo_ais"] == "SIN_AIS": return aid["lat_nominal"], aid["lon_nominal"] offset_m = random.uniform(0, aid["radio_borneo_m"]) angle = random.uniform(0, 360) dlat = (offset_m * math.cos(math.radians(angle))) / 111320 dlon = (offset_m * math.sin(math.radians(angle))) / (111320 * math.cos(math.radians(aid["lat_nominal"]))) return aid["lat_nominal"] + dlat, aid["lon_nominal"] + dlon async def run_simulator(broadcast_callback): vessels = [v.copy() for v in MIAMI_VESSELS] aids = [a.copy() for a in MIAMI_AIDS] while True: now = datetime.utcnow().isoformat() # Mover barcos for v in vessels: v = move_vessel(v, dt_seconds=5) await broadcast_callback({ "type": "vessel", "mmsi": v["mmsi"], "nombre": v["nombre"], "tipo": v["tipo"], "tipo_nombre": SHIP_TYPE_NAMES.get(v["tipo"], f"Type {v['tipo']}"), "destino": v["destino"], "lat": round(v["lat"], 6), "lon": round(v["lon"], 6), "sog": v["sog"], "cog": round(v["cog"], 1), "heading": round(v["heading"], 1), # Static AIS Type 5 / Type 24 "imo": v.get("imo"), "callsign": v.get("callsign"), "bandera": v.get("bandera"), "length": v.get("length"), "beam": v.get("beam"), "to_bow": v.get("to_bow"), "to_stern": v.get("to_stern"), "to_port": v.get("to_port"), "to_starboard": v.get("to_starboard"), "draught": v.get("draught"), "eta": v.get("eta"), # Dynamic Type 1/2/3 "nav_status": v.get("nav_status"), "nav_status_name": NAV_STATUS_NAMES.get(v.get("nav_status", 15), "Unknown"), "rot": v.get("rot"), "fix_type": v.get("fix_type"), "timestamp": now }) # Actualizar ayudas con AIS for a in aids: if a["tipo_ais"] == "SIN_AIS": continue lat_act, lon_act = simulate_aid_borneo(a) desplazamiento = haversine_distance(a["lat_nominal"], a["lon_nominal"], lat_act, lon_act) await broadcast_callback({ "type": "aid_position", "id": a["id"], "mmsi": a["mmsi"], "lat_actual": round(lat_act, 6), "lon_actual": round(lon_act, 6), "desplazamiento_m": round(desplazamiento, 2), "en_posicion": desplazamiento <= 15.0, "timestamp": now }) await asyncio.sleep(5)