diff --git a/static/css/ais_dashboard.css b/static/css/ais_dashboard.css index d564cf8..87ead8d 100644 --- a/static/css/ais_dashboard.css +++ b/static/css/ais_dashboard.css @@ -1201,3 +1201,33 @@ body { font-size: 18px; } } + +/* GPS Indicator */ +.gps-indicator { + display: inline-flex; + align-items: center; + gap: 4px; + padding: 2px 8px; + background: rgba(34, 197, 94, 0.15); + border: 1px solid #22c55e; + border-radius: 12px; + font-size: 10px; + font-weight: 600; + color: #22c55e; + text-transform: uppercase; + letter-spacing: 0.5px; + margin-left: 10px; +} + +.gps-indicator .gps-dot { + width: 6px; + height: 6px; + background: #22c55e; + border-radius: 50%; + animation: gps-pulse 2s ease-in-out infinite; +} + +@keyframes gps-pulse { + 0%, 100% { opacity: 1; transform: scale(1); } + 50% { opacity: 0.5; transform: scale(0.8); } +} diff --git a/templates/ais_dashboard.html b/templates/ais_dashboard.html index c4fd8a0..ea32c21 100644 --- a/templates/ais_dashboard.html +++ b/templates/ais_dashboard.html @@ -84,6 +84,7 @@ STANDBY
--:--:-- UTC
+ @@ -217,6 +218,11 @@ let rangeRingsLayer = null; let observerMarker = null; + // GPS state + let gpsConnected = false; + let gpsEventSource = null; + let gpsReconnectTimeout = null; + // Statistics let stats = { totalVesselsSeen: new Set(), @@ -948,6 +954,106 @@ document.getElementById('utcTime').textContent = utc + ' UTC'; } + // ============================================ + // GPS FUNCTIONS (gpsd auto-connect) + // ============================================ + async function autoConnectGps() { + try { + const response = await fetch('/gps/auto-connect', { method: 'POST' }); + const data = await response.json(); + + if (data.status === 'connected') { + gpsConnected = true; + startGpsStream(); + showGpsIndicator(true); + console.log('GPS: Auto-connected to gpsd'); + if (data.position) { + updateLocationFromGps(data.position); + } + } else { + console.log('GPS: gpsd not available -', data.message); + } + } catch (e) { + console.log('GPS: Auto-connect failed -', e.message); + } + } + + function startGpsStream() { + if (gpsEventSource) { + gpsEventSource.close(); + } + if (gpsReconnectTimeout) { + clearTimeout(gpsReconnectTimeout); + gpsReconnectTimeout = null; + } + + gpsEventSource = new EventSource('/gps/stream'); + gpsEventSource.onmessage = (event) => { + try { + const data = JSON.parse(event.data); + if (data.type === 'position' && data.latitude && data.longitude) { + updateLocationFromGps(data); + } + } catch (e) { + console.error('GPS parse error:', e); + } + }; + gpsEventSource.onerror = (e) => { + // Don't log every error - connection suspends are normal + if (gpsEventSource) { + gpsEventSource.close(); + gpsEventSource = null; + } + // Auto-reconnect after 5 seconds if still connected + if (gpsConnected && !gpsReconnectTimeout) { + gpsReconnectTimeout = setTimeout(() => { + gpsReconnectTimeout = null; + if (gpsConnected) { + startGpsStream(); + } + }, 5000); + } + }; + } + + // Reconnect GPS stream when tab becomes visible + document.addEventListener('visibilitychange', () => { + if (!document.hidden && gpsConnected && !gpsEventSource) { + startGpsStream(); + } + }); + + function updateLocationFromGps(position) { + observerLocation.lat = position.latitude; + observerLocation.lon = position.longitude; + document.getElementById('obsLat').value = position.latitude.toFixed(4); + document.getElementById('obsLon').value = position.longitude.toFixed(4); + + // Update observer marker position + if (observerMarker) { + observerMarker.setLatLng([position.latitude, position.longitude]); + } + + // Center map on GPS location (on first fix) + if (vesselMap && !vesselMap._gpsInitialized) { + vesselMap.setView([position.latitude, position.longitude], vesselMap.getZoom()); + vesselMap._gpsInitialized = true; + } + + // Redraw range rings at new position + drawRangeRings(); + + // Save to localStorage + localStorage.setItem('ais_observerLocation', JSON.stringify(observerLocation)); + } + + function showGpsIndicator(show) { + const indicator = document.getElementById('gpsIndicator'); + if (indicator) { + indicator.style.display = show ? 'inline-flex' : 'none'; + } + } + // Session timer functions function startSessionTimer() { if (!stats.sessionStart) { @@ -1344,7 +1450,11 @@ } // Initialize - document.addEventListener('DOMContentLoaded', initMap); + document.addEventListener('DOMContentLoaded', function() { + initMap(); + // Auto-connect to gpsd if available + autoConnectGps(); + });