mirror of
https://github.com/seemueller-io/yachtpit.git
synced 2025-09-08 22:46:45 +00:00
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.
This commit is contained in:
356
Cargo.lock
generated
356
Cargo.lock
generated
@@ -118,6 +118,25 @@ 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",
|
||||||
|
"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 +310,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 +487,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 +571,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 +615,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 +633,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 +653,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 +662,7 @@ dependencies = [
|
|||||||
"sync_wrapper",
|
"sync_wrapper",
|
||||||
"tower-layer",
|
"tower-layer",
|
||||||
"tower-service",
|
"tower-service",
|
||||||
|
"tracing",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
@@ -610,7 +673,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 +690,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 +746,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 +2143,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 +2620,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 +3675,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 +3785,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 +3814,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 +3825,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 +3858,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 +3875,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 +3912,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 +4653,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 +5842,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 +6307,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 +6327,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 +6336,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 +6450,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 +6788,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 +6849,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 +7596,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 +7724,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 +7753,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 +7784,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 +7796,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 +7821,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 +7936,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 +9319,7 @@ dependencies = [
|
|||||||
"gdkx11",
|
"gdkx11",
|
||||||
"gtk",
|
"gtk",
|
||||||
"html5ever",
|
"html5ever",
|
||||||
"http",
|
"http 1.3.1",
|
||||||
"javascriptcore-rs",
|
"javascriptcore-rs",
|
||||||
"jni",
|
"jni",
|
||||||
"kuchikiki",
|
"kuchikiki",
|
||||||
@@ -9134,6 +9450,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
22
crates/ais/Cargo.toml
Normal file
22
crates/ais/Cargo.toml
Normal file
@@ -0,0 +1,22 @@
|
|||||||
|
[package]
|
||||||
|
name = "ais"
|
||||||
|
version = "0.1.0"
|
||||||
|
edition = "2021"
|
||||||
|
|
||||||
|
[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"
|
||||||
|
|
||||||
|
[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
|
||||||
|
|
||||||
|
|
1082
crates/ais/src/ais.rs
Normal file
1082
crates/ais/src/ais.rs
Normal file
File diff suppressed because it is too large
Load Diff
10
crates/ais/src/main.rs
Normal file
10
crates/ais/src/main.rs
Normal file
@@ -0,0 +1,10 @@
|
|||||||
|
use crate::ais::start_ais_server;
|
||||||
|
|
||||||
|
mod ais;
|
||||||
|
|
||||||
|
#[tokio::main]
|
||||||
|
async fn main() {
|
||||||
|
if let Err(e) = start_ais_server().await {
|
||||||
|
eprintln!("Server error: {:?}", e);
|
||||||
|
}
|
||||||
|
}
|
1216
crates/base-map/map/package-lock.json
generated
1216
crates/base-map/map/package-lock.json
generated
File diff suppressed because it is too large
Load Diff
@@ -8,7 +8,9 @@
|
|||||||
"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",
|
||||||
@@ -20,23 +22,27 @@
|
|||||||
"@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,15 +1,19 @@
|
|||||||
import {useState, useMemo, useEffect} from 'react';
|
import {useState, useMemo, useEffect, 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.tsx';
|
||||||
|
import { type VesselData } from './real-ais-provider.tsx';
|
||||||
|
import { useRealAISProvider } from './real-ais-provider.tsx';
|
||||||
|
|
||||||
import PORTS from './test_data/nautical-base-data.json';
|
import PORTS from './test_data/nautical-base-data.json';
|
||||||
import {Box} from "@chakra-ui/react";
|
import {Box} from "@chakra-ui/react";
|
||||||
@@ -28,6 +32,65 @@ export interface Geolocation {
|
|||||||
|
|
||||||
export default function MapNext(props: any = {mapboxPublicKey: "", geolocation: Geolocation, vesselPosition: undefined, layer: undefined, mapView: undefined} as any) {
|
export default function MapNext(props: any = {mapboxPublicKey: "", geolocation: Geolocation, vesselPosition: undefined, layer: undefined, mapView: undefined} as any) {
|
||||||
const [popupInfo, setPopupInfo] = useState(null);
|
const [popupInfo, setPopupInfo] = useState(null);
|
||||||
|
const [vesselPopupInfo, setVesselPopupInfo] = useState<VesselData | null>(null);
|
||||||
|
const [boundingBox, setBoundingBox] = useState<{sw_lat: number, sw_lon: number, ne_lat: number, ne_lon: number} | undefined>(undefined);
|
||||||
|
const [userLocationLoaded, setUserLocationLoaded] = useState(false);
|
||||||
|
const [mapFocused, setMapFocused] = useState(false);
|
||||||
|
const mapRef = useRef<MapRef | null>(null);
|
||||||
|
|
||||||
|
// Use the real AIS provider with bounding box, user location, and map focus status
|
||||||
|
const { vessels } = useRealAISProvider(boundingBox, userLocationLoaded, mapFocused);
|
||||||
|
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
console.log("vessles", vessels);
|
||||||
|
}, [vessels]);
|
||||||
|
|
||||||
|
// Function to update bounding box from map bounds
|
||||||
|
const updateBoundingBox = useCallback(() => {
|
||||||
|
if (mapRef.current) {
|
||||||
|
const map = mapRef.current.getMap();
|
||||||
|
const bounds = map.getBounds();
|
||||||
|
if (bounds) {
|
||||||
|
const sw = bounds.getSouthWest();
|
||||||
|
const ne = bounds.getNorthEast();
|
||||||
|
|
||||||
|
setBoundingBox({
|
||||||
|
sw_lat: sw.lat,
|
||||||
|
sw_lon: sw.lng,
|
||||||
|
ne_lat: ne.lat,
|
||||||
|
ne_lon: ne.lng
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
// Handle map move events
|
||||||
|
const handleMapMove = useCallback(() => {
|
||||||
|
updateBoundingBox();
|
||||||
|
}, [updateBoundingBox]);
|
||||||
|
|
||||||
|
// Initialize bounding box when map loads
|
||||||
|
const handleMapLoad = useCallback(() => {
|
||||||
|
updateBoundingBox();
|
||||||
|
}, [updateBoundingBox]);
|
||||||
|
|
||||||
|
// Handle user location events
|
||||||
|
const handleGeolocate = useCallback((position: GeolocationPosition) => {
|
||||||
|
console.log('User location loaded:', position);
|
||||||
|
setUserLocationLoaded(true);
|
||||||
|
setMapFocused(true); // When geolocate succeeds, the map focuses on user location
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
const handleTrackUserLocationStart = useCallback(() => {
|
||||||
|
console.log('Started tracking user location');
|
||||||
|
// User location tracking started, but not necessarily loaded yet
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
const handleTrackUserLocationEnd = useCallback(() => {
|
||||||
|
console.log('Stopped tracking user location');
|
||||||
|
setMapFocused(false);
|
||||||
|
}, []);
|
||||||
|
|
||||||
const pins = useMemo(
|
const pins = useMemo(
|
||||||
() =>
|
() =>
|
||||||
@@ -55,6 +118,29 @@ export default function MapNext(props: any = {mapboxPublicKey: "", geolocation:
|
|||||||
[]
|
[]
|
||||||
);
|
);
|
||||||
|
|
||||||
|
const vesselMarkers = useMemo(
|
||||||
|
() =>
|
||||||
|
vessels.map((vessel) => (
|
||||||
|
<Marker
|
||||||
|
key={`vessel-${vessel.id}`}
|
||||||
|
longitude={vessel.longitude}
|
||||||
|
latitude={vessel.latitude}
|
||||||
|
anchor="center"
|
||||||
|
onClick={e => {
|
||||||
|
e.originalEvent.stopPropagation();
|
||||||
|
setVesselPopupInfo(vessel);
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<VesselMarker
|
||||||
|
heading={vessel.heading}
|
||||||
|
color={vessel.type === 'Yacht' ? '#00cc66' : vessel.type === 'Fishing Vessel' ? '#ff6600' : '#0066cc'}
|
||||||
|
size={14}
|
||||||
|
/>
|
||||||
|
</Marker>
|
||||||
|
)),
|
||||||
|
[vessels]
|
||||||
|
);
|
||||||
|
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
console.log("props.vesselPosition", props?.vesselPosition);
|
console.log("props.vesselPosition", props?.vesselPosition);
|
||||||
@@ -64,6 +150,7 @@ export default function MapNext(props: any = {mapboxPublicKey: "", geolocation:
|
|||||||
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 +159,27 @@ 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}}
|
||||||
|
onLoad={handleMapLoad}
|
||||||
|
onMoveEnd={handleMapMove}
|
||||||
>
|
>
|
||||||
<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}
|
||||||
|
|
||||||
{popupInfo && (
|
{popupInfo && (
|
||||||
<Popup
|
<Popup
|
||||||
@@ -126,6 +223,38 @@ export default function MapNext(props: any = {mapboxPublicKey: "", geolocation:
|
|||||||
</Popup>
|
</Popup>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
|
{vesselPopupInfo && (
|
||||||
|
<Popup
|
||||||
|
anchor="top"
|
||||||
|
longitude={vesselPopupInfo.longitude}
|
||||||
|
latitude={vesselPopupInfo.latitude}
|
||||||
|
onClose={() => setVesselPopupInfo(null)}
|
||||||
|
>
|
||||||
|
<div style={{ minWidth: '200px' }}>
|
||||||
|
<h3 style={{ margin: '0 0 8px 0', fontSize: '16px', fontWeight: 'bold' }}>
|
||||||
|
{vesselPopupInfo.name}
|
||||||
|
</h3>
|
||||||
|
<div style={{ fontSize: '14px', lineHeight: '1.4' }}>
|
||||||
|
<div><strong>Type:</strong> {vesselPopupInfo.type}</div>
|
||||||
|
<div><strong>MMSI:</strong> {vesselPopupInfo.mmsi}</div>
|
||||||
|
<div><strong>Call Sign:</strong> {vesselPopupInfo.callSign}</div>
|
||||||
|
<div><strong>Speed:</strong> {vesselPopupInfo.speed.toFixed(1)} knots</div>
|
||||||
|
<div><strong>Heading:</strong> {vesselPopupInfo.heading.toFixed(0)}°</div>
|
||||||
|
<div><strong>Length:</strong> {vesselPopupInfo.length.toFixed(0)}m</div>
|
||||||
|
{vesselPopupInfo.destination && (
|
||||||
|
<div><strong>Destination:</strong> {vesselPopupInfo.destination}</div>
|
||||||
|
)}
|
||||||
|
{vesselPopupInfo.eta && (
|
||||||
|
<div><strong>ETA:</strong> {vesselPopupInfo.eta}</div>
|
||||||
|
)}
|
||||||
|
<div style={{ fontSize: '12px', color: '#666', marginTop: '8px' }}>
|
||||||
|
Last Update: {vesselPopupInfo.lastUpdate.toLocaleTimeString()}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</Popup>
|
||||||
|
)}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
</Map>
|
</Map>
|
||||||
|
291
crates/base-map/map/src/real-ais-provider.tsx
Normal file
291
crates/base-map/map/src/real-ais-provider.tsx
Normal file
@@ -0,0 +1,291 @@
|
|||||||
|
import { useState, useEffect, useCallback, useRef } from 'react';
|
||||||
|
|
||||||
|
// Vessel data interface
|
||||||
|
export interface VesselData {
|
||||||
|
id: string;
|
||||||
|
name: string;
|
||||||
|
type: string;
|
||||||
|
latitude: number;
|
||||||
|
longitude: number;
|
||||||
|
heading: number; // degrees 0-359
|
||||||
|
speed: number; // knots
|
||||||
|
length: number; // meters
|
||||||
|
width: number; // meters
|
||||||
|
mmsi: string; // Maritime Mobile Service Identity
|
||||||
|
callSign: string;
|
||||||
|
destination?: string;
|
||||||
|
eta?: string;
|
||||||
|
lastUpdate: Date;
|
||||||
|
}
|
||||||
|
|
||||||
|
// AIS service response structure (matching Rust AisResponse)
|
||||||
|
interface AisResponse {
|
||||||
|
message_type?: string;
|
||||||
|
mmsi?: string;
|
||||||
|
ship_name?: string;
|
||||||
|
latitude?: number;
|
||||||
|
longitude?: number;
|
||||||
|
timestamp?: string;
|
||||||
|
speed_over_ground?: number;
|
||||||
|
course_over_ground?: number;
|
||||||
|
heading?: number;
|
||||||
|
navigation_status?: string;
|
||||||
|
ship_type?: string;
|
||||||
|
raw_message: any;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Bounding box for AIS queries
|
||||||
|
interface BoundingBox {
|
||||||
|
sw_lat: number;
|
||||||
|
sw_lon: number;
|
||||||
|
ne_lat: number;
|
||||||
|
ne_lon: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Convert AIS service response to VesselData format
|
||||||
|
const convertAisResponseToVesselData = (aisResponse: AisResponse): any | null => {
|
||||||
|
// Skip responses that don't have essential vessel data
|
||||||
|
|
||||||
|
console.log({aisResponse})
|
||||||
|
// return aisResponse.raw_message;
|
||||||
|
return {
|
||||||
|
id: aisResponse.mmsi,
|
||||||
|
name: aisResponse.ship_name || `Vessel ${aisResponse.mmsi}`,
|
||||||
|
type: aisResponse.ship_type || 'Unknown',
|
||||||
|
latitude: aisResponse.latitude,
|
||||||
|
longitude: aisResponse.longitude,
|
||||||
|
heading: aisResponse.heading || 0,
|
||||||
|
speed: aisResponse.speed_over_ground || 0,
|
||||||
|
length: 100, // Default length, could be extracted from raw_message if available
|
||||||
|
width: 20, // Default width
|
||||||
|
mmsi: aisResponse.mmsi,
|
||||||
|
callSign: '', // Could be extracted from raw_message if available
|
||||||
|
destination: '', // Could be extracted from raw_message if available
|
||||||
|
eta: '', // Could be extracted from raw_message if available
|
||||||
|
lastUpdate: new Date()
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
// WebSocket message types for communication with the backend
|
||||||
|
interface WebSocketMessage {
|
||||||
|
type: string;
|
||||||
|
bounding_box?: BoundingBox;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Hook to provide real AIS data from the service via WebSocket
|
||||||
|
export const useRealAISProvider = (boundingBox?: BoundingBox, userLocationLoaded?: boolean, mapFocused?: boolean) => {
|
||||||
|
const [vessels, setVessels] = useState<VesselData[]>([]);
|
||||||
|
const [isActive, setIsActive] = useState(true);
|
||||||
|
const [isLoading, setIsLoading] = useState(false);
|
||||||
|
const [error, setError] = useState<string | null>(null);
|
||||||
|
const [isConnected, setIsConnected] = useState(false);
|
||||||
|
const [aisStreamStarted, setAisStreamStarted] = useState(false);
|
||||||
|
|
||||||
|
const wsRef = useRef<WebSocket | null>(null);
|
||||||
|
const lastBoundingBoxRef = useRef<BoundingBox | undefined>(undefined);
|
||||||
|
const reconnectTimeoutRef = useRef<any | null>(null);
|
||||||
|
const vesselMapRef = useRef<Map<string, VesselData>>(new Map());
|
||||||
|
|
||||||
|
// Connect to WebSocket
|
||||||
|
const connectWebSocket = useCallback(() => {
|
||||||
|
// Prevent multiple connections
|
||||||
|
if (!isActive) return;
|
||||||
|
|
||||||
|
// Check if we already have an active or connecting WebSocket
|
||||||
|
if (wsRef.current &&
|
||||||
|
(wsRef.current.readyState === WebSocket.OPEN ||
|
||||||
|
wsRef.current.readyState === WebSocket.CONNECTING)) {
|
||||||
|
console.log('WebSocket already connected or connecting, skipping...');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Close any existing connection before creating a new one
|
||||||
|
if (wsRef.current) {
|
||||||
|
wsRef.current.close();
|
||||||
|
wsRef.current = null;
|
||||||
|
}
|
||||||
|
|
||||||
|
setIsLoading(true);
|
||||||
|
setError(null);
|
||||||
|
|
||||||
|
try {
|
||||||
|
console.log('Creating new WebSocket connection...');
|
||||||
|
const ws = new WebSocket('ws://localhost:3000/ws');
|
||||||
|
wsRef.current = ws;
|
||||||
|
|
||||||
|
ws.onopen = () => {
|
||||||
|
console.log('Connected to AIS WebSocket');
|
||||||
|
setIsConnected(true);
|
||||||
|
setIsLoading(false);
|
||||||
|
setError(null);
|
||||||
|
|
||||||
|
// Send bounding box configuration if available
|
||||||
|
// Note: We'll send the bounding box separately to avoid connection recreation
|
||||||
|
const currentBoundingBox = lastBoundingBoxRef.current;
|
||||||
|
if (currentBoundingBox) {
|
||||||
|
const message: WebSocketMessage = {
|
||||||
|
type: 'set_bounding_box',
|
||||||
|
bounding_box: currentBoundingBox
|
||||||
|
};
|
||||||
|
ws.send(JSON.stringify(message));
|
||||||
|
console.log('Sent initial bounding box configuration:', currentBoundingBox);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
ws.onmessage = (event) => {
|
||||||
|
try {
|
||||||
|
const data = JSON.parse(event.data);
|
||||||
|
|
||||||
|
// Handle connection confirmation and bounding box confirmations
|
||||||
|
if (typeof data === 'string' || data.type) {
|
||||||
|
console.log('Received WebSocket message:', data);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const vesselData = convertAisResponseToVesselData(data);
|
||||||
|
if (vesselData) {
|
||||||
|
// Update vessel map for efficient updates
|
||||||
|
vesselMapRef.current.set(vesselData.mmsi, vesselData);
|
||||||
|
|
||||||
|
// Update vessels state with current map values
|
||||||
|
setVessels(Array.from(vesselMapRef.current.values()));
|
||||||
|
}
|
||||||
|
} catch (err) {
|
||||||
|
console.error('Error parsing WebSocket message:', err);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
ws.onerror = (error) => {
|
||||||
|
console.error('WebSocket error:', error);
|
||||||
|
setError('WebSocket connection error');
|
||||||
|
setIsConnected(false);
|
||||||
|
};
|
||||||
|
|
||||||
|
ws.onclose = (event) => {
|
||||||
|
console.log('WebSocket connection closed:', event.code, event.reason);
|
||||||
|
setIsConnected(false);
|
||||||
|
setIsLoading(false);
|
||||||
|
|
||||||
|
// Attempt to reconnect if the connection was active
|
||||||
|
if (isActive && !event.wasClean) {
|
||||||
|
setError('Connection lost, attempting to reconnect...');
|
||||||
|
reconnectTimeoutRef.current = setTimeout(() => {
|
||||||
|
connectWebSocket();
|
||||||
|
}, 3000); // Reconnect after 3 seconds
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
} catch (err) {
|
||||||
|
console.error('Error creating WebSocket connection:', err);
|
||||||
|
setError(err instanceof Error ? err.message : 'Unknown WebSocket error');
|
||||||
|
setIsLoading(false);
|
||||||
|
}
|
||||||
|
}, [isActive]); // Removed boundingBox dependency to prevent reconnections
|
||||||
|
|
||||||
|
// Send bounding box update to WebSocket
|
||||||
|
const updateBoundingBox = useCallback((bbox: BoundingBox) => {
|
||||||
|
if (wsRef.current?.readyState === WebSocket.OPEN) {
|
||||||
|
const message: WebSocketMessage = {
|
||||||
|
type: 'set_bounding_box',
|
||||||
|
bounding_box: bbox
|
||||||
|
};
|
||||||
|
wsRef.current.send(JSON.stringify(message));
|
||||||
|
console.log('Updated bounding box:', bbox);
|
||||||
|
}
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
// Send start AIS stream message to WebSocket
|
||||||
|
const startAisStream = useCallback(() => {
|
||||||
|
if (wsRef.current?.readyState === WebSocket.OPEN && !aisStreamStarted) {
|
||||||
|
const message: WebSocketMessage = {
|
||||||
|
type: 'start_ais_stream'
|
||||||
|
};
|
||||||
|
wsRef.current.send(JSON.stringify(message));
|
||||||
|
console.log('Sent start AIS stream request');
|
||||||
|
setAisStreamStarted(true);
|
||||||
|
}
|
||||||
|
}, [aisStreamStarted]);
|
||||||
|
|
||||||
|
// Connect to WebSocket when component mounts or becomes active
|
||||||
|
useEffect(() => {
|
||||||
|
if (isActive) {
|
||||||
|
connectWebSocket();
|
||||||
|
}
|
||||||
|
|
||||||
|
return () => {
|
||||||
|
if (reconnectTimeoutRef.current) {
|
||||||
|
clearTimeout(reconnectTimeoutRef.current);
|
||||||
|
}
|
||||||
|
if (wsRef.current) {
|
||||||
|
wsRef.current.close();
|
||||||
|
wsRef.current = null;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}, [isActive, connectWebSocket]);
|
||||||
|
|
||||||
|
// Handle bounding box changes
|
||||||
|
useEffect(() => {
|
||||||
|
if (!boundingBox || !isActive) return;
|
||||||
|
|
||||||
|
// Check if bounding box actually changed to avoid unnecessary updates
|
||||||
|
const lastBbox = lastBoundingBoxRef.current;
|
||||||
|
if (lastBbox &&
|
||||||
|
lastBbox.sw_lat === boundingBox.sw_lat &&
|
||||||
|
lastBbox.sw_lon === boundingBox.sw_lon &&
|
||||||
|
lastBbox.ne_lat === boundingBox.ne_lat &&
|
||||||
|
lastBbox.ne_lon === boundingBox.ne_lon) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
lastBoundingBoxRef.current = boundingBox;
|
||||||
|
|
||||||
|
// Clear existing vessels when bounding box changes
|
||||||
|
vesselMapRef.current.clear();
|
||||||
|
setVessels([]);
|
||||||
|
|
||||||
|
// Send new bounding box to WebSocket
|
||||||
|
updateBoundingBox(boundingBox);
|
||||||
|
}, [boundingBox, updateBoundingBox, isActive]);
|
||||||
|
|
||||||
|
// Handle active state changes
|
||||||
|
useEffect(() => {
|
||||||
|
if (!isActive) {
|
||||||
|
// Close WebSocket connection when inactive
|
||||||
|
if (wsRef.current) {
|
||||||
|
wsRef.current.close();
|
||||||
|
wsRef.current = null;
|
||||||
|
}
|
||||||
|
setIsConnected(false);
|
||||||
|
setError(null);
|
||||||
|
|
||||||
|
// Clear reconnection timeout
|
||||||
|
if (reconnectTimeoutRef.current) {
|
||||||
|
clearTimeout(reconnectTimeoutRef.current);
|
||||||
|
reconnectTimeoutRef.current = null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}, [isActive]);
|
||||||
|
|
||||||
|
// Start AIS stream when user location is loaded and map is focused
|
||||||
|
useEffect(() => {
|
||||||
|
if (userLocationLoaded && mapFocused && isConnected && !aisStreamStarted) {
|
||||||
|
console.log('User location loaded and map focused, starting AIS stream...');
|
||||||
|
startAisStream();
|
||||||
|
}
|
||||||
|
}, [userLocationLoaded, mapFocused, isConnected, aisStreamStarted, startAisStream]);
|
||||||
|
|
||||||
|
return {
|
||||||
|
vessels,
|
||||||
|
isActive,
|
||||||
|
setIsActive,
|
||||||
|
isLoading,
|
||||||
|
error,
|
||||||
|
isConnected,
|
||||||
|
refreshVessels: () => {
|
||||||
|
// For WebSocket, we can trigger a reconnection to refresh data
|
||||||
|
if (wsRef.current) {
|
||||||
|
wsRef.current.close();
|
||||||
|
}
|
||||||
|
connectWebSocket();
|
||||||
|
}
|
||||||
|
};
|
||||||
|
};
|
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 * as React from 'react';
|
||||||
|
|
||||||
|
interface VesselMarkerProps {
|
||||||
|
size?: number;
|
||||||
|
color?: string;
|
||||||
|
heading?: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
const vesselStyle = {
|
||||||
|
cursor: 'pointer',
|
||||||
|
stroke: '#fff',
|
||||||
|
strokeWidth: 2
|
||||||
|
};
|
||||||
|
|
||||||
|
function VesselMarker({ size = 12, color = '#0066cc', heading = 0 }: VesselMarkerProps) {
|
||||||
|
return (
|
||||||
|
<svg
|
||||||
|
height={size}
|
||||||
|
width={size}
|
||||||
|
viewBox="0 0 24 24"
|
||||||
|
style={{
|
||||||
|
...vesselStyle,
|
||||||
|
transform: `rotate(${heading}deg)`,
|
||||||
|
transformOrigin: 'center'
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<circle
|
||||||
|
cx="12"
|
||||||
|
cy="12"
|
||||||
|
r="10"
|
||||||
|
fill={color}
|
||||||
|
/>
|
||||||
|
{/* Small arrow to indicate heading */}
|
||||||
|
<path
|
||||||
|
d="M12 4 L16 12 L12 10 L8 12 Z"
|
||||||
|
fill="#fff"
|
||||||
|
/>
|
||||||
|
</svg>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export default React.memo(VesselMarker);
|
246
crates/base-map/map/test/real-ais-provider.test.tsx
Normal file
246
crates/base-map/map/test/real-ais-provider.test.tsx
Normal file
@@ -0,0 +1,246 @@
|
|||||||
|
import { renderHook, act } from '@testing-library/react';
|
||||||
|
import { vi } from 'vitest';
|
||||||
|
import { useRealAISProvider } from '../src/real-ais-provider.tsx';
|
||||||
|
|
||||||
|
// Mock WebSocket
|
||||||
|
class MockWebSocket {
|
||||||
|
static instances: MockWebSocket[] = [];
|
||||||
|
static CONNECTING = 0;
|
||||||
|
static OPEN = 1;
|
||||||
|
static CLOSING = 2;
|
||||||
|
static CLOSED = 3;
|
||||||
|
|
||||||
|
readyState: number = MockWebSocket.CONNECTING;
|
||||||
|
onopen: ((event: Event) => void) | null = null;
|
||||||
|
onmessage: ((event: MessageEvent) => void) | null = null;
|
||||||
|
onerror: ((event: Event) => void) | null = null;
|
||||||
|
onclose: ((event: CloseEvent) => void) | null = null;
|
||||||
|
|
||||||
|
constructor(public url: string) {
|
||||||
|
MockWebSocket.instances.push(this);
|
||||||
|
|
||||||
|
// Simulate connection opening after a short delay
|
||||||
|
setTimeout(() => {
|
||||||
|
this.readyState = MockWebSocket.OPEN;
|
||||||
|
if (this.onopen) {
|
||||||
|
this.onopen(new Event('open'));
|
||||||
|
}
|
||||||
|
}, 10);
|
||||||
|
}
|
||||||
|
|
||||||
|
send(data: string) {
|
||||||
|
console.log('MockWebSocket send:', data);
|
||||||
|
}
|
||||||
|
|
||||||
|
close() {
|
||||||
|
this.readyState = MockWebSocket.CLOSED;
|
||||||
|
if (this.onclose) {
|
||||||
|
this.onclose(new CloseEvent('close', { wasClean: true }));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
static reset() {
|
||||||
|
MockWebSocket.instances = [];
|
||||||
|
}
|
||||||
|
|
||||||
|
static getConnectionCount() {
|
||||||
|
return MockWebSocket.instances.filter(ws =>
|
||||||
|
ws.readyState === MockWebSocket.OPEN ||
|
||||||
|
ws.readyState === MockWebSocket.CONNECTING
|
||||||
|
).length;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Replace global WebSocket with mock
|
||||||
|
(global as any).WebSocket = MockWebSocket;
|
||||||
|
|
||||||
|
describe('useRealAISProvider WebSocket Connection Management', () => {
|
||||||
|
beforeEach(() => {
|
||||||
|
MockWebSocket.reset();
|
||||||
|
vi.clearAllMocks();
|
||||||
|
});
|
||||||
|
|
||||||
|
afterEach(() => {
|
||||||
|
MockWebSocket.reset();
|
||||||
|
});
|
||||||
|
|
||||||
|
test('should create only one WebSocket connection on initial render', async () => {
|
||||||
|
const boundingBox = {
|
||||||
|
sw_lat: 33.0,
|
||||||
|
sw_lon: -119.0,
|
||||||
|
ne_lat: 34.0,
|
||||||
|
ne_lon: -118.0
|
||||||
|
};
|
||||||
|
|
||||||
|
const { result } = renderHook(() => useRealAISProvider(boundingBox));
|
||||||
|
|
||||||
|
// Wait for connection to be established
|
||||||
|
await act(async () => {
|
||||||
|
await new Promise(resolve => setTimeout(resolve, 50));
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(MockWebSocket.instances).toHaveLength(1);
|
||||||
|
expect(MockWebSocket.getConnectionCount()).toBe(1);
|
||||||
|
expect(result.current.isConnected).toBe(true);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('should not create multiple connections when bounding box changes', async () => {
|
||||||
|
const initialBoundingBox = {
|
||||||
|
sw_lat: 33.0,
|
||||||
|
sw_lon: -119.0,
|
||||||
|
ne_lat: 34.0,
|
||||||
|
ne_lon: -118.0
|
||||||
|
};
|
||||||
|
|
||||||
|
const { result, rerender } = renderHook(
|
||||||
|
({ boundingBox }) => useRealAISProvider(boundingBox),
|
||||||
|
{ initialProps: { boundingBox: initialBoundingBox } }
|
||||||
|
);
|
||||||
|
|
||||||
|
// Wait for initial connection
|
||||||
|
await act(async () => {
|
||||||
|
await new Promise(resolve => setTimeout(resolve, 50));
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(MockWebSocket.instances).toHaveLength(1);
|
||||||
|
expect(MockWebSocket.getConnectionCount()).toBe(1);
|
||||||
|
|
||||||
|
// Change bounding box multiple times
|
||||||
|
const newBoundingBox1 = {
|
||||||
|
sw_lat: 34.0,
|
||||||
|
sw_lon: -120.0,
|
||||||
|
ne_lat: 35.0,
|
||||||
|
ne_lon: -119.0
|
||||||
|
};
|
||||||
|
|
||||||
|
const newBoundingBox2 = {
|
||||||
|
sw_lat: 35.0,
|
||||||
|
sw_lon: -121.0,
|
||||||
|
ne_lat: 36.0,
|
||||||
|
ne_lon: -120.0
|
||||||
|
};
|
||||||
|
|
||||||
|
await act(async () => {
|
||||||
|
rerender({ boundingBox: newBoundingBox1 });
|
||||||
|
await new Promise(resolve => setTimeout(resolve, 20));
|
||||||
|
});
|
||||||
|
|
||||||
|
await act(async () => {
|
||||||
|
rerender({ boundingBox: newBoundingBox2 });
|
||||||
|
await new Promise(resolve => setTimeout(resolve, 20));
|
||||||
|
});
|
||||||
|
|
||||||
|
// Should still have only one connection
|
||||||
|
expect(MockWebSocket.getConnectionCount()).toBe(1);
|
||||||
|
expect(result.current.isConnected).toBe(true);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('should properly cleanup connection when component unmounts', async () => {
|
||||||
|
const boundingBox = {
|
||||||
|
sw_lat: 33.0,
|
||||||
|
sw_lon: -119.0,
|
||||||
|
ne_lat: 34.0,
|
||||||
|
ne_lon: -118.0
|
||||||
|
};
|
||||||
|
|
||||||
|
const { result, unmount } = renderHook(() => useRealAISProvider(boundingBox));
|
||||||
|
|
||||||
|
// Wait for connection
|
||||||
|
await act(async () => {
|
||||||
|
await new Promise(resolve => setTimeout(resolve, 50));
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(MockWebSocket.getConnectionCount()).toBe(1);
|
||||||
|
expect(result.current.isConnected).toBe(true);
|
||||||
|
|
||||||
|
// Unmount component
|
||||||
|
unmount();
|
||||||
|
|
||||||
|
// Connection should be closed
|
||||||
|
expect(MockWebSocket.instances[0].readyState).toBe(MockWebSocket.CLOSED);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('should not create connection when isActive is false', async () => {
|
||||||
|
const boundingBox = {
|
||||||
|
sw_lat: 33.0,
|
||||||
|
sw_lon: -119.0,
|
||||||
|
ne_lat: 34.0,
|
||||||
|
ne_lon: -118.0
|
||||||
|
};
|
||||||
|
|
||||||
|
// Create a custom hook that starts with isActive = false
|
||||||
|
const { result } = renderHook(() => {
|
||||||
|
const provider = useRealAISProvider(boundingBox);
|
||||||
|
// Set inactive immediately on first render
|
||||||
|
if (provider.isActive) {
|
||||||
|
provider.setIsActive(false);
|
||||||
|
}
|
||||||
|
return provider;
|
||||||
|
});
|
||||||
|
|
||||||
|
// Wait a bit to ensure no connection is created
|
||||||
|
await act(async () => {
|
||||||
|
await new Promise(resolve => setTimeout(resolve, 100));
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(MockWebSocket.instances).toHaveLength(0);
|
||||||
|
expect(result.current.isConnected).toBe(false);
|
||||||
|
expect(result.current.isActive).toBe(false);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('should handle reconnection properly without creating multiple connections', async () => {
|
||||||
|
const boundingBox = {
|
||||||
|
sw_lat: 33.0,
|
||||||
|
sw_lon: -119.0,
|
||||||
|
ne_lat: 34.0,
|
||||||
|
ne_lon: -118.0
|
||||||
|
};
|
||||||
|
|
||||||
|
const { result } = renderHook(() => useRealAISProvider(boundingBox));
|
||||||
|
|
||||||
|
// Wait for initial connection
|
||||||
|
await act(async () => {
|
||||||
|
await new Promise(resolve => setTimeout(resolve, 50));
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(MockWebSocket.getConnectionCount()).toBe(1);
|
||||||
|
|
||||||
|
// Simulate connection loss
|
||||||
|
await act(async () => {
|
||||||
|
const ws = MockWebSocket.instances[0];
|
||||||
|
ws.readyState = MockWebSocket.CLOSED;
|
||||||
|
if (ws.onclose) {
|
||||||
|
ws.onclose(new CloseEvent('close', { wasClean: false }));
|
||||||
|
}
|
||||||
|
await new Promise(resolve => setTimeout(resolve, 3100)); // Wait for reconnection timeout
|
||||||
|
});
|
||||||
|
|
||||||
|
// Should have attempted reconnection but still only one active connection
|
||||||
|
expect(MockWebSocket.getConnectionCount()).toBe(1);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('should send bounding box configuration on connection', async () => {
|
||||||
|
const boundingBox = {
|
||||||
|
sw_lat: 33.0,
|
||||||
|
sw_lon: -119.0,
|
||||||
|
ne_lat: 34.0,
|
||||||
|
ne_lon: -118.0
|
||||||
|
};
|
||||||
|
|
||||||
|
const sendSpy = vi.spyOn(MockWebSocket.prototype, 'send');
|
||||||
|
|
||||||
|
const { result } = renderHook(() => useRealAISProvider(boundingBox));
|
||||||
|
|
||||||
|
// Wait for connection and bounding box message
|
||||||
|
await act(async () => {
|
||||||
|
await new Promise(resolve => setTimeout(resolve, 50));
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(sendSpy).toHaveBeenCalledWith(
|
||||||
|
JSON.stringify({
|
||||||
|
type: 'set_bounding_box',
|
||||||
|
bounding_box: boundingBox
|
||||||
|
})
|
||||||
|
);
|
||||||
|
});
|
||||||
|
});
|
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: ['./src/test-setup.ts'],
|
||||||
|
globals: true,
|
||||||
|
},
|
||||||
|
});
|
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);
|
Reference in New Issue
Block a user