mirror of
https://github.com/smittix/intercept.git
synced 2026-07-24 08:58:09 -07:00
New svg style icons for the AIS vessel tracking map
This commit is contained in:
@@ -495,9 +495,8 @@ body {
|
|||||||
}
|
}
|
||||||
|
|
||||||
.no-vessel-icon {
|
.no-vessel-icon {
|
||||||
font-size: 36px;
|
|
||||||
margin-bottom: 10px;
|
margin-bottom: 10px;
|
||||||
opacity: 0.5;
|
color: var(--accent-cyan);
|
||||||
}
|
}
|
||||||
|
|
||||||
.vessel-header {
|
.vessel-header {
|
||||||
@@ -508,7 +507,9 @@ body {
|
|||||||
}
|
}
|
||||||
|
|
||||||
.vessel-icon {
|
.vessel-icon {
|
||||||
font-size: 32px;
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
}
|
}
|
||||||
|
|
||||||
.vessel-name {
|
.vessel-name {
|
||||||
@@ -595,7 +596,10 @@ body {
|
|||||||
}
|
}
|
||||||
|
|
||||||
.vessel-item-icon {
|
.vessel-item-icon {
|
||||||
font-size: 20px;
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
width: 24px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.vessel-item-info {
|
.vessel-item-info {
|
||||||
@@ -747,19 +751,12 @@ body {
|
|||||||
border: none;
|
border: none;
|
||||||
}
|
}
|
||||||
|
|
||||||
.vessel-marker-inner {
|
.vessel-marker svg {
|
||||||
width: 24px;
|
transition: filter 0.2s ease;
|
||||||
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.selected .vessel-marker-inner {
|
.vessel-marker.selected svg {
|
||||||
filter: drop-shadow(0 0 6px var(--accent-cyan));
|
filter: drop-shadow(0 0 8px rgba(255,255,255,0.8)) !important;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Range rings */
|
/* Range rings */
|
||||||
|
|||||||
+107
-54
@@ -78,7 +78,11 @@
|
|||||||
</div>
|
</div>
|
||||||
<div class="selected-info" id="selectedInfo">
|
<div class="selected-info" id="selectedInfo">
|
||||||
<div class="no-vessel">
|
<div class="no-vessel">
|
||||||
<div class="no-vessel-icon">🚢</div>
|
<div class="no-vessel-icon">
|
||||||
|
<svg width="32" height="32" viewBox="0 0 24 24" style="opacity: 0.5;">
|
||||||
|
<path fill="currentColor" d="M12 2L8 6V18L10 20H14L16 18V6L12 2Z"/>
|
||||||
|
</svg>
|
||||||
|
</div>
|
||||||
<div>Select a vessel</div>
|
<div>Select a vessel</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -204,34 +208,48 @@
|
|||||||
let messageRateInterval = null;
|
let messageRateInterval = null;
|
||||||
let lastMessageCount = 0;
|
let lastMessageCount = 0;
|
||||||
|
|
||||||
// Ship type to icon mapping
|
// Vessel SVG icon paths (top-down view, pointing up)
|
||||||
const SHIP_ICONS = {
|
const VESSEL_ICONS = {
|
||||||
30: '🐟', // Fishing
|
// Generic cargo/container ship - pointed bow, rectangular hull
|
||||||
31: '🚢', // Towing
|
cargo: 'M12 2L8 6V18L10 20H14L16 18V6L12 2ZM10 8H14V16H10V8Z',
|
||||||
32: '🚢', // Towing
|
// Tanker - rounded bow, long hull
|
||||||
36: '⛵', // Sailing
|
tanker: 'M12 2C10 2 8 4 8 6V18C8 19 9 20 10 20H14C15 20 16 19 16 18V6C16 4 14 2 12 2ZM10 8H14V16H10V8Z',
|
||||||
37: '⛵', // Pleasure craft
|
// Passenger/cruise - multiple decks indicated
|
||||||
60: '🚢', // Passenger
|
passenger: 'M12 2L8 5V18L10 20H14L16 18V5L12 2ZM9 7H15V10H9V7ZM9 11H15V14H9V11ZM9 15H15V18H9V15Z',
|
||||||
61: '🚢', // Passenger
|
// Tug - small, compact, powerful
|
||||||
62: '🚢', // Passenger
|
tug: 'M12 4L9 7V16L10 18H14L15 16V7L12 4ZM10 9H14V14H10V9Z',
|
||||||
63: '🚢', // Passenger
|
// Fishing vessel - with mast/outriggers
|
||||||
64: '🚢', // Passenger
|
fishing: 'M12 2L12 5L8 8V17L10 19H14L16 17V8L12 5ZM6 10L8 12V15L6 13V10ZM18 10V13L16 15V12L18 10ZM10 10H14V15H10V10Z',
|
||||||
65: '🚢', // Passenger
|
// Sailing vessel - sail shape
|
||||||
66: '🚢', // Passenger
|
sailing: 'M12 2L12 6L8 10V18L10 20H14L16 18V10L12 6ZM12 3L16 8H12V3ZM10 11H14V17H10V11Z',
|
||||||
67: '🚢', // Passenger
|
// Military - angular, aggressive bow
|
||||||
68: '🚢', // Passenger
|
military: 'M12 1L7 6V8L8 9V18L10 20H14L16 18V9L17 8V6L12 1ZM10 10H14V16H10V10Z',
|
||||||
69: '🚢', // Passenger
|
// High speed craft - sleek, pointed
|
||||||
70: '🚢', // Cargo
|
hsc: 'M12 1L9 5V18L10 20H14L15 18V5L12 1ZM10 7H14V17H10V7Z',
|
||||||
71: '🚢', // Cargo - hazardous A
|
// Search & rescue - distinctive cross marking
|
||||||
72: '🚢', // Cargo - hazardous B
|
sar: 'M12 2L8 6V18L10 20H14L16 18V6L12 2ZM11 8H13V11H16V13H13V16H11V13H8V11H11V8Z',
|
||||||
73: '🚢', // Cargo - hazardous C
|
// Pilot vessel
|
||||||
74: '🚢', // Cargo - hazardous D
|
pilot: 'M12 3L9 6V17L10 19H14L15 17V6L12 3ZM10 8H14V15H10V8ZM11 9V10H13V9H11Z',
|
||||||
80: '🚢', // Tanker
|
// Law enforcement
|
||||||
81: '🚢', // Tanker - hazardous A
|
law: 'M12 2L8 6V18L10 20H14L16 18V6L12 2ZM10 8H14V10H10V8ZM11 11H13V16H11V11Z',
|
||||||
82: '🚢', // Tanker - hazardous B
|
// Generic vessel (default)
|
||||||
83: '🚢', // Tanker - hazardous C
|
default: 'M12 2L8 6V18L10 20H14L16 18V6L12 2Z'
|
||||||
84: '🚢', // Tanker - hazardous D
|
};
|
||||||
default: '🚢' // Generic ship
|
|
||||||
|
// 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
|
// Ship type categories
|
||||||
@@ -255,8 +273,50 @@
|
|||||||
return 'Other';
|
return 'Other';
|
||||||
}
|
}
|
||||||
|
|
||||||
function getShipIcon(type) {
|
// Get vessel icon type from AIS ship type code
|
||||||
return SHIP_ICONS[type] || SHIP_ICONS.default;
|
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: `<svg width="${size}" height="${size}" viewBox="0 0 24 24" style="transform: rotate(${rotation}deg); filter: drop-shadow(0 0 ${glowSize} ${glowColor});">
|
||||||
|
<path fill="${color}" d="${path}"/>
|
||||||
|
</svg>`,
|
||||||
|
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 `<svg width="${size}" height="${size}" viewBox="0 0 24 24" style="vertical-align: middle;">
|
||||||
|
<path fill="${color}" d="${path}"/>
|
||||||
|
</svg>`;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Navigation status text
|
// Navigation status text
|
||||||
@@ -544,20 +604,9 @@
|
|||||||
if (!vessel.lat || !vessel.lon) return;
|
if (!vessel.lat || !vessel.lon) return;
|
||||||
|
|
||||||
const heading = vessel.heading || vessel.course || 0;
|
const heading = vessel.heading || vessel.course || 0;
|
||||||
const icon = getShipIcon(vessel.ship_type);
|
const vesselType = getVesselIconType(vessel.ship_type);
|
||||||
|
const isSelected = mmsi === selectedMmsi;
|
||||||
const markerHtml = `
|
const divIcon = createVesselMarkerIcon(heading, vesselType, isSelected);
|
||||||
<div class="vessel-marker-inner" style="transform: rotate(${heading}deg);">
|
|
||||||
${icon}
|
|
||||||
</div>
|
|
||||||
`;
|
|
||||||
|
|
||||||
const divIcon = L.divIcon({
|
|
||||||
className: 'vessel-marker' + (mmsi === selectedMmsi ? ' selected' : ''),
|
|
||||||
html: markerHtml,
|
|
||||||
iconSize: [24, 24],
|
|
||||||
iconAnchor: [12, 12]
|
|
||||||
});
|
|
||||||
|
|
||||||
if (markers[mmsi]) {
|
if (markers[mmsi]) {
|
||||||
markers[mmsi].setLatLng([vessel.lat, vessel.lon]);
|
markers[mmsi].setLatLng([vessel.lat, vessel.lon]);
|
||||||
@@ -573,13 +622,17 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
function selectVessel(mmsi) {
|
function selectVessel(mmsi) {
|
||||||
|
const prevSelected = selectedMmsi;
|
||||||
selectedMmsi = mmsi;
|
selectedMmsi = mmsi;
|
||||||
|
|
||||||
// Update marker styles
|
// Update marker icons for previous and new selection
|
||||||
Object.keys(markers).forEach(m => {
|
[prevSelected, mmsi].forEach(m => {
|
||||||
const el = markers[m].getElement();
|
if (m && vessels[m] && markers[m]) {
|
||||||
if (el) {
|
const vessel = vessels[m];
|
||||||
el.querySelector('.vessel-marker-inner')?.parentElement?.classList.toggle('selected', m === mmsi);
|
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) {
|
function showVesselDetails(vessel) {
|
||||||
const container = document.getElementById('selectedInfo');
|
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 category = getShipCategory(vessel.ship_type);
|
||||||
const navStatus = NAV_STATUS[vessel.nav_status] || vessel.nav_status_text || 'Unknown';
|
const navStatus = NAV_STATUS[vessel.nav_status] || vessel.nav_status_text || 'Unknown';
|
||||||
|
|
||||||
container.innerHTML = `
|
container.innerHTML = `
|
||||||
<div class="vessel-header">
|
<div class="vessel-header">
|
||||||
<div class="vessel-icon">${icon}</div>
|
<div class="vessel-icon">${iconSvg}</div>
|
||||||
<div>
|
<div>
|
||||||
<div class="vessel-name">${vessel.name || 'Unknown Vessel'}</div>
|
<div class="vessel-name">${vessel.name || 'Unknown Vessel'}</div>
|
||||||
<div class="vessel-mmsi">MMSI: ${vessel.mmsi}</div>
|
<div class="vessel-mmsi">MMSI: ${vessel.mmsi}</div>
|
||||||
@@ -676,12 +729,12 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
container.innerHTML = vesselArray.map(v => {
|
container.innerHTML = vesselArray.map(v => {
|
||||||
const icon = getShipIcon(v.ship_type);
|
const iconSvg = getShipIconSvg(v.ship_type, 20);
|
||||||
const category = getShipCategory(v.ship_type);
|
const category = getShipCategory(v.ship_type);
|
||||||
return `
|
return `
|
||||||
<div class="vessel-item ${v.mmsi === selectedMmsi ? 'selected' : ''}"
|
<div class="vessel-item ${v.mmsi === selectedMmsi ? 'selected' : ''}"
|
||||||
data-mmsi="${v.mmsi}" onclick="selectVessel('${v.mmsi}')">
|
data-mmsi="${v.mmsi}" onclick="selectVessel('${v.mmsi}')">
|
||||||
<div class="vessel-item-icon">${icon}</div>
|
<div class="vessel-item-icon">${iconSvg}</div>
|
||||||
<div class="vessel-item-info">
|
<div class="vessel-item-info">
|
||||||
<div class="vessel-item-name">${v.name || 'Unknown'}</div>
|
<div class="vessel-item-name">${v.name || 'Unknown'}</div>
|
||||||
<div class="vessel-item-type">${category} | ${v.mmsi}</div>
|
<div class="vessel-item-type">${category} | ${v.mmsi}</div>
|
||||||
|
|||||||
@@ -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!")
|
|
||||||
Reference in New Issue
Block a user