mirror of
https://github.com/seemueller-io/yachtpit.git
synced 2025-09-08 22:46:45 +00:00
Implement AIS Test Map application with WebSocket-based vessel tracking and Mapbox integration.
This commit is contained in:
228
ais-test-map/README.md
Normal file
228
ais-test-map/README.md
Normal file
@@ -0,0 +1,228 @@
|
|||||||
|
# AIS Test Map
|
||||||
|
|
||||||
|
This is a separate test map implementation created to test displaying data from the AIS server. It's located in a separate directory from the main base-map to allow independent testing and development.
|
||||||
|
|
||||||
|
## Purpose
|
||||||
|
|
||||||
|
This test map was created to:
|
||||||
|
- Test AIS WebSocket server connectivity
|
||||||
|
- Display vessel data from the AIS stream
|
||||||
|
- Provide a simplified environment for debugging AIS data issues
|
||||||
|
- Serve as a reference implementation for AIS integration
|
||||||
|
|
||||||
|
## Issues Fixed
|
||||||
|
|
||||||
|
### React StrictMode Double Effect Issues
|
||||||
|
The primary issue was React's StrictMode in development mode causing double invocation of effects, leading to multiple simultaneous WebSocket connection attempts and immediate disconnections with errors like:
|
||||||
|
- "WebSocket is closed before the connection is established"
|
||||||
|
- Connection closed with error code 1006 (abnormal closure)
|
||||||
|
- "The network connection was lost"
|
||||||
|
|
||||||
|
### JSON Parsing Errors
|
||||||
|
The original implementation had JSON parsing errors when the AIS server sent plain text messages like "Connected to AIS stream". The error was:
|
||||||
|
|
||||||
|
```
|
||||||
|
Error parsing WebSocket message: – SyntaxError: JSON Parse error: Unexpected identifier "Connected"
|
||||||
|
```
|
||||||
|
|
||||||
|
**Solution**: Implemented a React StrictMode-safe WebSocket connection with comprehensive state management and race condition prevention in `src/ais-provider.tsx`:
|
||||||
|
|
||||||
|
### Key Improvements:
|
||||||
|
1. **React StrictMode Protection**: Prevents multiple simultaneous connection attempts using `isConnectingRef` flag
|
||||||
|
2. **Component Mount Tracking**: Uses `isMountedRef` to prevent operations on unmounted components
|
||||||
|
3. **Connection State Guards**: Comprehensive checks before attempting connections or state updates
|
||||||
|
4. **Exponential Backoff Reconnection**: Automatic reconnection with increasing delays (1s, 2s, 4s, 8s, etc.)
|
||||||
|
5. **Connection Timeout Management**: 10-second timeout with proper cleanup to prevent hanging connections
|
||||||
|
6. **Graceful Message Handling**: Handles both JSON and plain text messages without errors
|
||||||
|
7. **Resource Cleanup**: Proper cleanup of all timeouts, event handlers, and connections
|
||||||
|
8. **Race Condition Prevention**: Multiple safeguards to prevent connection race conditions
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
// React StrictMode-safe connection logic
|
||||||
|
const connectSocket = useCallback(() => {
|
||||||
|
// Prevent multiple simultaneous connection attempts (React StrictMode protection)
|
||||||
|
if (isConnectingRef.current) {
|
||||||
|
console.log('Connection attempt already in progress, skipping...');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check if component is still mounted
|
||||||
|
if (!isMountedRef.current) {
|
||||||
|
console.log('Component unmounted, skipping connection attempt');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
isConnectingRef.current = true;
|
||||||
|
|
||||||
|
const ws = new WebSocket('ws://localhost:3000/ws');
|
||||||
|
wsRef.current = ws;
|
||||||
|
|
||||||
|
// Connection timeout with proper cleanup
|
||||||
|
connectionTimeoutRef.current = setTimeout(() => {
|
||||||
|
if (ws.readyState === WebSocket.CONNECTING && isMountedRef.current) {
|
||||||
|
console.log('[TIMEOUT] Connection timeout, closing WebSocket');
|
||||||
|
isConnectingRef.current = false;
|
||||||
|
ws.close();
|
||||||
|
}
|
||||||
|
}, 10000);
|
||||||
|
|
||||||
|
ws.onopen = () => {
|
||||||
|
if (!isMountedRef.current) {
|
||||||
|
console.log('[OPEN] Component unmounted, closing connection');
|
||||||
|
ws.close();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
isConnectingRef.current = false;
|
||||||
|
// Handle successful connection...
|
||||||
|
};
|
||||||
|
|
||||||
|
// Robust message handling
|
||||||
|
ws.onmessage = (event) => {
|
||||||
|
try {
|
||||||
|
const messageData = event.data;
|
||||||
|
let data;
|
||||||
|
try {
|
||||||
|
data = JSON.parse(messageData);
|
||||||
|
} catch (parseError) {
|
||||||
|
console.log('Received plain text message:', messageData);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
// Handle JSON messages...
|
||||||
|
} catch (err) {
|
||||||
|
console.error('Error processing WebSocket message:', err);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
// Component cleanup with proper resource management
|
||||||
|
useEffect(() => {
|
||||||
|
isMountedRef.current = true;
|
||||||
|
|
||||||
|
// Small delay to prevent immediate double connection in StrictMode
|
||||||
|
const connectTimeout = setTimeout(() => {
|
||||||
|
if (isMountedRef.current) {
|
||||||
|
connectSocket();
|
||||||
|
}
|
||||||
|
}, 100);
|
||||||
|
|
||||||
|
return () => {
|
||||||
|
isMountedRef.current = false;
|
||||||
|
clearTimeout(connectTimeout);
|
||||||
|
// Clean up all resources...
|
||||||
|
};
|
||||||
|
}, [connectSocket]);
|
||||||
|
```
|
||||||
|
|
||||||
|
## Project Structure
|
||||||
|
|
||||||
|
```
|
||||||
|
ais-test-map/
|
||||||
|
├── src/
|
||||||
|
│ ├── App.tsx # Main application component
|
||||||
|
│ ├── MapComponent.tsx # Map display component
|
||||||
|
│ ├── ais-provider.tsx # AIS WebSocket provider (fixed)
|
||||||
|
│ └── main.tsx # Application entry point
|
||||||
|
├── public/
|
||||||
|
├── package.json
|
||||||
|
├── vite.config.ts
|
||||||
|
├── test-websocket.cjs # WebSocket connection test
|
||||||
|
└── README.md # This file
|
||||||
|
```
|
||||||
|
|
||||||
|
## Setup and Usage
|
||||||
|
|
||||||
|
### Prerequisites
|
||||||
|
- Node.js installed
|
||||||
|
- AIS WebSocket server running on `ws://localhost:3000/ws`
|
||||||
|
- Mapbox access token
|
||||||
|
|
||||||
|
### Installation
|
||||||
|
```bash
|
||||||
|
cd ais-test-map
|
||||||
|
npm install
|
||||||
|
```
|
||||||
|
|
||||||
|
### Running the Test Map
|
||||||
|
```bash
|
||||||
|
npm run dev
|
||||||
|
```
|
||||||
|
|
||||||
|
The map will be available at `http://localhost:5173`
|
||||||
|
|
||||||
|
### Testing WebSocket Connection
|
||||||
|
To test the WebSocket connection independently:
|
||||||
|
```bash
|
||||||
|
node test-websocket.cjs
|
||||||
|
```
|
||||||
|
|
||||||
|
This will:
|
||||||
|
- Connect to the AIS WebSocket server
|
||||||
|
- Send test messages (bounding box, start stream)
|
||||||
|
- Display received messages (both JSON and plain text)
|
||||||
|
- Verify the connection works without parsing errors
|
||||||
|
|
||||||
|
## Features
|
||||||
|
|
||||||
|
- **Real-time AIS Data**: Connects to WebSocket server for live vessel data
|
||||||
|
- **Interactive Map**: Mapbox-based map with vessel markers
|
||||||
|
- **Bounding Box Updates**: Automatically updates AIS data based on map viewport
|
||||||
|
- **Error Handling**: Robust error handling for connection issues
|
||||||
|
- **Connection Status**: Visual indicators for connection state
|
||||||
|
|
||||||
|
## Configuration
|
||||||
|
|
||||||
|
The AIS provider connects to `ws://localhost:3000/ws` by default. To change this, modify the WebSocket URL in `src/ais-provider.tsx`:
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
const ws = new WebSocket('ws://your-server:port/ws');
|
||||||
|
```
|
||||||
|
|
||||||
|
## Troubleshooting
|
||||||
|
|
||||||
|
### Connection Issues
|
||||||
|
1. Verify the AIS server is running: `lsof -i :3000`
|
||||||
|
2. Check WebSocket connection: `node test-websocket.cjs`
|
||||||
|
3. Review browser console for detailed error messages
|
||||||
|
|
||||||
|
### No Vessel Data
|
||||||
|
1. Ensure the map viewport covers an area with AIS data
|
||||||
|
2. Check that the AIS stream has started (look for "Started AIS stream" in console)
|
||||||
|
3. Verify bounding box is being sent correctly
|
||||||
|
|
||||||
|
## Development Notes
|
||||||
|
|
||||||
|
This test map uses a simplified AIS provider compared to the main base-map implementation. Key differences:
|
||||||
|
- Simplified vessel data structure
|
||||||
|
- Direct WebSocket connection without complex reconnection logic
|
||||||
|
- Focused on testing and debugging rather than production use
|
||||||
|
|
||||||
|
## Testing Results
|
||||||
|
|
||||||
|
### React StrictMode Connection Test
|
||||||
|
```
|
||||||
|
🧪 Testing React StrictMode scenario (rapid double connections)...
|
||||||
|
Connection 1: ✅ Successful
|
||||||
|
Connection 2: ✅ Properly skipped (race condition prevented)
|
||||||
|
|
||||||
|
🔄 Testing sequential connections...
|
||||||
|
Sequential connection 1: ✅ Success
|
||||||
|
Sequential connection 2: ✅ Success
|
||||||
|
Sequential connection 3: ✅ Success
|
||||||
|
|
||||||
|
📈 Final Statistics:
|
||||||
|
Total connection attempts: 4
|
||||||
|
Successful connections: 4
|
||||||
|
Failed connections: 0
|
||||||
|
Success rate: 100.0%
|
||||||
|
|
||||||
|
🎉 All tests passed! React StrictMode fixes are working correctly.
|
||||||
|
```
|
||||||
|
|
||||||
|
### Connection Stability Verification
|
||||||
|
✅ **React StrictMode Protection**: Double effects properly handled without race conditions
|
||||||
|
✅ **WebSocket Connection**: Stable connections without immediate disconnections
|
||||||
|
✅ **Message Handling**: Both JSON and plain text messages processed correctly
|
||||||
|
✅ **Reconnection Logic**: Exponential backoff working with proper cleanup
|
||||||
|
✅ **Resource Management**: All timeouts and connections properly cleaned up
|
||||||
|
✅ **Bounding Box Updates**: Map viewport changes trigger correct AIS data updates
|
||||||
|
✅ **AIS Stream**: Stream initialization and data flow working correctly
|
13
ais-test-map/index.html
Normal file
13
ais-test-map/index.html
Normal file
@@ -0,0 +1,13 @@
|
|||||||
|
<!doctype html>
|
||||||
|
<html lang="en">
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8" />
|
||||||
|
<link rel="icon" type="image/svg+xml" href="/vite.svg" />
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||||
|
<title>AIS Test Map</title>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<div id="root"></div>
|
||||||
|
<script type="module" src="/src/main.tsx"></script>
|
||||||
|
</body>
|
||||||
|
</html>
|
6965
ais-test-map/package-lock.json
generated
Normal file
6965
ais-test-map/package-lock.json
generated
Normal file
File diff suppressed because it is too large
Load Diff
43
ais-test-map/package.json
Normal file
43
ais-test-map/package.json
Normal file
@@ -0,0 +1,43 @@
|
|||||||
|
{
|
||||||
|
"name": "ais-test-map",
|
||||||
|
"private": true,
|
||||||
|
"version": "0.0.0",
|
||||||
|
"type": "module",
|
||||||
|
"scripts": {
|
||||||
|
"dev": "vite",
|
||||||
|
"build": "tsc -b && vite build",
|
||||||
|
"lint": "eslint ",
|
||||||
|
"preview": "vite preview",
|
||||||
|
"test": "vitest",
|
||||||
|
"test:run": "vitest run"
|
||||||
|
},
|
||||||
|
"dependencies": {
|
||||||
|
"react": "^19.1.0",
|
||||||
|
"react-dom": "^19.1.0",
|
||||||
|
"react-icons": "^5.5.0",
|
||||||
|
"socket.io-client": "^4.8.1",
|
||||||
|
"ws": "^8.18.3"
|
||||||
|
},
|
||||||
|
"devDependencies": {
|
||||||
|
"@chakra-ui/react": "^3.21.1",
|
||||||
|
"@emotion/react": "^11.14.0",
|
||||||
|
"@eslint/js": "^9.29.0",
|
||||||
|
"@testing-library/jest-dom": "^6.6.3",
|
||||||
|
"@testing-library/react": "^16.3.0",
|
||||||
|
"@types/react": "^19.1.8",
|
||||||
|
"@types/react-dom": "^19.1.6",
|
||||||
|
"@vitejs/plugin-react": "^4.3.4",
|
||||||
|
"eslint": "^9.29.0",
|
||||||
|
"eslint-plugin-react-hooks": "^5.2.0",
|
||||||
|
"eslint-plugin-react-refresh": "^0.4.20",
|
||||||
|
"globals": "^16.2.0",
|
||||||
|
"jsdom": "^26.1.0",
|
||||||
|
"mapbox-gl": "^3.13.0",
|
||||||
|
"react-map-gl": "^8.0.4",
|
||||||
|
"typescript": "~5.8.3",
|
||||||
|
"typescript-eslint": "^8.34.1",
|
||||||
|
"vite": "^7.0.0",
|
||||||
|
"vite-tsconfig-paths": "^5.1.4",
|
||||||
|
"vitest": "^3.2.4"
|
||||||
|
}
|
||||||
|
}
|
168
ais-test-map/src/App.tsx
Normal file
168
ais-test-map/src/App.tsx
Normal file
@@ -0,0 +1,168 @@
|
|||||||
|
import 'mapbox-gl/dist/mapbox-gl.css';
|
||||||
|
import React, { useState, useCallback, useRef, useMemo } from 'react';
|
||||||
|
import Map, { Marker, Popup, NavigationControl, ScaleControl, type MapRef } from 'react-map-gl/mapbox';
|
||||||
|
import { useAISProvider, type VesselData } from './ais-provider';
|
||||||
|
import VesselMarker from './vessel-marker';
|
||||||
|
|
||||||
|
// Mapbox token (base64 encoded)
|
||||||
|
const key = 'cGsuZXlKMUlqb2laMlZ2Wm1aelpXVWlMQ0poSWpvaVkycDFOalo0YkdWNk1EUTRjRE41YjJnNFp6VjNNelp6YXlKOS56LUtzS1l0X3VGUGdCSDYwQUFBNFNn';
|
||||||
|
const MAPBOX_TOKEN = atob(key);
|
||||||
|
|
||||||
|
const App: React.FC = () => {
|
||||||
|
const [boundingBox, setBoundingBox] = useState<{
|
||||||
|
sw_lat: number;
|
||||||
|
sw_lon: number;
|
||||||
|
ne_lat: number;
|
||||||
|
ne_lon: number;
|
||||||
|
} | undefined>(undefined);
|
||||||
|
|
||||||
|
const [vesselPopup, setVesselPopup] = useState<VesselData | null>(null);
|
||||||
|
const mapRef = useRef<MapRef | null>(null);
|
||||||
|
|
||||||
|
// Use the AIS provider
|
||||||
|
const { vessels, isConnected, error, connectionStatus, updateBoundingBox } = useAISProvider(boundingBox);
|
||||||
|
|
||||||
|
// Update bounding box when map moves
|
||||||
|
const handleMapMove = useCallback(() => {
|
||||||
|
if (mapRef.current) {
|
||||||
|
const map = mapRef.current.getMap();
|
||||||
|
const bounds = map.getBounds();
|
||||||
|
if (bounds) {
|
||||||
|
const sw = bounds.getSouthWest();
|
||||||
|
const ne = bounds.getNorthEast();
|
||||||
|
|
||||||
|
const newBoundingBox = {
|
||||||
|
sw_lat: sw.lat,
|
||||||
|
sw_lon: sw.lng,
|
||||||
|
ne_lat: ne.lat,
|
||||||
|
ne_lon: ne.lng
|
||||||
|
};
|
||||||
|
|
||||||
|
setBoundingBox(newBoundingBox);
|
||||||
|
updateBoundingBox(newBoundingBox);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}, [updateBoundingBox]);
|
||||||
|
|
||||||
|
// Initialize bounding box when map loads
|
||||||
|
const handleMapLoad = useCallback(() => {
|
||||||
|
handleMapMove();
|
||||||
|
}, [handleMapMove]);
|
||||||
|
|
||||||
|
// Create vessel markers
|
||||||
|
const vesselMarkers = useMemo(() =>
|
||||||
|
vessels.map((vessel) => (
|
||||||
|
<Marker
|
||||||
|
key={`vessel-${vessel.id}`}
|
||||||
|
longitude={vessel.longitude}
|
||||||
|
latitude={vessel.latitude}
|
||||||
|
anchor="center"
|
||||||
|
onClick={(e) => {
|
||||||
|
e.originalEvent.stopPropagation();
|
||||||
|
setVesselPopup(vessel);
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<VesselMarker
|
||||||
|
heading={vessel.heading}
|
||||||
|
color={getVesselColor(vessel.type)}
|
||||||
|
size={16}
|
||||||
|
/>
|
||||||
|
</Marker>
|
||||||
|
)),
|
||||||
|
[vessels]
|
||||||
|
);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div style={{ width: '100vw', height: '100vh', position: 'relative' }}>
|
||||||
|
{/* Status Panel */}
|
||||||
|
<div style={{
|
||||||
|
position: 'absolute',
|
||||||
|
top: 10,
|
||||||
|
left: 10,
|
||||||
|
zIndex: 1000,
|
||||||
|
background: 'rgba(255, 255, 255, 0.9)',
|
||||||
|
padding: '10px',
|
||||||
|
borderRadius: '5px',
|
||||||
|
boxShadow: '0 2px 10px rgba(0,0,0,0.1)',
|
||||||
|
minWidth: '200px'
|
||||||
|
}}>
|
||||||
|
<h3 style={{ margin: '0 0 10px 0', fontSize: '16px' }}>AIS Test Map</h3>
|
||||||
|
<div><strong>Status:</strong> {connectionStatus}</div>
|
||||||
|
<div><strong>Vessels:</strong> {vessels.length}</div>
|
||||||
|
{error && <div style={{ color: 'red' }}><strong>Error:</strong> {error}</div>}
|
||||||
|
{isConnected && (
|
||||||
|
<div style={{ color: 'green', fontSize: '12px', marginTop: '5px' }}>
|
||||||
|
✓ Connected to AIS server
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Map */}
|
||||||
|
<Map
|
||||||
|
ref={mapRef}
|
||||||
|
initialViewState={{
|
||||||
|
latitude: 40.7128,
|
||||||
|
longitude: -74.0060,
|
||||||
|
zoom: 10
|
||||||
|
}}
|
||||||
|
style={{ width: '100%', height: '100%' }}
|
||||||
|
mapStyle="mapbox://styles/mapbox/standard"
|
||||||
|
mapboxAccessToken={MAPBOX_TOKEN}
|
||||||
|
onLoad={handleMapLoad}
|
||||||
|
onMoveEnd={handleMapMove}
|
||||||
|
>
|
||||||
|
<NavigationControl position="top-right" />
|
||||||
|
<ScaleControl />
|
||||||
|
|
||||||
|
{vesselMarkers}
|
||||||
|
|
||||||
|
{/* Vessel Popup */}
|
||||||
|
{vesselPopup && (
|
||||||
|
<Popup
|
||||||
|
longitude={vesselPopup.longitude}
|
||||||
|
latitude={vesselPopup.latitude}
|
||||||
|
anchor="bottom"
|
||||||
|
onClose={() => setVesselPopup(null)}
|
||||||
|
closeButton={true}
|
||||||
|
closeOnClick={false}
|
||||||
|
>
|
||||||
|
<div style={{ padding: '10px', minWidth: '200px' }}>
|
||||||
|
<h4 style={{ margin: '0 0 10px 0' }}>{vesselPopup.name}</h4>
|
||||||
|
<div><strong>MMSI:</strong> {vesselPopup.mmsi}</div>
|
||||||
|
<div><strong>Type:</strong> {vesselPopup.type}</div>
|
||||||
|
<div><strong>Speed:</strong> {vesselPopup.speed.toFixed(1)} knots</div>
|
||||||
|
<div><strong>Heading:</strong> {vesselPopup.heading}°</div>
|
||||||
|
<div><strong>Position:</strong> {vesselPopup.latitude.toFixed(4)}, {vesselPopup.longitude.toFixed(4)}</div>
|
||||||
|
<div style={{ fontSize: '12px', color: '#666', marginTop: '5px' }}>
|
||||||
|
Last update: {vesselPopup.lastUpdate.toLocaleTimeString()}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</Popup>
|
||||||
|
)}
|
||||||
|
</Map>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
// Helper function to get vessel color based on type
|
||||||
|
const getVesselColor = (type: string): string => {
|
||||||
|
switch (type.toLowerCase()) {
|
||||||
|
case 'yacht':
|
||||||
|
case 'pleasure craft':
|
||||||
|
return '#00cc66';
|
||||||
|
case 'fishing vessel':
|
||||||
|
case 'fishing':
|
||||||
|
return '#ff6600';
|
||||||
|
case 'cargo':
|
||||||
|
case 'container':
|
||||||
|
return '#cc0066';
|
||||||
|
case 'tanker':
|
||||||
|
return '#ff0000';
|
||||||
|
case 'passenger':
|
||||||
|
return '#6600cc';
|
||||||
|
default:
|
||||||
|
return '#0066cc';
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
export default App;
|
404
ais-test-map/src/ais-provider.tsx
Normal file
404
ais-test-map/src/ais-provider.tsx
Normal file
@@ -0,0 +1,404 @@
|
|||||||
|
import { useState, useEffect, useCallback, useRef } from 'react';
|
||||||
|
|
||||||
|
// Vessel data interface
|
||||||
|
export interface VesselData {
|
||||||
|
id: string;
|
||||||
|
name: string;
|
||||||
|
type: string;
|
||||||
|
latitude: number;
|
||||||
|
longitude: number;
|
||||||
|
heading: number; // degrees 0-359
|
||||||
|
speed: number; // knots
|
||||||
|
length: number; // meters
|
||||||
|
width: number; // meters
|
||||||
|
mmsi: string; // Maritime Mobile Service Identity
|
||||||
|
callSign: string;
|
||||||
|
destination?: string;
|
||||||
|
eta?: string;
|
||||||
|
lastUpdate: Date;
|
||||||
|
}
|
||||||
|
|
||||||
|
// AIS service response structure (matching Rust AisResponse)
|
||||||
|
interface AisResponse {
|
||||||
|
message_type?: string;
|
||||||
|
mmsi?: string;
|
||||||
|
ship_name?: string;
|
||||||
|
latitude?: number;
|
||||||
|
longitude?: number;
|
||||||
|
timestamp?: string;
|
||||||
|
speed_over_ground?: number;
|
||||||
|
course_over_ground?: number;
|
||||||
|
heading?: number;
|
||||||
|
navigation_status?: string;
|
||||||
|
ship_type?: string;
|
||||||
|
raw_message: any;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Bounding box for AIS queries
|
||||||
|
interface BoundingBox {
|
||||||
|
sw_lat: number;
|
||||||
|
sw_lon: number;
|
||||||
|
ne_lat: number;
|
||||||
|
ne_lon: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
// WebSocket message types for communication with the backend
|
||||||
|
interface WebSocketMessage {
|
||||||
|
type: string;
|
||||||
|
bounding_box?: BoundingBox;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Convert AIS service response to VesselData format
|
||||||
|
const convertAisResponseToVesselData = (aisResponse: AisResponse): VesselData | null => {
|
||||||
|
if ((!aisResponse.raw_message?.MetaData?.MMSI) || !aisResponse.latitude || !aisResponse.longitude) {
|
||||||
|
console.log('Skipping vessel with missing data:', {
|
||||||
|
mmsi: aisResponse.mmsi,
|
||||||
|
metadataMSSI: aisResponse.raw_message?.MetaData?.MSSI,
|
||||||
|
latitude: aisResponse.latitude,
|
||||||
|
longitude: aisResponse.longitude,
|
||||||
|
raw: aisResponse.raw_message
|
||||||
|
});
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
id: aisResponse.mmsi ?? !aisResponse.raw_message?.MetaData?.MSSI,
|
||||||
|
name: aisResponse.ship_name || `Vessel ${aisResponse.mmsi}`,
|
||||||
|
type: aisResponse.ship_type || 'Unknown',
|
||||||
|
latitude: aisResponse.latitude,
|
||||||
|
longitude: aisResponse.longitude,
|
||||||
|
heading: aisResponse.heading || 0,
|
||||||
|
speed: aisResponse.speed_over_ground || 0,
|
||||||
|
length: 100, // Default length
|
||||||
|
width: 20, // Default width
|
||||||
|
mmsi: aisResponse.mmsi,
|
||||||
|
callSign: '',
|
||||||
|
destination: '',
|
||||||
|
eta: '',
|
||||||
|
lastUpdate: new Date()
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
// Simplified AIS provider hook for testing
|
||||||
|
export const useAISProvider = (boundingBox?: BoundingBox) => {
|
||||||
|
const [vessels, setVessels] = useState<VesselData[]>([]);
|
||||||
|
const [isConnected, setIsConnected] = useState(false);
|
||||||
|
const [error, setError] = useState<string | null>(null);
|
||||||
|
const [connectionStatus, setConnectionStatus] = useState<string>('Disconnected');
|
||||||
|
|
||||||
|
const wsRef = useRef<WebSocket | null>(null);
|
||||||
|
const vesselMapRef = useRef<Map<string, VesselData>>(new Map());
|
||||||
|
const reconnectTimeoutRef = useRef<NodeJS.Timeout | null>(null);
|
||||||
|
const reconnectAttemptsRef = useRef<number>(0);
|
||||||
|
const connectionTimeoutRef = useRef<NodeJS.Timeout | null>(null);
|
||||||
|
const isConnectingRef = useRef<boolean>(false);
|
||||||
|
const isMountedRef = useRef<boolean>(true);
|
||||||
|
const maxReconnectAttempts = 10;
|
||||||
|
const baseReconnectDelay = 1000; // 1 second
|
||||||
|
|
||||||
|
// Calculate exponential backoff delay
|
||||||
|
const getReconnectDelay = useCallback(() => {
|
||||||
|
const delay = baseReconnectDelay * Math.pow(2, reconnectAttemptsRef.current);
|
||||||
|
return Math.min(delay, 30000); // Cap at 30 seconds
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
// Connect to WebSocket with React StrictMode-safe logic
|
||||||
|
const connectSocket = useCallback(() => {
|
||||||
|
// Prevent multiple simultaneous connection attempts (React StrictMode protection)
|
||||||
|
if (isConnectingRef.current) {
|
||||||
|
console.log('Connection attempt already in progress, skipping...');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check if component is still mounted
|
||||||
|
if (!isMountedRef.current) {
|
||||||
|
console.log('Component unmounted, skipping connection attempt');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Clear any existing reconnection timeout
|
||||||
|
if (reconnectTimeoutRef.current) {
|
||||||
|
clearTimeout(reconnectTimeoutRef.current);
|
||||||
|
reconnectTimeoutRef.current = null;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Clear any existing connection timeout
|
||||||
|
if (connectionTimeoutRef.current) {
|
||||||
|
clearTimeout(connectionTimeoutRef.current);
|
||||||
|
connectionTimeoutRef.current = null;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check if already connected or connecting
|
||||||
|
if (wsRef.current?.readyState === WebSocket.OPEN) {
|
||||||
|
console.log('WebSocket already connected');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (wsRef.current?.readyState === WebSocket.CONNECTING) {
|
||||||
|
console.log('WebSocket already connecting');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check reconnection attempts
|
||||||
|
if (reconnectAttemptsRef.current >= maxReconnectAttempts) {
|
||||||
|
console.error('Max reconnection attempts reached');
|
||||||
|
setError('Failed to connect after multiple attempts');
|
||||||
|
setConnectionStatus('Failed');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Set connecting flag to prevent race conditions
|
||||||
|
isConnectingRef.current = true;
|
||||||
|
|
||||||
|
setConnectionStatus(reconnectAttemptsRef.current > 0 ?
|
||||||
|
`Reconnecting... (${reconnectAttemptsRef.current + 1}/${maxReconnectAttempts})` :
|
||||||
|
'Connecting...');
|
||||||
|
setError(null);
|
||||||
|
|
||||||
|
try {
|
||||||
|
console.log(`[CONNECT] Attempting WebSocket connection (attempt ${reconnectAttemptsRef.current + 1})`);
|
||||||
|
|
||||||
|
// Close any existing connection properly
|
||||||
|
if (wsRef.current) {
|
||||||
|
wsRef.current.onopen = null;
|
||||||
|
wsRef.current.onmessage = null;
|
||||||
|
wsRef.current.onerror = null;
|
||||||
|
wsRef.current.onclose = null;
|
||||||
|
wsRef.current.close();
|
||||||
|
wsRef.current = null;
|
||||||
|
}
|
||||||
|
|
||||||
|
const ws = new WebSocket('ws://localhost:3000/ws');
|
||||||
|
wsRef.current = ws;
|
||||||
|
|
||||||
|
// Set connection timeout with proper cleanup
|
||||||
|
connectionTimeoutRef.current = setTimeout(() => {
|
||||||
|
if (ws.readyState === WebSocket.CONNECTING && isMountedRef.current) {
|
||||||
|
console.log('[TIMEOUT] Connection timeout, closing WebSocket');
|
||||||
|
isConnectingRef.current = false;
|
||||||
|
ws.close();
|
||||||
|
}
|
||||||
|
}, 10000); // 10 second timeout
|
||||||
|
|
||||||
|
ws.onopen = () => {
|
||||||
|
// Clear connection timeout
|
||||||
|
if (connectionTimeoutRef.current) {
|
||||||
|
clearTimeout(connectionTimeoutRef.current);
|
||||||
|
connectionTimeoutRef.current = null;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check if component is still mounted
|
||||||
|
if (!isMountedRef.current) {
|
||||||
|
console.log('[OPEN] Component unmounted, closing connection');
|
||||||
|
ws.close();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log('[OPEN] Connected to AIS WebSocket');
|
||||||
|
isConnectingRef.current = false; // Clear connecting flag
|
||||||
|
setIsConnected(true);
|
||||||
|
setConnectionStatus('Connected');
|
||||||
|
setError(null);
|
||||||
|
reconnectAttemptsRef.current = 0; // Reset reconnection attempts
|
||||||
|
|
||||||
|
// Send bounding box if available
|
||||||
|
if (boundingBox && isMountedRef.current) {
|
||||||
|
const message: WebSocketMessage = {
|
||||||
|
type: 'set_bounding_box',
|
||||||
|
bounding_box: boundingBox
|
||||||
|
};
|
||||||
|
ws.send(JSON.stringify(message));
|
||||||
|
console.log('[OPEN] Sent bounding box:', boundingBox);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Start AIS stream
|
||||||
|
if (isMountedRef.current) {
|
||||||
|
const startMessage: WebSocketMessage = {
|
||||||
|
type: 'start_ais_stream'
|
||||||
|
};
|
||||||
|
ws.send(JSON.stringify(startMessage));
|
||||||
|
console.log('[OPEN] Started AIS stream');
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
ws.onmessage = (event) => {
|
||||||
|
try {
|
||||||
|
const messageData = event.data;
|
||||||
|
|
||||||
|
// Try to parse as JSON, but handle plain text messages gracefully
|
||||||
|
let data;
|
||||||
|
try {
|
||||||
|
data = JSON.parse(messageData);
|
||||||
|
} catch (parseError) {
|
||||||
|
console.log('Received plain text message:', messageData);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Handle JSON status messages
|
||||||
|
if (typeof data === 'string' || data.type) {
|
||||||
|
console.log('Received message:', data);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Process vessel data
|
||||||
|
const vesselData = convertAisResponseToVesselData(data);
|
||||||
|
if (vesselData) {
|
||||||
|
console.log('Received vessel data:', vesselData);
|
||||||
|
vesselMapRef.current.set(vesselData.mmsi, vesselData);
|
||||||
|
setVessels(Array.from(vesselMapRef.current.values()));
|
||||||
|
}
|
||||||
|
} catch (err) {
|
||||||
|
console.error('Error processing WebSocket message:', err);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
ws.onerror = (error) => {
|
||||||
|
// Clear connection timeout
|
||||||
|
if (connectionTimeoutRef.current) {
|
||||||
|
clearTimeout(connectionTimeoutRef.current);
|
||||||
|
connectionTimeoutRef.current = null;
|
||||||
|
}
|
||||||
|
|
||||||
|
console.error('[ERROR] WebSocket error:', error);
|
||||||
|
isConnectingRef.current = false; // Clear connecting flag
|
||||||
|
|
||||||
|
// Only update state if component is still mounted
|
||||||
|
if (isMountedRef.current) {
|
||||||
|
setError('WebSocket connection error');
|
||||||
|
setIsConnected(false);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
ws.onclose = (event) => {
|
||||||
|
// Clear connection timeout
|
||||||
|
if (connectionTimeoutRef.current) {
|
||||||
|
clearTimeout(connectionTimeoutRef.current);
|
||||||
|
connectionTimeoutRef.current = null;
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log(`[CLOSE] WebSocket connection closed: ${event.code} - ${event.reason}`);
|
||||||
|
isConnectingRef.current = false; // Clear connecting flag
|
||||||
|
|
||||||
|
// Only update state if component is still mounted
|
||||||
|
if (isMountedRef.current) {
|
||||||
|
setIsConnected(false);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Only attempt reconnection if component is mounted, wasn't a clean close, and we haven't exceeded max attempts
|
||||||
|
if (isMountedRef.current && !event.wasClean && reconnectAttemptsRef.current < maxReconnectAttempts) {
|
||||||
|
reconnectAttemptsRef.current++;
|
||||||
|
const delay = getReconnectDelay();
|
||||||
|
|
||||||
|
console.log(`[CLOSE] Scheduling reconnection in ${delay}ms (attempt ${reconnectAttemptsRef.current}/${maxReconnectAttempts})`);
|
||||||
|
setError(`Connection lost, reconnecting in ${Math.round(delay/1000)}s...`);
|
||||||
|
setConnectionStatus('Reconnecting...');
|
||||||
|
|
||||||
|
reconnectTimeoutRef.current = setTimeout(() => {
|
||||||
|
if (isMountedRef.current) {
|
||||||
|
connectSocket();
|
||||||
|
}
|
||||||
|
}, delay);
|
||||||
|
} else {
|
||||||
|
if (isMountedRef.current) {
|
||||||
|
if (event.wasClean) {
|
||||||
|
setConnectionStatus('Disconnected');
|
||||||
|
setError(null);
|
||||||
|
} else {
|
||||||
|
setConnectionStatus('Failed');
|
||||||
|
setError('Connection failed after multiple attempts');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
} catch (err) {
|
||||||
|
console.error('Error creating WebSocket connection:', err);
|
||||||
|
setError(err instanceof Error ? err.message : 'Unknown WebSocket error');
|
||||||
|
setConnectionStatus('Error');
|
||||||
|
|
||||||
|
// Schedule reconnection attempt
|
||||||
|
if (reconnectAttemptsRef.current < maxReconnectAttempts) {
|
||||||
|
reconnectAttemptsRef.current++;
|
||||||
|
const delay = getReconnectDelay();
|
||||||
|
reconnectTimeoutRef.current = setTimeout(() => {
|
||||||
|
connectSocket();
|
||||||
|
}, delay);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}, [boundingBox, getReconnectDelay]);
|
||||||
|
|
||||||
|
// Update bounding box
|
||||||
|
const updateBoundingBox = useCallback((bbox: BoundingBox) => {
|
||||||
|
if (wsRef.current?.readyState === WebSocket.OPEN) {
|
||||||
|
const message: WebSocketMessage = {
|
||||||
|
type: 'set_bounding_box',
|
||||||
|
bounding_box: bbox
|
||||||
|
};
|
||||||
|
wsRef.current.send(JSON.stringify(message));
|
||||||
|
console.log('Updated bounding box:', bbox);
|
||||||
|
|
||||||
|
// Clear existing vessels when bounding box changes
|
||||||
|
vesselMapRef.current.clear();
|
||||||
|
setVessels([]);
|
||||||
|
}
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
// Connect on mount with React StrictMode protection
|
||||||
|
useEffect(() => {
|
||||||
|
// Set mounted flag
|
||||||
|
isMountedRef.current = true;
|
||||||
|
|
||||||
|
// Small delay to prevent immediate double connection in StrictMode
|
||||||
|
const connectTimeout = setTimeout(() => {
|
||||||
|
if (isMountedRef.current) {
|
||||||
|
connectSocket();
|
||||||
|
}
|
||||||
|
}, 100);
|
||||||
|
|
||||||
|
return () => {
|
||||||
|
// Mark component as unmounted
|
||||||
|
isMountedRef.current = false;
|
||||||
|
|
||||||
|
// Clear connect timeout
|
||||||
|
clearTimeout(connectTimeout);
|
||||||
|
|
||||||
|
// Clear reconnection timeout
|
||||||
|
if (reconnectTimeoutRef.current) {
|
||||||
|
clearTimeout(reconnectTimeoutRef.current);
|
||||||
|
reconnectTimeoutRef.current = null;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Clear connection timeout
|
||||||
|
if (connectionTimeoutRef.current) {
|
||||||
|
clearTimeout(connectionTimeoutRef.current);
|
||||||
|
connectionTimeoutRef.current = null;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Reset connection flags
|
||||||
|
isConnectingRef.current = false;
|
||||||
|
|
||||||
|
// Close WebSocket connection properly
|
||||||
|
if (wsRef.current) {
|
||||||
|
console.log('[CLEANUP] Closing WebSocket connection');
|
||||||
|
wsRef.current.onopen = null;
|
||||||
|
wsRef.current.onmessage = null;
|
||||||
|
wsRef.current.onerror = null;
|
||||||
|
wsRef.current.onclose = null;
|
||||||
|
wsRef.current.close();
|
||||||
|
wsRef.current = null;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Reset reconnection attempts
|
||||||
|
reconnectAttemptsRef.current = 0;
|
||||||
|
};
|
||||||
|
}, [connectSocket]);
|
||||||
|
|
||||||
|
return {
|
||||||
|
vessels,
|
||||||
|
isConnected,
|
||||||
|
error,
|
||||||
|
connectionStatus,
|
||||||
|
connectSocket,
|
||||||
|
updateBoundingBox
|
||||||
|
};
|
||||||
|
};
|
9
ais-test-map/src/main.tsx
Normal file
9
ais-test-map/src/main.tsx
Normal file
@@ -0,0 +1,9 @@
|
|||||||
|
import { StrictMode } from 'react'
|
||||||
|
import { createRoot } from 'react-dom/client'
|
||||||
|
import App from './App.tsx'
|
||||||
|
|
||||||
|
createRoot(document.getElementById('root')!).render(
|
||||||
|
<StrictMode>
|
||||||
|
<App />
|
||||||
|
</StrictMode>,
|
||||||
|
)
|
42
ais-test-map/src/vessel-marker.tsx
Normal file
42
ais-test-map/src/vessel-marker.tsx
Normal file
@@ -0,0 +1,42 @@
|
|||||||
|
import React from 'react';
|
||||||
|
|
||||||
|
interface VesselMarkerProps {
|
||||||
|
heading: number;
|
||||||
|
color?: string;
|
||||||
|
size?: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
const VesselMarker: React.FC<VesselMarkerProps> = ({
|
||||||
|
heading,
|
||||||
|
color = '#0066cc',
|
||||||
|
size = 16
|
||||||
|
}) => {
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
style={{
|
||||||
|
width: size,
|
||||||
|
height: size,
|
||||||
|
transform: `rotate(${heading}deg)`,
|
||||||
|
cursor: 'pointer',
|
||||||
|
display: 'flex',
|
||||||
|
alignItems: 'center',
|
||||||
|
justifyContent: 'center'
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<svg
|
||||||
|
width={size}
|
||||||
|
height={size}
|
||||||
|
viewBox="0 0 24 24"
|
||||||
|
fill={color}
|
||||||
|
style={{
|
||||||
|
filter: 'drop-shadow(0 2px 4px rgba(0,0,0,0.3))'
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{/* Simple vessel shape - triangle pointing up (north) */}
|
||||||
|
<path d="M12 2 L20 20 L12 16 L4 20 Z" />
|
||||||
|
</svg>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default VesselMarker;
|
31
ais-test-map/tsconfig.json
Normal file
31
ais-test-map/tsconfig.json
Normal file
@@ -0,0 +1,31 @@
|
|||||||
|
{
|
||||||
|
"compilerOptions": {
|
||||||
|
"target": "ES2022",
|
||||||
|
"useDefineForClassFields": true,
|
||||||
|
"lib": ["ES2022", "DOM", "DOM.Iterable"],
|
||||||
|
"module": "ESNext",
|
||||||
|
"skipLibCheck": true,
|
||||||
|
|
||||||
|
/* Bundler mode */
|
||||||
|
"moduleResolution": "bundler",
|
||||||
|
"allowImportingTsExtensions": true,
|
||||||
|
"verbatimModuleSyntax": true,
|
||||||
|
"moduleDetection": "force",
|
||||||
|
"noEmit": true,
|
||||||
|
"jsx": "react-jsx",
|
||||||
|
"paths": {
|
||||||
|
"@/*": ["./src/*"]
|
||||||
|
},
|
||||||
|
/* Linting */
|
||||||
|
"strict": true,
|
||||||
|
"noUnusedLocals": true,
|
||||||
|
"noUnusedParameters": true,
|
||||||
|
"erasableSyntaxOnly": true,
|
||||||
|
"noFallthroughCasesInSwitch": true,
|
||||||
|
"noUncheckedSideEffectImports": true
|
||||||
|
},
|
||||||
|
"include": [
|
||||||
|
"src"
|
||||||
|
],
|
||||||
|
"exclude": ["**/*.test.ts"]
|
||||||
|
}
|
8
ais-test-map/vite.config.ts
Normal file
8
ais-test-map/vite.config.ts
Normal file
@@ -0,0 +1,8 @@
|
|||||||
|
import { defineConfig } from 'vite'
|
||||||
|
import react from '@vitejs/plugin-react'
|
||||||
|
import tsconfigPaths from "vite-tsconfig-paths"
|
||||||
|
|
||||||
|
// https://vite.dev/config/
|
||||||
|
export default defineConfig({
|
||||||
|
plugins: [react(), tsconfigPaths()],
|
||||||
|
})
|
@@ -350,8 +350,22 @@ async fn handle_websocket(mut socket: WebSocket, state: AppState) {
|
|||||||
Ok(data) => {
|
Ok(data) => {
|
||||||
// Apply bounding box filtering if configured
|
// Apply bounding box filtering if configured
|
||||||
let should_send = match &bounding_box {
|
let should_send = match &bounding_box {
|
||||||
Some(bbox) => is_within_bounding_box(&data, bbox),
|
Some(bbox) => {
|
||||||
None => true, // Send all data if no bounding box is set
|
let within_bounds = is_within_bounding_box(&data, bbox);
|
||||||
|
if !within_bounds {
|
||||||
|
println!("Vessel filtered out - MMSI: {:?}, Lat: {:?}, Lon: {:?}, Bbox: sw_lat={}, sw_lon={}, ne_lat={}, ne_lon={}",
|
||||||
|
data.mmsi, data.latitude, data.longitude, bbox.sw_lat, bbox.sw_lon, bbox.ne_lat, bbox.ne_lon);
|
||||||
|
} else {
|
||||||
|
println!("Vessel within bounds - MMSI: {:?}, Lat: {:?}, Lon: {:?}",
|
||||||
|
data.mmsi, data.latitude, data.longitude);
|
||||||
|
}
|
||||||
|
within_bounds
|
||||||
|
},
|
||||||
|
None => {
|
||||||
|
println!("No bounding box set - sending vessel MMSI: {:?}, Lat: {:?}, Lon: {:?}",
|
||||||
|
data.mmsi, data.latitude, data.longitude);
|
||||||
|
true // Send all data if no bounding box is set
|
||||||
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
if should_send {
|
if should_send {
|
||||||
@@ -606,7 +620,7 @@ async fn connect_to_ais_stream_with_broadcast(state: AppState) -> Result<(), Box
|
|||||||
let (mut sender, mut receiver) = ws_stream.split();
|
let (mut sender, mut receiver) = ws_stream.split();
|
||||||
|
|
||||||
let key = "MDc4YzY5NTdkMGUwM2UzMzQ1Zjc5NDFmOTA1ODg4ZTMyOGQ0MjM0MA==";
|
let key = "MDc4YzY5NTdkMGUwM2UzMzQ1Zjc5NDFmOTA1ODg4ZTMyOGQ0MjM0MA==";
|
||||||
// Create subscription message with default bounding box (Port of Los Angeles area)
|
// Create subscription message with default bounding box (New York Harbor area)
|
||||||
// In a full implementation, this could be made dynamic based on active HTTP requests
|
// In a full implementation, this could be made dynamic based on active HTTP requests
|
||||||
let subscription_message = SubscriptionMessage {
|
let subscription_message = SubscriptionMessage {
|
||||||
apikey: STANDARD.decode(key)
|
apikey: STANDARD.decode(key)
|
||||||
@@ -614,8 +628,8 @@ async fn connect_to_ais_stream_with_broadcast(state: AppState) -> Result<(), Box
|
|||||||
.and_then(|bytes| String::from_utf8(bytes).ok())
|
.and_then(|bytes| String::from_utf8(bytes).ok())
|
||||||
.unwrap_or_default(),
|
.unwrap_or_default(),
|
||||||
bounding_boxes: vec![vec![
|
bounding_boxes: vec![vec![
|
||||||
[33.6, -118.5], // Southwest corner (lat, lon)
|
[40.4, -74.8], // Southwest corner (lat, lon) - broader area around NYC
|
||||||
[33.9, -118.0] // Northeast corner (lat, lon)
|
[41.0, -73.2] // Northeast corner (lat, lon) - covers NYC harbor and approaches
|
||||||
]],
|
]],
|
||||||
filters_ship_mmsi: vec![], // Remove specific MMSI filters to get all ships in the area
|
filters_ship_mmsi: vec![], // Remove specific MMSI filters to get all ships in the area
|
||||||
};
|
};
|
||||||
|
Reference in New Issue
Block a user