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
+ GPS
@@ -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();
+ });