From 81ca7ab06cab6c90fa64b928422d10825172d664 Mon Sep 17 00:00:00 2001 From: geoffsee <> Date: Mon, 21 Jul 2025 18:14:26 -0400 Subject: [PATCH] Remove AIS provider integration and related vessel markers --- crates/base-map/map/src/MapNext.tsx | 134 +------- crates/base-map/map/src/real-ais-provider.tsx | 291 ------------------ crates/base-map/map/src/vessel-marker.tsx | 42 --- .../map/test/real-ais-provider.test.tsx | 246 --------------- 4 files changed, 1 insertion(+), 712 deletions(-) delete mode 100644 crates/base-map/map/src/real-ais-provider.tsx delete mode 100644 crates/base-map/map/src/vessel-marker.tsx delete mode 100644 crates/base-map/map/test/real-ais-provider.test.tsx diff --git a/crates/base-map/map/src/MapNext.tsx b/crates/base-map/map/src/MapNext.tsx index cfc3446..8716f1a 100644 --- a/crates/base-map/map/src/MapNext.tsx +++ b/crates/base-map/map/src/MapNext.tsx @@ -1,4 +1,4 @@ -import {useState, useMemo, useEffect, useCallback, useRef} from 'react'; +import {useState, useMemo, useCallback, useRef} from 'react'; import Map, { Marker, Popup, @@ -11,14 +11,9 @@ import Map, { import ControlPanel from './control-panel.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 {Box} from "@chakra-ui/react"; -import { useColorMode } from './components/ui/color-mode'; -import { getNeumorphicStyle, getNeumorphicColors } from './theme/neumorphic-theme'; 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) { - const { colorMode } = useColorMode(); const [popupInfo, setPopupInfo] = useState(null); - const [vesselPopupInfo, setVesselPopupInfo] = useState(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(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 const handleGeolocate = useCallback((position: GeolocationPosition) => { console.log('User location loaded:', position); - setUserLocationLoaded(true); - setMapFocused(true); // When geolocate succeeds, the map focuses on user location }, []); const handleTrackUserLocationStart = useCallback(() => { console.log('Started tracking user location'); - // User location tracking started, but not necessarily loaded yet }, []); const handleTrackUserLocationEnd = useCallback(() => { console.log('Stopped tracking user location'); - setMapFocused(false); }, []); const pins = useMemo( @@ -121,35 +70,6 @@ export default function MapNext(props: any = {mapboxPublicKey: "", geolocation: [] ); - const vesselMarkers = useMemo( - () => - vessels.map((vessel) => ( - { - e.originalEvent.stopPropagation(); - setVesselPopupInfo(vessel); - }} - > - - - )), - [vessels] - ); - - - useEffect(() => { - console.log("props.vesselPosition", props?.vesselPosition); - // setLocationLock(props.vesselPosition) - }, [props.vesselPosition]); - return ( {pins} - {vesselMarkers} {popupInfo && ( )} - {vesselPopupInfo && ( - setVesselPopupInfo(null)} - > - - - {vesselPopupInfo.name} - - - Type: {vesselPopupInfo.type} - MMSI: {vesselPopupInfo.mmsi} - Call Sign: {vesselPopupInfo.callSign} - Speed: {vesselPopupInfo.speed.toFixed(1)} knots - Heading: {vesselPopupInfo.heading.toFixed(0)}° - Length: {vesselPopupInfo.length.toFixed(0)}m - {vesselPopupInfo.destination && ( - Destination: {vesselPopupInfo.destination} - )} - {vesselPopupInfo.eta && ( - ETA: {vesselPopupInfo.eta} - )} - - Last Update: {vesselPopupInfo.lastUpdate.toLocaleTimeString()} - - - - - )} - diff --git a/crates/base-map/map/src/real-ais-provider.tsx b/crates/base-map/map/src/real-ais-provider.tsx deleted file mode 100644 index 3369a67..0000000 --- a/crates/base-map/map/src/real-ais-provider.tsx +++ /dev/null @@ -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([]); - const [isActive, setIsActive] = useState(true); - const [isLoading, setIsLoading] = useState(false); - const [error, setError] = useState(null); - const [isConnected, setIsConnected] = useState(false); - const [aisStreamStarted, setAisStreamStarted] = useState(false); - - const wsRef = useRef(null); - const lastBoundingBoxRef = useRef(undefined); - const reconnectTimeoutRef = useRef(null); - const vesselMapRef = useRef>(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(); - } - }; -}; \ No newline at end of file diff --git a/crates/base-map/map/src/vessel-marker.tsx b/crates/base-map/map/src/vessel-marker.tsx deleted file mode 100644 index 80aa3a9..0000000 --- a/crates/base-map/map/src/vessel-marker.tsx +++ /dev/null @@ -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 ( - - - {/* Small arrow to indicate heading */} - - - ); -} - -export default React.memo(VesselMarker); \ No newline at end of file diff --git a/crates/base-map/map/test/real-ais-provider.test.tsx b/crates/base-map/map/test/real-ais-provider.test.tsx deleted file mode 100644 index 25af71d..0000000 --- a/crates/base-map/map/test/real-ais-provider.test.tsx +++ /dev/null @@ -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 - }) - ); - }); -}); \ No newline at end of file