From 7528b2117bcd34683b5597d40418941b5c193f60 Mon Sep 17 00:00:00 2001 From: geoffsee <> Date: Sun, 20 Jul 2025 22:27:39 -0400 Subject: [PATCH] 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. --- Cargo.lock | 356 ++++- Cargo.toml | 2 +- crates/ais/Cargo.lock | 1308 +++++++++++++++++ crates/ais/Cargo.toml | 22 + crates/ais/error.md | 91 ++ crates/ais/src/ais.rs | 1082 ++++++++++++++ crates/ais/src/main.rs | 10 + crates/base-map/map/package-lock.json | 1216 ++++++++++++++- crates/base-map/map/package.json | 14 +- crates/base-map/map/src/MapNext.tsx | 137 +- crates/base-map/map/src/real-ais-provider.tsx | 291 ++++ crates/base-map/map/src/vessel-marker.tsx | 42 + .../map/test/real-ais-provider.test.tsx | 246 ++++ crates/base-map/map/test/test-setup.ts | 10 + crates/base-map/map/tsconfig.app.json | 3 +- crates/base-map/map/tsconfig.json | 1 + crates/base-map/map/vitest.config.ts | 11 + package-lock.json | 33 + package.json | 5 + test_bounding_box.js | 67 + test_browser_websocket.html | 134 ++ test_integration.sh | 30 + test_websocket.js | 51 + 23 files changed, 5134 insertions(+), 28 deletions(-) create mode 100644 crates/ais/Cargo.lock create mode 100644 crates/ais/Cargo.toml create mode 100644 crates/ais/error.md create mode 100644 crates/ais/src/ais.rs create mode 100644 crates/ais/src/main.rs create mode 100644 crates/base-map/map/src/real-ais-provider.tsx create mode 100644 crates/base-map/map/src/vessel-marker.tsx create mode 100644 crates/base-map/map/test/real-ais-provider.test.tsx create mode 100644 crates/base-map/map/test/test-setup.ts create mode 100644 crates/base-map/map/vitest.config.ts create mode 100644 package-lock.json create mode 100644 package.json create mode 100644 test_bounding_box.js create mode 100644 test_browser_websocket.html create mode 100755 test_integration.sh create mode 100644 test_websocket.js diff --git a/Cargo.lock b/Cargo.lock index 7b77d43..b4e56d1 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -118,6 +118,25 @@ dependencies = [ "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]] name = "aligned-vec" version = "0.6.4" @@ -291,6 +310,16 @@ dependencies = [ "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]] name = "assert_type_match" version = "0.1.1" @@ -458,6 +487,28 @@ dependencies = [ "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]] name = "async-task" version = "4.7.1" @@ -520,6 +571,12 @@ dependencies = [ "portable-atomic-util", ] +[[package]] +name = "auto-future" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3c1e7e457ea78e524f48639f551fd79703ac3f2237f5ecccdf4708f8a75ad373" + [[package]] name = "autocfg" version = "1.5.0" @@ -558,9 +615,10 @@ dependencies = [ "async-trait", "axum-core", "axum-macros", + "base64 0.22.1", "bytes", "futures-util", - "http", + "http 1.3.1", "http-body", "http-body-util", "hyper", @@ -575,11 +633,15 @@ dependencies = [ "serde", "serde_json", "serde_path_to_error", + "serde_urlencoded", + "sha1", "sync_wrapper", "tokio", - "tower", + "tokio-tungstenite 0.24.0", + "tower 0.5.2", "tower-layer", "tower-service", + "tracing", ] [[package]] @@ -591,7 +653,7 @@ dependencies = [ "async-trait", "bytes", "futures-util", - "http", + "http 1.3.1", "http-body", "http-body-util", "mime", @@ -600,6 +662,7 @@ dependencies = [ "sync_wrapper", "tower-layer", "tower-service", + "tracing", ] [[package]] @@ -610,7 +673,7 @@ checksum = "077959a7f8cf438676af90b483304528eb7e16eadadb7f44e9ada4f9dceb9e62" dependencies = [ "axum-core", "chrono", - "http", + "http 1.3.1", "mime_guess", "rust-embed", "tower-service", @@ -627,6 +690,35 @@ dependencies = [ "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]] name = "backtrace" version = "0.3.75" @@ -654,7 +746,7 @@ dependencies = [ "serde_json", "tao", "tokio", - "tower-http", + "tower-http 0.6.6", "tracing", "tracing-subscriber", "web-sys", @@ -2051,6 +2143,15 @@ version = "1.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" 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]] name = "combine" version = "4.6.7" @@ -2519,6 +2620,12 @@ dependencies = [ "unicode-xid", ] +[[package]] +name = "diff" +version = "0.1.13" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "56254986775e3233ffa9c4d7d3faaf6d36a2c09d30b20687e9f88bc8bafc16c8" + [[package]] name = "digest" version = "0.10.7" @@ -3568,7 +3675,7 @@ dependencies = [ "fnv", "futures-core", "futures-sink", - "http", + "http 1.3.1", "indexmap 2.10.0", "slab", "tokio", @@ -3678,6 +3785,17 @@ dependencies = [ "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]] name = "http" version = "1.3.1" @@ -3696,7 +3814,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1efedce1fb8e6913f23e0c92de8e62cd5b772a67e7b3946df930a62566c93184" dependencies = [ "bytes", - "http", + "http 1.3.1", ] [[package]] @@ -3707,7 +3825,7 @@ checksum = "b021d93e26becf5dc7e1b75b1bed1fd93124b374ceb73f43d4d4eafec896a64a" dependencies = [ "bytes", "futures-core", - "http", + "http 1.3.1", "http-body", "pin-project-lite", ] @@ -3740,7 +3858,7 @@ dependencies = [ "futures-channel", "futures-util", "h2", - "http", + "http 1.3.1", "http-body", "httparse", "httpdate", @@ -3757,7 +3875,7 @@ version = "0.27.7" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e3c93eb611681b207e1fe55d5a71ecf91572ec8a6705cdb6857f7d8d5242cf58" dependencies = [ - "http", + "http 1.3.1", "hyper", "hyper-util", "rustls", @@ -3794,7 +3912,7 @@ dependencies = [ "futures-channel", "futures-core", "futures-util", - "http", + "http 1.3.1", "http-body", "hyper", "ipnet", @@ -4535,6 +4653,30 @@ dependencies = [ "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]] name = "naga" version = "24.0.0" @@ -5700,6 +5842,16 @@ version = "0.3.1" source = "registry+https://github.com/rust-lang/crates.io-index" 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]] name = "prettyplease" version = "0.2.35" @@ -6155,7 +6307,7 @@ dependencies = [ "encoding_rs", "futures-core", "h2", - "http", + "http 1.3.1", "http-body", "http-body-util", "hyper", @@ -6175,8 +6327,8 @@ dependencies = [ "sync_wrapper", "tokio", "tokio-native-tls", - "tower", - "tower-http", + "tower 0.5.2", + "tower-http 0.6.6", "tower-service", "url", "wasm-bindgen", @@ -6184,6 +6336,15 @@ dependencies = [ "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]] name = "rfd" version = "0.15.4" @@ -6289,6 +6450,22 @@ dependencies = [ "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]] name = "rustc-demangle" version = "0.1.25" @@ -6611,6 +6788,17 @@ dependencies = [ "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]] name = "sha2" version = "0.10.9" @@ -6661,6 +6849,12 @@ dependencies = [ "quote", ] +[[package]] +name = "similar" +version = "2.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bbbb5d9659141646ae647b42fe094daf6c6192d1620870b449d9557f748b2daa" + [[package]] name = "siphasher" version = "0.3.11" @@ -7402,6 +7596,56 @@ dependencies = [ "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]] name = "tokio-util" version = "0.7.15" @@ -7480,6 +7724,22 @@ dependencies = [ "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]] name = "tower" version = "0.5.2" @@ -7493,6 +7753,23 @@ dependencies = [ "tokio", "tower-layer", "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]] @@ -7507,7 +7784,7 @@ dependencies = [ "bytes", "futures-core", "futures-util", - "http", + "http 1.3.1", "http-body", "http-body-util", "http-range-header", @@ -7519,7 +7796,7 @@ dependencies = [ "pin-project-lite", "tokio", "tokio-util", - "tower", + "tower 0.5.2", "tower-layer", "tower-service", "tracing", @@ -7544,6 +7821,7 @@ version = "0.1.41" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "784e0ac535deb450455cbfa28a6f0df145ea1bb7ae51b821cf5e7927fdcfbdd0" dependencies = [ + "log", "pin-project-lite", "tracing-attributes", "tracing-core", @@ -7658,6 +7936,44 @@ version = "0.25.1" source = "registry+https://github.com/rust-lang/crates.io-index" 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]] name = "typeid" version = "1.0.3" @@ -9003,7 +9319,7 @@ dependencies = [ "gdkx11", "gtk", "html5ever", - "http", + "http 1.3.1", "javascriptcore-rs", "jni", "kuchikiki", @@ -9134,6 +9450,12 @@ dependencies = [ "wry", ] +[[package]] +name = "yansi" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cfe53a6657fd280eaa890a3bc59152892ffa3e30101319d168b781ed6529b049" + [[package]] name = "yazi" version = "0.2.1" diff --git a/Cargo.toml b/Cargo.toml index ebcc7a9..5179dcd 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,5 +1,5 @@ [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" default-members = [ diff --git a/crates/ais/Cargo.lock b/crates/ais/Cargo.lock new file mode 100644 index 0000000..9871ab8 --- /dev/null +++ b/crates/ais/Cargo.lock @@ -0,0 +1,1308 @@ +# This file is automatically @generated by Cargo. +# It is not intended for manual editing. +version = 4 + +[[package]] +name = "addr2line" +version = "0.24.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dfbe277e56a376000877090da837660b4427aad530e3028d44e0bffe4f89a1c1" +dependencies = [ + "gimli", +] + +[[package]] +name = "adler2" +version = "2.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "320119579fcad9c21884f5c4861d16174d0e06250625266f50fe6898340abefa" + +[[package]] +name = "ais" +version = "0.1.0" +dependencies = [ + "futures-util", + "serde", + "serde_json", + "tokio", + "tokio-tungstenite", + "url", +] + +[[package]] +name = "autocfg" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c08606f8c3cbf4ce6ec8e28fb0014a2c086708fe954eaa885384a6165172e7e8" + +[[package]] +name = "backtrace" +version = "0.3.75" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6806a6321ec58106fea15becdad98371e28d92ccbc7c8f1b3b6dd724fe8f1002" +dependencies = [ + "addr2line", + "cfg-if", + "libc", + "miniz_oxide", + "object", + "rustc-demangle", + "windows-targets 0.52.6", +] + +[[package]] +name = "bitflags" +version = "2.9.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1b8e56985ec62d17e9c1001dc89c88ecd7dc08e47eba5ec7c29c7b5eeecde967" + +[[package]] +name = "block-buffer" +version = "0.10.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3078c7629b62d3f0439517fa394996acacc5cbc91c5a20d8c658e77abd503a71" +dependencies = [ + "generic-array", +] + +[[package]] +name = "byteorder" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1fd0f2584146f6f2ef48085050886acf353beff7305ebd1ae69500e27c67f64b" + +[[package]] +name = "bytes" +version = "1.10.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d71b6127be86fdcfddb610f7182ac57211d4b18a3e9c82eb2d17662f2227ad6a" + +[[package]] +name = "cc" +version = "1.2.30" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "deec109607ca693028562ed836a5f1c4b8bd77755c4e132fc5ce11b0b6211ae7" +dependencies = [ + "shlex", +] + +[[package]] +name = "cfg-if" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9555578bc9e57714c812a1f84e4fc5b4d21fcb063490c624de019f7464c91268" + +[[package]] +name = "core-foundation" +version = "0.9.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "91e195e091a93c46f7102ec7818a2aa394e1e1771c3ab4825963fa03e45afb8f" +dependencies = [ + "core-foundation-sys", + "libc", +] + +[[package]] +name = "core-foundation-sys" +version = "0.8.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "773648b94d0e5d620f64f280777445740e61fe701025087ec8b57f45c791888b" + +[[package]] +name = "cpufeatures" +version = "0.2.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "59ed5838eebb26a2bb2e58f6d5b5316989ae9d08bab10e0e6d103e656d1b0280" +dependencies = [ + "libc", +] + +[[package]] +name = "crypto-common" +version = "0.1.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1bfb12502f3fc46cca1bb51ac28df9d618d813cdc3d2f25b9fe775a34af26bb3" +dependencies = [ + "generic-array", + "typenum", +] + +[[package]] +name = "data-encoding" +version = "2.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2a2330da5de22e8a3cb63252ce2abb30116bf5265e89c0e01bc17015ce30a476" + +[[package]] +name = "digest" +version = "0.10.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9ed9a281f7bc9b7576e61468ba615a66a5c8cfdff42420a70aa82701a3b1e292" +dependencies = [ + "block-buffer", + "crypto-common", +] + +[[package]] +name = "displaydoc" +version = "0.2.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "97369cbbc041bc366949bc74d34658d6cda5621039731c6310521892a3a20ae0" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "errno" +version = "0.3.13" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "778e2ac28f6c47af28e4907f13ffd1e1ddbd400980a9abd7c8df189bf578a5ad" +dependencies = [ + "libc", + "windows-sys 0.60.2", +] + +[[package]] +name = "fastrand" +version = "2.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "37909eebbb50d72f9059c3b6d82c0463f2ff062c9e95845c43a6c9c0355411be" + +[[package]] +name = "fnv" +version = "1.0.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3f9eec918d3f24069decb9af1554cad7c880e2da24a9afd88aca000531ab82c1" + +[[package]] +name = "foreign-types" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f6f339eb8adc052cd2ca78910fda869aefa38d22d5cb648e6485e4d3fc06f3b1" +dependencies = [ + "foreign-types-shared", +] + +[[package]] +name = "foreign-types-shared" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "00b0228411908ca8685dba7fc2cdd70ec9990a6e753e89b6ac91a84c40fbaf4b" + +[[package]] +name = "form_urlencoded" +version = "1.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e13624c2627564efccf4934284bdd98cbaa14e79b0b5a141218e507b3a823456" +dependencies = [ + "percent-encoding", +] + +[[package]] +name = "futures-core" +version = "0.3.31" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "05f29059c0c2090612e8d742178b0580d2dc940c837851ad723096f87af6663e" + +[[package]] +name = "futures-macro" +version = "0.3.31" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "162ee34ebcb7c64a8abebc059ce0fee27c2262618d7b60ed8faf72fef13c3650" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "futures-sink" +version = "0.3.31" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e575fab7d1e0dcb8d0c7bcf9a63ee213816ab51902e6d244a95819acacf1d4f7" + +[[package]] +name = "futures-task" +version = "0.3.31" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f90f7dce0722e95104fcb095585910c0977252f286e354b5e3bd38902cd99988" + +[[package]] +name = "futures-util" +version = "0.3.31" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9fa08315bb612088cc391249efdc3bc77536f16c91f6cf495e6fbe85b20a4a81" +dependencies = [ + "futures-core", + "futures-macro", + "futures-sink", + "futures-task", + "pin-project-lite", + "pin-utils", + "slab", +] + +[[package]] +name = "generic-array" +version = "0.14.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "85649ca51fd72272d7821adaf274ad91c288277713d9c18820d8499a7ff69e9a" +dependencies = [ + "typenum", + "version_check", +] + +[[package]] +name = "getrandom" +version = "0.2.16" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "335ff9f135e4384c8150d6f27c6daed433577f86b4750418338c01a1a2528592" +dependencies = [ + "cfg-if", + "libc", + "wasi 0.11.1+wasi-snapshot-preview1", +] + +[[package]] +name = "getrandom" +version = "0.3.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "26145e563e54f2cadc477553f1ec5ee650b00862f0a58bcd12cbdc5f0ea2d2f4" +dependencies = [ + "cfg-if", + "libc", + "r-efi", + "wasi 0.14.2+wasi-0.2.4", +] + +[[package]] +name = "gimli" +version = "0.31.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "07e28edb80900c19c28f1072f2e8aeca7fa06b23cd4169cefe1af5aa3260783f" + +[[package]] +name = "http" +version = "0.2.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "601cbb57e577e2f5ef5be8e7b83f0f63994f25aa94d673e54a92d5c516d101f1" +dependencies = [ + "bytes", + "fnv", + "itoa", +] + +[[package]] +name = "httparse" +version = "1.10.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6dbf3de79e51f3d586ab4cb9d5c3e2c14aa28ed23d180cf89b4df0454a69cc87" + +[[package]] +name = "icu_collections" +version = "2.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "200072f5d0e3614556f94a9930d5dc3e0662a652823904c3a75dc3b0af7fee47" +dependencies = [ + "displaydoc", + "potential_utf", + "yoke", + "zerofrom", + "zerovec", +] + +[[package]] +name = "icu_locale_core" +version = "2.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0cde2700ccaed3872079a65fb1a78f6c0a36c91570f28755dda67bc8f7d9f00a" +dependencies = [ + "displaydoc", + "litemap", + "tinystr", + "writeable", + "zerovec", +] + +[[package]] +name = "icu_normalizer" +version = "2.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "436880e8e18df4d7bbc06d58432329d6458cc84531f7ac5f024e93deadb37979" +dependencies = [ + "displaydoc", + "icu_collections", + "icu_normalizer_data", + "icu_properties", + "icu_provider", + "smallvec", + "zerovec", +] + +[[package]] +name = "icu_normalizer_data" +version = "2.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "00210d6893afc98edb752b664b8890f0ef174c8adbb8d0be9710fa66fbbf72d3" + +[[package]] +name = "icu_properties" +version = "2.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "016c619c1eeb94efb86809b015c58f479963de65bdb6253345c1a1276f22e32b" +dependencies = [ + "displaydoc", + "icu_collections", + "icu_locale_core", + "icu_properties_data", + "icu_provider", + "potential_utf", + "zerotrie", + "zerovec", +] + +[[package]] +name = "icu_properties_data" +version = "2.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "298459143998310acd25ffe6810ed544932242d3f07083eee1084d83a71bd632" + +[[package]] +name = "icu_provider" +version = "2.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "03c80da27b5f4187909049ee2d72f276f0d9f99a42c306bd0131ecfe04d8e5af" +dependencies = [ + "displaydoc", + "icu_locale_core", + "stable_deref_trait", + "tinystr", + "writeable", + "yoke", + "zerofrom", + "zerotrie", + "zerovec", +] + +[[package]] +name = "idna" +version = "1.0.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "686f825264d630750a544639377bae737628043f20d38bbc029e8f29ea968a7e" +dependencies = [ + "idna_adapter", + "smallvec", + "utf8_iter", +] + +[[package]] +name = "idna_adapter" +version = "1.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3acae9609540aa318d1bc588455225fb2085b9ed0c4f6bd0d9d5bcd86f1a0344" +dependencies = [ + "icu_normalizer", + "icu_properties", +] + +[[package]] +name = "io-uring" +version = "0.7.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b86e202f00093dcba4275d4636b93ef9dd75d025ae560d2521b45ea28ab49013" +dependencies = [ + "bitflags", + "cfg-if", + "libc", +] + +[[package]] +name = "itoa" +version = "1.0.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4a5f13b858c8d314ee3e8f639011f7ccefe71f97f96e50151fb991f267928e2c" + +[[package]] +name = "libc" +version = "0.2.174" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1171693293099992e19cddea4e8b849964e9846f4acee11b3948bcc337be8776" + +[[package]] +name = "linux-raw-sys" +version = "0.9.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cd945864f07fe9f5371a27ad7b52a172b4b499999f1d97574c9fa68373937e12" + +[[package]] +name = "litemap" +version = "0.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "241eaef5fd12c88705a01fc1066c48c4b36e0dd4377dcdc7ec3942cea7a69956" + +[[package]] +name = "lock_api" +version = "0.4.13" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "96936507f153605bddfcda068dd804796c84324ed2510809e5b2a624c81da765" +dependencies = [ + "autocfg", + "scopeguard", +] + +[[package]] +name = "log" +version = "0.4.27" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "13dc2df351e3202783a1fe0d44375f7295ffb4049267b0f3018346dc122a1d94" + +[[package]] +name = "memchr" +version = "2.7.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "32a282da65faaf38286cf3be983213fcf1d2e2a58700e808f83f4ea9a4804bc0" + +[[package]] +name = "miniz_oxide" +version = "0.8.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1fa76a2c86f704bdb222d66965fb3d63269ce38518b83cb0575fca855ebb6316" +dependencies = [ + "adler2", +] + +[[package]] +name = "mio" +version = "1.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "78bed444cc8a2160f01cbcf811ef18cac863ad68ae8ca62092e8db51d51c761c" +dependencies = [ + "libc", + "wasi 0.11.1+wasi-snapshot-preview1", + "windows-sys 0.59.0", +] + +[[package]] +name = "native-tls" +version = "0.2.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "87de3442987e9dbec73158d5c715e7ad9072fda936bb03d19d7fa10e00520f0e" +dependencies = [ + "libc", + "log", + "openssl", + "openssl-probe", + "openssl-sys", + "schannel", + "security-framework", + "security-framework-sys", + "tempfile", +] + +[[package]] +name = "object" +version = "0.36.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "62948e14d923ea95ea2c7c86c71013138b66525b86bdc08d2dcc262bdb497b87" +dependencies = [ + "memchr", +] + +[[package]] +name = "once_cell" +version = "1.21.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "42f5e15c9953c5e4ccceeb2e7382a716482c34515315f7b03532b8b4e8393d2d" + +[[package]] +name = "openssl" +version = "0.10.73" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8505734d46c8ab1e19a1dce3aef597ad87dcb4c37e7188231769bd6bd51cebf8" +dependencies = [ + "bitflags", + "cfg-if", + "foreign-types", + "libc", + "once_cell", + "openssl-macros", + "openssl-sys", +] + +[[package]] +name = "openssl-macros" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a948666b637a0f465e8564c73e89d4dde00d72d4d473cc972f390fc3dcee7d9c" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "openssl-probe" +version = "0.1.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d05e27ee213611ffe7d6348b942e8f942b37114c00cc03cec254295a4a17852e" + +[[package]] +name = "openssl-sys" +version = "0.9.109" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "90096e2e47630d78b7d1c20952dc621f957103f8bc2c8359ec81290d75238571" +dependencies = [ + "cc", + "libc", + "pkg-config", + "vcpkg", +] + +[[package]] +name = "parking_lot" +version = "0.12.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "70d58bf43669b5795d1576d0641cfb6fbb2057bf629506267a92807158584a13" +dependencies = [ + "lock_api", + "parking_lot_core", +] + +[[package]] +name = "parking_lot_core" +version = "0.9.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bc838d2a56b5b1a6c25f55575dfc605fabb63bb2365f6c2353ef9159aa69e4a5" +dependencies = [ + "cfg-if", + "libc", + "redox_syscall", + "smallvec", + "windows-targets 0.52.6", +] + +[[package]] +name = "percent-encoding" +version = "2.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e3148f5046208a5d56bcfc03053e3ca6334e51da8dfb19b6cdc8b306fae3283e" + +[[package]] +name = "pin-project-lite" +version = "0.2.16" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3b3cff922bd51709b605d9ead9aa71031d81447142d828eb4a6eba76fe619f9b" + +[[package]] +name = "pin-utils" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8b870d8c151b6f2fb93e84a13146138f05d02ed11c7e7c54f8826aaaf7c9f184" + +[[package]] +name = "pkg-config" +version = "0.3.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7edddbd0b52d732b21ad9a5fab5c704c14cd949e5e9a1ec5929a24fded1b904c" + +[[package]] +name = "potential_utf" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e5a7c30837279ca13e7c867e9e40053bc68740f988cb07f7ca6df43cc734b585" +dependencies = [ + "zerovec", +] + +[[package]] +name = "ppv-lite86" +version = "0.2.21" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "85eae3c4ed2f50dcfe72643da4befc30deadb458a9b590d720cde2f2b1e97da9" +dependencies = [ + "zerocopy", +] + +[[package]] +name = "proc-macro2" +version = "1.0.95" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "02b3e5e68a3a1a02aad3ec490a98007cbc13c37cbe84a3cd7b8e406d76e7f778" +dependencies = [ + "unicode-ident", +] + +[[package]] +name = "quote" +version = "1.0.40" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1885c039570dc00dcb4ff087a89e185fd56bae234ddc7f056a945bf36467248d" +dependencies = [ + "proc-macro2", +] + +[[package]] +name = "r-efi" +version = "5.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "69cdb34c158ceb288df11e18b4bd39de994f6657d83847bdffdbd7f346754b0f" + +[[package]] +name = "rand" +version = "0.8.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "34af8d1a0e25924bc5b7c43c079c942339d8f0a8b57c39049bef581b46327404" +dependencies = [ + "libc", + "rand_chacha", + "rand_core", +] + +[[package]] +name = "rand_chacha" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e6c10a63a0fa32252be49d21e7709d4d4baf8d231c2dbce1eaa8141b9b127d88" +dependencies = [ + "ppv-lite86", + "rand_core", +] + +[[package]] +name = "rand_core" +version = "0.6.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ec0be4795e2f6a28069bec0b5ff3e2ac9bafc99e6a9a7dc3547996c5c816922c" +dependencies = [ + "getrandom 0.2.16", +] + +[[package]] +name = "redox_syscall" +version = "0.5.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "de3a5d9f0aba1dbcec1cc47f0ff94a4b778fe55bca98a6dfa92e4e094e57b1c4" +dependencies = [ + "bitflags", +] + +[[package]] +name = "rustc-demangle" +version = "0.1.25" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "989e6739f80c4ad5b13e0fd7fe89531180375b18520cc8c82080e4dc4035b84f" + +[[package]] +name = "rustix" +version = "1.0.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "11181fbabf243db407ef8df94a6ce0b2f9a733bd8be4ad02b4eda9602296cac8" +dependencies = [ + "bitflags", + "errno", + "libc", + "linux-raw-sys", + "windows-sys 0.60.2", +] + +[[package]] +name = "ryu" +version = "1.0.20" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "28d3b2b1366ec20994f1fd18c3c594f05c5dd4bc44d8bb0c1c632c8d6829481f" + +[[package]] +name = "schannel" +version = "0.1.27" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1f29ebaa345f945cec9fbbc532eb307f0fdad8161f281b6369539c8d84876b3d" +dependencies = [ + "windows-sys 0.59.0", +] + +[[package]] +name = "scopeguard" +version = "1.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "94143f37725109f92c262ed2cf5e59bce7498c01bcc1502d7b9afe439a4e9f49" + +[[package]] +name = "security-framework" +version = "2.11.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "897b2245f0b511c87893af39b033e5ca9cce68824c4d7e7630b5a1d339658d02" +dependencies = [ + "bitflags", + "core-foundation", + "core-foundation-sys", + "libc", + "security-framework-sys", +] + +[[package]] +name = "security-framework-sys" +version = "2.14.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "49db231d56a190491cb4aeda9527f1ad45345af50b0851622a7adb8c03b01c32" +dependencies = [ + "core-foundation-sys", + "libc", +] + +[[package]] +name = "serde" +version = "1.0.219" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5f0e2c6ed6606019b4e29e69dbaba95b11854410e5347d525002456dbbb786b6" +dependencies = [ + "serde_derive", +] + +[[package]] +name = "serde_derive" +version = "1.0.219" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5b0276cf7f2c73365f7157c8123c21cd9a50fbbd844757af28ca1f5925fc2a00" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "serde_json" +version = "1.0.141" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "30b9eff21ebe718216c6ec64e1d9ac57087aad11efc64e32002bce4a0d4c03d3" +dependencies = [ + "itoa", + "memchr", + "ryu", + "serde", +] + +[[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]] +name = "shlex" +version = "1.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0fda2ff0d084019ba4d7c6f371c95d8fd75ce3524c3cb8fb653a3023f6323e64" + +[[package]] +name = "signal-hook-registry" +version = "1.4.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9203b8055f63a2a00e2f593bb0510367fe707d7ff1e5c872de2f537b339e5410" +dependencies = [ + "libc", +] + +[[package]] +name = "slab" +version = "0.4.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "04dc19736151f35336d325007ac991178d504a119863a2fcb3758cdb5e52c50d" + +[[package]] +name = "smallvec" +version = "1.15.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "67b1b7a3b5fe4f1376887184045fcf45c69e92af734b7aaddc05fb777b6fbd03" + +[[package]] +name = "socket2" +version = "0.5.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e22376abed350d73dd1cd119b57ffccad95b4e585a7cda43e286245ce23c0678" +dependencies = [ + "libc", + "windows-sys 0.52.0", +] + +[[package]] +name = "stable_deref_trait" +version = "1.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a8f112729512f8e442d81f95a8a7ddf2b7c6b8a1a6f509a95864142b30cab2d3" + +[[package]] +name = "syn" +version = "2.0.104" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "17b6f705963418cdb9927482fa304bc562ece2fdd4f616084c50b7023b435a40" +dependencies = [ + "proc-macro2", + "quote", + "unicode-ident", +] + +[[package]] +name = "synstructure" +version = "0.13.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "728a70f3dbaf5bab7f0c4b1ac8d7ae5ea60a4b5549c8a5914361c99147a709d2" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "tempfile" +version = "3.20.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e8a64e3985349f2441a1a9ef0b853f869006c3855f2cda6862a94d26ebb9d6a1" +dependencies = [ + "fastrand", + "getrandom 0.3.3", + "once_cell", + "rustix", + "windows-sys 0.59.0", +] + +[[package]] +name = "thiserror" +version = "1.0.69" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b6aaf5339b578ea85b50e080feb250a3e8ae8cfcdff9a461c9ec2904bc923f52" +dependencies = [ + "thiserror-impl", +] + +[[package]] +name = "thiserror-impl" +version = "1.0.69" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4fee6c4efc90059e10f81e6d42c60a18f76588c3d74cb83a0b242a2b6c7504c1" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "tinystr" +version = "0.8.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5d4f6d1145dcb577acf783d4e601bc1d76a13337bb54e6233add580b07344c8b" +dependencies = [ + "displaydoc", + "zerovec", +] + +[[package]] +name = "tokio" +version = "1.46.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0cc3a2344dafbe23a245241fe8b09735b521110d30fcefbbd5feb1797ca35d17" +dependencies = [ + "backtrace", + "bytes", + "io-uring", + "libc", + "mio", + "parking_lot", + "pin-project-lite", + "signal-hook-registry", + "slab", + "socket2", + "tokio-macros", + "windows-sys 0.52.0", +] + +[[package]] +name = "tokio-macros" +version = "2.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6e06d43f1345a3bcd39f6a56dbb7dcab2ba47e68e8ac134855e7e2bdbaf8cab8" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "tokio-native-tls" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bbae76ab933c85776efabc971569dd6119c580d8f5d448769dec1764bf796ef2" +dependencies = [ + "native-tls", + "tokio", +] + +[[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", +] + +[[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", + "httparse", + "log", + "native-tls", + "rand", + "sha1", + "thiserror", + "url", + "utf-8", +] + +[[package]] +name = "typenum" +version = "1.18.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1dccffe3ce07af9386bfd29e80c0ab1a8205a2fc34e4bcd40364df902cfa8f3f" + +[[package]] +name = "unicode-ident" +version = "1.0.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5a5f39404a5da50712a4c1eecf25e90dd62b613502b7e925fd4e4d19b5c96512" + +[[package]] +name = "url" +version = "2.5.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "32f8b686cadd1473f4bd0117a5d28d36b1ade384ea9b5069a1c40aefed7fda60" +dependencies = [ + "form_urlencoded", + "idna", + "percent-encoding", +] + +[[package]] +name = "utf-8" +version = "0.7.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "09cc8ee72d2a9becf2f2febe0205bbed8fc6615b7cb429ad062dc7b7ddd036a9" + +[[package]] +name = "utf8_iter" +version = "1.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b6c140620e7ffbb22c2dee59cafe6084a59b5ffc27a8859a5f0d494b5d52b6be" + +[[package]] +name = "vcpkg" +version = "0.2.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "accd4ea62f7bb7a82fe23066fb0957d48ef677f6eeb8215f372f52e48bb32426" + +[[package]] +name = "version_check" +version = "0.9.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0b928f33d975fc6ad9f86c8f283853ad26bdd5b10b7f1542aa2fa15e2289105a" + +[[package]] +name = "wasi" +version = "0.11.1+wasi-snapshot-preview1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ccf3ec651a847eb01de73ccad15eb7d99f80485de043efb2f370cd654f4ea44b" + +[[package]] +name = "wasi" +version = "0.14.2+wasi-0.2.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9683f9a5a998d873c0d21fcbe3c083009670149a8fab228644b8bd36b2c48cb3" +dependencies = [ + "wit-bindgen-rt", +] + +[[package]] +name = "windows-sys" +version = "0.52.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "282be5f36a8ce781fad8c8ae18fa3f9beff57ec1b52cb3de0789201425d9a33d" +dependencies = [ + "windows-targets 0.52.6", +] + +[[package]] +name = "windows-sys" +version = "0.59.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1e38bc4d79ed67fd075bcc251a1c39b32a1776bbe92e5bef1f0bf1f8c531853b" +dependencies = [ + "windows-targets 0.52.6", +] + +[[package]] +name = "windows-sys" +version = "0.60.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f2f500e4d28234f72040990ec9d39e3a6b950f9f22d3dba18416c35882612bcb" +dependencies = [ + "windows-targets 0.53.2", +] + +[[package]] +name = "windows-targets" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9b724f72796e036ab90c1021d4780d4d3d648aca59e491e6b98e725b84e99973" +dependencies = [ + "windows_aarch64_gnullvm 0.52.6", + "windows_aarch64_msvc 0.52.6", + "windows_i686_gnu 0.52.6", + "windows_i686_gnullvm 0.52.6", + "windows_i686_msvc 0.52.6", + "windows_x86_64_gnu 0.52.6", + "windows_x86_64_gnullvm 0.52.6", + "windows_x86_64_msvc 0.52.6", +] + +[[package]] +name = "windows-targets" +version = "0.53.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c66f69fcc9ce11da9966ddb31a40968cad001c5bedeb5c2b82ede4253ab48aef" +dependencies = [ + "windows_aarch64_gnullvm 0.53.0", + "windows_aarch64_msvc 0.53.0", + "windows_i686_gnu 0.53.0", + "windows_i686_gnullvm 0.53.0", + "windows_i686_msvc 0.53.0", + "windows_x86_64_gnu 0.53.0", + "windows_x86_64_gnullvm 0.53.0", + "windows_x86_64_msvc 0.53.0", +] + +[[package]] +name = "windows_aarch64_gnullvm" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "32a4622180e7a0ec044bb555404c800bc9fd9ec262ec147edd5989ccd0c02cd3" + +[[package]] +name = "windows_aarch64_gnullvm" +version = "0.53.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "86b8d5f90ddd19cb4a147a5fa63ca848db3df085e25fee3cc10b39b6eebae764" + +[[package]] +name = "windows_aarch64_msvc" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "09ec2a7bb152e2252b53fa7803150007879548bc709c039df7627cabbd05d469" + +[[package]] +name = "windows_aarch64_msvc" +version = "0.53.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c7651a1f62a11b8cbd5e0d42526e55f2c99886c77e007179efff86c2b137e66c" + +[[package]] +name = "windows_i686_gnu" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8e9b5ad5ab802e97eb8e295ac6720e509ee4c243f69d781394014ebfe8bbfa0b" + +[[package]] +name = "windows_i686_gnu" +version = "0.53.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c1dc67659d35f387f5f6c479dc4e28f1d4bb90ddd1a5d3da2e5d97b42d6272c3" + +[[package]] +name = "windows_i686_gnullvm" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0eee52d38c090b3caa76c563b86c3a4bd71ef1a819287c19d586d7334ae8ed66" + +[[package]] +name = "windows_i686_gnullvm" +version = "0.53.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9ce6ccbdedbf6d6354471319e781c0dfef054c81fbc7cf83f338a4296c0cae11" + +[[package]] +name = "windows_i686_msvc" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "240948bc05c5e7c6dabba28bf89d89ffce3e303022809e73deaefe4f6ec56c66" + +[[package]] +name = "windows_i686_msvc" +version = "0.53.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "581fee95406bb13382d2f65cd4a908ca7b1e4c2f1917f143ba16efe98a589b5d" + +[[package]] +name = "windows_x86_64_gnu" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "147a5c80aabfbf0c7d901cb5895d1de30ef2907eb21fbbab29ca94c5b08b1a78" + +[[package]] +name = "windows_x86_64_gnu" +version = "0.53.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2e55b5ac9ea33f2fc1716d1742db15574fd6fc8dadc51caab1c16a3d3b4190ba" + +[[package]] +name = "windows_x86_64_gnullvm" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "24d5b23dc417412679681396f2b49f3de8c1473deb516bd34410872eff51ed0d" + +[[package]] +name = "windows_x86_64_gnullvm" +version = "0.53.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0a6e035dd0599267ce1ee132e51c27dd29437f63325753051e71dd9e42406c57" + +[[package]] +name = "windows_x86_64_msvc" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "589f6da84c646204747d1270a2a5661ea66ed1cced2631d546fdfb155959f9ec" + +[[package]] +name = "windows_x86_64_msvc" +version = "0.53.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "271414315aff87387382ec3d271b52d7ae78726f5d44ac98b4f4030c91880486" + +[[package]] +name = "wit-bindgen-rt" +version = "0.39.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6f42320e61fe2cfd34354ecb597f86f413484a798ba44a8ca1165c58d42da6c1" +dependencies = [ + "bitflags", +] + +[[package]] +name = "writeable" +version = "0.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ea2f10b9bb0928dfb1b42b65e1f9e36f7f54dbdf08457afefb38afcdec4fa2bb" + +[[package]] +name = "yoke" +version = "0.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5f41bb01b8226ef4bfd589436a297c53d118f65921786300e427be8d487695cc" +dependencies = [ + "serde", + "stable_deref_trait", + "yoke-derive", + "zerofrom", +] + +[[package]] +name = "yoke-derive" +version = "0.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "38da3c9736e16c5d3c8c597a9aaa5d1fa565d0532ae05e27c24aa62fb32c0ab6" +dependencies = [ + "proc-macro2", + "quote", + "syn", + "synstructure", +] + +[[package]] +name = "zerocopy" +version = "0.8.26" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1039dd0d3c310cf05de012d8a39ff557cb0d23087fd44cad61df08fc31907a2f" +dependencies = [ + "zerocopy-derive", +] + +[[package]] +name = "zerocopy-derive" +version = "0.8.26" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9ecf5b4cc5364572d7f4c329661bcc82724222973f2cab6f050a4e5c22f75181" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "zerofrom" +version = "0.1.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "50cc42e0333e05660c3587f3bf9d0478688e15d870fab3346451ce7f8c9fbea5" +dependencies = [ + "zerofrom-derive", +] + +[[package]] +name = "zerofrom-derive" +version = "0.1.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d71e5d6e06ab090c67b5e44993ec16b72dcbaabc526db883a360057678b48502" +dependencies = [ + "proc-macro2", + "quote", + "syn", + "synstructure", +] + +[[package]] +name = "zerotrie" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "36f0bbd478583f79edad978b407914f61b2972f5af6fa089686016be8f9af595" +dependencies = [ + "displaydoc", + "yoke", + "zerofrom", +] + +[[package]] +name = "zerovec" +version = "0.11.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4a05eb080e015ba39cc9e23bbe5e7fb04d5fb040350f99f34e338d5fdd294428" +dependencies = [ + "yoke", + "zerofrom", + "zerovec-derive", +] + +[[package]] +name = "zerovec-derive" +version = "0.11.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5b96237efa0c878c64bd89c436f661be4e46b2f3eff1ebb976f7ef2321d2f58f" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] diff --git a/crates/ais/Cargo.toml b/crates/ais/Cargo.toml new file mode 100644 index 0000000..f6dbf41 --- /dev/null +++ b/crates/ais/Cargo.toml @@ -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" \ No newline at end of file diff --git a/crates/ais/error.md b/crates/ais/error.md new file mode 100644 index 0000000..6281bd6 --- /dev/null +++ b/crates/ais/error.md @@ -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::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::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::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::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::force +at /Users/williamseemueller/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/once_cell-1.21.3/src/lib.rs:774:13 +10: 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::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::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::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::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::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::force +at /Users/williamseemueller/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/once_cell-1.21.3/src/lib.rs:774:13 +19: 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::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::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::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::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::force +at /Users/williamseemueller/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/once_cell-1.21.3/src/lib.rs:774:13 +28: 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 + + diff --git a/crates/ais/src/ais.rs b/crates/ais/src/ais.rs new file mode 100644 index 0000000..c6032b8 --- /dev/null +++ b/crates/ais/src/ais.rs @@ -0,0 +1,1082 @@ +use tokio_tungstenite::{connect_async, tungstenite::protocol::Message}; +use serde::{Deserialize, Serialize}; +use serde_json::Value; +use futures_util::{SinkExt, StreamExt}; +use axum::extract::ws::{WebSocket, Message as WsMessage}; +use url::Url; +use axum::{ + extract::{Query, WebSocketUpgrade, State}, + http::StatusCode, + response::{Json, Response}, + routing::get, + Router, +}; +use std::sync::Arc; +use tokio::sync::{broadcast, Mutex}; +use tower_http::cors::CorsLayer; +use base64::{engine::general_purpose::STANDARD, Engine as _}; + +#[derive(Serialize, Deserialize, Debug)] +struct SubscriptionMessage { + #[serde(rename = "Apikey")] + apikey: String, + #[serde(rename = "BoundingBoxes")] + bounding_boxes: Vec>, + #[serde(rename = "FiltersShipMMSI")] + filters_ship_mmsi: Vec, + // Uncomment and add if needed: + // #[serde(rename = "FilterMessageTypes")] + // filter_message_types: Vec, +} + +#[derive(Deserialize, Debug)] +struct BoundingBoxQuery { + sw_lat: f64, // Southwest latitude + sw_lon: f64, // Southwest longitude + ne_lat: f64, // Northeast latitude + ne_lon: f64, // Northeast longitude +} + +#[derive(Serialize, Deserialize, Debug, Clone)] +struct WebSocketBoundingBox { + sw_lat: f64, // Southwest latitude + sw_lon: f64, // Southwest longitude + ne_lat: f64, // Northeast latitude + ne_lon: f64, // Northeast longitude +} + +#[derive(Serialize, Deserialize, Debug)] +struct WebSocketMessage { + #[serde(rename = "type")] + message_type: String, + #[serde(skip_serializing_if = "Option::is_none")] + bounding_box: Option, +} + +#[derive(Serialize, Deserialize, Clone, Debug)] +struct AisResponse { + message_type: Option, + mmsi: Option, + ship_name: Option, + latitude: Option, + longitude: Option, + timestamp: Option, + speed_over_ground: Option, + course_over_ground: Option, + heading: Option, + navigation_status: Option, + ship_type: Option, + raw_message: Value, +} + +// Shared state for the application +#[derive(Clone)] +struct AppState { + ais_sender: Arc>>>, + ais_stream_started: Arc>, +} + +// Convert raw AIS message to structured response +fn parse_ais_message(ais_message: &Value) -> AisResponse { + let message_type = ais_message.get("MessageType") + .and_then(|v| v.as_str()) + .map(|s| s.to_string()); + + let metadata = ais_message.get("MetaData"); + let mmsi = metadata + .and_then(|m| m.get("MMSI")) + .and_then(|v| v.as_str()) + .map(|s| s.to_string()); + + let ship_name = metadata + .and_then(|m| m.get("ShipName")) + .and_then(|v| v.as_str()) + .map(|s| s.trim().to_string()); + + let latitude = metadata + .and_then(|m| m.get("latitude")) + .and_then(|v| v.as_f64()); + + let longitude = metadata + .and_then(|m| m.get("longitude")) + .and_then(|v| v.as_f64()); + + let timestamp = metadata + .and_then(|m| m.get("time_utc")) + .and_then(|v| v.as_str()) + .map(|s| s.to_string()); + + // Extract position report data + let message = ais_message.get("Message"); + let pos_report = message.and_then(|m| m.get("PositionReport")); + + let speed_over_ground = pos_report + .and_then(|pr| pr.get("Sog")) + .and_then(|v| v.as_f64()); + + let course_over_ground = pos_report + .and_then(|pr| pr.get("Cog")) + .and_then(|v| v.as_f64()); + + let heading = pos_report + .and_then(|pr| pr.get("TrueHeading")) + .and_then(|v| v.as_f64()); + + let navigation_status = pos_report + .and_then(|pr| pr.get("NavigationalStatus")) + .and_then(|v| v.as_u64()) + .map(|status| match status { + 0 => "Under way using engine", + 1 => "At anchor", + 2 => "Not under command", + 3 => "Restricted manoeuvrability", + 4 => "Constrained by her draught", + 5 => "Moored", + 6 => "Aground", + 7 => "Engaged in fishing", + 8 => "Under way sailing", + _ => "Other" + }.to_string()); + + // Extract ship type from static data + let ship_type = message + .and_then(|m| m.get("StaticDataReport")) + .and_then(|sdr| sdr.get("ReportB")) + .and_then(|rb| rb.get("ShipType")) + .and_then(|v| v.as_u64()) + .map(|st| get_ship_type_description(st).to_string()); + + AisResponse { + message_type, + mmsi, + ship_name, + latitude, + longitude, + timestamp, + speed_over_ground, + course_over_ground, + heading, + navigation_status, + ship_type, + raw_message: ais_message.clone(), + } +} + +// HTTP endpoint to get AIS data for a bounding box +async fn get_ais_data( + Query(params): Query, + axum::extract::State(_state): axum::extract::State, +) -> Result>, StatusCode> { + println!("Received bounding box request: {:?}", params); + + // For now, return a simple response indicating the bounding box was received + // In a full implementation, you might want to: + // 1. Store recent AIS data in memory/database + // 2. Filter by the bounding box + // 3. Return the filtered results + + let response = vec![AisResponse { + message_type: Some("Info".to_string()), + mmsi: None, + ship_name: Some("Bounding Box Query Received".to_string()), + latitude: Some((params.sw_lat + params.ne_lat) / 2.0), + longitude: Some((params.sw_lon + params.ne_lon) / 2.0), + timestamp: Some("Query processed".to_string()), + speed_over_ground: None, + course_over_ground: None, + heading: None, + navigation_status: Some("Query processed".to_string()), + ship_type: None, + raw_message: serde_json::json!({ + "bounding_box": { + "sw_lat": params.sw_lat, + "sw_lon": params.sw_lon, + "ne_lat": params.ne_lat, + "ne_lon": params.ne_lon + } + }), + }]; + + Ok(Json(response)) +} + +// WebSocket handler for real-time AIS data streaming +async fn websocket_handler( + ws: WebSocketUpgrade, + State(state): State, +) -> Response { + ws.on_upgrade(|socket| handle_websocket(socket, state)) +} + +// Function to check if AIS data is within bounding box +fn is_within_bounding_box(ais_data: &AisResponse, bbox: &WebSocketBoundingBox) -> bool { + if let (Some(lat), Some(lon)) = (ais_data.latitude, ais_data.longitude) { + lat >= bbox.sw_lat && lat <= bbox.ne_lat && + lon >= bbox.sw_lon && lon <= bbox.ne_lon + } else { + false // If no coordinates, don't include + } +} + +// Handle individual WebSocket connections +async fn handle_websocket(mut socket: WebSocket, state: AppState) { + // Get a receiver from the broadcast channel + let sender_guard = state.ais_sender.lock().await; + let mut receiver = match sender_guard.as_ref() { + Some(sender) => sender.subscribe(), + None => { + println!("No AIS sender available"); + let _ = socket.close().await; + return; + } + }; + drop(sender_guard); + + println!("WebSocket client connected"); + + // Store bounding box state for this connection + let mut bounding_box: Option = None; + + // Send initial connection confirmation + if socket.send(WsMessage::Text("Connected to AIS stream".to_string())).await.is_err() { + println!("Failed to send connection confirmation"); + return; + } + + // Handle incoming messages and broadcast AIS data + loop { + tokio::select! { + // Handle incoming WebSocket messages (for potential client commands) + msg = socket.recv() => { + match msg { + Some(Ok(WsMessage::Text(text))) => { + println!("Received from client: {}", text); + + // Try to parse as WebSocket message for bounding box configuration + match serde_json::from_str::(&text) { + Ok(ws_msg) => { + match ws_msg.message_type.as_str() { + "set_bounding_box" => { + if let Some(bbox) = ws_msg.bounding_box { + println!("Setting bounding box: {:?}", bbox); + bounding_box = Some(bbox.clone()); + + // Send confirmation + let confirmation = serde_json::json!({ + "type": "bounding_box_set", + "bounding_box": bbox + }); + if socket.send(WsMessage::Text(confirmation.to_string())).await.is_err() { + break; + } + } else { + // Clear bounding box if none provided + bounding_box = None; + let confirmation = serde_json::json!({ + "type": "bounding_box_cleared" + }); + if socket.send(WsMessage::Text(confirmation.to_string())).await.is_err() { + break; + } + } + } + "start_ais_stream" => { + println!("Received request to start AIS stream"); + + // Check if AIS stream is already started + let mut stream_started = state.ais_stream_started.lock().await; + if !*stream_started { + *stream_started = true; + drop(stream_started); + + // Start AIS stream connection in background + let ais_state = state.clone(); + tokio::spawn(async move { + if let Err(e) = connect_to_ais_stream_with_broadcast(ais_state).await { + eprintln!("WebSocket error: {:?}", e); + } + }); + + // Send confirmation + let confirmation = serde_json::json!({ + "type": "ais_stream_started" + }); + if socket.send(WsMessage::Text(confirmation.to_string())).await.is_err() { + break; + } + println!("AIS stream started successfully"); + } else { + // AIS stream already started + let confirmation = serde_json::json!({ + "type": "ais_stream_already_started" + }); + if socket.send(WsMessage::Text(confirmation.to_string())).await.is_err() { + break; + } + println!("AIS stream already started"); + } + } + _ => { + // Echo back unknown message types + if socket.send(WsMessage::Text(format!("Echo: {}", text))).await.is_err() { + break; + } + } + } + } + Err(_) => { + // If not valid JSON, echo back as before + if socket.send(WsMessage::Text(format!("Echo: {}", text))).await.is_err() { + break; + } + } + } + } + Some(Ok(WsMessage::Close(_))) => { + println!("WebSocket client disconnected"); + break; + } + Some(Err(e)) => { + println!("WebSocket error: {:?}", e); + break; + } + None => break, + _ => {} // Handle other message types if needed + } + } + // Forward AIS data to the client + ais_data = receiver.recv() => { + match ais_data { + Ok(data) => { + // Apply bounding box filtering if configured + let should_send = match &bounding_box { + Some(bbox) => is_within_bounding_box(&data, bbox), + None => true, // Send all data if no bounding box is set + }; + + if should_send { + match serde_json::to_string(&data) { + Ok(json_data) => { + if socket.send(WsMessage::Text(json_data)).await.is_err() { + println!("Failed to send AIS data to client"); + break; + } + } + Err(e) => { + println!("Failed to serialize AIS data: {:?}", e); + } + } + } + } + Err(broadcast::error::RecvError::Lagged(n)) => { + println!("WebSocket client lagged behind by {} messages", n); + // Continue receiving, client will catch up + } + Err(broadcast::error::RecvError::Closed) => { + println!("AIS broadcast channel closed"); + break; + } + } + } + } + } + + println!("WebSocket connection closed"); +} + +// Create the Axum router +fn create_router(state: AppState) -> Router { + Router::new() + .route("/ais", get(get_ais_data)) + .route("/ws", get(websocket_handler)) + .layer(CorsLayer::permissive()) + .with_state(state) +} + +fn print_detailed_ais_message(ais_message: &Value) { + println!("\n=== AIS MESSAGE DETAILS ==="); + + // Print message type + if let Some(msg_type) = ais_message.get("MessageType") { + println!("Message Type: {}", msg_type); + } + + // Print metadata information + if let Some(metadata) = ais_message.get("MetaData") { + if let Some(timestamp) = metadata.get("time_utc") { + println!("Timestamp: {}", timestamp); + } + + if let Some(mmsi) = metadata.get("MMSI") { + println!("MMSI: {}", mmsi); + } + + if let Some(ship_name) = metadata.get("ShipName") { + println!("Ship Name: {}", ship_name.as_str().unwrap_or("N/A").trim()); + } + + if let Some(lat) = metadata.get("latitude") { + println!("Latitude: {}", lat); + } + + if let Some(lon) = metadata.get("longitude") { + println!("Longitude: {}", lon); + } + } + + // Parse message content based on type + if let Some(message) = ais_message.get("Message") { + // Handle Position Report messages + if let Some(pos_report) = message.get("PositionReport") { + println!("\n--- Position Report Details ---"); + + if let Some(sog) = pos_report.get("Sog") { + println!("Speed Over Ground: {} knots", sog); + } + + if let Some(cog) = pos_report.get("Cog") { + println!("Course Over Ground: {}°", cog); + } + + if let Some(heading) = pos_report.get("TrueHeading") { + println!("True Heading: {}°", heading); + } + + if let Some(nav_status) = pos_report.get("NavigationalStatus") { + let status_text = match nav_status.as_u64().unwrap_or(15) { + 0 => "Under way using engine", + 1 => "At anchor", + 2 => "Not under command", + 3 => "Restricted manoeuvrability", + 4 => "Constrained by her draught", + 5 => "Moored", + 6 => "Aground", + 7 => "Engaged in fishing", + 8 => "Under way sailing", + 9 => "Reserved for HSC", + 10 => "Reserved for WIG", + 11 => "Power-driven vessel towing astern", + 12 => "Power-driven vessel pushing ahead", + 13 => "Reserved for future use", + 14 => "AIS-SART, MOB-AIS, EPIRB-AIS", + _ => "Not defined (default)" + }; + println!("Navigation Status: {} ({})", nav_status, status_text); + } + + if let Some(rot) = pos_report.get("RateOfTurn") { + if rot.as_i64().unwrap_or(127) != 127 { + println!("Rate of Turn: {}°/min", rot); + } else { + println!("Rate of Turn: Not available"); + } + } + + if let Some(accuracy) = pos_report.get("PositionAccuracy") { + println!("Position Accuracy: {}", if accuracy.as_bool().unwrap_or(false) { "High (< 10m)" } else { "Low (> 10m)" }); + } + + if let Some(raim) = pos_report.get("Raim") { + println!("RAIM: {}", if raim.as_bool().unwrap_or(false) { "In use" } else { "Not in use" }); + } + } + + // Handle Static Data Report messages + if let Some(static_report) = message.get("StaticDataReport") { + println!("\n--- Static Data Report Details ---"); + + if let Some(report_a) = static_report.get("ReportA") { + if let Some(name) = report_a.get("Name") { + println!("Vessel Name: {}", name.as_str().unwrap_or("N/A").trim()); + } + } + + if let Some(report_b) = static_report.get("ReportB") { + if let Some(call_sign) = report_b.get("CallSign") { + let call_sign_str = call_sign.as_str().unwrap_or("").trim(); + if !call_sign_str.is_empty() { + println!("Call Sign: {}", call_sign_str); + } + } + + if let Some(ship_type) = report_b.get("ShipType") { + let ship_type_num = ship_type.as_u64().unwrap_or(0); + if ship_type_num > 0 { + println!("Ship Type: {} ({})", ship_type_num, get_ship_type_description(ship_type_num)); + } + } + + if let Some(dimension) = report_b.get("Dimension") { + let a = dimension.get("A").and_then(|v| v.as_u64()).unwrap_or(0); + let b = dimension.get("B").and_then(|v| v.as_u64()).unwrap_or(0); + let c = dimension.get("C").and_then(|v| v.as_u64()).unwrap_or(0); + let d = dimension.get("D").and_then(|v| v.as_u64()).unwrap_or(0); + + if a > 0 || b > 0 || c > 0 || d > 0 { + println!("Dimensions: Length {}m ({}m to bow, {}m to stern), Width {}m ({}m to port, {}m to starboard)", + a + b, a, b, c + d, c, d); + } + } + } + } + + // Handle Voyage Data messages + if let Some(voyage_data) = message.get("VoyageData") { + println!("\n--- Voyage Data Details ---"); + + if let Some(destination) = voyage_data.get("Destination") { + println!("Destination: {}", destination.as_str().unwrap_or("N/A").trim()); + } + + if let Some(eta) = voyage_data.get("Eta") { + println!("ETA: {:?}", eta); + } + + if let Some(draught) = voyage_data.get("MaximumStaticDraught") { + println!("Maximum Draught: {} meters", draught); + } + } + } + + // Print raw message for debugging + println!("\nRaw JSON: {}", ais_message); + println!("========================\n"); +} + +fn get_ship_type_description(ship_type: u64) -> &'static str { + match ship_type { + 20..=29 => "Wing in ground (WIG)", + 30 => "Fishing", + 31 => "Towing", + 32 => "Towing: length exceeds 200m or breadth exceeds 25m", + 33 => "Dredging or underwater ops", + 34 => "Diving ops", + 35 => "Military ops", + 36 => "Sailing", + 37 => "Pleasure Craft", + 40..=49 => "High speed craft (HSC)", + 50 => "Pilot Vessel", + 51 => "Search and Rescue vessel", + 52 => "Tug", + 53 => "Port Tender", + 54 => "Anti-pollution equipment", + 55 => "Law Enforcement", + 58 => "Medical Transport", + 59 => "Noncombatant ship according to RR Resolution No. 18", + 60..=69 => "Passenger", + 70..=79 => "Cargo", + 80..=89 => "Tanker", + 90..=99 => "Other Type", + _ => "Unknown" + } +} + +// Start the HTTP server with AIS functionality +pub async fn start_ais_server() -> Result<(), Box> { + // Create broadcast channel for AIS data + let (tx, _rx) = broadcast::channel::(1000); + + // Create shared state + let state = AppState { + ais_sender: Arc::new(Mutex::new(Some(tx.clone()))), + ais_stream_started: Arc::new(Mutex::new(false)), + }; + + // Don't start AIS WebSocket connection immediately + // It will be started when the frontend signals that user location is loaded and map is focused + + // Create and start 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).await?; + Ok(()) +} + + +// Modified AIS stream function that broadcasts data and accepts dynamic bounding boxes +async fn connect_to_ais_stream_with_broadcast(state: AppState) -> Result<(), Box> { + // Connect to WebSocket + let url = Url::parse("wss://stream.aisstream.io/v0/stream")?; + let (ws_stream, _) = connect_async(url).await?; + println!("WebSocket connection opened for broadcast"); + + let (mut sender, mut receiver) = ws_stream.split(); + + let key = "MDc4YzY5NTdkMGUwM2UzMzQ1Zjc5NDFmOTA1ODg4ZTMyOGQ0MjM0MA=="; + // Create subscription message with default bounding box (Port of Los Angeles area) + // In a full implementation, this could be made dynamic based on active HTTP requests + let subscription_message = SubscriptionMessage { + apikey: STANDARD.decode(key) + .ok() + .and_then(|bytes| String::from_utf8(bytes).ok()) + .unwrap_or_default(), + bounding_boxes: vec![vec![ + [33.6, -118.5], // Southwest corner (lat, lon) + [33.9, -118.0] // Northeast corner (lat, lon) + ]], + filters_ship_mmsi: vec![], // Remove specific MMSI filters to get all ships in the area + }; + + // Send subscription message + let message_json = serde_json::to_string(&subscription_message)?; + sender.send(Message::Text(message_json)).await?; + println!("Subscription message sent for broadcast"); + + // Listen for messages and broadcast them + while let Some(message) = receiver.next().await { + match message? { + Message::Text(text) => { + match serde_json::from_str::(&text) { + Ok(ais_message) => { + // Parse and broadcast the message + let parsed_message = parse_ais_message(&ais_message); + + // Try to broadcast to HTTP clients + let sender_guard = state.ais_sender.lock().await; + if let Some(ref broadcaster) = *sender_guard { + let _ = broadcaster.send(parsed_message.clone()); + } + + // Still print detailed message for debugging + print_detailed_ais_message(&ais_message); + } + Err(e) => { + eprintln!("Failed to parse JSON: {:?}", e); + } + } + } + Message::Binary(data) => { + println!("Received binary data: {} bytes", data.len()); + + // Try to decode as UTF-8 string to see if it's JSON + if let Ok(text) = String::from_utf8(data.clone()) { + match serde_json::from_str::(&text) { + Ok(ais_message) => { + let parsed_message = parse_ais_message(&ais_message); + + // Try to broadcast to HTTP clients + let sender_guard = state.ais_sender.lock().await; + if let Some(ref broadcaster) = *sender_guard { + let _ = broadcaster.send(parsed_message.clone()); + } + + print_detailed_ais_message(&ais_message); + } + Err(e) => { + println!("Binary data is not valid JSON: {:?}", e); + } + } + } + } + _ => { + // Handle other message types like Close, Ping, Pong + } + } + } + + println!("WebSocket connection closed"); + Ok(()) +} + + +#[cfg(test)] +mod tests { + use super::*; + use axum_test::TestServer; + use serde_json::json; + use tokio::sync::broadcast; + + #[test] + fn test_get_ship_type_description() { + assert_eq!(get_ship_type_description(30), "Fishing"); + assert_eq!(get_ship_type_description(31), "Towing"); + assert_eq!(get_ship_type_description(36), "Sailing"); + assert_eq!(get_ship_type_description(37), "Pleasure Craft"); + assert_eq!(get_ship_type_description(60), "Passenger"); + assert_eq!(get_ship_type_description(70), "Cargo"); + assert_eq!(get_ship_type_description(80), "Tanker"); + assert_eq!(get_ship_type_description(999), "Unknown"); + } + + #[test] + fn test_parse_ais_message_position_report() { + let ais_message = json!({ + "MessageType": "PositionReport", + "MetaData": { + "MMSI": "123456789", + "ShipName": "TEST VESSEL", + "latitude": 33.7, + "longitude": -118.3, + "time_utc": "2023-01-01T12:00:00Z" + }, + "Message": { + "PositionReport": { + "Sog": 12.5, + "Cog": 180.0, + "TrueHeading": 175.0, + "NavigationalStatus": 0 + } + } + }); + + let result = parse_ais_message(&ais_message); + + assert_eq!(result.message_type, Some("PositionReport".to_string())); + assert_eq!(result.mmsi, Some("123456789".to_string())); + assert_eq!(result.ship_name, Some("TEST VESSEL".to_string())); + assert_eq!(result.latitude, Some(33.7)); + assert_eq!(result.longitude, Some(-118.3)); + assert_eq!(result.timestamp, Some("2023-01-01T12:00:00Z".to_string())); + assert_eq!(result.speed_over_ground, Some(12.5)); + assert_eq!(result.course_over_ground, Some(180.0)); + assert_eq!(result.heading, Some(175.0)); + assert_eq!(result.navigation_status, Some("Under way using engine".to_string())); + } + + #[test] + fn test_parse_ais_message_static_data() { + let ais_message = json!({ + "MessageType": "StaticDataReport", + "MetaData": { + "MMSI": "987654321", + "ShipName": "CARGO SHIP", + "latitude": 34.0, + "longitude": -118.0, + "time_utc": "2023-01-01T13:00:00Z" + }, + "Message": { + "StaticDataReport": { + "ReportB": { + "ShipType": 70 + } + } + } + }); + + let result = parse_ais_message(&ais_message); + + assert_eq!(result.message_type, Some("StaticDataReport".to_string())); + assert_eq!(result.mmsi, Some("987654321".to_string())); + assert_eq!(result.ship_name, Some("CARGO SHIP".to_string())); + assert_eq!(result.ship_type, Some("Cargo".to_string())); + } + + #[test] + fn test_parse_ais_message_empty() { + let ais_message = json!({}); + let result = parse_ais_message(&ais_message); + + assert_eq!(result.message_type, None); + assert_eq!(result.mmsi, None); + assert_eq!(result.ship_name, None); + assert_eq!(result.latitude, None); + assert_eq!(result.longitude, None); + } + + #[tokio::test] + async fn test_get_ais_data_endpoint() { + // Create test state + let (tx, _rx) = broadcast::channel::(100); + let state = AppState { + ais_sender: Arc::new(Mutex::new(Some(tx))), + ais_stream_started: Arc::new(Mutex::new(false)), + }; + + // Create test server + let app = create_router(state); + let server = TestServer::new(app).unwrap(); + + // Test valid bounding box request + let response = server + .get("/ais") + .add_query_param("sw_lat", "33.6") + .add_query_param("sw_lon", "-118.5") + .add_query_param("ne_lat", "33.9") + .add_query_param("ne_lon", "-118.0") + .await; + + response.assert_status_ok(); + + let json_response: Vec = response.json(); + assert_eq!(json_response.len(), 1); + assert_eq!(json_response[0].ship_name, Some("Bounding Box Query Received".to_string())); + assert_eq!(json_response[0].latitude, Some(33.75)); // Average of sw_lat and ne_lat + assert_eq!(json_response[0].longitude, Some(-118.25)); // Average of sw_lon and ne_lon + } + + #[tokio::test] + async fn test_get_ais_data_endpoint_missing_params() { + // Create test state + let (tx, _rx) = broadcast::channel::(100); + let state = AppState { + ais_sender: Arc::new(Mutex::new(Some(tx))), + ais_stream_started: Arc::new(Mutex::new(false)), + }; + + // Create test server + let app = create_router(state); + let server = TestServer::new(app).unwrap(); + + // Test request with missing parameters + let response = server + .get("/ais") + .add_query_param("sw_lat", "33.6") + .add_query_param("sw_lon", "-118.5") + // Missing ne_lat and ne_lon + .await; + + response.assert_status_bad_request(); + } + + #[tokio::test] + async fn test_get_ais_data_endpoint_invalid_params() { + // Create test state + let (tx, _rx) = broadcast::channel::(100); + let state = AppState { + ais_sender: Arc::new(Mutex::new(Some(tx))), + ais_stream_started: Arc::new(Mutex::new(false)), + }; + + // Create test server + let app = create_router(state); + let server = TestServer::new(app).unwrap(); + + // Test request with invalid parameter types + let response = server + .get("/ais") + .add_query_param("sw_lat", "invalid") + .add_query_param("sw_lon", "-118.5") + .add_query_param("ne_lat", "33.9") + .add_query_param("ne_lon", "-118.0") + .await; + + response.assert_status_bad_request(); + } + + #[test] + fn test_bounding_box_query_validation() { + // Test valid bounding box + let valid_query = BoundingBoxQuery { + sw_lat: 33.6, + sw_lon: -118.5, + ne_lat: 33.9, + ne_lon: -118.0, + }; + + // Basic validation - northeast should be greater than southwest + assert!(valid_query.ne_lat > valid_query.sw_lat); + assert!(valid_query.ne_lon > valid_query.sw_lon); + } + + #[test] + fn test_ais_response_serialization() { + let response = AisResponse { + message_type: Some("PositionReport".to_string()), + mmsi: Some("123456789".to_string()), + ship_name: Some("Test Ship".to_string()), + latitude: Some(33.7), + longitude: Some(-118.3), + timestamp: Some("2023-01-01T12:00:00Z".to_string()), + speed_over_ground: Some(10.5), + course_over_ground: Some(90.0), + heading: Some(85.0), + navigation_status: Some("Under way using engine".to_string()), + ship_type: Some("Cargo".to_string()), + raw_message: json!({"test": "data"}), + }; + + // Test that the response can be serialized to JSON + let json_result = serde_json::to_string(&response); + assert!(json_result.is_ok()); + + let json_string = json_result.unwrap(); + assert!(json_string.contains("PositionReport")); + assert!(json_string.contains("123456789")); + assert!(json_string.contains("Test Ship")); + } + + #[tokio::test] + async fn test_app_state_creation() { + let (tx, _rx) = broadcast::channel::(100); + let state = AppState { + ais_sender: Arc::new(Mutex::new(Some(tx.clone()))), + ais_stream_started: Arc::new(Mutex::new(false)), + }; + + // Test that we can access the sender + let sender_guard = state.ais_sender.lock().await; + assert!(sender_guard.is_some()); + } + + #[test] + fn test_subscription_message_serialization() { + let subscription = SubscriptionMessage { + apikey: "test_key".to_string(), + bounding_boxes: vec![vec![ + [33.6, -118.5], + [33.9, -118.0] + ]], + filters_ship_mmsi: vec!["123456789".to_string()], + }; + + let json_result = serde_json::to_string(&subscription); + assert!(json_result.is_ok()); + + let json_string = json_result.unwrap(); + assert!(json_string.contains("Apikey")); + assert!(json_string.contains("BoundingBoxes")); + assert!(json_string.contains("FiltersShipMMSI")); + } + + #[tokio::test] + async fn test_websocket_endpoint_exists() { + // Create test state + let (tx, _rx) = broadcast::channel::(100); + let state = AppState { + ais_sender: Arc::new(Mutex::new(Some(tx))), + ais_stream_started: Arc::new(Mutex::new(false)), + }; + + // Create test server + let app = create_router(state); + let server = TestServer::new(app).unwrap(); + + // Test that the websocket endpoint exists and returns appropriate response + // Note: axum-test doesn't support websocket upgrades, but we can test that the route exists + let response = server.get("/ws").await; + + // The websocket endpoint should return a 400 Bad Request status + // when accessed via HTTP GET without proper websocket headers + response.assert_status(axum::http::StatusCode::BAD_REQUEST); + } + + #[test] + fn test_is_within_bounding_box() { + let bbox = WebSocketBoundingBox { + sw_lat: 33.0, + sw_lon: -119.0, + ne_lat: 34.0, + ne_lon: -118.0, + }; + + // Test point within bounding box + let ais_within = AisResponse { + message_type: Some("PositionReport".to_string()), + mmsi: Some("123456789".to_string()), + ship_name: Some("Test Ship".to_string()), + latitude: Some(33.5), + longitude: Some(-118.5), + timestamp: Some("2023-01-01T12:00:00Z".to_string()), + speed_over_ground: Some(10.0), + course_over_ground: Some(90.0), + heading: Some(85.0), + navigation_status: Some("Under way using engine".to_string()), + ship_type: Some("Cargo".to_string()), + raw_message: serde_json::json!({"test": "data"}), + }; + + assert!(is_within_bounding_box(&ais_within, &bbox)); + + // Test point outside bounding box (latitude too high) + let ais_outside_lat = AisResponse { + latitude: Some(35.0), + longitude: Some(-118.5), + ..ais_within.clone() + }; + + assert!(!is_within_bounding_box(&ais_outside_lat, &bbox)); + + // Test point outside bounding box (longitude too low) + let ais_outside_lon = AisResponse { + latitude: Some(33.5), + longitude: Some(-120.0), + ..ais_within.clone() + }; + + assert!(!is_within_bounding_box(&ais_outside_lon, &bbox)); + + // Test point with missing coordinates + let ais_no_coords = AisResponse { + latitude: None, + longitude: None, + ..ais_within.clone() + }; + + assert!(!is_within_bounding_box(&ais_no_coords, &bbox)); + + // Test point on boundary (should be included) + let ais_on_boundary = AisResponse { + latitude: Some(33.0), // Exactly on southwest latitude + longitude: Some(-118.0), // Exactly on northeast longitude + ..ais_within.clone() + }; + + assert!(is_within_bounding_box(&ais_on_boundary, &bbox)); + } + + #[test] + fn test_websocket_message_serialization() { + // Test bounding box message + let bbox_msg = WebSocketMessage { + message_type: "set_bounding_box".to_string(), + bounding_box: Some(WebSocketBoundingBox { + sw_lat: 33.0, + sw_lon: -119.0, + ne_lat: 34.0, + ne_lon: -118.0, + }), + }; + + let json_result = serde_json::to_string(&bbox_msg); + assert!(json_result.is_ok()); + + let json_string = json_result.unwrap(); + assert!(json_string.contains("set_bounding_box")); + assert!(json_string.contains("33.0")); + assert!(json_string.contains("-119.0")); + + // Test message without bounding box + let clear_msg = WebSocketMessage { + message_type: "clear_bounding_box".to_string(), + bounding_box: None, + }; + + let json_result = serde_json::to_string(&clear_msg); + assert!(json_result.is_ok()); + + let json_string = json_result.unwrap(); + assert!(json_string.contains("clear_bounding_box")); + // The bounding_box field should be omitted when None due to skip_serializing_if + assert!(!json_string.contains("\"bounding_box\"")); + } + + #[test] + fn test_websocket_message_deserialization() { + // Test parsing valid bounding box message + let json_str = r#"{"type":"set_bounding_box","bounding_box":{"sw_lat":33.0,"sw_lon":-119.0,"ne_lat":34.0,"ne_lon":-118.0}}"#; + let result: Result = serde_json::from_str(json_str); + assert!(result.is_ok()); + + let msg = result.unwrap(); + assert_eq!(msg.message_type, "set_bounding_box"); + assert!(msg.bounding_box.is_some()); + + let bbox = msg.bounding_box.unwrap(); + assert_eq!(bbox.sw_lat, 33.0); + assert_eq!(bbox.sw_lon, -119.0); + assert_eq!(bbox.ne_lat, 34.0); + assert_eq!(bbox.ne_lon, -118.0); + + // Test parsing message without bounding box + let json_str = r#"{"type":"clear_bounding_box"}"#; + let result: Result = serde_json::from_str(json_str); + assert!(result.is_ok()); + + let msg = result.unwrap(); + assert_eq!(msg.message_type, "clear_bounding_box"); + assert!(msg.bounding_box.is_none()); + } +} \ No newline at end of file diff --git a/crates/ais/src/main.rs b/crates/ais/src/main.rs new file mode 100644 index 0000000..8bac7af --- /dev/null +++ b/crates/ais/src/main.rs @@ -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); + } +} \ No newline at end of file diff --git a/crates/base-map/map/package-lock.json b/crates/base-map/map/package-lock.json index 21310ba..30bdd8b 100644 --- a/crates/base-map/map/package-lock.json +++ b/crates/base-map/map/package-lock.json @@ -18,6 +18,8 @@ "@emotion/react": "^11.14.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/react": "^19.1.8", "@types/react-dom": "^19.1.6", @@ -29,14 +31,23 @@ "geojson": "^0.5.0", "globals": "^16.2.0", "js-cookie": "^3.0.5", + "jsdom": "^26.1.0", "mapbox-gl": "^3.13.0", "react-map-gl": "^8.0.4", "typescript": "~5.8.3", "typescript-eslint": "^8.34.1", "vite": "^7.0.0", - "vite-tsconfig-paths": "^5.1.4" + "vite-tsconfig-paths": "^5.1.4", + "vitest": "^3.2.4" } }, + "node_modules/@adobe/css-tools": { + "version": "4.4.3", + "resolved": "https://registry.npmjs.org/@adobe/css-tools/-/css-tools-4.4.3.tgz", + "integrity": "sha512-VQKMkwriZbaOgVCby1UDY/LDk5fIjhQicCvVPFqfe+69fWaPWydbWJ3wRt59/YzIwda1I81loas3oCoHxnqvdA==", + "dev": true, + "license": "MIT" + }, "node_modules/@ark-ui/react": { "version": "5.16.1", "resolved": "https://registry.npmjs.org/@ark-ui/react/-/react-5.16.1.tgz", @@ -108,6 +119,20 @@ "react-dom": ">=18.0.0" } }, + "node_modules/@asamuzakjp/css-color": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/@asamuzakjp/css-color/-/css-color-3.2.0.tgz", + "integrity": "sha512-K1A6z8tS3XsmCMM86xoWdn7Fkdn9m6RSVtocUrJYIwZnFVkng/PvkEoWtOWmP+Scc6saYWHWZYbndEEXxl24jw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@csstools/css-calc": "^2.1.3", + "@csstools/css-color-parser": "^3.0.9", + "@csstools/css-parser-algorithms": "^3.0.4", + "@csstools/css-tokenizer": "^3.0.3", + "lru-cache": "^10.4.3" + } + }, "node_modules/@babel/code-frame": { "version": "7.27.1", "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.27.1.tgz", @@ -280,6 +305,121 @@ "react-dom": ">=18" } }, + "node_modules/@csstools/color-helpers": { + "version": "5.0.2", + "resolved": "https://registry.npmjs.org/@csstools/color-helpers/-/color-helpers-5.0.2.tgz", + "integrity": "sha512-JqWH1vsgdGcw2RR6VliXXdA0/59LttzlU8UlRT/iUUsEeWfYq8I+K0yhihEUTTHLRm1EXvpsCx3083EU15ecsA==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/csstools" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/csstools" + } + ], + "license": "MIT-0", + "engines": { + "node": ">=18" + } + }, + "node_modules/@csstools/css-calc": { + "version": "2.1.4", + "resolved": "https://registry.npmjs.org/@csstools/css-calc/-/css-calc-2.1.4.tgz", + "integrity": "sha512-3N8oaj+0juUw/1H3YwmDDJXCgTB1gKU6Hc/bB502u9zR0q2vd786XJH9QfrKIEgFlZmhZiq6epXl4rHqhzsIgQ==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/csstools" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/csstools" + } + ], + "license": "MIT", + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "@csstools/css-parser-algorithms": "^3.0.5", + "@csstools/css-tokenizer": "^3.0.4" + } + }, + "node_modules/@csstools/css-color-parser": { + "version": "3.0.10", + "resolved": "https://registry.npmjs.org/@csstools/css-color-parser/-/css-color-parser-3.0.10.tgz", + "integrity": "sha512-TiJ5Ajr6WRd1r8HSiwJvZBiJOqtH86aHpUjq5aEKWHiII2Qfjqd/HCWKPOW8EP4vcspXbHnXrwIDlu5savQipg==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/csstools" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/csstools" + } + ], + "license": "MIT", + "dependencies": { + "@csstools/color-helpers": "^5.0.2", + "@csstools/css-calc": "^2.1.4" + }, + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "@csstools/css-parser-algorithms": "^3.0.5", + "@csstools/css-tokenizer": "^3.0.4" + } + }, + "node_modules/@csstools/css-parser-algorithms": { + "version": "3.0.5", + "resolved": "https://registry.npmjs.org/@csstools/css-parser-algorithms/-/css-parser-algorithms-3.0.5.tgz", + "integrity": "sha512-DaDeUkXZKjdGhgYaHNJTV9pV7Y9B3b644jCLs9Upc3VeNGg6LWARAT6O+Q+/COo+2gg/bM5rhpMAtf70WqfBdQ==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/csstools" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/csstools" + } + ], + "license": "MIT", + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "@csstools/css-tokenizer": "^3.0.4" + } + }, + "node_modules/@csstools/css-tokenizer": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/@csstools/css-tokenizer/-/css-tokenizer-3.0.4.tgz", + "integrity": "sha512-Vd/9EVDiu6PPJt9yAh6roZP6El1xHrdvIVGjyBsHR0RYwNHgL7FJPyIIW4fANJNG6FtyZfvlRPpFI4ZM/lubvw==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/csstools" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/csstools" + } + ], + "license": "MIT", + "engines": { + "node": ">=18" + } + }, "node_modules/@emotion/babel-plugin": { "version": "11.13.5", "resolved": "https://registry.npmjs.org/@emotion/babel-plugin/-/babel-plugin-11.13.5.tgz", @@ -1829,6 +1969,122 @@ "@tauri-apps/api": "^2.6.0" } }, + "node_modules/@testing-library/dom": { + "version": "10.4.0", + "resolved": "https://registry.npmjs.org/@testing-library/dom/-/dom-10.4.0.tgz", + "integrity": "sha512-pemlzrSESWbdAloYml3bAJMEfNh1Z7EduzqPKprCH5S341frlpYnUEW0H72dLxa6IsYr+mPno20GiSm+h9dEdQ==", + "dev": true, + "license": "MIT", + "peer": true, + "dependencies": { + "@babel/code-frame": "^7.10.4", + "@babel/runtime": "^7.12.5", + "@types/aria-query": "^5.0.1", + "aria-query": "5.3.0", + "chalk": "^4.1.0", + "dom-accessibility-api": "^0.5.9", + "lz-string": "^1.5.0", + "pretty-format": "^27.0.2" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/@testing-library/jest-dom": { + "version": "6.6.3", + "resolved": "https://registry.npmjs.org/@testing-library/jest-dom/-/jest-dom-6.6.3.tgz", + "integrity": "sha512-IteBhl4XqYNkM54f4ejhLRJiZNqcSCoXUOG2CPK7qbD322KjQozM4kHQOfkG2oln9b9HTYqs+Sae8vBATubxxA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@adobe/css-tools": "^4.4.0", + "aria-query": "^5.0.0", + "chalk": "^3.0.0", + "css.escape": "^1.5.1", + "dom-accessibility-api": "^0.6.3", + "lodash": "^4.17.21", + "redent": "^3.0.0" + }, + "engines": { + "node": ">=14", + "npm": ">=6", + "yarn": ">=1" + } + }, + "node_modules/@testing-library/jest-dom/node_modules/chalk": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-3.0.0.tgz", + "integrity": "sha512-4D3B6Wf41KOYRFdszmDqMCGq5VV/uMAB273JILmO+3jAlh8X4qDtdtgCR3fxtbLEMzSx22QdhnDcJvu2u1fVwg==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-styles": "^4.1.0", + "supports-color": "^7.1.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/@testing-library/jest-dom/node_modules/dom-accessibility-api": { + "version": "0.6.3", + "resolved": "https://registry.npmjs.org/dom-accessibility-api/-/dom-accessibility-api-0.6.3.tgz", + "integrity": "sha512-7ZgogeTnjuHbo+ct10G9Ffp0mif17idi0IyWNVA/wcwcm7NPOD/WEHVP3n7n3MhXqxoIYm8d6MuZohYWIZ4T3w==", + "dev": true, + "license": "MIT" + }, + "node_modules/@testing-library/react": { + "version": "16.3.0", + "resolved": "https://registry.npmjs.org/@testing-library/react/-/react-16.3.0.tgz", + "integrity": "sha512-kFSyxiEDwv1WLl2fgsq6pPBbw5aWKrsY2/noi1Id0TK0UParSF62oFQFGHXIyaG4pp2tEub/Zlel+fjjZILDsw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/runtime": "^7.12.5" + }, + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "@testing-library/dom": "^10.0.0", + "@types/react": "^18.0.0 || ^19.0.0", + "@types/react-dom": "^18.0.0 || ^19.0.0", + "react": "^18.0.0 || ^19.0.0", + "react-dom": "^18.0.0 || ^19.0.0" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@types/aria-query": { + "version": "5.0.4", + "resolved": "https://registry.npmjs.org/@types/aria-query/-/aria-query-5.0.4.tgz", + "integrity": "sha512-rfT93uj5s0PRL7EzccGMs3brplhcrghnDoV26NqKhCAS1hVo+WdNsPvE/yb6ilfr5hi2MEk6d5EWJTKdxg8jVw==", + "dev": true, + "license": "MIT", + "peer": true + }, + "node_modules/@types/chai": { + "version": "5.2.2", + "resolved": "https://registry.npmjs.org/@types/chai/-/chai-5.2.2.tgz", + "integrity": "sha512-8kB30R7Hwqf40JPiKhVzodJs2Qc1ZJ5zuT3uzw5Hq/dhNCl3G3l83jfpdI1e20BP348+fV7VIL/+FxaXkqBmWg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/deep-eql": "*" + } + }, + "node_modules/@types/deep-eql": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/@types/deep-eql/-/deep-eql-4.0.2.tgz", + "integrity": "sha512-c9h9dVVMigMPc4bwTvC5dxqtqJZwQPePsWjPlpSOnojbor6pGqdk541lfA7AqFQr5pB1BRdq0juY9db81BwyFw==", + "dev": true, + "license": "MIT" + }, "node_modules/@types/estree": { "version": "1.0.8", "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.8.tgz", @@ -2239,6 +2495,121 @@ "vite": "^4 || ^5 || ^6 || ^7.0.0-beta.0" } }, + "node_modules/@vitest/expect": { + "version": "3.2.4", + "resolved": "https://registry.npmjs.org/@vitest/expect/-/expect-3.2.4.tgz", + "integrity": "sha512-Io0yyORnB6sikFlt8QW5K7slY4OjqNX9jmJQ02QDda8lyM6B5oNgVWoSoKPac8/kgnCUzuHQKrSLtu/uOqqrig==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/chai": "^5.2.2", + "@vitest/spy": "3.2.4", + "@vitest/utils": "3.2.4", + "chai": "^5.2.0", + "tinyrainbow": "^2.0.0" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/@vitest/mocker": { + "version": "3.2.4", + "resolved": "https://registry.npmjs.org/@vitest/mocker/-/mocker-3.2.4.tgz", + "integrity": "sha512-46ryTE9RZO/rfDd7pEqFl7etuyzekzEhUbTW3BvmeO/BcCMEgq59BKhek3dXDWgAj4oMK6OZi+vRr1wPW6qjEQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@vitest/spy": "3.2.4", + "estree-walker": "^3.0.3", + "magic-string": "^0.30.17" + }, + "funding": { + "url": "https://opencollective.com/vitest" + }, + "peerDependencies": { + "msw": "^2.4.9", + "vite": "^5.0.0 || ^6.0.0 || ^7.0.0-0" + }, + "peerDependenciesMeta": { + "msw": { + "optional": true + }, + "vite": { + "optional": true + } + } + }, + "node_modules/@vitest/pretty-format": { + "version": "3.2.4", + "resolved": "https://registry.npmjs.org/@vitest/pretty-format/-/pretty-format-3.2.4.tgz", + "integrity": "sha512-IVNZik8IVRJRTr9fxlitMKeJeXFFFN0JaB9PHPGQ8NKQbGpfjlTx9zO4RefN8gp7eqjNy8nyK3NZmBzOPeIxtA==", + "dev": true, + "license": "MIT", + "dependencies": { + "tinyrainbow": "^2.0.0" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/@vitest/runner": { + "version": "3.2.4", + "resolved": "https://registry.npmjs.org/@vitest/runner/-/runner-3.2.4.tgz", + "integrity": "sha512-oukfKT9Mk41LreEW09vt45f8wx7DordoWUZMYdY/cyAk7w5TWkTRCNZYF7sX7n2wB7jyGAl74OxgwhPgKaqDMQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@vitest/utils": "3.2.4", + "pathe": "^2.0.3", + "strip-literal": "^3.0.0" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/@vitest/snapshot": { + "version": "3.2.4", + "resolved": "https://registry.npmjs.org/@vitest/snapshot/-/snapshot-3.2.4.tgz", + "integrity": "sha512-dEYtS7qQP2CjU27QBC5oUOxLE/v5eLkGqPE0ZKEIDGMs4vKWe7IjgLOeauHsR0D5YuuycGRO5oSRXnwnmA78fQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@vitest/pretty-format": "3.2.4", + "magic-string": "^0.30.17", + "pathe": "^2.0.3" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/@vitest/spy": { + "version": "3.2.4", + "resolved": "https://registry.npmjs.org/@vitest/spy/-/spy-3.2.4.tgz", + "integrity": "sha512-vAfasCOe6AIK70iP5UD11Ac4siNUNJ9i/9PZ3NKx07sG6sUxeag1LWdNrMWeKKYBLlzuK+Gn65Yd5nyL6ds+nw==", + "dev": true, + "license": "MIT", + "dependencies": { + "tinyspy": "^4.0.3" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/@vitest/utils": { + "version": "3.2.4", + "resolved": "https://registry.npmjs.org/@vitest/utils/-/utils-3.2.4.tgz", + "integrity": "sha512-fB2V0JFrQSMsCo9HiSq3Ezpdv4iYaXRG1Sx8edX3MwxfyNn83mKiGzOcH+Fkxt4MHxr3y42fQi1oeAInqgX2QA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@vitest/pretty-format": "3.2.4", + "loupe": "^3.1.4", + "tinyrainbow": "^2.0.0" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, "node_modules/@zag-js/accordion": { "version": "1.18.2", "resolved": "https://registry.npmjs.org/@zag-js/accordion/-/accordion-1.18.2.tgz", @@ -3163,6 +3534,16 @@ "acorn": "^6.0.0 || ^7.0.0 || ^8.0.0" } }, + "node_modules/agent-base": { + "version": "7.1.4", + "resolved": "https://registry.npmjs.org/agent-base/-/agent-base-7.1.4.tgz", + "integrity": "sha512-MnA+YT8fwfJPgBx3m60MNqakm30XOkyIoH1y6huTQvC0PwZG7ki8NacLBcrPbNoo8vEZy7Jpuk7+jMO+CUovTQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 14" + } + }, "node_modules/ajv": { "version": "6.12.6", "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.12.6.tgz", @@ -3180,6 +3561,17 @@ "url": "https://github.com/sponsors/epoberezkin" } }, + "node_modules/ansi-regex": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", + "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", + "dev": true, + "license": "MIT", + "peer": true, + "engines": { + "node": ">=8" + } + }, "node_modules/ansi-styles": { "version": "4.3.0", "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", @@ -3203,6 +3595,16 @@ "dev": true, "license": "Python-2.0" }, + "node_modules/aria-query": { + "version": "5.3.0", + "resolved": "https://registry.npmjs.org/aria-query/-/aria-query-5.3.0.tgz", + "integrity": "sha512-b0P0sZPKtyu8HkeRAfCq0IfURZK+SuwMjY1UXGBU27wpAiTwQAIlq56IbIO+ytk/JjS1fMR14ee5WBBfKi5J6A==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "dequal": "^2.0.3" + } + }, "node_modules/arr-union": { "version": "3.1.0", "resolved": "https://registry.npmjs.org/arr-union/-/arr-union-3.1.0.tgz", @@ -3213,6 +3615,16 @@ "node": ">=0.10.0" } }, + "node_modules/assertion-error": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/assertion-error/-/assertion-error-2.0.1.tgz", + "integrity": "sha512-Izi8RQcffqCeNVgFigKli1ssklIbpHnCYc6AknXGYoB6grJqyeby7jv12JUQgmTAnIDnbck1uxksT4dzN3PWBA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + } + }, "node_modules/assign-symbols": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/assign-symbols/-/assign-symbols-1.0.0.tgz", @@ -3302,6 +3714,16 @@ "typewise-core": "^1.2" } }, + "node_modules/cac": { + "version": "6.7.14", + "resolved": "https://registry.npmjs.org/cac/-/cac-6.7.14.tgz", + "integrity": "sha512-b6Ilus+c3RrdDk+JhLKUAQfzzgLEPy6wcXqS7f/xe1EETvsDP6GORG7SFuOs6cID5YkqchW/LXZbX5bc8j7ZcQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, "node_modules/callsites": { "version": "3.1.0", "resolved": "https://registry.npmjs.org/callsites/-/callsites-3.1.0.tgz", @@ -3312,6 +3734,23 @@ "node": ">=6" } }, + "node_modules/chai": { + "version": "5.2.1", + "resolved": "https://registry.npmjs.org/chai/-/chai-5.2.1.tgz", + "integrity": "sha512-5nFxhUrX0PqtyogoYOA8IPswy5sZFTOsBFl/9bNsmDLgsxYTzSZQJDPppDnZPTQbzSEm0hqGjWPzRemQCYbD6A==", + "dev": true, + "license": "MIT", + "dependencies": { + "assertion-error": "^2.0.1", + "check-error": "^2.1.1", + "deep-eql": "^5.0.1", + "loupe": "^3.1.0", + "pathval": "^2.0.0" + }, + "engines": { + "node": ">=18" + } + }, "node_modules/chalk": { "version": "4.1.2", "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", @@ -3336,6 +3775,16 @@ "dev": true, "license": "ISC" }, + "node_modules/check-error": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/check-error/-/check-error-2.1.1.tgz", + "integrity": "sha512-OAlb+T7V4Op9OwdkjmguYRqncdlx5JiofwOAUkmTF+jNdHwzTaTs4sRAGpzLF3oOz5xAyDGrPgeIDFQmDOTiJw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 16" + } + }, "node_modules/color-convert": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", @@ -3412,6 +3861,13 @@ "node": ">= 8" } }, + "node_modules/css.escape": { + "version": "1.5.1", + "resolved": "https://registry.npmjs.org/css.escape/-/css.escape-1.5.1.tgz", + "integrity": "sha512-YUifsXXuknHlUsmlgyY0PKzgPOr7/FjCePfHNt0jxm83wHZi44VDMQ7/fGNkjY3/jV1MC+1CmZbaHzugyeRtpg==", + "dev": true, + "license": "MIT" + }, "node_modules/csscolorparser": { "version": "1.0.3", "resolved": "https://registry.npmjs.org/csscolorparser/-/csscolorparser-1.0.3.tgz", @@ -3419,6 +3875,20 @@ "dev": true, "license": "MIT" }, + "node_modules/cssstyle": { + "version": "4.6.0", + "resolved": "https://registry.npmjs.org/cssstyle/-/cssstyle-4.6.0.tgz", + "integrity": "sha512-2z+rWdzbbSZv6/rhtvzvqeZQHrBaqgogqt85sqFNbabZOuFbCVFb8kPeEtZjiKkbrm395irpNKiYeFeLiQnFPg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@asamuzakjp/css-color": "^3.2.0", + "rrweb-cssom": "^0.8.0" + }, + "engines": { + "node": ">=18" + } + }, "node_modules/csstype": { "version": "3.1.3", "resolved": "https://registry.npmjs.org/csstype/-/csstype-3.1.3.tgz", @@ -3426,6 +3896,20 @@ "dev": true, "license": "MIT" }, + "node_modules/data-urls": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/data-urls/-/data-urls-5.0.0.tgz", + "integrity": "sha512-ZYP5VBHshaDAiVZxjbRVcFJpc+4xGgT0bK3vzy1HLN8jTO975HEbuYzZJcHoQEY5K1a0z8YayJkyVETa08eNTg==", + "dev": true, + "license": "MIT", + "dependencies": { + "whatwg-mimetype": "^4.0.0", + "whatwg-url": "^14.0.0" + }, + "engines": { + "node": ">=18" + } + }, "node_modules/debug": { "version": "4.4.1", "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.1.tgz", @@ -3444,6 +3928,23 @@ } } }, + "node_modules/decimal.js": { + "version": "10.6.0", + "resolved": "https://registry.npmjs.org/decimal.js/-/decimal.js-10.6.0.tgz", + "integrity": "sha512-YpgQiITW3JXGntzdUmyUR1V812Hn8T1YVXhCu+wO3OpS4eU9l4YdD3qjyiKdV6mvV29zapkMeD390UVEf2lkUg==", + "dev": true, + "license": "MIT" + }, + "node_modules/deep-eql": { + "version": "5.0.2", + "resolved": "https://registry.npmjs.org/deep-eql/-/deep-eql-5.0.2.tgz", + "integrity": "sha512-h5k/5U50IJJFpzfL6nO9jaaumfjO/f2NjK/oYB2Djzm4p9L+3T9qWpZqZ2hAbLPuuYq9wrU08WQyBTL5GbPk5Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, "node_modules/deep-is": { "version": "0.1.4", "resolved": "https://registry.npmjs.org/deep-is/-/deep-is-0.1.4.tgz", @@ -3451,6 +3952,24 @@ "dev": true, "license": "MIT" }, + "node_modules/dequal": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/dequal/-/dequal-2.0.3.tgz", + "integrity": "sha512-0je+qPKHEMohvfRTCEo3CrPG6cAzAYgmzKyxRiYSSDkS6eGJdyVJm7WaYA5ECaAD9wLB2T4EEeymA5aFVcYXCA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/dom-accessibility-api": { + "version": "0.5.16", + "resolved": "https://registry.npmjs.org/dom-accessibility-api/-/dom-accessibility-api-0.5.16.tgz", + "integrity": "sha512-X7BJ2yElsnOJ30pZF4uIIDfBEVgF4XEBxL9Bxhy6dnrm5hkzqmsWHGTiHqRiITNhMyFLyAiWndIJP7Z1NTteDg==", + "dev": true, + "license": "MIT", + "peer": true + }, "node_modules/dom-walk": { "version": "0.1.2", "resolved": "https://registry.npmjs.org/dom-walk/-/dom-walk-0.1.2.tgz", @@ -3464,6 +3983,19 @@ "dev": true, "license": "ISC" }, + "node_modules/entities": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/entities/-/entities-6.0.1.tgz", + "integrity": "sha512-aN97NXWF6AWBTahfVOIrB/NShkzi5H7F9r1s9mD3cDj4Ko5f2qhhVoYMibXF7GlLveb/D2ioWay8lxI97Ven3g==", + "dev": true, + "license": "BSD-2-Clause", + "engines": { + "node": ">=0.12" + }, + "funding": { + "url": "https://github.com/fb55/entities?sponsor=1" + } + }, "node_modules/error-ex": { "version": "1.3.2", "resolved": "https://registry.npmjs.org/error-ex/-/error-ex-1.3.2.tgz", @@ -3474,6 +4006,13 @@ "is-arrayish": "^0.2.1" } }, + "node_modules/es-module-lexer": { + "version": "1.7.0", + "resolved": "https://registry.npmjs.org/es-module-lexer/-/es-module-lexer-1.7.0.tgz", + "integrity": "sha512-jEQoCwk8hyb2AZziIOLhDqpm5+2ww5uIE6lkO/6jcOCusfk6LhMHpXXfBLXTZ7Ydyt0j4VoUQv6uGNYbdW+kBA==", + "dev": true, + "license": "MIT" + }, "node_modules/esbuild": { "version": "0.25.6", "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.25.6.tgz", @@ -3697,6 +4236,16 @@ "node": ">=4.0" } }, + "node_modules/estree-walker": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/estree-walker/-/estree-walker-3.0.3.tgz", + "integrity": "sha512-7RUKfXgSMMkzt6ZuXmqapOurLGPPfgj6l9uRZ7lRGolvk0y2yocc35LdcxKC5PQZdn2DMqioAQ2NoWcrTKmm6g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/estree": "^1.0.0" + } + }, "node_modules/esutils": { "version": "2.0.3", "resolved": "https://registry.npmjs.org/esutils/-/esutils-2.0.3.tgz", @@ -3707,6 +4256,16 @@ "node": ">=0.10.0" } }, + "node_modules/expect-type": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/expect-type/-/expect-type-1.2.2.tgz", + "integrity": "sha512-JhFGDVJ7tmDJItKhYgJCGLOWjuK9vPxiXoUFLwLDc99NlmklilbiQJwoctZtt13+xMw91MCk/REan6MWHqDjyA==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=12.0.0" + } + }, "node_modules/extend-shallow": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/extend-shallow/-/extend-shallow-2.0.1.tgz", @@ -4009,6 +4568,60 @@ "react-is": "^16.7.0" } }, + "node_modules/html-encoding-sniffer": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/html-encoding-sniffer/-/html-encoding-sniffer-4.0.0.tgz", + "integrity": "sha512-Y22oTqIU4uuPgEemfz7NDJz6OeKf12Lsu+QC+s3BVpda64lTiMYCyGwg5ki4vFxkMwQdeZDl2adZoqUgdFuTgQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "whatwg-encoding": "^3.1.1" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/http-proxy-agent": { + "version": "7.0.2", + "resolved": "https://registry.npmjs.org/http-proxy-agent/-/http-proxy-agent-7.0.2.tgz", + "integrity": "sha512-T1gkAiYYDWYx3V5Bmyu7HcfcvL7mUrTWiM6yOfa3PIphViJ/gFPbvidQ+veqSOHci/PxBcDabeUNCzpOODJZig==", + "dev": true, + "license": "MIT", + "dependencies": { + "agent-base": "^7.1.0", + "debug": "^4.3.4" + }, + "engines": { + "node": ">= 14" + } + }, + "node_modules/https-proxy-agent": { + "version": "7.0.6", + "resolved": "https://registry.npmjs.org/https-proxy-agent/-/https-proxy-agent-7.0.6.tgz", + "integrity": "sha512-vK9P5/iUfdl95AI+JVyUuIcVtd4ofvtrOr3HNtM2yxC9bnMbEdp3x01OhQNnjb8IJYi38VlTE3mBXwcfvywuSw==", + "dev": true, + "license": "MIT", + "dependencies": { + "agent-base": "^7.1.2", + "debug": "4" + }, + "engines": { + "node": ">= 14" + } + }, + "node_modules/iconv-lite": { + "version": "0.6.3", + "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.6.3.tgz", + "integrity": "sha512-4fCk79wshMdzMp2rH06qWrJE4iolqLhCUH+OiuIgU++RB0+94NlDL81atO7GX55uUKueo0txHNtvEyI6D7WdMw==", + "dev": true, + "license": "MIT", + "dependencies": { + "safer-buffer": ">= 2.1.2 < 3.0.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, "node_modules/ieee754": { "version": "1.2.1", "resolved": "https://registry.npmjs.org/ieee754/-/ieee754-1.2.1.tgz", @@ -4067,6 +4680,16 @@ "node": ">=0.8.19" } }, + "node_modules/indent-string": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/indent-string/-/indent-string-4.0.0.tgz", + "integrity": "sha512-EdDDZu4A2OyIK7Lr/2zG+w5jmbuk1DVBnEwREQvBzspBJkCEbRa8GxU1lghYcaGJCnRWibjDXlq779X1/y5xwg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, "node_modules/is-arrayish": { "version": "0.2.1", "resolved": "https://registry.npmjs.org/is-arrayish/-/is-arrayish-0.2.1.tgz", @@ -4146,6 +4769,13 @@ "node": ">=0.10.0" } }, + "node_modules/is-potential-custom-element-name": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/is-potential-custom-element-name/-/is-potential-custom-element-name-1.0.1.tgz", + "integrity": "sha512-bCYeRA2rVibKZd+s2625gGnGF/t7DSqDs4dP7CrLA1m7jKWz6pps0LpYLJN8Q64HtmPKJ1hrN3nzPNKFEKOUiQ==", + "dev": true, + "license": "MIT" + }, "node_modules/isexe": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz", @@ -4193,6 +4823,46 @@ "js-yaml": "bin/js-yaml.js" } }, + "node_modules/jsdom": { + "version": "26.1.0", + "resolved": "https://registry.npmjs.org/jsdom/-/jsdom-26.1.0.tgz", + "integrity": "sha512-Cvc9WUhxSMEo4McES3P7oK3QaXldCfNWp7pl2NNeiIFlCoLr3kfq9kb1fxftiwk1FLV7CvpvDfonxtzUDeSOPg==", + "dev": true, + "license": "MIT", + "dependencies": { + "cssstyle": "^4.2.1", + "data-urls": "^5.0.0", + "decimal.js": "^10.5.0", + "html-encoding-sniffer": "^4.0.0", + "http-proxy-agent": "^7.0.2", + "https-proxy-agent": "^7.0.6", + "is-potential-custom-element-name": "^1.0.1", + "nwsapi": "^2.2.16", + "parse5": "^7.2.1", + "rrweb-cssom": "^0.8.0", + "saxes": "^6.0.0", + "symbol-tree": "^3.2.4", + "tough-cookie": "^5.1.1", + "w3c-xmlserializer": "^5.0.0", + "webidl-conversions": "^7.0.0", + "whatwg-encoding": "^3.1.1", + "whatwg-mimetype": "^4.0.0", + "whatwg-url": "^14.1.1", + "ws": "^8.18.0", + "xml-name-validator": "^5.0.0" + }, + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "canvas": "^3.0.0" + }, + "peerDependenciesMeta": { + "canvas": { + "optional": true + } + } + }, "node_modules/jsesc": { "version": "3.1.0", "resolved": "https://registry.npmjs.org/jsesc/-/jsesc-3.1.0.tgz", @@ -4295,6 +4965,13 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/lodash": { + "version": "4.17.21", + "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.21.tgz", + "integrity": "sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==", + "dev": true, + "license": "MIT" + }, "node_modules/lodash.merge": { "version": "4.6.2", "resolved": "https://registry.npmjs.org/lodash.merge/-/lodash.merge-4.6.2.tgz", @@ -4302,6 +4979,41 @@ "dev": true, "license": "MIT" }, + "node_modules/loupe": { + "version": "3.1.4", + "resolved": "https://registry.npmjs.org/loupe/-/loupe-3.1.4.tgz", + "integrity": "sha512-wJzkKwJrheKtknCOKNEtDK4iqg/MxmZheEMtSTYvnzRdEYaZzmgH976nenp8WdJRdx5Vc1X/9MO0Oszl6ezeXg==", + "dev": true, + "license": "MIT" + }, + "node_modules/lru-cache": { + "version": "10.4.3", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-10.4.3.tgz", + "integrity": "sha512-JNAzZcXrCt42VGLuYz0zfAzDfAvJWW6AfYlDBQyDV5DClI2m5sAmK+OIO7s59XfsRsWHp02jAJrRadPRGTt6SQ==", + "dev": true, + "license": "ISC" + }, + "node_modules/lz-string": { + "version": "1.5.0", + "resolved": "https://registry.npmjs.org/lz-string/-/lz-string-1.5.0.tgz", + "integrity": "sha512-h5bgJWpxJNswbU7qCrV0tIKQCaS3blPDrqKWx+QxzuzL1zGUzij9XCWLrSLsJPu5t+eWA/ycetzYAO5IOMcWAQ==", + "dev": true, + "license": "MIT", + "peer": true, + "bin": { + "lz-string": "bin/bin.js" + } + }, + "node_modules/magic-string": { + "version": "0.30.17", + "resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.30.17.tgz", + "integrity": "sha512-sNPKHvyjVf7gyjwS4xGTaW/mCnF8wnjtifKBEhxfZ7E/S8tQ0rssrwGNn6q8JH/ohItJfSQp9mBtQYuTlH5QnA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/sourcemap-codec": "^1.5.0" + } + }, "node_modules/mapbox-gl": { "version": "3.13.0", "resolved": "https://registry.npmjs.org/mapbox-gl/-/mapbox-gl-3.13.0.tgz", @@ -4396,6 +5108,16 @@ "dom-walk": "^0.1.0" } }, + "node_modules/min-indent": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/min-indent/-/min-indent-1.0.1.tgz", + "integrity": "sha512-I9jwMn07Sy/IwOj3zVkVik2JTvgpaykDZEigL6Rx6N9LbMywwUSMtxET+7lVoDLLd3O3IXwJwvuuns8UB/HeAg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=4" + } + }, "node_modules/minimatch": { "version": "3.1.2", "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", @@ -4469,6 +5191,13 @@ "react-dom": "^16.8 || ^17 || ^18 || ^19 || ^19.0.0-rc" } }, + "node_modules/nwsapi": { + "version": "2.2.20", + "resolved": "https://registry.npmjs.org/nwsapi/-/nwsapi-2.2.20.tgz", + "integrity": "sha512-/ieB+mDe4MrrKMT8z+mQL8klXydZWGR5Dowt4RAGKbJ3kIGEx3X4ljUo+6V73IXtUPWgfOlU5B9MlGxFO5T+cA==", + "dev": true, + "license": "MIT" + }, "node_modules/optionator": { "version": "0.9.4", "resolved": "https://registry.npmjs.org/optionator/-/optionator-0.9.4.tgz", @@ -4551,6 +5280,19 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/parse5": { + "version": "7.3.0", + "resolved": "https://registry.npmjs.org/parse5/-/parse5-7.3.0.tgz", + "integrity": "sha512-IInvU7fabl34qmi9gY8XOVxhYyMyuH2xUNpb2q8/Y+7552KlejkRvqvD19nMoUW/uQGGbqNpA6Tufu5FL5BZgw==", + "dev": true, + "license": "MIT", + "dependencies": { + "entities": "^6.0.0" + }, + "funding": { + "url": "https://github.com/inikulin/parse5?sponsor=1" + } + }, "node_modules/path-exists": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-4.0.0.tgz", @@ -4588,6 +5330,23 @@ "node": ">=8" } }, + "node_modules/pathe": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/pathe/-/pathe-2.0.3.tgz", + "integrity": "sha512-WUjGcAqP1gQacoQe+OBJsFA7Ld4DyXuUIjZ5cc75cLHvJ7dtNsTugphxIADwspS+AraAUePCKrSVtPLFj/F88w==", + "dev": true, + "license": "MIT" + }, + "node_modules/pathval": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/pathval/-/pathval-2.0.1.tgz", + "integrity": "sha512-//nshmD55c46FuFw26xV/xFAaB5HF9Xdap7HJBBnrKdAd6/GxDBaNA1870O79+9ueg61cZLSVc+OaFlfmObYVQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 14.16" + } + }, "node_modules/pbf": { "version": "3.3.0", "resolved": "https://registry.npmjs.org/pbf/-/pbf-3.3.0.tgz", @@ -4692,6 +5451,44 @@ "node": ">= 0.8.0" } }, + "node_modules/pretty-format": { + "version": "27.5.1", + "resolved": "https://registry.npmjs.org/pretty-format/-/pretty-format-27.5.1.tgz", + "integrity": "sha512-Qb1gy5OrP5+zDf2Bvnzdl3jsTf1qXVMazbvCoKhtKqVs4/YK4ozX4gKQJJVyNe+cajNPn0KoC0MC3FUmaHWEmQ==", + "dev": true, + "license": "MIT", + "peer": true, + "dependencies": { + "ansi-regex": "^5.0.1", + "ansi-styles": "^5.0.0", + "react-is": "^17.0.1" + }, + "engines": { + "node": "^10.13.0 || ^12.13.0 || ^14.15.0 || >=15.0.0" + } + }, + "node_modules/pretty-format/node_modules/ansi-styles": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-5.2.0.tgz", + "integrity": "sha512-Cxwpt2SfTzTtXcfOlzGEee8O+c+MmUgGrNiBcXnuWxuFJHe6a5Hz7qwhwe5OgaSYI0IJvkLqWX1ASG+cJOkEiA==", + "dev": true, + "license": "MIT", + "peer": true, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/pretty-format/node_modules/react-is": { + "version": "17.0.2", + "resolved": "https://registry.npmjs.org/react-is/-/react-is-17.0.2.tgz", + "integrity": "sha512-w2GsyukL62IJnlaff/nRegPQR94C/XXamvMWmSHRJ4y7Ts/4ocGRmTHvOs8PSE6pB3dWOrD/nueuU5sduBsQ4w==", + "dev": true, + "license": "MIT", + "peer": true + }, "node_modules/process": { "version": "0.11.10", "resolved": "https://registry.npmjs.org/process/-/process-0.11.10.tgz", @@ -4826,6 +5623,20 @@ } } }, + "node_modules/redent": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/redent/-/redent-3.0.0.tgz", + "integrity": "sha512-6tDA8g98We0zd0GvVeMT9arEOnTw9qM03L9cJXaCjrip1OO764RDBLBfrB4cwzNGDj5OA5ioymC9GkizgWJDUg==", + "dev": true, + "license": "MIT", + "dependencies": { + "indent-string": "^4.0.0", + "strip-indent": "^3.0.0" + }, + "engines": { + "node": ">=8" + } + }, "node_modules/resolve": { "version": "1.22.10", "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.22.10.tgz", @@ -4925,6 +5736,13 @@ "fsevents": "~2.3.2" } }, + "node_modules/rrweb-cssom": { + "version": "0.8.0", + "resolved": "https://registry.npmjs.org/rrweb-cssom/-/rrweb-cssom-0.8.0.tgz", + "integrity": "sha512-guoltQEx+9aMf2gDZ0s62EcV8lsXR+0w8915TC3ITdn2YueuNjdAYh/levpU9nFaoChh9RUS5ZdQMrKfVEN9tw==", + "dev": true, + "license": "MIT" + }, "node_modules/run-parallel": { "version": "1.2.0", "resolved": "https://registry.npmjs.org/run-parallel/-/run-parallel-1.2.0.tgz", @@ -4956,6 +5774,26 @@ "dev": true, "license": "BSD-3-Clause" }, + "node_modules/safer-buffer": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz", + "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==", + "dev": true, + "license": "MIT" + }, + "node_modules/saxes": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/saxes/-/saxes-6.0.0.tgz", + "integrity": "sha512-xAg7SOnEhrm5zI3puOOKyy1OMcMlIJZYNJY7xLBwSze0UjhPLnWfj2GF2EpT0jmzaJKIWKHLsaSSajf35bcYnA==", + "dev": true, + "license": "ISC", + "dependencies": { + "xmlchars": "^2.2.0" + }, + "engines": { + "node": ">=v12.22.7" + } + }, "node_modules/scheduler": { "version": "0.26.0", "resolved": "https://registry.npmjs.org/scheduler/-/scheduler-0.26.0.tgz", @@ -5024,6 +5862,13 @@ "node": ">=8" } }, + "node_modules/siginfo": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/siginfo/-/siginfo-2.0.0.tgz", + "integrity": "sha512-ybx0WO1/8bSBLEWXZvEd7gMW3Sn3JFlW3TvX1nREbDLRNQNaeNN8WK0meBwPdAaOI7TtRRRJn/Es1zhrrCHu7g==", + "dev": true, + "license": "ISC" + }, "node_modules/sort-asc": { "version": "0.2.0", "resolved": "https://registry.npmjs.org/sort-asc/-/sort-asc-0.2.0.tgz", @@ -5129,6 +5974,33 @@ "node": ">=0.10.0" } }, + "node_modules/stackback": { + "version": "0.0.2", + "resolved": "https://registry.npmjs.org/stackback/-/stackback-0.0.2.tgz", + "integrity": "sha512-1XMJE5fQo1jGH6Y/7ebnwPOBEkIEnT4QF32d5R1+VXdXveM0IBMJt8zfaxX1P3QhVwrYe+576+jkANtSS2mBbw==", + "dev": true, + "license": "MIT" + }, + "node_modules/std-env": { + "version": "3.9.0", + "resolved": "https://registry.npmjs.org/std-env/-/std-env-3.9.0.tgz", + "integrity": "sha512-UGvjygr6F6tpH7o2qyqR6QYpwraIjKSdtzyBdyytFOHmPZY917kwdwLG0RbOjWOnKmnm3PeHjaoLLMie7kPLQw==", + "dev": true, + "license": "MIT" + }, + "node_modules/strip-indent": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/strip-indent/-/strip-indent-3.0.0.tgz", + "integrity": "sha512-laJTa3Jb+VQpaC6DseHhF7dXVqHTfJPCRDaEbid/drOhgitgYku/letMUqOXFoWV0zIIUbjpdH2t+tYj4bQMRQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "min-indent": "^1.0.0" + }, + "engines": { + "node": ">=8" + } + }, "node_modules/strip-json-comments": { "version": "3.1.1", "resolved": "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-3.1.1.tgz", @@ -5142,6 +6014,26 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/strip-literal": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/strip-literal/-/strip-literal-3.0.0.tgz", + "integrity": "sha512-TcccoMhJOM3OebGhSBEmp3UZ2SfDMZUEBdRA/9ynfLi8yYajyWX3JiXArcJt4Umh4vISpspkQIY8ZZoCqjbviA==", + "dev": true, + "license": "MIT", + "dependencies": { + "js-tokens": "^9.0.1" + }, + "funding": { + "url": "https://github.com/sponsors/antfu" + } + }, + "node_modules/strip-literal/node_modules/js-tokens": { + "version": "9.0.1", + "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-9.0.1.tgz", + "integrity": "sha512-mxa9E9ITFOt0ban3j6L5MpjwegGz6lBQmM1IJkWeBZGcMxto50+eWdjC/52xDbS2vy0k7vIMK0Fe2wfL9OQSpQ==", + "dev": true, + "license": "MIT" + }, "node_modules/stylis": { "version": "4.2.0", "resolved": "https://registry.npmjs.org/stylis/-/stylis-4.2.0.tgz", @@ -5185,6 +6077,27 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/symbol-tree": { + "version": "3.2.4", + "resolved": "https://registry.npmjs.org/symbol-tree/-/symbol-tree-3.2.4.tgz", + "integrity": "sha512-9QNk5KwDF+Bvz+PyObkmSYjI5ksVUYtjW7AU22r2NKcfLJcXp96hkDWU3+XndOsUb+AQ9QhfzfCT2O+CNWT5Tw==", + "dev": true, + "license": "MIT" + }, + "node_modules/tinybench": { + "version": "2.9.0", + "resolved": "https://registry.npmjs.org/tinybench/-/tinybench-2.9.0.tgz", + "integrity": "sha512-0+DUvqWMValLmha6lr4kD8iAMK1HzV0/aKnCtWb9v9641TnP/MFb7Pc2bxoxQjTXAErryXVgUOfv2YqNllqGeg==", + "dev": true, + "license": "MIT" + }, + "node_modules/tinyexec": { + "version": "0.3.2", + "resolved": "https://registry.npmjs.org/tinyexec/-/tinyexec-0.3.2.tgz", + "integrity": "sha512-KQQR9yN7R5+OSwaK0XQoj22pwHoTlgYqmUscPYoknOoWCWfj/5/ABTMRi69FrKU5ffPVh5QcFikpWJI/P1ocHA==", + "dev": true, + "license": "MIT" + }, "node_modules/tinyglobby": { "version": "0.2.14", "resolved": "https://registry.npmjs.org/tinyglobby/-/tinyglobby-0.2.14.tgz", @@ -5230,6 +6143,16 @@ "url": "https://github.com/sponsors/jonschlinkert" } }, + "node_modules/tinypool": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/tinypool/-/tinypool-1.1.1.tgz", + "integrity": "sha512-Zba82s87IFq9A9XmjiX5uZA/ARWDrB03OHlq+Vw1fSdt0I+4/Kutwy8BP4Y/y/aORMo61FQ0vIb5j44vSo5Pkg==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^18.0.0 || >=20.0.0" + } + }, "node_modules/tinyqueue": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/tinyqueue/-/tinyqueue-3.0.0.tgz", @@ -5237,6 +6160,46 @@ "dev": true, "license": "ISC" }, + "node_modules/tinyrainbow": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/tinyrainbow/-/tinyrainbow-2.0.0.tgz", + "integrity": "sha512-op4nsTR47R6p0vMUUoYl/a+ljLFVtlfaXkLQmqfLR1qHma1h/ysYk4hEXZ880bf2CYgTskvTa/e196Vd5dDQXw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/tinyspy": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/tinyspy/-/tinyspy-4.0.3.tgz", + "integrity": "sha512-t2T/WLB2WRgZ9EpE4jgPJ9w+i66UZfDc8wHh0xrwiRNN+UwH98GIJkTeZqX9rg0i0ptwzqW+uYeIF0T4F8LR7A==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/tldts": { + "version": "6.1.86", + "resolved": "https://registry.npmjs.org/tldts/-/tldts-6.1.86.tgz", + "integrity": "sha512-WMi/OQ2axVTf/ykqCQgXiIct+mSQDFdH2fkwhPwgEwvJ1kSzZRiinb0zF2Xb8u4+OqPChmyI6MEu4EezNJz+FQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "tldts-core": "^6.1.86" + }, + "bin": { + "tldts": "bin/cli.js" + } + }, + "node_modules/tldts-core": { + "version": "6.1.86", + "resolved": "https://registry.npmjs.org/tldts-core/-/tldts-core-6.1.86.tgz", + "integrity": "sha512-Je6p7pkk+KMzMv2XXKmAE3McmolOQFdxkKw0R8EYNr7sELW46JqnNeTX8ybPiQgvg1ymCoF8LXs5fzFaZvJPTA==", + "dev": true, + "license": "MIT" + }, "node_modules/to-regex-range": { "version": "5.0.1", "resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz", @@ -5250,6 +6213,32 @@ "node": ">=8.0" } }, + "node_modules/tough-cookie": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/tough-cookie/-/tough-cookie-5.1.2.tgz", + "integrity": "sha512-FVDYdxtnj0G6Qm/DhNPSb8Ju59ULcup3tuJxkFb5K8Bv2pUXILbf0xZWU8PX8Ov19OXljbUyveOFwRMwkXzO+A==", + "dev": true, + "license": "BSD-3-Clause", + "dependencies": { + "tldts": "^6.1.32" + }, + "engines": { + "node": ">=16" + } + }, + "node_modules/tr46": { + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/tr46/-/tr46-5.1.1.tgz", + "integrity": "sha512-hdF5ZgjTqgAntKkklYw0R03MG2x/bSzTtkxmIRw/sTNV8YXsCJ1tfLAX23lhxhHJlEf3CRCOCGGWw3vI3GaSPw==", + "dev": true, + "license": "MIT", + "dependencies": { + "punycode": "^2.3.1" + }, + "engines": { + "node": ">=18" + } + }, "node_modules/ts-api-utils": { "version": "2.1.0", "resolved": "https://registry.npmjs.org/ts-api-utils/-/ts-api-utils-2.1.0.tgz", @@ -5467,6 +6456,29 @@ } } }, + "node_modules/vite-node": { + "version": "3.2.4", + "resolved": "https://registry.npmjs.org/vite-node/-/vite-node-3.2.4.tgz", + "integrity": "sha512-EbKSKh+bh1E1IFxeO0pg1n4dvoOTt0UDiXMd/qn++r98+jPO1xtJilvXldeuQ8giIB5IkpjCgMleHMNEsGH6pg==", + "dev": true, + "license": "MIT", + "dependencies": { + "cac": "^6.7.14", + "debug": "^4.4.1", + "es-module-lexer": "^1.7.0", + "pathe": "^2.0.3", + "vite": "^5.0.0 || ^6.0.0 || ^7.0.0-0" + }, + "bin": { + "vite-node": "vite-node.mjs" + }, + "engines": { + "node": "^18.0.0 || ^20.0.0 || >=22.0.0" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, "node_modules/vite-tsconfig-paths": { "version": "5.1.4", "resolved": "https://registry.npmjs.org/vite-tsconfig-paths/-/vite-tsconfig-paths-5.1.4.tgz", @@ -5515,6 +6527,92 @@ "url": "https://github.com/sponsors/jonschlinkert" } }, + "node_modules/vitest": { + "version": "3.2.4", + "resolved": "https://registry.npmjs.org/vitest/-/vitest-3.2.4.tgz", + "integrity": "sha512-LUCP5ev3GURDysTWiP47wRRUpLKMOfPh+yKTx3kVIEiu5KOMeqzpnYNsKyOoVrULivR8tLcks4+lga33Whn90A==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/chai": "^5.2.2", + "@vitest/expect": "3.2.4", + "@vitest/mocker": "3.2.4", + "@vitest/pretty-format": "^3.2.4", + "@vitest/runner": "3.2.4", + "@vitest/snapshot": "3.2.4", + "@vitest/spy": "3.2.4", + "@vitest/utils": "3.2.4", + "chai": "^5.2.0", + "debug": "^4.4.1", + "expect-type": "^1.2.1", + "magic-string": "^0.30.17", + "pathe": "^2.0.3", + "picomatch": "^4.0.2", + "std-env": "^3.9.0", + "tinybench": "^2.9.0", + "tinyexec": "^0.3.2", + "tinyglobby": "^0.2.14", + "tinypool": "^1.1.1", + "tinyrainbow": "^2.0.0", + "vite": "^5.0.0 || ^6.0.0 || ^7.0.0-0", + "vite-node": "3.2.4", + "why-is-node-running": "^2.3.0" + }, + "bin": { + "vitest": "vitest.mjs" + }, + "engines": { + "node": "^18.0.0 || ^20.0.0 || >=22.0.0" + }, + "funding": { + "url": "https://opencollective.com/vitest" + }, + "peerDependencies": { + "@edge-runtime/vm": "*", + "@types/debug": "^4.1.12", + "@types/node": "^18.0.0 || ^20.0.0 || >=22.0.0", + "@vitest/browser": "3.2.4", + "@vitest/ui": "3.2.4", + "happy-dom": "*", + "jsdom": "*" + }, + "peerDependenciesMeta": { + "@edge-runtime/vm": { + "optional": true + }, + "@types/debug": { + "optional": true + }, + "@types/node": { + "optional": true + }, + "@vitest/browser": { + "optional": true + }, + "@vitest/ui": { + "optional": true + }, + "happy-dom": { + "optional": true + }, + "jsdom": { + "optional": true + } + } + }, + "node_modules/vitest/node_modules/picomatch": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.3.tgz", + "integrity": "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, "node_modules/vt-pbf": { "version": "3.1.3", "resolved": "https://registry.npmjs.org/vt-pbf/-/vt-pbf-3.1.3.tgz", @@ -5527,6 +6625,66 @@ "pbf": "^3.2.1" } }, + "node_modules/w3c-xmlserializer": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/w3c-xmlserializer/-/w3c-xmlserializer-5.0.0.tgz", + "integrity": "sha512-o8qghlI8NZHU1lLPrpi2+Uq7abh4GGPpYANlalzWxyWteJOCsr/P+oPBA49TOLu5FTZO4d3F9MnWJfiMo4BkmA==", + "dev": true, + "license": "MIT", + "dependencies": { + "xml-name-validator": "^5.0.0" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/webidl-conversions": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-7.0.0.tgz", + "integrity": "sha512-VwddBukDzu71offAQR975unBIGqfKZpM+8ZX6ySk8nYhVoo5CYaZyzt3YBvYtRtO+aoGlqxPg/B87NGVZ/fu6g==", + "dev": true, + "license": "BSD-2-Clause", + "engines": { + "node": ">=12" + } + }, + "node_modules/whatwg-encoding": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/whatwg-encoding/-/whatwg-encoding-3.1.1.tgz", + "integrity": "sha512-6qN4hJdMwfYBtE3YBTTHhoeuUrDBPZmbQaxWAqSALV/MeEnR5z1xd8UKud2RAkFoPkmB+hli1TZSnyi84xz1vQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "iconv-lite": "0.6.3" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/whatwg-mimetype": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/whatwg-mimetype/-/whatwg-mimetype-4.0.0.tgz", + "integrity": "sha512-QaKxh0eNIi2mE9p2vEdzfagOKHCcj1pJ56EEHGQOVxp8r9/iszLUUV7v89x9O1p/T+NlTM5W7jW6+cz4Fq1YVg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + } + }, + "node_modules/whatwg-url": { + "version": "14.2.0", + "resolved": "https://registry.npmjs.org/whatwg-url/-/whatwg-url-14.2.0.tgz", + "integrity": "sha512-De72GdQZzNTUBBChsXueQUnPKDkg/5A5zp7pFDuQAj5UFoENpiACU0wlCvzpAGnTkj++ihpKwKyYewn/XNUbKw==", + "dev": true, + "license": "MIT", + "dependencies": { + "tr46": "^5.1.0", + "webidl-conversions": "^7.0.0" + }, + "engines": { + "node": ">=18" + } + }, "node_modules/which": { "version": "2.0.2", "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz", @@ -5543,6 +6701,23 @@ "node": ">= 8" } }, + "node_modules/why-is-node-running": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/why-is-node-running/-/why-is-node-running-2.3.0.tgz", + "integrity": "sha512-hUrmaWBdVDcxvYqnyh09zunKzROWjbZTiNy8dBEjkS7ehEDQibXJ7XvlmtbwuTclUiIyN+CyXQD4Vmko8fNm8w==", + "dev": true, + "license": "MIT", + "dependencies": { + "siginfo": "^2.0.0", + "stackback": "0.0.2" + }, + "bin": { + "why-is-node-running": "cli.js" + }, + "engines": { + "node": ">=8" + } + }, "node_modules/word-wrap": { "version": "1.2.5", "resolved": "https://registry.npmjs.org/word-wrap/-/word-wrap-1.2.5.tgz", @@ -5553,6 +6728,45 @@ "node": ">=0.10.0" } }, + "node_modules/ws": { + "version": "8.18.3", + "resolved": "https://registry.npmjs.org/ws/-/ws-8.18.3.tgz", + "integrity": "sha512-PEIGCY5tSlUt50cqyMXfCzX+oOPqN0vuGqWzbcJ2xvnkzkq46oOpz7dQaTDBdfICb4N14+GARUDw2XV2N4tvzg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10.0.0" + }, + "peerDependencies": { + "bufferutil": "^4.0.1", + "utf-8-validate": ">=5.0.2" + }, + "peerDependenciesMeta": { + "bufferutil": { + "optional": true + }, + "utf-8-validate": { + "optional": true + } + } + }, + "node_modules/xml-name-validator": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/xml-name-validator/-/xml-name-validator-5.0.0.tgz", + "integrity": "sha512-EvGK8EJ3DhaHfbRlETOWAS5pO9MZITeauHKJyb8wyajUfQUenkIg2MvLDTZ4T/TgIcm3HU0TFBgWWboAZ30UHg==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=18" + } + }, + "node_modules/xmlchars": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/xmlchars/-/xmlchars-2.2.0.tgz", + "integrity": "sha512-JZnDKK8B0RCDw84FNdDAIpZK+JuJw+s7Lz8nksI7SIuU3UXJJslUthsi+uWBUYOwPFwW7W7PRLRfUKpxjtjFCw==", + "dev": true, + "license": "MIT" + }, "node_modules/yaml": { "version": "2.8.0", "resolved": "https://registry.npmjs.org/yaml/-/yaml-2.8.0.tgz", diff --git a/crates/base-map/map/package.json b/crates/base-map/map/package.json index 58c4a40..42c2b16 100644 --- a/crates/base-map/map/package.json +++ b/crates/base-map/map/package.json @@ -8,7 +8,9 @@ "build": "tsc -b && vite build", "lint": "eslint ", "preview": "vite preview", - "background-server": "(cd ../ && cargo run &)" + "background-server": "(cd ../ && cargo run &)", + "test": "vitest", + "test:run": "vitest run" }, "dependencies": { "next-themes": "^0.4.6", @@ -20,23 +22,27 @@ "@chakra-ui/react": "^3.21.1", "@emotion/react": "^11.14.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/react": "^19.1.8", "@types/react-dom": "^19.1.6", "@vitejs/plugin-react-swc": "^3.10.2", + "bevy_flurx_api": "^0.1.0", "eslint": "^9.29.0", "eslint-plugin-react-hooks": "^5.2.0", "eslint-plugin-react-refresh": "^0.4.20", + "geojson": "^0.5.0", "globals": "^16.2.0", "js-cookie": "^3.0.5", + "jsdom": "^26.1.0", "mapbox-gl": "^3.13.0", "react-map-gl": "^8.0.4", "typescript": "~5.8.3", "typescript-eslint": "^8.34.1", "vite": "^7.0.0", - "@tauri-apps/plugin-geolocation": "^2.3.0", "vite-tsconfig-paths": "^5.1.4", - "bevy_flurx_api": "^0.1.0", - "geojson": "^0.5.0" + "vitest": "^3.2.4" } } diff --git a/crates/base-map/map/src/MapNext.tsx b/crates/base-map/map/src/MapNext.tsx index 2cff6f5..e5c6944 100644 --- a/crates/base-map/map/src/MapNext.tsx +++ b/crates/base-map/map/src/MapNext.tsx @@ -1,15 +1,19 @@ -import {useState, useMemo, useEffect} from 'react'; +import {useState, useMemo, useEffect, useCallback, useRef} from 'react'; import Map, { Marker, Popup, NavigationControl, FullscreenControl, ScaleControl, - GeolocateControl + GeolocateControl, + type MapRef } from 'react-map-gl/mapbox'; import ControlPanel from './control-panel.tsx'; import Pin from './pin.tsx'; +import VesselMarker from './vessel-marker.tsx'; +import { type VesselData } from './real-ais-provider.tsx'; +import { useRealAISProvider } from './real-ais-provider.tsx'; import PORTS from './test_data/nautical-base-data.json'; import {Box} from "@chakra-ui/react"; @@ -28,6 +32,65 @@ export interface Geolocation { export default function MapNext(props: any = {mapboxPublicKey: "", geolocation: Geolocation, vesselPosition: undefined, layer: undefined, mapView: undefined} as any) { const [popupInfo, setPopupInfo] = useState(null); + const [vesselPopupInfo, setVesselPopupInfo] = useState(null); + const [boundingBox, setBoundingBox] = useState<{sw_lat: number, sw_lon: number, ne_lat: number, ne_lon: number} | undefined>(undefined); + const [userLocationLoaded, setUserLocationLoaded] = useState(false); + const [mapFocused, setMapFocused] = useState(false); + const mapRef = useRef(null); + + // Use the real AIS provider with bounding box, user location, and map focus status + const { vessels } = useRealAISProvider(boundingBox, userLocationLoaded, mapFocused); + + + useEffect(() => { + console.log("vessles", vessels); + }, [vessels]); + + // Function to update bounding box from map bounds + const updateBoundingBox = useCallback(() => { + if (mapRef.current) { + const map = mapRef.current.getMap(); + const bounds = map.getBounds(); + if (bounds) { + const sw = bounds.getSouthWest(); + const ne = bounds.getNorthEast(); + + setBoundingBox({ + sw_lat: sw.lat, + sw_lon: sw.lng, + ne_lat: ne.lat, + ne_lon: ne.lng + }); + } + } + }, []); + + // Handle map move events + const handleMapMove = useCallback(() => { + updateBoundingBox(); + }, [updateBoundingBox]); + + // Initialize bounding box when map loads + const handleMapLoad = useCallback(() => { + updateBoundingBox(); + }, [updateBoundingBox]); + + // Handle user location events + const handleGeolocate = useCallback((position: GeolocationPosition) => { + console.log('User location loaded:', position); + setUserLocationLoaded(true); + setMapFocused(true); // When geolocate succeeds, the map focuses on user location + }, []); + + const handleTrackUserLocationStart = useCallback(() => { + console.log('Started tracking user location'); + // User location tracking started, but not necessarily loaded yet + }, []); + + const handleTrackUserLocationEnd = useCallback(() => { + console.log('Stopped tracking user location'); + setMapFocused(false); + }, []); const pins = useMemo( () => @@ -55,6 +118,29 @@ export default function MapNext(props: any = {mapboxPublicKey: "", geolocation: [] ); + const vesselMarkers = useMemo( + () => + vessels.map((vessel) => ( + { + e.originalEvent.stopPropagation(); + setVesselPopupInfo(vessel); + }} + > + + + )), + [vessels] + ); + useEffect(() => { console.log("props.vesselPosition", props?.vesselPosition); @@ -64,6 +150,7 @@ export default function MapNext(props: any = {mapboxPublicKey: "", geolocation: return ( - + {pins} + {vesselMarkers} {popupInfo && ( )} + {vesselPopupInfo && ( + setVesselPopupInfo(null)} + > +
+

+ {vesselPopupInfo.name} +

+
+
Type: {vesselPopupInfo.type}
+
MMSI: {vesselPopupInfo.mmsi}
+
Call Sign: {vesselPopupInfo.callSign}
+
Speed: {vesselPopupInfo.speed.toFixed(1)} knots
+
Heading: {vesselPopupInfo.heading.toFixed(0)}°
+
Length: {vesselPopupInfo.length.toFixed(0)}m
+ {vesselPopupInfo.destination && ( +
Destination: {vesselPopupInfo.destination}
+ )} + {vesselPopupInfo.eta && ( +
ETA: {vesselPopupInfo.eta}
+ )} +
+ Last Update: {vesselPopupInfo.lastUpdate.toLocaleTimeString()} +
+
+
+
+ )} +
diff --git a/crates/base-map/map/src/real-ais-provider.tsx b/crates/base-map/map/src/real-ais-provider.tsx new file mode 100644 index 0000000..3369a67 --- /dev/null +++ b/crates/base-map/map/src/real-ais-provider.tsx @@ -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([]); + const [isActive, setIsActive] = useState(true); + const [isLoading, setIsLoading] = useState(false); + const [error, setError] = useState(null); + const [isConnected, setIsConnected] = useState(false); + const [aisStreamStarted, setAisStreamStarted] = useState(false); + + const wsRef = useRef(null); + const lastBoundingBoxRef = useRef(undefined); + const reconnectTimeoutRef = useRef(null); + const vesselMapRef = useRef>(new Map()); + + // Connect to WebSocket + const connectWebSocket = useCallback(() => { + // Prevent multiple connections + if (!isActive) return; + + // Check if we already have an active or connecting WebSocket + if (wsRef.current && + (wsRef.current.readyState === WebSocket.OPEN || + wsRef.current.readyState === WebSocket.CONNECTING)) { + console.log('WebSocket already connected or connecting, skipping...'); + return; + } + + // Close any existing connection before creating a new one + if (wsRef.current) { + wsRef.current.close(); + wsRef.current = null; + } + + setIsLoading(true); + setError(null); + + try { + console.log('Creating new WebSocket connection...'); + const ws = new WebSocket('ws://localhost:3000/ws'); + wsRef.current = ws; + + ws.onopen = () => { + console.log('Connected to AIS WebSocket'); + setIsConnected(true); + setIsLoading(false); + setError(null); + + // Send bounding box configuration if available + // Note: We'll send the bounding box separately to avoid connection recreation + const currentBoundingBox = lastBoundingBoxRef.current; + if (currentBoundingBox) { + const message: WebSocketMessage = { + type: 'set_bounding_box', + bounding_box: currentBoundingBox + }; + ws.send(JSON.stringify(message)); + console.log('Sent initial bounding box configuration:', currentBoundingBox); + } + }; + + ws.onmessage = (event) => { + try { + const data = JSON.parse(event.data); + + // Handle connection confirmation and bounding box confirmations + if (typeof data === 'string' || data.type) { + console.log('Received WebSocket message:', data); + return; + } + const vesselData = convertAisResponseToVesselData(data); + if (vesselData) { + // Update vessel map for efficient updates + vesselMapRef.current.set(vesselData.mmsi, vesselData); + + // Update vessels state with current map values + setVessels(Array.from(vesselMapRef.current.values())); + } + } catch (err) { + console.error('Error parsing WebSocket message:', err); + } + }; + + ws.onerror = (error) => { + console.error('WebSocket error:', error); + setError('WebSocket connection error'); + setIsConnected(false); + }; + + ws.onclose = (event) => { + console.log('WebSocket connection closed:', event.code, event.reason); + setIsConnected(false); + setIsLoading(false); + + // Attempt to reconnect if the connection was active + if (isActive && !event.wasClean) { + setError('Connection lost, attempting to reconnect...'); + reconnectTimeoutRef.current = setTimeout(() => { + connectWebSocket(); + }, 3000); // Reconnect after 3 seconds + } + }; + + } catch (err) { + console.error('Error creating WebSocket connection:', err); + setError(err instanceof Error ? err.message : 'Unknown WebSocket error'); + setIsLoading(false); + } + }, [isActive]); // Removed boundingBox dependency to prevent reconnections + + // Send bounding box update to WebSocket + const updateBoundingBox = useCallback((bbox: BoundingBox) => { + if (wsRef.current?.readyState === WebSocket.OPEN) { + const message: WebSocketMessage = { + type: 'set_bounding_box', + bounding_box: bbox + }; + wsRef.current.send(JSON.stringify(message)); + console.log('Updated bounding box:', bbox); + } + }, []); + + // Send start AIS stream message to WebSocket + const startAisStream = useCallback(() => { + if (wsRef.current?.readyState === WebSocket.OPEN && !aisStreamStarted) { + const message: WebSocketMessage = { + type: 'start_ais_stream' + }; + wsRef.current.send(JSON.stringify(message)); + console.log('Sent start AIS stream request'); + setAisStreamStarted(true); + } + }, [aisStreamStarted]); + + // Connect to WebSocket when component mounts or becomes active + useEffect(() => { + if (isActive) { + connectWebSocket(); + } + + return () => { + if (reconnectTimeoutRef.current) { + clearTimeout(reconnectTimeoutRef.current); + } + if (wsRef.current) { + wsRef.current.close(); + wsRef.current = null; + } + }; + }, [isActive, connectWebSocket]); + + // Handle bounding box changes + useEffect(() => { + if (!boundingBox || !isActive) return; + + // Check if bounding box actually changed to avoid unnecessary updates + const lastBbox = lastBoundingBoxRef.current; + if (lastBbox && + lastBbox.sw_lat === boundingBox.sw_lat && + lastBbox.sw_lon === boundingBox.sw_lon && + lastBbox.ne_lat === boundingBox.ne_lat && + lastBbox.ne_lon === boundingBox.ne_lon) { + return; + } + + lastBoundingBoxRef.current = boundingBox; + + // Clear existing vessels when bounding box changes + vesselMapRef.current.clear(); + setVessels([]); + + // Send new bounding box to WebSocket + updateBoundingBox(boundingBox); + }, [boundingBox, updateBoundingBox, isActive]); + + // Handle active state changes + useEffect(() => { + if (!isActive) { + // Close WebSocket connection when inactive + if (wsRef.current) { + wsRef.current.close(); + wsRef.current = null; + } + setIsConnected(false); + setError(null); + + // Clear reconnection timeout + if (reconnectTimeoutRef.current) { + clearTimeout(reconnectTimeoutRef.current); + reconnectTimeoutRef.current = null; + } + } + }, [isActive]); + + // Start AIS stream when user location is loaded and map is focused + useEffect(() => { + if (userLocationLoaded && mapFocused && isConnected && !aisStreamStarted) { + console.log('User location loaded and map focused, starting AIS stream...'); + startAisStream(); + } + }, [userLocationLoaded, mapFocused, isConnected, aisStreamStarted, startAisStream]); + + return { + vessels, + isActive, + setIsActive, + isLoading, + error, + isConnected, + refreshVessels: () => { + // For WebSocket, we can trigger a reconnection to refresh data + if (wsRef.current) { + wsRef.current.close(); + } + connectWebSocket(); + } + }; +}; \ No newline at end of file diff --git a/crates/base-map/map/src/vessel-marker.tsx b/crates/base-map/map/src/vessel-marker.tsx new file mode 100644 index 0000000..80aa3a9 --- /dev/null +++ b/crates/base-map/map/src/vessel-marker.tsx @@ -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 ( + + + {/* Small arrow to indicate heading */} + + + ); +} + +export default React.memo(VesselMarker); \ No newline at end of file diff --git a/crates/base-map/map/test/real-ais-provider.test.tsx b/crates/base-map/map/test/real-ais-provider.test.tsx new file mode 100644 index 0000000..25af71d --- /dev/null +++ b/crates/base-map/map/test/real-ais-provider.test.tsx @@ -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 + }) + ); + }); +}); \ No newline at end of file diff --git a/crates/base-map/map/test/test-setup.ts b/crates/base-map/map/test/test-setup.ts new file mode 100644 index 0000000..75c2713 --- /dev/null +++ b/crates/base-map/map/test/test-setup.ts @@ -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(), +}; \ No newline at end of file diff --git a/crates/base-map/map/tsconfig.app.json b/crates/base-map/map/tsconfig.app.json index 3546f30..d6552b5 100644 --- a/crates/base-map/map/tsconfig.app.json +++ b/crates/base-map/map/tsconfig.app.json @@ -27,5 +27,6 @@ }, "include": [ "src" - ] + ], + "exclude": ["**/*.test.ts"] } diff --git a/crates/base-map/map/tsconfig.json b/crates/base-map/map/tsconfig.json index c957027..7acb747 100644 --- a/crates/base-map/map/tsconfig.json +++ b/crates/base-map/map/tsconfig.json @@ -4,4 +4,5 @@ { "path": "./tsconfig.app.json"}, { "path": "./tsconfig.node.json"} ], + } diff --git a/crates/base-map/map/vitest.config.ts b/crates/base-map/map/vitest.config.ts new file mode 100644 index 0000000..e6d847e --- /dev/null +++ b/crates/base-map/map/vitest.config.ts @@ -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, + }, +}); \ No newline at end of file diff --git a/package-lock.json b/package-lock.json new file mode 100644 index 0000000..4c93a7f --- /dev/null +++ b/package-lock.json @@ -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 + } + } + } + } +} diff --git a/package.json b/package.json new file mode 100644 index 0000000..31fef03 --- /dev/null +++ b/package.json @@ -0,0 +1,5 @@ +{ + "dependencies": { + "ws": "^8.18.3" + } +} diff --git a/test_bounding_box.js b/test_bounding_box.js new file mode 100644 index 0000000..5e90e19 --- /dev/null +++ b/test_bounding_box.js @@ -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); \ No newline at end of file diff --git a/test_browser_websocket.html b/test_browser_websocket.html new file mode 100644 index 0000000..add55ba --- /dev/null +++ b/test_browser_websocket.html @@ -0,0 +1,134 @@ + + + + + + AIS WebSocket Browser Test + + + +

AIS WebSocket Browser Test

+
Disconnected
+ + + + +

Messages

+
+ + + + \ No newline at end of file diff --git a/test_integration.sh b/test_integration.sh new file mode 100755 index 0000000..447cdf2 --- /dev/null +++ b/test_integration.sh @@ -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." \ No newline at end of file diff --git a/test_websocket.js b/test_websocket.js new file mode 100644 index 0000000..54f6014 --- /dev/null +++ b/test_websocket.js @@ -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); \ No newline at end of file