Files
AidsMonitoring/backend/services/ais_simulator.py
T

196 lines
9.6 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
"""
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)