mirror of
https://github.com/seemueller-io/yachtpit.git
synced 2025-09-08 22:46:45 +00:00
AIS (Automatic identification system) Integration: Maritime (#12)
* WIP: Enable dynamic AIS stream handling based on user location and map focus. - Prevent AIS stream from starting immediately; start upon user interaction. - Add `ais_stream_started` state for WebSocket management. - Extend `useRealAISProvider` with `userLocationLoaded` and `mapFocused` to control stream. - Update frontend components to handle geolocation and map focus. - Exclude test files from compilation Introduce WebSocket integration for AIS services - Added WebSocket-based `useRealAISProvider` React hook for real-time AIS vessel data. - Created various tests including unit, integration, and browser tests to validate WebSocket functionality. - Added `ws` dependency to enable WebSocket communication. - Implemented vessel data mapping and bounding box handling for dynamic updates. * **Introduce Neumorphic UI design with new themes and styles** - Added NeumorphicTheme implementation for light and dark modes. - Refactored `LayerSelector` and `MapNext` components to use the neumorphic style and color utilities. - Updated `menu.rs` with neumorphic-inspired button and background styling. - Enhanced GPS feed and vessel popups with neumorphic visuals, improving clarity and aesthetics. - Temporarily disabled base-map dependency in `yachtpit` for isolation testing. * update names in layer selector * Update search button text to "Search..." for better clarity. * Add key event handlers for search and result selection in App.tsx * Implement AIS Test Map application with WebSocket-based vessel tracking and Mapbox integration. * Refactor AIS server to use Axum framework with shared stream manager and state handling. Fix metadata key mismatch in frontend vessel mapper. * Remove AIS provider integration and related vessel markers * Remove `ais-test-map` application, including dependencies, configuration, and source files. * ais data feed functional, bb query is overshot, performance degraded * Add AIS module as a build dependency --------- Co-authored-by: geoffsee <>
This commit is contained in:
358
Cargo.lock
generated
358
Cargo.lock
generated
@@ -118,6 +118,26 @@ dependencies = [
|
|||||||
"memchr",
|
"memchr",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "ais"
|
||||||
|
version = "0.1.0"
|
||||||
|
dependencies = [
|
||||||
|
"axum",
|
||||||
|
"axum-test",
|
||||||
|
"base64 0.22.1",
|
||||||
|
"futures-util",
|
||||||
|
"mockito",
|
||||||
|
"serde",
|
||||||
|
"serde_json",
|
||||||
|
"tokio",
|
||||||
|
"tokio-test",
|
||||||
|
"tokio-tungstenite 0.20.1",
|
||||||
|
"tokio-util",
|
||||||
|
"tower 0.4.13",
|
||||||
|
"tower-http 0.5.2",
|
||||||
|
"url",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "aligned-vec"
|
name = "aligned-vec"
|
||||||
version = "0.6.4"
|
version = "0.6.4"
|
||||||
@@ -291,6 +311,16 @@ dependencies = [
|
|||||||
"libloading",
|
"libloading",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "assert-json-diff"
|
||||||
|
version = "2.0.2"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "47e4f2b81832e72834d7518d8487a0396a28cc408186a2e8854c0f98011faf12"
|
||||||
|
dependencies = [
|
||||||
|
"serde",
|
||||||
|
"serde_json",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "assert_type_match"
|
name = "assert_type_match"
|
||||||
version = "0.1.1"
|
version = "0.1.1"
|
||||||
@@ -458,6 +488,28 @@ dependencies = [
|
|||||||
"windows-sys 0.59.0",
|
"windows-sys 0.59.0",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "async-stream"
|
||||||
|
version = "0.3.6"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "0b5a71a6f37880a80d1d7f19efd781e4b5de42c88f0722cc13bcb6cc2cfe8476"
|
||||||
|
dependencies = [
|
||||||
|
"async-stream-impl",
|
||||||
|
"futures-core",
|
||||||
|
"pin-project-lite",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "async-stream-impl"
|
||||||
|
version = "0.3.6"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "c7c24de15d275a1ecfd47a380fb4d5ec9bfe0933f309ed5e705b775596a3574d"
|
||||||
|
dependencies = [
|
||||||
|
"proc-macro2",
|
||||||
|
"quote",
|
||||||
|
"syn 2.0.104",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "async-task"
|
name = "async-task"
|
||||||
version = "4.7.1"
|
version = "4.7.1"
|
||||||
@@ -520,6 +572,12 @@ dependencies = [
|
|||||||
"portable-atomic-util",
|
"portable-atomic-util",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "auto-future"
|
||||||
|
version = "1.0.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "3c1e7e457ea78e524f48639f551fd79703ac3f2237f5ecccdf4708f8a75ad373"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "autocfg"
|
name = "autocfg"
|
||||||
version = "1.5.0"
|
version = "1.5.0"
|
||||||
@@ -558,9 +616,10 @@ dependencies = [
|
|||||||
"async-trait",
|
"async-trait",
|
||||||
"axum-core",
|
"axum-core",
|
||||||
"axum-macros",
|
"axum-macros",
|
||||||
|
"base64 0.22.1",
|
||||||
"bytes",
|
"bytes",
|
||||||
"futures-util",
|
"futures-util",
|
||||||
"http",
|
"http 1.3.1",
|
||||||
"http-body",
|
"http-body",
|
||||||
"http-body-util",
|
"http-body-util",
|
||||||
"hyper",
|
"hyper",
|
||||||
@@ -575,11 +634,15 @@ dependencies = [
|
|||||||
"serde",
|
"serde",
|
||||||
"serde_json",
|
"serde_json",
|
||||||
"serde_path_to_error",
|
"serde_path_to_error",
|
||||||
|
"serde_urlencoded",
|
||||||
|
"sha1",
|
||||||
"sync_wrapper",
|
"sync_wrapper",
|
||||||
"tokio",
|
"tokio",
|
||||||
"tower",
|
"tokio-tungstenite 0.24.0",
|
||||||
|
"tower 0.5.2",
|
||||||
"tower-layer",
|
"tower-layer",
|
||||||
"tower-service",
|
"tower-service",
|
||||||
|
"tracing",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
@@ -591,7 +654,7 @@ dependencies = [
|
|||||||
"async-trait",
|
"async-trait",
|
||||||
"bytes",
|
"bytes",
|
||||||
"futures-util",
|
"futures-util",
|
||||||
"http",
|
"http 1.3.1",
|
||||||
"http-body",
|
"http-body",
|
||||||
"http-body-util",
|
"http-body-util",
|
||||||
"mime",
|
"mime",
|
||||||
@@ -600,6 +663,7 @@ dependencies = [
|
|||||||
"sync_wrapper",
|
"sync_wrapper",
|
||||||
"tower-layer",
|
"tower-layer",
|
||||||
"tower-service",
|
"tower-service",
|
||||||
|
"tracing",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
@@ -610,7 +674,7 @@ checksum = "077959a7f8cf438676af90b483304528eb7e16eadadb7f44e9ada4f9dceb9e62"
|
|||||||
dependencies = [
|
dependencies = [
|
||||||
"axum-core",
|
"axum-core",
|
||||||
"chrono",
|
"chrono",
|
||||||
"http",
|
"http 1.3.1",
|
||||||
"mime_guess",
|
"mime_guess",
|
||||||
"rust-embed",
|
"rust-embed",
|
||||||
"tower-service",
|
"tower-service",
|
||||||
@@ -627,6 +691,35 @@ dependencies = [
|
|||||||
"syn 2.0.104",
|
"syn 2.0.104",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "axum-test"
|
||||||
|
version = "14.10.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "167294800740b4b6bc7bfbccbf3a1d50a6c6e097342580ec4c11d1672e456292"
|
||||||
|
dependencies = [
|
||||||
|
"anyhow",
|
||||||
|
"async-trait",
|
||||||
|
"auto-future",
|
||||||
|
"axum",
|
||||||
|
"bytes",
|
||||||
|
"cookie",
|
||||||
|
"http 1.3.1",
|
||||||
|
"http-body-util",
|
||||||
|
"hyper",
|
||||||
|
"hyper-util",
|
||||||
|
"mime",
|
||||||
|
"pretty_assertions",
|
||||||
|
"reserve-port",
|
||||||
|
"rust-multipart-rfc7578_2",
|
||||||
|
"serde",
|
||||||
|
"serde_json",
|
||||||
|
"serde_urlencoded",
|
||||||
|
"smallvec",
|
||||||
|
"tokio",
|
||||||
|
"tower 0.4.13",
|
||||||
|
"url",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "backtrace"
|
name = "backtrace"
|
||||||
version = "0.3.75"
|
version = "0.3.75"
|
||||||
@@ -654,7 +747,7 @@ dependencies = [
|
|||||||
"serde_json",
|
"serde_json",
|
||||||
"tao",
|
"tao",
|
||||||
"tokio",
|
"tokio",
|
||||||
"tower-http",
|
"tower-http 0.6.6",
|
||||||
"tracing",
|
"tracing",
|
||||||
"tracing-subscriber",
|
"tracing-subscriber",
|
||||||
"web-sys",
|
"web-sys",
|
||||||
@@ -2051,6 +2144,15 @@ version = "1.1.0"
|
|||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "3d7b894f5411737b7867f4827955924d7c254fc9f4d91a6aad6b097804b1018b"
|
checksum = "3d7b894f5411737b7867f4827955924d7c254fc9f4d91a6aad6b097804b1018b"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "colored"
|
||||||
|
version = "3.0.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "fde0e0ec90c9dfb3b4b1a0891a7dcd0e2bffde2f7efed5fe7c9bb00e5bfb915e"
|
||||||
|
dependencies = [
|
||||||
|
"windows-sys 0.59.0",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "combine"
|
name = "combine"
|
||||||
version = "4.6.7"
|
version = "4.6.7"
|
||||||
@@ -2519,6 +2621,12 @@ dependencies = [
|
|||||||
"unicode-xid",
|
"unicode-xid",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "diff"
|
||||||
|
version = "0.1.13"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "56254986775e3233ffa9c4d7d3faaf6d36a2c09d30b20687e9f88bc8bafc16c8"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "digest"
|
name = "digest"
|
||||||
version = "0.10.7"
|
version = "0.10.7"
|
||||||
@@ -3568,7 +3676,7 @@ dependencies = [
|
|||||||
"fnv",
|
"fnv",
|
||||||
"futures-core",
|
"futures-core",
|
||||||
"futures-sink",
|
"futures-sink",
|
||||||
"http",
|
"http 1.3.1",
|
||||||
"indexmap 2.10.0",
|
"indexmap 2.10.0",
|
||||||
"slab",
|
"slab",
|
||||||
"tokio",
|
"tokio",
|
||||||
@@ -3678,6 +3786,17 @@ dependencies = [
|
|||||||
"syn 1.0.109",
|
"syn 1.0.109",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "http"
|
||||||
|
version = "0.2.12"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "601cbb57e577e2f5ef5be8e7b83f0f63994f25aa94d673e54a92d5c516d101f1"
|
||||||
|
dependencies = [
|
||||||
|
"bytes",
|
||||||
|
"fnv",
|
||||||
|
"itoa 1.0.15",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "http"
|
name = "http"
|
||||||
version = "1.3.1"
|
version = "1.3.1"
|
||||||
@@ -3696,7 +3815,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
|
|||||||
checksum = "1efedce1fb8e6913f23e0c92de8e62cd5b772a67e7b3946df930a62566c93184"
|
checksum = "1efedce1fb8e6913f23e0c92de8e62cd5b772a67e7b3946df930a62566c93184"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"bytes",
|
"bytes",
|
||||||
"http",
|
"http 1.3.1",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
@@ -3707,7 +3826,7 @@ checksum = "b021d93e26becf5dc7e1b75b1bed1fd93124b374ceb73f43d4d4eafec896a64a"
|
|||||||
dependencies = [
|
dependencies = [
|
||||||
"bytes",
|
"bytes",
|
||||||
"futures-core",
|
"futures-core",
|
||||||
"http",
|
"http 1.3.1",
|
||||||
"http-body",
|
"http-body",
|
||||||
"pin-project-lite",
|
"pin-project-lite",
|
||||||
]
|
]
|
||||||
@@ -3740,7 +3859,7 @@ dependencies = [
|
|||||||
"futures-channel",
|
"futures-channel",
|
||||||
"futures-util",
|
"futures-util",
|
||||||
"h2",
|
"h2",
|
||||||
"http",
|
"http 1.3.1",
|
||||||
"http-body",
|
"http-body",
|
||||||
"httparse",
|
"httparse",
|
||||||
"httpdate",
|
"httpdate",
|
||||||
@@ -3757,7 +3876,7 @@ version = "0.27.7"
|
|||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "e3c93eb611681b207e1fe55d5a71ecf91572ec8a6705cdb6857f7d8d5242cf58"
|
checksum = "e3c93eb611681b207e1fe55d5a71ecf91572ec8a6705cdb6857f7d8d5242cf58"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"http",
|
"http 1.3.1",
|
||||||
"hyper",
|
"hyper",
|
||||||
"hyper-util",
|
"hyper-util",
|
||||||
"rustls",
|
"rustls",
|
||||||
@@ -3794,7 +3913,7 @@ dependencies = [
|
|||||||
"futures-channel",
|
"futures-channel",
|
||||||
"futures-core",
|
"futures-core",
|
||||||
"futures-util",
|
"futures-util",
|
||||||
"http",
|
"http 1.3.1",
|
||||||
"http-body",
|
"http-body",
|
||||||
"hyper",
|
"hyper",
|
||||||
"ipnet",
|
"ipnet",
|
||||||
@@ -4535,6 +4654,30 @@ dependencies = [
|
|||||||
"yachtpit",
|
"yachtpit",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "mockito"
|
||||||
|
version = "1.7.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "7760e0e418d9b7e5777c0374009ca4c93861b9066f18cb334a20ce50ab63aa48"
|
||||||
|
dependencies = [
|
||||||
|
"assert-json-diff",
|
||||||
|
"bytes",
|
||||||
|
"colored",
|
||||||
|
"futures-util",
|
||||||
|
"http 1.3.1",
|
||||||
|
"http-body",
|
||||||
|
"http-body-util",
|
||||||
|
"hyper",
|
||||||
|
"hyper-util",
|
||||||
|
"log",
|
||||||
|
"rand 0.9.1",
|
||||||
|
"regex",
|
||||||
|
"serde_json",
|
||||||
|
"serde_urlencoded",
|
||||||
|
"similar",
|
||||||
|
"tokio",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "naga"
|
name = "naga"
|
||||||
version = "24.0.0"
|
version = "24.0.0"
|
||||||
@@ -5700,6 +5843,16 @@ version = "0.3.1"
|
|||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "e8cf8e6a8aa66ce33f63993ffc4ea4271eb5b0530a9002db8455ea6050c77bfa"
|
checksum = "e8cf8e6a8aa66ce33f63993ffc4ea4271eb5b0530a9002db8455ea6050c77bfa"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "pretty_assertions"
|
||||||
|
version = "1.4.1"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "3ae130e2f271fbc2ac3a40fb1d07180839cdbbe443c7a27e1e3c13c5cac0116d"
|
||||||
|
dependencies = [
|
||||||
|
"diff",
|
||||||
|
"yansi",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "prettyplease"
|
name = "prettyplease"
|
||||||
version = "0.2.35"
|
version = "0.2.35"
|
||||||
@@ -6155,7 +6308,7 @@ dependencies = [
|
|||||||
"encoding_rs",
|
"encoding_rs",
|
||||||
"futures-core",
|
"futures-core",
|
||||||
"h2",
|
"h2",
|
||||||
"http",
|
"http 1.3.1",
|
||||||
"http-body",
|
"http-body",
|
||||||
"http-body-util",
|
"http-body-util",
|
||||||
"hyper",
|
"hyper",
|
||||||
@@ -6175,8 +6328,8 @@ dependencies = [
|
|||||||
"sync_wrapper",
|
"sync_wrapper",
|
||||||
"tokio",
|
"tokio",
|
||||||
"tokio-native-tls",
|
"tokio-native-tls",
|
||||||
"tower",
|
"tower 0.5.2",
|
||||||
"tower-http",
|
"tower-http 0.6.6",
|
||||||
"tower-service",
|
"tower-service",
|
||||||
"url",
|
"url",
|
||||||
"wasm-bindgen",
|
"wasm-bindgen",
|
||||||
@@ -6184,6 +6337,15 @@ dependencies = [
|
|||||||
"web-sys",
|
"web-sys",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "reserve-port"
|
||||||
|
version = "2.3.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "21918d6644020c6f6ef1993242989bf6d4952d2e025617744f184c02df51c356"
|
||||||
|
dependencies = [
|
||||||
|
"thiserror 2.0.12",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "rfd"
|
name = "rfd"
|
||||||
version = "0.15.4"
|
version = "0.15.4"
|
||||||
@@ -6289,6 +6451,22 @@ dependencies = [
|
|||||||
"walkdir",
|
"walkdir",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "rust-multipart-rfc7578_2"
|
||||||
|
version = "0.6.1"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "03b748410c0afdef2ebbe3685a6a862e2ee937127cdaae623336a459451c8d57"
|
||||||
|
dependencies = [
|
||||||
|
"bytes",
|
||||||
|
"futures-core",
|
||||||
|
"futures-util",
|
||||||
|
"http 0.2.12",
|
||||||
|
"mime",
|
||||||
|
"mime_guess",
|
||||||
|
"rand 0.8.5",
|
||||||
|
"thiserror 1.0.69",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "rustc-demangle"
|
name = "rustc-demangle"
|
||||||
version = "0.1.25"
|
version = "0.1.25"
|
||||||
@@ -6611,6 +6789,17 @@ dependencies = [
|
|||||||
"stable_deref_trait",
|
"stable_deref_trait",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "sha1"
|
||||||
|
version = "0.10.6"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "e3bf829a2d51ab4a5ddf1352d8470c140cadc8301b2ae1789db023f01cedd6ba"
|
||||||
|
dependencies = [
|
||||||
|
"cfg-if",
|
||||||
|
"cpufeatures",
|
||||||
|
"digest",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "sha2"
|
name = "sha2"
|
||||||
version = "0.10.9"
|
version = "0.10.9"
|
||||||
@@ -6661,6 +6850,12 @@ dependencies = [
|
|||||||
"quote",
|
"quote",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "similar"
|
||||||
|
version = "2.7.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "bbbb5d9659141646ae647b42fe094daf6c6192d1620870b449d9557f748b2daa"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "siphasher"
|
name = "siphasher"
|
||||||
version = "0.3.11"
|
version = "0.3.11"
|
||||||
@@ -7402,6 +7597,56 @@ dependencies = [
|
|||||||
"tokio",
|
"tokio",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "tokio-stream"
|
||||||
|
version = "0.1.17"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "eca58d7bba4a75707817a2c44174253f9236b2d5fbd055602e9d5c07c139a047"
|
||||||
|
dependencies = [
|
||||||
|
"futures-core",
|
||||||
|
"pin-project-lite",
|
||||||
|
"tokio",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "tokio-test"
|
||||||
|
version = "0.4.4"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "2468baabc3311435b55dd935f702f42cd1b8abb7e754fb7dfb16bd36aa88f9f7"
|
||||||
|
dependencies = [
|
||||||
|
"async-stream",
|
||||||
|
"bytes",
|
||||||
|
"futures-core",
|
||||||
|
"tokio",
|
||||||
|
"tokio-stream",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "tokio-tungstenite"
|
||||||
|
version = "0.20.1"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "212d5dcb2a1ce06d81107c3d0ffa3121fe974b73f068c8282cb1c32328113b6c"
|
||||||
|
dependencies = [
|
||||||
|
"futures-util",
|
||||||
|
"log",
|
||||||
|
"native-tls",
|
||||||
|
"tokio",
|
||||||
|
"tokio-native-tls",
|
||||||
|
"tungstenite 0.20.1",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "tokio-tungstenite"
|
||||||
|
version = "0.24.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "edc5f74e248dc973e0dbb7b74c7e0d6fcc301c694ff50049504004ef4d0cdcd9"
|
||||||
|
dependencies = [
|
||||||
|
"futures-util",
|
||||||
|
"log",
|
||||||
|
"tokio",
|
||||||
|
"tungstenite 0.24.0",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "tokio-util"
|
name = "tokio-util"
|
||||||
version = "0.7.15"
|
version = "0.7.15"
|
||||||
@@ -7480,6 +7725,22 @@ dependencies = [
|
|||||||
"winnow 0.7.12",
|
"winnow 0.7.12",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "tower"
|
||||||
|
version = "0.4.13"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "b8fa9be0de6cf49e536ce1851f987bd21a43b771b09473c3549a6c853db37c1c"
|
||||||
|
dependencies = [
|
||||||
|
"futures-core",
|
||||||
|
"futures-util",
|
||||||
|
"pin-project",
|
||||||
|
"pin-project-lite",
|
||||||
|
"tokio",
|
||||||
|
"tower-layer",
|
||||||
|
"tower-service",
|
||||||
|
"tracing",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "tower"
|
name = "tower"
|
||||||
version = "0.5.2"
|
version = "0.5.2"
|
||||||
@@ -7493,6 +7754,23 @@ dependencies = [
|
|||||||
"tokio",
|
"tokio",
|
||||||
"tower-layer",
|
"tower-layer",
|
||||||
"tower-service",
|
"tower-service",
|
||||||
|
"tracing",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "tower-http"
|
||||||
|
version = "0.5.2"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "1e9cd434a998747dd2c4276bc96ee2e0c7a2eadf3cae88e52be55a05fa9053f5"
|
||||||
|
dependencies = [
|
||||||
|
"bitflags 2.9.1",
|
||||||
|
"bytes",
|
||||||
|
"http 1.3.1",
|
||||||
|
"http-body",
|
||||||
|
"http-body-util",
|
||||||
|
"pin-project-lite",
|
||||||
|
"tower-layer",
|
||||||
|
"tower-service",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
@@ -7507,7 +7785,7 @@ dependencies = [
|
|||||||
"bytes",
|
"bytes",
|
||||||
"futures-core",
|
"futures-core",
|
||||||
"futures-util",
|
"futures-util",
|
||||||
"http",
|
"http 1.3.1",
|
||||||
"http-body",
|
"http-body",
|
||||||
"http-body-util",
|
"http-body-util",
|
||||||
"http-range-header",
|
"http-range-header",
|
||||||
@@ -7519,7 +7797,7 @@ dependencies = [
|
|||||||
"pin-project-lite",
|
"pin-project-lite",
|
||||||
"tokio",
|
"tokio",
|
||||||
"tokio-util",
|
"tokio-util",
|
||||||
"tower",
|
"tower 0.5.2",
|
||||||
"tower-layer",
|
"tower-layer",
|
||||||
"tower-service",
|
"tower-service",
|
||||||
"tracing",
|
"tracing",
|
||||||
@@ -7544,6 +7822,7 @@ version = "0.1.41"
|
|||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "784e0ac535deb450455cbfa28a6f0df145ea1bb7ae51b821cf5e7927fdcfbdd0"
|
checksum = "784e0ac535deb450455cbfa28a6f0df145ea1bb7ae51b821cf5e7927fdcfbdd0"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
|
"log",
|
||||||
"pin-project-lite",
|
"pin-project-lite",
|
||||||
"tracing-attributes",
|
"tracing-attributes",
|
||||||
"tracing-core",
|
"tracing-core",
|
||||||
@@ -7658,6 +7937,44 @@ version = "0.25.1"
|
|||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "d2df906b07856748fa3f6e0ad0cbaa047052d4a7dd609e231c4f72cee8c36f31"
|
checksum = "d2df906b07856748fa3f6e0ad0cbaa047052d4a7dd609e231c4f72cee8c36f31"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "tungstenite"
|
||||||
|
version = "0.20.1"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "9e3dac10fd62eaf6617d3a904ae222845979aec67c615d1c842b4002c7666fb9"
|
||||||
|
dependencies = [
|
||||||
|
"byteorder",
|
||||||
|
"bytes",
|
||||||
|
"data-encoding",
|
||||||
|
"http 0.2.12",
|
||||||
|
"httparse",
|
||||||
|
"log",
|
||||||
|
"native-tls",
|
||||||
|
"rand 0.8.5",
|
||||||
|
"sha1",
|
||||||
|
"thiserror 1.0.69",
|
||||||
|
"url",
|
||||||
|
"utf-8",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "tungstenite"
|
||||||
|
version = "0.24.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "18e5b8366ee7a95b16d32197d0b2604b43a0be89dc5fac9f8e96ccafbaedda8a"
|
||||||
|
dependencies = [
|
||||||
|
"byteorder",
|
||||||
|
"bytes",
|
||||||
|
"data-encoding",
|
||||||
|
"http 1.3.1",
|
||||||
|
"httparse",
|
||||||
|
"log",
|
||||||
|
"rand 0.8.5",
|
||||||
|
"sha1",
|
||||||
|
"thiserror 1.0.69",
|
||||||
|
"utf-8",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "typeid"
|
name = "typeid"
|
||||||
version = "1.0.3"
|
version = "1.0.3"
|
||||||
@@ -9003,7 +9320,7 @@ dependencies = [
|
|||||||
"gdkx11",
|
"gdkx11",
|
||||||
"gtk",
|
"gtk",
|
||||||
"html5ever",
|
"html5ever",
|
||||||
"http",
|
"http 1.3.1",
|
||||||
"javascriptcore-rs",
|
"javascriptcore-rs",
|
||||||
"jni",
|
"jni",
|
||||||
"kuchikiki",
|
"kuchikiki",
|
||||||
@@ -9109,6 +9426,7 @@ checksum = "6fd8403733700263c6eb89f192880191f1b83e332f7a20371ddcf421c4a337c7"
|
|||||||
name = "yachtpit"
|
name = "yachtpit"
|
||||||
version = "0.1.0"
|
version = "0.1.0"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
|
"anyhow",
|
||||||
"base-map",
|
"base-map",
|
||||||
"bevy",
|
"bevy",
|
||||||
"bevy_asset_loader",
|
"bevy_asset_loader",
|
||||||
@@ -9134,6 +9452,12 @@ dependencies = [
|
|||||||
"wry",
|
"wry",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "yansi"
|
||||||
|
version = "1.0.1"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "cfe53a6657fd280eaa890a3bc59152892ffa3e30101319d168b781ed6529b049"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "yazi"
|
name = "yazi"
|
||||||
version = "0.2.1"
|
version = "0.2.1"
|
||||||
|
@@ -1,5 +1,5 @@
|
|||||||
[workspace]
|
[workspace]
|
||||||
members = ["crates/yachtpit", "crates/yachtpit/mobile", "crates/systems", "crates/components", "crates/datalink", "crates/datalink-provider", "crates/base-map"]
|
members = ["crates/yachtpit", "crates/yachtpit/mobile", "crates/systems", "crates/components", "crates/datalink", "crates/datalink-provider", "crates/base-map", "crates/ais"]
|
||||||
resolver = "2"
|
resolver = "2"
|
||||||
|
|
||||||
default-members = [
|
default-members = [
|
||||||
|
1308
crates/ais/Cargo.lock
generated
Normal file
1308
crates/ais/Cargo.lock
generated
Normal file
File diff suppressed because it is too large
Load Diff
30
crates/ais/Cargo.toml
Normal file
30
crates/ais/Cargo.toml
Normal file
@@ -0,0 +1,30 @@
|
|||||||
|
[package]
|
||||||
|
name = "ais"
|
||||||
|
version = "0.1.0"
|
||||||
|
edition = "2021"
|
||||||
|
|
||||||
|
[[bin]]
|
||||||
|
name = "ais"
|
||||||
|
path = "src/main.rs"
|
||||||
|
|
||||||
|
[profile.dev]
|
||||||
|
debug = false
|
||||||
|
|
||||||
|
[dependencies]
|
||||||
|
tokio = { version = "1.0", features = ["full"] }
|
||||||
|
tokio-tungstenite = { version = "0.20", features = ["native-tls"] }
|
||||||
|
serde = { version = "1.0", features = ["derive"] }
|
||||||
|
serde_json = "1.0"
|
||||||
|
futures-util = "0.3"
|
||||||
|
url = "2.4"
|
||||||
|
axum = { version = "0.7", features = ["ws"] }
|
||||||
|
tower = "0.4"
|
||||||
|
tower-http = { version = "0.5", features = ["cors"] }
|
||||||
|
base64 = "0.22.1"
|
||||||
|
tokio-util = "0.7.15"
|
||||||
|
|
||||||
|
[dev-dependencies]
|
||||||
|
tokio-test = "0.4"
|
||||||
|
axum-test = "14.0"
|
||||||
|
mockito = "1.0"
|
||||||
|
serde_json = "1.0"
|
91
crates/ais/error.md
Normal file
91
crates/ais/error.md
Normal file
@@ -0,0 +1,91 @@
|
|||||||
|
/Users/williamseemueller/.cargo/bin/cargo run --color=always --package ais --bin ais --profile dev
|
||||||
|
warning: function `start_ais_stream_with_callbacks` is never used
|
||||||
|
--> src/ais.rs:78:8
|
||||||
|
|
|
||||||
|
78 | pub fn start_ais_stream_with_callbacks() {
|
||||||
|
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
|
||||||
|
|
|
||||||
|
= note: `#[warn(dead_code)]` on by default
|
||||||
|
|
||||||
|
warning: `ais` (bin "ais") generated 1 warning
|
||||||
|
Finished `dev` profile [unoptimized + debuginfo] target(s) in 0.01s
|
||||||
|
Running `target/debug/ais`
|
||||||
|
|
||||||
|
thread 'main' panicked at /Users/williamseemueller/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/js-sys-0.3.77/src/lib.rs:6063:9:
|
||||||
|
cannot access imported statics on non-wasm targets
|
||||||
|
stack backtrace:
|
||||||
|
0: __rustc::rust_begin_unwind
|
||||||
|
at /rustc/6b00bc3880198600130e1cf62b8f8a93494488cc/library/std/src/panicking.rs:697:5
|
||||||
|
1: core::panicking::panic_fmt
|
||||||
|
at /rustc/6b00bc3880198600130e1cf62b8f8a93494488cc/library/core/src/panicking.rs:75:14
|
||||||
|
2: js_sys::global::get_global_object::SELF::init::__wbg_static_accessor_SELF_37c5d418e4bf5819
|
||||||
|
at /Users/williamseemueller/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/js-sys-0.3.77/src/lib.rs:6063:9
|
||||||
|
3: js_sys::global::get_global_object::SELF::init
|
||||||
|
at /Users/williamseemueller/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/js-sys-0.3.77/src/lib.rs:6063:9
|
||||||
|
4: core::ops::function::FnOnce::call_once
|
||||||
|
at /Users/williamseemueller/.rustup/toolchains/stable-aarch64-apple-darwin/lib/rustlib/src/rust/library/core/src/ops/function.rs:250:5
|
||||||
|
5: once_cell::unsync::Lazy<T,F>::force::{{closure}}
|
||||||
|
at /Users/williamseemueller/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/once_cell-1.21.3/src/lib.rs:775:28
|
||||||
|
6: once_cell::unsync::OnceCell<T>::get_or_init::{{closure}}
|
||||||
|
at /Users/williamseemueller/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/once_cell-1.21.3/src/lib.rs:591:57
|
||||||
|
7: once_cell::unsync::OnceCell<T>::get_or_try_init
|
||||||
|
at /Users/williamseemueller/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/once_cell-1.21.3/src/lib.rs:629:23
|
||||||
|
8: once_cell::unsync::OnceCell<T>::get_or_init
|
||||||
|
at /Users/williamseemueller/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/once_cell-1.21.3/src/lib.rs:591:19
|
||||||
|
9: once_cell::unsync::Lazy<T,F>::force
|
||||||
|
at /Users/williamseemueller/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/once_cell-1.21.3/src/lib.rs:774:13
|
||||||
|
10: <wasm_bindgen::__rt::LazyCell<T> as core::ops::deref::Deref>::deref
|
||||||
|
at /Users/williamseemueller/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/wasm-bindgen-0.2.100/src/rt/mod.rs:56:9
|
||||||
|
11: wasm_bindgen::JsThreadLocal<T>::with
|
||||||
|
at /Users/williamseemueller/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/wasm-bindgen-0.2.100/src/lib.rs:1271:18
|
||||||
|
12: js_sys::global::get_global_object
|
||||||
|
at /Users/williamseemueller/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/js-sys-0.3.77/src/lib.rs:6082:29
|
||||||
|
13: core::ops::function::FnOnce::call_once
|
||||||
|
at /Users/williamseemueller/.rustup/toolchains/stable-aarch64-apple-darwin/lib/rustlib/src/rust/library/core/src/ops/function.rs:250:5
|
||||||
|
14: once_cell::unsync::Lazy<T,F>::force::{{closure}}
|
||||||
|
at /Users/williamseemueller/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/once_cell-1.21.3/src/lib.rs:775:28
|
||||||
|
15: once_cell::unsync::OnceCell<T>::get_or_init::{{closure}}
|
||||||
|
at /Users/williamseemueller/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/once_cell-1.21.3/src/lib.rs:591:57
|
||||||
|
16: once_cell::unsync::OnceCell<T>::get_or_try_init
|
||||||
|
at /Users/williamseemueller/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/once_cell-1.21.3/src/lib.rs:629:23
|
||||||
|
17: once_cell::unsync::OnceCell<T>::get_or_init
|
||||||
|
at /Users/williamseemueller/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/once_cell-1.21.3/src/lib.rs:591:19
|
||||||
|
18: once_cell::unsync::Lazy<T,F>::force
|
||||||
|
at /Users/williamseemueller/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/once_cell-1.21.3/src/lib.rs:774:13
|
||||||
|
19: <once_cell::unsync::Lazy<T,F> as core::ops::deref::Deref>::deref
|
||||||
|
at /Users/williamseemueller/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/once_cell-1.21.3/src/lib.rs:843:13
|
||||||
|
20: js_sys::global
|
||||||
|
at /Users/williamseemueller/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/js-sys-0.3.77/src/lib.rs:6051:12
|
||||||
|
21: wasm_bindgen_futures::queue::Queue::new
|
||||||
|
at /Users/williamseemueller/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/wasm-bindgen-futures-0.4.50/src/queue.rs:89:35
|
||||||
|
22: core::ops::function::FnOnce::call_once
|
||||||
|
at /Users/williamseemueller/.rustup/toolchains/stable-aarch64-apple-darwin/lib/rustlib/src/rust/library/core/src/ops/function.rs:250:5
|
||||||
|
23: once_cell::unsync::Lazy<T,F>::force::{{closure}}
|
||||||
|
at /Users/williamseemueller/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/once_cell-1.21.3/src/lib.rs:775:28
|
||||||
|
24: once_cell::unsync::OnceCell<T>::get_or_init::{{closure}}
|
||||||
|
at /Users/williamseemueller/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/once_cell-1.21.3/src/lib.rs:591:57
|
||||||
|
25: once_cell::unsync::OnceCell<T>::get_or_try_init
|
||||||
|
at /Users/williamseemueller/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/once_cell-1.21.3/src/lib.rs:629:23
|
||||||
|
26: once_cell::unsync::OnceCell<T>::get_or_init
|
||||||
|
at /Users/williamseemueller/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/once_cell-1.21.3/src/lib.rs:591:19
|
||||||
|
27: once_cell::unsync::Lazy<T,F>::force
|
||||||
|
at /Users/williamseemueller/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/once_cell-1.21.3/src/lib.rs:774:13
|
||||||
|
28: <once_cell::unsync::Lazy<T,F> as core::ops::deref::Deref>::deref
|
||||||
|
at /Users/williamseemueller/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/once_cell-1.21.3/src/lib.rs:843:13
|
||||||
|
29: wasm_bindgen_futures::queue::Queue::with
|
||||||
|
at /Users/williamseemueller/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/wasm-bindgen-futures-0.4.50/src/queue.rs:124:11
|
||||||
|
30: wasm_bindgen_futures::task::singlethread::Task::spawn
|
||||||
|
at /Users/williamseemueller/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/wasm-bindgen-futures-0.4.50/src/task/singlethread.rs:36:9
|
||||||
|
31: wasm_bindgen_futures::spawn_local
|
||||||
|
at /Users/williamseemueller/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/wasm-bindgen-futures-0.4.50/src/lib.rs:93:5
|
||||||
|
32: ais::ais::start_ais_stream
|
||||||
|
at ./src/ais.rs:22:5
|
||||||
|
33: ais::main
|
||||||
|
at ./src/main.rs:7:5
|
||||||
|
34: core::ops::function::FnOnce::call_once
|
||||||
|
at /Users/williamseemueller/.rustup/toolchains/stable-aarch64-apple-darwin/lib/rustlib/src/rust/library/core/src/ops/function.rs:250:5
|
||||||
|
note: Some details are omitted, run with `RUST_BACKTRACE=full` for a verbose backtrace.
|
||||||
|
|
||||||
|
Process finished with exit code 101
|
||||||
|
|
||||||
|
|
1007
crates/ais/src/ais.rs
Normal file
1007
crates/ais/src/ais.rs
Normal file
File diff suppressed because it is too large
Load Diff
36
crates/ais/src/main.rs
Normal file
36
crates/ais/src/main.rs
Normal file
@@ -0,0 +1,36 @@
|
|||||||
|
use std::sync::Arc;
|
||||||
|
use axum::Router;
|
||||||
|
use axum::routing::get;
|
||||||
|
use tower_http::cors::CorsLayer;
|
||||||
|
use crate::ais::{AisStreamManager, AppState};
|
||||||
|
|
||||||
|
mod ais;
|
||||||
|
|
||||||
|
#[tokio::main]
|
||||||
|
async fn main() -> Result<(), Box<dyn std::error::Error>> {
|
||||||
|
// Create the shared state with the AIS stream manager
|
||||||
|
let state = AppState {
|
||||||
|
ais_stream_manager: Arc::new(AisStreamManager::new()),
|
||||||
|
};
|
||||||
|
|
||||||
|
// Create and start the Axum HTTP server
|
||||||
|
let app = create_router(state);
|
||||||
|
let listener = tokio::net::TcpListener::bind("0.0.0.0:3000").await?;
|
||||||
|
|
||||||
|
println!("AIS server running on http://0.0.0.0:3000");
|
||||||
|
|
||||||
|
axum::serve(listener, app)
|
||||||
|
.with_graceful_shutdown(ais::shutdown_signal())
|
||||||
|
.await?;
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
// Create the Axum router
|
||||||
|
fn create_router(state: AppState) -> Router {
|
||||||
|
Router::new()
|
||||||
|
.route("/ais", get(crate::ais::get_ais_data))
|
||||||
|
.route("/ws", get(crate::ais::websocket_handler))
|
||||||
|
.layer(CorsLayer::permissive())
|
||||||
|
.with_state(state)
|
||||||
|
}
|
1356
crates/base-map/map/package-lock.json
generated
1356
crates/base-map/map/package-lock.json
generated
File diff suppressed because it is too large
Load Diff
@@ -8,35 +8,43 @@
|
|||||||
"build": "tsc -b && vite build",
|
"build": "tsc -b && vite build",
|
||||||
"lint": "eslint ",
|
"lint": "eslint ",
|
||||||
"preview": "vite preview",
|
"preview": "vite preview",
|
||||||
"background-server": "(cd ../ && cargo run &)"
|
"background-server": "(cd ../ && cargo run &)",
|
||||||
|
"test": "vitest",
|
||||||
|
"test:run": "vitest run"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"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",
|
||||||
"@emotion/react": "^11.14.0",
|
"@emotion/react": "^11.14.0",
|
||||||
"@eslint/js": "^9.29.0",
|
"@eslint/js": "^9.29.0",
|
||||||
|
"@tauri-apps/plugin-geolocation": "^2.3.0",
|
||||||
|
"@testing-library/jest-dom": "^6.6.3",
|
||||||
|
"@testing-library/react": "^16.3.0",
|
||||||
"@types/js-cookie": "^3.0.6",
|
"@types/js-cookie": "^3.0.6",
|
||||||
"@types/react": "^19.1.8",
|
"@types/react": "^19.1.8",
|
||||||
"@types/react-dom": "^19.1.6",
|
"@types/react-dom": "^19.1.6",
|
||||||
"@vitejs/plugin-react-swc": "^3.10.2",
|
"@vitejs/plugin-react-swc": "^3.10.2",
|
||||||
|
"bevy_flurx_api": "^0.1.0",
|
||||||
"eslint": "^9.29.0",
|
"eslint": "^9.29.0",
|
||||||
"eslint-plugin-react-hooks": "^5.2.0",
|
"eslint-plugin-react-hooks": "^5.2.0",
|
||||||
"eslint-plugin-react-refresh": "^0.4.20",
|
"eslint-plugin-react-refresh": "^0.4.20",
|
||||||
|
"geojson": "^0.5.0",
|
||||||
"globals": "^16.2.0",
|
"globals": "^16.2.0",
|
||||||
"js-cookie": "^3.0.5",
|
"js-cookie": "^3.0.5",
|
||||||
|
"jsdom": "^26.1.0",
|
||||||
"mapbox-gl": "^3.13.0",
|
"mapbox-gl": "^3.13.0",
|
||||||
"react-map-gl": "^8.0.4",
|
"react-map-gl": "^8.0.4",
|
||||||
"typescript": "~5.8.3",
|
"typescript": "~5.8.3",
|
||||||
"typescript-eslint": "^8.34.1",
|
"typescript-eslint": "^8.34.1",
|
||||||
"vite": "^7.0.0",
|
"vite": "^7.0.0",
|
||||||
"@tauri-apps/plugin-geolocation": "^2.3.0",
|
|
||||||
"vite-tsconfig-paths": "^5.1.4",
|
"vite-tsconfig-paths": "^5.1.4",
|
||||||
"bevy_flurx_api": "^0.1.0",
|
"vitest": "^3.2.4"
|
||||||
"geojson": "^0.5.0"
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@@ -1,124 +1,24 @@
|
|||||||
import 'mapbox-gl/dist/mapbox-gl.css';
|
import 'mapbox-gl/dist/mapbox-gl.css';
|
||||||
import {Box, Button, HStack, Input} from '@chakra-ui/react';
|
import {Box, Button, HStack, Text} from '@chakra-ui/react';
|
||||||
|
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 {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
|
// public key
|
||||||
const key =
|
const key =
|
||||||
'cGsuZXlKMUlqb2laMlZ2Wm1aelpXVWlMQ0poSWpvaVkycDFOalo0YkdWNk1EUTRjRE41YjJnNFp6VjNNelp6YXlKOS56LUtzS1l0X3VGUGdCSDYwQUFBNFNn';
|
'cGsuZXlKMUlqb2laMlZ2Wm1aelpXVWlMQ0poSWpvaVkycDFOalo0YkdWNk1EUTRjRE41YjJnNFp6VjNNelp6YXlKOS56LUtzS1l0X3VGUGdCSDYwQUFBNFNn';
|
||||||
|
|
||||||
const layers = [
|
|
||||||
{ name: 'OSM', value: 'mapbox://styles/mapbox/dark-v11' },
|
|
||||||
{ name: 'Satellite', value: 'mapbox://styles/mapbox/satellite-v9' },
|
|
||||||
];
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
// 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;
|
|
||||||
}
|
|
||||||
|
|
||||||
export type Layer = { name: string; value: string };
|
|
||||||
export type Layers = Layer[];
|
|
||||||
|
|
||||||
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.');
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
// interface MapViewParams {
|
|
||||||
// latitude: number;
|
|
||||||
// longitude: number;
|
|
||||||
// zoom: number;
|
|
||||||
// }
|
|
||||||
|
|
||||||
// interface AuthParams {
|
|
||||||
// authenticated: boolean;
|
|
||||||
// token: string | null;
|
|
||||||
// }
|
|
||||||
|
|
||||||
function LayerSelector(props: { onClick: (e: any) => Promise<void> }) {
|
|
||||||
const [isOpen, setIsOpen] = useState(false);
|
|
||||||
|
|
||||||
return (
|
|
||||||
<Box position="relative">
|
|
||||||
<Button colorScheme="blue" size="sm" variant="solid" onClick={() => setIsOpen(!isOpen)}>
|
|
||||||
Layer
|
|
||||||
</Button>
|
|
||||||
|
|
||||||
{isOpen && (
|
|
||||||
<Box
|
|
||||||
position="absolute"
|
|
||||||
top="100%"
|
|
||||||
left={0}
|
|
||||||
w="200px"
|
|
||||||
bg="rgba(0, 0, 0, 0.8)"
|
|
||||||
boxShadow="md"
|
|
||||||
zIndex={2}
|
|
||||||
>
|
|
||||||
{layers.map(layer => (
|
|
||||||
<Box
|
|
||||||
key={layer.value}
|
|
||||||
id={layer.value}
|
|
||||||
p={2}
|
|
||||||
cursor="pointer"
|
|
||||||
color="white"
|
|
||||||
_hover={{ bg: 'whiteAlpha.200' }}
|
|
||||||
onClick={async e => {
|
|
||||||
setIsOpen(false);
|
|
||||||
await props.onClick(e);
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
{layer.name}
|
|
||||||
</Box>
|
|
||||||
))}
|
|
||||||
</Box>
|
|
||||||
)}
|
|
||||||
</Box>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
function App() {
|
function App() {
|
||||||
|
const {colorMode} = useColorMode();
|
||||||
const [isSearchOpen, setIsSearchOpen] = useState(false);
|
const [isSearchOpen, setIsSearchOpen] = useState(false);
|
||||||
const [selectedLayer, setSelectedLayer] = useState(layers[0]);
|
const [selectedLayer, setSelectedLayer] = useState(layers[0]);
|
||||||
const [searchInput, setSearchInput] = useState('');
|
const [searchInput, setSearchInput] = useState('');
|
||||||
@@ -129,261 +29,7 @@ function App() {
|
|||||||
zoom: 14
|
zoom: 14
|
||||||
});
|
});
|
||||||
|
|
||||||
// Map state that can be updated from Rust
|
const custom_geolocation = new NativeGeolocation({
|
||||||
// const [mapView, setMapView] = useState({
|
|
||||||
// longitude: -122.4,
|
|
||||||
// latitude: 37.8,
|
|
||||||
// zoom: 14
|
|
||||||
// });
|
|
||||||
|
|
||||||
// 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
|
|
||||||
// }
|
|
||||||
// }
|
|
||||||
// ] : []
|
|
||||||
// };
|
|
||||||
|
|
||||||
|
|
||||||
// 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);
|
|
||||||
// }
|
|
||||||
// }
|
|
||||||
// }, []);
|
|
||||||
|
|
||||||
|
|
||||||
const selectSearchResult = useCallback(async (searchResult: { lat: string, lon: string }) => {
|
|
||||||
// Navigate to the selected location with zoom
|
|
||||||
console.log(`Navigating to: ${searchResult.lat}, ${searchResult.lon}`);
|
|
||||||
setMapView({
|
|
||||||
longitude: parseFloat(searchResult.lon),
|
|
||||||
latitude: parseFloat(searchResult.lat),
|
|
||||||
zoom: 15
|
|
||||||
});
|
|
||||||
}, []);
|
|
||||||
|
|
||||||
const handleSearchClick = useCallback(async () => {
|
|
||||||
if (isSearchOpen && searchInput.length > 1) {
|
|
||||||
try {
|
|
||||||
console.log(`Trying to geocode: ${searchInput}`);
|
|
||||||
const geocode = await fetch('https://geocode.geoffsee.com', {
|
|
||||||
method: 'POST',
|
|
||||||
mode: 'cors',
|
|
||||||
body: JSON.stringify({
|
|
||||||
location: searchInput,
|
|
||||||
}),
|
|
||||||
});
|
|
||||||
const coordinates = await geocode.json();
|
|
||||||
const { lat, lon } = coordinates;
|
|
||||||
console.log(`Got geocode coordinates: ${lat}, ${lon}`);
|
|
||||||
setSearchResults([{ lat, lon }]);
|
|
||||||
} catch (e) {
|
|
||||||
console.error('Geocoding failed:', e);
|
|
||||||
// Continue without results
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
setIsSearchOpen(!isSearchOpen);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (typeof window !== 'undefined' && (window as any).__FLURX__) {
|
|
||||||
try {
|
|
||||||
await (window as any).__FLURX__.invoke("search_clicked");
|
|
||||||
console.log('Search clicked');
|
|
||||||
} catch (error) {
|
|
||||||
console.error('Failed to invoke search_clicked:', error);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}, [isSearchOpen, searchInput]);
|
|
||||||
|
|
||||||
const handleLayerChange = useCallback(async (e: any) => {
|
|
||||||
const newLayer = layers.find(layer => layer.value === e.target.id);
|
|
||||||
if (newLayer) {
|
|
||||||
setSelectedLayer(newLayer);
|
|
||||||
console.log('Layer changed to:', newLayer.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 () => {
|
|
||||||
if (typeof window !== 'undefined' && (window as any).__FLURX__) {
|
|
||||||
try {
|
|
||||||
const vesselStatus: VesselStatus = await (window as any).__FLURX__.invoke("get_vessel_status");
|
|
||||||
console.log('Vessel status:', vesselStatus);
|
|
||||||
setVesselPosition(vesselStatus);
|
|
||||||
} catch (error) {
|
|
||||||
console.error('Failed to get vessel status:', error);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
// Poll every 5 seconds
|
|
||||||
const interval = setInterval(pollVesselStatus, 5000);
|
|
||||||
// Also poll immediately
|
|
||||||
pollVesselStatus();
|
|
||||||
return () => clearInterval(interval);
|
|
||||||
}, []);
|
|
||||||
|
|
||||||
// Initialize map with data from Rust
|
|
||||||
useEffect(() => {
|
|
||||||
const initializeMap = async () => {
|
|
||||||
if (typeof window !== 'undefined' && (window as any).__FLURX__) {
|
|
||||||
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);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
initializeMap();
|
|
||||||
}, []);
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
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 bottom-left */}
|
|
||||||
{vesselPosition && (
|
|
||||||
<Box
|
|
||||||
position="absolute"
|
|
||||||
top={65}
|
|
||||||
right={4}
|
|
||||||
zIndex={1}
|
|
||||||
bg="rgba(0, 0, 0, 0.8)"
|
|
||||||
color="white"
|
|
||||||
p={3}
|
|
||||||
borderRadius="md"
|
|
||||||
fontSize="sm"
|
|
||||||
fontFamily="monospace"
|
|
||||||
minW="200px"
|
|
||||||
>
|
|
||||||
<Box fontWeight="bold" mb={2}>GPS Feed</Box>
|
|
||||||
<Box>Lat: {vesselPosition.latitude.toFixed(6)}°</Box>
|
|
||||||
<Box>Lon: {vesselPosition.longitude.toFixed(6)}°</Box>
|
|
||||||
<Box>Heading: {vesselPosition.heading.toFixed(1)}°</Box>
|
|
||||||
<Box>Speed: {vesselPosition.speed.toFixed(1)} kts</Box>
|
|
||||||
</Box>
|
|
||||||
)}
|
|
||||||
{/* Button bar — absolutely positioned inside the wrapper */}
|
|
||||||
<HStack position="absolute" top={4} right={4} zIndex={1}>
|
|
||||||
<Box
|
|
||||||
display="flex"
|
|
||||||
alignItems="center"
|
|
||||||
>
|
|
||||||
<Button
|
|
||||||
colorScheme="teal"
|
|
||||||
size="sm"
|
|
||||||
variant="solid"
|
|
||||||
onClick={handleSearchClick}
|
|
||||||
mr={2}
|
|
||||||
>
|
|
||||||
Search
|
|
||||||
</Button>
|
|
||||||
{isSearchOpen && <Box
|
|
||||||
w="200px"
|
|
||||||
transition="all 0.3s"
|
|
||||||
transform={`translateX(${isSearchOpen ? "0" : "100%"})`}
|
|
||||||
background="rgba(0, 0, 0, 0.8)"
|
|
||||||
opacity={isSearchOpen ? 1 : 0}
|
|
||||||
color="white"
|
|
||||||
>
|
|
||||||
<Input
|
|
||||||
placeholder="Search..."
|
|
||||||
size="sm"
|
|
||||||
value={searchInput}
|
|
||||||
onChange={e => setSearchInput(e.target.value)}
|
|
||||||
color="white"
|
|
||||||
bg="rgba(0, 0, 0, 0.8)"
|
|
||||||
border="none"
|
|
||||||
borderRadius="0"
|
|
||||||
_focus={{
|
|
||||||
outline: 'none',
|
|
||||||
}}
|
|
||||||
_placeholder={{
|
|
||||||
color: "#d1cfcf"
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
{searchResults.length > 0 && (
|
|
||||||
<Box
|
|
||||||
position="absolute"
|
|
||||||
top="100%"
|
|
||||||
left={0}
|
|
||||||
w="200px"
|
|
||||||
bg="rgba(0, 0, 0, 0.8)"
|
|
||||||
boxShadow="md"
|
|
||||||
zIndex={2}
|
|
||||||
>
|
|
||||||
{searchResults.map((result, index) => (
|
|
||||||
<Box
|
|
||||||
key={index}
|
|
||||||
p={2}
|
|
||||||
cursor="pointer"
|
|
||||||
color="white"
|
|
||||||
_hover={{ bg: 'whiteAlpha.200' }}
|
|
||||||
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} />
|
|
||||||
</HStack>
|
|
||||||
<MapNext mapboxPublicKey={atob(key)} vesselPosition={vesselPosition} layer={selectedLayer} mapView={mapView} 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);
|
||||||
@@ -529,22 +175,215 @@ function App() {
|
|||||||
errorCallback(positionError);
|
errorCallback(positionError);
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
})}/>
|
});
|
||||||
{/*<Map*/}
|
|
||||||
{/* mapboxAccessToken={atob(key)}*/}
|
// Vessel position state
|
||||||
{/* initialViewState={mapView}*/}
|
const [vesselPosition, setVesselPosition] = useState<VesselStatus | null>(null);
|
||||||
{/* onMove={handleMapViewChange}*/}
|
|
||||||
{/* mapStyle="mapbox://styles/mapbox/dark-v11"*/}
|
// AIS state management
|
||||||
{/* reuseMaps*/}
|
const [aisEnabled, setAisEnabled] = useState(false);
|
||||||
{/* attributionControl={false}*/}
|
const [boundingBox, _setBoundingBox] = useState<{
|
||||||
{/* style={{width: '100%', height: '100%'}} // let the wrapper dictate size*/}
|
sw_lat: number;
|
||||||
{/*>*/}
|
sw_lon: number;
|
||||||
{/* /!*{vesselPosition && (*!/*/}
|
ne_lat: number;
|
||||||
{/* /!* <Source id="vessel-data" type="geojson" data={vesselGeojson}>*!/*/}
|
ne_lon: number;
|
||||||
{/* /!* <Layer {...vesselLayerStyle} />*!/*/}
|
} | undefined>(undefined);
|
||||||
{/* /!* </Source>*!/*/}
|
const [vesselPopup, setVesselPopup] = useState<VesselData | null>(null);
|
||||||
{/* /!*)}*!/*/}
|
|
||||||
{/*</Map>*/}
|
// 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 }) => {
|
||||||
|
// Navigate to the selected location with zoom
|
||||||
|
console.log(`Navigating to: ${searchResult.lat}, ${searchResult.lon}`);
|
||||||
|
setMapView({
|
||||||
|
longitude: parseFloat(searchResult.lon),
|
||||||
|
latitude: parseFloat(searchResult.lat),
|
||||||
|
zoom: 15
|
||||||
|
});
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
const handleSearchClick = useCallback(async () => {
|
||||||
|
console.log("calling hsc")
|
||||||
|
if (isSearchOpen && searchInput.length > 1) {
|
||||||
|
try {
|
||||||
|
console.log(`Trying to geocode: ${searchInput}`);
|
||||||
|
const geocode = await fetch('https://geocode.geoffsee.com', {
|
||||||
|
method: 'POST',
|
||||||
|
mode: 'cors',
|
||||||
|
body: JSON.stringify({
|
||||||
|
location: searchInput,
|
||||||
|
}),
|
||||||
|
});
|
||||||
|
const coordinates = await geocode.json();
|
||||||
|
const {lat, lon} = coordinates;
|
||||||
|
console.log(`Got geocode coordinates: ${lat}, ${lon}`);
|
||||||
|
setSearchResults([{lat, lon}]);
|
||||||
|
} catch (e) {
|
||||||
|
console.error('Geocoding failed:', e);
|
||||||
|
// Continue without results
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
setIsSearchOpen(!isSearchOpen);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (typeof window !== 'undefined' && (window as any).__FLURX__) {
|
||||||
|
try {
|
||||||
|
await (window as any).__FLURX__.invoke("search_clicked");
|
||||||
|
console.log('Search clicked');
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Failed to invoke search_clicked:', error);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}, [isSearchOpen, searchInput]);
|
||||||
|
|
||||||
|
const handleLayerChange = useCallback(async (layer: any) => {
|
||||||
|
console.log('Layer change requested:', layer);
|
||||||
|
setSelectedLayer(layer);
|
||||||
|
console.log('Layer changed to:', layer.name);
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
// Poll for vessel status updates
|
||||||
|
useEffect(() => {
|
||||||
|
const pollVesselStatus = async () => {
|
||||||
|
if (typeof window !== 'undefined' && (window as any).__FLURX__) {
|
||||||
|
try {
|
||||||
|
const vesselStatus: VesselStatus = await (window as any).__FLURX__.invoke("get_vessel_status");
|
||||||
|
console.log('Vessel status:', vesselStatus);
|
||||||
|
setVesselPosition(vesselStatus);
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Failed to get vessel status:', error);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// Poll every 5 seconds
|
||||||
|
const interval = setInterval(pollVesselStatus, 5000);
|
||||||
|
// Also poll immediately
|
||||||
|
pollVesselStatus();
|
||||||
|
return () => clearInterval(interval);
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
// Initialize map with data from Rust
|
||||||
|
useEffect(() => {
|
||||||
|
const initializeMap = async () => {
|
||||||
|
if (typeof window !== 'undefined' && (window as any).__FLURX__) {
|
||||||
|
try {
|
||||||
|
const mapInit: GpsPosition = await (window as any).__FLURX__.invoke("get_map_init");
|
||||||
|
console.log('Map initialization data:', mapInit);
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Failed to get map initialization data:', error);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
initializeMap();
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
|
||||||
|
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 && (
|
||||||
|
<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}>
|
||||||
|
<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);
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
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={custom_geolocation}
|
||||||
|
aisVessels={aisEnabled ? vessels : []}
|
||||||
|
onVesselClick={setVesselPopup}
|
||||||
|
vesselPopup={vesselPopup}
|
||||||
|
onVesselPopupClose={() => setVesselPopup(null)}
|
||||||
|
/>
|
||||||
</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.');
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
91
crates/base-map/map/src/LayerSelector.tsx
Normal file
91
crates/base-map/map/src/LayerSelector.tsx
Normal file
@@ -0,0 +1,91 @@
|
|||||||
|
import 'mapbox-gl/dist/mapbox-gl.css';
|
||||||
|
import {Button, Menu, Portal} from '@chakra-ui/react';
|
||||||
|
import {useColorMode} from './components/ui/color-mode';
|
||||||
|
import {useState} from "react";
|
||||||
|
import {getNeumorphicColors, getNeumorphicStyle} from './theme/neumorphic-theme';
|
||||||
|
|
||||||
|
export const layers = [
|
||||||
|
{ name: 'Standard', value: 'mapbox://styles/mapbox/dark-v11' },
|
||||||
|
{ name: 'Satellite', value: 'mapbox://styles/mapbox/satellite-v9' },
|
||||||
|
];
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
// const vesselLayerStyle: CircleLayerSpecification = {
|
||||||
|
// id: 'vessel',
|
||||||
|
// type: 'circle',
|
||||||
|
// paint: {
|
||||||
|
// 'circle-radius': 8,
|
||||||
|
// 'circle-color': '#ff4444',
|
||||||
|
// 'circle-stroke-width': 2,
|
||||||
|
// 'circle-stroke-color': '#ffffff'
|
||||||
|
// },
|
||||||
|
// source: ''
|
||||||
|
// };
|
||||||
|
|
||||||
|
|
||||||
|
export type Layer = { name: string; value: string };
|
||||||
|
export type Layers = Layer[];
|
||||||
|
|
||||||
|
// interface MapViewParams {
|
||||||
|
// latitude: number;
|
||||||
|
// longitude: number;
|
||||||
|
// zoom: number;
|
||||||
|
// }
|
||||||
|
|
||||||
|
// interface AuthParams {
|
||||||
|
// authenticated: boolean;
|
||||||
|
// token: string | null;
|
||||||
|
// }
|
||||||
|
|
||||||
|
export function LayerSelector(props: { onClick: (layer: Layer) => Promise<void> }) {
|
||||||
|
const { colorMode } = useColorMode();
|
||||||
|
const [selectedLayer, setSelectedLayer] = useState(layers[0]);
|
||||||
|
const neumorphicStyle = getNeumorphicStyle(colorMode as 'light' | 'dark');
|
||||||
|
const colors = getNeumorphicColors(colorMode as 'light' | 'dark');
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Menu.Root>
|
||||||
|
<Menu.Trigger asChild>
|
||||||
|
<Button
|
||||||
|
size="sm"
|
||||||
|
variant="surface"
|
||||||
|
{...neumorphicStyle}
|
||||||
|
>
|
||||||
|
{selectedLayer?.name || 'Layer'}
|
||||||
|
</Button>
|
||||||
|
</Menu.Trigger>
|
||||||
|
<Portal>
|
||||||
|
<Menu.Positioner>
|
||||||
|
<Menu.Content
|
||||||
|
minW="200px"
|
||||||
|
py={2}
|
||||||
|
{...neumorphicStyle}
|
||||||
|
>
|
||||||
|
{layers.map(layer => (
|
||||||
|
<Menu.Item
|
||||||
|
key={layer.value}
|
||||||
|
id={layer.value}
|
||||||
|
value={layer.value}
|
||||||
|
borderRadius={6}
|
||||||
|
transition="all 0.2s ease-in-out"
|
||||||
|
_hover={{
|
||||||
|
bg: colors.accent + '20',
|
||||||
|
transform: 'translateY(-1px)',
|
||||||
|
}}
|
||||||
|
onClick={(e) => {
|
||||||
|
// @ts-ignore
|
||||||
|
console.log(e.target.id)
|
||||||
|
setSelectedLayer(layer);
|
||||||
|
props.onClick(layer);
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{layer.name}
|
||||||
|
</Menu.Item>
|
||||||
|
))}
|
||||||
|
</Menu.Content>
|
||||||
|
</Menu.Positioner>
|
||||||
|
</Portal>
|
||||||
|
</Menu.Root>
|
||||||
|
);
|
||||||
|
}
|
@@ -1,15 +1,18 @@
|
|||||||
import {useState, useMemo, useEffect} from 'react';
|
import {useState, useMemo, useCallback, useRef} from 'react';
|
||||||
import Map, {
|
import Map, {
|
||||||
Marker,
|
Marker,
|
||||||
Popup,
|
Popup,
|
||||||
NavigationControl,
|
NavigationControl,
|
||||||
FullscreenControl,
|
FullscreenControl,
|
||||||
ScaleControl,
|
ScaleControl,
|
||||||
GeolocateControl
|
GeolocateControl,
|
||||||
|
type MapRef
|
||||||
} from 'react-map-gl/mapbox';
|
} from 'react-map-gl/mapbox';
|
||||||
|
|
||||||
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";
|
||||||
@@ -26,8 +29,34 @@ 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);
|
||||||
|
|
||||||
|
// Handle user location events
|
||||||
|
const handleGeolocate = useCallback((position: GeolocationPosition) => {
|
||||||
|
console.log('User location loaded:', position);
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
const handleTrackUserLocationStart = useCallback(() => {
|
||||||
|
console.log('Started tracking user location');
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
const handleTrackUserLocationEnd = useCallback(() => {
|
||||||
|
console.log('Stopped tracking user location');
|
||||||
|
}, []);
|
||||||
|
|
||||||
const pins = useMemo(
|
const pins = useMemo(
|
||||||
() =>
|
() =>
|
||||||
@@ -55,15 +84,56 @@ 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';
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
useEffect(() => {
|
// Create vessel markers
|
||||||
console.log("props.vesselPosition", props?.vesselPosition);
|
const vesselMarkers = useMemo(() =>
|
||||||
// setLocationLock(props.vesselPosition)
|
(props.aisVessels || []).map((vessel) => (
|
||||||
}, [props.vesselPosition]);
|
<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
|
||||||
|
ref={mapRef}
|
||||||
initialViewState={{
|
initialViewState={{
|
||||||
latitude: props.mapView?.latitude || 40,
|
latitude: props.mapView?.latitude || 40,
|
||||||
longitude: props.mapView?.longitude || -100,
|
longitude: props.mapView?.longitude || -100,
|
||||||
@@ -72,17 +142,49 @@ export default function MapNext(props: any = {mapboxPublicKey: "", geolocation:
|
|||||||
pitch: 0
|
pitch: 0
|
||||||
}}
|
}}
|
||||||
key={`${props.mapView?.latitude}-${props.mapView?.longitude}-${props.mapView?.zoom}`}
|
key={`${props.mapView?.latitude}-${props.mapView?.longitude}-${props.mapView?.zoom}`}
|
||||||
|
|
||||||
mapStyle={props.layer?.value || "mapbox://styles/mapbox/standard"}
|
mapStyle={props.layer?.value || "mapbox://styles/mapbox/standard"}
|
||||||
mapboxAccessToken={props.mapboxPublicKey}
|
mapboxAccessToken={props.mapboxPublicKey}
|
||||||
style={{position: "fixed", width: '100%', height: '100%', bottom: 0, top: 0, left: 0, right: 0}}
|
style={{position: "fixed", width: '100%', height: '100%', bottom: 0, top: 0, left: 0, right: 0}}
|
||||||
>
|
>
|
||||||
<GeolocateControl showUserHeading={true} showUserLocation={true} geolocation={props.geolocation} position="top-left" />
|
<GeolocateControl
|
||||||
|
showUserHeading={true}
|
||||||
|
showUserLocation={true}
|
||||||
|
geolocation={props.geolocation}
|
||||||
|
position="top-left"
|
||||||
|
onGeolocate={handleGeolocate}
|
||||||
|
onTrackUserLocationStart={handleTrackUserLocationStart}
|
||||||
|
onTrackUserLocationEnd={handleTrackUserLocationEnd}
|
||||||
|
/>
|
||||||
<FullscreenControl position="top-left" />
|
<FullscreenControl position="top-left" />
|
||||||
<NavigationControl position="top-left" />
|
<NavigationControl position="top-left" />
|
||||||
<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
|
||||||
|
404
crates/base-map/map/src/ais-provider.tsx
Normal file
404
crates/base-map/map/src/ais-provider.tsx
Normal file
@@ -0,0 +1,404 @@
|
|||||||
|
import { useState, useEffect, useCallback, useRef } from 'react';
|
||||||
|
|
||||||
|
// Vessel data interface
|
||||||
|
export interface VesselData {
|
||||||
|
id: string;
|
||||||
|
name: string;
|
||||||
|
type: string;
|
||||||
|
latitude: number;
|
||||||
|
longitude: number;
|
||||||
|
heading: number; // degrees 0-359
|
||||||
|
speed: number; // knots
|
||||||
|
length: number; // meters
|
||||||
|
width: number; // meters
|
||||||
|
mmsi: string; // Maritime Mobile Service Identity
|
||||||
|
callSign: string;
|
||||||
|
destination?: string;
|
||||||
|
eta?: string;
|
||||||
|
lastUpdate: Date;
|
||||||
|
}
|
||||||
|
|
||||||
|
// AIS service response structure (matching Rust AisResponse)
|
||||||
|
interface AisResponse {
|
||||||
|
message_type?: string;
|
||||||
|
mmsi?: string;
|
||||||
|
ship_name?: string;
|
||||||
|
latitude?: number;
|
||||||
|
longitude?: number;
|
||||||
|
timestamp?: string;
|
||||||
|
speed_over_ground?: number;
|
||||||
|
course_over_ground?: number;
|
||||||
|
heading?: number;
|
||||||
|
navigation_status?: string;
|
||||||
|
ship_type?: string;
|
||||||
|
raw_message: any;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Bounding box for AIS queries
|
||||||
|
interface BoundingBox {
|
||||||
|
sw_lat: number;
|
||||||
|
sw_lon: number;
|
||||||
|
ne_lat: number;
|
||||||
|
ne_lon: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
// WebSocket message types for communication with the backend
|
||||||
|
interface WebSocketMessage {
|
||||||
|
type: string;
|
||||||
|
bounding_box?: BoundingBox;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Convert AIS service response to VesselData format
|
||||||
|
const convertAisResponseToVesselData = (aisResponse: AisResponse): VesselData | null => {
|
||||||
|
if ((!aisResponse.raw_message?.MetaData?.MMSI) || !aisResponse.latitude || !aisResponse.longitude) {
|
||||||
|
console.log('Skipping vessel with missing data:', {
|
||||||
|
mmsi: aisResponse.mmsi,
|
||||||
|
metadataMSSI: aisResponse.raw_message?.MetaData?.MSSI,
|
||||||
|
latitude: aisResponse.latitude,
|
||||||
|
longitude: aisResponse.longitude,
|
||||||
|
raw: aisResponse.raw_message
|
||||||
|
});
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
id: aisResponse.mmsi ?? aisResponse.raw_message?.MetaData?.MMSI,
|
||||||
|
name: aisResponse.ship_name || `Vessel ${aisResponse.mmsi}`,
|
||||||
|
type: aisResponse.ship_type || 'Unknown',
|
||||||
|
latitude: aisResponse.latitude,
|
||||||
|
longitude: aisResponse.longitude,
|
||||||
|
heading: aisResponse.heading || 0,
|
||||||
|
speed: aisResponse.speed_over_ground || 0,
|
||||||
|
length: 100, // Default length
|
||||||
|
width: 20, // Default width
|
||||||
|
mmsi: aisResponse.mmsi ?? aisResponse.raw_message?.MetaData?.MMSI,
|
||||||
|
callSign: '',
|
||||||
|
destination: '',
|
||||||
|
eta: '',
|
||||||
|
lastUpdate: new Date()
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
// Simplified AIS provider hook for testing
|
||||||
|
export const useAISProvider = (boundingBox?: BoundingBox) => {
|
||||||
|
const [vessels, setVessels] = useState<VesselData[]>([]);
|
||||||
|
const [isConnected, setIsConnected] = useState(false);
|
||||||
|
const [error, setError] = useState<string | null>(null);
|
||||||
|
const [connectionStatus, setConnectionStatus] = useState<string>('Disconnected');
|
||||||
|
|
||||||
|
const wsRef = useRef<WebSocket | null>(null);
|
||||||
|
const vesselMapRef = useRef<Map<string, VesselData>>(new Map());
|
||||||
|
const reconnectTimeoutRef = useRef<any | null>(null);
|
||||||
|
const reconnectAttemptsRef = useRef<number>(0);
|
||||||
|
const connectionTimeoutRef = useRef<any | null>(null);
|
||||||
|
const isConnectingRef = useRef<boolean>(false);
|
||||||
|
const isMountedRef = useRef<boolean>(true);
|
||||||
|
const maxReconnectAttempts = 10;
|
||||||
|
const baseReconnectDelay = 1000; // 1 second
|
||||||
|
|
||||||
|
// Calculate exponential backoff delay
|
||||||
|
const getReconnectDelay = useCallback(() => {
|
||||||
|
const delay = baseReconnectDelay * Math.pow(2, reconnectAttemptsRef.current);
|
||||||
|
return Math.min(delay, 30000); // Cap at 30 seconds
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
// Connect to WebSocket with React StrictMode-safe logic
|
||||||
|
const connectSocket = useCallback(() => {
|
||||||
|
// Prevent multiple simultaneous connection attempts (React StrictMode protection)
|
||||||
|
if (isConnectingRef.current) {
|
||||||
|
console.log('Connection attempt already in progress, skipping...');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check if component is still mounted
|
||||||
|
if (!isMountedRef.current) {
|
||||||
|
console.log('Component unmounted, skipping connection attempt');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Clear any existing reconnection timeout
|
||||||
|
if (reconnectTimeoutRef.current) {
|
||||||
|
clearTimeout(reconnectTimeoutRef.current);
|
||||||
|
reconnectTimeoutRef.current = null;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Clear any existing connection timeout
|
||||||
|
if (connectionTimeoutRef.current) {
|
||||||
|
clearTimeout(connectionTimeoutRef.current);
|
||||||
|
connectionTimeoutRef.current = null;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check if already connected or connecting
|
||||||
|
if (wsRef.current?.readyState === WebSocket.OPEN) {
|
||||||
|
console.log('WebSocket already connected');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (wsRef.current?.readyState === WebSocket.CONNECTING) {
|
||||||
|
console.log('WebSocket already connecting');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check reconnection attempts
|
||||||
|
if (reconnectAttemptsRef.current >= maxReconnectAttempts) {
|
||||||
|
console.error('Max reconnection attempts reached');
|
||||||
|
setError('Failed to connect after multiple attempts');
|
||||||
|
setConnectionStatus('Failed');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Set connecting flag to prevent race conditions
|
||||||
|
isConnectingRef.current = true;
|
||||||
|
|
||||||
|
setConnectionStatus(reconnectAttemptsRef.current > 0 ?
|
||||||
|
`Reconnecting... (${reconnectAttemptsRef.current + 1}/${maxReconnectAttempts})` :
|
||||||
|
'Connecting...');
|
||||||
|
setError(null);
|
||||||
|
|
||||||
|
try {
|
||||||
|
console.log(`[CONNECT] Attempting WebSocket connection (attempt ${reconnectAttemptsRef.current + 1})`);
|
||||||
|
|
||||||
|
// Close any existing connection properly
|
||||||
|
if (wsRef.current) {
|
||||||
|
wsRef.current.onopen = null;
|
||||||
|
wsRef.current.onmessage = null;
|
||||||
|
wsRef.current.onerror = null;
|
||||||
|
wsRef.current.onclose = null;
|
||||||
|
wsRef.current.close();
|
||||||
|
wsRef.current = null;
|
||||||
|
}
|
||||||
|
|
||||||
|
const ws = new WebSocket('ws://localhost:3000/ws');
|
||||||
|
wsRef.current = ws;
|
||||||
|
|
||||||
|
// Set connection timeout with proper cleanup
|
||||||
|
connectionTimeoutRef.current = setTimeout(() => {
|
||||||
|
if (ws.readyState === WebSocket.CONNECTING && isMountedRef.current) {
|
||||||
|
console.log('[TIMEOUT] Connection timeout, closing WebSocket');
|
||||||
|
isConnectingRef.current = false;
|
||||||
|
ws.close();
|
||||||
|
}
|
||||||
|
}, 10000); // 10 second timeout
|
||||||
|
|
||||||
|
ws.onopen = () => {
|
||||||
|
// Clear connection timeout
|
||||||
|
if (connectionTimeoutRef.current) {
|
||||||
|
clearTimeout(connectionTimeoutRef.current);
|
||||||
|
connectionTimeoutRef.current = null;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check if component is still mounted
|
||||||
|
if (!isMountedRef.current) {
|
||||||
|
console.log('[OPEN] Component unmounted, closing connection');
|
||||||
|
ws.close();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log('[OPEN] Connected to AIS WebSocket');
|
||||||
|
isConnectingRef.current = false; // Clear connecting flag
|
||||||
|
setIsConnected(true);
|
||||||
|
setConnectionStatus('Connected');
|
||||||
|
setError(null);
|
||||||
|
reconnectAttemptsRef.current = 0; // Reset reconnection attempts
|
||||||
|
|
||||||
|
// Send bounding box if available
|
||||||
|
if (boundingBox && isMountedRef.current) {
|
||||||
|
const message: WebSocketMessage = {
|
||||||
|
type: 'set_bounding_box',
|
||||||
|
bounding_box: boundingBox
|
||||||
|
};
|
||||||
|
ws.send(JSON.stringify(message));
|
||||||
|
console.log('[OPEN] Sent bounding box:', boundingBox);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Start AIS stream
|
||||||
|
if (isMountedRef.current) {
|
||||||
|
const startMessage: WebSocketMessage = {
|
||||||
|
type: 'start_ais_stream'
|
||||||
|
};
|
||||||
|
ws.send(JSON.stringify(startMessage));
|
||||||
|
console.log('[OPEN] Started AIS stream');
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
ws.onmessage = (event) => {
|
||||||
|
try {
|
||||||
|
const messageData = event.data;
|
||||||
|
|
||||||
|
// Try to parse as JSON, but handle plain text messages gracefully
|
||||||
|
let data;
|
||||||
|
try {
|
||||||
|
data = JSON.parse(messageData);
|
||||||
|
} catch (parseError) {
|
||||||
|
console.log('Received plain text message:', messageData);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Handle JSON status messages
|
||||||
|
if (typeof data === 'string' || data.type) {
|
||||||
|
console.log('Received message:', data);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Process vessel data
|
||||||
|
const vesselData = convertAisResponseToVesselData(data);
|
||||||
|
if (vesselData) {
|
||||||
|
console.log('Received vessel data:', vesselData);
|
||||||
|
vesselMapRef.current.set(vesselData.mmsi, vesselData);
|
||||||
|
setVessels(Array.from(vesselMapRef.current.values()));
|
||||||
|
}
|
||||||
|
} catch (err) {
|
||||||
|
console.error('Error processing WebSocket message:', err);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
ws.onerror = (error) => {
|
||||||
|
// Clear connection timeout
|
||||||
|
if (connectionTimeoutRef.current) {
|
||||||
|
clearTimeout(connectionTimeoutRef.current);
|
||||||
|
connectionTimeoutRef.current = null;
|
||||||
|
}
|
||||||
|
|
||||||
|
console.error('[ERROR] WebSocket error:', error);
|
||||||
|
isConnectingRef.current = false; // Clear connecting flag
|
||||||
|
|
||||||
|
// Only update state if component is still mounted
|
||||||
|
if (isMountedRef.current) {
|
||||||
|
setError('WebSocket connection error');
|
||||||
|
setIsConnected(false);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
ws.onclose = (event) => {
|
||||||
|
// Clear connection timeout
|
||||||
|
if (connectionTimeoutRef.current) {
|
||||||
|
clearTimeout(connectionTimeoutRef.current);
|
||||||
|
connectionTimeoutRef.current = null;
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log(`[CLOSE] WebSocket connection closed: ${event.code} - ${event.reason}`);
|
||||||
|
isConnectingRef.current = false; // Clear connecting flag
|
||||||
|
|
||||||
|
// Only update state if component is still mounted
|
||||||
|
if (isMountedRef.current) {
|
||||||
|
setIsConnected(false);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Only attempt reconnection if component is mounted, wasn't a clean close, and we haven't exceeded max attempts
|
||||||
|
if (isMountedRef.current && !event.wasClean && reconnectAttemptsRef.current < maxReconnectAttempts) {
|
||||||
|
reconnectAttemptsRef.current++;
|
||||||
|
const delay = getReconnectDelay();
|
||||||
|
|
||||||
|
console.log(`[CLOSE] Scheduling reconnection in ${delay}ms (attempt ${reconnectAttemptsRef.current}/${maxReconnectAttempts})`);
|
||||||
|
setError(`Connection lost, reconnecting in ${Math.round(delay/1000)}s...`);
|
||||||
|
setConnectionStatus('Reconnecting...');
|
||||||
|
|
||||||
|
reconnectTimeoutRef.current = setTimeout(() => {
|
||||||
|
if (isMountedRef.current) {
|
||||||
|
connectSocket();
|
||||||
|
}
|
||||||
|
}, delay);
|
||||||
|
} else {
|
||||||
|
if (isMountedRef.current) {
|
||||||
|
if (event.wasClean) {
|
||||||
|
setConnectionStatus('Disconnected');
|
||||||
|
setError(null);
|
||||||
|
} else {
|
||||||
|
setConnectionStatus('Failed');
|
||||||
|
setError('Connection failed after multiple attempts');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
} catch (err) {
|
||||||
|
console.error('Error creating WebSocket connection:', err);
|
||||||
|
setError(err instanceof Error ? err.message : 'Unknown WebSocket error');
|
||||||
|
setConnectionStatus('Error');
|
||||||
|
|
||||||
|
// Schedule reconnection attempt
|
||||||
|
if (reconnectAttemptsRef.current < maxReconnectAttempts) {
|
||||||
|
reconnectAttemptsRef.current++;
|
||||||
|
const delay = getReconnectDelay();
|
||||||
|
reconnectTimeoutRef.current = setTimeout(() => {
|
||||||
|
connectSocket();
|
||||||
|
}, delay);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}, [boundingBox, getReconnectDelay]);
|
||||||
|
|
||||||
|
// Update bounding box
|
||||||
|
const updateBoundingBox = useCallback((bbox: BoundingBox) => {
|
||||||
|
if (wsRef.current?.readyState === WebSocket.OPEN) {
|
||||||
|
const message: WebSocketMessage = {
|
||||||
|
type: 'set_bounding_box',
|
||||||
|
bounding_box: bbox
|
||||||
|
};
|
||||||
|
wsRef.current.send(JSON.stringify(message));
|
||||||
|
console.log('Updated bounding box:', bbox);
|
||||||
|
|
||||||
|
// Clear existing vessels when bounding box changes
|
||||||
|
// vesselMapRef.current.clear();
|
||||||
|
// setVessels([]);
|
||||||
|
}
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
// Connect on mount with React StrictMode protection
|
||||||
|
useEffect(() => {
|
||||||
|
// Set mounted flag
|
||||||
|
isMountedRef.current = true;
|
||||||
|
|
||||||
|
// Small delay to prevent immediate double connection in StrictMode
|
||||||
|
const connectTimeout = setTimeout(() => {
|
||||||
|
if (isMountedRef.current) {
|
||||||
|
connectSocket();
|
||||||
|
}
|
||||||
|
}, 100);
|
||||||
|
|
||||||
|
return () => {
|
||||||
|
// Mark component as unmounted
|
||||||
|
isMountedRef.current = false;
|
||||||
|
|
||||||
|
// Clear connect timeout
|
||||||
|
clearTimeout(connectTimeout);
|
||||||
|
|
||||||
|
// Clear reconnection timeout
|
||||||
|
if (reconnectTimeoutRef.current) {
|
||||||
|
clearTimeout(reconnectTimeoutRef.current);
|
||||||
|
reconnectTimeoutRef.current = null;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Clear connection timeout
|
||||||
|
if (connectionTimeoutRef.current) {
|
||||||
|
clearTimeout(connectionTimeoutRef.current);
|
||||||
|
connectionTimeoutRef.current = null;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Reset connection flags
|
||||||
|
isConnectingRef.current = false;
|
||||||
|
|
||||||
|
// Close WebSocket connection properly
|
||||||
|
if (wsRef.current) {
|
||||||
|
console.log('[CLEANUP] Closing WebSocket connection');
|
||||||
|
wsRef.current.onopen = null;
|
||||||
|
wsRef.current.onmessage = null;
|
||||||
|
wsRef.current.onerror = null;
|
||||||
|
wsRef.current.onclose = null;
|
||||||
|
wsRef.current.close();
|
||||||
|
wsRef.current = null;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Reset reconnection attempts
|
||||||
|
reconnectAttemptsRef.current = 0;
|
||||||
|
};
|
||||||
|
}, [connectSocket]);
|
||||||
|
|
||||||
|
return {
|
||||||
|
vessels,
|
||||||
|
isConnected,
|
||||||
|
error,
|
||||||
|
connectionStatus,
|
||||||
|
connectSocket,
|
||||||
|
updateBoundingBox
|
||||||
|
};
|
||||||
|
};
|
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>;
|
||||||
|
}
|
75
crates/base-map/map/src/theme/neumorphic-theme.ts
Normal file
75
crates/base-map/map/src/theme/neumorphic-theme.ts
Normal file
@@ -0,0 +1,75 @@
|
|||||||
|
import { defineConfig } from "@chakra-ui/react";
|
||||||
|
|
||||||
|
// Neumorphic color palette
|
||||||
|
const neumorphicColors = {
|
||||||
|
light: {
|
||||||
|
bg: '#e0e5ec',
|
||||||
|
surface: '#e0e5ec',
|
||||||
|
text: '#2d3748',
|
||||||
|
textSecondary: '#4a5568',
|
||||||
|
accent: '#3182ce',
|
||||||
|
shadow: {
|
||||||
|
dark: '#a3b1c6',
|
||||||
|
light: '#ffffff',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
dark: {
|
||||||
|
bg: '#2d3748',
|
||||||
|
surface: '#ffffff',
|
||||||
|
text: '#f7fafc',
|
||||||
|
textSecondary: '#e2e8f0',
|
||||||
|
accent: '#63b3ed',
|
||||||
|
shadow: {
|
||||||
|
dark: '#1a202c',
|
||||||
|
light: '#4a5568',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
// Neumorphic shadow mixins
|
||||||
|
const neumorphicShadows = {
|
||||||
|
light: {
|
||||||
|
raised: '1px 1px 2px #a3b1c6, -1px -1px 2px #ffffff',
|
||||||
|
pressed: 'inset 2px 2px 4px #a3b1c6, inset -2px -2px 4px #ffffff',
|
||||||
|
subtle: '1px 1px 2px #a3b1c6, -1px -1px 2px #ffffff',
|
||||||
|
subtlePressed: 'inset 1px 1px 2px #a3b1c6, inset -1px -1px 2px #ffffff',
|
||||||
|
floating: '6px 6px 12px #a3b1c6, -6px -6px 12px #ffffff',
|
||||||
|
},
|
||||||
|
dark: {
|
||||||
|
raised: '2px 2px 4px #1a202c, -2px -2px 4px #4a5568',
|
||||||
|
pressed: 'inset 2px 2px 4px #1a202c, inset -2px -2px 4px #4a5568',
|
||||||
|
subtle: '2px 2px 2px #1a202c, -2px -2px 2px #4a5568',
|
||||||
|
subtlePressed: 'inset 2px 2px 2px #1a202c, inset -2px -2px 2px #4a5568',
|
||||||
|
floating: '6px 6px 12px #1a202c, -6px -6px 12px #4a5568',
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
// Simplified theme configuration to avoid TypeScript errors
|
||||||
|
// The utility functions below provide the neumorphic styling functionality
|
||||||
|
export const neumorphicTheme = defineConfig({
|
||||||
|
theme: {
|
||||||
|
// Theme configuration simplified to avoid type errors
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
// Utility functions for neumorphic styling
|
||||||
|
export const getNeumorphicStyle = (colorMode: 'light' | 'dark', variant: 'raised' | 'pressed' | 'subtle' | 'floating' = 'raised') => {
|
||||||
|
const colors = neumorphicColors[colorMode];
|
||||||
|
const shadows = neumorphicShadows[colorMode];
|
||||||
|
|
||||||
|
return {
|
||||||
|
bg: colors.surface,
|
||||||
|
color: colors.text,
|
||||||
|
borderRadius: 6,
|
||||||
|
boxShadow: shadows[variant] || shadows.raised,
|
||||||
|
transition: 'all 0.3s ease-in-out',
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
export const getNeumorphicColors = (colorMode: 'light' | 'dark') => {
|
||||||
|
return neumorphicColors[colorMode];
|
||||||
|
};
|
||||||
|
|
||||||
|
export const getNeumorphicShadows = (colorMode: 'light' | 'dark') => {
|
||||||
|
return neumorphicShadows[colorMode];
|
||||||
|
};
|
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;
|
||||||
|
// }
|
42
crates/base-map/map/src/vessel-marker.tsx
Normal file
42
crates/base-map/map/src/vessel-marker.tsx
Normal file
@@ -0,0 +1,42 @@
|
|||||||
|
import React from 'react';
|
||||||
|
|
||||||
|
interface VesselMarkerProps {
|
||||||
|
heading: number;
|
||||||
|
color?: string;
|
||||||
|
size?: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
const VesselMarker: React.FC<VesselMarkerProps> = ({
|
||||||
|
heading,
|
||||||
|
color = '#0066cc',
|
||||||
|
size = 16
|
||||||
|
}) => {
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
style={{
|
||||||
|
width: size,
|
||||||
|
height: size,
|
||||||
|
transform: `rotate(${heading}deg)`,
|
||||||
|
cursor: 'pointer',
|
||||||
|
display: 'flex',
|
||||||
|
alignItems: 'center',
|
||||||
|
justifyContent: 'center'
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<svg
|
||||||
|
width={size}
|
||||||
|
height={size}
|
||||||
|
viewBox="0 0 24 24"
|
||||||
|
fill={color}
|
||||||
|
style={{
|
||||||
|
filter: 'drop-shadow(0 2px 4px rgba(0,0,0,0.3))'
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{/* Simple vessel shape - triangle pointing up (north) */}
|
||||||
|
<path d="M12 2 L20 20 L12 16 L4 20 Z" />
|
||||||
|
</svg>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default VesselMarker;
|
10
crates/base-map/map/test/test-setup.ts
Normal file
10
crates/base-map/map/test/test-setup.ts
Normal file
@@ -0,0 +1,10 @@
|
|||||||
|
import '@testing-library/jest-dom';
|
||||||
|
import { vi } from 'vitest';
|
||||||
|
|
||||||
|
// Mock console methods to reduce noise in tests
|
||||||
|
global.console = {
|
||||||
|
...console,
|
||||||
|
log: vi.fn(),
|
||||||
|
warn: vi.fn(),
|
||||||
|
error: vi.fn(),
|
||||||
|
};
|
@@ -27,5 +27,6 @@
|
|||||||
},
|
},
|
||||||
"include": [
|
"include": [
|
||||||
"src"
|
"src"
|
||||||
]
|
],
|
||||||
|
"exclude": ["**/*.test.ts"]
|
||||||
}
|
}
|
||||||
|
@@ -4,4 +4,5 @@
|
|||||||
{ "path": "./tsconfig.app.json"},
|
{ "path": "./tsconfig.app.json"},
|
||||||
{ "path": "./tsconfig.node.json"}
|
{ "path": "./tsconfig.node.json"}
|
||||||
],
|
],
|
||||||
|
|
||||||
}
|
}
|
||||||
|
11
crates/base-map/map/vitest.config.ts
Normal file
11
crates/base-map/map/vitest.config.ts
Normal file
@@ -0,0 +1,11 @@
|
|||||||
|
import { defineConfig } from 'vitest/config';
|
||||||
|
import react from '@vitejs/plugin-react-swc';
|
||||||
|
|
||||||
|
export default defineConfig({
|
||||||
|
plugins: [react()],
|
||||||
|
test: {
|
||||||
|
environment: 'jsdom',
|
||||||
|
setupFiles: ['./test/test-setup.ts'],
|
||||||
|
globals: true,
|
||||||
|
},
|
||||||
|
});
|
@@ -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,5 @@ console_error_panic_hook = "0.1"
|
|||||||
|
|
||||||
[build-dependencies]
|
[build-dependencies]
|
||||||
embed-resource = "1"
|
embed-resource = "1"
|
||||||
base-map = { path = "../base-map" }
|
base-map = { path = "../base-map" } # Comment to Temporarily disable for testing
|
||||||
|
ais = { path = "../ais" } # Comment to Temporarily disable for testing
|
||||||
|
@@ -2,19 +2,39 @@
|
|||||||
#![cfg_attr(not(debug_assertions), windows_subsystem = "windows")]
|
#![cfg_attr(not(debug_assertions), windows_subsystem = "windows")]
|
||||||
|
|
||||||
use bevy::asset::AssetMetaCheck;
|
use bevy::asset::AssetMetaCheck;
|
||||||
|
use bevy::ecs::spawn::SpawnableList;
|
||||||
use bevy::prelude::*;
|
use bevy::prelude::*;
|
||||||
use bevy::window::PrimaryWindow;
|
use bevy::window::PrimaryWindow;
|
||||||
use bevy::winit::WinitWindows;
|
use bevy::winit::WinitWindows;
|
||||||
use bevy::DefaultPlugins;
|
use bevy::DefaultPlugins;
|
||||||
use yachtpit::GamePlugin;
|
|
||||||
use std::io::Cursor;
|
use std::io::Cursor;
|
||||||
|
use tokio::process::Command;
|
||||||
use winit::window::Icon;
|
use winit::window::Icon;
|
||||||
|
use yachtpit::GamePlugin;
|
||||||
|
|
||||||
|
#[cfg(not(target_arch = "wasm32"))]
|
||||||
|
#[tokio::main]
|
||||||
|
async fn main() {
|
||||||
|
// Start AIS server in background
|
||||||
|
tokio::spawn(async {
|
||||||
|
info!("Starting AIS server...");
|
||||||
|
let mut cmd = Command::new("target/release/ais").spawn().unwrap();
|
||||||
|
match cmd.wait().await {
|
||||||
|
Ok(status) => info!("AIS server process exited with status: {}", status),
|
||||||
|
Err(e) => error!("Error waiting for AIS server process: {}", e),
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
launch_bevy();
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
#[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"))]
|
fn launch_bevy() {
|
||||||
App::new()
|
App::new()
|
||||||
.insert_resource(ClearColor(Color::NONE))
|
.insert_resource(ClearColor(Color::NONE))
|
||||||
.add_plugins(
|
.add_plugins(
|
||||||
@@ -22,7 +42,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,
|
||||||
@@ -36,11 +55,18 @@ fn main() {
|
|||||||
}),
|
}),
|
||||||
)
|
)
|
||||||
.add_plugins(GamePlugin)
|
.add_plugins(GamePlugin)
|
||||||
.add_systems(Startup, set_window_icon)
|
.add_systems(Startup, set_window_icon) // Changed here
|
||||||
.add_plugins(WebviewWryPlugin::default())
|
.add_plugins(WebviewWryPlugin::default())
|
||||||
.run();
|
.run();
|
||||||
|
}
|
||||||
|
|
||||||
#[cfg(target_arch = "wasm32")]
|
#[cfg(target_arch = "wasm32")]
|
||||||
|
async fn main() {
|
||||||
|
launch_bevy();
|
||||||
|
}
|
||||||
|
|
||||||
|
#[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,8 +99,54 @@ fn main() {
|
|||||||
})
|
})
|
||||||
.run();
|
.run();
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
//
|
||||||
|
// fn start_ais_server() {
|
||||||
|
// static mut SERVER_STARTED: bool = false;
|
||||||
|
//
|
||||||
|
// unsafe {
|
||||||
|
// if SERVER_STARTED {
|
||||||
|
// return;
|
||||||
|
// }
|
||||||
|
// SERVER_STARTED = true;
|
||||||
|
// }
|
||||||
|
//
|
||||||
|
// let rt = tokio::runtime::Runtime::new().unwrap();
|
||||||
|
// rt.block_on(async {
|
||||||
|
// info!("Starting AIS server...");
|
||||||
|
// if let Ok(mut cmd) = Command::new("cargo")
|
||||||
|
// .current_dir("../ais")
|
||||||
|
// .arg("run").arg("--release")
|
||||||
|
// .spawn() {
|
||||||
|
// info!("AIS server process spawned");
|
||||||
|
// let status = cmd.wait().await;
|
||||||
|
// match status {
|
||||||
|
// Ok(exit_status) => match exit_status.code() {
|
||||||
|
// Some(code) => info!("AIS server exited with status code: {}", code),
|
||||||
|
// None => info!("AIS server terminated by signal"),
|
||||||
|
// },
|
||||||
|
// Err(e) => error!("AIS server failed: {}", e),
|
||||||
|
// }
|
||||||
|
// } else {
|
||||||
|
// error!("Failed to start AIS server - unable to spawn process");
|
||||||
|
// }
|
||||||
|
// });
|
||||||
|
// }
|
||||||
|
|
||||||
|
// fn start_ais_server() {
|
||||||
|
// // This task will run on the Tokio runtime's thread pool without blocking Bevy
|
||||||
|
// tokio::spawn(async {
|
||||||
|
// info!("Starting AIS server in the background...");
|
||||||
|
//
|
||||||
|
//
|
||||||
|
//
|
||||||
|
// // This now waits on the background task, not the main Bevy thread
|
||||||
|
// match cmd.wait().await {
|
||||||
|
// Ok(status) => info!("AIS server process exited with status: {}", status),
|
||||||
|
// Err(e) => error!("Error waiting for AIS server process: {}", e),
|
||||||
|
// }
|
||||||
|
// });
|
||||||
|
// }
|
||||||
|
|
||||||
// Sets the icon on windows and X11
|
// Sets the icon on windows and X11
|
||||||
fn set_window_icon(
|
fn set_window_icon(
|
||||||
|
@@ -13,17 +13,42 @@ impl Plugin for MenuPlugin {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Component)]
|
#[derive(Component, Clone)]
|
||||||
struct ButtonColors {
|
struct ButtonColors {
|
||||||
normal: Color,
|
normal: Color,
|
||||||
hovered: Color,
|
hovered: Color,
|
||||||
|
pressed: Color,
|
||||||
|
}
|
||||||
|
|
||||||
|
// Neumorphic color palette for luxury design
|
||||||
|
struct NeumorphicColors;
|
||||||
|
|
||||||
|
impl NeumorphicColors {
|
||||||
|
// Base surface color - soft gray with warm undertones
|
||||||
|
const SURFACE: Color = Color::linear_rgb(0.88, 0.90, 0.92);
|
||||||
|
|
||||||
|
// Primary button colors with depth
|
||||||
|
const PRIMARY_NORMAL: Color = Color::linear_rgb(0.85, 0.87, 0.90);
|
||||||
|
const PRIMARY_HOVERED: Color = Color::linear_rgb(0.90, 0.92, 0.95);
|
||||||
|
const PRIMARY_PRESSED: Color = Color::linear_rgb(0.80, 0.82, 0.85);
|
||||||
|
|
||||||
|
// Secondary button colors (more subtle)
|
||||||
|
const SECONDARY_NORMAL: Color = Color::linear_rgb(0.86, 0.88, 0.91);
|
||||||
|
const SECONDARY_HOVERED: Color = Color::linear_rgb(0.88, 0.90, 0.93);
|
||||||
|
const SECONDARY_PRESSED: Color = Color::linear_rgb(0.82, 0.84, 0.87);
|
||||||
|
|
||||||
|
// Text colors for contrast
|
||||||
|
const TEXT_PRIMARY: Color = Color::linear_rgb(0.25, 0.30, 0.35);
|
||||||
|
const TEXT_SECONDARY: Color = Color::linear_rgb(0.45, 0.50, 0.55);
|
||||||
|
const TEXT_ACCENT: Color = Color::linear_rgb(0.20, 0.45, 0.75);
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Default for ButtonColors {
|
impl Default for ButtonColors {
|
||||||
fn default() -> Self {
|
fn default() -> Self {
|
||||||
ButtonColors {
|
ButtonColors {
|
||||||
normal: Color::linear_rgb(0.15, 0.15, 0.15),
|
normal: NeumorphicColors::PRIMARY_NORMAL,
|
||||||
hovered: Color::linear_rgb(0.25, 0.25, 0.25),
|
hovered: NeumorphicColors::PRIMARY_HOVERED,
|
||||||
|
pressed: NeumorphicColors::PRIMARY_PRESSED,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -34,6 +59,18 @@ struct Menu;
|
|||||||
fn setup_menu(mut commands: Commands) {
|
fn setup_menu(mut commands: Commands) {
|
||||||
info!("menu");
|
info!("menu");
|
||||||
commands.spawn((Camera2d, Msaa::Off));
|
commands.spawn((Camera2d, Msaa::Off));
|
||||||
|
|
||||||
|
// Set neumorphic background
|
||||||
|
commands.spawn((
|
||||||
|
Node {
|
||||||
|
width: Val::Percent(100.0),
|
||||||
|
height: Val::Percent(100.0),
|
||||||
|
position_type: PositionType::Absolute,
|
||||||
|
..default()
|
||||||
|
},
|
||||||
|
BackgroundColor(NeumorphicColors::SURFACE),
|
||||||
|
));
|
||||||
|
|
||||||
commands
|
commands
|
||||||
.spawn((
|
.spawn((
|
||||||
Node {
|
Node {
|
||||||
@@ -52,23 +89,27 @@ fn setup_menu(mut commands: Commands) {
|
|||||||
.spawn((
|
.spawn((
|
||||||
Button,
|
Button,
|
||||||
Node {
|
Node {
|
||||||
width: Val::Px(140.0),
|
width: Val::Px(180.0),
|
||||||
height: Val::Px(50.0),
|
height: Val::Px(65.0),
|
||||||
justify_content: JustifyContent::Center,
|
justify_content: JustifyContent::Center,
|
||||||
align_items: AlignItems::Center,
|
align_items: AlignItems::Center,
|
||||||
|
border: UiRect::all(Val::Px(2.0)),
|
||||||
|
margin: UiRect::all(Val::Px(8.0)),
|
||||||
..Default::default()
|
..Default::default()
|
||||||
},
|
},
|
||||||
BackgroundColor(button_colors.normal),
|
BackgroundColor(button_colors.normal),
|
||||||
|
BorderColor(Color::linear_rgb(0.82, 0.84, 0.87)),
|
||||||
|
BorderRadius::all(Val::Px(16.0)),
|
||||||
button_colors,
|
button_colors,
|
||||||
ChangeState(GameState::Playing),
|
ChangeState(GameState::Playing),
|
||||||
))
|
))
|
||||||
.with_child((
|
.with_child((
|
||||||
Text::new("Play"),
|
Text::new("▶ PLAY"),
|
||||||
TextFont {
|
TextFont {
|
||||||
font_size: 40.0,
|
font_size: 28.0,
|
||||||
..default()
|
..default()
|
||||||
},
|
},
|
||||||
TextColor(Color::linear_rgb(0.9, 0.9, 0.9)),
|
TextColor(NeumorphicColors::TEXT_PRIMARY),
|
||||||
));
|
));
|
||||||
});
|
});
|
||||||
commands
|
commands
|
||||||
@@ -85,74 +126,71 @@ fn setup_menu(mut commands: Commands) {
|
|||||||
Menu,
|
Menu,
|
||||||
))
|
))
|
||||||
.with_children(|children| {
|
.with_children(|children| {
|
||||||
|
let secondary_button_colors = ButtonColors {
|
||||||
|
normal: NeumorphicColors::SECONDARY_NORMAL,
|
||||||
|
hovered: NeumorphicColors::SECONDARY_HOVERED,
|
||||||
|
pressed: NeumorphicColors::SECONDARY_PRESSED,
|
||||||
|
};
|
||||||
|
|
||||||
children
|
children
|
||||||
.spawn((
|
.spawn((
|
||||||
Button,
|
Button,
|
||||||
Node {
|
Node {
|
||||||
width: Val::Px(170.0),
|
width: Val::Px(180.0),
|
||||||
height: Val::Px(50.0),
|
height: Val::Px(45.0),
|
||||||
justify_content: JustifyContent::SpaceAround,
|
justify_content: JustifyContent::Center,
|
||||||
align_items: AlignItems::Center,
|
align_items: AlignItems::Center,
|
||||||
padding: UiRect::all(Val::Px(5.)),
|
padding: UiRect::all(Val::Px(8.)),
|
||||||
|
border: UiRect::all(Val::Px(1.0)),
|
||||||
|
margin: UiRect::horizontal(Val::Px(8.0)),
|
||||||
..Default::default()
|
..Default::default()
|
||||||
},
|
},
|
||||||
BackgroundColor(Color::NONE),
|
BackgroundColor(secondary_button_colors.normal),
|
||||||
ButtonColors {
|
BorderColor(Color::linear_rgb(0.80, 0.82, 0.85)),
|
||||||
normal: Color::NONE,
|
BorderRadius::all(Val::Px(12.0)),
|
||||||
..default()
|
secondary_button_colors.clone(),
|
||||||
},
|
|
||||||
OpenLink("https://bevyengine.org"),
|
OpenLink("https://bevyengine.org"),
|
||||||
))
|
))
|
||||||
.with_children(|parent| {
|
.with_child((
|
||||||
parent.spawn((
|
Text::new("🚀 Made with Bevy"),
|
||||||
Text::new("Made with Bevy"),
|
|
||||||
TextFont {
|
TextFont {
|
||||||
font_size: 15.0,
|
font_size: 14.0,
|
||||||
..default()
|
..default()
|
||||||
},
|
},
|
||||||
TextColor(Color::linear_rgb(0.9, 0.9, 0.9)),
|
TextColor(NeumorphicColors::TEXT_SECONDARY),
|
||||||
));
|
));
|
||||||
parent.spawn((
|
|
||||||
Node {
|
|
||||||
width: Val::Px(32.),
|
|
||||||
..default()
|
|
||||||
},
|
|
||||||
));
|
|
||||||
});
|
|
||||||
children
|
children
|
||||||
.spawn((
|
.spawn((
|
||||||
Button,
|
Button,
|
||||||
Node {
|
Node {
|
||||||
width: Val::Px(170.0),
|
width: Val::Px(180.0),
|
||||||
height: Val::Px(50.0),
|
height: Val::Px(45.0),
|
||||||
justify_content: JustifyContent::SpaceAround,
|
justify_content: JustifyContent::Center,
|
||||||
align_items: AlignItems::Center,
|
align_items: AlignItems::Center,
|
||||||
padding: UiRect::all(Val::Px(5.)),
|
padding: UiRect::all(Val::Px(8.)),
|
||||||
|
border: UiRect::all(Val::Px(1.0)),
|
||||||
|
margin: UiRect::horizontal(Val::Px(8.0)),
|
||||||
..default()
|
..default()
|
||||||
},
|
},
|
||||||
BackgroundColor(Color::NONE),
|
BackgroundColor(secondary_button_colors.normal),
|
||||||
|
BorderColor(Color::linear_rgb(0.80, 0.82, 0.85)),
|
||||||
|
BorderRadius::all(Val::Px(12.0)),
|
||||||
ButtonColors {
|
ButtonColors {
|
||||||
normal: Color::NONE,
|
normal: NeumorphicColors::SECONDARY_NORMAL,
|
||||||
hovered: Color::linear_rgb(0.25, 0.25, 0.25),
|
hovered: NeumorphicColors::SECONDARY_HOVERED,
|
||||||
|
pressed: NeumorphicColors::SECONDARY_PRESSED,
|
||||||
},
|
},
|
||||||
OpenLink("https://github.com/NiklasEi/bevy_game_template"),
|
OpenLink("https://github.com/NiklasEi/bevy_game_template"),
|
||||||
))
|
))
|
||||||
.with_children(|parent| {
|
.with_child((
|
||||||
parent.spawn((
|
Text::new("📖 Open Source"),
|
||||||
Text::new("Open source"),
|
|
||||||
TextFont {
|
TextFont {
|
||||||
font_size: 15.0,
|
font_size: 14.0,
|
||||||
..default()
|
..default()
|
||||||
},
|
},
|
||||||
TextColor(Color::linear_rgb(0.9, 0.9, 0.9)),
|
TextColor(NeumorphicColors::TEXT_SECONDARY),
|
||||||
));
|
));
|
||||||
parent.spawn((
|
|
||||||
Node {
|
|
||||||
width: Val::Px(32.),
|
|
||||||
..default()
|
|
||||||
},
|
|
||||||
));
|
|
||||||
});
|
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -178,6 +216,10 @@ fn click_play_button(
|
|||||||
for (interaction, mut color, button_colors, change_state, open_link) in &mut interaction_query {
|
for (interaction, mut color, button_colors, change_state, open_link) in &mut interaction_query {
|
||||||
match *interaction {
|
match *interaction {
|
||||||
Interaction::Pressed => {
|
Interaction::Pressed => {
|
||||||
|
// Apply pressed state visual feedback
|
||||||
|
*color = button_colors.pressed.into();
|
||||||
|
|
||||||
|
// Handle button actions
|
||||||
if let Some(state) = change_state {
|
if let Some(state) = change_state {
|
||||||
next_state.set(state.0.clone());
|
next_state.set(state.0.clone());
|
||||||
} else if let Some(link) = open_link {
|
} else if let Some(link) = open_link {
|
||||||
@@ -187,9 +229,11 @@ fn click_play_button(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
Interaction::Hovered => {
|
Interaction::Hovered => {
|
||||||
|
// Smooth transition to hovered state
|
||||||
*color = button_colors.hovered.into();
|
*color = button_colors.hovered.into();
|
||||||
}
|
}
|
||||||
Interaction::None => {
|
Interaction::None => {
|
||||||
|
// Return to normal state
|
||||||
*color = button_colors.normal.into();
|
*color = button_colors.normal.into();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
33
package-lock.json
generated
Normal file
33
package-lock.json
generated
Normal file
@@ -0,0 +1,33 @@
|
|||||||
|
{
|
||||||
|
"name": "yachtpit",
|
||||||
|
"lockfileVersion": 3,
|
||||||
|
"requires": true,
|
||||||
|
"packages": {
|
||||||
|
"": {
|
||||||
|
"dependencies": {
|
||||||
|
"ws": "^8.18.3"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/ws": {
|
||||||
|
"version": "8.18.3",
|
||||||
|
"resolved": "https://registry.npmjs.org/ws/-/ws-8.18.3.tgz",
|
||||||
|
"integrity": "sha512-PEIGCY5tSlUt50cqyMXfCzX+oOPqN0vuGqWzbcJ2xvnkzkq46oOpz7dQaTDBdfICb4N14+GARUDw2XV2N4tvzg==",
|
||||||
|
"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
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
5
package.json
Normal file
5
package.json
Normal file
@@ -0,0 +1,5 @@
|
|||||||
|
{
|
||||||
|
"dependencies": {
|
||||||
|
"ws": "^8.18.3"
|
||||||
|
}
|
||||||
|
}
|
67
test_bounding_box.js
Normal file
67
test_bounding_box.js
Normal file
@@ -0,0 +1,67 @@
|
|||||||
|
#!/usr/bin/env node
|
||||||
|
|
||||||
|
// Test WebSocket bounding box functionality
|
||||||
|
const WebSocket = require('ws');
|
||||||
|
|
||||||
|
const ws = new WebSocket('ws://localhost:3000/ws');
|
||||||
|
|
||||||
|
ws.on('open', function open() {
|
||||||
|
console.log('Connected to AIS WebSocket server');
|
||||||
|
|
||||||
|
// Send bounding box configuration message
|
||||||
|
const boundingBoxMessage = {
|
||||||
|
type: 'set_bounding_box',
|
||||||
|
bounding_box: {
|
||||||
|
sw_lat: 33.7,
|
||||||
|
sw_lon: -118.3,
|
||||||
|
ne_lat: 33.8,
|
||||||
|
ne_lon: -118.2
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
console.log('Sending bounding box configuration:', boundingBoxMessage);
|
||||||
|
ws.send(JSON.stringify(boundingBoxMessage));
|
||||||
|
});
|
||||||
|
|
||||||
|
ws.on('message', function message(data) {
|
||||||
|
const message = data.toString();
|
||||||
|
|
||||||
|
if (message.startsWith('Connected to AIS stream')) {
|
||||||
|
console.log('✓ Received connection confirmation:', message);
|
||||||
|
} else {
|
||||||
|
try {
|
||||||
|
const parsedData = JSON.parse(message);
|
||||||
|
|
||||||
|
if (parsedData.type === 'bounding_box_set') {
|
||||||
|
console.log('✓ Received bounding box confirmation:', parsedData);
|
||||||
|
} else if (parsedData.mmsi || parsedData.ship_name) {
|
||||||
|
console.log('✓ Received filtered AIS data:', {
|
||||||
|
mmsi: parsedData.mmsi,
|
||||||
|
ship_name: parsedData.ship_name,
|
||||||
|
latitude: parsedData.latitude,
|
||||||
|
longitude: parsedData.longitude,
|
||||||
|
timestamp: parsedData.timestamp
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
console.log('✓ Received message:', parsedData);
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
console.log('✓ Received text message:', message);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
ws.on('error', function error(err) {
|
||||||
|
console.error('WebSocket error:', err);
|
||||||
|
});
|
||||||
|
|
||||||
|
ws.on('close', function close() {
|
||||||
|
console.log('WebSocket connection closed');
|
||||||
|
});
|
||||||
|
|
||||||
|
// Keep the script running for 15 seconds to receive some filtered data
|
||||||
|
setTimeout(() => {
|
||||||
|
console.log('Closing connection after 15 seconds...');
|
||||||
|
ws.close();
|
||||||
|
process.exit(0);
|
||||||
|
}, 15000);
|
134
test_browser_websocket.html
Normal file
134
test_browser_websocket.html
Normal file
@@ -0,0 +1,134 @@
|
|||||||
|
<!DOCTYPE html>
|
||||||
|
<html lang="en">
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8">
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||||
|
<title>AIS WebSocket Browser Test</title>
|
||||||
|
<style>
|
||||||
|
body { font-family: Arial, sans-serif; margin: 20px; }
|
||||||
|
.status { padding: 10px; margin: 10px 0; border-radius: 5px; }
|
||||||
|
.connected { background-color: #d4edda; color: #155724; }
|
||||||
|
.disconnected { background-color: #f8d7da; color: #721c24; }
|
||||||
|
.error { background-color: #fff3cd; color: #856404; }
|
||||||
|
.message { background-color: #f8f9fa; padding: 10px; margin: 5px 0; border-left: 3px solid #007bff; }
|
||||||
|
.vessel-data { background-color: #e7f3ff; padding: 10px; margin: 5px 0; border-left: 3px solid #28a745; }
|
||||||
|
#messages { max-height: 400px; overflow-y: auto; }
|
||||||
|
</style>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<h1>AIS WebSocket Browser Test</h1>
|
||||||
|
<div id="status" class="status disconnected">Disconnected</div>
|
||||||
|
<button id="connectBtn">Connect</button>
|
||||||
|
<button id="disconnectBtn" disabled>Disconnect</button>
|
||||||
|
<button id="setBoundingBoxBtn" disabled>Set Bounding Box</button>
|
||||||
|
|
||||||
|
<h2>Messages</h2>
|
||||||
|
<div id="messages"></div>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
let ws = null;
|
||||||
|
const statusDiv = document.getElementById('status');
|
||||||
|
const messagesDiv = document.getElementById('messages');
|
||||||
|
const connectBtn = document.getElementById('connectBtn');
|
||||||
|
const disconnectBtn = document.getElementById('disconnectBtn');
|
||||||
|
const setBoundingBoxBtn = document.getElementById('setBoundingBoxBtn');
|
||||||
|
|
||||||
|
function updateStatus(message, className) {
|
||||||
|
statusDiv.textContent = message;
|
||||||
|
statusDiv.className = `status ${className}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
function addMessage(message, className = 'message') {
|
||||||
|
const div = document.createElement('div');
|
||||||
|
div.className = className;
|
||||||
|
div.innerHTML = `<strong>${new Date().toLocaleTimeString()}</strong>: ${message}`;
|
||||||
|
messagesDiv.appendChild(div);
|
||||||
|
messagesDiv.scrollTop = messagesDiv.scrollHeight;
|
||||||
|
}
|
||||||
|
|
||||||
|
function connect() {
|
||||||
|
if (ws && ws.readyState === WebSocket.OPEN) return;
|
||||||
|
|
||||||
|
updateStatus('Connecting...', 'error');
|
||||||
|
ws = new WebSocket('ws://localhost:3000/ws');
|
||||||
|
|
||||||
|
ws.onopen = function() {
|
||||||
|
updateStatus('Connected', 'connected');
|
||||||
|
addMessage('Connected to AIS WebSocket server');
|
||||||
|
connectBtn.disabled = true;
|
||||||
|
disconnectBtn.disabled = false;
|
||||||
|
setBoundingBoxBtn.disabled = false;
|
||||||
|
};
|
||||||
|
|
||||||
|
ws.onmessage = function(event) {
|
||||||
|
try {
|
||||||
|
const data = JSON.parse(event.data);
|
||||||
|
|
||||||
|
// Handle different message types
|
||||||
|
if (typeof data === 'string' || data.type) {
|
||||||
|
addMessage(`Server message: ${JSON.stringify(data)}`);
|
||||||
|
} else if (data.mmsi) {
|
||||||
|
// AIS vessel data
|
||||||
|
const vesselInfo = `
|
||||||
|
<strong>Vessel Data:</strong><br>
|
||||||
|
MMSI: ${data.mmsi || 'N/A'}<br>
|
||||||
|
Name: ${data.ship_name || 'N/A'}<br>
|
||||||
|
Position: ${data.latitude || 'N/A'}, ${data.longitude || 'N/A'}<br>
|
||||||
|
Speed: ${data.speed_over_ground || 'N/A'} knots<br>
|
||||||
|
Course: ${data.course_over_ground || 'N/A'}°<br>
|
||||||
|
Type: ${data.ship_type || 'N/A'}
|
||||||
|
`;
|
||||||
|
addMessage(vesselInfo, 'vessel-data');
|
||||||
|
} else {
|
||||||
|
addMessage(`Unknown data: ${JSON.stringify(data)}`);
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
addMessage(`Raw message: ${event.data}`);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
ws.onerror = function(error) {
|
||||||
|
updateStatus('Error', 'error');
|
||||||
|
addMessage(`WebSocket error: ${error}`);
|
||||||
|
};
|
||||||
|
|
||||||
|
ws.onclose = function(event) {
|
||||||
|
updateStatus('Disconnected', 'disconnected');
|
||||||
|
addMessage(`Connection closed: ${event.code} - ${event.reason}`);
|
||||||
|
connectBtn.disabled = false;
|
||||||
|
disconnectBtn.disabled = true;
|
||||||
|
setBoundingBoxBtn.disabled = true;
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
function disconnect() {
|
||||||
|
if (ws) {
|
||||||
|
ws.close();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function setBoundingBox() {
|
||||||
|
if (ws && ws.readyState === WebSocket.OPEN) {
|
||||||
|
const message = {
|
||||||
|
type: 'set_bounding_box',
|
||||||
|
bounding_box: {
|
||||||
|
sw_lat: 33.7,
|
||||||
|
sw_lon: -118.3,
|
||||||
|
ne_lat: 33.8,
|
||||||
|
ne_lon: -118.2
|
||||||
|
}
|
||||||
|
};
|
||||||
|
ws.send(JSON.stringify(message));
|
||||||
|
addMessage('Sent bounding box configuration');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
connectBtn.addEventListener('click', connect);
|
||||||
|
disconnectBtn.addEventListener('click', disconnect);
|
||||||
|
setBoundingBoxBtn.addEventListener('click', setBoundingBox);
|
||||||
|
|
||||||
|
// Auto-connect on page load
|
||||||
|
connect();
|
||||||
|
</script>
|
||||||
|
</body>
|
||||||
|
</html>
|
30
test_integration.sh
Executable file
30
test_integration.sh
Executable file
@@ -0,0 +1,30 @@
|
|||||||
|
#!/bin/bash
|
||||||
|
|
||||||
|
echo "Testing AIS Service Integration"
|
||||||
|
echo "==============================="
|
||||||
|
|
||||||
|
# Test 1: Los Angeles area (default)
|
||||||
|
echo "Test 1: Los Angeles area"
|
||||||
|
curl -s "http://localhost:8081/ais?sw_lat=33.6&sw_lon=-118.5&ne_lat=33.9&ne_lon=-118.0" | jq '.[0].ship_name'
|
||||||
|
echo ""
|
||||||
|
|
||||||
|
# Test 2: San Francisco Bay area
|
||||||
|
echo "Test 2: San Francisco Bay area"
|
||||||
|
curl -s "http://localhost:8081/ais?sw_lat=37.5&sw_lon=-122.5&ne_lat=37.9&ne_lon=-122.0" | jq '.[0].ship_name'
|
||||||
|
echo ""
|
||||||
|
|
||||||
|
# Test 3: New York Harbor area
|
||||||
|
echo "Test 3: New York Harbor area"
|
||||||
|
curl -s "http://localhost:8081/ais?sw_lat=40.5&sw_lon=-74.2&ne_lat=40.8&ne_lon=-73.8" | jq '.[0].ship_name'
|
||||||
|
echo ""
|
||||||
|
|
||||||
|
# Test 4: Check response structure
|
||||||
|
echo "Test 4: Response structure check"
|
||||||
|
response=$(curl -s "http://localhost:8081/ais?sw_lat=33.6&sw_lon=-118.5&ne_lat=33.9&ne_lon=-118.0")
|
||||||
|
echo "Response contains bounding box: $(echo $response | jq '.[0].raw_message.bounding_box != null')"
|
||||||
|
echo "Response has latitude: $(echo $response | jq '.[0].latitude != null')"
|
||||||
|
echo "Response has longitude: $(echo $response | jq '.[0].longitude != null')"
|
||||||
|
echo ""
|
||||||
|
|
||||||
|
echo "Integration test completed successfully!"
|
||||||
|
echo "The React map will call the AIS service with similar requests when the map bounds change."
|
51
test_websocket.js
Normal file
51
test_websocket.js
Normal file
@@ -0,0 +1,51 @@
|
|||||||
|
#!/usr/bin/env node
|
||||||
|
|
||||||
|
// Simple WebSocket test client for AIS data
|
||||||
|
const WebSocket = require('ws');
|
||||||
|
|
||||||
|
const ws = new WebSocket('ws://localhost:3000/ws');
|
||||||
|
|
||||||
|
ws.on('open', function open() {
|
||||||
|
console.log('Connected to AIS WebSocket server');
|
||||||
|
|
||||||
|
// Send a test message
|
||||||
|
ws.send('Hello from test client');
|
||||||
|
});
|
||||||
|
|
||||||
|
ws.on('message', function message(data) {
|
||||||
|
const message = data.toString();
|
||||||
|
|
||||||
|
if (message.startsWith('Connected to AIS stream')) {
|
||||||
|
console.log('✓ Received connection confirmation:', message);
|
||||||
|
} else if (message.startsWith('Echo:')) {
|
||||||
|
console.log('✓ Received echo response:', message);
|
||||||
|
} else {
|
||||||
|
try {
|
||||||
|
const aisData = JSON.parse(message);
|
||||||
|
console.log('✓ Received AIS data:', {
|
||||||
|
mmsi: aisData.mmsi,
|
||||||
|
ship_name: aisData.ship_name,
|
||||||
|
latitude: aisData.latitude,
|
||||||
|
longitude: aisData.longitude,
|
||||||
|
timestamp: aisData.timestamp
|
||||||
|
});
|
||||||
|
} catch (e) {
|
||||||
|
console.log('✓ Received message:', message);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
ws.on('error', function error(err) {
|
||||||
|
console.error('WebSocket error:', err);
|
||||||
|
});
|
||||||
|
|
||||||
|
ws.on('close', function close() {
|
||||||
|
console.log('WebSocket connection closed');
|
||||||
|
});
|
||||||
|
|
||||||
|
// Keep the script running for 30 seconds to receive some data
|
||||||
|
setTimeout(() => {
|
||||||
|
console.log('Closing connection after 30 seconds...');
|
||||||
|
ws.close();
|
||||||
|
process.exit(0);
|
||||||
|
}, 30000);
|
BIN
yachtpit-og.png
BIN
yachtpit-og.png
Binary file not shown.
Before Width: | Height: | Size: 120 KiB |
BIN
yachtpit-x.png
BIN
yachtpit-x.png
Binary file not shown.
Before Width: | Height: | Size: 1.2 MiB After Width: | Height: | Size: 1.4 MiB |
Reference in New Issue
Block a user