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"
|
||||
version = "0.1.0"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"base-map",
|
||||
"bevy",
|
||||
"bevy_asset_loader",
|
||||
"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",
|
||||
"react": "^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": {
|
||||
"@chakra-ui/react": "^3.21.1",
|
||||
@@ -1712,6 +1714,12 @@
|
||||
"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": {
|
||||
"version": "1.12.14",
|
||||
"resolved": "https://registry.npmjs.org/@swc/core/-/core-1.12.14.tgz",
|
||||
@@ -3983,6 +3991,66 @@
|
||||
"dev": true,
|
||||
"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": {
|
||||
"version": "6.0.1",
|
||||
"resolved": "https://registry.npmjs.org/entities/-/entities-6.0.1.tgz",
|
||||
@@ -5145,7 +5213,6 @@
|
||||
"version": "2.1.3",
|
||||
"resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz",
|
||||
"integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==",
|
||||
"dev": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/murmurhash-js": {
|
||||
@@ -5869,6 +5936,68 @@
|
||||
"dev": true,
|
||||
"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": {
|
||||
"version": "0.2.0",
|
||||
"resolved": "https://registry.npmjs.org/sort-asc/-/sort-asc-0.2.0.tgz",
|
||||
@@ -6732,7 +6861,6 @@
|
||||
"version": "8.18.3",
|
||||
"resolved": "https://registry.npmjs.org/ws/-/ws-8.18.3.tgz",
|
||||
"integrity": "sha512-PEIGCY5tSlUt50cqyMXfCzX+oOPqN0vuGqWzbcJ2xvnkzkq46oOpz7dQaTDBdfICb4N14+GARUDw2XV2N4tvzg==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=10.0.0"
|
||||
@@ -6767,6 +6895,14 @@
|
||||
"dev": true,
|
||||
"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": {
|
||||
"version": "2.8.0",
|
||||
"resolved": "https://registry.npmjs.org/yaml/-/yaml-2.8.0.tgz",
|
||||
|
@@ -16,7 +16,9 @@
|
||||
"next-themes": "^0.4.6",
|
||||
"react": "^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": {
|
||||
"@chakra-ui/react": "^3.21.1",
|
||||
|
@@ -1,229 +1,24 @@
|
||||
import 'mapbox-gl/dist/mapbox-gl.css';
|
||||
import {Box, Button, HStack, Input, Text} from '@chakra-ui/react';
|
||||
import { useColorMode } from './components/ui/color-mode';
|
||||
import {Box, Button, HStack, Text} from '@chakra-ui/react';
|
||||
import {useColorMode} from './components/ui/color-mode';
|
||||
import {useCallback, useEffect, useState} from "react";
|
||||
import MapNext, {type Geolocation} from "@/MapNext.tsx";
|
||||
import { getNeumorphicStyle, getNeumorphicColors } from './theme/neumorphic-theme';
|
||||
import MapNext from "@/MapNext.tsx";
|
||||
import {getNeumorphicColors, getNeumorphicStyle} from './theme/neumorphic-theme';
|
||||
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
|
||||
const key =
|
||||
'cGsuZXlKMUlqb2laMlZ2Wm1aelpXVWlMQ0poSWpvaVkycDFOalo0YkdWNk1EUTRjRE41YjJnNFp6VjNNelp6YXlKOS56LUtzS1l0X3VGUGdCSDYwQUFBNFNn';
|
||||
|
||||
|
||||
// const vesselLayerStyle: CircleLayerSpecification = {
|
||||
// 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) => {
|
||||
if (typeof window !== 'undefined' && (window as any).geolocationWatches) {
|
||||
const interval = (window as any).geolocationWatches.get(watchId);
|
||||
if (interval) {
|
||||
clearInterval(interval);
|
||||
(window as any).geolocationWatches.delete(watchId);
|
||||
}
|
||||
}
|
||||
},
|
||||
watchPosition: (successCallback: PositionCallback, errorCallback?: PositionErrorCallback | null, options?: PositionOptions) => {
|
||||
if (typeof window === 'undefined') return 0;
|
||||
|
||||
// Initialize watches map if it doesn't exist
|
||||
if (!(window as any).geolocationWatches) {
|
||||
(window as any).geolocationWatches = new Map();
|
||||
}
|
||||
if (!(window as any).geolocationWatchId) {
|
||||
(window as any).geolocationWatchId = 0;
|
||||
}
|
||||
|
||||
const watchId = ++(window as any).geolocationWatchId;
|
||||
|
||||
const pollPosition = async () => {
|
||||
if ((window as any).__FLURX__) {
|
||||
try {
|
||||
const vesselStatus: VesselStatus = await (window as any).__FLURX__.invoke("get_vessel_status");
|
||||
const position: GeolocationPosition = {
|
||||
coords: {
|
||||
latitude: vesselStatus.latitude,
|
||||
longitude: vesselStatus.longitude,
|
||||
altitude: null,
|
||||
accuracy: 10, // Assume 10m accuracy
|
||||
altitudeAccuracy: null,
|
||||
heading: vesselStatus.heading,
|
||||
speed: vesselStatus.speed,
|
||||
toJSON: () => ({
|
||||
latitude: vesselStatus.latitude,
|
||||
longitude: vesselStatus.longitude,
|
||||
altitude: null,
|
||||
accuracy: 10,
|
||||
altitudeAccuracy: null,
|
||||
heading: vesselStatus.heading,
|
||||
speed: vesselStatus.speed
|
||||
})
|
||||
},
|
||||
timestamp: Date.now(),
|
||||
toJSON: () => ({
|
||||
coords: {
|
||||
latitude: vesselStatus.latitude,
|
||||
longitude: vesselStatus.longitude,
|
||||
altitude: null,
|
||||
accuracy: 10,
|
||||
altitudeAccuracy: null,
|
||||
heading: vesselStatus.heading,
|
||||
speed: vesselStatus.speed
|
||||
},
|
||||
timestamp: Date.now()
|
||||
})
|
||||
};
|
||||
successCallback(position);
|
||||
} catch (error) {
|
||||
if (errorCallback) {
|
||||
const positionError: GeolocationPositionError = {
|
||||
code: 2, // POSITION_UNAVAILABLE
|
||||
message: 'Failed to get vessel status: ' + error,
|
||||
PERMISSION_DENIED: 1,
|
||||
POSITION_UNAVAILABLE: 2,
|
||||
TIMEOUT: 3
|
||||
};
|
||||
errorCallback(positionError);
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
// Poll immediately and then at intervals
|
||||
pollPosition();
|
||||
const interval = setInterval(pollPosition, options?.timeout || 5000);
|
||||
(window as any).geolocationWatches.set(watchId, interval);
|
||||
|
||||
return watchId;
|
||||
},
|
||||
getCurrentPosition: (successCallback: PositionCallback, errorCallback?: PositionErrorCallback | null, _options?: PositionOptions) => {
|
||||
if (typeof window !== 'undefined' && (window as any).__FLURX__) {
|
||||
(async () => {
|
||||
try {
|
||||
const vesselStatus: VesselStatus = await (window as any).__FLURX__.invoke("get_vessel_status");
|
||||
const position: GeolocationPosition = {
|
||||
coords: {
|
||||
latitude: vesselStatus.latitude,
|
||||
longitude: vesselStatus.longitude,
|
||||
altitude: null,
|
||||
accuracy: 10, // Assume 10m accuracy
|
||||
altitudeAccuracy: null,
|
||||
heading: vesselStatus.heading,
|
||||
speed: vesselStatus.speed,
|
||||
toJSON: () => ({
|
||||
latitude: vesselStatus.latitude,
|
||||
longitude: vesselStatus.longitude,
|
||||
altitude: null,
|
||||
accuracy: 10,
|
||||
altitudeAccuracy: null,
|
||||
heading: vesselStatus.heading,
|
||||
speed: vesselStatus.speed
|
||||
})
|
||||
},
|
||||
timestamp: Date.now(),
|
||||
toJSON: () => ({
|
||||
coords: {
|
||||
latitude: vesselStatus.latitude,
|
||||
longitude: vesselStatus.longitude,
|
||||
altitude: null,
|
||||
accuracy: 10,
|
||||
altitudeAccuracy: null,
|
||||
heading: vesselStatus.heading,
|
||||
speed: vesselStatus.speed
|
||||
},
|
||||
timestamp: Date.now()
|
||||
})
|
||||
};
|
||||
successCallback(position);
|
||||
} catch (error) {
|
||||
if (errorCallback) {
|
||||
const positionError: GeolocationPositionError = {
|
||||
code: 2, // POSITION_UNAVAILABLE
|
||||
message: 'Failed to get vessel status: ' + error,
|
||||
PERMISSION_DENIED: 1,
|
||||
POSITION_UNAVAILABLE: 2,
|
||||
TIMEOUT: 3
|
||||
};
|
||||
errorCallback(positionError);
|
||||
}
|
||||
}
|
||||
})();
|
||||
} else if (errorCallback) {
|
||||
const positionError: GeolocationPositionError = {
|
||||
code: 2, // POSITION_UNAVAILABLE
|
||||
message: '__FLURX__ not available',
|
||||
PERMISSION_DENIED: 1,
|
||||
POSITION_UNAVAILABLE: 2,
|
||||
TIMEOUT: 3
|
||||
};
|
||||
errorCallback(positionError);
|
||||
}
|
||||
},
|
||||
});
|
||||
|
||||
// interface MapViewParams {
|
||||
// latitude: number;
|
||||
// longitude: number;
|
||||
// zoom: number;
|
||||
// }
|
||||
|
||||
// interface AuthParams {
|
||||
// authenticated: boolean;
|
||||
// token: string | null;
|
||||
// }
|
||||
|
||||
|
||||
|
||||
function App() {
|
||||
const { colorMode } = useColorMode();
|
||||
const {colorMode} = useColorMode();
|
||||
const [isSearchOpen, setIsSearchOpen] = useState(false);
|
||||
const [selectedLayer, setSelectedLayer] = useState(layers[0]);
|
||||
const [searchInput, setSearchInput] = useState('');
|
||||
@@ -234,47 +29,174 @@ function App() {
|
||||
zoom: 14
|
||||
});
|
||||
|
||||
// Map state that can be updated from Rust
|
||||
// const [mapView, setMapView] = useState({
|
||||
// longitude: -122.4,
|
||||
// latitude: 37.8,
|
||||
// zoom: 14
|
||||
// });
|
||||
const custom_geolocation = new NativeGeolocation({
|
||||
clearWatch: (watchId: number) => {
|
||||
if (typeof window !== 'undefined' && (window as any).geolocationWatches) {
|
||||
const interval = (window as any).geolocationWatches.get(watchId);
|
||||
if (interval) {
|
||||
clearInterval(interval);
|
||||
(window as any).geolocationWatches.delete(watchId);
|
||||
}
|
||||
}
|
||||
},
|
||||
watchPosition: (successCallback: PositionCallback, errorCallback?: PositionErrorCallback | null, options?: PositionOptions) => {
|
||||
if (typeof window === 'undefined') return 0;
|
||||
|
||||
// Initialize watches map if it doesn't exist
|
||||
if (!(window as any).geolocationWatches) {
|
||||
(window as any).geolocationWatches = new Map();
|
||||
}
|
||||
if (!(window as any).geolocationWatchId) {
|
||||
(window as any).geolocationWatchId = 0;
|
||||
}
|
||||
|
||||
const watchId = ++(window as any).geolocationWatchId;
|
||||
|
||||
const pollPosition = async () => {
|
||||
if ((window as any).__FLURX__) {
|
||||
try {
|
||||
const vesselStatus: VesselStatus = await (window as any).__FLURX__.invoke("get_vessel_status");
|
||||
const position: GeolocationPosition = {
|
||||
coords: {
|
||||
latitude: vesselStatus.latitude,
|
||||
longitude: vesselStatus.longitude,
|
||||
altitude: null,
|
||||
accuracy: 10, // Assume 10m accuracy
|
||||
altitudeAccuracy: null,
|
||||
heading: vesselStatus.heading,
|
||||
speed: vesselStatus.speed,
|
||||
toJSON: () => ({
|
||||
latitude: vesselStatus.latitude,
|
||||
longitude: vesselStatus.longitude,
|
||||
altitude: null,
|
||||
accuracy: 10,
|
||||
altitudeAccuracy: null,
|
||||
heading: vesselStatus.heading,
|
||||
speed: vesselStatus.speed
|
||||
})
|
||||
},
|
||||
timestamp: Date.now(),
|
||||
toJSON: () => ({
|
||||
coords: {
|
||||
latitude: vesselStatus.latitude,
|
||||
longitude: vesselStatus.longitude,
|
||||
altitude: null,
|
||||
accuracy: 10,
|
||||
altitudeAccuracy: null,
|
||||
heading: vesselStatus.heading,
|
||||
speed: vesselStatus.speed
|
||||
},
|
||||
timestamp: Date.now()
|
||||
})
|
||||
};
|
||||
successCallback(position);
|
||||
} catch (error) {
|
||||
if (errorCallback) {
|
||||
const positionError: GeolocationPositionError = {
|
||||
code: 2, // POSITION_UNAVAILABLE
|
||||
message: 'Failed to get vessel status: ' + error,
|
||||
PERMISSION_DENIED: 1,
|
||||
POSITION_UNAVAILABLE: 2,
|
||||
TIMEOUT: 3
|
||||
};
|
||||
errorCallback(positionError);
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
// Poll immediately and then at intervals
|
||||
pollPosition();
|
||||
const interval = setInterval(pollPosition, options?.timeout || 5000);
|
||||
(window as any).geolocationWatches.set(watchId, interval);
|
||||
|
||||
return watchId;
|
||||
},
|
||||
getCurrentPosition: (successCallback: PositionCallback, errorCallback?: PositionErrorCallback | null, _options?: PositionOptions) => {
|
||||
if (typeof window !== 'undefined' && (window as any).__FLURX__) {
|
||||
(async () => {
|
||||
try {
|
||||
const vesselStatus: VesselStatus = await (window as any).__FLURX__.invoke("get_vessel_status");
|
||||
const position: GeolocationPosition = {
|
||||
coords: {
|
||||
latitude: vesselStatus.latitude,
|
||||
longitude: vesselStatus.longitude,
|
||||
altitude: null,
|
||||
accuracy: 10, // Assume 10m accuracy
|
||||
altitudeAccuracy: null,
|
||||
heading: vesselStatus.heading,
|
||||
speed: vesselStatus.speed,
|
||||
toJSON: () => ({
|
||||
latitude: vesselStatus.latitude,
|
||||
longitude: vesselStatus.longitude,
|
||||
altitude: null,
|
||||
accuracy: 10,
|
||||
altitudeAccuracy: null,
|
||||
heading: vesselStatus.heading,
|
||||
speed: vesselStatus.speed
|
||||
})
|
||||
},
|
||||
timestamp: Date.now(),
|
||||
toJSON: () => ({
|
||||
coords: {
|
||||
latitude: vesselStatus.latitude,
|
||||
longitude: vesselStatus.longitude,
|
||||
altitude: null,
|
||||
accuracy: 10,
|
||||
altitudeAccuracy: null,
|
||||
heading: vesselStatus.heading,
|
||||
speed: vesselStatus.speed
|
||||
},
|
||||
timestamp: Date.now()
|
||||
})
|
||||
};
|
||||
successCallback(position);
|
||||
} catch (error) {
|
||||
if (errorCallback) {
|
||||
const positionError: GeolocationPositionError = {
|
||||
code: 2, // POSITION_UNAVAILABLE
|
||||
message: 'Failed to get vessel status: ' + error,
|
||||
PERMISSION_DENIED: 1,
|
||||
POSITION_UNAVAILABLE: 2,
|
||||
TIMEOUT: 3
|
||||
};
|
||||
errorCallback(positionError);
|
||||
}
|
||||
}
|
||||
})();
|
||||
} else if (errorCallback) {
|
||||
const positionError: GeolocationPositionError = {
|
||||
code: 2, // POSITION_UNAVAILABLE
|
||||
message: '__FLURX__ not available',
|
||||
PERMISSION_DENIED: 1,
|
||||
POSITION_UNAVAILABLE: 2,
|
||||
TIMEOUT: 3
|
||||
};
|
||||
errorCallback(positionError);
|
||||
}
|
||||
},
|
||||
});
|
||||
|
||||
// Vessel position state
|
||||
const [vesselPosition, setVesselPosition] = useState<VesselStatus | null>(null);
|
||||
|
||||
// Create vessel geojson data
|
||||
// const vesselGeojson: FeatureCollection = {
|
||||
// type: 'FeatureCollection',
|
||||
// features: vesselPosition ? [
|
||||
// {
|
||||
// type: 'Feature',
|
||||
// geometry: {
|
||||
// type: 'Point',
|
||||
// coordinates: [vesselPosition.longitude, vesselPosition.latitude]
|
||||
// },
|
||||
// properties: {
|
||||
// title: 'Vessel Position',
|
||||
// heading: vesselPosition.heading,
|
||||
// speed: vesselPosition.speed
|
||||
// }
|
||||
// }
|
||||
// ] : []
|
||||
// };
|
||||
// AIS state management
|
||||
const [aisEnabled, setAisEnabled] = useState(false);
|
||||
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);
|
||||
|
||||
|
||||
// Button click handlers
|
||||
// const handleNavigationClick = useCallback(async () => {
|
||||
// if (typeof window !== 'undefined' && (window as any).__FLURX__) {
|
||||
// try {
|
||||
// await (window as any).__FLURX__.invoke("navigation_clicked");
|
||||
// console.log('Navigation clicked');
|
||||
// } catch (error) {
|
||||
// console.error('Failed to invoke navigation_clicked:', error);
|
||||
// }
|
||||
// }
|
||||
// }, []);
|
||||
// Use the AIS provider when enabled
|
||||
const {
|
||||
vessels,
|
||||
isConnected: aisConnected,
|
||||
error: aisError,
|
||||
connectionStatus
|
||||
} = useAISProvider(aisEnabled ? boundingBox : undefined);
|
||||
|
||||
|
||||
const selectSearchResult = useCallback(async (searchResult: { lat: string, lon: string }) => {
|
||||
@@ -300,9 +222,9 @@ function App() {
|
||||
}),
|
||||
});
|
||||
const coordinates = await geocode.json();
|
||||
const { lat, lon } = coordinates;
|
||||
const {lat, lon} = coordinates;
|
||||
console.log(`Got geocode coordinates: ${lat}, ${lon}`);
|
||||
setSearchResults([{ lat, lon }]);
|
||||
setSearchResults([{lat, lon}]);
|
||||
} catch (e) {
|
||||
console.error('Geocoding failed:', e);
|
||||
// Continue without results
|
||||
@@ -327,25 +249,6 @@ function App() {
|
||||
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
|
||||
useEffect(() => {
|
||||
const pollVesselStatus = async () => {
|
||||
@@ -374,11 +277,6 @@ function App() {
|
||||
try {
|
||||
const mapInit: GpsPosition = await (window as any).__FLURX__.invoke("get_map_init");
|
||||
console.log('Map initialization data:', mapInit);
|
||||
// setMapView({
|
||||
// latitude: mapInit.latitude,
|
||||
// longitude: mapInit.longitude,
|
||||
// zoom: mapInit.zoom
|
||||
// });
|
||||
} catch (error) {
|
||||
console.error('Failed to get map initialization data:', error);
|
||||
}
|
||||
@@ -389,141 +287,103 @@ function App() {
|
||||
}, []);
|
||||
|
||||
|
||||
|
||||
return (
|
||||
/* Full-screen wrapper — fills the viewport and becomes the positioning context */
|
||||
<Box w="100vw" h="100vh" position="relative" overflow="hidden">
|
||||
{/* GPS Feed Display — absolutely positioned at top-right */}
|
||||
<Box
|
||||
position="absolute"
|
||||
top={65}
|
||||
right={4}
|
||||
maxW="20%"
|
||||
zIndex={1}
|
||||
p={4}
|
||||
>
|
||||
{vesselPosition && (
|
||||
<Box
|
||||
position="absolute"
|
||||
top={65}
|
||||
right={4}
|
||||
zIndex={1}
|
||||
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>
|
||||
<Box mb={1}>Lat: {vesselPosition.latitude.toFixed(6)}°</Box>
|
||||
<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>
|
||||
<GpsFeed vesselPosition={vesselPosition} colorMode={colorMode}/>
|
||||
)}
|
||||
|
||||
{/* 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 */}
|
||||
<HStack position="absolute" top={4} right={4} zIndex={1}>
|
||||
<Box
|
||||
display="flex"
|
||||
alignItems="center"
|
||||
position="relative"
|
||||
>
|
||||
<Button
|
||||
size="sm"
|
||||
variant="surface"
|
||||
onClick={handleSearchClick}
|
||||
mr={2}
|
||||
{...getNeumorphicStyle(colorMode as 'light' | 'dark')}
|
||||
>
|
||||
<Text>Search...</Text>
|
||||
</Button>
|
||||
{isSearchOpen && <Box
|
||||
w="200px"
|
||||
transform={`translateX(${isSearchOpen ? "0" : "100%"})`}
|
||||
opacity={isSearchOpen ? 1 : 0}
|
||||
onKeyDown={(e) => {
|
||||
console.log(e);
|
||||
if(e.key === 'Escape') {
|
||||
setIsSearchOpen(false)
|
||||
}
|
||||
}}
|
||||
backdropFilter="blur(10px)"
|
||||
{...getNeumorphicStyle(colorMode as 'light' | 'dark', 'pressed')}
|
||||
>
|
||||
<Input
|
||||
placeholder="Search..."
|
||||
size="sm"
|
||||
value={searchInput}
|
||||
onChange={e => setSearchInput(e.target.value)}
|
||||
onKeyPress={async (e) => {
|
||||
console.log(e);
|
||||
if (e.key === 'Enter' && searchResults.length === 0 && searchInput.length > 2) {
|
||||
await handleSearchClick()
|
||||
<Search
|
||||
onClick={handleSearchClick}
|
||||
colorMode={colorMode}
|
||||
searchOpen={isSearchOpen}
|
||||
onKeyDown={(e) => {
|
||||
console.log(e);
|
||||
if (e.key === 'Escape') {
|
||||
setIsSearchOpen(false)
|
||||
}
|
||||
}}
|
||||
value={searchInput}
|
||||
onChange={e => setSearchInput(e.target.value)}
|
||||
onKeyPress={async (e) => {
|
||||
console.log(e);
|
||||
if (e.key === 'Enter' && searchResults.length === 0 && searchInput.length > 2) {
|
||||
await handleSearchClick()
|
||||
}
|
||||
}}
|
||||
searchResults={searchResults}
|
||||
callbackfn={(result, index) => {
|
||||
const colors = getNeumorphicColors(colorMode as 'light' | 'dark');
|
||||
return (
|
||||
<SearchResult key={index} onKeyPress={async (e) => {
|
||||
if (e.key === 'Enter' && searchResults.length > 0) {
|
||||
console.log(`Selecting result ${result.lat}, ${result.lon}`);
|
||||
await selectSearchResult(result);
|
||||
setSearchResults([]);
|
||||
setIsSearchOpen(false);
|
||||
}
|
||||
}}
|
||||
border="none"
|
||||
{...getNeumorphicStyle(colorMode as 'light' | 'dark', 'pressed')}
|
||||
/>
|
||||
{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');
|
||||
return (
|
||||
<Box
|
||||
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) {
|
||||
console.log(`Selecting result ${result.lat}, ${result.lon}`);
|
||||
await selectSearchResult(result);
|
||||
setSearchResults([]);
|
||||
setIsSearchOpen(false);
|
||||
}
|
||||
}}
|
||||
_hover={{
|
||||
bg: colors.accent + '20',
|
||||
transform: 'translateY(-1px)',
|
||||
}}
|
||||
onClick={async () => {
|
||||
console.log(`Selecting result ${result.lat}, ${result.lon}`);
|
||||
await selectSearchResult(result);
|
||||
setSearchResults([]);
|
||||
setIsSearchOpen(false);
|
||||
}}
|
||||
>
|
||||
{`${result.lat}, ${result.lon}`}
|
||||
</Box>
|
||||
);
|
||||
})}
|
||||
</Box>
|
||||
)}
|
||||
</Box>}
|
||||
</Box>
|
||||
<LayerSelector onClick={handleLayerChange} />
|
||||
colors={colors}
|
||||
onClick={async () => {
|
||||
console.log(`Selecting result ${result.lat}, ${result.lon}`);
|
||||
await selectSearchResult(result);
|
||||
setSearchResults([]);
|
||||
setIsSearchOpen(false);
|
||||
}}
|
||||
result={result}
|
||||
/>
|
||||
);
|
||||
}}/>
|
||||
<Button
|
||||
size="sm"
|
||||
variant="surface"
|
||||
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}/>
|
||||
</HStack>
|
||||
<MapNext mapboxPublicKey={atob(key)} vesselPosition={vesselPosition} layer={selectedLayer} mapView={mapView} geolocation={window.navigator.geolocation || custom_geolocation}/>
|
||||
{/*<Map*/}
|
||||
{/* mapboxAccessToken={atob(key)}*/}
|
||||
{/* initialViewState={mapView}*/}
|
||||
{/* onMove={handleMapViewChange}*/}
|
||||
{/* mapStyle="mapbox://styles/mapbox/dark-v11"*/}
|
||||
{/* reuseMaps*/}
|
||||
{/* attributionControl={false}*/}
|
||||
{/* style={{width: '100%', height: '100%'}} // let the wrapper dictate size*/}
|
||||
{/*>*/}
|
||||
{/* /!*{vesselPosition && (*!/*/}
|
||||
{/* /!* <Source id="vessel-data" type="geojson" data={vesselGeojson}>*!/*/}
|
||||
{/* /!* <Layer {...vesselLayerStyle} />*!/*/}
|
||||
{/* /!* </Source>*!/*/}
|
||||
{/* /!*)}*!/*/}
|
||||
{/*</Map>*/}
|
||||
<MapNext
|
||||
mapboxPublicKey={atob(key)}
|
||||
vesselPosition={vesselPosition}
|
||||
layer={selectedLayer}
|
||||
mapView={mapView}
|
||||
geolocation={custom_geolocation}
|
||||
aisVessels={aisEnabled ? vessels : []}
|
||||
onVesselClick={setVesselPopup}
|
||||
vesselPopup={vesselPopup}
|
||||
onVesselPopupClose={() => setVesselPopup(null)}
|
||||
/>
|
||||
</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 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 {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 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 (
|
||||
<Box>
|
||||
<Map
|
||||
@@ -100,6 +160,31 @@ export default function MapNext(props: any = {mapboxPublicKey: "", geolocation:
|
||||
<ScaleControl />
|
||||
|
||||
{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 && (
|
||||
<Popup
|
||||
|
@@ -88,9 +88,9 @@ export const useAISProvider = (boundingBox?: BoundingBox) => {
|
||||
|
||||
const wsRef = useRef<WebSocket | null>(null);
|
||||
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 connectionTimeoutRef = useRef<NodeJS.Timeout | null>(null);
|
||||
const connectionTimeoutRef = useRef<any | null>(null);
|
||||
const isConnectingRef = useRef<boolean>(false);
|
||||
const isMountedRef = useRef<boolean>(true);
|
||||
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 }
|
||||
## 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"] }
|
||||
anyhow = "1.0.98"
|
||||
|
||||
# Platform-specific tokio features
|
||||
[target.'cfg(not(target_arch = "wasm32"))'.dependencies]
|
||||
@@ -100,4 +101,4 @@ console_error_panic_hook = "0.1"
|
||||
|
||||
[build-dependencies]
|
||||
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 std::io::Cursor;
|
||||
use winit::window::Icon;
|
||||
use tokio::process::Command;
|
||||
|
||||
#[cfg(not(target_arch = "wasm32"))]
|
||||
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()
|
||||
.insert_resource(ClearColor(Color::NONE))
|
||||
.add_plugins(
|
||||
@@ -22,7 +32,6 @@ fn main() {
|
||||
.set(WindowPlugin {
|
||||
primary_window: Some(Window {
|
||||
// Bind to canvas included in `index.html`
|
||||
canvas: Some("#yachtpit-canvas".to_owned()),
|
||||
fit_canvas_to_parent: true,
|
||||
// Tells wasm not to override default event handling, like F5 and Ctrl+R
|
||||
prevent_default_event_handling: false,
|
||||
@@ -37,10 +46,14 @@ fn main() {
|
||||
)
|
||||
.add_plugins(GamePlugin)
|
||||
.add_systems(Startup, set_window_icon)
|
||||
.add_systems(Update, start_ais_server)
|
||||
.add_plugins(WebviewWryPlugin::default())
|
||||
.run();
|
||||
}
|
||||
|
||||
#[cfg(target_arch = "wasm32")]
|
||||
|
||||
#[cfg(target_arch = "wasm32")]
|
||||
fn launch_bevy() {
|
||||
{
|
||||
// Add console logging for WASM debugging
|
||||
console_error_panic_hook::set_once();
|
||||
@@ -73,9 +86,20 @@ fn main() {
|
||||
})
|
||||
.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
|
||||
fn set_window_icon(
|
||||
windows: NonSend<WinitWindows>,
|
||||
|
@@ -148,7 +148,7 @@ fn setup_menu(mut commands: Commands) {
|
||||
BackgroundColor(secondary_button_colors.normal),
|
||||
BorderColor(Color::linear_rgb(0.80, 0.82, 0.85)),
|
||||
BorderRadius::all(Val::Px(12.0)),
|
||||
secondary_button_colors,
|
||||
secondary_button_colors.clone(),
|
||||
OpenLink("https://bevyengine.org"),
|
||||
))
|
||||
.with_child((
|
||||
|
Reference in New Issue
Block a user