"""Vessel AIS models for live maritime tracking.""" from sqlalchemy import BigInteger, Column, DateTime, Float, Index, Integer, JSON, SmallInteger, String from sqlalchemy.sql import func from app.core.enums import ConnectionState from app.core.time import to_iso8601_utc from app.db.session import Base class VesselStatic(Base): """Slow-changing vessel identity and dimensions.""" __tablename__ = "vessel_static" mmsi = Column(BigInteger, primary_key=True) name = Column(String(128), nullable=True) callsign = Column(String(16), nullable=True) vessel_type = Column(SmallInteger, nullable=True, index=True) vessel_type_name = Column(String(64), nullable=True, index=True) flag = Column(String(4), nullable=True, index=True) length = Column(Float, nullable=True) width = Column(Float, nullable=True) draught = Column(Float, nullable=True) imo = Column(BigInteger, nullable=True) updated_at = Column(DateTime(timezone=True), nullable=False, server_default=func.now()) def to_dict(self) -> dict: return { "mmsi": self.mmsi, "name": self.name, "callsign": self.callsign, "vessel_type": self.vessel_type, "vessel_type_name": self.vessel_type_name, "flag": self.flag, "length": self.length, "width": self.width, "draught": self.draught, "imo": self.imo, "updated_at": to_iso8601_utc(self.updated_at), } class VesselPosition(Base): """Append-only AIS positions retained for short history windows.""" __tablename__ = "vessel_position" id = Column(Integer, primary_key=True, autoincrement=True) mmsi = Column(BigInteger, nullable=False, index=True) lat = Column(Float, nullable=False) lon = Column(Float, nullable=False) sog = Column(Float, nullable=True) cog = Column(Float, nullable=True) heading = Column(SmallInteger, nullable=True) nav_status = Column(SmallInteger, nullable=True, index=True) received_at = Column(DateTime(timezone=True), nullable=False, server_default=func.now(), index=True) __table_args__ = ( Index("idx_vessel_pos_mmsi_time", "mmsi", "received_at"), Index("idx_vessel_pos_time", "received_at"), Index("idx_vessel_pos_lat_lon", "lat", "lon"), ) def to_dict(self) -> dict: return { "id": self.id, "mmsi": self.mmsi, "lat": self.lat, "lon": self.lon, "sog": self.sog, "cog": self.cog, "heading": self.heading, "nav_status": self.nav_status, "received_at": to_iso8601_utc(self.received_at), } class VesselCurrentState(Base): """Latest renderable state for one vessel, independent from AIS history.""" __tablename__ = "vessel_current_state" mmsi = Column(BigInteger, primary_key=True) lat = Column(Float, nullable=False) lon = Column(Float, nullable=False) sog = Column(Float, nullable=True) cog = Column(Float, nullable=True) heading = Column(SmallInteger, nullable=True) nav_status = Column(SmallInteger, nullable=True, index=True) name = Column(String(128), nullable=True) callsign = Column(String(16), nullable=True) vessel_type = Column(SmallInteger, nullable=True, index=True) vessel_type_name = Column(String(64), nullable=True, index=True) flag = Column(String(4), nullable=True, index=True) length = Column(Float, nullable=True) width = Column(Float, nullable=True) draught = Column(Float, nullable=True) imo = Column(BigInteger, nullable=True) source = Column(String(100), nullable=False, index=True) observed_at = Column(DateTime(timezone=True), nullable=False) field_sources = Column(JSON, default=dict) selected_reasons = Column(JSON, default=dict) source_summary = Column(JSON, default=dict) quality_flags = Column(JSON, default=list) updated_at = Column(DateTime(timezone=True), nullable=False, server_default=func.now()) __table_args__ = ( Index("idx_vessel_current_bbox", "lon", "lat"), Index("idx_vessel_current_observed", "observed_at"), ) def to_dict(self) -> dict: return { "mmsi": self.mmsi, "lat": self.lat, "lon": self.lon, "sog": self.sog, "cog": self.cog, "heading": self.heading, "nav_status": self.nav_status, "name": self.name, "callsign": self.callsign, "vessel_type": self.vessel_type, "vessel_type_name": self.vessel_type_name, "flag": self.flag, "length": self.length, "width": self.width, "draught": self.draught, "imo": self.imo, "source": self.source, "received_at": self.observed_at, "field_sources": self.field_sources or {}, "selected_reasons": self.selected_reasons or {}, "source_summary": self.source_summary or {}, "quality_flags": self.quality_flags or [], } class AISRawObservation(Base): """Source-level AIS fact before aggregation and conflict resolution.""" __tablename__ = "ais_raw_observations" id = Column(Integer, primary_key=True, autoincrement=True) target_schema = Column(String(64), nullable=False, default="vessel_ais", index=True) source = Column(String(100), nullable=False, index=True) entity_key = Column(String(64), nullable=False, index=True) delivery_mode = Column(String(32), nullable=False, index=True) transport = Column(String(32), nullable=False, index=True) message_type = Column(String(64), nullable=True, index=True) source_message_id = Column(String(128), nullable=True, index=True) observation_hash = Column(String(64), nullable=False, unique=True, index=True) observed_at = Column(DateTime(timezone=True), nullable=False, index=True) collected_at = Column(DateTime(timezone=True), nullable=False, server_default=func.now(), index=True) normalized_payload = Column(JSON, default=dict) raw_payload = Column(JSON, default=dict) quality_flags = Column(JSON, default=list) __table_args__ = ( Index("idx_ais_raw_entity_observed", "target_schema", "entity_key", "observed_at"), Index("idx_ais_raw_schema_observed_entity", "target_schema", "observed_at", "entity_key"), Index("idx_ais_raw_source_entity", "source", "entity_key"), ) def to_dict(self) -> dict: return { "id": self.id, "target_schema": self.target_schema, "source": self.source, "entity_key": self.entity_key, "delivery_mode": self.delivery_mode, "transport": self.transport, "message_type": self.message_type, "source_message_id": self.source_message_id, "observation_hash": self.observation_hash, "observed_at": to_iso8601_utc(self.observed_at), "collected_at": to_iso8601_utc(self.collected_at), "normalized_payload": self.normalized_payload or {}, "raw_payload": self.raw_payload or {}, "quality_flags": self.quality_flags or [], } class AISConflictRecord(Base): """Recorded field-level disagreement between AIS sources.""" __tablename__ = "ais_conflict_records" id = Column(Integer, primary_key=True, autoincrement=True) target_schema = Column(String(64), nullable=False, default="vessel_ais", index=True) entity_key = Column(String(64), nullable=False, index=True) field = Column(String(64), nullable=False, index=True) candidates = Column(JSON, default=dict) selected_source = Column(String(100), nullable=True, index=True) selected_value = Column(JSON, nullable=True) selected_reason = Column(String(64), nullable=True, index=True) resolved_by = Column(String(32), nullable=False, default="system", index=True) status = Column(String(32), nullable=False, default="open", index=True) created_at = Column(DateTime(timezone=True), nullable=False, server_default=func.now(), index=True) updated_at = Column(DateTime(timezone=True), nullable=False, server_default=func.now()) __table_args__ = ( Index("idx_ais_conflict_entity_field", "target_schema", "entity_key", "field"), ) def to_dict(self) -> dict: return { "id": self.id, "target_schema": self.target_schema, "entity_key": self.entity_key, "field": self.field, "candidates": self.candidates or {}, "selected_source": self.selected_source, "selected_value": self.selected_value, "selected_reason": self.selected_reason, "resolved_by": self.resolved_by, "status": self.status, "created_at": to_iso8601_utc(self.created_at), "updated_at": to_iso8601_utc(self.updated_at), } class AISSourceHealth(Base): """Runtime health signal for an AIS collector source.""" __tablename__ = "ais_source_health" source = Column(String(100), primary_key=True) connection_state = Column(String(32), nullable=False, default=ConnectionState.DISCONNECTED.value, index=True) last_seen_at = Column(DateTime(timezone=True), nullable=True, index=True) last_success_at = Column(DateTime(timezone=True), nullable=True, index=True) last_error = Column(String(500), nullable=True) message_rate = Column(Float, nullable=True) lag_seconds = Column(Float, nullable=True) updated_at = Column(DateTime(timezone=True), nullable=False, server_default=func.now(), index=True) def to_dict(self) -> dict: return { "source": self.source, "connection_state": self.connection_state, "last_seen_at": to_iso8601_utc(self.last_seen_at), "last_success_at": to_iso8601_utc(self.last_success_at), "last_error": self.last_error, "message_rate": self.message_rate, "lag_seconds": self.lag_seconds, "updated_at": to_iso8601_utc(self.updated_at), }