diff --git a/routes/ais.py b/routes/ais.py index 6aacbfc..36cae5e 100644 --- a/routes/ais.py +++ b/routes/ais.py @@ -163,10 +163,13 @@ def process_ais_message(msg: dict) -> dict | None: vessel = app_module.ais_vessels.get(mmsi) or {'mmsi': mmsi} # Extract common fields - if 'lat' in msg and 'lon' in msg: + # AIS-catcher JSON_FULL uses 'longitude'/'latitude', but some versions use 'lon'/'lat' + lat_val = msg.get('latitude') or msg.get('lat') + lon_val = msg.get('longitude') or msg.get('lon') + if lat_val is not None and lon_val is not None: try: - lat = float(msg['lat']) - lon = float(msg['lon']) + lat = float(lat_val) + lon = float(lon_val) # Validate coordinates (AIS uses 181 for unavailable) if -90 <= lat <= 90 and -180 <= lon <= 180: vessel['lat'] = lat diff --git a/static/css/ais_dashboard.css b/static/css/ais_dashboard.css index 068350c..d564cf8 100644 --- a/static/css/ais_dashboard.css +++ b/static/css/ais_dashboard.css @@ -495,9 +495,8 @@ body { } .no-vessel-icon { - font-size: 36px; margin-bottom: 10px; - opacity: 0.5; + color: var(--accent-cyan); } .vessel-header { @@ -508,7 +507,9 @@ body { } .vessel-icon { - font-size: 32px; + display: flex; + align-items: center; + justify-content: center; } .vessel-name { @@ -595,7 +596,10 @@ body { } .vessel-item-icon { - font-size: 20px; + display: flex; + align-items: center; + justify-content: center; + width: 24px; } .vessel-item-info { @@ -747,19 +751,12 @@ body { border: none; } -.vessel-marker-inner { - width: 24px; - height: 24px; - display: flex; - align-items: center; - justify-content: center; - font-size: 18px; - filter: drop-shadow(0 0 2px rgba(0,0,0,0.8)); - transition: transform 0.3s ease; +.vessel-marker svg { + transition: filter 0.2s ease; } -.vessel-marker.selected .vessel-marker-inner { - filter: drop-shadow(0 0 6px var(--accent-cyan)); +.vessel-marker.selected svg { + filter: drop-shadow(0 0 8px rgba(255,255,255,0.8)) !important; } /* Range rings */ diff --git a/templates/ais_dashboard.html b/templates/ais_dashboard.html index 63014ea..1a90afa 100644 --- a/templates/ais_dashboard.html +++ b/templates/ais_dashboard.html @@ -78,7 +78,11 @@
-
🚢
+
+ + + +
Select a vessel
@@ -204,34 +208,48 @@ let messageRateInterval = null; let lastMessageCount = 0; - // Ship type to icon mapping - const SHIP_ICONS = { - 30: '🐟', // Fishing - 31: '🚢', // Towing - 32: '🚢', // Towing - 36: '⛵', // Sailing - 37: '⛵', // Pleasure craft - 60: '🚢', // Passenger - 61: '🚢', // Passenger - 62: '🚢', // Passenger - 63: '🚢', // Passenger - 64: '🚢', // Passenger - 65: '🚢', // Passenger - 66: '🚢', // Passenger - 67: '🚢', // Passenger - 68: '🚢', // Passenger - 69: '🚢', // Passenger - 70: '🚢', // Cargo - 71: '🚢', // Cargo - hazardous A - 72: '🚢', // Cargo - hazardous B - 73: '🚢', // Cargo - hazardous C - 74: '🚢', // Cargo - hazardous D - 80: '🚢', // Tanker - 81: '🚢', // Tanker - hazardous A - 82: '🚢', // Tanker - hazardous B - 83: '🚢', // Tanker - hazardous C - 84: '🚢', // Tanker - hazardous D - default: '🚢' // Generic ship + // Vessel SVG icon paths (top-down view, pointing up) + const VESSEL_ICONS = { + // Generic cargo/container ship - pointed bow, rectangular hull + cargo: 'M12 2L8 6V18L10 20H14L16 18V6L12 2ZM10 8H14V16H10V8Z', + // Tanker - rounded bow, long hull + tanker: 'M12 2C10 2 8 4 8 6V18C8 19 9 20 10 20H14C15 20 16 19 16 18V6C16 4 14 2 12 2ZM10 8H14V16H10V8Z', + // Passenger/cruise - multiple decks indicated + passenger: 'M12 2L8 5V18L10 20H14L16 18V5L12 2ZM9 7H15V10H9V7ZM9 11H15V14H9V11ZM9 15H15V18H9V15Z', + // Tug - small, compact, powerful + tug: 'M12 4L9 7V16L10 18H14L15 16V7L12 4ZM10 9H14V14H10V9Z', + // Fishing vessel - with mast/outriggers + fishing: 'M12 2L12 5L8 8V17L10 19H14L16 17V8L12 5ZM6 10L8 12V15L6 13V10ZM18 10V13L16 15V12L18 10ZM10 10H14V15H10V10Z', + // Sailing vessel - sail shape + sailing: 'M12 2L12 6L8 10V18L10 20H14L16 18V10L12 6ZM12 3L16 8H12V3ZM10 11H14V17H10V11Z', + // Military - angular, aggressive bow + military: 'M12 1L7 6V8L8 9V18L10 20H14L16 18V9L17 8V6L12 1ZM10 10H14V16H10V10Z', + // High speed craft - sleek, pointed + hsc: 'M12 1L9 5V18L10 20H14L15 18V5L12 1ZM10 7H14V17H10V7Z', + // Search & rescue - distinctive cross marking + sar: 'M12 2L8 6V18L10 20H14L16 18V6L12 2ZM11 8H13V11H16V13H13V16H11V13H8V11H11V8Z', + // Pilot vessel + pilot: 'M12 3L9 6V17L10 19H14L15 17V6L12 3ZM10 8H14V15H10V8ZM11 9V10H13V9H11Z', + // Law enforcement + law: 'M12 2L8 6V18L10 20H14L16 18V6L12 2ZM10 8H14V10H10V8ZM11 11H13V16H11V11Z', + // Generic vessel (default) + default: 'M12 2L8 6V18L10 20H14L16 18V6L12 2Z' + }; + + // Vessel type colors + const VESSEL_COLORS = { + cargo: '#00d4ff', // Cyan + tanker: '#ff6b35', // Orange + passenger: '#a855f7', // Purple + tug: '#fbbf24', // Yellow + fishing: '#22c55e', // Green + sailing: '#60a5fa', // Light blue + military: '#ef4444', // Red + hsc: '#f472b6', // Pink + sar: '#ff0000', // Bright red + pilot: '#ffffff', // White + law: '#3b82f6', // Blue + default: '#00d4ff' // Cyan }; // Ship type categories @@ -255,8 +273,50 @@ return 'Other'; } - function getShipIcon(type) { - return SHIP_ICONS[type] || SHIP_ICONS.default; + // Get vessel icon type from AIS ship type code + function getVesselIconType(type) { + if (!type) return 'default'; + if (type === 30) return 'fishing'; + if (type >= 31 && type <= 32) return 'tug'; + if (type === 35) return 'military'; + if (type >= 36 && type <= 37) return 'sailing'; + if (type >= 40 && type < 50) return 'hsc'; + if (type === 50) return 'pilot'; + if (type === 51) return 'sar'; + if (type === 52) return 'tug'; + if (type === 55) return 'law'; + if (type >= 60 && type < 70) return 'passenger'; + if (type >= 70 && type < 80) return 'cargo'; + if (type >= 80 && type < 90) return 'tanker'; + return 'default'; + } + + // Create SVG vessel marker icon + function createVesselMarkerIcon(rotation, vesselType, isSelected = false) { + const path = VESSEL_ICONS[vesselType] || VESSEL_ICONS.default; + const color = VESSEL_COLORS[vesselType] || VESSEL_COLORS.default; + const size = 24; + const glowColor = isSelected ? 'rgba(255,255,255,0.8)' : color; + const glowSize = isSelected ? '8px' : '4px'; + + return L.divIcon({ + className: 'vessel-marker' + (isSelected ? ' selected' : ''), + html: ` + + `, + iconSize: [size, size], + iconAnchor: [size/2, size/2] + }); + } + + // Legacy function for vessel list icons (returns SVG string) + function getShipIconSvg(type, size = 18) { + const vesselType = getVesselIconType(type); + const path = VESSEL_ICONS[vesselType] || VESSEL_ICONS.default; + const color = VESSEL_COLORS[vesselType] || VESSEL_COLORS.default; + return ` + + `; } // Navigation status text @@ -544,20 +604,9 @@ if (!vessel.lat || !vessel.lon) return; const heading = vessel.heading || vessel.course || 0; - const icon = getShipIcon(vessel.ship_type); - - const markerHtml = ` -
- ${icon} -
- `; - - const divIcon = L.divIcon({ - className: 'vessel-marker' + (mmsi === selectedMmsi ? ' selected' : ''), - html: markerHtml, - iconSize: [24, 24], - iconAnchor: [12, 12] - }); + const vesselType = getVesselIconType(vessel.ship_type); + const isSelected = mmsi === selectedMmsi; + const divIcon = createVesselMarkerIcon(heading, vesselType, isSelected); if (markers[mmsi]) { markers[mmsi].setLatLng([vessel.lat, vessel.lon]); @@ -573,13 +622,17 @@ } function selectVessel(mmsi) { + const prevSelected = selectedMmsi; selectedMmsi = mmsi; - // Update marker styles - Object.keys(markers).forEach(m => { - const el = markers[m].getElement(); - if (el) { - el.querySelector('.vessel-marker-inner')?.parentElement?.classList.toggle('selected', m === mmsi); + // Update marker icons for previous and new selection + [prevSelected, mmsi].forEach(m => { + if (m && vessels[m] && markers[m]) { + const vessel = vessels[m]; + const heading = vessel.heading || vessel.course || 0; + const vesselType = getVesselIconType(vessel.ship_type); + const isSelected = m === mmsi; + markers[m].setIcon(createVesselMarkerIcon(heading, vesselType, isSelected)); } }); @@ -595,13 +648,13 @@ function showVesselDetails(vessel) { const container = document.getElementById('selectedInfo'); - const icon = getShipIcon(vessel.ship_type); + const iconSvg = getShipIconSvg(vessel.ship_type, 28); const category = getShipCategory(vessel.ship_type); const navStatus = NAV_STATUS[vessel.nav_status] || vessel.nav_status_text || 'Unknown'; container.innerHTML = `
-
${icon}
+
${iconSvg}
${vessel.name || 'Unknown Vessel'}
MMSI: ${vessel.mmsi}
@@ -676,12 +729,12 @@ } container.innerHTML = vesselArray.map(v => { - const icon = getShipIcon(v.ship_type); + const iconSvg = getShipIconSvg(v.ship_type, 20); const category = getShipCategory(v.ship_type); return `
-
${icon}
+
${iconSvg}
${v.name || 'Unknown'}
${category} | ${v.mmsi}
diff --git a/utils/sdr/rtlsdr.py b/utils/sdr/rtlsdr.py index fbd9522..b5d8451 100644 --- a/utils/sdr/rtlsdr.py +++ b/utils/sdr/rtlsdr.py @@ -179,7 +179,7 @@ class RTLSDRCommandBuilder(CommandBuilder): cmd = [ 'AIS-catcher', f'-d:{device.index}', # Device index (colon format required) - '-S', str(tcp_port), 'JSON', # TCP server with JSON output + '-S', str(tcp_port), 'JSON_FULL', 'on', # TCP server with full JSON output '-q', # Quiet mode (less console output) ]