From 7c6416ac389997cd8e5e3fa393c2e1a2c549092c Mon Sep 17 00:00:00 2001 From: Marc Date: Sun, 25 Jan 2026 13:40:52 -0600 Subject: [PATCH] New svg style icons for the AIS vessel tracking map --- static/css/ais_dashboard.css | 27 ++-- templates/ais_dashboard.html | 161 +++++++++++++++-------- test_ais_local.py | 248 ----------------------------------- 3 files changed, 119 insertions(+), 317 deletions(-) delete mode 100644 test_ais_local.py 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/test_ais_local.py b/test_ais_local.py deleted file mode 100644 index b22cfbe..0000000 --- a/test_ais_local.py +++ /dev/null @@ -1,248 +0,0 @@ -#!/usr/bin/env python3 -""" -Local test script to simulate AIS-catcher TCP JSON output. -This helps verify the AIS parsing and vessel display without real hardware. - -Usage: - Terminal 1: python test_ais_local.py --server (starts mock AIS-catcher) - Terminal 2: sudo -E venv/bin/python intercept.py (start the app) - Then click "Start Tracking" in the AIS page - it should show test vessels -""" - -import argparse -import json -import socket -import time -import random -import threading - - -# Sample vessel data mimicking AIS-catcher JSON_FULL output -# Uses 'latitude'/'longitude' as per AIS-catcher JSON_FULL format -SAMPLE_VESSELS = [ - { - "mmsi": 316039000, - "shipname": "ATLANTIC EAGLE", - "callsign": "CFG4521", - "shiptype": 70, - "shiptype_text": "Cargo", - "latitude": 45.5017, - "longitude": -73.5673, - "speed": 12.3, - "course": 45.0, - "heading": 47, - "status": 0, - "status_text": "Under way using engine", - "destination": "MONTREAL", - "to_bow": 150, - "to_stern": 30, - "to_port": 15, - "to_starboard": 15, - "type": 1 - }, - { - "mmsi": 316007861, - "shipname": "PACIFIC STAR", - "callsign": "CFG9912", - "shiptype": 60, - "shiptype_text": "Passenger", - "latitude": 45.4817, - "longitude": -73.5873, - "speed": 8.5, - "course": 270.0, - "heading": 268, - "status": 0, - "status_text": "Under way using engine", - "destination": "QUEBEC CITY", - "to_bow": 200, - "to_stern": 50, - "to_port": 20, - "to_starboard": 20, - "type": 1 - }, - { - "mmsi": 316001103, - "shipname": "RIVER QUEEN", - "callsign": "CFG1234", - "shiptype": 52, - "shiptype_text": "Tug", - "latitude": 45.5117, - "longitude": -73.5473, - "speed": 5.2, - "course": 180.0, - "heading": 182, - "status": 0, - "status_text": "Under way using engine", - "destination": "SOREL", - "to_bow": 25, - "to_stern": 10, - "to_port": 5, - "to_starboard": 5, - "type": 1 - }, -] - - -def update_vessel_position(vessel): - """Simulate vessel movement.""" - # Small random movement - vessel["latitude"] += random.uniform(-0.001, 0.001) - vessel["longitude"] += random.uniform(-0.001, 0.001) - # Small speed variation - vessel["speed"] = max(0, vessel["speed"] + random.uniform(-0.5, 0.5)) - # Slight course change - vessel["course"] = (vessel["course"] + random.uniform(-2, 2)) % 360 - vessel["heading"] = int(vessel["course"]) % 360 - return vessel - - -def mock_ais_server(port=10110): - """Run a mock AIS-catcher TCP server sending JSON.""" - server = socket.socket(socket.AF_INET, socket.SOCK_STREAM) - server.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1) - server.bind(('localhost', port)) - server.listen(5) - print(f"Mock AIS-catcher TCP server running on port {port}") - print(f"Sending JSON format (like 'AIS-catcher -S {port} JSON')") - print("Waiting for connections...") - - clients = [] - - def handle_client(client_sock, addr): - print(f"Client connected: {addr}") - clients.append(client_sock) - try: - while True: - # Keep connection alive, actual sending is done in broadcast - time.sleep(1) - except Exception as e: - print(f"Client {addr} disconnected: {e}") - finally: - if client_sock in clients: - clients.remove(client_sock) - client_sock.close() - - def broadcast_vessels(): - """Periodically send vessel updates to all clients.""" - vessels = [v.copy() for v in SAMPLE_VESSELS] - while True: - for vessel in vessels: - vessel = update_vessel_position(vessel) - json_line = json.dumps(vessel) + "\n" - - dead_clients = [] - for client in clients: - try: - client.send(json_line.encode('utf-8')) - except Exception: - dead_clients.append(client) - - for client in dead_clients: - clients.remove(client) - - if clients: - print(f"Sent: MMSI {vessel['mmsi']} @ ({vessel['latitude']:.4f}, {vessel['longitude']:.4f})") - - time.sleep(2) # Send updates every 2 seconds - - # Start broadcast thread - broadcast_thread = threading.Thread(target=broadcast_vessels, daemon=True) - broadcast_thread.start() - - # Accept connections - while True: - try: - client_sock, addr = server.accept() - thread = threading.Thread(target=handle_client, args=(client_sock, addr), daemon=True) - thread.start() - except KeyboardInterrupt: - print("\nShutting down...") - break - - -def test_parse_json(): - """Test that our JSON matches what the parser expects.""" - # Import the parser - import sys - sys.path.insert(0, '/opt/intercept') - from routes.ais import process_ais_message - - print("Testing JSON parsing...") - for vessel_data in SAMPLE_VESSELS: - result = process_ais_message(vessel_data) - if result: - print(f" MMSI {result['mmsi']}: {result.get('name', 'Unknown')} @ ({result.get('lat')}, {result.get('lon')})") - assert result.get('lat') is not None, "lat should be set" - assert result.get('lon') is not None, "lon should be set" - assert result.get('name') is not None, "name should be set" - else: - print(f" FAILED to parse: {vessel_data}") - print("All JSON parsing tests passed!") - - -def test_tcp_client(): - """Test connecting to the mock server as a client.""" - print("Connecting to mock AIS server on localhost:10110...") - sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM) - sock.settimeout(10) - - try: - sock.connect(('localhost', 10110)) - print("Connected! Receiving data...") - - buffer = "" - received = 0 - while received < 5: - data = sock.recv(4096).decode('utf-8') - if not data: - break - buffer += data - - while '\n' in buffer: - line, buffer = buffer.split('\n', 1) - line = line.strip() - if line: - try: - msg = json.loads(line) - print(f" Received: MMSI {msg.get('mmsi')} - {msg.get('shipname')}") - received += 1 - except json.JSONDecodeError as e: - print(f" JSON ERROR: {e}") - print(f" Line was: {line[:100]}") - - print(f"Successfully received {received} vessel updates!") - except socket.timeout: - print("Connection timed out - is the mock server running?") - except ConnectionRefusedError: - print("Connection refused - start the mock server first with: python test_ais_local.py --server") - finally: - sock.close() - - -if __name__ == "__main__": - parser = argparse.ArgumentParser(description="Test AIS functionality locally") - parser.add_argument("--server", action="store_true", help="Run mock AIS-catcher TCP server") - parser.add_argument("--client", action="store_true", help="Test TCP client connection") - parser.add_argument("--parse", action="store_true", help="Test JSON parsing") - parser.add_argument("--port", type=int, default=10110, help="TCP port (default: 10110)") - - args = parser.parse_args() - - if args.server: - mock_ais_server(args.port) - elif args.client: - test_tcp_client() - elif args.parse: - test_parse_json() - else: - print("Usage:") - print(" python test_ais_local.py --server # Start mock AIS-catcher") - print(" python test_ais_local.py --client # Test client connection") - print(" python test_ais_local.py --parse # Test JSON parsing") - print() - print("Full test workflow:") - print(" 1. Terminal 1: python test_ais_local.py --server") - print(" 2. Terminal 2: python test_ais_local.py --client (verify mock works)") - print(" 3. Terminal 2: sudo -E venv/bin/python intercept.py") - print(" 4. Browser: Open AIS page and click 'Start Tracking'") - print(" 5. Vessels should appear on the map!")