mirror of
https://github.com/seemueller-io/yachtpit.git
synced 2025-09-08 22:46:45 +00:00
Remove AIS provider integration and related vessel markers
This commit is contained in:
@@ -1,4 +1,4 @@
|
|||||||
import {useState, useMemo, useEffect, useCallback, useRef} from 'react';
|
import {useState, useMemo, useCallback, useRef} from 'react';
|
||||||
import Map, {
|
import Map, {
|
||||||
Marker,
|
Marker,
|
||||||
Popup,
|
Popup,
|
||||||
@@ -11,14 +11,9 @@ import Map, {
|
|||||||
|
|
||||||
import ControlPanel from './control-panel.tsx';
|
import ControlPanel from './control-panel.tsx';
|
||||||
import Pin from './pin.tsx';
|
import Pin from './pin.tsx';
|
||||||
import VesselMarker from './vessel-marker.tsx';
|
|
||||||
import { type VesselData } from './real-ais-provider.tsx';
|
|
||||||
import { useRealAISProvider } from './real-ais-provider.tsx';
|
|
||||||
|
|
||||||
import PORTS from './test_data/nautical-base-data.json';
|
import PORTS from './test_data/nautical-base-data.json';
|
||||||
import {Box} from "@chakra-ui/react";
|
import {Box} from "@chakra-ui/react";
|
||||||
import { useColorMode } from './components/ui/color-mode';
|
|
||||||
import { getNeumorphicStyle, getNeumorphicColors } from './theme/neumorphic-theme';
|
|
||||||
|
|
||||||
|
|
||||||
export interface Geolocation {
|
export interface Geolocation {
|
||||||
@@ -33,66 +28,20 @@ export interface Geolocation {
|
|||||||
|
|
||||||
|
|
||||||
export default function MapNext(props: any = {mapboxPublicKey: "", geolocation: Geolocation, vesselPosition: undefined, layer: undefined, mapView: undefined} as any) {
|
export default function MapNext(props: any = {mapboxPublicKey: "", geolocation: Geolocation, vesselPosition: undefined, layer: undefined, mapView: undefined} as any) {
|
||||||
const { colorMode } = useColorMode();
|
|
||||||
const [popupInfo, setPopupInfo] = useState(null);
|
const [popupInfo, setPopupInfo] = useState(null);
|
||||||
const [vesselPopupInfo, setVesselPopupInfo] = useState<VesselData | null>(null);
|
|
||||||
const [boundingBox, setBoundingBox] = useState<{sw_lat: number, sw_lon: number, ne_lat: number, ne_lon: number} | undefined>(undefined);
|
|
||||||
const [userLocationLoaded, setUserLocationLoaded] = useState(false);
|
|
||||||
const [mapFocused, setMapFocused] = useState(false);
|
|
||||||
const mapRef = useRef<MapRef | null>(null);
|
const mapRef = useRef<MapRef | null>(null);
|
||||||
|
|
||||||
// Use the real AIS provider with bounding box, user location, and map focus status
|
|
||||||
const { vessels } = useRealAISProvider(boundingBox, userLocationLoaded, mapFocused);
|
|
||||||
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
console.log("vessles", vessels);
|
|
||||||
}, [vessels]);
|
|
||||||
|
|
||||||
// Function to update bounding box from map bounds
|
|
||||||
const updateBoundingBox = useCallback(() => {
|
|
||||||
if (mapRef.current) {
|
|
||||||
const map = mapRef.current.getMap();
|
|
||||||
const bounds = map.getBounds();
|
|
||||||
if (bounds) {
|
|
||||||
const sw = bounds.getSouthWest();
|
|
||||||
const ne = bounds.getNorthEast();
|
|
||||||
|
|
||||||
setBoundingBox({
|
|
||||||
sw_lat: sw.lat,
|
|
||||||
sw_lon: sw.lng,
|
|
||||||
ne_lat: ne.lat,
|
|
||||||
ne_lon: ne.lng
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}, []);
|
|
||||||
|
|
||||||
// Handle map move events
|
|
||||||
const handleMapMove = useCallback(() => {
|
|
||||||
updateBoundingBox();
|
|
||||||
}, [updateBoundingBox]);
|
|
||||||
|
|
||||||
// Initialize bounding box when map loads
|
|
||||||
const handleMapLoad = useCallback(() => {
|
|
||||||
updateBoundingBox();
|
|
||||||
}, [updateBoundingBox]);
|
|
||||||
|
|
||||||
// Handle user location events
|
// Handle user location events
|
||||||
const handleGeolocate = useCallback((position: GeolocationPosition) => {
|
const handleGeolocate = useCallback((position: GeolocationPosition) => {
|
||||||
console.log('User location loaded:', position);
|
console.log('User location loaded:', position);
|
||||||
setUserLocationLoaded(true);
|
|
||||||
setMapFocused(true); // When geolocate succeeds, the map focuses on user location
|
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
const handleTrackUserLocationStart = useCallback(() => {
|
const handleTrackUserLocationStart = useCallback(() => {
|
||||||
console.log('Started tracking user location');
|
console.log('Started tracking user location');
|
||||||
// User location tracking started, but not necessarily loaded yet
|
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
const handleTrackUserLocationEnd = useCallback(() => {
|
const handleTrackUserLocationEnd = useCallback(() => {
|
||||||
console.log('Stopped tracking user location');
|
console.log('Stopped tracking user location');
|
||||||
setMapFocused(false);
|
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
const pins = useMemo(
|
const pins = useMemo(
|
||||||
@@ -121,35 +70,6 @@ export default function MapNext(props: any = {mapboxPublicKey: "", geolocation:
|
|||||||
[]
|
[]
|
||||||
);
|
);
|
||||||
|
|
||||||
const vesselMarkers = useMemo(
|
|
||||||
() =>
|
|
||||||
vessels.map((vessel) => (
|
|
||||||
<Marker
|
|
||||||
key={`vessel-${vessel.id}`}
|
|
||||||
longitude={vessel.longitude}
|
|
||||||
latitude={vessel.latitude}
|
|
||||||
anchor="center"
|
|
||||||
onClick={e => {
|
|
||||||
e.originalEvent.stopPropagation();
|
|
||||||
setVesselPopupInfo(vessel);
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<VesselMarker
|
|
||||||
heading={vessel.heading}
|
|
||||||
color={vessel.type === 'Yacht' ? '#00cc66' : vessel.type === 'Fishing Vessel' ? '#ff6600' : '#0066cc'}
|
|
||||||
size={14}
|
|
||||||
/>
|
|
||||||
</Marker>
|
|
||||||
)),
|
|
||||||
[vessels]
|
|
||||||
);
|
|
||||||
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
console.log("props.vesselPosition", props?.vesselPosition);
|
|
||||||
// setLocationLock(props.vesselPosition)
|
|
||||||
}, [props.vesselPosition]);
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Box>
|
<Box>
|
||||||
<Map
|
<Map
|
||||||
@@ -165,8 +85,6 @@ export default function MapNext(props: any = {mapboxPublicKey: "", geolocation:
|
|||||||
mapStyle={props.layer?.value || "mapbox://styles/mapbox/standard"}
|
mapStyle={props.layer?.value || "mapbox://styles/mapbox/standard"}
|
||||||
mapboxAccessToken={props.mapboxPublicKey}
|
mapboxAccessToken={props.mapboxPublicKey}
|
||||||
style={{position: "fixed", width: '100%', height: '100%', bottom: 0, top: 0, left: 0, right: 0}}
|
style={{position: "fixed", width: '100%', height: '100%', bottom: 0, top: 0, left: 0, right: 0}}
|
||||||
onLoad={handleMapLoad}
|
|
||||||
onMoveEnd={handleMapMove}
|
|
||||||
>
|
>
|
||||||
<GeolocateControl
|
<GeolocateControl
|
||||||
showUserHeading={true}
|
showUserHeading={true}
|
||||||
@@ -182,7 +100,6 @@ export default function MapNext(props: any = {mapboxPublicKey: "", geolocation:
|
|||||||
<ScaleControl />
|
<ScaleControl />
|
||||||
|
|
||||||
{pins}
|
{pins}
|
||||||
{vesselMarkers}
|
|
||||||
|
|
||||||
{popupInfo && (
|
{popupInfo && (
|
||||||
<Popup
|
<Popup
|
||||||
@@ -226,55 +143,6 @@ export default function MapNext(props: any = {mapboxPublicKey: "", geolocation:
|
|||||||
</Popup>
|
</Popup>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{vesselPopupInfo && (
|
|
||||||
<Popup
|
|
||||||
anchor="top"
|
|
||||||
longitude={vesselPopupInfo.longitude}
|
|
||||||
latitude={vesselPopupInfo.latitude}
|
|
||||||
onClose={() => setVesselPopupInfo(null)}
|
|
||||||
>
|
|
||||||
<Box
|
|
||||||
minW="240px"
|
|
||||||
p={4}
|
|
||||||
backdropFilter="blur(10px)"
|
|
||||||
{...getNeumorphicStyle(colorMode as 'light' | 'dark')}
|
|
||||||
>
|
|
||||||
<Box
|
|
||||||
mb={3}
|
|
||||||
fontSize="lg"
|
|
||||||
fontWeight="bold"
|
|
||||||
color={getNeumorphicColors(colorMode as 'light' | 'dark').text}
|
|
||||||
>
|
|
||||||
{vesselPopupInfo.name}
|
|
||||||
</Box>
|
|
||||||
<Box fontSize="sm" lineHeight="1.6">
|
|
||||||
<Box mb={1}><strong>Type:</strong> {vesselPopupInfo.type}</Box>
|
|
||||||
<Box mb={1}><strong>MMSI:</strong> {vesselPopupInfo.mmsi}</Box>
|
|
||||||
<Box mb={1}><strong>Call Sign:</strong> {vesselPopupInfo.callSign}</Box>
|
|
||||||
<Box mb={1}><strong>Speed:</strong> {vesselPopupInfo.speed.toFixed(1)} knots</Box>
|
|
||||||
<Box mb={1}><strong>Heading:</strong> {vesselPopupInfo.heading.toFixed(0)}°</Box>
|
|
||||||
<Box mb={1}><strong>Length:</strong> {vesselPopupInfo.length.toFixed(0)}m</Box>
|
|
||||||
{vesselPopupInfo.destination && (
|
|
||||||
<Box mb={1}><strong>Destination:</strong> {vesselPopupInfo.destination}</Box>
|
|
||||||
)}
|
|
||||||
{vesselPopupInfo.eta && (
|
|
||||||
<Box mb={1}><strong>ETA:</strong> {vesselPopupInfo.eta}</Box>
|
|
||||||
)}
|
|
||||||
<Box
|
|
||||||
fontSize="xs"
|
|
||||||
color={getNeumorphicColors(colorMode as 'light' | 'dark').textSecondary}
|
|
||||||
mt={3}
|
|
||||||
pt={2}
|
|
||||||
borderTop="1px solid"
|
|
||||||
borderColor={getNeumorphicColors(colorMode as 'light' | 'dark').textSecondary + '30'}
|
|
||||||
>
|
|
||||||
Last Update: {vesselPopupInfo.lastUpdate.toLocaleTimeString()}
|
|
||||||
</Box>
|
|
||||||
</Box>
|
|
||||||
</Box>
|
|
||||||
</Popup>
|
|
||||||
)}
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
</Map>
|
</Map>
|
||||||
|
@@ -1,291 +0,0 @@
|
|||||||
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;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Convert AIS service response to VesselData format
|
|
||||||
const convertAisResponseToVesselData = (aisResponse: AisResponse): any | null => {
|
|
||||||
// Skip responses that don't have essential vessel data
|
|
||||||
|
|
||||||
console.log({aisResponse})
|
|
||||||
// return aisResponse.raw_message;
|
|
||||||
return {
|
|
||||||
id: aisResponse.mmsi,
|
|
||||||
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, could be extracted from raw_message if available
|
|
||||||
width: 20, // Default width
|
|
||||||
mmsi: aisResponse.mmsi,
|
|
||||||
callSign: '', // Could be extracted from raw_message if available
|
|
||||||
destination: '', // Could be extracted from raw_message if available
|
|
||||||
eta: '', // Could be extracted from raw_message if available
|
|
||||||
lastUpdate: new Date()
|
|
||||||
};
|
|
||||||
};
|
|
||||||
|
|
||||||
// WebSocket message types for communication with the backend
|
|
||||||
interface WebSocketMessage {
|
|
||||||
type: string;
|
|
||||||
bounding_box?: BoundingBox;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Hook to provide real AIS data from the service via WebSocket
|
|
||||||
export const useRealAISProvider = (boundingBox?: BoundingBox, userLocationLoaded?: boolean, mapFocused?: boolean) => {
|
|
||||||
const [vessels, setVessels] = useState<VesselData[]>([]);
|
|
||||||
const [isActive, setIsActive] = useState(true);
|
|
||||||
const [isLoading, setIsLoading] = useState(false);
|
|
||||||
const [error, setError] = useState<string | null>(null);
|
|
||||||
const [isConnected, setIsConnected] = useState(false);
|
|
||||||
const [aisStreamStarted, setAisStreamStarted] = useState(false);
|
|
||||||
|
|
||||||
const wsRef = useRef<WebSocket | null>(null);
|
|
||||||
const lastBoundingBoxRef = useRef<BoundingBox | undefined>(undefined);
|
|
||||||
const reconnectTimeoutRef = useRef<any | null>(null);
|
|
||||||
const vesselMapRef = useRef<Map<string, VesselData>>(new Map());
|
|
||||||
|
|
||||||
// Connect to WebSocket
|
|
||||||
const connectWebSocket = useCallback(() => {
|
|
||||||
// Prevent multiple connections
|
|
||||||
if (!isActive) return;
|
|
||||||
|
|
||||||
// Check if we already have an active or connecting WebSocket
|
|
||||||
if (wsRef.current &&
|
|
||||||
(wsRef.current.readyState === WebSocket.OPEN ||
|
|
||||||
wsRef.current.readyState === WebSocket.CONNECTING)) {
|
|
||||||
console.log('WebSocket already connected or connecting, skipping...');
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Close any existing connection before creating a new one
|
|
||||||
if (wsRef.current) {
|
|
||||||
wsRef.current.close();
|
|
||||||
wsRef.current = null;
|
|
||||||
}
|
|
||||||
|
|
||||||
setIsLoading(true);
|
|
||||||
setError(null);
|
|
||||||
|
|
||||||
try {
|
|
||||||
console.log('Creating new WebSocket connection...');
|
|
||||||
const ws = new WebSocket('ws://localhost:3000/ws');
|
|
||||||
wsRef.current = ws;
|
|
||||||
|
|
||||||
ws.onopen = () => {
|
|
||||||
console.log('Connected to AIS WebSocket');
|
|
||||||
setIsConnected(true);
|
|
||||||
setIsLoading(false);
|
|
||||||
setError(null);
|
|
||||||
|
|
||||||
// Send bounding box configuration if available
|
|
||||||
// Note: We'll send the bounding box separately to avoid connection recreation
|
|
||||||
const currentBoundingBox = lastBoundingBoxRef.current;
|
|
||||||
if (currentBoundingBox) {
|
|
||||||
const message: WebSocketMessage = {
|
|
||||||
type: 'set_bounding_box',
|
|
||||||
bounding_box: currentBoundingBox
|
|
||||||
};
|
|
||||||
ws.send(JSON.stringify(message));
|
|
||||||
console.log('Sent initial bounding box configuration:', currentBoundingBox);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
ws.onmessage = (event) => {
|
|
||||||
try {
|
|
||||||
const data = JSON.parse(event.data);
|
|
||||||
|
|
||||||
// Handle connection confirmation and bounding box confirmations
|
|
||||||
if (typeof data === 'string' || data.type) {
|
|
||||||
console.log('Received WebSocket message:', data);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
const vesselData = convertAisResponseToVesselData(data);
|
|
||||||
if (vesselData) {
|
|
||||||
// Update vessel map for efficient updates
|
|
||||||
vesselMapRef.current.set(vesselData.mmsi, vesselData);
|
|
||||||
|
|
||||||
// Update vessels state with current map values
|
|
||||||
setVessels(Array.from(vesselMapRef.current.values()));
|
|
||||||
}
|
|
||||||
} catch (err) {
|
|
||||||
console.error('Error parsing WebSocket message:', err);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
ws.onerror = (error) => {
|
|
||||||
console.error('WebSocket error:', error);
|
|
||||||
setError('WebSocket connection error');
|
|
||||||
setIsConnected(false);
|
|
||||||
};
|
|
||||||
|
|
||||||
ws.onclose = (event) => {
|
|
||||||
console.log('WebSocket connection closed:', event.code, event.reason);
|
|
||||||
setIsConnected(false);
|
|
||||||
setIsLoading(false);
|
|
||||||
|
|
||||||
// Attempt to reconnect if the connection was active
|
|
||||||
if (isActive && !event.wasClean) {
|
|
||||||
setError('Connection lost, attempting to reconnect...');
|
|
||||||
reconnectTimeoutRef.current = setTimeout(() => {
|
|
||||||
connectWebSocket();
|
|
||||||
}, 3000); // Reconnect after 3 seconds
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
} catch (err) {
|
|
||||||
console.error('Error creating WebSocket connection:', err);
|
|
||||||
setError(err instanceof Error ? err.message : 'Unknown WebSocket error');
|
|
||||||
setIsLoading(false);
|
|
||||||
}
|
|
||||||
}, [isActive]); // Removed boundingBox dependency to prevent reconnections
|
|
||||||
|
|
||||||
// Send bounding box update to WebSocket
|
|
||||||
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);
|
|
||||||
}
|
|
||||||
}, []);
|
|
||||||
|
|
||||||
// Send start AIS stream message to WebSocket
|
|
||||||
const startAisStream = useCallback(() => {
|
|
||||||
if (wsRef.current?.readyState === WebSocket.OPEN && !aisStreamStarted) {
|
|
||||||
const message: WebSocketMessage = {
|
|
||||||
type: 'start_ais_stream'
|
|
||||||
};
|
|
||||||
wsRef.current.send(JSON.stringify(message));
|
|
||||||
console.log('Sent start AIS stream request');
|
|
||||||
setAisStreamStarted(true);
|
|
||||||
}
|
|
||||||
}, [aisStreamStarted]);
|
|
||||||
|
|
||||||
// Connect to WebSocket when component mounts or becomes active
|
|
||||||
useEffect(() => {
|
|
||||||
if (isActive) {
|
|
||||||
connectWebSocket();
|
|
||||||
}
|
|
||||||
|
|
||||||
return () => {
|
|
||||||
if (reconnectTimeoutRef.current) {
|
|
||||||
clearTimeout(reconnectTimeoutRef.current);
|
|
||||||
}
|
|
||||||
if (wsRef.current) {
|
|
||||||
wsRef.current.close();
|
|
||||||
wsRef.current = null;
|
|
||||||
}
|
|
||||||
};
|
|
||||||
}, [isActive, connectWebSocket]);
|
|
||||||
|
|
||||||
// Handle bounding box changes
|
|
||||||
useEffect(() => {
|
|
||||||
if (!boundingBox || !isActive) return;
|
|
||||||
|
|
||||||
// Check if bounding box actually changed to avoid unnecessary updates
|
|
||||||
const lastBbox = lastBoundingBoxRef.current;
|
|
||||||
if (lastBbox &&
|
|
||||||
lastBbox.sw_lat === boundingBox.sw_lat &&
|
|
||||||
lastBbox.sw_lon === boundingBox.sw_lon &&
|
|
||||||
lastBbox.ne_lat === boundingBox.ne_lat &&
|
|
||||||
lastBbox.ne_lon === boundingBox.ne_lon) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
lastBoundingBoxRef.current = boundingBox;
|
|
||||||
|
|
||||||
// Clear existing vessels when bounding box changes
|
|
||||||
vesselMapRef.current.clear();
|
|
||||||
setVessels([]);
|
|
||||||
|
|
||||||
// Send new bounding box to WebSocket
|
|
||||||
updateBoundingBox(boundingBox);
|
|
||||||
}, [boundingBox, updateBoundingBox, isActive]);
|
|
||||||
|
|
||||||
// Handle active state changes
|
|
||||||
useEffect(() => {
|
|
||||||
if (!isActive) {
|
|
||||||
// Close WebSocket connection when inactive
|
|
||||||
if (wsRef.current) {
|
|
||||||
wsRef.current.close();
|
|
||||||
wsRef.current = null;
|
|
||||||
}
|
|
||||||
setIsConnected(false);
|
|
||||||
setError(null);
|
|
||||||
|
|
||||||
// Clear reconnection timeout
|
|
||||||
if (reconnectTimeoutRef.current) {
|
|
||||||
clearTimeout(reconnectTimeoutRef.current);
|
|
||||||
reconnectTimeoutRef.current = null;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}, [isActive]);
|
|
||||||
|
|
||||||
// Start AIS stream when user location is loaded and map is focused
|
|
||||||
useEffect(() => {
|
|
||||||
if (userLocationLoaded && mapFocused && isConnected && !aisStreamStarted) {
|
|
||||||
console.log('User location loaded and map focused, starting AIS stream...');
|
|
||||||
startAisStream();
|
|
||||||
}
|
|
||||||
}, [userLocationLoaded, mapFocused, isConnected, aisStreamStarted, startAisStream]);
|
|
||||||
|
|
||||||
return {
|
|
||||||
vessels,
|
|
||||||
isActive,
|
|
||||||
setIsActive,
|
|
||||||
isLoading,
|
|
||||||
error,
|
|
||||||
isConnected,
|
|
||||||
refreshVessels: () => {
|
|
||||||
// For WebSocket, we can trigger a reconnection to refresh data
|
|
||||||
if (wsRef.current) {
|
|
||||||
wsRef.current.close();
|
|
||||||
}
|
|
||||||
connectWebSocket();
|
|
||||||
}
|
|
||||||
};
|
|
||||||
};
|
|
@@ -1,42 +0,0 @@
|
|||||||
import * as React from 'react';
|
|
||||||
|
|
||||||
interface VesselMarkerProps {
|
|
||||||
size?: number;
|
|
||||||
color?: string;
|
|
||||||
heading?: number;
|
|
||||||
}
|
|
||||||
|
|
||||||
const vesselStyle = {
|
|
||||||
cursor: 'pointer',
|
|
||||||
stroke: '#fff',
|
|
||||||
strokeWidth: 2
|
|
||||||
};
|
|
||||||
|
|
||||||
function VesselMarker({ size = 12, color = '#0066cc', heading = 0 }: VesselMarkerProps) {
|
|
||||||
return (
|
|
||||||
<svg
|
|
||||||
height={size}
|
|
||||||
width={size}
|
|
||||||
viewBox="0 0 24 24"
|
|
||||||
style={{
|
|
||||||
...vesselStyle,
|
|
||||||
transform: `rotate(${heading}deg)`,
|
|
||||||
transformOrigin: 'center'
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<circle
|
|
||||||
cx="12"
|
|
||||||
cy="12"
|
|
||||||
r="10"
|
|
||||||
fill={color}
|
|
||||||
/>
|
|
||||||
{/* Small arrow to indicate heading */}
|
|
||||||
<path
|
|
||||||
d="M12 4 L16 12 L12 10 L8 12 Z"
|
|
||||||
fill="#fff"
|
|
||||||
/>
|
|
||||||
</svg>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
export default React.memo(VesselMarker);
|
|
@@ -1,246 +0,0 @@
|
|||||||
import { renderHook, act } from '@testing-library/react';
|
|
||||||
import { vi } from 'vitest';
|
|
||||||
import { useRealAISProvider } from '../src/real-ais-provider.tsx';
|
|
||||||
|
|
||||||
// Mock WebSocket
|
|
||||||
class MockWebSocket {
|
|
||||||
static instances: MockWebSocket[] = [];
|
|
||||||
static CONNECTING = 0;
|
|
||||||
static OPEN = 1;
|
|
||||||
static CLOSING = 2;
|
|
||||||
static CLOSED = 3;
|
|
||||||
|
|
||||||
readyState: number = MockWebSocket.CONNECTING;
|
|
||||||
onopen: ((event: Event) => void) | null = null;
|
|
||||||
onmessage: ((event: MessageEvent) => void) | null = null;
|
|
||||||
onerror: ((event: Event) => void) | null = null;
|
|
||||||
onclose: ((event: CloseEvent) => void) | null = null;
|
|
||||||
|
|
||||||
constructor(public url: string) {
|
|
||||||
MockWebSocket.instances.push(this);
|
|
||||||
|
|
||||||
// Simulate connection opening after a short delay
|
|
||||||
setTimeout(() => {
|
|
||||||
this.readyState = MockWebSocket.OPEN;
|
|
||||||
if (this.onopen) {
|
|
||||||
this.onopen(new Event('open'));
|
|
||||||
}
|
|
||||||
}, 10);
|
|
||||||
}
|
|
||||||
|
|
||||||
send(data: string) {
|
|
||||||
console.log('MockWebSocket send:', data);
|
|
||||||
}
|
|
||||||
|
|
||||||
close() {
|
|
||||||
this.readyState = MockWebSocket.CLOSED;
|
|
||||||
if (this.onclose) {
|
|
||||||
this.onclose(new CloseEvent('close', { wasClean: true }));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
static reset() {
|
|
||||||
MockWebSocket.instances = [];
|
|
||||||
}
|
|
||||||
|
|
||||||
static getConnectionCount() {
|
|
||||||
return MockWebSocket.instances.filter(ws =>
|
|
||||||
ws.readyState === MockWebSocket.OPEN ||
|
|
||||||
ws.readyState === MockWebSocket.CONNECTING
|
|
||||||
).length;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Replace global WebSocket with mock
|
|
||||||
(global as any).WebSocket = MockWebSocket;
|
|
||||||
|
|
||||||
describe('useRealAISProvider WebSocket Connection Management', () => {
|
|
||||||
beforeEach(() => {
|
|
||||||
MockWebSocket.reset();
|
|
||||||
vi.clearAllMocks();
|
|
||||||
});
|
|
||||||
|
|
||||||
afterEach(() => {
|
|
||||||
MockWebSocket.reset();
|
|
||||||
});
|
|
||||||
|
|
||||||
test('should create only one WebSocket connection on initial render', async () => {
|
|
||||||
const boundingBox = {
|
|
||||||
sw_lat: 33.0,
|
|
||||||
sw_lon: -119.0,
|
|
||||||
ne_lat: 34.0,
|
|
||||||
ne_lon: -118.0
|
|
||||||
};
|
|
||||||
|
|
||||||
const { result } = renderHook(() => useRealAISProvider(boundingBox));
|
|
||||||
|
|
||||||
// Wait for connection to be established
|
|
||||||
await act(async () => {
|
|
||||||
await new Promise(resolve => setTimeout(resolve, 50));
|
|
||||||
});
|
|
||||||
|
|
||||||
expect(MockWebSocket.instances).toHaveLength(1);
|
|
||||||
expect(MockWebSocket.getConnectionCount()).toBe(1);
|
|
||||||
expect(result.current.isConnected).toBe(true);
|
|
||||||
});
|
|
||||||
|
|
||||||
test('should not create multiple connections when bounding box changes', async () => {
|
|
||||||
const initialBoundingBox = {
|
|
||||||
sw_lat: 33.0,
|
|
||||||
sw_lon: -119.0,
|
|
||||||
ne_lat: 34.0,
|
|
||||||
ne_lon: -118.0
|
|
||||||
};
|
|
||||||
|
|
||||||
const { result, rerender } = renderHook(
|
|
||||||
({ boundingBox }) => useRealAISProvider(boundingBox),
|
|
||||||
{ initialProps: { boundingBox: initialBoundingBox } }
|
|
||||||
);
|
|
||||||
|
|
||||||
// Wait for initial connection
|
|
||||||
await act(async () => {
|
|
||||||
await new Promise(resolve => setTimeout(resolve, 50));
|
|
||||||
});
|
|
||||||
|
|
||||||
expect(MockWebSocket.instances).toHaveLength(1);
|
|
||||||
expect(MockWebSocket.getConnectionCount()).toBe(1);
|
|
||||||
|
|
||||||
// Change bounding box multiple times
|
|
||||||
const newBoundingBox1 = {
|
|
||||||
sw_lat: 34.0,
|
|
||||||
sw_lon: -120.0,
|
|
||||||
ne_lat: 35.0,
|
|
||||||
ne_lon: -119.0
|
|
||||||
};
|
|
||||||
|
|
||||||
const newBoundingBox2 = {
|
|
||||||
sw_lat: 35.0,
|
|
||||||
sw_lon: -121.0,
|
|
||||||
ne_lat: 36.0,
|
|
||||||
ne_lon: -120.0
|
|
||||||
};
|
|
||||||
|
|
||||||
await act(async () => {
|
|
||||||
rerender({ boundingBox: newBoundingBox1 });
|
|
||||||
await new Promise(resolve => setTimeout(resolve, 20));
|
|
||||||
});
|
|
||||||
|
|
||||||
await act(async () => {
|
|
||||||
rerender({ boundingBox: newBoundingBox2 });
|
|
||||||
await new Promise(resolve => setTimeout(resolve, 20));
|
|
||||||
});
|
|
||||||
|
|
||||||
// Should still have only one connection
|
|
||||||
expect(MockWebSocket.getConnectionCount()).toBe(1);
|
|
||||||
expect(result.current.isConnected).toBe(true);
|
|
||||||
});
|
|
||||||
|
|
||||||
test('should properly cleanup connection when component unmounts', async () => {
|
|
||||||
const boundingBox = {
|
|
||||||
sw_lat: 33.0,
|
|
||||||
sw_lon: -119.0,
|
|
||||||
ne_lat: 34.0,
|
|
||||||
ne_lon: -118.0
|
|
||||||
};
|
|
||||||
|
|
||||||
const { result, unmount } = renderHook(() => useRealAISProvider(boundingBox));
|
|
||||||
|
|
||||||
// Wait for connection
|
|
||||||
await act(async () => {
|
|
||||||
await new Promise(resolve => setTimeout(resolve, 50));
|
|
||||||
});
|
|
||||||
|
|
||||||
expect(MockWebSocket.getConnectionCount()).toBe(1);
|
|
||||||
expect(result.current.isConnected).toBe(true);
|
|
||||||
|
|
||||||
// Unmount component
|
|
||||||
unmount();
|
|
||||||
|
|
||||||
// Connection should be closed
|
|
||||||
expect(MockWebSocket.instances[0].readyState).toBe(MockWebSocket.CLOSED);
|
|
||||||
});
|
|
||||||
|
|
||||||
test('should not create connection when isActive is false', async () => {
|
|
||||||
const boundingBox = {
|
|
||||||
sw_lat: 33.0,
|
|
||||||
sw_lon: -119.0,
|
|
||||||
ne_lat: 34.0,
|
|
||||||
ne_lon: -118.0
|
|
||||||
};
|
|
||||||
|
|
||||||
// Create a custom hook that starts with isActive = false
|
|
||||||
const { result } = renderHook(() => {
|
|
||||||
const provider = useRealAISProvider(boundingBox);
|
|
||||||
// Set inactive immediately on first render
|
|
||||||
if (provider.isActive) {
|
|
||||||
provider.setIsActive(false);
|
|
||||||
}
|
|
||||||
return provider;
|
|
||||||
});
|
|
||||||
|
|
||||||
// Wait a bit to ensure no connection is created
|
|
||||||
await act(async () => {
|
|
||||||
await new Promise(resolve => setTimeout(resolve, 100));
|
|
||||||
});
|
|
||||||
|
|
||||||
expect(MockWebSocket.instances).toHaveLength(0);
|
|
||||||
expect(result.current.isConnected).toBe(false);
|
|
||||||
expect(result.current.isActive).toBe(false);
|
|
||||||
});
|
|
||||||
|
|
||||||
test('should handle reconnection properly without creating multiple connections', async () => {
|
|
||||||
const boundingBox = {
|
|
||||||
sw_lat: 33.0,
|
|
||||||
sw_lon: -119.0,
|
|
||||||
ne_lat: 34.0,
|
|
||||||
ne_lon: -118.0
|
|
||||||
};
|
|
||||||
|
|
||||||
const { result } = renderHook(() => useRealAISProvider(boundingBox));
|
|
||||||
|
|
||||||
// Wait for initial connection
|
|
||||||
await act(async () => {
|
|
||||||
await new Promise(resolve => setTimeout(resolve, 50));
|
|
||||||
});
|
|
||||||
|
|
||||||
expect(MockWebSocket.getConnectionCount()).toBe(1);
|
|
||||||
|
|
||||||
// Simulate connection loss
|
|
||||||
await act(async () => {
|
|
||||||
const ws = MockWebSocket.instances[0];
|
|
||||||
ws.readyState = MockWebSocket.CLOSED;
|
|
||||||
if (ws.onclose) {
|
|
||||||
ws.onclose(new CloseEvent('close', { wasClean: false }));
|
|
||||||
}
|
|
||||||
await new Promise(resolve => setTimeout(resolve, 3100)); // Wait for reconnection timeout
|
|
||||||
});
|
|
||||||
|
|
||||||
// Should have attempted reconnection but still only one active connection
|
|
||||||
expect(MockWebSocket.getConnectionCount()).toBe(1);
|
|
||||||
});
|
|
||||||
|
|
||||||
test('should send bounding box configuration on connection', async () => {
|
|
||||||
const boundingBox = {
|
|
||||||
sw_lat: 33.0,
|
|
||||||
sw_lon: -119.0,
|
|
||||||
ne_lat: 34.0,
|
|
||||||
ne_lon: -118.0
|
|
||||||
};
|
|
||||||
|
|
||||||
const sendSpy = vi.spyOn(MockWebSocket.prototype, 'send');
|
|
||||||
|
|
||||||
const { result } = renderHook(() => useRealAISProvider(boundingBox));
|
|
||||||
|
|
||||||
// Wait for connection and bounding box message
|
|
||||||
await act(async () => {
|
|
||||||
await new Promise(resolve => setTimeout(resolve, 50));
|
|
||||||
});
|
|
||||||
|
|
||||||
expect(sendSpy).toHaveBeenCalledWith(
|
|
||||||
JSON.stringify({
|
|
||||||
type: 'set_bounding_box',
|
|
||||||
bounding_box: boundingBox
|
|
||||||
})
|
|
||||||
);
|
|
||||||
});
|
|
||||||
});
|
|
Reference in New Issue
Block a user