196 lines
9.6 KiB
Python
196 lines
9.6 KiB
Python
"""
|
||
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)
|