diff --git a/Cargo.lock b/Cargo.lock index 7b77d43..9052c71 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -118,6 +118,26 @@ 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", + "tokio-util", + "tower 0.4.13", + "tower-http 0.5.2", + "url", +] + [[package]] name = "aligned-vec" version = "0.6.4" @@ -291,6 +311,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 +488,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 +572,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 +616,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 +634,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 +654,7 @@ dependencies = [ "async-trait", "bytes", "futures-util", - "http", + "http 1.3.1", "http-body", "http-body-util", "mime", @@ -600,6 +663,7 @@ dependencies = [ "sync_wrapper", "tower-layer", "tower-service", + "tracing", ] [[package]] @@ -610,7 +674,7 @@ checksum = "077959a7f8cf438676af90b483304528eb7e16eadadb7f44e9ada4f9dceb9e62" dependencies = [ "axum-core", "chrono", - "http", + "http 1.3.1", "mime_guess", "rust-embed", "tower-service", @@ -627,6 +691,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 +747,7 @@ dependencies = [ "serde_json", "tao", "tokio", - "tower-http", + "tower-http 0.6.6", "tracing", "tracing-subscriber", "web-sys", @@ -2051,6 +2144,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 +2621,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 +3676,7 @@ dependencies = [ "fnv", "futures-core", "futures-sink", - "http", + "http 1.3.1", "indexmap 2.10.0", "slab", "tokio", @@ -3678,6 +3786,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 +3815,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1efedce1fb8e6913f23e0c92de8e62cd5b772a67e7b3946df930a62566c93184" dependencies = [ "bytes", - "http", + "http 1.3.1", ] [[package]] @@ -3707,7 +3826,7 @@ checksum = "b021d93e26becf5dc7e1b75b1bed1fd93124b374ceb73f43d4d4eafec896a64a" dependencies = [ "bytes", "futures-core", - "http", + "http 1.3.1", "http-body", "pin-project-lite", ] @@ -3740,7 +3859,7 @@ dependencies = [ "futures-channel", "futures-util", "h2", - "http", + "http 1.3.1", "http-body", "httparse", "httpdate", @@ -3757,7 +3876,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 +3913,7 @@ dependencies = [ "futures-channel", "futures-core", "futures-util", - "http", + "http 1.3.1", "http-body", "hyper", "ipnet", @@ -4535,6 +4654,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 +5843,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 +6308,7 @@ dependencies = [ "encoding_rs", "futures-core", "h2", - "http", + "http 1.3.1", "http-body", "http-body-util", "hyper", @@ -6175,8 +6328,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 +6337,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 +6451,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 +6789,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 +6850,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 +7597,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 +7725,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 +7754,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 +7785,7 @@ dependencies = [ "bytes", "futures-core", "futures-util", - "http", + "http 1.3.1", "http-body", "http-body-util", "http-range-header", @@ -7519,7 +7797,7 @@ dependencies = [ "pin-project-lite", "tokio", "tokio-util", - "tower", + "tower 0.5.2", "tower-layer", "tower-service", "tracing", @@ -7544,6 +7822,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 +7937,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 +9320,7 @@ dependencies = [ "gdkx11", "gtk", "html5ever", - "http", + "http 1.3.1", "javascriptcore-rs", "jni", "kuchikiki", @@ -9109,6 +9426,7 @@ checksum = "6fd8403733700263c6eb89f192880191f1b83e332f7a20371ddcf421c4a337c7" name = "yachtpit" version = "0.1.0" dependencies = [ + "anyhow", "base-map", "bevy", "bevy_asset_loader", @@ -9134,6 +9452,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..adefb0a --- /dev/null +++ b/crates/ais/Cargo.toml @@ -0,0 +1,30 @@ +[package] +name = "ais" +version = "0.1.0" +edition = "2021" + +[[bin]] +name = "ais" +path = "src/main.rs" + +[profile.dev] +debug = false + +[dependencies] +tokio = { version = "1.0", features = ["full"] } +tokio-tungstenite = { version = "0.20", features = ["native-tls"] } +serde = { version = "1.0", features = ["derive"] } +serde_json = "1.0" +futures-util = "0.3" +url = "2.4" +axum = { version = "0.7", features = ["ws"] } +tower = "0.4" +tower-http = { version = "0.5", features = ["cors"] } +base64 = "0.22.1" +tokio-util = "0.7.15" + +[dev-dependencies] +tokio-test = "0.4" +axum-test = "14.0" +mockito = "1.0" +serde_json = "1.0" \ 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..9d0e713 --- /dev/null +++ b/crates/ais/src/ais.rs @@ -0,0 +1,1007 @@ +use axum::{ + extract::{ws::{Message as WsMessage, WebSocket}, Query, State, WebSocketUpgrade}, + http::StatusCode, + response::{Json, Response} + + , +}; +use base64::{engine::general_purpose::STANDARD, Engine as _}; +use futures_util::{SinkExt, StreamExt}; +use serde::{Deserialize, Serialize}; +use serde_json::Value; +use std::sync::Arc; +use tokio::{ + sync::{broadcast, Mutex}, + task::JoinHandle, +}; +use tokio_tungstenite::{connect_async, tungstenite::protocol::Message}; +use tokio_util::sync::CancellationToken; +use url::Url; + + +#[derive(Serialize, Deserialize, Debug)] +pub 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)] +pub 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)] +pub 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)] +pub struct WebSocketMessage { + #[serde(rename = "type")] + message_type: String, + #[serde(skip_serializing_if = "Option::is_none")] + bounding_box: Option, +} + +#[derive(Serialize, Deserialize, Clone, Debug)] +pub 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, +} + +// Manages the lifecycle of the upstream AIS stream. +pub struct AisStreamManager { + state: Mutex, +} + +// The internal state of the manager, protected by a Mutex. +#[derive(Default)] +struct ManagerState { + tx: Option>, + stream_task: Option>, + cancellation_token: Option, + client_count: usize, +} + +impl AisStreamManager { + pub(crate) fn new() -> Self { + Self { + state: Mutex::new(ManagerState::default()), + } + } + + // Starts the AIS stream if it's not already running. + // This is called by the first client that connects. + async fn start_stream_if_needed(&self) -> broadcast::Sender { + let mut state = self.state.lock().await; + + state.client_count += 1; + println!("Client connected. Total clients: {}", state.client_count); + + if state.stream_task.is_none() { + println!("Starting new AIS stream..."); + let (tx, _) = broadcast::channel(1000); + let token = CancellationToken::new(); + + let stream_task = tokio::spawn(connect_to_ais_stream_with_broadcast( + tx.clone(), + token.clone(), + )); + + state.tx = Some(tx.clone()); + state.stream_task = Some(stream_task); + state.cancellation_token = Some(token); + println!("AIS stream started."); + tx + } else { + // Stream is already running, return the existing sender. + state.tx.as_ref().unwrap().clone() + } + } + + // Stops the AIS stream if no clients are connected. + async fn stop_stream_if_unneeded(&self) { + let mut state = self.state.lock().await; + + state.client_count -= 1; + println!("Client disconnected. Total clients: {}", state.client_count); + + if state.client_count == 0 { + println!("Last client disconnected. Stopping AIS stream..."); + if let Some(token) = state.cancellation_token.take() { + token.cancel(); + } + if let Some(task) = state.stream_task.take() { + // Wait for the task to finish to ensure clean shutdown. + let _ = task.await; + } + state.tx = None; + println!("AIS stream stopped."); + } + } +} + +// An RAII guard to ensure we decrement the client count when a connection is dropped. +struct ConnectionGuard { + manager: Arc, +} + +impl Drop for ConnectionGuard { + fn drop(&mut self) { + let manager = self.manager.clone(); + tokio::spawn(async move { + manager.stop_stream_if_unneeded().await; + }); + } +} + + +// Shared state for the application +#[derive(Clone)] +pub struct AppState { + pub(crate) ais_stream_manager: 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 +pub(crate) async fn get_ais_data( + Query(params): Query, + State(_state): State, +) -> Result>, StatusCode> { + println!("Received bounding box request: {:?}", params); + + // This remains a placeholder. A full implementation could query a database + // populated by the AIS stream. + + 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 +pub(crate) async fn websocket_handler( + ws: WebSocketUpgrade, + State(state): State, +) -> Response { + ws.on_upgrade(|socket| handle_websocket(socket, state.ais_stream_manager)) +} + +// 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, manager: Arc) { + // This guard ensures that when the function returns (and the connection closes), + // the client count is decremented. + let _guard = ConnectionGuard { manager: manager.clone() }; + + // Start the stream if it's the first client, and get a sender. + let ais_tx = manager.start_stream_if_needed().await; + let mut ais_rx = ais_tx.subscribe(); + + // 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() { + return; + } + + // Handle incoming messages and broadcast AIS data + loop { + tokio::select! { + // Handle incoming messages from the client (e.g., to set a bounding box) + msg = socket.recv() => { + match msg { + Some(Ok(WsMessage::Text(text))) => { + // Try to parse as a command message + if let Ok(ws_msg) = serde_json::from_str::(&text) { + if ws_msg.message_type == "set_bounding_box" { + if let Some(bbox) = ws_msg.bounding_box { + println!("Setting bounding box: {:?}", bbox); + bounding_box = Some(bbox); + } else { + println!("Clearing bounding box"); + bounding_box = None; + } + } + } else { + // Echo back unrecognized messages + if socket.send(WsMessage::Text(format!("Echo: {}", text))).await.is_err() { + break; + } + } + } + Some(Ok(WsMessage::Close(_))) => break, // Client disconnected + Some(Err(e)) => { + println!("WebSocket error: {:?}", e); + break; + } + None => break, // Connection closed + _ => {} // Ignore other message types + } + } + // Forward AIS data from the broadcast channel to the client + ais_data_result = ais_rx.recv() => { + match ais_data_result { + Ok(data) => { + // Apply bounding box filter if it exists + let should_send = bounding_box.as_ref() + .map(|bbox| is_within_bounding_box(&data, bbox)) + .unwrap_or(true); // Send if no bbox is set + + if should_send { + if let Ok(json_data) = serde_json::to_string(&data) { + if socket.send(WsMessage::Text(json_data)).await.is_err() { + // Client is likely disconnected + break; + } + } + } + } + Err(broadcast::error::RecvError::Lagged(n)) => { + println!("WebSocket client lagged behind by {} messages", n); + } + Err(broadcast::error::RecvError::Closed) => { + // This happens if the sender is dropped, e.g., during stream shutdown. + break; + } + } + } + } + } +} + + + +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" + } +} + + +// Connects to the AIS stream and broadcasts messages. +// Shuts down when the cancellation_token is triggered. +async fn connect_to_ais_stream_with_broadcast( + tx: broadcast::Sender, + cancellation_token: CancellationToken, +) { + loop { + tokio::select! { + // Check if the task has been cancelled. + _ = cancellation_token.cancelled() => { + println!("Cancellation signal received. Shutting down AIS stream connection."); + return; + } + // Try to connect and process messages. + result = connect_and_process_ais_stream(&tx, &cancellation_token) => { + if let Err(e) = result { + eprintln!("AIS stream error: {}. Reconnecting in 5 seconds...", e); + } + // If the connection drops, wait before retrying, but still listen for cancellation. + tokio::select! { + _ = tokio::time::sleep(tokio::time::Duration::from_secs(5)) => {}, + _ = cancellation_token.cancelled() => { + println!("Cancellation signal received during reconnect wait. Shutting down."); + return; + } + } + } + } + } +} + + +async fn connect_and_process_ais_stream( + tx: &broadcast::Sender, + cancellation_token: &CancellationToken +) -> Result<(), Box> { // <--- THE FIX IS HERE + + let url = Url::parse("wss://stream.aisstream.io/v0/stream")?; + let (ws_stream, _) = connect_async(url).await.map_err(|e| format!("WebSocket connection failed: {}", e))?; + println!("Upstream WebSocket connection to aisstream.io opened."); + + let (mut sender, mut receiver) = ws_stream.split(); + + let key = "MDc4YzY5NTdkMGUwM2UzMzQ1Zjc5NDFmOTA1ODg4ZTMyOGQ0MjM0MA=="; + let subscription_message = SubscriptionMessage { + apikey: STANDARD.decode(key) + .ok() + .and_then(|bytes| String::from_utf8(bytes).ok()) + .unwrap_or_default(), + bounding_boxes: vec![vec![[-90.0, -180.0], [90.0, 180.0]]], // Global coverage + filters_ship_mmsi: vec![], + }; + + let message_json = serde_json::to_string(&subscription_message)?; + sender.send(Message::Text(message_json)).await?; + println!("Upstream subscription message sent."); + + loop { + tokio::select! { + // Forward messages from upstream + message = receiver.next() => { + match message { + Some(Ok(msg)) => { + if process_upstream_message(msg, tx).is_err() { + // If there's a critical error processing, break to reconnect + break; + } + }, + Some(Err(e)) => { + eprintln!("Upstream WebSocket error: {}", e); + return Err(e.into()); + }, + None => { + println!("Upstream WebSocket connection closed."); + return Ok(()); // Connection closed normally + } + } + } + // Listen for the shutdown signal + _ = cancellation_token.cancelled() => { + println!("Closing upstream WebSocket connection due to cancellation."); + let _ = sender.send(Message::Close(None)).await; + return Ok(()); + } + } + } + Ok(()) +} + +fn process_upstream_message( + msg: Message, + tx: &broadcast::Sender, +) -> Result<(), ()> { + let text = match msg { + Message::Text(text) => text, + Message::Binary(data) => String::from_utf8_lossy(&data).to_string(), + Message::Ping(_) | Message::Pong(_) | Message::Close(_) => return Ok(()), + Message::Frame(_) => return Ok(()), + }; + + if let Ok(ais_message) = serde_json::from_str::(&text) { + let parsed_message = parse_ais_message(&ais_message); + // The broadcast send will fail if there are no receivers, which is fine. + let _ = tx.send(parsed_message); + } else { + eprintln!("Failed to parse JSON from upstream: {}", text); + } + Ok(()) +} + + +// Graceful shutdown signal handler +pub async fn shutdown_signal() { + let ctrl_c = async { + tokio::signal::ctrl_c() + .await + .expect("failed to install Ctrl+C handler"); + }; + + #[cfg(unix)] + let terminate = async { + tokio::signal::unix::signal(tokio::signal::unix::SignalKind::terminate()) + .expect("failed to install signal handler") + .recv() + .await; + }; + + #[cfg(not(unix))] + let terminate = std::future::pending::<()>(); + + tokio::select! { + _ = ctrl_c => {}, + _ = terminate => {}, + } + + println!("Signal received, starting graceful shutdown"); +} + + + + + +#[cfg(test)] +mod tests { + use super::*; + use crate::create_router; + use axum_test::TestServer; + use serde_json::json; + + #[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 state = AppState { + ais_stream_manager: Arc::new(AisStreamManager::new()), + }; + + // 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 state = AppState { + ais_stream_manager: Arc::new(AisStreamManager::new()), + }; + + // 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 state = AppState { + ais_stream_manager: Arc::new(AisStreamManager::new()), + }; + + // 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 state = AppState { + ais_stream_manager: Arc::new(AisStreamManager::new()), + }; + // Test that the manager is accessible. + assert_eq!(state.ais_stream_manager.state.lock().await.client_count, 0); + } + + #[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 state = AppState { + ais_stream_manager: Arc::new(AisStreamManager::new()), + }; + + // Create test server + let app = create_router(state); + let server = TestServer::new(app).unwrap(); + + // The websocket endpoint should return 400 Bad Request + // when accessed via HTTP GET without proper websocket headers + let response = server.get("/ws").await; + 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)); + } +} \ 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..889caa5 --- /dev/null +++ b/crates/ais/src/main.rs @@ -0,0 +1,36 @@ +use std::sync::Arc; +use axum::Router; +use axum::routing::get; +use tower_http::cors::CorsLayer; +use crate::ais::{AisStreamManager, AppState}; + +mod ais; + +#[tokio::main] +async fn main() -> Result<(), Box> { + // Create the shared state with the AIS stream manager + let state = AppState { + ais_stream_manager: Arc::new(AisStreamManager::new()), + }; + + // Create and start the Axum HTTP server + let app = create_router(state); + let listener = tokio::net::TcpListener::bind("0.0.0.0:3000").await?; + + println!("AIS server running on http://0.0.0.0:3000"); + + axum::serve(listener, app) + .with_graceful_shutdown(ais::shutdown_signal()) + .await?; + + Ok(()) +} + +// Create the Axum router +fn create_router(state: AppState) -> Router { + Router::new() + .route("/ais", get(crate::ais::get_ais_data)) + .route("/ws", get(crate::ais::websocket_handler)) + .layer(CorsLayer::permissive()) + .with_state(state) +} \ 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..0745087 100644 --- a/crates/base-map/map/package-lock.json +++ b/crates/base-map/map/package-lock.json @@ -11,13 +11,17 @@ "next-themes": "^0.4.6", "react": "^19.1.0", "react-dom": "^19.1.0", - "react-icons": "^5.5.0" + "react-icons": "^5.5.0", + "socket.io-client": "^4.8.1", + "ws": "^8.18.3" }, "devDependencies": { "@chakra-ui/react": "^3.21.1", "@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 +33,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 +121,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 +307,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", @@ -1572,6 +1714,12 @@ "win32" ] }, + "node_modules/@socket.io/component-emitter": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/@socket.io/component-emitter/-/component-emitter-3.1.2.tgz", + "integrity": "sha512-9BCxFwvbGg/RsZK9tjXd8s4UcwR0MWeFQ1XEKIQVVvAGJyINdrqKMcTRyLoK8Rse1GjzLV9cwjWV1olXRWEXVA==", + "license": "MIT" + }, "node_modules/@swc/core": { "version": "1.12.14", "resolved": "https://registry.npmjs.org/@swc/core/-/core-1.12.14.tgz", @@ -1829,6 +1977,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 +2503,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 +3542,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 +3569,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 +3603,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 +3623,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 +3722,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 +3742,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 +3783,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 +3869,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 +3883,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 +3904,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 +3936,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 +3960,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 +3991,79 @@ "dev": true, "license": "ISC" }, + "node_modules/engine.io-client": { + "version": "6.6.3", + "resolved": "https://registry.npmjs.org/engine.io-client/-/engine.io-client-6.6.3.tgz", + "integrity": "sha512-T0iLjnyNWahNyv/lcjS2y4oE358tVS/SYQNxYXGAJ9/GLgH4VCvOQ/mhTjqU88mLZCQgiG8RIegFHYCdVC+j5w==", + "license": "MIT", + "dependencies": { + "@socket.io/component-emitter": "~3.1.0", + "debug": "~4.3.1", + "engine.io-parser": "~5.2.1", + "ws": "~8.17.1", + "xmlhttprequest-ssl": "~2.1.1" + } + }, + "node_modules/engine.io-client/node_modules/debug": { + "version": "4.3.7", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.7.tgz", + "integrity": "sha512-Er2nc/H7RrMXZBFCEim6TCmMk02Z8vLC2Rbi1KEBggpo0fS6l0S1nnapwmIi3yW/+GOJap1Krg4w0Hg80oCqgQ==", + "license": "MIT", + "dependencies": { + "ms": "^2.1.3" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/engine.io-client/node_modules/ws": { + "version": "8.17.1", + "resolved": "https://registry.npmjs.org/ws/-/ws-8.17.1.tgz", + "integrity": "sha512-6XQFvXTkbfUOZOKKILFG1PDK2NDQs4azKQl26T0YS5CxqWLgXajbPZ+h4gZekJyRqFU8pvnbAbbs/3TgRPy+GQ==", + "license": "MIT", + "engines": { + "node": ">=10.0.0" + }, + "peerDependencies": { + "bufferutil": "^4.0.1", + "utf-8-validate": ">=5.0.2" + }, + "peerDependenciesMeta": { + "bufferutil": { + "optional": true + }, + "utf-8-validate": { + "optional": true + } + } + }, + "node_modules/engine.io-parser": { + "version": "5.2.3", + "resolved": "https://registry.npmjs.org/engine.io-parser/-/engine.io-parser-5.2.3.tgz", + "integrity": "sha512-HqD3yTBfnBxIrbnM1DoD6Pcq8NECnh8d4As1Qgh0z5Gg3jRRIqijury0CL3ghu/edArpUYiYqQiDUQBIs4np3Q==", + "license": "MIT", + "engines": { + "node": ">=10.0.0" + } + }, + "node_modules/entities": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/entities/-/entities-6.0.1.tgz", + "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 +4074,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 +4304,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 +4324,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 +4636,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 +4748,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 +4837,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 +4891,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 +5033,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 +5047,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 +5176,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", @@ -4423,7 +5213,6 @@ "version": "2.1.3", "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", - "dev": true, "license": "MIT" }, "node_modules/murmurhash-js": { @@ -4469,6 +5258,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 +5347,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 +5397,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 +5518,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 +5690,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 +5803,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 +5841,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 +5929,75 @@ "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/socket.io-client": { + "version": "4.8.1", + "resolved": "https://registry.npmjs.org/socket.io-client/-/socket.io-client-4.8.1.tgz", + "integrity": "sha512-hJVXfu3E28NmzGk8o1sHhN3om52tRvwYeidbj7xKy2eIIse5IoKX3USlS6Tqt3BHAtflLIkCQBkzVrEEfWUyYQ==", + "license": "MIT", + "dependencies": { + "@socket.io/component-emitter": "~3.1.0", + "debug": "~4.3.2", + "engine.io-client": "~6.6.1", + "socket.io-parser": "~4.2.4" + }, + "engines": { + "node": ">=10.0.0" + } + }, + "node_modules/socket.io-client/node_modules/debug": { + "version": "4.3.7", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.7.tgz", + "integrity": "sha512-Er2nc/H7RrMXZBFCEim6TCmMk02Z8vLC2Rbi1KEBggpo0fS6l0S1nnapwmIi3yW/+GOJap1Krg4w0Hg80oCqgQ==", + "license": "MIT", + "dependencies": { + "ms": "^2.1.3" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/socket.io-parser": { + "version": "4.2.4", + "resolved": "https://registry.npmjs.org/socket.io-parser/-/socket.io-parser-4.2.4.tgz", + "integrity": "sha512-/GbIKmo8ioc+NIWIhwdecY0ge+qVBSMdgxGygevmdHj24bsfgtCmcUUcQ5ZzcylGFHsN3k4HB4Cgkl96KVnuew==", + "license": "MIT", + "dependencies": { + "@socket.io/component-emitter": "~3.1.0", + "debug": "~4.3.1" + }, + "engines": { + "node": ">=10.0.0" + } + }, + "node_modules/socket.io-parser/node_modules/debug": { + "version": "4.3.7", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.7.tgz", + "integrity": "sha512-Er2nc/H7RrMXZBFCEim6TCmMk02Z8vLC2Rbi1KEBggpo0fS6l0S1nnapwmIi3yW/+GOJap1Krg4w0Hg80oCqgQ==", + "license": "MIT", + "dependencies": { + "ms": "^2.1.3" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, "node_modules/sort-asc": { "version": "0.2.0", "resolved": "https://registry.npmjs.org/sort-asc/-/sort-asc-0.2.0.tgz", @@ -5129,6 +6103,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 +6143,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 +6206,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 +6272,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 +6289,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 +6342,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 +6585,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 +6656,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 +6754,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 +6830,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 +6857,52 @@ "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==", + "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/xmlhttprequest-ssl": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/xmlhttprequest-ssl/-/xmlhttprequest-ssl-2.1.2.tgz", + "integrity": "sha512-TEU+nJVUUnA4CYJFLvK5X9AOeH4KvDvhIfm0vV1GaQRtchnG0hgK5p8hw/xjv8cunWYCsiPCSDzObPyhEwq3KQ==", + "engines": { + "node": ">=0.4.0" + } + }, "node_modules/yaml": { "version": "2.8.0", "resolved": "https://registry.npmjs.org/yaml/-/yaml-2.8.0.tgz", diff --git a/crates/base-map/map/package.json b/crates/base-map/map/package.json index 58c4a40..96bf4a0 100644 --- a/crates/base-map/map/package.json +++ b/crates/base-map/map/package.json @@ -8,35 +8,43 @@ "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", "react": "^19.1.0", "react-dom": "^19.1.0", - "react-icons": "^5.5.0" + "react-icons": "^5.5.0", + "socket.io-client": "^4.8.1", + "ws": "^8.18.3" }, "devDependencies": { "@chakra-ui/react": "^3.21.1", "@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/App.tsx b/crates/base-map/map/src/App.tsx index 6ef2801..3a3e184 100644 --- a/crates/base-map/map/src/App.tsx +++ b/crates/base-map/map/src/App.tsx @@ -1,124 +1,24 @@ import 'mapbox-gl/dist/mapbox-gl.css'; -import {Box, Button, HStack, Input} from '@chakra-ui/react'; +import {Box, Button, HStack, Text} from '@chakra-ui/react'; +import {useColorMode} from './components/ui/color-mode'; import {useCallback, useEffect, useState} from "react"; -import MapNext, {type Geolocation} from "@/MapNext.tsx"; +import MapNext from "@/MapNext.tsx"; +import {getNeumorphicColors, getNeumorphicStyle} from './theme/neumorphic-theme'; +import {layers, LayerSelector} from "@/LayerSelector.tsx"; +import {useAISProvider, type VesselData} from './ais-provider'; +import type {GpsPosition, VesselStatus} from './types'; +import {GpsFeed} from "@/components/map/GpsFeedInfo.tsx"; +import {AisFeed} from './components/map/AisFeedInfo'; +import {Search} from "@/components/map/Search.tsx"; +import {SearchResult} from "@/components/map/SearchResult.tsx"; +import {NativeGeolocation} from "@/CustomGeolocate.ts"; // public key const key = 'cGsuZXlKMUlqb2laMlZ2Wm1aelpXVWlMQ0poSWpvaVkycDFOalo0YkdWNk1EUTRjRE41YjJnNFp6VjNNelp6YXlKOS56LUtzS1l0X3VGUGdCSDYwQUFBNFNn'; -const layers = [ - { name: 'OSM', value: 'mapbox://styles/mapbox/dark-v11' }, - { name: 'Satellite', value: 'mapbox://styles/mapbox/satellite-v9' }, -]; - - - -// const vesselLayerStyle: CircleLayerSpecification = { -// id: 'vessel', -// type: 'circle', -// paint: { -// 'circle-radius': 8, -// 'circle-color': '#ff4444', -// 'circle-stroke-width': 2, -// 'circle-stroke-color': '#ffffff' -// }, -// source: '' -// }; - -// Types for bevy_flurx_ipc communication -interface GpsPosition { - latitude: number; - longitude: number; - zoom: number; -} - -interface VesselStatus { - latitude: number; - longitude: number; - heading: number; - speed: number; -} - -export type Layer = { name: string; value: string }; -export type Layers = Layer[]; - -class MyGeolocation implements Geolocation { - constructor({clearWatch, getCurrentPosition, watchPosition}: { - clearWatch: (watchId: number) => void; - getCurrentPosition: (successCallback: PositionCallback, errorCallback?: PositionErrorCallback | null, options?: PositionOptions) => void; - watchPosition: (successCallback: PositionCallback, errorCallback?: PositionErrorCallback | null, options?: PositionOptions) => number; - }) { - this.clearWatch = clearWatch; - this.watchPosition = watchPosition; - this.getCurrentPosition = getCurrentPosition; - } - clearWatch(_watchId: number): void { - throw new Error('Method not implemented.'); - } - getCurrentPosition(_successCallback: PositionCallback, _errorCallback?: PositionErrorCallback | null, _options?: PositionOptions): void { - throw new Error('Method not implemented.'); - } - watchPosition(_successCallback: PositionCallback, _errorCallback?: PositionErrorCallback | null, _options?: PositionOptions): number { - throw new Error('Method not implemented.'); - } - -} - -// interface MapViewParams { -// latitude: number; -// longitude: number; -// zoom: number; -// } - -// interface AuthParams { -// authenticated: boolean; -// token: string | null; -// } - -function LayerSelector(props: { onClick: (e: any) => Promise }) { - const [isOpen, setIsOpen] = useState(false); - - return ( - - - - {isOpen && ( - - {layers.map(layer => ( - { - setIsOpen(false); - await props.onClick(e); - }} - > - {layer.name} - - ))} - - )} - - ); -} - function App() { - + const {colorMode} = useColorMode(); const [isSearchOpen, setIsSearchOpen] = useState(false); const [selectedLayer, setSelectedLayer] = useState(layers[0]); const [searchInput, setSearchInput] = useState(''); @@ -129,47 +29,174 @@ function App() { zoom: 14 }); - // Map state that can be updated from Rust - // const [mapView, setMapView] = useState({ - // longitude: -122.4, - // latitude: 37.8, - // zoom: 14 - // }); + const custom_geolocation = new NativeGeolocation({ + clearWatch: (watchId: number) => { + if (typeof window !== 'undefined' && (window as any).geolocationWatches) { + const interval = (window as any).geolocationWatches.get(watchId); + if (interval) { + clearInterval(interval); + (window as any).geolocationWatches.delete(watchId); + } + } + }, + watchPosition: (successCallback: PositionCallback, errorCallback?: PositionErrorCallback | null, options?: PositionOptions) => { + if (typeof window === 'undefined') return 0; + + // Initialize watches map if it doesn't exist + if (!(window as any).geolocationWatches) { + (window as any).geolocationWatches = new Map(); + } + if (!(window as any).geolocationWatchId) { + (window as any).geolocationWatchId = 0; + } + + const watchId = ++(window as any).geolocationWatchId; + + const pollPosition = async () => { + if ((window as any).__FLURX__) { + try { + const vesselStatus: VesselStatus = await (window as any).__FLURX__.invoke("get_vessel_status"); + const position: GeolocationPosition = { + coords: { + latitude: vesselStatus.latitude, + longitude: vesselStatus.longitude, + altitude: null, + accuracy: 10, // Assume 10m accuracy + altitudeAccuracy: null, + heading: vesselStatus.heading, + speed: vesselStatus.speed, + toJSON: () => ({ + latitude: vesselStatus.latitude, + longitude: vesselStatus.longitude, + altitude: null, + accuracy: 10, + altitudeAccuracy: null, + heading: vesselStatus.heading, + speed: vesselStatus.speed + }) + }, + timestamp: Date.now(), + toJSON: () => ({ + coords: { + latitude: vesselStatus.latitude, + longitude: vesselStatus.longitude, + altitude: null, + accuracy: 10, + altitudeAccuracy: null, + heading: vesselStatus.heading, + speed: vesselStatus.speed + }, + timestamp: Date.now() + }) + }; + successCallback(position); + } catch (error) { + if (errorCallback) { + const positionError: GeolocationPositionError = { + code: 2, // POSITION_UNAVAILABLE + message: 'Failed to get vessel status: ' + error, + PERMISSION_DENIED: 1, + POSITION_UNAVAILABLE: 2, + TIMEOUT: 3 + }; + errorCallback(positionError); + } + } + } + }; + + // Poll immediately and then at intervals + pollPosition(); + const interval = setInterval(pollPosition, options?.timeout || 5000); + (window as any).geolocationWatches.set(watchId, interval); + + return watchId; + }, + getCurrentPosition: (successCallback: PositionCallback, errorCallback?: PositionErrorCallback | null, _options?: PositionOptions) => { + if (typeof window !== 'undefined' && (window as any).__FLURX__) { + (async () => { + try { + const vesselStatus: VesselStatus = await (window as any).__FLURX__.invoke("get_vessel_status"); + const position: GeolocationPosition = { + coords: { + latitude: vesselStatus.latitude, + longitude: vesselStatus.longitude, + altitude: null, + accuracy: 10, // Assume 10m accuracy + altitudeAccuracy: null, + heading: vesselStatus.heading, + speed: vesselStatus.speed, + toJSON: () => ({ + latitude: vesselStatus.latitude, + longitude: vesselStatus.longitude, + altitude: null, + accuracy: 10, + altitudeAccuracy: null, + heading: vesselStatus.heading, + speed: vesselStatus.speed + }) + }, + timestamp: Date.now(), + toJSON: () => ({ + coords: { + latitude: vesselStatus.latitude, + longitude: vesselStatus.longitude, + altitude: null, + accuracy: 10, + altitudeAccuracy: null, + heading: vesselStatus.heading, + speed: vesselStatus.speed + }, + timestamp: Date.now() + }) + }; + successCallback(position); + } catch (error) { + if (errorCallback) { + const positionError: GeolocationPositionError = { + code: 2, // POSITION_UNAVAILABLE + message: 'Failed to get vessel status: ' + error, + PERMISSION_DENIED: 1, + POSITION_UNAVAILABLE: 2, + TIMEOUT: 3 + }; + errorCallback(positionError); + } + } + })(); + } else if (errorCallback) { + const positionError: GeolocationPositionError = { + code: 2, // POSITION_UNAVAILABLE + message: '__FLURX__ not available', + PERMISSION_DENIED: 1, + POSITION_UNAVAILABLE: 2, + TIMEOUT: 3 + }; + errorCallback(positionError); + } + }, + }); // Vessel position state const [vesselPosition, setVesselPosition] = useState(null); - // Create vessel geojson data - // const vesselGeojson: FeatureCollection = { - // type: 'FeatureCollection', - // features: vesselPosition ? [ - // { - // type: 'Feature', - // geometry: { - // type: 'Point', - // coordinates: [vesselPosition.longitude, vesselPosition.latitude] - // }, - // properties: { - // title: 'Vessel Position', - // heading: vesselPosition.heading, - // speed: vesselPosition.speed - // } - // } - // ] : [] - // }; - + // AIS state management + const [aisEnabled, setAisEnabled] = useState(false); + const [boundingBox, _setBoundingBox] = useState<{ + sw_lat: number; + sw_lon: number; + ne_lat: number; + ne_lon: number; + } | undefined>(undefined); + const [vesselPopup, setVesselPopup] = useState(null); - // Button click handlers - // const handleNavigationClick = useCallback(async () => { - // if (typeof window !== 'undefined' && (window as any).__FLURX__) { - // try { - // await (window as any).__FLURX__.invoke("navigation_clicked"); - // console.log('Navigation clicked'); - // } catch (error) { - // console.error('Failed to invoke navigation_clicked:', error); - // } - // } - // }, []); + // Use the AIS provider when enabled + const { + vessels, + isConnected: aisConnected, + error: aisError, + connectionStatus + } = useAISProvider(aisEnabled ? boundingBox : undefined); const selectSearchResult = useCallback(async (searchResult: { lat: string, lon: string }) => { @@ -183,6 +210,7 @@ function App() { }, []); const handleSearchClick = useCallback(async () => { + console.log("calling hsc") if (isSearchOpen && searchInput.length > 1) { try { console.log(`Trying to geocode: ${searchInput}`); @@ -194,9 +222,9 @@ function App() { }), }); const coordinates = await geocode.json(); - const { lat, lon } = coordinates; + const {lat, lon} = coordinates; console.log(`Got geocode coordinates: ${lat}, ${lon}`); - setSearchResults([{ lat, lon }]); + setSearchResults([{lat, lon}]); } catch (e) { console.error('Geocoding failed:', e); // Continue without results @@ -204,7 +232,7 @@ function App() { } else { setIsSearchOpen(!isSearchOpen); } - + if (typeof window !== 'undefined' && (window as any).__FLURX__) { try { await (window as any).__FLURX__.invoke("search_clicked"); @@ -215,33 +243,12 @@ function App() { } }, [isSearchOpen, searchInput]); - const handleLayerChange = useCallback(async (e: any) => { - const newLayer = layers.find(layer => layer.value === e.target.id); - if (newLayer) { - setSelectedLayer(newLayer); - console.log('Layer changed to:', newLayer.name); - } + const handleLayerChange = useCallback(async (layer: any) => { + console.log('Layer change requested:', layer); + setSelectedLayer(layer); + console.log('Layer changed to:', layer.name); }, []); - // const handleMapViewChange = useCallback(async (evt: any) => { - // const { longitude, latitude, zoom } = evt.viewState; - // setMapView({ longitude, latitude, zoom }); - // - // if (typeof window !== 'undefined' && (window as any).__FLURX__) { - // try { - // const mapViewParams: MapViewParams = { - // latitude, - // longitude, - // zoom - // }; - // await (window as any).__FLURX__.invoke("map_view_changed", mapViewParams); - // console.log('Map view changed:', mapViewParams); - // } catch (error) { - // console.error('Failed to invoke map_view_changed:', error); - // } - // } - // }, []); - // Poll for vessel status updates useEffect(() => { const pollVesselStatus = async () => { @@ -270,11 +277,6 @@ function App() { try { const mapInit: GpsPosition = await (window as any).__FLURX__.invoke("get_map_init"); console.log('Map initialization data:', mapInit); - // setMapView({ - // latitude: mapInit.latitude, - // longitude: mapInit.longitude, - // zoom: mapInit.zoom - // }); } catch (error) { console.error('Failed to get map initialization data:', error); } @@ -285,266 +287,103 @@ function App() { }, []); - return ( /* Full-screen wrapper — fills the viewport and becomes the positioning context */ - {/* GPS Feed Display — absolutely positioned at bottom-left */} + {/* GPS Feed Display — absolutely positioned at top-right */} + {vesselPosition && ( - - GPS Feed - Lat: {vesselPosition.latitude.toFixed(6)}° - Lon: {vesselPosition.longitude.toFixed(6)}° - Heading: {vesselPosition.heading.toFixed(1)}° - Speed: {vesselPosition.speed.toFixed(1)} kts - + )} + + {/* AIS Status Panel */} + {aisEnabled && ( + + )} + {/* Button bar — absolutely positioned inside the wrapper */} - { + console.log(e); + if (e.key === 'Escape') { + setIsSearchOpen(false) + } + }} + value={searchInput} + onChange={e => setSearchInput(e.target.value)} + onKeyPress={async (e) => { + console.log(e); + if (e.key === 'Enter' && searchResults.length === 0 && searchInput.length > 2) { + await handleSearchClick() + } + }} + searchResults={searchResults} + callbackfn={(result, index) => { + const colors = getNeumorphicColors(colorMode as 'light' | 'dark'); + return ( + { + if (e.key === 'Enter' && searchResults.length > 0) { + console.log(`Selecting result ${result.lat}, ${result.lon}`); + await selectSearchResult(result); + setSearchResults([]); + setIsSearchOpen(false); + } + }} + colors={colors} + onClick={async () => { + console.log(`Selecting result ${result.lat}, ${result.lon}`); + await selectSearchResult(result); + setSearchResults([]); + setIsSearchOpen(false); + }} + result={result} + /> + ); + }}/> + - {isSearchOpen && - setSearchInput(e.target.value)} - color="white" - bg="rgba(0, 0, 0, 0.8)" - border="none" - borderRadius="0" - _focus={{ - outline: 'none', - }} - _placeholder={{ - color: "#d1cfcf" - }} - /> - {searchResults.length > 0 && ( - - {searchResults.map((result, index) => ( - { - console.log(`Selecting result ${result.lat}, ${result.lon}`); - await selectSearchResult(result); - setSearchResults([]); - setIsSearchOpen(false); - }} - > - {`${result.lat}, ${result.lon}`} - - ))} - - )} - } - - + AIS {aisEnabled ? 'ON' : 'OFF'} + + - { - if (typeof window !== 'undefined' && (window as any).geolocationWatches) { - const interval = (window as any).geolocationWatches.get(watchId); - if (interval) { - clearInterval(interval); - (window as any).geolocationWatches.delete(watchId); - } - } - }, - watchPosition: (successCallback: PositionCallback, errorCallback?: PositionErrorCallback | null, options?: PositionOptions) => { - if (typeof window === 'undefined') return 0; - - // Initialize watches map if it doesn't exist - if (!(window as any).geolocationWatches) { - (window as any).geolocationWatches = new Map(); - } - if (!(window as any).geolocationWatchId) { - (window as any).geolocationWatchId = 0; - } - - const watchId = ++(window as any).geolocationWatchId; - - const pollPosition = async () => { - if ((window as any).__FLURX__) { - try { - const vesselStatus: VesselStatus = await (window as any).__FLURX__.invoke("get_vessel_status"); - const position: GeolocationPosition = { - coords: { - latitude: vesselStatus.latitude, - longitude: vesselStatus.longitude, - altitude: null, - accuracy: 10, // Assume 10m accuracy - altitudeAccuracy: null, - heading: vesselStatus.heading, - speed: vesselStatus.speed, - toJSON: () => ({ - latitude: vesselStatus.latitude, - longitude: vesselStatus.longitude, - altitude: null, - accuracy: 10, - altitudeAccuracy: null, - heading: vesselStatus.heading, - speed: vesselStatus.speed - }) - }, - timestamp: Date.now(), - toJSON: () => ({ - coords: { - latitude: vesselStatus.latitude, - longitude: vesselStatus.longitude, - altitude: null, - accuracy: 10, - altitudeAccuracy: null, - heading: vesselStatus.heading, - speed: vesselStatus.speed - }, - timestamp: Date.now() - }) - }; - successCallback(position); - } catch (error) { - if (errorCallback) { - const positionError: GeolocationPositionError = { - code: 2, // POSITION_UNAVAILABLE - message: 'Failed to get vessel status: ' + error, - PERMISSION_DENIED: 1, - POSITION_UNAVAILABLE: 2, - TIMEOUT: 3 - }; - errorCallback(positionError); - } - } - } - }; - - // Poll immediately and then at intervals - pollPosition(); - const interval = setInterval(pollPosition, options?.timeout || 5000); - (window as any).geolocationWatches.set(watchId, interval); - - return watchId; - }, - getCurrentPosition: (successCallback: PositionCallback, errorCallback?: PositionErrorCallback | null, _options?: PositionOptions) => { - if (typeof window !== 'undefined' && (window as any).__FLURX__) { - (async () => { - try { - const vesselStatus: VesselStatus = await (window as any).__FLURX__.invoke("get_vessel_status"); - const position: GeolocationPosition = { - coords: { - latitude: vesselStatus.latitude, - longitude: vesselStatus.longitude, - altitude: null, - accuracy: 10, // Assume 10m accuracy - altitudeAccuracy: null, - heading: vesselStatus.heading, - speed: vesselStatus.speed, - toJSON: () => ({ - latitude: vesselStatus.latitude, - longitude: vesselStatus.longitude, - altitude: null, - accuracy: 10, - altitudeAccuracy: null, - heading: vesselStatus.heading, - speed: vesselStatus.speed - }) - }, - timestamp: Date.now(), - toJSON: () => ({ - coords: { - latitude: vesselStatus.latitude, - longitude: vesselStatus.longitude, - altitude: null, - accuracy: 10, - altitudeAccuracy: null, - heading: vesselStatus.heading, - speed: vesselStatus.speed - }, - timestamp: Date.now() - }) - }; - successCallback(position); - } catch (error) { - if (errorCallback) { - const positionError: GeolocationPositionError = { - code: 2, // POSITION_UNAVAILABLE - message: 'Failed to get vessel status: ' + error, - PERMISSION_DENIED: 1, - POSITION_UNAVAILABLE: 2, - TIMEOUT: 3 - }; - errorCallback(positionError); - } - } - })(); - } else if (errorCallback) { - const positionError: GeolocationPositionError = { - code: 2, // POSITION_UNAVAILABLE - message: '__FLURX__ not available', - PERMISSION_DENIED: 1, - POSITION_UNAVAILABLE: 2, - TIMEOUT: 3 - }; - errorCallback(positionError); - } - }, - })}/> - {/**/} - {/* /!*{vesselPosition && (*!/*/} - {/* /!* *!/*/} - {/* /!* *!/*/} - {/* /!* *!/*/} - {/* /!*)}*!/*/} - {/**/} + setVesselPopup(null)} + /> ); } diff --git a/crates/base-map/map/src/CustomGeolocate.ts b/crates/base-map/map/src/CustomGeolocate.ts new file mode 100644 index 0000000..c38a0e2 --- /dev/null +++ b/crates/base-map/map/src/CustomGeolocate.ts @@ -0,0 +1,27 @@ +import type {Geolocation} from "@/MapNext.tsx"; + + +export class NativeGeolocation implements Geolocation { + constructor({clearWatch, getCurrentPosition, watchPosition}: { + clearWatch: (watchId: number) => void; + getCurrentPosition: (successCallback: PositionCallback, errorCallback?: PositionErrorCallback | null, options?: PositionOptions) => void; + watchPosition: (successCallback: PositionCallback, errorCallback?: PositionErrorCallback | null, options?: PositionOptions) => number; + }) { + this.clearWatch = clearWatch; + this.watchPosition = watchPosition; + this.getCurrentPosition = getCurrentPosition; + } + + clearWatch(_watchId: number): void { + throw new Error('Method not implemented.'); + } + + getCurrentPosition(_successCallback: PositionCallback, _errorCallback?: PositionErrorCallback | null, _options?: PositionOptions): void { + throw new Error('Method not implemented.'); + } + + watchPosition(_successCallback: PositionCallback, _errorCallback?: PositionErrorCallback | null, _options?: PositionOptions): number { + throw new Error('Method not implemented.'); + } + +} diff --git a/crates/base-map/map/src/LayerSelector.tsx b/crates/base-map/map/src/LayerSelector.tsx new file mode 100644 index 0000000..c9126e2 --- /dev/null +++ b/crates/base-map/map/src/LayerSelector.tsx @@ -0,0 +1,91 @@ +import 'mapbox-gl/dist/mapbox-gl.css'; +import {Button, Menu, Portal} from '@chakra-ui/react'; +import {useColorMode} from './components/ui/color-mode'; +import {useState} from "react"; +import {getNeumorphicColors, getNeumorphicStyle} from './theme/neumorphic-theme'; + +export const layers = [ + { name: 'Standard', value: 'mapbox://styles/mapbox/dark-v11' }, + { name: 'Satellite', value: 'mapbox://styles/mapbox/satellite-v9' }, +]; + + + +// const vesselLayerStyle: CircleLayerSpecification = { +// id: 'vessel', +// type: 'circle', +// paint: { +// 'circle-radius': 8, +// 'circle-color': '#ff4444', +// 'circle-stroke-width': 2, +// 'circle-stroke-color': '#ffffff' +// }, +// source: '' +// }; + + +export type Layer = { name: string; value: string }; +export type Layers = Layer[]; + +// interface MapViewParams { +// latitude: number; +// longitude: number; +// zoom: number; +// } + +// interface AuthParams { +// authenticated: boolean; +// token: string | null; +// } + +export function LayerSelector(props: { onClick: (layer: Layer) => Promise }) { + const { colorMode } = useColorMode(); + const [selectedLayer, setSelectedLayer] = useState(layers[0]); + const neumorphicStyle = getNeumorphicStyle(colorMode as 'light' | 'dark'); + const colors = getNeumorphicColors(colorMode as 'light' | 'dark'); + + return ( + + + + + + + + {layers.map(layer => ( + { + // @ts-ignore + console.log(e.target.id) + setSelectedLayer(layer); + props.onClick(layer); + }} + > + {layer.name} + + ))} + + + + + ); +} diff --git a/crates/base-map/map/src/MapNext.tsx b/crates/base-map/map/src/MapNext.tsx index 2cff6f5..122a836 100644 --- a/crates/base-map/map/src/MapNext.tsx +++ b/crates/base-map/map/src/MapNext.tsx @@ -1,15 +1,18 @@ -import {useState, useMemo, useEffect} from 'react'; +import {useState, useMemo, 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'; +import type { VesselData } from './ais-provider'; import PORTS from './test_data/nautical-base-data.json'; import {Box} from "@chakra-ui/react"; @@ -26,8 +29,34 @@ export interface Geolocation { -export default function MapNext(props: any = {mapboxPublicKey: "", geolocation: Geolocation, vesselPosition: undefined, layer: undefined, mapView: undefined} as any) { +interface MapNextProps { + mapboxPublicKey: string; + geolocation: Geolocation; + vesselPosition?: any; + layer?: any; + mapView?: any; + aisVessels?: VesselData[]; + onVesselClick?: (vessel: VesselData) => void; + vesselPopup?: VesselData | null; + onVesselPopupClose?: () => void; +} + +export default function MapNext(props: MapNextProps) { const [popupInfo, setPopupInfo] = useState(null); + const mapRef = useRef(null); + + // Handle user location events + const handleGeolocate = useCallback((position: GeolocationPosition) => { + console.log('User location loaded:', position); + }, []); + + const handleTrackUserLocationStart = useCallback(() => { + console.log('Started tracking user location'); + }, []); + + const handleTrackUserLocationEnd = useCallback(() => { + console.log('Stopped tracking user location'); + }, []); const pins = useMemo( () => @@ -55,15 +84,56 @@ export default function MapNext(props: any = {mapboxPublicKey: "", geolocation: [] ); + // Helper function to get vessel color based on type + const getVesselColor = (type: string): string => { + switch (type.toLowerCase()) { + case 'yacht': + case 'pleasure craft': + return '#00cc66'; + case 'fishing vessel': + case 'fishing': + return '#ff6600'; + case 'cargo': + case 'container': + return '#cc0066'; + case 'tanker': + return '#ff0000'; + case 'passenger': + return '#6600cc'; + default: + return '#0066cc'; + } + }; - useEffect(() => { - console.log("props.vesselPosition", props?.vesselPosition); - // setLocationLock(props.vesselPosition) - }, [props.vesselPosition]); + // Create vessel markers + const vesselMarkers = useMemo(() => + (props.aisVessels || []).map((vessel) => ( + { + e.originalEvent.stopPropagation(); + if (props.onVesselClick) { + props.onVesselClick(vessel); + } + }} + > + + + )), + [props.aisVessels, props.onVesselClick] + ); return ( - + {pins} + {vesselMarkers} + + {/* Vessel Popup */} + {props.vesselPopup && ( + props.onVesselPopupClose && props.onVesselPopupClose()} + closeButton={true} + closeOnClick={false} + > +
+

{props.vesselPopup.name}

+
MMSI: {props.vesselPopup.mmsi}
+
Type: {props.vesselPopup.type}
+
Speed: {props.vesselPopup.speed.toFixed(1)} knots
+
Heading: {props.vesselPopup.heading}°
+
Position: {props.vesselPopup.latitude.toFixed(4)}, {props.vesselPopup.longitude.toFixed(4)}
+
+ Last update: {props.vesselPopup.lastUpdate.toLocaleTimeString()} +
+
+
+ )} {popupInfo && ( { + if ((!aisResponse.raw_message?.MetaData?.MMSI) || !aisResponse.latitude || !aisResponse.longitude) { + console.log('Skipping vessel with missing data:', { + mmsi: aisResponse.mmsi, + metadataMSSI: aisResponse.raw_message?.MetaData?.MSSI, + latitude: aisResponse.latitude, + longitude: aisResponse.longitude, + raw: aisResponse.raw_message + }); + return null; + } + + return { + id: aisResponse.mmsi ?? aisResponse.raw_message?.MetaData?.MMSI, + name: aisResponse.ship_name || `Vessel ${aisResponse.mmsi}`, + type: aisResponse.ship_type || 'Unknown', + latitude: aisResponse.latitude, + longitude: aisResponse.longitude, + heading: aisResponse.heading || 0, + speed: aisResponse.speed_over_ground || 0, + length: 100, // Default length + width: 20, // Default width + mmsi: aisResponse.mmsi ?? aisResponse.raw_message?.MetaData?.MMSI, + callSign: '', + destination: '', + eta: '', + lastUpdate: new Date() + }; +}; + +// Simplified AIS provider hook for testing +export const useAISProvider = (boundingBox?: BoundingBox) => { + const [vessels, setVessels] = useState([]); + const [isConnected, setIsConnected] = useState(false); + const [error, setError] = useState(null); + const [connectionStatus, setConnectionStatus] = useState('Disconnected'); + + const wsRef = useRef(null); + const vesselMapRef = useRef>(new Map()); + const reconnectTimeoutRef = useRef(null); + const reconnectAttemptsRef = useRef(0); + const connectionTimeoutRef = useRef(null); + const isConnectingRef = useRef(false); + const isMountedRef = useRef(true); + const maxReconnectAttempts = 10; + const baseReconnectDelay = 1000; // 1 second + + // Calculate exponential backoff delay + const getReconnectDelay = useCallback(() => { + const delay = baseReconnectDelay * Math.pow(2, reconnectAttemptsRef.current); + return Math.min(delay, 30000); // Cap at 30 seconds + }, []); + + // Connect to WebSocket with React StrictMode-safe logic + const connectSocket = useCallback(() => { + // Prevent multiple simultaneous connection attempts (React StrictMode protection) + if (isConnectingRef.current) { + console.log('Connection attempt already in progress, skipping...'); + return; + } + + // Check if component is still mounted + if (!isMountedRef.current) { + console.log('Component unmounted, skipping connection attempt'); + return; + } + + // Clear any existing reconnection timeout + if (reconnectTimeoutRef.current) { + clearTimeout(reconnectTimeoutRef.current); + reconnectTimeoutRef.current = null; + } + + // Clear any existing connection timeout + if (connectionTimeoutRef.current) { + clearTimeout(connectionTimeoutRef.current); + connectionTimeoutRef.current = null; + } + + // Check if already connected or connecting + if (wsRef.current?.readyState === WebSocket.OPEN) { + console.log('WebSocket already connected'); + return; + } + + if (wsRef.current?.readyState === WebSocket.CONNECTING) { + console.log('WebSocket already connecting'); + return; + } + + // Check reconnection attempts + if (reconnectAttemptsRef.current >= maxReconnectAttempts) { + console.error('Max reconnection attempts reached'); + setError('Failed to connect after multiple attempts'); + setConnectionStatus('Failed'); + return; + } + + // Set connecting flag to prevent race conditions + isConnectingRef.current = true; + + setConnectionStatus(reconnectAttemptsRef.current > 0 ? + `Reconnecting... (${reconnectAttemptsRef.current + 1}/${maxReconnectAttempts})` : + 'Connecting...'); + setError(null); + + try { + console.log(`[CONNECT] Attempting WebSocket connection (attempt ${reconnectAttemptsRef.current + 1})`); + + // Close any existing connection properly + if (wsRef.current) { + wsRef.current.onopen = null; + wsRef.current.onmessage = null; + wsRef.current.onerror = null; + wsRef.current.onclose = null; + wsRef.current.close(); + wsRef.current = null; + } + + const ws = new WebSocket('ws://localhost:3000/ws'); + wsRef.current = ws; + + // Set connection timeout with proper cleanup + connectionTimeoutRef.current = setTimeout(() => { + if (ws.readyState === WebSocket.CONNECTING && isMountedRef.current) { + console.log('[TIMEOUT] Connection timeout, closing WebSocket'); + isConnectingRef.current = false; + ws.close(); + } + }, 10000); // 10 second timeout + + ws.onopen = () => { + // Clear connection timeout + if (connectionTimeoutRef.current) { + clearTimeout(connectionTimeoutRef.current); + connectionTimeoutRef.current = null; + } + + // Check if component is still mounted + if (!isMountedRef.current) { + console.log('[OPEN] Component unmounted, closing connection'); + ws.close(); + return; + } + + console.log('[OPEN] Connected to AIS WebSocket'); + isConnectingRef.current = false; // Clear connecting flag + setIsConnected(true); + setConnectionStatus('Connected'); + setError(null); + reconnectAttemptsRef.current = 0; // Reset reconnection attempts + + // Send bounding box if available + if (boundingBox && isMountedRef.current) { + const message: WebSocketMessage = { + type: 'set_bounding_box', + bounding_box: boundingBox + }; + ws.send(JSON.stringify(message)); + console.log('[OPEN] Sent bounding box:', boundingBox); + } + + // Start AIS stream + if (isMountedRef.current) { + const startMessage: WebSocketMessage = { + type: 'start_ais_stream' + }; + ws.send(JSON.stringify(startMessage)); + console.log('[OPEN] Started AIS stream'); + } + }; + + ws.onmessage = (event) => { + try { + const messageData = event.data; + + // Try to parse as JSON, but handle plain text messages gracefully + let data; + try { + data = JSON.parse(messageData); + } catch (parseError) { + console.log('Received plain text message:', messageData); + return; + } + + // Handle JSON status messages + if (typeof data === 'string' || data.type) { + console.log('Received message:', data); + return; + } + + // Process vessel data + const vesselData = convertAisResponseToVesselData(data); + if (vesselData) { + console.log('Received vessel data:', vesselData); + vesselMapRef.current.set(vesselData.mmsi, vesselData); + setVessels(Array.from(vesselMapRef.current.values())); + } + } catch (err) { + console.error('Error processing WebSocket message:', err); + } + }; + + ws.onerror = (error) => { + // Clear connection timeout + if (connectionTimeoutRef.current) { + clearTimeout(connectionTimeoutRef.current); + connectionTimeoutRef.current = null; + } + + console.error('[ERROR] WebSocket error:', error); + isConnectingRef.current = false; // Clear connecting flag + + // Only update state if component is still mounted + if (isMountedRef.current) { + setError('WebSocket connection error'); + setIsConnected(false); + } + }; + + ws.onclose = (event) => { + // Clear connection timeout + if (connectionTimeoutRef.current) { + clearTimeout(connectionTimeoutRef.current); + connectionTimeoutRef.current = null; + } + + console.log(`[CLOSE] WebSocket connection closed: ${event.code} - ${event.reason}`); + isConnectingRef.current = false; // Clear connecting flag + + // Only update state if component is still mounted + if (isMountedRef.current) { + setIsConnected(false); + } + + // Only attempt reconnection if component is mounted, wasn't a clean close, and we haven't exceeded max attempts + if (isMountedRef.current && !event.wasClean && reconnectAttemptsRef.current < maxReconnectAttempts) { + reconnectAttemptsRef.current++; + const delay = getReconnectDelay(); + + console.log(`[CLOSE] Scheduling reconnection in ${delay}ms (attempt ${reconnectAttemptsRef.current}/${maxReconnectAttempts})`); + setError(`Connection lost, reconnecting in ${Math.round(delay/1000)}s...`); + setConnectionStatus('Reconnecting...'); + + reconnectTimeoutRef.current = setTimeout(() => { + if (isMountedRef.current) { + connectSocket(); + } + }, delay); + } else { + if (isMountedRef.current) { + if (event.wasClean) { + setConnectionStatus('Disconnected'); + setError(null); + } else { + setConnectionStatus('Failed'); + setError('Connection failed after multiple attempts'); + } + } + } + }; + + } catch (err) { + console.error('Error creating WebSocket connection:', err); + setError(err instanceof Error ? err.message : 'Unknown WebSocket error'); + setConnectionStatus('Error'); + + // Schedule reconnection attempt + if (reconnectAttemptsRef.current < maxReconnectAttempts) { + reconnectAttemptsRef.current++; + const delay = getReconnectDelay(); + reconnectTimeoutRef.current = setTimeout(() => { + connectSocket(); + }, delay); + } + } + }, [boundingBox, getReconnectDelay]); + + // Update bounding box + const updateBoundingBox = useCallback((bbox: BoundingBox) => { + if (wsRef.current?.readyState === WebSocket.OPEN) { + const message: WebSocketMessage = { + type: 'set_bounding_box', + bounding_box: bbox + }; + wsRef.current.send(JSON.stringify(message)); + console.log('Updated bounding box:', bbox); + + // Clear existing vessels when bounding box changes + // vesselMapRef.current.clear(); + // setVessels([]); + } + }, []); + + // Connect on mount with React StrictMode protection + useEffect(() => { + // Set mounted flag + isMountedRef.current = true; + + // Small delay to prevent immediate double connection in StrictMode + const connectTimeout = setTimeout(() => { + if (isMountedRef.current) { + connectSocket(); + } + }, 100); + + return () => { + // Mark component as unmounted + isMountedRef.current = false; + + // Clear connect timeout + clearTimeout(connectTimeout); + + // Clear reconnection timeout + if (reconnectTimeoutRef.current) { + clearTimeout(reconnectTimeoutRef.current); + reconnectTimeoutRef.current = null; + } + + // Clear connection timeout + if (connectionTimeoutRef.current) { + clearTimeout(connectionTimeoutRef.current); + connectionTimeoutRef.current = null; + } + + // Reset connection flags + isConnectingRef.current = false; + + // Close WebSocket connection properly + if (wsRef.current) { + console.log('[CLEANUP] Closing WebSocket connection'); + wsRef.current.onopen = null; + wsRef.current.onmessage = null; + wsRef.current.onerror = null; + wsRef.current.onclose = null; + wsRef.current.close(); + wsRef.current = null; + } + + // Reset reconnection attempts + reconnectAttemptsRef.current = 0; + }; + }, [connectSocket]); + + return { + vessels, + isConnected, + error, + connectionStatus, + connectSocket, + updateBoundingBox + }; +}; \ No newline at end of file diff --git a/crates/base-map/map/src/components/map/AisFeedInfo.tsx b/crates/base-map/map/src/components/map/AisFeedInfo.tsx new file mode 100644 index 0000000..5243769 --- /dev/null +++ b/crates/base-map/map/src/components/map/AisFeedInfo.tsx @@ -0,0 +1,34 @@ +import type {VesselData} from "@/ais-provider.tsx"; +import type { VesselStatus } from "@/types"; +import { Box } from "@chakra-ui/react"; +import {getNeumorphicStyle} from "@/theme/neumorphic-theme.ts"; + +export function AisFeed(props: { + vesselPosition: VesselStatus | null, + colorMode: "light" | "dark", + connectionStatus: string, + vesselData: VesselData[], + aisError: string | null, + aisConnected: boolean +}) { + return + AIS Status + Status: {props.connectionStatus} + Vessels: {props.vesselData.length} + {props.aisError && Error: {props.aisError}} + {props.aisConnected && ( + + ✓ Connected to AIS server + + )} + ; +} diff --git a/crates/base-map/map/src/components/map/GpsFeedInfo.tsx b/crates/base-map/map/src/components/map/GpsFeedInfo.tsx new file mode 100644 index 0000000..c9b7508 --- /dev/null +++ b/crates/base-map/map/src/components/map/GpsFeedInfo.tsx @@ -0,0 +1,23 @@ +import type {VesselStatus} from "@/types.ts"; +import {Box} from "@chakra-ui/react"; +import {getNeumorphicStyle} from "@/theme/neumorphic-theme.ts"; + +export function GpsFeed(props: { vesselPosition: VesselStatus, colorMode: 'light' | 'dark' }) { + return <> + + GPS Feed + Lat: {props.vesselPosition.latitude.toFixed(6)}° + Lon: {props.vesselPosition.longitude.toFixed(6)}° + Heading: {props.vesselPosition.heading.toFixed(1)}° + Speed: {props.vesselPosition.speed.toFixed(1)} kts + + ; +} \ No newline at end of file diff --git a/crates/base-map/map/src/components/map/Search.tsx b/crates/base-map/map/src/components/map/Search.tsx new file mode 100644 index 0000000..355e2d6 --- /dev/null +++ b/crates/base-map/map/src/components/map/Search.tsx @@ -0,0 +1,63 @@ +import {Box, Button, Input, Text} from "@chakra-ui/react"; +import {getNeumorphicStyle} from "@/theme/neumorphic-theme.ts"; + +export function Search(props: { + onClick: () => Promise, + colorMode: "light" | "dark", + searchOpen: boolean, + onKeyDown: (e: any) => void, + value: string, + onChange: (e: any) => void, + onKeyPress: (e: any) => Promise, + searchResults: any[], + callbackfn: (result: any, index: any) => any // JSX.Element +}) { + return + + {props.searchOpen && + + {props.searchResults.length > 0 && ( + + {props.searchResults.map(props.callbackfn)} + + )} + } + ; +} diff --git a/crates/base-map/map/src/components/map/SearchResult.tsx b/crates/base-map/map/src/components/map/SearchResult.tsx new file mode 100644 index 0000000..e4ac97e --- /dev/null +++ b/crates/base-map/map/src/components/map/SearchResult.tsx @@ -0,0 +1,39 @@ +import {Box} from "@chakra-ui/react"; + +interface SearchResultProps { + onKeyPress: (e: any) => Promise; + colors: { + bg: string; + surface: string; + text: string; + textSecondary: string; + accent: string; + shadow: { dark: string; light: string } + } | { + bg: string; + surface: string; + text: string; + textSecondary: string; + accent: string; + shadow: { dark: string; light: string } + }; + onClick: () => Promise; + result: any; +} + +export function SearchResult(props: SearchResultProps) { + return + {`${props.result.lat}, ${props.result.lon}`} + ; +} diff --git a/crates/base-map/map/src/theme/neumorphic-theme.ts b/crates/base-map/map/src/theme/neumorphic-theme.ts new file mode 100644 index 0000000..8decf43 --- /dev/null +++ b/crates/base-map/map/src/theme/neumorphic-theme.ts @@ -0,0 +1,75 @@ +import { defineConfig } from "@chakra-ui/react"; + +// Neumorphic color palette +const neumorphicColors = { + light: { + bg: '#e0e5ec', + surface: '#e0e5ec', + text: '#2d3748', + textSecondary: '#4a5568', + accent: '#3182ce', + shadow: { + dark: '#a3b1c6', + light: '#ffffff', + }, + }, + dark: { + bg: '#2d3748', + surface: '#ffffff', + text: '#f7fafc', + textSecondary: '#e2e8f0', + accent: '#63b3ed', + shadow: { + dark: '#1a202c', + light: '#4a5568', + }, + }, +}; + +// Neumorphic shadow mixins +const neumorphicShadows = { + light: { + raised: '1px 1px 2px #a3b1c6, -1px -1px 2px #ffffff', + pressed: 'inset 2px 2px 4px #a3b1c6, inset -2px -2px 4px #ffffff', + subtle: '1px 1px 2px #a3b1c6, -1px -1px 2px #ffffff', + subtlePressed: 'inset 1px 1px 2px #a3b1c6, inset -1px -1px 2px #ffffff', + floating: '6px 6px 12px #a3b1c6, -6px -6px 12px #ffffff', + }, + dark: { + raised: '2px 2px 4px #1a202c, -2px -2px 4px #4a5568', + pressed: 'inset 2px 2px 4px #1a202c, inset -2px -2px 4px #4a5568', + subtle: '2px 2px 2px #1a202c, -2px -2px 2px #4a5568', + subtlePressed: 'inset 2px 2px 2px #1a202c, inset -2px -2px 2px #4a5568', + floating: '6px 6px 12px #1a202c, -6px -6px 12px #4a5568', + }, +}; + +// Simplified theme configuration to avoid TypeScript errors +// The utility functions below provide the neumorphic styling functionality +export const neumorphicTheme = defineConfig({ + theme: { + // Theme configuration simplified to avoid type errors + }, +}); + +// Utility functions for neumorphic styling +export const getNeumorphicStyle = (colorMode: 'light' | 'dark', variant: 'raised' | 'pressed' | 'subtle' | 'floating' = 'raised') => { + const colors = neumorphicColors[colorMode]; + const shadows = neumorphicShadows[colorMode]; + + return { + bg: colors.surface, + color: colors.text, + borderRadius: 6, + boxShadow: shadows[variant] || shadows.raised, + transition: 'all 0.3s ease-in-out', + }; +}; + +export const getNeumorphicColors = (colorMode: 'light' | 'dark') => { + return neumorphicColors[colorMode]; +}; + +export const getNeumorphicShadows = (colorMode: 'light' | 'dark') => { + return neumorphicShadows[colorMode]; +}; \ No newline at end of file diff --git a/crates/base-map/map/src/types.ts b/crates/base-map/map/src/types.ts new file mode 100644 index 0000000..5c7e10d --- /dev/null +++ b/crates/base-map/map/src/types.ts @@ -0,0 +1,24 @@ +// Types for bevy_flurx_ipc communication +export interface GpsPosition { + latitude: number; + longitude: number; + zoom: number; +} + +export interface VesselStatus { + latitude: number; + longitude: number; + heading: number; + speed: number; +} + +// interface MapViewParams { +// latitude: number; +// longitude: number; +// zoom: number; +// } + +// interface AuthParams { +// authenticated: boolean; +// token: string | null; +// } \ 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..f4fc305 --- /dev/null +++ b/crates/base-map/map/src/vessel-marker.tsx @@ -0,0 +1,42 @@ +import React from 'react'; + +interface VesselMarkerProps { + heading: number; + color?: string; + size?: number; +} + +const VesselMarker: React.FC = ({ + heading, + color = '#0066cc', + size = 16 +}) => { + return ( +
+ + {/* Simple vessel shape - triangle pointing up (north) */} + + +
+ ); +}; + +export default VesselMarker; \ 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..7e74977 --- /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: ['./test/test-setup.ts'], + globals: true, + }, +}); \ No newline at end of file diff --git a/crates/yachtpit/Cargo.toml b/crates/yachtpit/Cargo.toml index 9b94779..4d845c4 100644 --- a/crates/yachtpit/Cargo.toml +++ b/crates/yachtpit/Cargo.toml @@ -80,6 +80,7 @@ winit = { version = "0.30", default-features = false } image = { version = "0.25", default-features = false } ## This greatly improves WGPU's performance due to its heavy use of trace! calls log = { version = "0.4", features = ["max_level_debug", "release_max_level_warn"] } +anyhow = "1.0.98" # Platform-specific tokio features [target.'cfg(not(target_arch = "wasm32"))'.dependencies] @@ -100,4 +101,5 @@ console_error_panic_hook = "0.1" [build-dependencies] embed-resource = "1" -base-map = { path = "../base-map" } +base-map = { path = "../base-map" } # Comment to Temporarily disable for testing +ais = { path = "../ais" } # Comment to Temporarily disable for testing diff --git a/crates/yachtpit/src/main.rs b/crates/yachtpit/src/main.rs index 571d5b8..d343eda 100644 --- a/crates/yachtpit/src/main.rs +++ b/crates/yachtpit/src/main.rs @@ -2,19 +2,39 @@ #![cfg_attr(not(debug_assertions), windows_subsystem = "windows")] use bevy::asset::AssetMetaCheck; +use bevy::ecs::spawn::SpawnableList; use bevy::prelude::*; use bevy::window::PrimaryWindow; use bevy::winit::WinitWindows; use bevy::DefaultPlugins; -use yachtpit::GamePlugin; use std::io::Cursor; +use tokio::process::Command; use winit::window::Icon; +use yachtpit::GamePlugin; + +#[cfg(not(target_arch = "wasm32"))] +#[tokio::main] +async fn main() { + // Start AIS server in background + tokio::spawn(async { + info!("Starting AIS server..."); + let mut cmd = Command::new("target/release/ais").spawn().unwrap(); + match cmd.wait().await { + Ok(status) => info!("AIS server process exited with status: {}", status), + Err(e) => error!("Error waiting for AIS server process: {}", e), + } + }); + + launch_bevy(); +} + + #[cfg(not(target_arch = "wasm32"))] use bevy_webview_wry::WebviewWryPlugin; -fn main() { - #[cfg(not(target_arch = "wasm32"))] +#[cfg(not(target_arch = "wasm32"))] +fn launch_bevy() { App::new() .insert_resource(ClearColor(Color::NONE)) .add_plugins( @@ -22,7 +42,6 @@ fn main() { .set(WindowPlugin { primary_window: Some(Window { // Bind to canvas included in `index.html` - canvas: Some("#yachtpit-canvas".to_owned()), fit_canvas_to_parent: true, // Tells wasm not to override default event handling, like F5 and Ctrl+R prevent_default_event_handling: false, @@ -36,11 +55,18 @@ fn main() { }), ) .add_plugins(GamePlugin) - .add_systems(Startup, set_window_icon) + .add_systems(Startup, set_window_icon) // Changed here .add_plugins(WebviewWryPlugin::default()) .run(); +} - #[cfg(target_arch = "wasm32")] +#[cfg(target_arch = "wasm32")] +async fn main() { + launch_bevy(); +} + +#[cfg(target_arch = "wasm32")] +fn launch_bevy() { { // Add console logging for WASM debugging console_error_panic_hook::set_once(); @@ -73,8 +99,54 @@ fn main() { }) .run(); } - } +// +// fn start_ais_server() { +// static mut SERVER_STARTED: bool = false; +// +// unsafe { +// if SERVER_STARTED { +// return; +// } +// SERVER_STARTED = true; +// } +// +// let rt = tokio::runtime::Runtime::new().unwrap(); +// rt.block_on(async { +// info!("Starting AIS server..."); +// if let Ok(mut cmd) = Command::new("cargo") +// .current_dir("../ais") +// .arg("run").arg("--release") +// .spawn() { +// info!("AIS server process spawned"); +// let status = cmd.wait().await; +// match status { +// Ok(exit_status) => match exit_status.code() { +// Some(code) => info!("AIS server exited with status code: {}", code), +// None => info!("AIS server terminated by signal"), +// }, +// Err(e) => error!("AIS server failed: {}", e), +// } +// } else { +// error!("Failed to start AIS server - unable to spawn process"); +// } +// }); +// } + +// fn start_ais_server() { +// // This task will run on the Tokio runtime's thread pool without blocking Bevy +// tokio::spawn(async { +// info!("Starting AIS server in the background..."); +// +// +// +// // This now waits on the background task, not the main Bevy thread +// match cmd.wait().await { +// Ok(status) => info!("AIS server process exited with status: {}", status), +// Err(e) => error!("Error waiting for AIS server process: {}", e), +// } +// }); +// } // Sets the icon on windows and X11 fn set_window_icon( diff --git a/crates/yachtpit/src/ui/menu.rs b/crates/yachtpit/src/ui/menu.rs index d680315..61008d9 100644 --- a/crates/yachtpit/src/ui/menu.rs +++ b/crates/yachtpit/src/ui/menu.rs @@ -13,17 +13,42 @@ impl Plugin for MenuPlugin { } } -#[derive(Component)] +#[derive(Component, Clone)] struct ButtonColors { normal: Color, hovered: Color, + pressed: Color, +} + +// Neumorphic color palette for luxury design +struct NeumorphicColors; + +impl NeumorphicColors { + // Base surface color - soft gray with warm undertones + const SURFACE: Color = Color::linear_rgb(0.88, 0.90, 0.92); + + // Primary button colors with depth + const PRIMARY_NORMAL: Color = Color::linear_rgb(0.85, 0.87, 0.90); + const PRIMARY_HOVERED: Color = Color::linear_rgb(0.90, 0.92, 0.95); + const PRIMARY_PRESSED: Color = Color::linear_rgb(0.80, 0.82, 0.85); + + // Secondary button colors (more subtle) + const SECONDARY_NORMAL: Color = Color::linear_rgb(0.86, 0.88, 0.91); + const SECONDARY_HOVERED: Color = Color::linear_rgb(0.88, 0.90, 0.93); + const SECONDARY_PRESSED: Color = Color::linear_rgb(0.82, 0.84, 0.87); + + // Text colors for contrast + const TEXT_PRIMARY: Color = Color::linear_rgb(0.25, 0.30, 0.35); + const TEXT_SECONDARY: Color = Color::linear_rgb(0.45, 0.50, 0.55); + const TEXT_ACCENT: Color = Color::linear_rgb(0.20, 0.45, 0.75); } impl Default for ButtonColors { fn default() -> Self { ButtonColors { - normal: Color::linear_rgb(0.15, 0.15, 0.15), - hovered: Color::linear_rgb(0.25, 0.25, 0.25), + normal: NeumorphicColors::PRIMARY_NORMAL, + hovered: NeumorphicColors::PRIMARY_HOVERED, + pressed: NeumorphicColors::PRIMARY_PRESSED, } } } @@ -34,6 +59,18 @@ struct Menu; fn setup_menu(mut commands: Commands) { info!("menu"); commands.spawn((Camera2d, Msaa::Off)); + + // Set neumorphic background + commands.spawn(( + Node { + width: Val::Percent(100.0), + height: Val::Percent(100.0), + position_type: PositionType::Absolute, + ..default() + }, + BackgroundColor(NeumorphicColors::SURFACE), + )); + commands .spawn(( Node { @@ -52,23 +89,27 @@ fn setup_menu(mut commands: Commands) { .spawn(( Button, Node { - width: Val::Px(140.0), - height: Val::Px(50.0), + width: Val::Px(180.0), + height: Val::Px(65.0), justify_content: JustifyContent::Center, align_items: AlignItems::Center, + border: UiRect::all(Val::Px(2.0)), + margin: UiRect::all(Val::Px(8.0)), ..Default::default() }, BackgroundColor(button_colors.normal), + BorderColor(Color::linear_rgb(0.82, 0.84, 0.87)), + BorderRadius::all(Val::Px(16.0)), button_colors, ChangeState(GameState::Playing), )) .with_child(( - Text::new("Play"), + Text::new("▶ PLAY"), TextFont { - font_size: 40.0, + font_size: 28.0, ..default() }, - TextColor(Color::linear_rgb(0.9, 0.9, 0.9)), + TextColor(NeumorphicColors::TEXT_PRIMARY), )); }); commands @@ -85,74 +126,71 @@ fn setup_menu(mut commands: Commands) { Menu, )) .with_children(|children| { + let secondary_button_colors = ButtonColors { + normal: NeumorphicColors::SECONDARY_NORMAL, + hovered: NeumorphicColors::SECONDARY_HOVERED, + pressed: NeumorphicColors::SECONDARY_PRESSED, + }; + children .spawn(( Button, Node { - width: Val::Px(170.0), - height: Val::Px(50.0), - justify_content: JustifyContent::SpaceAround, + width: Val::Px(180.0), + height: Val::Px(45.0), + justify_content: JustifyContent::Center, align_items: AlignItems::Center, - padding: UiRect::all(Val::Px(5.)), + padding: UiRect::all(Val::Px(8.)), + border: UiRect::all(Val::Px(1.0)), + margin: UiRect::horizontal(Val::Px(8.0)), ..Default::default() }, - BackgroundColor(Color::NONE), - ButtonColors { - normal: Color::NONE, - ..default() - }, + BackgroundColor(secondary_button_colors.normal), + BorderColor(Color::linear_rgb(0.80, 0.82, 0.85)), + BorderRadius::all(Val::Px(12.0)), + secondary_button_colors.clone(), OpenLink("https://bevyengine.org"), )) - .with_children(|parent| { - parent.spawn(( - Text::new("Made with Bevy"), - TextFont { - font_size: 15.0, - ..default() - }, - TextColor(Color::linear_rgb(0.9, 0.9, 0.9)), - )); - parent.spawn(( - Node { - width: Val::Px(32.), - ..default() - }, - )); - }); + .with_child(( + Text::new("🚀 Made with Bevy"), + TextFont { + font_size: 14.0, + ..default() + }, + TextColor(NeumorphicColors::TEXT_SECONDARY), + )); + children .spawn(( Button, Node { - width: Val::Px(170.0), - height: Val::Px(50.0), - justify_content: JustifyContent::SpaceAround, + width: Val::Px(180.0), + height: Val::Px(45.0), + justify_content: JustifyContent::Center, align_items: AlignItems::Center, - padding: UiRect::all(Val::Px(5.)), + padding: UiRect::all(Val::Px(8.)), + border: UiRect::all(Val::Px(1.0)), + margin: UiRect::horizontal(Val::Px(8.0)), ..default() }, - BackgroundColor(Color::NONE), + BackgroundColor(secondary_button_colors.normal), + BorderColor(Color::linear_rgb(0.80, 0.82, 0.85)), + BorderRadius::all(Val::Px(12.0)), ButtonColors { - normal: Color::NONE, - hovered: Color::linear_rgb(0.25, 0.25, 0.25), + normal: NeumorphicColors::SECONDARY_NORMAL, + hovered: NeumorphicColors::SECONDARY_HOVERED, + pressed: NeumorphicColors::SECONDARY_PRESSED, }, OpenLink("https://github.com/NiklasEi/bevy_game_template"), )) - .with_children(|parent| { - parent.spawn(( - Text::new("Open source"), - TextFont { - font_size: 15.0, - ..default() - }, - TextColor(Color::linear_rgb(0.9, 0.9, 0.9)), - )); - parent.spawn(( - Node { - width: Val::Px(32.), - ..default() - }, - )); - }); + .with_child(( + Text::new("📖 Open Source"), + TextFont { + font_size: 14.0, + ..default() + }, + TextColor(NeumorphicColors::TEXT_SECONDARY), + )); }); } @@ -178,6 +216,10 @@ fn click_play_button( for (interaction, mut color, button_colors, change_state, open_link) in &mut interaction_query { match *interaction { Interaction::Pressed => { + // Apply pressed state visual feedback + *color = button_colors.pressed.into(); + + // Handle button actions if let Some(state) = change_state { next_state.set(state.0.clone()); } else if let Some(link) = open_link { @@ -187,9 +229,11 @@ fn click_play_button( } } Interaction::Hovered => { + // Smooth transition to hovered state *color = button_colors.hovered.into(); } Interaction::None => { + // Return to normal state *color = button_colors.normal.into(); } } 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 diff --git a/yachtpit-og.png b/yachtpit-og.png deleted file mode 100644 index f81e585..0000000 Binary files a/yachtpit-og.png and /dev/null differ diff --git a/yachtpit-x.png b/yachtpit-x.png index 7cb739f..1bc257b 100644 Binary files a/yachtpit-x.png and b/yachtpit-x.png differ