mirror of
https://github.com/seemueller-io/yachtpit.git
synced 2025-09-08 22:46:45 +00:00
Remove ais-test-map
application, including dependencies, configuration, and source files.
This commit is contained in:
2
Cargo.lock
generated
2
Cargo.lock
generated
@@ -9426,6 +9426,8 @@ checksum = "6fd8403733700263c6eb89f192880191f1b83e332f7a20371ddcf421c4a337c7"
|
|||||||
name = "yachtpit"
|
name = "yachtpit"
|
||||||
version = "0.1.0"
|
version = "0.1.0"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
|
"anyhow",
|
||||||
|
"base-map",
|
||||||
"bevy",
|
"bevy",
|
||||||
"bevy_asset_loader",
|
"bevy_asset_loader",
|
||||||
"bevy_flurx",
|
"bevy_flurx",
|
||||||
|
@@ -1,228 +0,0 @@
|
|||||||
# 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
|
|
@@ -1,13 +0,0 @@
|
|||||||
<!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
6965
ais-test-map/package-lock.json
generated
File diff suppressed because it is too large
Load Diff
@@ -1,43 +0,0 @@
|
|||||||
{
|
|
||||||
"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"
|
|
||||||
}
|
|
||||||
}
|
|
@@ -1,168 +0,0 @@
|
|||||||
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;
|
|
@@ -1,9 +0,0 @@
|
|||||||
import { StrictMode } from 'react'
|
|
||||||
import { createRoot } from 'react-dom/client'
|
|
||||||
import App from './App.tsx'
|
|
||||||
|
|
||||||
createRoot(document.getElementById('root')!).render(
|
|
||||||
<StrictMode>
|
|
||||||
<App />
|
|
||||||
</StrictMode>,
|
|
||||||
)
|
|
@@ -1,31 +0,0 @@
|
|||||||
{
|
|
||||||
"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"]
|
|
||||||
}
|
|
@@ -1,8 +0,0 @@
|
|||||||
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()],
|
|
||||||
})
|
|
142
crates/base-map/map/package-lock.json
generated
142
crates/base-map/map/package-lock.json
generated
@@ -11,7 +11,9 @@
|
|||||||
"next-themes": "^0.4.6",
|
"next-themes": "^0.4.6",
|
||||||
"react": "^19.1.0",
|
"react": "^19.1.0",
|
||||||
"react-dom": "^19.1.0",
|
"react-dom": "^19.1.0",
|
||||||
"react-icons": "^5.5.0"
|
"react-icons": "^5.5.0",
|
||||||
|
"socket.io-client": "^4.8.1",
|
||||||
|
"ws": "^8.18.3"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@chakra-ui/react": "^3.21.1",
|
"@chakra-ui/react": "^3.21.1",
|
||||||
@@ -1712,6 +1714,12 @@
|
|||||||
"win32"
|
"win32"
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
|
"node_modules/@socket.io/component-emitter": {
|
||||||
|
"version": "3.1.2",
|
||||||
|
"resolved": "https://registry.npmjs.org/@socket.io/component-emitter/-/component-emitter-3.1.2.tgz",
|
||||||
|
"integrity": "sha512-9BCxFwvbGg/RsZK9tjXd8s4UcwR0MWeFQ1XEKIQVVvAGJyINdrqKMcTRyLoK8Rse1GjzLV9cwjWV1olXRWEXVA==",
|
||||||
|
"license": "MIT"
|
||||||
|
},
|
||||||
"node_modules/@swc/core": {
|
"node_modules/@swc/core": {
|
||||||
"version": "1.12.14",
|
"version": "1.12.14",
|
||||||
"resolved": "https://registry.npmjs.org/@swc/core/-/core-1.12.14.tgz",
|
"resolved": "https://registry.npmjs.org/@swc/core/-/core-1.12.14.tgz",
|
||||||
@@ -3983,6 +3991,66 @@
|
|||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "ISC"
|
"license": "ISC"
|
||||||
},
|
},
|
||||||
|
"node_modules/engine.io-client": {
|
||||||
|
"version": "6.6.3",
|
||||||
|
"resolved": "https://registry.npmjs.org/engine.io-client/-/engine.io-client-6.6.3.tgz",
|
||||||
|
"integrity": "sha512-T0iLjnyNWahNyv/lcjS2y4oE358tVS/SYQNxYXGAJ9/GLgH4VCvOQ/mhTjqU88mLZCQgiG8RIegFHYCdVC+j5w==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"@socket.io/component-emitter": "~3.1.0",
|
||||||
|
"debug": "~4.3.1",
|
||||||
|
"engine.io-parser": "~5.2.1",
|
||||||
|
"ws": "~8.17.1",
|
||||||
|
"xmlhttprequest-ssl": "~2.1.1"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/engine.io-client/node_modules/debug": {
|
||||||
|
"version": "4.3.7",
|
||||||
|
"resolved": "https://registry.npmjs.org/debug/-/debug-4.3.7.tgz",
|
||||||
|
"integrity": "sha512-Er2nc/H7RrMXZBFCEim6TCmMk02Z8vLC2Rbi1KEBggpo0fS6l0S1nnapwmIi3yW/+GOJap1Krg4w0Hg80oCqgQ==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"ms": "^2.1.3"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">=6.0"
|
||||||
|
},
|
||||||
|
"peerDependenciesMeta": {
|
||||||
|
"supports-color": {
|
||||||
|
"optional": true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/engine.io-client/node_modules/ws": {
|
||||||
|
"version": "8.17.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/ws/-/ws-8.17.1.tgz",
|
||||||
|
"integrity": "sha512-6XQFvXTkbfUOZOKKILFG1PDK2NDQs4azKQl26T0YS5CxqWLgXajbPZ+h4gZekJyRqFU8pvnbAbbs/3TgRPy+GQ==",
|
||||||
|
"license": "MIT",
|
||||||
|
"engines": {
|
||||||
|
"node": ">=10.0.0"
|
||||||
|
},
|
||||||
|
"peerDependencies": {
|
||||||
|
"bufferutil": "^4.0.1",
|
||||||
|
"utf-8-validate": ">=5.0.2"
|
||||||
|
},
|
||||||
|
"peerDependenciesMeta": {
|
||||||
|
"bufferutil": {
|
||||||
|
"optional": true
|
||||||
|
},
|
||||||
|
"utf-8-validate": {
|
||||||
|
"optional": true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/engine.io-parser": {
|
||||||
|
"version": "5.2.3",
|
||||||
|
"resolved": "https://registry.npmjs.org/engine.io-parser/-/engine.io-parser-5.2.3.tgz",
|
||||||
|
"integrity": "sha512-HqD3yTBfnBxIrbnM1DoD6Pcq8NECnh8d4As1Qgh0z5Gg3jRRIqijury0CL3ghu/edArpUYiYqQiDUQBIs4np3Q==",
|
||||||
|
"license": "MIT",
|
||||||
|
"engines": {
|
||||||
|
"node": ">=10.0.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/entities": {
|
"node_modules/entities": {
|
||||||
"version": "6.0.1",
|
"version": "6.0.1",
|
||||||
"resolved": "https://registry.npmjs.org/entities/-/entities-6.0.1.tgz",
|
"resolved": "https://registry.npmjs.org/entities/-/entities-6.0.1.tgz",
|
||||||
@@ -5145,7 +5213,6 @@
|
|||||||
"version": "2.1.3",
|
"version": "2.1.3",
|
||||||
"resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz",
|
"resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz",
|
||||||
"integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==",
|
"integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==",
|
||||||
"dev": true,
|
|
||||||
"license": "MIT"
|
"license": "MIT"
|
||||||
},
|
},
|
||||||
"node_modules/murmurhash-js": {
|
"node_modules/murmurhash-js": {
|
||||||
@@ -5869,6 +5936,68 @@
|
|||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "ISC"
|
"license": "ISC"
|
||||||
},
|
},
|
||||||
|
"node_modules/socket.io-client": {
|
||||||
|
"version": "4.8.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/socket.io-client/-/socket.io-client-4.8.1.tgz",
|
||||||
|
"integrity": "sha512-hJVXfu3E28NmzGk8o1sHhN3om52tRvwYeidbj7xKy2eIIse5IoKX3USlS6Tqt3BHAtflLIkCQBkzVrEEfWUyYQ==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"@socket.io/component-emitter": "~3.1.0",
|
||||||
|
"debug": "~4.3.2",
|
||||||
|
"engine.io-client": "~6.6.1",
|
||||||
|
"socket.io-parser": "~4.2.4"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">=10.0.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/socket.io-client/node_modules/debug": {
|
||||||
|
"version": "4.3.7",
|
||||||
|
"resolved": "https://registry.npmjs.org/debug/-/debug-4.3.7.tgz",
|
||||||
|
"integrity": "sha512-Er2nc/H7RrMXZBFCEim6TCmMk02Z8vLC2Rbi1KEBggpo0fS6l0S1nnapwmIi3yW/+GOJap1Krg4w0Hg80oCqgQ==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"ms": "^2.1.3"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">=6.0"
|
||||||
|
},
|
||||||
|
"peerDependenciesMeta": {
|
||||||
|
"supports-color": {
|
||||||
|
"optional": true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/socket.io-parser": {
|
||||||
|
"version": "4.2.4",
|
||||||
|
"resolved": "https://registry.npmjs.org/socket.io-parser/-/socket.io-parser-4.2.4.tgz",
|
||||||
|
"integrity": "sha512-/GbIKmo8ioc+NIWIhwdecY0ge+qVBSMdgxGygevmdHj24bsfgtCmcUUcQ5ZzcylGFHsN3k4HB4Cgkl96KVnuew==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"@socket.io/component-emitter": "~3.1.0",
|
||||||
|
"debug": "~4.3.1"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">=10.0.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/socket.io-parser/node_modules/debug": {
|
||||||
|
"version": "4.3.7",
|
||||||
|
"resolved": "https://registry.npmjs.org/debug/-/debug-4.3.7.tgz",
|
||||||
|
"integrity": "sha512-Er2nc/H7RrMXZBFCEim6TCmMk02Z8vLC2Rbi1KEBggpo0fS6l0S1nnapwmIi3yW/+GOJap1Krg4w0Hg80oCqgQ==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"ms": "^2.1.3"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">=6.0"
|
||||||
|
},
|
||||||
|
"peerDependenciesMeta": {
|
||||||
|
"supports-color": {
|
||||||
|
"optional": true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/sort-asc": {
|
"node_modules/sort-asc": {
|
||||||
"version": "0.2.0",
|
"version": "0.2.0",
|
||||||
"resolved": "https://registry.npmjs.org/sort-asc/-/sort-asc-0.2.0.tgz",
|
"resolved": "https://registry.npmjs.org/sort-asc/-/sort-asc-0.2.0.tgz",
|
||||||
@@ -6732,7 +6861,6 @@
|
|||||||
"version": "8.18.3",
|
"version": "8.18.3",
|
||||||
"resolved": "https://registry.npmjs.org/ws/-/ws-8.18.3.tgz",
|
"resolved": "https://registry.npmjs.org/ws/-/ws-8.18.3.tgz",
|
||||||
"integrity": "sha512-PEIGCY5tSlUt50cqyMXfCzX+oOPqN0vuGqWzbcJ2xvnkzkq46oOpz7dQaTDBdfICb4N14+GARUDw2XV2N4tvzg==",
|
"integrity": "sha512-PEIGCY5tSlUt50cqyMXfCzX+oOPqN0vuGqWzbcJ2xvnkzkq46oOpz7dQaTDBdfICb4N14+GARUDw2XV2N4tvzg==",
|
||||||
"dev": true,
|
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": ">=10.0.0"
|
"node": ">=10.0.0"
|
||||||
@@ -6767,6 +6895,14 @@
|
|||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT"
|
"license": "MIT"
|
||||||
},
|
},
|
||||||
|
"node_modules/xmlhttprequest-ssl": {
|
||||||
|
"version": "2.1.2",
|
||||||
|
"resolved": "https://registry.npmjs.org/xmlhttprequest-ssl/-/xmlhttprequest-ssl-2.1.2.tgz",
|
||||||
|
"integrity": "sha512-TEU+nJVUUnA4CYJFLvK5X9AOeH4KvDvhIfm0vV1GaQRtchnG0hgK5p8hw/xjv8cunWYCsiPCSDzObPyhEwq3KQ==",
|
||||||
|
"engines": {
|
||||||
|
"node": ">=0.4.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/yaml": {
|
"node_modules/yaml": {
|
||||||
"version": "2.8.0",
|
"version": "2.8.0",
|
||||||
"resolved": "https://registry.npmjs.org/yaml/-/yaml-2.8.0.tgz",
|
"resolved": "https://registry.npmjs.org/yaml/-/yaml-2.8.0.tgz",
|
||||||
|
@@ -16,7 +16,9 @@
|
|||||||
"next-themes": "^0.4.6",
|
"next-themes": "^0.4.6",
|
||||||
"react": "^19.1.0",
|
"react": "^19.1.0",
|
||||||
"react-dom": "^19.1.0",
|
"react-dom": "^19.1.0",
|
||||||
"react-icons": "^5.5.0"
|
"react-icons": "^5.5.0",
|
||||||
|
"socket.io-client": "^4.8.1",
|
||||||
|
"ws": "^8.18.3"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@chakra-ui/react": "^3.21.1",
|
"@chakra-ui/react": "^3.21.1",
|
||||||
|
@@ -1,67 +1,35 @@
|
|||||||
import 'mapbox-gl/dist/mapbox-gl.css';
|
import 'mapbox-gl/dist/mapbox-gl.css';
|
||||||
import {Box, Button, HStack, Input, Text} from '@chakra-ui/react';
|
import {Box, Button, HStack, Text} from '@chakra-ui/react';
|
||||||
import {useColorMode} from './components/ui/color-mode';
|
import {useColorMode} from './components/ui/color-mode';
|
||||||
import {useCallback, useEffect, useState} from "react";
|
import {useCallback, useEffect, useState} from "react";
|
||||||
import MapNext, {type Geolocation} from "@/MapNext.tsx";
|
import MapNext from "@/MapNext.tsx";
|
||||||
import { getNeumorphicStyle, getNeumorphicColors } from './theme/neumorphic-theme';
|
import {getNeumorphicColors, getNeumorphicStyle} from './theme/neumorphic-theme';
|
||||||
import {layers, LayerSelector} from "@/LayerSelector.tsx";
|
import {layers, LayerSelector} from "@/LayerSelector.tsx";
|
||||||
|
import {useAISProvider, type VesselData} from './ais-provider';
|
||||||
|
import type {GpsPosition, VesselStatus} from './types';
|
||||||
|
import {GpsFeed} from "@/components/map/GpsFeedInfo.tsx";
|
||||||
|
import {AisFeed} from './components/map/AisFeedInfo';
|
||||||
|
import {Search} from "@/components/map/Search.tsx";
|
||||||
|
import {SearchResult} from "@/components/map/SearchResult.tsx";
|
||||||
|
import {NativeGeolocation} from "@/CustomGeolocate.ts";
|
||||||
|
|
||||||
// public key
|
// public key
|
||||||
const key =
|
const key =
|
||||||
'cGsuZXlKMUlqb2laMlZ2Wm1aelpXVWlMQ0poSWpvaVkycDFOalo0YkdWNk1EUTRjRE41YjJnNFp6VjNNelp6YXlKOS56LUtzS1l0X3VGUGdCSDYwQUFBNFNn';
|
'cGsuZXlKMUlqb2laMlZ2Wm1aelpXVWlMQ0poSWpvaVkycDFOalo0YkdWNk1EUTRjRE41YjJnNFp6VjNNelp6YXlKOS56LUtzS1l0X3VGUGdCSDYwQUFBNFNn';
|
||||||
|
|
||||||
|
function App() {
|
||||||
|
const {colorMode} = useColorMode();
|
||||||
|
const [isSearchOpen, setIsSearchOpen] = useState(false);
|
||||||
|
const [selectedLayer, setSelectedLayer] = useState(layers[0]);
|
||||||
|
const [searchInput, setSearchInput] = useState('');
|
||||||
|
const [searchResults, setSearchResults] = useState<any[]>([]);
|
||||||
|
const [mapView, setMapView] = useState({
|
||||||
|
longitude: -122.4,
|
||||||
|
latitude: 37.8,
|
||||||
|
zoom: 14
|
||||||
|
});
|
||||||
|
|
||||||
// const vesselLayerStyle: CircleLayerSpecification = {
|
const custom_geolocation = new NativeGeolocation({
|
||||||
// id: 'vessel',
|
|
||||||
// type: 'circle',
|
|
||||||
// paint: {
|
|
||||||
// 'circle-radius': 8,
|
|
||||||
// 'circle-color': '#ff4444',
|
|
||||||
// 'circle-stroke-width': 2,
|
|
||||||
// 'circle-stroke-color': '#ffffff'
|
|
||||||
// },
|
|
||||||
// source: ''
|
|
||||||
// };
|
|
||||||
|
|
||||||
// Types for bevy_flurx_ipc communication
|
|
||||||
interface GpsPosition {
|
|
||||||
latitude: number;
|
|
||||||
longitude: number;
|
|
||||||
zoom: number;
|
|
||||||
}
|
|
||||||
|
|
||||||
interface VesselStatus {
|
|
||||||
latitude: number;
|
|
||||||
longitude: number;
|
|
||||||
heading: number;
|
|
||||||
speed: number;
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
class MyGeolocation implements Geolocation {
|
|
||||||
constructor({clearWatch, getCurrentPosition, watchPosition}: {
|
|
||||||
clearWatch: (watchId: number) => void;
|
|
||||||
getCurrentPosition: (successCallback: PositionCallback, errorCallback?: PositionErrorCallback | null, options?: PositionOptions) => void;
|
|
||||||
watchPosition: (successCallback: PositionCallback, errorCallback?: PositionErrorCallback | null, options?: PositionOptions) => number;
|
|
||||||
}) {
|
|
||||||
this.clearWatch = clearWatch;
|
|
||||||
this.watchPosition = watchPosition;
|
|
||||||
this.getCurrentPosition = getCurrentPosition;
|
|
||||||
}
|
|
||||||
clearWatch(_watchId: number): void {
|
|
||||||
throw new Error('Method not implemented.');
|
|
||||||
}
|
|
||||||
getCurrentPosition(_successCallback: PositionCallback, _errorCallback?: PositionErrorCallback | null, _options?: PositionOptions): void {
|
|
||||||
throw new Error('Method not implemented.');
|
|
||||||
}
|
|
||||||
watchPosition(_successCallback: PositionCallback, _errorCallback?: PositionErrorCallback | null, _options?: PositionOptions): number {
|
|
||||||
throw new Error('Method not implemented.');
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
const custom_geolocation = new MyGeolocation({
|
|
||||||
clearWatch: (watchId: number) => {
|
clearWatch: (watchId: number) => {
|
||||||
if (typeof window !== 'undefined' && (window as any).geolocationWatches) {
|
if (typeof window !== 'undefined' && (window as any).geolocationWatches) {
|
||||||
const interval = (window as any).geolocationWatches.get(watchId);
|
const interval = (window as any).geolocationWatches.get(watchId);
|
||||||
@@ -209,72 +177,26 @@ const custom_geolocation = new MyGeolocation({
|
|||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
// interface MapViewParams {
|
|
||||||
// latitude: number;
|
|
||||||
// longitude: number;
|
|
||||||
// zoom: number;
|
|
||||||
// }
|
|
||||||
|
|
||||||
// interface AuthParams {
|
|
||||||
// authenticated: boolean;
|
|
||||||
// token: string | null;
|
|
||||||
// }
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
function App() {
|
|
||||||
const { colorMode } = useColorMode();
|
|
||||||
const [isSearchOpen, setIsSearchOpen] = useState(false);
|
|
||||||
const [selectedLayer, setSelectedLayer] = useState(layers[0]);
|
|
||||||
const [searchInput, setSearchInput] = useState('');
|
|
||||||
const [searchResults, setSearchResults] = useState<any[]>([]);
|
|
||||||
const [mapView, setMapView] = useState({
|
|
||||||
longitude: -122.4,
|
|
||||||
latitude: 37.8,
|
|
||||||
zoom: 14
|
|
||||||
});
|
|
||||||
|
|
||||||
// Map state that can be updated from Rust
|
|
||||||
// const [mapView, setMapView] = useState({
|
|
||||||
// longitude: -122.4,
|
|
||||||
// latitude: 37.8,
|
|
||||||
// zoom: 14
|
|
||||||
// });
|
|
||||||
|
|
||||||
// Vessel position state
|
// Vessel position state
|
||||||
const [vesselPosition, setVesselPosition] = useState<VesselStatus | null>(null);
|
const [vesselPosition, setVesselPosition] = useState<VesselStatus | null>(null);
|
||||||
|
|
||||||
// Create vessel geojson data
|
// AIS state management
|
||||||
// const vesselGeojson: FeatureCollection = {
|
const [aisEnabled, setAisEnabled] = useState(false);
|
||||||
// type: 'FeatureCollection',
|
const [boundingBox, _setBoundingBox] = useState<{
|
||||||
// features: vesselPosition ? [
|
sw_lat: number;
|
||||||
// {
|
sw_lon: number;
|
||||||
// type: 'Feature',
|
ne_lat: number;
|
||||||
// geometry: {
|
ne_lon: number;
|
||||||
// type: 'Point',
|
} | undefined>(undefined);
|
||||||
// coordinates: [vesselPosition.longitude, vesselPosition.latitude]
|
const [vesselPopup, setVesselPopup] = useState<VesselData | null>(null);
|
||||||
// },
|
|
||||||
// properties: {
|
|
||||||
// title: 'Vessel Position',
|
|
||||||
// heading: vesselPosition.heading,
|
|
||||||
// speed: vesselPosition.speed
|
|
||||||
// }
|
|
||||||
// }
|
|
||||||
// ] : []
|
|
||||||
// };
|
|
||||||
|
|
||||||
|
// Use the AIS provider when enabled
|
||||||
// Button click handlers
|
const {
|
||||||
// const handleNavigationClick = useCallback(async () => {
|
vessels,
|
||||||
// if (typeof window !== 'undefined' && (window as any).__FLURX__) {
|
isConnected: aisConnected,
|
||||||
// try {
|
error: aisError,
|
||||||
// await (window as any).__FLURX__.invoke("navigation_clicked");
|
connectionStatus
|
||||||
// console.log('Navigation clicked');
|
} = useAISProvider(aisEnabled ? boundingBox : undefined);
|
||||||
// } catch (error) {
|
|
||||||
// console.error('Failed to invoke navigation_clicked:', error);
|
|
||||||
// }
|
|
||||||
// }
|
|
||||||
// }, []);
|
|
||||||
|
|
||||||
|
|
||||||
const selectSearchResult = useCallback(async (searchResult: { lat: string, lon: string }) => {
|
const selectSearchResult = useCallback(async (searchResult: { lat: string, lon: string }) => {
|
||||||
@@ -327,25 +249,6 @@ function App() {
|
|||||||
console.log('Layer changed to:', layer.name);
|
console.log('Layer changed to:', layer.name);
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
// const handleMapViewChange = useCallback(async (evt: any) => {
|
|
||||||
// const { longitude, latitude, zoom } = evt.viewState;
|
|
||||||
// setMapView({ longitude, latitude, zoom });
|
|
||||||
//
|
|
||||||
// if (typeof window !== 'undefined' && (window as any).__FLURX__) {
|
|
||||||
// try {
|
|
||||||
// const mapViewParams: MapViewParams = {
|
|
||||||
// latitude,
|
|
||||||
// longitude,
|
|
||||||
// zoom
|
|
||||||
// };
|
|
||||||
// await (window as any).__FLURX__.invoke("map_view_changed", mapViewParams);
|
|
||||||
// console.log('Map view changed:', mapViewParams);
|
|
||||||
// } catch (error) {
|
|
||||||
// console.error('Failed to invoke map_view_changed:', error);
|
|
||||||
// }
|
|
||||||
// }
|
|
||||||
// }, []);
|
|
||||||
|
|
||||||
// Poll for vessel status updates
|
// Poll for vessel status updates
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const pollVesselStatus = async () => {
|
const pollVesselStatus = async () => {
|
||||||
@@ -374,11 +277,6 @@ function App() {
|
|||||||
try {
|
try {
|
||||||
const mapInit: GpsPosition = await (window as any).__FLURX__.invoke("get_map_init");
|
const mapInit: GpsPosition = await (window as any).__FLURX__.invoke("get_map_init");
|
||||||
console.log('Map initialization data:', mapInit);
|
console.log('Map initialization data:', mapInit);
|
||||||
// setMapView({
|
|
||||||
// latitude: mapInit.latitude,
|
|
||||||
// longitude: mapInit.longitude,
|
|
||||||
// zoom: mapInit.zoom
|
|
||||||
// });
|
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Failed to get map initialization data:', error);
|
console.error('Failed to get map initialization data:', error);
|
||||||
}
|
}
|
||||||
@@ -389,63 +287,46 @@ function App() {
|
|||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
/* Full-screen wrapper — fills the viewport and becomes the positioning context */
|
/* Full-screen wrapper — fills the viewport and becomes the positioning context */
|
||||||
<Box w="100vw" h="100vh" position="relative" overflow="hidden">
|
<Box w="100vw" h="100vh" position="relative" overflow="hidden">
|
||||||
{/* GPS Feed Display — absolutely positioned at top-right */}
|
{/* GPS Feed Display — absolutely positioned at top-right */}
|
||||||
{vesselPosition && (
|
|
||||||
<Box
|
<Box
|
||||||
position="absolute"
|
position="absolute"
|
||||||
top={65}
|
top={65}
|
||||||
right={4}
|
right={4}
|
||||||
|
maxW="20%"
|
||||||
zIndex={1}
|
zIndex={1}
|
||||||
p={4}
|
p={4}
|
||||||
fontSize="sm"
|
|
||||||
fontFamily="monospace"
|
|
||||||
minW="220px"
|
|
||||||
backdropFilter="blur(10px)"
|
|
||||||
{...getNeumorphicStyle(colorMode as 'light' | 'dark')}
|
|
||||||
>
|
>
|
||||||
<Box fontWeight="bold" mb={3} fontSize="md">GPS Feed</Box>
|
{vesselPosition && (
|
||||||
<Box mb={1}>Lat: {vesselPosition.latitude.toFixed(6)}°</Box>
|
<GpsFeed vesselPosition={vesselPosition} colorMode={colorMode}/>
|
||||||
<Box mb={1}>Lon: {vesselPosition.longitude.toFixed(6)}°</Box>
|
|
||||||
<Box mb={1}>Heading: {vesselPosition.heading.toFixed(1)}°</Box>
|
|
||||||
<Box>Speed: {vesselPosition.speed.toFixed(1)} kts</Box>
|
|
||||||
</Box>
|
|
||||||
)}
|
)}
|
||||||
|
|
||||||
|
{/* AIS Status Panel */}
|
||||||
|
{aisEnabled && (
|
||||||
|
<AisFeed
|
||||||
|
vesselPosition={vesselPosition}
|
||||||
|
colorMode={colorMode}
|
||||||
|
connectionStatus={connectionStatus}
|
||||||
|
vesselData={vessels}
|
||||||
|
aisError={aisError}
|
||||||
|
aisConnected={aisConnected}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
</Box>
|
||||||
{/* Button bar — absolutely positioned inside the wrapper */}
|
{/* Button bar — absolutely positioned inside the wrapper */}
|
||||||
<HStack position="absolute" top={4} right={4} zIndex={1}>
|
<HStack position="absolute" top={4} right={4} zIndex={1}>
|
||||||
<Box
|
<Search
|
||||||
display="flex"
|
|
||||||
alignItems="center"
|
|
||||||
position="relative"
|
|
||||||
>
|
|
||||||
<Button
|
|
||||||
size="sm"
|
|
||||||
variant="surface"
|
|
||||||
onClick={handleSearchClick}
|
onClick={handleSearchClick}
|
||||||
mr={2}
|
colorMode={colorMode}
|
||||||
{...getNeumorphicStyle(colorMode as 'light' | 'dark')}
|
searchOpen={isSearchOpen}
|
||||||
>
|
|
||||||
<Text>Search...</Text>
|
|
||||||
</Button>
|
|
||||||
{isSearchOpen && <Box
|
|
||||||
w="200px"
|
|
||||||
transform={`translateX(${isSearchOpen ? "0" : "100%"})`}
|
|
||||||
opacity={isSearchOpen ? 1 : 0}
|
|
||||||
onKeyDown={(e) => {
|
onKeyDown={(e) => {
|
||||||
console.log(e);
|
console.log(e);
|
||||||
if (e.key === 'Escape') {
|
if (e.key === 'Escape') {
|
||||||
setIsSearchOpen(false)
|
setIsSearchOpen(false)
|
||||||
}
|
}
|
||||||
}}
|
}}
|
||||||
backdropFilter="blur(10px)"
|
|
||||||
{...getNeumorphicStyle(colorMode as 'light' | 'dark', 'pressed')}
|
|
||||||
>
|
|
||||||
<Input
|
|
||||||
placeholder="Search..."
|
|
||||||
size="sm"
|
|
||||||
value={searchInput}
|
value={searchInput}
|
||||||
onChange={e => setSearchInput(e.target.value)}
|
onChange={e => setSearchInput(e.target.value)}
|
||||||
onKeyPress={async (e) => {
|
onKeyPress={async (e) => {
|
||||||
@@ -454,32 +335,11 @@ function App() {
|
|||||||
await handleSearchClick()
|
await handleSearchClick()
|
||||||
}
|
}
|
||||||
}}
|
}}
|
||||||
border="none"
|
searchResults={searchResults}
|
||||||
{...getNeumorphicStyle(colorMode as 'light' | 'dark', 'pressed')}
|
callbackfn={(result, index) => {
|
||||||
/>
|
|
||||||
{searchResults.length > 0 && (
|
|
||||||
<Box
|
|
||||||
position="absolute"
|
|
||||||
top="100%"
|
|
||||||
left={0}
|
|
||||||
w="200px"
|
|
||||||
zIndex={2}
|
|
||||||
mt={2}
|
|
||||||
|
|
||||||
backdropFilter="blur(10px)"
|
|
||||||
{...getNeumorphicStyle(colorMode as 'light' | 'dark')}
|
|
||||||
>
|
|
||||||
{searchResults.map((result, index) => {
|
|
||||||
const colors = getNeumorphicColors(colorMode as 'light' | 'dark');
|
const colors = getNeumorphicColors(colorMode as 'light' | 'dark');
|
||||||
return (
|
return (
|
||||||
<Box
|
<SearchResult key={index} onKeyPress={async (e) => {
|
||||||
key={index}
|
|
||||||
p={3}
|
|
||||||
cursor="pointer"
|
|
||||||
borderRadius="8px"
|
|
||||||
transition="all 0.2s ease-in-out"
|
|
||||||
onKeyPress={async (e) => {
|
|
||||||
console.log(e.key)
|
|
||||||
if (e.key === 'Enter' && searchResults.length > 0) {
|
if (e.key === 'Enter' && searchResults.length > 0) {
|
||||||
console.log(`Selecting result ${result.lat}, ${result.lon}`);
|
console.log(`Selecting result ${result.lat}, ${result.lon}`);
|
||||||
await selectSearchResult(result);
|
await selectSearchResult(result);
|
||||||
@@ -487,43 +347,43 @@ function App() {
|
|||||||
setIsSearchOpen(false);
|
setIsSearchOpen(false);
|
||||||
}
|
}
|
||||||
}}
|
}}
|
||||||
_hover={{
|
colors={colors}
|
||||||
bg: colors.accent + '20',
|
|
||||||
transform: 'translateY(-1px)',
|
|
||||||
}}
|
|
||||||
onClick={async () => {
|
onClick={async () => {
|
||||||
console.log(`Selecting result ${result.lat}, ${result.lon}`);
|
console.log(`Selecting result ${result.lat}, ${result.lon}`);
|
||||||
await selectSearchResult(result);
|
await selectSearchResult(result);
|
||||||
setSearchResults([]);
|
setSearchResults([]);
|
||||||
setIsSearchOpen(false);
|
setIsSearchOpen(false);
|
||||||
}}
|
}}
|
||||||
>
|
result={result}
|
||||||
{`${result.lat}, ${result.lon}`}
|
/>
|
||||||
</Box>
|
|
||||||
);
|
);
|
||||||
})}
|
}}/>
|
||||||
</Box>
|
<Button
|
||||||
)}
|
size="sm"
|
||||||
</Box>}
|
variant="surface"
|
||||||
</Box>
|
onClick={() => setAisEnabled(!aisEnabled)}
|
||||||
|
mr={2}
|
||||||
|
{...getNeumorphicStyle(colorMode as 'light' | 'dark')}
|
||||||
|
bg={aisEnabled ? 'green.500' : undefined}
|
||||||
|
_hover={{
|
||||||
|
bg: aisEnabled ? 'green.600' : undefined,
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Text>AIS {aisEnabled ? 'ON' : 'OFF'}</Text>
|
||||||
|
</Button>
|
||||||
<LayerSelector onClick={handleLayerChange}/>
|
<LayerSelector onClick={handleLayerChange}/>
|
||||||
</HStack>
|
</HStack>
|
||||||
<MapNext mapboxPublicKey={atob(key)} vesselPosition={vesselPosition} layer={selectedLayer} mapView={mapView} geolocation={window.navigator.geolocation || custom_geolocation}/>
|
<MapNext
|
||||||
{/*<Map*/}
|
mapboxPublicKey={atob(key)}
|
||||||
{/* mapboxAccessToken={atob(key)}*/}
|
vesselPosition={vesselPosition}
|
||||||
{/* initialViewState={mapView}*/}
|
layer={selectedLayer}
|
||||||
{/* onMove={handleMapViewChange}*/}
|
mapView={mapView}
|
||||||
{/* mapStyle="mapbox://styles/mapbox/dark-v11"*/}
|
geolocation={custom_geolocation}
|
||||||
{/* reuseMaps*/}
|
aisVessels={aisEnabled ? vessels : []}
|
||||||
{/* attributionControl={false}*/}
|
onVesselClick={setVesselPopup}
|
||||||
{/* style={{width: '100%', height: '100%'}} // let the wrapper dictate size*/}
|
vesselPopup={vesselPopup}
|
||||||
{/*>*/}
|
onVesselPopupClose={() => setVesselPopup(null)}
|
||||||
{/* /!*{vesselPosition && (*!/*/}
|
/>
|
||||||
{/* /!* <Source id="vessel-data" type="geojson" data={vesselGeojson}>*!/*/}
|
|
||||||
{/* /!* <Layer {...vesselLayerStyle} />*!/*/}
|
|
||||||
{/* /!* </Source>*!/*/}
|
|
||||||
{/* /!*)}*!/*/}
|
|
||||||
{/*</Map>*/}
|
|
||||||
</Box>
|
</Box>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
27
crates/base-map/map/src/CustomGeolocate.ts
Normal file
27
crates/base-map/map/src/CustomGeolocate.ts
Normal file
@@ -0,0 +1,27 @@
|
|||||||
|
import type {Geolocation} from "@/MapNext.tsx";
|
||||||
|
|
||||||
|
|
||||||
|
export class NativeGeolocation implements Geolocation {
|
||||||
|
constructor({clearWatch, getCurrentPosition, watchPosition}: {
|
||||||
|
clearWatch: (watchId: number) => void;
|
||||||
|
getCurrentPosition: (successCallback: PositionCallback, errorCallback?: PositionErrorCallback | null, options?: PositionOptions) => void;
|
||||||
|
watchPosition: (successCallback: PositionCallback, errorCallback?: PositionErrorCallback | null, options?: PositionOptions) => number;
|
||||||
|
}) {
|
||||||
|
this.clearWatch = clearWatch;
|
||||||
|
this.watchPosition = watchPosition;
|
||||||
|
this.getCurrentPosition = getCurrentPosition;
|
||||||
|
}
|
||||||
|
|
||||||
|
clearWatch(_watchId: number): void {
|
||||||
|
throw new Error('Method not implemented.');
|
||||||
|
}
|
||||||
|
|
||||||
|
getCurrentPosition(_successCallback: PositionCallback, _errorCallback?: PositionErrorCallback | null, _options?: PositionOptions): void {
|
||||||
|
throw new Error('Method not implemented.');
|
||||||
|
}
|
||||||
|
|
||||||
|
watchPosition(_successCallback: PositionCallback, _errorCallback?: PositionErrorCallback | null, _options?: PositionOptions): number {
|
||||||
|
throw new Error('Method not implemented.');
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
@@ -11,6 +11,8 @@ 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';
|
||||||
|
import type { VesselData } from './ais-provider';
|
||||||
|
|
||||||
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";
|
||||||
@@ -27,7 +29,19 @@ export interface Geolocation {
|
|||||||
|
|
||||||
|
|
||||||
|
|
||||||
export default function MapNext(props: any = {mapboxPublicKey: "", geolocation: Geolocation, vesselPosition: undefined, layer: undefined, mapView: undefined} as any) {
|
interface MapNextProps {
|
||||||
|
mapboxPublicKey: string;
|
||||||
|
geolocation: Geolocation;
|
||||||
|
vesselPosition?: any;
|
||||||
|
layer?: any;
|
||||||
|
mapView?: any;
|
||||||
|
aisVessels?: VesselData[];
|
||||||
|
onVesselClick?: (vessel: VesselData) => void;
|
||||||
|
vesselPopup?: VesselData | null;
|
||||||
|
onVesselPopupClose?: () => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
export default function MapNext(props: MapNextProps) {
|
||||||
const [popupInfo, setPopupInfo] = useState(null);
|
const [popupInfo, setPopupInfo] = useState(null);
|
||||||
const mapRef = useRef<MapRef | null>(null);
|
const mapRef = useRef<MapRef | null>(null);
|
||||||
|
|
||||||
@@ -70,6 +84,52 @@ export default function MapNext(props: any = {mapboxPublicKey: "", geolocation:
|
|||||||
[]
|
[]
|
||||||
);
|
);
|
||||||
|
|
||||||
|
// 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';
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// Create vessel markers
|
||||||
|
const vesselMarkers = useMemo(() =>
|
||||||
|
(props.aisVessels || []).map((vessel) => (
|
||||||
|
<Marker
|
||||||
|
key={`vessel-${vessel.id}`}
|
||||||
|
longitude={vessel.longitude}
|
||||||
|
latitude={vessel.latitude}
|
||||||
|
anchor="center"
|
||||||
|
onClick={(e) => {
|
||||||
|
e.originalEvent.stopPropagation();
|
||||||
|
if (props.onVesselClick) {
|
||||||
|
props.onVesselClick(vessel);
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<VesselMarker
|
||||||
|
heading={vessel.heading}
|
||||||
|
color={getVesselColor(vessel.type)}
|
||||||
|
size={16}
|
||||||
|
/>
|
||||||
|
</Marker>
|
||||||
|
)),
|
||||||
|
[props.aisVessels, props.onVesselClick]
|
||||||
|
);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Box>
|
<Box>
|
||||||
<Map
|
<Map
|
||||||
@@ -100,6 +160,31 @@ export default function MapNext(props: any = {mapboxPublicKey: "", geolocation:
|
|||||||
<ScaleControl />
|
<ScaleControl />
|
||||||
|
|
||||||
{pins}
|
{pins}
|
||||||
|
{vesselMarkers}
|
||||||
|
|
||||||
|
{/* Vessel Popup */}
|
||||||
|
{props.vesselPopup && (
|
||||||
|
<Popup
|
||||||
|
longitude={props.vesselPopup.longitude}
|
||||||
|
latitude={props.vesselPopup.latitude}
|
||||||
|
anchor="bottom"
|
||||||
|
onClose={() => props.onVesselPopupClose && props.onVesselPopupClose()}
|
||||||
|
closeButton={true}
|
||||||
|
closeOnClick={false}
|
||||||
|
>
|
||||||
|
<div style={{ padding: '10px', minWidth: '200px' }}>
|
||||||
|
<h4 style={{ margin: '0 0 10px 0' }}>{props.vesselPopup.name}</h4>
|
||||||
|
<div><strong>MMSI:</strong> {props.vesselPopup.mmsi}</div>
|
||||||
|
<div><strong>Type:</strong> {props.vesselPopup.type}</div>
|
||||||
|
<div><strong>Speed:</strong> {props.vesselPopup.speed.toFixed(1)} knots</div>
|
||||||
|
<div><strong>Heading:</strong> {props.vesselPopup.heading}°</div>
|
||||||
|
<div><strong>Position:</strong> {props.vesselPopup.latitude.toFixed(4)}, {props.vesselPopup.longitude.toFixed(4)}</div>
|
||||||
|
<div style={{ fontSize: '12px', color: '#666', marginTop: '5px' }}>
|
||||||
|
Last update: {props.vesselPopup.lastUpdate.toLocaleTimeString()}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</Popup>
|
||||||
|
)}
|
||||||
|
|
||||||
{popupInfo && (
|
{popupInfo && (
|
||||||
<Popup
|
<Popup
|
||||||
|
@@ -88,9 +88,9 @@ export const useAISProvider = (boundingBox?: BoundingBox) => {
|
|||||||
|
|
||||||
const wsRef = useRef<WebSocket | null>(null);
|
const wsRef = useRef<WebSocket | null>(null);
|
||||||
const vesselMapRef = useRef<Map<string, VesselData>>(new Map());
|
const vesselMapRef = useRef<Map<string, VesselData>>(new Map());
|
||||||
const reconnectTimeoutRef = useRef<NodeJS.Timeout | null>(null);
|
const reconnectTimeoutRef = useRef<any | null>(null);
|
||||||
const reconnectAttemptsRef = useRef<number>(0);
|
const reconnectAttemptsRef = useRef<number>(0);
|
||||||
const connectionTimeoutRef = useRef<NodeJS.Timeout | null>(null);
|
const connectionTimeoutRef = useRef<any | null>(null);
|
||||||
const isConnectingRef = useRef<boolean>(false);
|
const isConnectingRef = useRef<boolean>(false);
|
||||||
const isMountedRef = useRef<boolean>(true);
|
const isMountedRef = useRef<boolean>(true);
|
||||||
const maxReconnectAttempts = 10;
|
const maxReconnectAttempts = 10;
|
34
crates/base-map/map/src/components/map/AisFeedInfo.tsx
Normal file
34
crates/base-map/map/src/components/map/AisFeedInfo.tsx
Normal file
@@ -0,0 +1,34 @@
|
|||||||
|
import type {VesselData} from "@/ais-provider.tsx";
|
||||||
|
import type { VesselStatus } from "@/types";
|
||||||
|
import { Box } from "@chakra-ui/react";
|
||||||
|
import {getNeumorphicStyle} from "@/theme/neumorphic-theme.ts";
|
||||||
|
|
||||||
|
export function AisFeed(props: {
|
||||||
|
vesselPosition: VesselStatus | null,
|
||||||
|
colorMode: "light" | "dark",
|
||||||
|
connectionStatus: string,
|
||||||
|
vesselData: VesselData[],
|
||||||
|
aisError: string | null,
|
||||||
|
aisConnected: boolean
|
||||||
|
}) {
|
||||||
|
return <Box
|
||||||
|
position="relative"
|
||||||
|
zIndex={1}
|
||||||
|
p={4}
|
||||||
|
fontSize="sm"
|
||||||
|
fontFamily="monospace"
|
||||||
|
maxH="20%"
|
||||||
|
backdropFilter="blur(10px)"
|
||||||
|
{...getNeumorphicStyle(props.colorMode as "light" | "dark")}
|
||||||
|
>
|
||||||
|
<Box fontWeight="bold" mb={3} fontSize="md">AIS Status</Box>
|
||||||
|
<Box mb={1}>Status: {props.connectionStatus}</Box>
|
||||||
|
<Box mb={1}>Vessels: {props.vesselData.length}</Box>
|
||||||
|
{props.aisError && <Box color="red.500" fontSize="xs">Error: {props.aisError}</Box>}
|
||||||
|
{props.aisConnected && (
|
||||||
|
<Box color="green.500" fontSize="xs" mt={2}>
|
||||||
|
✓ Connected to AIS server
|
||||||
|
</Box>
|
||||||
|
)}
|
||||||
|
</Box>;
|
||||||
|
}
|
23
crates/base-map/map/src/components/map/GpsFeedInfo.tsx
Normal file
23
crates/base-map/map/src/components/map/GpsFeedInfo.tsx
Normal file
@@ -0,0 +1,23 @@
|
|||||||
|
import type {VesselStatus} from "@/types.ts";
|
||||||
|
import {Box} from "@chakra-ui/react";
|
||||||
|
import {getNeumorphicStyle} from "@/theme/neumorphic-theme.ts";
|
||||||
|
|
||||||
|
export function GpsFeed(props: { vesselPosition: VesselStatus, colorMode: 'light' | 'dark' }) {
|
||||||
|
return <>
|
||||||
|
<Box
|
||||||
|
position="relative"
|
||||||
|
zIndex={1}
|
||||||
|
fontSize="sm"
|
||||||
|
maxH="20%"
|
||||||
|
fontFamily="monospace"
|
||||||
|
backdropFilter="blur(10px)"
|
||||||
|
{...getNeumorphicStyle(props.colorMode)}
|
||||||
|
>
|
||||||
|
<Box fontWeight="bold" mb={3} fontSize="md">GPS Feed</Box>
|
||||||
|
<Box mb={1}>Lat: {props.vesselPosition.latitude.toFixed(6)}°</Box>
|
||||||
|
<Box mb={1}>Lon: {props.vesselPosition.longitude.toFixed(6)}°</Box>
|
||||||
|
<Box mb={1}>Heading: {props.vesselPosition.heading.toFixed(1)}°</Box>
|
||||||
|
<Box>Speed: {props.vesselPosition.speed.toFixed(1)} kts</Box>
|
||||||
|
</Box>
|
||||||
|
</>;
|
||||||
|
}
|
63
crates/base-map/map/src/components/map/Search.tsx
Normal file
63
crates/base-map/map/src/components/map/Search.tsx
Normal file
@@ -0,0 +1,63 @@
|
|||||||
|
import {Box, Button, Input, Text} from "@chakra-ui/react";
|
||||||
|
import {getNeumorphicStyle} from "@/theme/neumorphic-theme.ts";
|
||||||
|
|
||||||
|
export function Search(props: {
|
||||||
|
onClick: () => Promise<void>,
|
||||||
|
colorMode: "light" | "dark",
|
||||||
|
searchOpen: boolean,
|
||||||
|
onKeyDown: (e: any) => void,
|
||||||
|
value: string,
|
||||||
|
onChange: (e: any) => void,
|
||||||
|
onKeyPress: (e: any) => Promise<void>,
|
||||||
|
searchResults: any[],
|
||||||
|
callbackfn: (result: any, index: any) => any // JSX.Element
|
||||||
|
}) {
|
||||||
|
return <Box
|
||||||
|
display="flex"
|
||||||
|
alignItems="center"
|
||||||
|
position="relative"
|
||||||
|
>
|
||||||
|
<Button
|
||||||
|
size="sm"
|
||||||
|
variant="surface"
|
||||||
|
onClick={props.onClick}
|
||||||
|
mr={2}
|
||||||
|
{...getNeumorphicStyle(props.colorMode as "light" | "dark")}
|
||||||
|
>
|
||||||
|
<Text>Search...</Text>
|
||||||
|
</Button>
|
||||||
|
{props.searchOpen && <Box
|
||||||
|
w="200px"
|
||||||
|
transform={`translateX(${props.searchOpen ? "0" : "100%"})`}
|
||||||
|
opacity={props.searchOpen ? 1 : 0}
|
||||||
|
onKeyDown={props.onKeyDown}
|
||||||
|
backdropFilter="blur(10px)"
|
||||||
|
{...getNeumorphicStyle(props.colorMode as "light" | "dark", "pressed")}
|
||||||
|
>
|
||||||
|
<Input
|
||||||
|
placeholder="Search..."
|
||||||
|
size="sm"
|
||||||
|
value={props.value}
|
||||||
|
onChange={props.onChange}
|
||||||
|
onKeyPress={props.onKeyPress}
|
||||||
|
border="none"
|
||||||
|
{...getNeumorphicStyle(props.colorMode as "light" | "dark", "pressed")}
|
||||||
|
/>
|
||||||
|
{props.searchResults.length > 0 && (
|
||||||
|
<Box
|
||||||
|
position="absolute"
|
||||||
|
top="100%"
|
||||||
|
left={0}
|
||||||
|
w="200px"
|
||||||
|
zIndex={2}
|
||||||
|
mt={2}
|
||||||
|
|
||||||
|
backdropFilter="blur(10px)"
|
||||||
|
{...getNeumorphicStyle(props.colorMode as "light" | "dark")}
|
||||||
|
>
|
||||||
|
{props.searchResults.map(props.callbackfn)}
|
||||||
|
</Box>
|
||||||
|
)}
|
||||||
|
</Box>}
|
||||||
|
</Box>;
|
||||||
|
}
|
39
crates/base-map/map/src/components/map/SearchResult.tsx
Normal file
39
crates/base-map/map/src/components/map/SearchResult.tsx
Normal file
@@ -0,0 +1,39 @@
|
|||||||
|
import {Box} from "@chakra-ui/react";
|
||||||
|
|
||||||
|
interface SearchResultProps {
|
||||||
|
onKeyPress: (e: any) => Promise<void>;
|
||||||
|
colors: {
|
||||||
|
bg: string;
|
||||||
|
surface: string;
|
||||||
|
text: string;
|
||||||
|
textSecondary: string;
|
||||||
|
accent: string;
|
||||||
|
shadow: { dark: string; light: string }
|
||||||
|
} | {
|
||||||
|
bg: string;
|
||||||
|
surface: string;
|
||||||
|
text: string;
|
||||||
|
textSecondary: string;
|
||||||
|
accent: string;
|
||||||
|
shadow: { dark: string; light: string }
|
||||||
|
};
|
||||||
|
onClick: () => Promise<void>;
|
||||||
|
result: any;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function SearchResult(props: SearchResultProps) {
|
||||||
|
return <Box
|
||||||
|
p={3}
|
||||||
|
cursor="pointer"
|
||||||
|
borderRadius="8px"
|
||||||
|
transition="all 0.2s ease-in-out"
|
||||||
|
onKeyPress={props.onKeyPress}
|
||||||
|
_hover={{
|
||||||
|
bg: props.colors.accent + "20",
|
||||||
|
transform: "translateY(-1px)",
|
||||||
|
}}
|
||||||
|
onClick={props.onClick}
|
||||||
|
>
|
||||||
|
{`${props.result.lat}, ${props.result.lon}`}
|
||||||
|
</Box>;
|
||||||
|
}
|
24
crates/base-map/map/src/types.ts
Normal file
24
crates/base-map/map/src/types.ts
Normal file
@@ -0,0 +1,24 @@
|
|||||||
|
// Types for bevy_flurx_ipc communication
|
||||||
|
export interface GpsPosition {
|
||||||
|
latitude: number;
|
||||||
|
longitude: number;
|
||||||
|
zoom: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface VesselStatus {
|
||||||
|
latitude: number;
|
||||||
|
longitude: number;
|
||||||
|
heading: number;
|
||||||
|
speed: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
// interface MapViewParams {
|
||||||
|
// latitude: number;
|
||||||
|
// longitude: number;
|
||||||
|
// zoom: number;
|
||||||
|
// }
|
||||||
|
|
||||||
|
// interface AuthParams {
|
||||||
|
// authenticated: boolean;
|
||||||
|
// token: string | null;
|
||||||
|
// }
|
@@ -80,6 +80,7 @@ winit = { version = "0.30", default-features = false }
|
|||||||
image = { version = "0.25", default-features = false }
|
image = { version = "0.25", default-features = false }
|
||||||
## This greatly improves WGPU's performance due to its heavy use of trace! calls
|
## This greatly improves WGPU's performance due to its heavy use of trace! calls
|
||||||
log = { version = "0.4", features = ["max_level_debug", "release_max_level_warn"] }
|
log = { version = "0.4", features = ["max_level_debug", "release_max_level_warn"] }
|
||||||
|
anyhow = "1.0.98"
|
||||||
|
|
||||||
# Platform-specific tokio features
|
# Platform-specific tokio features
|
||||||
[target.'cfg(not(target_arch = "wasm32"))'.dependencies]
|
[target.'cfg(not(target_arch = "wasm32"))'.dependencies]
|
||||||
@@ -100,4 +101,4 @@ console_error_panic_hook = "0.1"
|
|||||||
|
|
||||||
[build-dependencies]
|
[build-dependencies]
|
||||||
embed-resource = "1"
|
embed-resource = "1"
|
||||||
# base-map = { path = "../base-map" } # Temporarily disabled for testing
|
base-map = { path = "../base-map" } # Comment to Temporarily disable for testing
|
||||||
|
@@ -9,12 +9,22 @@ use bevy::DefaultPlugins;
|
|||||||
use yachtpit::GamePlugin;
|
use yachtpit::GamePlugin;
|
||||||
use std::io::Cursor;
|
use std::io::Cursor;
|
||||||
use winit::window::Icon;
|
use winit::window::Icon;
|
||||||
|
use tokio::process::Command;
|
||||||
|
|
||||||
#[cfg(not(target_arch = "wasm32"))]
|
#[cfg(not(target_arch = "wasm32"))]
|
||||||
use bevy_webview_wry::WebviewWryPlugin;
|
use bevy_webview_wry::WebviewWryPlugin;
|
||||||
|
|
||||||
fn main() {
|
|
||||||
#[cfg(not(target_arch = "wasm32"))]
|
#[cfg(not(target_arch = "wasm32"))]
|
||||||
|
#[tokio::main(flavor = "multi_thread")]
|
||||||
|
async fn main() {
|
||||||
|
|
||||||
|
launch_bevy();
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
#[cfg(not(target_arch = "wasm32"))]
|
||||||
|
fn launch_bevy() {
|
||||||
App::new()
|
App::new()
|
||||||
.insert_resource(ClearColor(Color::NONE))
|
.insert_resource(ClearColor(Color::NONE))
|
||||||
.add_plugins(
|
.add_plugins(
|
||||||
@@ -22,7 +32,6 @@ fn main() {
|
|||||||
.set(WindowPlugin {
|
.set(WindowPlugin {
|
||||||
primary_window: Some(Window {
|
primary_window: Some(Window {
|
||||||
// Bind to canvas included in `index.html`
|
// Bind to canvas included in `index.html`
|
||||||
canvas: Some("#yachtpit-canvas".to_owned()),
|
|
||||||
fit_canvas_to_parent: true,
|
fit_canvas_to_parent: true,
|
||||||
// Tells wasm not to override default event handling, like F5 and Ctrl+R
|
// Tells wasm not to override default event handling, like F5 and Ctrl+R
|
||||||
prevent_default_event_handling: false,
|
prevent_default_event_handling: false,
|
||||||
@@ -37,10 +46,14 @@ fn main() {
|
|||||||
)
|
)
|
||||||
.add_plugins(GamePlugin)
|
.add_plugins(GamePlugin)
|
||||||
.add_systems(Startup, set_window_icon)
|
.add_systems(Startup, set_window_icon)
|
||||||
|
.add_systems(Update, start_ais_server)
|
||||||
.add_plugins(WebviewWryPlugin::default())
|
.add_plugins(WebviewWryPlugin::default())
|
||||||
.run();
|
.run();
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
#[cfg(target_arch = "wasm32")]
|
#[cfg(target_arch = "wasm32")]
|
||||||
|
fn launch_bevy() {
|
||||||
{
|
{
|
||||||
// Add console logging for WASM debugging
|
// Add console logging for WASM debugging
|
||||||
console_error_panic_hook::set_once();
|
console_error_panic_hook::set_once();
|
||||||
@@ -73,9 +86,20 @@ fn main() {
|
|||||||
})
|
})
|
||||||
.run();
|
.run();
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn start_ais_server() {
|
||||||
|
let rt = tokio::runtime::Runtime::new().unwrap();
|
||||||
|
rt.block_on(async {
|
||||||
|
if let Ok(mut cmd) = Command::new("cargo")
|
||||||
|
.current_dir("../ais-server")
|
||||||
|
.arg("run").arg("--release").spawn() {
|
||||||
|
let _ = cmd.wait().await;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
// Sets the icon on windows and X11
|
// Sets the icon on windows and X11
|
||||||
fn set_window_icon(
|
fn set_window_icon(
|
||||||
windows: NonSend<WinitWindows>,
|
windows: NonSend<WinitWindows>,
|
||||||
|
@@ -148,7 +148,7 @@ fn setup_menu(mut commands: Commands) {
|
|||||||
BackgroundColor(secondary_button_colors.normal),
|
BackgroundColor(secondary_button_colors.normal),
|
||||||
BorderColor(Color::linear_rgb(0.80, 0.82, 0.85)),
|
BorderColor(Color::linear_rgb(0.80, 0.82, 0.85)),
|
||||||
BorderRadius::all(Val::Px(12.0)),
|
BorderRadius::all(Val::Px(12.0)),
|
||||||
secondary_button_colors,
|
secondary_button_colors.clone(),
|
||||||
OpenLink("https://bevyengine.org"),
|
OpenLink("https://bevyengine.org"),
|
||||||
))
|
))
|
||||||
.with_child((
|
.with_child((
|
||||||
|
Reference in New Issue
Block a user