From 33e3aef0647221667287a2c5e4d926edf9bfada9 Mon Sep 17 00:00:00 2001 From: AM Smith Date: Tue, 5 Sep 2023 20:10:43 -0700 Subject: [PATCH] chore: refactor --- .cspell.project-words.txt | 2 + Cargo.lock | 259 +-- client-grpc/Cargo.toml | 2 +- client-grpc/examples/grpc.rs | 20 +- client-grpc/src/client.rs | 5 +- client-grpc/src/grpc.rs | 11 +- client-grpc/tests/integration_test.rs | 13 +- log4rs.yaml | 2 +- proto/grpc.proto | 11 +- server/Cargo.toml | 17 +- server/src/grpc/api/cancel_itinerary.rs | 137 ++ server/src/grpc/api/confirm_itinerary.rs | 240 +++ server/src/grpc/api/mod.rs | 110 ++ server/src/grpc/api/query_flight.rs | 604 +++++++ server/src/grpc/client.rs | 4 +- server/src/grpc/mod.rs | 2 +- server/src/grpc/queries.rs | 1071 ----------- server/src/grpc/server.rs | 32 +- server/src/main.rs | 15 - server/src/router/flight_plan.rs | 255 +++ server/src/router/itinerary.rs | 927 ++++++++++ server/src/router/mod.rs | 71 +- server/src/router/router_types/edge.rs | 19 - server/src/router/router_types/location.rs | 45 - server/src/router/router_types/mod.rs | 5 - server/src/router/router_types/node.rs | 353 ---- server/src/router/router_types/router.rs | 734 -------- server/src/router/router_types/status.rs | 12 - server/src/router/router_utils/flightplan.rs | 41 - server/src/router/router_utils/graph.rs | 88 - server/src/router/router_utils/haversine.rs | 54 - server/src/router/router_utils/mock.rs | 245 --- server/src/router/router_utils/mod.rs | 9 - .../src/router/router_utils/router_state.rs | 1566 ----------------- server/src/router/router_utils/schedule.rs | 330 ---- server/src/router/schedule.rs | 624 +++++++ server/src/router/vehicle.rs | 484 +++++ server/src/router/vertiport.rs | 817 +++++++++ server/src/test_util.rs | 14 - 39 files changed, 4412 insertions(+), 4838 deletions(-) create mode 100644 server/src/grpc/api/cancel_itinerary.rs create mode 100644 server/src/grpc/api/confirm_itinerary.rs create mode 100644 server/src/grpc/api/mod.rs create mode 100644 server/src/grpc/api/query_flight.rs delete mode 100644 server/src/grpc/queries.rs create mode 100644 server/src/router/flight_plan.rs create mode 100644 server/src/router/itinerary.rs delete mode 100644 server/src/router/router_types/edge.rs delete mode 100644 server/src/router/router_types/location.rs delete mode 100644 server/src/router/router_types/mod.rs delete mode 100644 server/src/router/router_types/node.rs delete mode 100644 server/src/router/router_types/router.rs delete mode 100644 server/src/router/router_types/status.rs delete mode 100644 server/src/router/router_utils/flightplan.rs delete mode 100644 server/src/router/router_utils/graph.rs delete mode 100644 server/src/router/router_utils/haversine.rs delete mode 100644 server/src/router/router_utils/mock.rs delete mode 100644 server/src/router/router_utils/mod.rs delete mode 100644 server/src/router/router_utils/router_state.rs delete mode 100644 server/src/router/router_utils/schedule.rs create mode 100644 server/src/router/schedule.rs create mode 100644 server/src/router/vehicle.rs create mode 100644 server/src/router/vertiport.rs diff --git a/.cspell.project-words.txt b/.cspell.project-words.txt index b6e6c3b..c74c836 100644 --- a/.cspell.project-words.txt +++ b/.cspell.project-words.txt @@ -68,3 +68,5 @@ Montara oneshot owlot Refactorings +timeslot +timeslots diff --git a/Cargo.lock b/Cargo.lock index 8826ef2..6b3a6c3 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -47,7 +47,7 @@ dependencies = [ "amq-protocol-types", "amq-protocol-uri", "cookie-factory", - "nom 7.1.3", + "nom", "serde", ] @@ -69,7 +69,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "156ff13c8a3ced600b4e54ed826a2ae6242b6069d00dd98466827cef07d3daff" dependencies = [ "cookie-factory", - "nom 7.1.3", + "nom", "serde", "serde_json", ] @@ -102,15 +102,16 @@ dependencies = [ [[package]] name = "anstream" -version = "0.5.0" +version = "0.3.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b1f58811cfac344940f1a400b6e6231ce35171f614f26439e80f8c1465c5cc0c" +checksum = "0ca84f3628370c59db74ee214b3263d58f9aadd9b4fe7e711fd87dc452b7f163" dependencies = [ "anstyle", "anstyle-parse", "anstyle-query", "anstyle-wincon", "colorchoice", + "is-terminal", "utf8parse", ] @@ -140,9 +141,9 @@ dependencies = [ [[package]] name = "anstyle-wincon" -version = "2.1.0" +version = "1.0.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "58f54d10c6dfa51283a066ceab3ec1ab78d13fae00aa49243a45e4571fb79dfd" +checksum = "c677ab05e09154296dd37acecd46420c17b9713e8366facafa8fc0885167cf4c" dependencies = [ "anstyle", "windows-sys", @@ -175,23 +176,6 @@ version = "2.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3d62b7694a562cdf5a74227903507c56ab2cc8bdd1f781ed5cb4cf9c9f810bfc" -[[package]] -name = "arrayvec" -version = "0.5.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "23b62fc65de8e4e7f52534fb52b0f3ed04746ae267519eef2a83941e8085068b" - -[[package]] -name = "arrow-macros-core" -version = "0.1.1-develop.2" -source = "git+https://github.com/Arrow-air/lib-common.git?tag=v0.1.1-develop.2#c7e0fc4918d3346a13ab84e2d4fc16d430a60c59" -dependencies = [ - "proc-macro-error", - "proc-macro2", - "quote", - "syn 1.0.109", -] - [[package]] name = "arrow-macros-core" version = "0.2.0" @@ -203,22 +187,12 @@ dependencies = [ "syn 1.0.109", ] -[[package]] -name = "arrow-macros-derive" -version = "0.1.1-develop.2" -source = "git+https://github.com/Arrow-air/lib-common.git?tag=v0.1.1-develop.2#c7e0fc4918d3346a13ab84e2d4fc16d430a60c59" -dependencies = [ - "arrow-macros-core 0.1.1-develop.2", - "proc-macro-error", - "proc-macro2", -] - [[package]] name = "arrow-macros-derive" version = "0.2.0" source = "git+https://github.com/Arrow-air/lib-common.git?tag=v0.2.0#ecae9794fb2220aa03fdc04329bda32ca8441056" dependencies = [ - "arrow-macros-core 0.2.0", + "arrow-macros-core", "proc-macro-error", "proc-macro2", ] @@ -585,17 +559,6 @@ dependencies = [ "windows-targets", ] -[[package]] -name = "chrono-tz" -version = "0.6.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "29c39203181991a7dd4343b8005bd804e7a9a37afb8ac070e43771e8c820bbde" -dependencies = [ - "chrono", - "chrono-tz-build 0.0.3", - "phf", -] - [[package]] name = "chrono-tz" version = "0.8.3" @@ -603,19 +566,8 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f1369bc6b9e9a7dfdae2055f6ec151fe9c554a9d23d357c0237cee2e25eaabb7" dependencies = [ "chrono", - "chrono-tz-build 0.2.0", - "phf", -] - -[[package]] -name = "chrono-tz-build" -version = "0.0.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6f509c3a87b33437b05e2458750a0700e5bdd6956176773e6c7d6dd15a283a0c" -dependencies = [ - "parse-zoneinfo", + "chrono-tz-build", "phf", - "phf_codegen", ] [[package]] @@ -641,19 +593,20 @@ dependencies = [ [[package]] name = "clap" -version = "4.4.2" +version = "4.3.24" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6a13b88d2c62ff462f88e4a121f17a82c1af05693a2f192b5c38d14de73c19f6" +checksum = "fb690e81c7840c0d7aade59f242ea3b41b9bc27bcd5997890e7702ae4b32e487" dependencies = [ "clap_builder", "clap_derive", + "once_cell", ] [[package]] name = "clap_builder" -version = "4.4.2" +version = "4.3.24" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2bb9faaa7c2ef94b2743a21f5a29e6f0010dff4caa69ac8e9d6cf8b6fa74da08" +checksum = "5ed2e96bc16d8d740f6f48d663eddf4b8a0983e79210fd55479b7bcd0a69860e" dependencies = [ "anstream", "anstyle", @@ -663,9 +616,9 @@ dependencies = [ [[package]] name = "clap_derive" -version = "4.4.2" +version = "4.3.12" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0862016ff20d69b84ef8247369fabf5c008a7417002411897d40ee1f4532b873" +checksum = "54a9bb5758fc5dfe728d1019941681eccaf0cf8a4189b692a0ee2f2ecf90a050" dependencies = [ "heck", "proc-macro2", @@ -675,9 +628,9 @@ dependencies = [ [[package]] name = "clap_lex" -version = "0.5.1" +version = "0.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cd7cc57abe963c6d3b9d8be5b06ba7c8957a930305ca90304f24ef040aa6f961" +checksum = "2da6da31387c7e4ef160ffab6d5e7f00c42626fe39aea70a7b0f1773f7dd6c1b" [[package]] name = "colorchoice" @@ -703,7 +656,7 @@ dependencies = [ "async-trait", "json5", "lazy_static", - "nom 7.1.3", + "nom", "pathdiff", "ron", "rust-ini", @@ -1237,8 +1190,24 @@ dependencies = [ "geographiclib-rs", "log", "num-traits", - "robust", - "rstar", + "robust 0.2.3", + "rstar 0.10.0", +] + +[[package]] +name = "geo" +version = "0.26.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1645cf1d7fea7dac1a66f7357f3df2677ada708b8d9db8e9b043878930095a96" +dependencies = [ + "earcutr", + "float_next_after", + "geo-types", + "geographiclib-rs", + "log", + "num-traits", + "robust 1.1.0", + "rstar 0.11.0", ] [[package]] @@ -1249,7 +1218,8 @@ checksum = "9705398c5c7b26132e74513f4ee7c1d7dafd786004991b375c172be2be0eecaa" dependencies = [ "approx", "num-traits", - "rstar", + "rstar 0.10.0", + "rstar 0.11.0", "serde", ] @@ -1560,13 +1530,25 @@ dependencies = [ "windows-sys", ] +[[package]] +name = "is-terminal" +version = "0.4.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cb0889898416213fab133e1d33a0e5858a48177452750691bde3666d0fdbaf8b" +dependencies = [ + "hermit-abi", + "rustix 0.38.11", + "windows-sys", +] + [[package]] name = "iso8601-duration" -version = "0.1.0" +version = "0.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "60b51dd97fa24074214b9eb14da518957573f4dec3189112610ae1ccec9ac464" +checksum = "a26adff60a5d3ca10dc271ad37a34ff376595d2a1e5f21d02564929ca888c511" dependencies = [ - "nom 5.1.3", + "chrono", + "nom", ] [[package]] @@ -1632,41 +1614,12 @@ version = "1.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e2abad23fbc42b3700f2f279844dc832adb2b2eb069b2df918f455c4e18cc646" -[[package]] -name = "lexical-core" -version = "0.7.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6607c62aa161d23d17a9072cc5da0be67cdfc89d3afb1e8d9c842bebc2525ffe" -dependencies = [ - "arrayvec", - "bitflags 1.3.2", - "cfg-if", - "ryu", - "static_assertions", -] - -[[package]] -name = "lib-common" -version = "0.1.1-develop.2" -source = "git+https://github.com/Arrow-air/lib-common.git?tag=v0.1.1-develop.2#c7e0fc4918d3346a13ab84e2d4fc16d430a60c59" -dependencies = [ - "arrow-macros-derive 0.1.1-develop.2", - "cargo-husky", - "chrono", - "futures", - "log", - "prost", - "prost-types", - "tonic", - "trybuild", -] - [[package]] name = "lib-common" version = "0.2.0" source = "git+https://github.com/Arrow-air/lib-common.git?tag=v0.2.0#ecae9794fb2220aa03fdc04329bda32ca8441056" dependencies = [ - "arrow-macros-derive 0.2.0", + "arrow-macros-derive", "cargo-husky", "chrono", "futures", @@ -1857,17 +1810,6 @@ dependencies = [ "tempfile", ] -[[package]] -name = "nom" -version = "5.1.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "08959a387a676302eebf4ddbcbc611da04285579f76f88ee0506c63b1a61dd4b" -dependencies = [ - "lexical-core", - "memchr", - "version_check", -] - [[package]] name = "nom" version = "7.1.3" @@ -2334,7 +2276,6 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "90fcb95eef784c2ac79119d1dd819e162b5da872ce6f3c3abe1e8ca1c082f72b" dependencies = [ "siphasher", - "uncased", ] [[package]] @@ -2839,6 +2780,12 @@ version = "0.2.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e5864e7ef1a6b7bcf1d6ca3f655e65e724ed3b52546a0d0a663c991522f552ea" +[[package]] +name = "robust" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cbf4a6aa5f6d6888f39e980649f3ad6b666acdce1d78e95b8a2cb076e687ae30" + [[package]] name = "ron" version = "0.7.1" @@ -2857,7 +2804,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "615777991e415d96aa97f6e45e809111911497c029e5d0df7f6d751d218d28e8" dependencies = [ "chrono", - "chrono-tz 0.8.3", + "chrono-tz", "lazy_static", "log", "regex", @@ -2875,6 +2822,17 @@ dependencies = [ "smallvec", ] +[[package]] +name = "rstar" +version = "0.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "73111312eb7a2287d229f06c00ff35b51ddee180f017ab6dec1f69d62ac098d6" +dependencies = [ + "heapless", + "num-traits", + "smallvec", +] + [[package]] name = "rust-3d" version = "0.34.0" @@ -3293,12 +3251,6 @@ version = "1.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a8f112729512f8e442d81f95a8a7ddf2b7c6b8a1a6f509a95864142b30cab2d3" -[[package]] -name = "static_assertions" -version = "1.1.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a2eb9349b6444b326872e140eb1cf5e7c522154d69e7a0ffb0fb81c06b37543f" - [[package]] name = "stringprep" version = "0.1.4" @@ -3340,7 +3292,7 @@ dependencies = [ "dotenv", "hyper", "lapin", - "lib-common 0.2.0", + "lib-common", "log", "log4rs", "openssl", @@ -3365,7 +3317,7 @@ version = "0.1.1-develop.8" source = "git+https://github.com/Arrow-air/svc-compliance.git?tag=v0.1.1-develop.8#3bd3af88eac75fe3245293c1302c53dd205d6c15" dependencies = [ "cfg-if", - "lib-common 0.2.0", + "lib-common", "log", "prost", "prost-types", @@ -3391,7 +3343,7 @@ dependencies = [ "dotenv", "futures", "hyper", - "lib-common 0.2.0", + "lib-common", "log", "log4rs", "native-tls", @@ -3427,7 +3379,7 @@ source = "git+https://github.com/Arrow-air/svc-gis.git?tag=v0.0.1-develop.8#02b3 dependencies = [ "cfg-if", "chrono", - "lib-common 0.2.0", + "lib-common", "log", "num-derive", "num-traits", @@ -3448,16 +3400,16 @@ dependencies = [ "cargo-husky", "cfg-if", "chrono", - "chrono-tz 0.6.3", + "chrono-tz", "clap", "clap_lex", "config", "dotenv", "futures", - "geo", + "geo 0.26.0", "iso8601-duration", "lazy_static", - "lib-common 0.2.0", + "lib-common", "log", "log4rs", "logtest", @@ -3475,7 +3427,7 @@ dependencies = [ "svc-compliance-client-grpc", "svc-gis-client-grpc", "svc-scheduler", - "svc-storage-client-grpc 0.10.1-develop.22", + "svc-storage-client-grpc", "tokio", "tokio-util", "tonic", @@ -3491,9 +3443,9 @@ version = "0.3.1-develop.1" dependencies = [ "cfg-if", "chrono", - "chrono-tz 0.8.3", + "chrono-tz", "futures", - "lib-common 0.2.0", + "lib-common", "log", "logtest", "prost", @@ -3501,7 +3453,7 @@ dependencies = [ "prost-wkt-types", "svc-scheduler", "svc-scheduler-client-grpc", - "svc-storage-client-grpc 0.10.1-develop.21", + "svc-storage-client-grpc", "tokio", "tonic", "tower", @@ -3529,7 +3481,7 @@ dependencies = [ "geo-types", "hyper", "lazy_static", - "lib-common 0.2.0", + "lib-common", "log", "log4rs", "native-tls", @@ -3558,34 +3510,6 @@ dependencies = [ "uuid", ] -[[package]] -name = "svc-storage-client-grpc" -version = "0.10.1-develop.21" -source = "git+https://github.com/Arrow-air/svc-storage.git?tag=v0.10.1-develop.21#547b501d849c2a6cbba7c29f1825b9edd3ed68a3" -dependencies = [ - "anyhow", - "cfg-if", - "chrono", - "geo", - "geo-types", - "lib-common 0.1.1-develop.2", - "log", - "num-derive", - "num-traits", - "ordered-float 3.9.1", - "prost", - "prost-types", - "prost-wkt-types", - "rand 0.8.5", - "serde", - "serde_json", - "tokio", - "tonic", - "tonic-build", - "utoipa", - "uuid", -] - [[package]] name = "svc-storage-client-grpc" version = "0.10.1-develop.22" @@ -3595,10 +3519,10 @@ dependencies = [ "cfg-if", "chrono", "futures", - "geo", + "geo 0.25.1", "geo-types", "lazy_static", - "lib-common 0.2.0", + "lib-common", "log", "num-derive", "num-traits", @@ -4087,15 +4011,6 @@ version = "0.1.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ed646292ffc8188ef8ea4d1e0e0150fb15a5c2e12ad9b8fc191ae7a8a7f3c4b9" -[[package]] -name = "uncased" -version = "0.9.9" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9b9bc53168a4be7402ab86c3aad243a84dd7381d09be0eddc81280c1da95ca68" -dependencies = [ - "version_check", -] - [[package]] name = "unicase" version = "2.7.0" diff --git a/client-grpc/Cargo.toml b/client-grpc/Cargo.toml index 4ca748b..bb148df 100644 --- a/client-grpc/Cargo.toml +++ b/client-grpc/Cargo.toml @@ -43,7 +43,7 @@ uuid = "1.2" [dependencies.svc-storage-client-grpc] features = ["flight_plan", "itinerary"] git = "https://github.com/Arrow-air/svc-storage.git" -tag = "v0.10.1-develop.21" +tag = "v0.10.1-develop.22" [dependencies.lib-common] features = ["grpc"] diff --git a/client-grpc/examples/grpc.rs b/client-grpc/examples/grpc.rs index 8c37653..fd22d00 100644 --- a/client-grpc/examples/grpc.rs +++ b/client-grpc/examples/grpc.rs @@ -1,6 +1,6 @@ //! gRPC client implementation -use chrono::{TimeZone, Utc}; +use chrono::{offset::LocalResult::Single, TimeZone, Utc}; use lib_common::grpc::get_endpoint_from_env; use svc_scheduler_client_grpc::prelude::{scheduler::*, *}; @@ -19,20 +19,20 @@ async fn main() -> Result<(), Box> { assert_eq!(ready.ready, true); let departure_time = match Utc.with_ymd_and_hms(2022, 10, 25, 15, 0, 0) { - Ok(time) => time.timestamp() - Err(e) => { - println!("(main) failed to parse departure time: {}", e); + Single(time) => time.timestamp(), + _ => { + println!("(main) failed to parse arrival time."); return Ok(()); } - } + }; let arrival_time = match Utc.with_ymd_and_hms(2022, 10, 25, 16, 0, 0) { - Ok(time) => time.timestamp() - Err(e) => { - println!("(main) failed to parse arrival time: {}", e); + Single(time) => time.timestamp(), + _ => { + println!("(main) failed to parse arrival time."); return Ok(()); } - } + }; let request = QueryFlightRequest { is_cargo: true, @@ -51,7 +51,7 @@ async fn main() -> Result<(), Box> { }; let response = client.query_flight(request).await?.into_inner().itineraries; - let itinerary_id = (&response)[0].id.clone(); + let itinerary_id = (&response)[0].itinerary_id.clone(); println!("(main) itinerary id={}", itinerary_id); let response = client diff --git a/client-grpc/src/client.rs b/client-grpc/src/client.rs index 8eeb007..c06baf1 100644 --- a/client-grpc/src/client.rs +++ b/client-grpc/src/client.rs @@ -97,9 +97,8 @@ impl crate::service::Client> for SchedulerClient { }; let itineraries = vec![Itinerary { - id: uuid::Uuid::new_v4().to_string(), - flight_plan: Some(flight_plan), - deadhead_flight_plans: vec![], + itinerary_id: uuid::Uuid::new_v4().to_string(), + flight_plans: vec![flight_plan], }]; Ok(tonic::Response::new(QueryFlightResponse { itineraries })) diff --git a/client-grpc/src/grpc.rs b/client-grpc/src/grpc.rs index b88d238..aca89bd 100644 --- a/client-grpc/src/grpc.rs +++ b/client-grpc/src/grpc.rs @@ -42,15 +42,10 @@ pub struct ConfirmItineraryRequest { pub struct Itinerary { /// itinerary id #[prost(string, tag = "1")] - pub id: ::prost::alloc::string::String, + pub itinerary_id: ::prost::alloc::string::String, /// flight_plan - #[prost(message, optional, tag = "2")] - pub flight_plan: ::core::option::Option< - ::svc_storage_client_grpc::prelude::flight_plan::Object, - >, - /// deadhead flight plans - #[prost(message, repeated, tag = "3")] - pub deadhead_flight_plans: ::prost::alloc::vec::Vec< + #[prost(message, repeated, tag = "2")] + pub flight_plans: ::prost::alloc::vec::Vec< ::svc_storage_client_grpc::prelude::flight_plan::Object, >, } diff --git a/client-grpc/tests/integration_test.rs b/client-grpc/tests/integration_test.rs index cccf6d3..db22c4a 100644 --- a/client-grpc/tests/integration_test.rs +++ b/client-grpc/tests/integration_test.rs @@ -4,12 +4,10 @@ use lib_common::grpc::get_endpoint_from_env; use std::time::SystemTime; use svc_scheduler_client_grpc::prelude::{scheduler::*, *}; -use tokio::sync::OnceCell; #[tokio::test] async fn test_flights_query() -> Result<(), Box> { - let (server_host, server_port) = - lib_common::grpc::get_endpoint_from_env("GRPC_HOST", "GRPC_PORT"); + let (server_host, server_port) = get_endpoint_from_env("GRPC_HOST", "GRPC_PORT"); let client = SchedulerClient::new_client(&server_host, server_port, "scheduler"); let sys_time = SystemTime::now(); let request = QueryFlightRequest { @@ -30,8 +28,7 @@ async fn test_flights_query() -> Result<(), Box> { #[tokio::test] async fn test_cancel_itinerary() -> Result<(), Box> { - let (server_host, server_port) = - lib_common::grpc::get_endpoint_from_env("GRPC_HOST", "GRPC_PORT"); + let (server_host, server_port) = get_endpoint_from_env("GRPC_HOST", "GRPC_PORT"); let client = SchedulerClient::new_client(&server_host, server_port, "scheduler"); let request = Id { id: "1234".to_string(), @@ -45,8 +42,7 @@ async fn test_cancel_itinerary() -> Result<(), Box> { #[tokio::test] async fn test_confirm_itinerary() -> Result<(), Box> { - let (server_host, server_port) = - lib_common::grpc::get_endpoint_from_env("GRPC_HOST", "GRPC_PORT"); + let (server_host, server_port) = get_endpoint_from_env("GRPC_HOST", "GRPC_PORT"); let client = SchedulerClient::new_client(&server_host, server_port, "scheduler"); let request = ConfirmItineraryRequest { id: "1234".to_string(), @@ -61,8 +57,7 @@ async fn test_confirm_itinerary() -> Result<(), Box> { #[tokio::test] async fn test_is_ready() -> Result<(), Box> { - let (server_host, server_port) = - lib_common::grpc::get_endpoint_from_env("GRPC_HOST", "GRPC_PORT"); + let (server_host, server_port) = get_endpoint_from_env("GRPC_HOST", "GRPC_PORT"); let client = SchedulerClient::new_client(&server_host, server_port, "scheduler"); let request = ReadyRequest {}; diff --git a/log4rs.yaml b/log4rs.yaml index 1fac27c..e3bbe59 100644 --- a/log4rs.yaml +++ b/log4rs.yaml @@ -77,7 +77,7 @@ loggers: appenders: - grpc_clients app::router: - level: debug + level: info appenders: - router test::unit: diff --git a/proto/grpc.proto b/proto/grpc.proto index 93e44c1..05d5445 100644 --- a/proto/grpc.proto +++ b/proto/grpc.proto @@ -36,19 +36,18 @@ message ConfirmItineraryRequest { string user_id = 2; } -// Dummy message, will be replaced with -// svc_storage_client_grpc::flight_plan::Object type +// This is replaced by the FlightPlanObject from svc_storage +// during the build process. See build.rs message FlightPlanObject { + bool arbitrary = 1; } // Itinerary includes id, flight plan and potential deadhead flights message Itinerary { // itinerary id - string id = 1; + string itinerary_id = 1; //flight_plan - FlightPlanObject flight_plan = 2; - //deadhead flight plans - repeated FlightPlanObject deadhead_flight_plans = 3; + repeated FlightPlanObject flight_plans = 2; } //QueryFlightResponse diff --git a/server/Cargo.toml b/server/Cargo.toml index 5988f16..5460ffc 100644 --- a/server/Cargo.toml +++ b/server/Cargo.toml @@ -12,14 +12,13 @@ license-file.workspace = true repository.workspace = true [features] -default = [] -dev = ["mock"] -test_util = [ +default = [] +dev = ["mock"] +test_util = [ "mock", "stub_backends", "svc-storage-client-grpc/mock", "svc-compliance-client-grpc/mock", - # "svc-gis-client-grpc/mock", ] vendored-openssl = ["openssl/vendored"] @@ -41,18 +40,18 @@ anyhow = "1.0" cargo-husky = "1" cfg-if = "1.0" chrono = { version = "0.4", features = ["serde"] } -chrono-tz = "0.6" +chrono-tz = "0.8" # pin clap to 4.3 for now, since 4.4 requires rustc 1.70.0 or newer. Only needed # for svc-compliance, can be removed once resolved -clap = { version = "4.3.24", features = ["derive"] } +clap = { version = "=4.3.24", features = ["derive"] } # pin clap_lex to 0.5.0 for now, since 0.5.1 requires rustc 1.70.0 or newer. # Only needed for svc-compliance, can be removed once resolved -clap_lex = "0.5.0" +clap_lex = "=0.5.0" config = "0.13" dotenv = "0.15" futures = "0.3" -geo = "0.25" -iso8601-duration = "0.1" +geo = "0.26" +iso8601-duration = { version = "0.2", features = ["chrono"] } lazy_static = "1.4" log = "0.4" openssl = "0.10" diff --git a/server/src/grpc/api/cancel_itinerary.rs b/server/src/grpc/api/cancel_itinerary.rs new file mode 100644 index 0000000..67abcc5 --- /dev/null +++ b/server/src/grpc/api/cancel_itinerary.rs @@ -0,0 +1,137 @@ +//! This module contains the gRPC cancel_itinerary endpoint implementation. + +use crate::grpc::client::get_clients; +use crate::grpc::server::grpc_server::{CancelItineraryResponse, Id}; +use chrono::Utc; +use svc_storage_client_grpc::prelude::Id as StorageId; +use svc_storage_client_grpc::prelude::*; +use tonic::{Request, Response, Status}; +use uuid::Uuid; + +/// Cancels a draft or confirmed flight plan +pub async fn cancel_itinerary( + request: Request, +) -> Result, Status> { + let itinerary_id = match Uuid::parse_str(&request.into_inner().id) { + Ok(id) => id.to_string(), + Err(_) => { + return Err(Status::invalid_argument("Invalid itinerary ID.")); + } + }; + + let clients = get_clients().await; + grpc_info!("(cancel_itinerary) for id {}.", &itinerary_id); + + // + // Look within unconfirmed itineraries + // + if super::remove_draft_itinerary_by_id(&itinerary_id) { + let response = CancelItineraryResponse { + id: itinerary_id, + cancelled: true, + cancellation_time: Some(Utc::now().into()), + reason: "user cancelled".into(), + }; + + return Ok(Response::new(response)); + } + + // + // Look within confirmed itineraries + // + clients + .storage + .itinerary + .get_by_id(StorageId { + id: itinerary_id.clone(), + }) + .await?; + + // + // TODO(R4) Don't allow cancellations within X minutes of the first flight + // + + // + // Remove itinerary + // + let update_object = itinerary::UpdateObject { + id: itinerary_id.clone(), + data: Option::from(itinerary::Data { + user_id: "".to_string(), // will be masked + status: itinerary::ItineraryStatus::Cancelled as i32, + }), + mask: Some(FieldMask { + paths: vec!["status".to_string()], + }), + }; + clients.storage.itinerary.update(update_object).await?; + + grpc_info!( + "(cancel_itinerary) cancel_itinerary with id {} cancelled in storage.", + &itinerary_id + ); + + let response = clients + .storage + .itinerary_flight_plan_link + .get_linked_ids(StorageId { + id: itinerary_id.clone(), + }) + .await?; + + // + // Cancel associated flight plans + // + //TODO(R4): svc-storage currently doesn't check the FieldMask, so we'll + //have to provide it with the right data object for now. Will now be handled + //with temp code in for loop, but should be: + //let mut flight_plan_data = flight_plan::Data::default(); + //flight_plan_data.flight_status = flight_plan::FlightStatus::Cancelled as i32; + for id in response.into_inner().ids { + // begin temp code + let flight_plan = clients + .storage + .flight_plan + .get_by_id(StorageId { id: id.clone() }) + .await?; + + let Some(mut flight_plan_data) = flight_plan.into_inner().data else { + grpc_warn!("(cancel_itinerary) WARNING: Could not cancel flight plan with ID: {}", id); + continue; + }; + + flight_plan_data.flight_status = flight_plan::FlightStatus::Cancelled as i32; + // end temp code + + // + // TODO(R4): Don't cancel flight plan if it exists in another itinerary + // + let request = flight_plan::UpdateObject { + id: id.clone(), + data: Some(flight_plan_data.clone()), + mask: Some(FieldMask { + paths: vec!["flight_status".to_string()], + }), + }; + let result = clients.storage.flight_plan.update(request).await; + + // Keep going even if there's a warning + if result.is_err() { + grpc_warn!( + "(cancel_itinerary) WARNING: Could not cancel flight plan with ID: {}", + id + ); + } + } + + // + // Reply + // + let response = CancelItineraryResponse { + id: itinerary_id, + cancelled: true, + cancellation_time: Some(Utc::now().into()), + reason: "user cancelled".into(), + }; + Ok(Response::new(response)) +} diff --git a/server/src/grpc/api/confirm_itinerary.rs b/server/src/grpc/api/confirm_itinerary.rs new file mode 100644 index 0000000..0d4f5d9 --- /dev/null +++ b/server/src/grpc/api/confirm_itinerary.rs @@ -0,0 +1,240 @@ +//! This module contains the gRPC confirm_itinerary endpoint implementation. + +use svc_storage_client_grpc::prelude::*; + +use crate::grpc::client::get_clients; +use crate::grpc::server::grpc_server::{ConfirmItineraryRequest, ConfirmItineraryResponse}; +// TODO(R4): Compliance service will handle this without needing a request from scheduler +use chrono::Utc; +use svc_compliance_client_grpc::client::FlightPlanRequest; +use svc_compliance_client_grpc::service::Client as ComplianceServiceClient; +use svc_storage_client_grpc::prelude::{flight_plan, IdList}; +use tonic::{Request, Response, Status}; + +/// Registers an itinerary with svc-storage. +/// +/// There's two steps involved with registering an itinerary: +/// 1) Register a new itinerary with the `itinerary` table in the database +/// 2) Link flight plan IDs to the itinerary (a separate `itinerary_flight_plan` table) +async fn create_itinerary( + draft_itinerary_id: String, + user_id: String, + confirmed_flight_plan_ids: Vec, +) -> Result { + let clients = get_clients().await; + // + // 1) Add itinerary to `itinerary` DB table + // + let data = itinerary::Data { + user_id: user_id.to_string(), + status: itinerary::ItineraryStatus::Active as i32, + }; + + let Some(db_itinerary) = clients.storage.itinerary + .insert(data) + .await? + .into_inner() + .object + else { + return Err(Status::internal("Couldn't add itinerary to storage.")); + }; + + // + // 2) Link flight plans to itinerary in `itinerary_flight_plan` + // + clients + .storage + .itinerary_flight_plan_link + .link(itinerary::ItineraryFlightPlans { + id: db_itinerary.id.clone(), + other_id_list: Some(IdList { + ids: confirmed_flight_plan_ids, + }), + }) + .await?; + + // At this point all draft flight plans have been confirmed and can be + // removed from local memory along with the draft itinerary + let _ = super::remove_draft_itinerary_by_id(&draft_itinerary_id); + Ok(db_itinerary) +} + +/// Confirms a flight plan by registering it with svc-storage +/// After confirmation, the flight plan will be removed from local cache +async fn confirm_draft_flight_plan(flight_plan_id: String) -> Result { + let clients = get_clients().await; + + let Some(flight_plan) = super::get_draft_fp_by_id(&flight_plan_id) else { + return Err(Status::internal("Draft flight plan ID doesn't exist.")); + }; + + // + // Confirm a flight plan with the database + // + let Some(fp) = clients.storage.flight_plan + .insert(flight_plan) + .await? + .into_inner() + .object + else { + return Err(Status::internal("Failed to add a flight plan to the database.")); + }; + + grpc_info!( + "(confirm_draft_flight_plan) flight plan with draft id: {} + inserted into storage with permanent id: {}", + &flight_plan_id, + &fp.id + ); + + // + // Remove the flight plan from the cache now that it's in the database + // + let _ = super::remove_draft_fp_by_id(&flight_plan_id); + + // TODO(R4): Compliance service will handle this without needing a request from scheduler + // Retrieve user data in case that's relevant to compliance + let compliance_res = clients + .compliance + .submit_flight_plan(FlightPlanRequest { + flight_plan_id: fp.id.clone(), + data: "".to_string(), + }) + .await? + .into_inner(); + grpc_info!( + "(confirm_draft_flight_plan) Compliance response for flight plan id : {} is submitted: {}", + &fp.id, + compliance_res.submitted + ); + + Ok(fp) +} + +/// Confirms an itinerary +pub async fn confirm_itinerary( + request: Request, +) -> Result, Status> { + // + // Return if the itinerary has expired in cache + // + let request = request.into_inner(); + let draft_itinerary_id = request.id; + grpc_info!("(confirm_itinerary) with id {}", &draft_itinerary_id); + + let Some(draft_itinerary_flights) = super::get_draft_itinerary_by_id(&draft_itinerary_id) else { + return Err(Status::not_found("Itinerary ID not found or timed out.")); + }; + + // + // For each Draft flight in the itinerary, push to svc-storage + // + let mut confirmed_flight_plan_ids: Vec = vec![]; + for fp_id in draft_itinerary_flights { + // TODO(R4) - Check if flight plan is already in database + // if fp.fp_type == FlightPlanType::Existing { + // // TODO(R4) - update record with new parcel/passenger data + + // confirmed_flight_plan_ids.push(fp.fp_id); + // continue; + // } + + // TODO(R4) - insert all flight plans at same time (one transaction) + // so if one fails they all fail + let confirmation = confirm_draft_flight_plan(fp_id).await; + let Ok(confirmed_fp) = confirmation else { + break; + }; + + confirmed_flight_plan_ids.push(confirmed_fp.id); + } + + // + // Create and insert itinerary + // + let result = create_itinerary( + draft_itinerary_id, + request.user_id, + confirmed_flight_plan_ids, + ) + .await; + + match result { + Ok(itinerary) => { + let response = ConfirmItineraryResponse { + id: itinerary.id, + confirmed: true, + confirmation_time: Some(Utc::now().into()), + }; + + Ok(Response::new(response)) + } + Err(e) => Err(e), + } +} + +// #[cfg(test)] +// mod tests { +// use super::*; +// use crate::config::Config; +// use crate::grpc::api::cancel_itinerary::cancel_itinerary; +// use crate::grpc::api::query_flight::query_flight; +// use crate::grpc_server::QueryFlightRequest; +// use crate::init_logger; + +// #[tokio::test] +// async fn test_confirm_and_cancel_itinerary() { +// init_logger(&Config::try_from_env().unwrap_or_default()); +// unit_test_info!("(test_confirm_and_cancel_itinerary) start"); +// ensure_storage_mock_data().await; +// let res = confirm_itinerary(Request::new(ConfirmItineraryRequest { +// id: "itinerary1".to_string(), +// user_id: "".to_string(), +// })) +// .await; +// //test confirming a flight that does not exist will return an error +// assert_eq!(res.is_err(), true); + +// let vertiports = get_vertiports_from_storage().await; +// let res = query_flight(Request::new(QueryFlightRequest { +// is_cargo: false, +// persons: None, +// weight_grams: None, +// earliest_departure_time: Some( +// Utc.datetime_from_str("2022-10-25 11:20:00", "%Y-%m-%d %H:%M:%S") +// .unwrap() +// .into(), +// ), +// latest_arrival_time: Some( +// Utc.datetime_from_str("2022-10-25 12:15:00", "%Y-%m-%d %H:%M:%S") +// .unwrap() +// .into(), +// ), +// vertiport_depart_id: vertiports[0].id.clone(), +// vertiport_arrive_id: vertiports[1].id.clone(), +// })) +// .await; +// unit_test_debug!( +// "(test_confirm_and_cancel_itinerary) query_flight result: {:#?}", +// res +// ); +// assert!(res.is_ok()); +// let id = res.unwrap().into_inner().itineraries[0].id.clone(); +// let res = confirm_itinerary(Request::new(ConfirmItineraryRequest { +// id, +// user_id: "".to_string(), +// })) +// .await; +// assert!(res.is_ok()); +// let confirm_response: ConfirmItineraryResponse = res.unwrap().into_inner(); +// //test confirming a flight that does exist will return a success +// assert_eq!(confirm_response.confirmed, true); + +// let id = confirm_response.id.clone(); +// let res = cancel_itinerary(Request::new(Id { id })).await; +// assert!(res.is_ok()); +// assert_eq!(res.unwrap().into_inner().cancelled, true); + +// unit_test_info!("(test_confirm_and_cancel_itinerary) success"); +// } +// } diff --git a/server/src/grpc/api/mod.rs b/server/src/grpc/api/mod.rs new file mode 100644 index 0000000..d8867fa --- /dev/null +++ b/server/src/grpc/api/mod.rs @@ -0,0 +1,110 @@ +//! gRPC API parent module + +pub mod cancel_itinerary; +pub mod confirm_itinerary; +pub mod query_flight; + +use lazy_static::lazy_static; +use std::collections::HashMap; +use std::sync::Mutex; +use svc_storage_client_grpc::prelude::flight_plan; + +/// Time to wait for a flight plan to be confirmed before cancelling it +const ITINERARY_EXPIRATION_S: u64 = 30; + +lazy_static! { + static ref UNCONFIRMED_ITINERARIES: Mutex>> = + Mutex::new(HashMap::new()); + static ref UNCONFIRMED_FLIGHT_PLANS: Mutex> = + Mutex::new(HashMap::new()); +} + +/// gets a hashmap of unconfirmed itineraries +/// "itineraries" are a list of flight plan IDs, which can represent DRAFT +/// (in memory) or EXISTING (in database) flight plans +pub fn unconfirmed_itineraries() -> &'static Mutex>> { + &UNCONFIRMED_ITINERARIES +} + +/// gets a hashmap of unconfirmed flight plans +pub fn unconfirmed_flight_plans() -> &'static Mutex> { + &UNCONFIRMED_FLIGHT_PLANS +} + +/// Gets itinerary from hash map of unconfirmed itineraries +pub fn get_draft_itinerary_by_id(id: &str) -> Option> { + unconfirmed_itineraries() + .lock() + .expect("Mutex Lock Error getting itinerary from temp storage") + .get(id) + .cloned() +} + +/// Gets flight plan from hash map of unconfirmed flight plans +pub fn get_draft_fp_by_id(id: &str) -> Option { + unconfirmed_flight_plans() + .lock() + .expect("Mutex Lock Error getting flight plan from temp storage") + .get(id) + .cloned() +} + +/// spawns a thread that will cancel the itinerary after a certain amount of time (ITINERARY_EXPIRATION_S) +fn cancel_itinerary_after_timeout(id: String) { + tokio::spawn(async move { + tokio::time::sleep(core::time::Duration::from_secs(ITINERARY_EXPIRATION_S)).await; + remove_draft_itinerary_by_id(&id); + grpc_debug!("Flight plan {} was not confirmed in time, cancelling", id); + }); +} + +/// Removes flight plan from hash map of unconfirmed flight plans +fn remove_draft_fp_by_id(id: &str) -> bool { + let mut flight_plans = unconfirmed_flight_plans() + .lock() + .expect("(remove_draft_fp_by_id) mutex Lock Error removing flight plan from temp storage"); + + match flight_plans.remove(id) { + Some(_) => { + grpc_debug!( + "(remove_draft_fp_by_id) with id {} removed from local cache", + &id + ); + true + } + _ => { + grpc_debug!( + "(remove_draft_fp_by_id) no such flight plan with ID {} in cache", + &id + ); + false + } + } +} + +/// Removes itinerary from hash map of unconfirmed flight plans +fn remove_draft_itinerary_by_id(id: &str) -> bool { + let mut itineraries = unconfirmed_itineraries().lock().expect( + "(remove_draft_itinerary_by_id) mutex Lock Error removing itinerary from temp storage", + ); + + let Some(itinerary) = itineraries.get(id) else { + grpc_debug!("(remove_draft_itinerary_by_id) no such itinerary with ID {} in cache", &id); + return false; + }; + + // Remove draft flight plans associated with this itinerary + for fp_id in itinerary { + // TODO(R4) - Remove flight plans if they are draft and only in + // one itinerary + // if fp.fp_type == FlightPlanType::Draft { + // Ignore if not found + let _ = remove_draft_fp_by_id(fp_id); + // } + } + + itineraries.remove(id); + + grpc_info!("cancel_itinerary with id {} removed from local cache", &id); + true +} diff --git a/server/src/grpc/api/query_flight.rs b/server/src/grpc/api/query_flight.rs new file mode 100644 index 0000000..a6ddf25 --- /dev/null +++ b/server/src/grpc/api/query_flight.rs @@ -0,0 +1,604 @@ +//! This module contains the gRPC query_flight endpoint implementation. + +use chrono::{DateTime, Duration, Utc}; +use std::fmt::{Display, Formatter, Result as FmtResult}; +use tonic::{Request, Response, Status}; +use uuid::Uuid; + +use crate::grpc::client::get_clients; +use crate::grpc::server::grpc_server::{Itinerary, QueryFlightRequest, QueryFlightResponse}; + +use crate::router::flight_plan::*; +use crate::router::itinerary::get_itineraries; +use crate::router::schedule::*; +use crate::router::vehicle::*; +use crate::router::vertiport::*; +use svc_storage_client_grpc::prelude::flight_plan::Object as FlightPlanObject; + +/// Time to block vertiport for cargo loading and takeoff +pub const LOADING_AND_TAKEOFF_TIME_SECONDS: i64 = 600; +/// Time to block vertiport for cargo unloading and landing +pub const LANDING_AND_UNLOADING_TIME_SECONDS: i64 = 600; +/// Maximum time between departure and arrival times for flight queries +pub const MAX_FLIGHT_QUERY_WINDOW_MINUTES: i64 = 360; // +/- 3 hours (6 total) + +/// Sanitized version of the gRPC query +struct FlightQuery { + departure_vertiport_id: String, + arrival_vertiport_id: String, + earliest_departure_time: DateTime, + latest_arrival_time: DateTime, + required_loading_time: Duration, + required_unloading_time: Duration, +} + +/// Error type for FlightQuery +#[derive(Debug, Clone, Copy)] +enum FlightQueryError { + InvalidVertiportId, + InvalidTime, + TimeRangeTooLarge, +} + +impl Display for FlightQueryError { + fn fmt(&self, f: &mut Formatter<'_>) -> FmtResult { + match self { + FlightQueryError::InvalidVertiportId => write!(f, "Invalid vertiport ID"), + FlightQueryError::InvalidTime => write!(f, "Invalid time"), + FlightQueryError::TimeRangeTooLarge => write!(f, "Time range too large"), + } + } +} + +impl TryFrom for FlightQuery { + type Error = FlightQueryError; + + fn try_from(request: QueryFlightRequest) -> Result { + const ERROR_PREFIX: &str = "(TryFrom FlightQuery)"; + + let departure_vertiport_id = match Uuid::parse_str(&request.vertiport_depart_id) { + Ok(id) => id.to_string(), + _ => { + grpc_error!( + "{} Invalid departure vertiport ID: {}", + ERROR_PREFIX, + request.vertiport_depart_id + ); + return Err(FlightQueryError::InvalidVertiportId); + } + }; + + let arrival_vertiport_id = match Uuid::parse_str(&request.vertiport_arrive_id) { + Ok(id) => id.to_string(), + _ => { + grpc_error!( + "{} Invalid departure vertiport ID: {}", + ERROR_PREFIX, + request.vertiport_arrive_id + ); + return Err(FlightQueryError::InvalidVertiportId); + } + }; + + let Some(latest_arrival_time) = request.latest_arrival_time.clone() else { + grpc_warn!("{} latest arrival time not provided.", ERROR_PREFIX); + return Err(FlightQueryError::InvalidTime); + }; + + let Some(earliest_departure_time) = request.earliest_departure_time else { + grpc_warn!("{} earliest departure time not provided.", ERROR_PREFIX); + return Err(FlightQueryError::InvalidTime); + }; + + let latest_arrival_time: DateTime = latest_arrival_time.into(); + let earliest_departure_time: DateTime = earliest_departure_time.into(); + + if earliest_departure_time > latest_arrival_time { + grpc_warn!( + "{} earliest departure time is after latest arrival time.", + ERROR_PREFIX + ); + return Err(FlightQueryError::InvalidTime); + } + + // Prevent attacks where a user requests a wide flight window, resulting in a large number of + // calls to svc-gis for routing + if latest_arrival_time - earliest_departure_time + > Duration::minutes(MAX_FLIGHT_QUERY_WINDOW_MINUTES) + { + grpc_warn!("{} time range too large.", ERROR_PREFIX); + return Err(FlightQueryError::TimeRangeTooLarge); + } + + if latest_arrival_time < Utc::now() { + grpc_warn!("{} latest arrival time is in the past.", ERROR_PREFIX); + return Err(FlightQueryError::InvalidTime); + } + + Ok(FlightQuery { + departure_vertiport_id, + arrival_vertiport_id, + latest_arrival_time, + earliest_departure_time, + // TODO(R4): Get needed loading/unloading times from request + required_loading_time: Duration::seconds(LOADING_AND_TAKEOFF_TIME_SECONDS), + required_unloading_time: Duration::seconds(LANDING_AND_UNLOADING_TIME_SECONDS), + }) + } +} + +/// Finds the first possible flight for customer location, flight type and requested time. +/// TODO(R5): Return a stream of messages for live updates on query progress +pub async fn query_flight( + request: Request, +) -> Result, Status> { + let request = request.into_inner(); + let request = match FlightQuery::try_from(request) { + Ok(request) => request, + Err(e) => { + let error_str = "Invalid flight query request"; + grpc_error!("(query_flight) {error_str}: {e}"); + return Err(Status::invalid_argument(error_str)); + } + }; + + let clients = get_clients().await; + + // TODO(R4): Don't get flight plans until we have a vertipad timeslot match - may not need to + // get existing flight plans with the vertipad_timeslot call planned for R4 + // Get all flight plans from this time to latest departure time (including partially fitting flight plans) + // - this assumes that all landed flights have updated vehicle.last_vertiport_id (otherwise we would need to look in to the past) + let existing_flight_plans = + match get_sorted_flight_plans(&request.latest_arrival_time, clients).await { + Ok(plans) => plans, + Err(e) => { + let error_str = "Could not get existing flight plans."; + grpc_error!("(query_flight) {} {}", error_str, e); + return Err(Status::internal(error_str)); + } + }; + + // Add draft flight plans to the "existing" flight plans + // let Ok(draft_flight_plans) = super::unconfirmed_flight_plans().lock() else { + // let error_str = "Could not get draft flight plans."; + // grpc_error!("(query_flight) {} {}", error_str, e); + // return Err(Status::internal(error_str)); + // }; + + // draft_flight_plans.iter().for_each(|(_, fp)| { + // // only push flight plans for aircraft that we have in our list + // // don't want to schedule new flights for removed aircraft + // if let Some(schedule) = aircraft.get_mut(&fp.vehicle_id) { + // schedule.push(fp.clone()); + // } else { + // grpc_warn!( + // "(query_flight) Flight plan for unknown aircraft: {}", + // fp.vehicle_id + // ); + // } + // }); + + // + // TODO(R4): Determine if there's an open space for cargo on an existing flight plan + // + + grpc_debug!( + "(query_flight) found existing flight plans: {:?}", + existing_flight_plans + ); + + let timeslot = Timeslot { + time_start: request.earliest_departure_time, + time_end: request.latest_arrival_time, + }; + + // + // Get available timeslots for departure vertiport that are large enough to + // fit the required loading and takeoff time. + // + let Ok(timeslot_pairs) = get_timeslot_pairs( + &request.departure_vertiport_id, + &request.arrival_vertiport_id, + &request.required_loading_time, + &request.required_unloading_time, + ×lot, + &existing_flight_plans, + clients, + ).await else { + let error_str = "Could not find a timeslot pairing."; + grpc_error!("(query_flight) {}", error_str); + return Err(Status::internal(error_str)); + }; + + if timeslot_pairs.is_empty() { + let info_str = "No routes available for the given time."; + grpc_info!("(query_flight) {info_str}"); + return Err(Status::not_found(info_str)); + } + + // + // Get all aircraft availabilities + // + let Ok(aircraft_gaps) = get_aircraft_gaps( + &existing_flight_plans, + clients + ).await else { + let error_str = "Could not get aircraft availabilities."; + grpc_error!("(query_flight) {}", error_str); + return Err(Status::internal(error_str)); + }; + + // + // See which aircraft are available to fly the route, + // including deadhead flights + // + grpc_debug!("(query_flight) timeslot pairs count {:?}", timeslot_pairs); + let Ok(itineraries) = get_itineraries( + &request.required_loading_time, + &request.required_unloading_time, + ×lot_pairs, + &aircraft_gaps, + clients + ).await else { + let error_str = "Could not get itineraries."; + grpc_error!("(query_flight) {}", error_str); + return Err(Status::internal(error_str)); + }; + grpc_debug!("(query_flight) itineraries count {:?}", itineraries); + + // + // Create draft itinerary and flight plans (in memory) + // + let mut response = QueryFlightResponse { + itineraries: vec![], + }; + + let Ok(mut unconfirmed_flight_plans) = super::unconfirmed_flight_plans().lock() else { + let error_str = "Could not get draft flight plans."; + grpc_error!("(query_flight) {}", error_str); + return Err(Status::internal(error_str)); + }; + + let Ok(mut unconfirmed_itineraries) = super::unconfirmed_itineraries().lock() else { + let error_str = "Could not get draft itineraries."; + grpc_error!("(query_flight) {}", error_str); + return Err(Status::internal(error_str)); + }; + + for itinerary in itineraries.into_iter() { + let mut flight_plans = vec![]; + for fp in itinerary { + let flight_plan_id = Uuid::new_v4().to_string(); + flight_plans.push(FlightPlanObject { + id: flight_plan_id.clone(), + data: Some(fp.clone()), + }); + + unconfirmed_flight_plans.insert(flight_plan_id.clone(), fp.clone()); + } + + let itinerary_id = Uuid::new_v4().to_string(); + unconfirmed_itineraries.insert( + itinerary_id.clone(), + flight_plans.iter().map(|fp| fp.id.clone()).collect(), + ); + + super::cancel_itinerary_after_timeout(itinerary_id.clone()); + response.itineraries.push(Itinerary { + itinerary_id, + flight_plans, + }); + } + + grpc_info!( + "(query_flight) query_flight returning: {} flight plans", + &response.itineraries.len() + ); + + Ok(Response::new(response)) +} + +#[cfg(test)] +#[cfg(feature = "stub_backends")] +mod tests { + use crate::test_util::{ensure_storage_mock_data, get_vertiports_from_storage}; + use crate::{init_logger, Config}; + + use super::*; + use chrono::{TimeZone, Utc}; + + // #[tokio::test] + // async fn test_get_sorted_flight_plans() { + // init_logger(&Config::try_from_env().unwrap_or_default()); + // unit_test_info!("(test_get_sorted_flight_plans) start"); + // ensure_storage_mock_data().await; + // let clients = get_clients().await; + + // // our mock setup inserts only 3 flight_plans with an arrival date before "2022-10-26 14:30:00" + // let expected_number_returned = 3; + + // let res = get_sorted_flight_plans( + // &Utc.datetime_from_str("2022-10-26 14:30:00", "%Y-%m-%d %H:%M:%S").unwrap(), + // &clients + // ).await; + // unit_test_debug!( + // "(test_get_sorted_flight_plans) flight_plans returned: {:#?}", + // res + // ); + + // assert!(res.is_ok()); + // assert_eq!(res.unwrap().len(), expected_number_returned); + // unit_test_info!("(test_get_sorted_flight_plans) success"); + // } + + // #[tokio::test] + // async fn test_query_flight() { + // init_logger(&Config::try_from_env().unwrap_or_default()); + // unit_test_info!("(test_query_flight) start"); + // ensure_storage_mock_data().await; + + // let vertiports = get_vertiports_from_storage().await; + // let res = query_flight(Request::new(QueryFlightRequest { + // is_cargo: false, + // persons: None, + // weight_grams: None, + // earliest_departure_time: Some( + // Utc.datetime_from_str("2022-10-25 11:20:00", "%Y-%m-%d %H:%M:%S") + // .unwrap() + // .into(), + // ), + // latest_arrival_time: Some( + // Utc.datetime_from_str("2022-10-25 12:15:00", "%Y-%m-%d %H:%M:%S") + // .unwrap() + // .into(), + // ), + // vertiport_depart_id: vertiports[0].id.clone(), + // vertiport_arrive_id: vertiports[1].id.clone(), + // })) + // .await; + // unit_test_debug!("(test_query_flight) query_flight result: {:?}", res); + // assert!(res.is_ok()); + // assert_eq!(res.unwrap().into_inner().itineraries.len(), 5); + // unit_test_info!("(test_query_flight) success"); + // } + + // ///4. destination vertiport is available for about 15 minutes, no other restrictions + // /// - returns 2 flights (assuming 10 minutes needed for unloading, this can fit 2 flights + // /// if first is exactly at the beginning of 15 minute gap and second is exactly after 5 minutes) + // #[tokio::test] + // async fn test_query_flight_4_dest_vertiport_tight_availability_should_return_two_flights() { + // init_logger(&Config::try_from_env().unwrap_or_default()); + // unit_test_info!("(test_query_flight_4_dest_vertiport_tight_availability_should_return_two_flights) start"); + // ensure_storage_mock_data().await; + + // let vertiports = get_vertiports_from_storage().await; + // let res = query_flight(Request::new(QueryFlightRequest { + // is_cargo: false, + // persons: None, + // weight_grams: None, + // earliest_departure_time: Some( + // Utc.datetime_from_str("2022-10-25 14:20:00", "%Y-%m-%d %H:%M:%S") + // .unwrap() + // .into(), + // ), + // latest_arrival_time: Some( + // Utc.datetime_from_str("2022-10-25 15:10:00", "%Y-%m-%d %H:%M:%S") + // .unwrap() + // .into(), + // ), + // vertiport_depart_id: vertiports[2].id.clone(), + // vertiport_arrive_id: vertiports[1].id.clone(), + // })) + // .await + // .unwrap(); + + // unit_test_debug!("(test_query_flight_4_dest_vertiport_tight_availability_should_return_two_flights) query_flight result: {:#?}", res); + // assert_eq!(res.into_inner().itineraries.len(), 2); + // unit_test_info!("(test_query_flight_4_dest_vertiport_tight_availability_should_return_two_flights) success"); + // } + + // ///5. source or destination vertiport doesn't have any vertipad free for the time range + // ///no flight plans returned + // #[tokio::test] + // async fn test_query_flight_5_dest_vertiport_no_availability_should_return_zero_flights() { + // init_logger(&Config::try_from_env().unwrap_or_default()); + // unit_test_info!( + // "(test_query_flight_5_dest_vertiport_no_availability_should_return_zero_flights) start" + // ); + // ensure_storage_mock_data().await; + + // let vertiports = get_vertiports_from_storage().await; + // let res = query_flight(Request::new(QueryFlightRequest { + // is_cargo: false, + // persons: None, + // weight_grams: None, + // earliest_departure_time: Some( + // Utc.datetime_from_str("2022-10-26 14:00:00", "%Y-%m-%d %H:%M:%S") + // .unwrap() + // .into(), + // ), + // latest_arrival_time: Some( + // Utc.datetime_from_str("2022-10-26 14:40:00", "%Y-%m-%d %H:%M:%S") + // .unwrap() + // .into(), + // ), + // vertiport_depart_id: vertiports[1].id.clone(), + // vertiport_arrive_id: vertiports[0].id.clone(), + // })) + // .await; + + // unit_test_debug!("(test_query_flight_5_dest_vertiport_no_availability_should_return_zero_flights) query_flight result: {:#?}", res); + // assert_eq!( + // res.unwrap_err() + // .message() + // .contains("No flight plans available"), + // true + // ); + // unit_test_info!( + // "(test_query_flight_5_dest_vertiport_no_availability_should_return_zero_flights) success" + // ); + // } + + // ///6. vertiports are available but aircraft are not at the vertiport for the requested time + // /// but at least one aircraft is IN FLIGHT to requested vertiport for that time and has availability for a next flight. + // /// - skips all unavailable time slots (4) and returns only time slots from when aircraft is available (1) + // #[tokio::test] + // async fn test_query_flight_6_no_aircraft_at_vertiport() { + // init_logger(&Config::try_from_env().unwrap_or_default()); + // unit_test_info!("(test_query_flight_6_no_aircraft_at_vertiport) start"); + // ensure_storage_mock_data().await; + + // let vertiports = get_vertiports_from_storage().await; + // let res = query_flight(Request::new(QueryFlightRequest { + // is_cargo: false, + // persons: None, + // weight_grams: None, + // earliest_departure_time: Some( + // Utc.datetime_from_str("2022-10-26 14:15:00", "%Y-%m-%d %H:%M:%S") + // .unwrap() + // .into(), + // ), + // latest_arrival_time: Some( + // Utc.datetime_from_str("2022-10-26 15:00:00", "%Y-%m-%d %H:%M:%S") + // .unwrap() + // .into(), + // ), + // vertiport_depart_id: vertiports[0].id.clone(), + // vertiport_arrive_id: vertiports[2].id.clone(), + // })) + // .await + // .unwrap() + // .into_inner(); + + // unit_test_debug!( + // "(test_query_flight_6_no_aircraft_at_vertiport) query_flight result: {:#?}", + // res + // ); + // assert_eq!(res.itineraries.len(), 1); + // assert_eq!(res.itineraries[0].flight_plans.len(), 1); + // unit_test_info!("(test_query_flight_6_no_aircraft_at_vertiport) success"); + // } + + // /// 7. vertiports are available but aircraft are not at the vertiport for the requested time + // /// but at least one aircraft is PARKED at other vertiport for the "requested time - N minutes" + // #[tokio::test] + // async fn test_query_flight_7_deadhead_flight_of_parked_vehicle() { + // init_logger(&Config::try_from_env().unwrap_or_default()); + // unit_test_info!("(test_query_flight_7_deadhead_flight_of_parked_vehicle) start"); + // ensure_storage_mock_data().await; + + // let vertiports = get_vertiports_from_storage().await; + // let res = query_flight(Request::new(QueryFlightRequest { + // is_cargo: false, + // persons: None, + // weight_grams: None, + // earliest_departure_time: Some( + // Utc.datetime_from_str("2022-10-26 16:00:00", "%Y-%m-%d %H:%M:%S") + // .unwrap() + // .into(), + // ), + // latest_arrival_time: Some( + // Utc.datetime_from_str("2022-10-26 16:30:00", "%Y-%m-%d %H:%M:%S") + // .unwrap() + // .into(), + // ), + // vertiport_depart_id: vertiports[2].id.clone(), + // vertiport_arrive_id: vertiports[0].id.clone(), + // })) + // .await + // .unwrap() + // .into_inner(); + + // unit_test_debug!( + // "(test_query_flight_7_deadhead_flight_of_parked_vehicle) query_flight result: {:#?}", + // res + // ); + // assert_eq!(res.itineraries.len(), 1); + // assert_eq!(res.itineraries[0].flight_plans.len(), 2); + // unit_test_info!("(test_query_flight_7_deadhead_flight_of_parked_vehicle) success"); + // } + + // /// 8. vertiports are available but aircraft are not at the vertiport for the requested time + // /// but at least one aircraft is EN ROUTE to another vertiport for the "requested time - N minutes - M minutes" + // #[tokio::test] + // async fn test_query_flight_8_deadhead_flight_of_in_flight_vehicle() { + // init_logger(&Config::try_from_env().unwrap_or_default()); + // unit_test_info!("(test_query_flight_8_deadhead_flight_of_in_flight_vehicle) start"); + // ensure_storage_mock_data().await; + + // let vertiports = get_vertiports_from_storage().await; + // let res = query_flight(Request::new(QueryFlightRequest { + // is_cargo: false, + // persons: None, + // weight_grams: None, + // earliest_departure_time: Some( + // Utc.datetime_from_str("2022-10-27 12:30:00", "%Y-%m-%d %H:%M:%S") + // .unwrap() + // .into(), + // ), + // latest_arrival_time: Some( + // Utc.datetime_from_str("2022-10-27 13:30:00", "%Y-%m-%d %H:%M:%S") + // .unwrap() + // .into(), + // ), + // vertiport_depart_id: vertiports[1].id.clone(), + // vertiport_arrive_id: vertiports[0].id.clone(), + // })) + // .await + // .unwrap() + // .into_inner(); + + // unit_test_debug!( + // "(test_query_flight_8_deadhead_flight_of_in_flight_vehicle) query_flight result: {:#?}", + // res + // ); + // assert_eq!(res.itineraries.len(), 2); + // assert_eq!(res.itineraries[0].flight_plans.len(), 2); + // unit_test_info!("(test_query_flight_8_deadhead_flight_of_in_flight_vehicle) success"); + // } + + /* TODO: R4 refactor code and re-implement this test + /// 9. destination vertiport is not available because of capacity + /// - if at requested time all pads are occupied and at least one is parked (not loading/unloading), + /// a extra flight plan should be created to move idle aircraft to the nearest unoccupied vertiport + /// (or to preferred vertiport in hub and spoke model). + #[tokio::test] + async fn test_query_flight_9_deadhead_destination_flight_no_capacity_at_destination_vertiport() + { + init_logger(&Config::try_from_env().unwrap_or_default()); + unit_test_info!("(test_query_flight_9_deadhead_destination_flight_no_capacity_at_destination_vertiport) start"); + ensure_storage_mock_data().await; + init_router().await; + + let vertiports = get_vertiports_from_storage().await; + let res = query_flight(Request::new(QueryFlightRequest { + is_cargo: false, + persons: None, + weight_grams: None, + earliest_departure_time: Some( + Utc.datetime_from_str("2022-10-27 15:10:00", "%Y-%m-%d %H:%M:%S") + .unwrap() + .into(), + ), + latest_arrival_time: Some( + Utc.datetime_from_str("2022-10-27 16:00:00", "%Y-%m-%d %H:%M:%S") + .unwrap() + .into(), + ), + vertiport_depart_id: vertiports[1].id.clone(), + vertiport_arrive_id: vertiports[3].id.clone(), + })) + .await + .unwrap() + .into_inner(); + + unit_test_debug!( + "(test_query_flight_9_deadhead_destination_flight_no_capacity_at_destination_vertiport) query_flight result: {:#?}", + res + ); + assert_eq!(res.itineraries.len(), 1); + assert_eq!(res.itineraries[0].deadhead_flight_plans.len(), 1); + unit_test_info!("(test_query_flight_9_deadhead_destination_flight_no_capacity_at_destination_vertiport) success"); + } + */ +} diff --git a/server/src/grpc/client.rs b/server/src/grpc/client.rs index 56392a8..4ae87c1 100644 --- a/server/src/grpc/client.rs +++ b/server/src/grpc/client.rs @@ -55,10 +55,8 @@ impl GrpcClients { #[cfg(test)] mod tests { - use svc_compliance_client_grpc::prelude::ComplianceClient; - use svc_gis_client_grpc::Client as GisServiceClient; - use super::*; + use svc_gis_client_grpc::prelude::*; #[tokio::test] async fn test_grpc_clients_default() { diff --git a/server/src/grpc/mod.rs b/server/src/grpc/mod.rs index b7381fb..e860a0e 100644 --- a/server/src/grpc/mod.rs +++ b/server/src/grpc/mod.rs @@ -3,6 +3,6 @@ #[macro_use] pub mod macros; +pub mod api; pub mod client; -pub mod queries; pub mod server; diff --git a/server/src/grpc/queries.rs b/server/src/grpc/queries.rs deleted file mode 100644 index 650ea90..0000000 --- a/server/src/grpc/queries.rs +++ /dev/null @@ -1,1071 +0,0 @@ -//! Implementation of the queries/actions that the scheduler service can perform. - -use crate::grpc::server::grpc_server::{ - CancelItineraryResponse, ConfirmItineraryRequest, ConfirmItineraryResponse, Id, Itinerary, - QueryFlightRequest, QueryFlightResponse, -}; -use crate::router::router_utils::router_state::get_possible_flights; -use crate::router::router_utils::router_state::{ - init_router_from_vertiports, is_router_initialized, -}; - -use crate::grpc::client::get_clients; -use svc_compliance_client_grpc::client::FlightPlanRequest; -use svc_compliance_client_grpc::service::Client as ComplianceServiceClient; -use svc_storage_client_grpc::prelude::{Id as StorageId, *}; - -use lazy_static::lazy_static; -use std::collections::HashMap; -use std::sync::Mutex; -use std::time::SystemTime; -use tonic::{Request, Response, Status}; -use uuid::Uuid; - -const ITINERARY_EXPIRATION_S: u64 = 30; - -// Some itineraries might be intermixed with flight plans -// that already exist (rideshare) -#[derive(PartialEq, Clone)] -enum FlightPlanType { - Draft, - Existing, -} - -lazy_static! { - static ref UNCONFIRMED_FLIGHT_PLANS: Mutex> = - Mutex::new(HashMap::new()); - static ref UNCONFIRMED_ITINERARIES: Mutex>> = - Mutex::new(HashMap::new()); -} -/// gets unconfirmed flight plans -fn unconfirmed_flight_plans() -> &'static Mutex> { - &UNCONFIRMED_FLIGHT_PLANS -} -/// gets a hashmap of unconfirmed itineraries -/// "itineraries" are a list of flight plan IDs, which can represent DRAFT -/// (in memory) or EXISTING (in database) flight plans -fn unconfirmed_itineraries() -> &'static Mutex>> { - &UNCONFIRMED_ITINERARIES -} - -#[derive(Clone)] -struct ItineraryFlightPlan { - fp_type: FlightPlanType, - fp_id: String, -} - -//------------------------------------------------------------------- -// Helper Functions -//------------------------------------------------------------------- - -fn fp_from_storage(fp_id: String, fp: flight_plan::Data) -> Result { - if fp.departure_vertiport_id.is_none() { - grpc_error!( - "(fp_from_storage) flight plan {} has no departure vertiport. Should not be possible.", - fp_id - ); - return Err(()); - } - - if fp.destination_vertiport_id.is_none() { - grpc_error!( - "(fp_from_storage) flight plan {} has no destination vertiport", - fp_id - ); - return Err(()); - } - - Ok(flight_plan::Object { - id: fp_id, - data: Some(fp), - }) -} - -/// spawns a thread that will cancel the itinerary after a certain amount of time (ITINERARY_EXPIRATION_S) -fn cancel_itinerary_after_timeout(id: String) { - tokio::spawn(async move { - tokio::time::sleep(core::time::Duration::from_secs(ITINERARY_EXPIRATION_S)).await; - remove_draft_itinerary_by_id(&id); - grpc_debug!("Flight plan {} was not confirmed in time, cancelling", id); - }); -} - -/// Gets itinerary from hash map of unconfirmed itineraries -fn get_draft_itinerary_by_id(id: &str) -> Option> { - unconfirmed_itineraries() - .lock() - .expect("Mutex Lock Error getting itinerary from temp storage") - .get(id) - .cloned() -} - -/// Gets flight plan from hash map of unconfirmed flight plans -fn get_draft_fp_by_id(id: &str) -> Option { - unconfirmed_flight_plans() - .lock() - .expect("Mutex Lock Error getting flight plan from temp storage") - .get(id) - .cloned() -} - -/// Removes flight plan from hash map of unconfirmed flight plans -fn remove_draft_fp_by_id(id: &str) -> bool { - let mut flight_plans = unconfirmed_flight_plans() - .lock() - .expect("(remove_draft_fp_by_id) mutex Lock Error removing flight plan from temp storage"); - - match flight_plans.remove(id) { - Some(_) => { - grpc_debug!( - "(remove_draft_fp_by_id) with id {} removed from local cache", - &id - ); - true - } - _ => { - grpc_debug!( - "(remove_draft_fp_by_id) no such flight plan with ID {} in cache", - &id - ); - false - } - } -} - -/// Removes itinerary from hash map of unconfirmed flight plans -fn remove_draft_itinerary_by_id(id: &str) -> bool { - let mut itineraries = unconfirmed_itineraries().lock().expect( - "(remove_draft_itinerary_by_id) mutex Lock Error removing itinerary from temp storage", - ); - - let Some(itinerary) = itineraries.get(id) else { - grpc_debug!("(remove_draft_itinerary_by_id) no such itinerary with ID {} in cache", &id); - return false; - }; - - // Remove draft flight plans associated with this itinerary - for fp in itinerary { - if fp.fp_type == FlightPlanType::Draft { - // Ignore if not found - let _ = remove_draft_fp_by_id(&fp.fp_id); - } - } - - itineraries.remove(id); - - grpc_info!("cancel_itinerary with id {} removed from local cache", &id); - true -} - -/// Confirms a flight plan by registering it with svc-storage -/// After confirmation, the flight plan will be removed from local cache -async fn confirm_draft_flight_plan(flight_plan_id: String) -> Result { - let clients = get_clients().await; - - let Some(flight_plan) = get_draft_fp_by_id(&flight_plan_id) else { - return Err(Status::internal("Draft flight plan ID doesn't exist.")); - }; - - // - // Confirm a flight plan with the database - // - let Some(fp) = clients.storage.flight_plan - .insert(flight_plan) - .await? - .into_inner() - .object - else { - return Err(Status::internal("Failed to add a flight plan to the database.")); - }; - - grpc_info!( - "(confirm_draft_flight_plan) flight plan with draft id: {} - inserted into storage with permanent id: {}", - &flight_plan_id, - &fp.id - ); - - // - // Remove the flight plan from the cache now that it's in the database - // - let _ = remove_draft_fp_by_id(&flight_plan_id); - - // TODO(R3) - // Retrieve user data in case that's relevant to compliance - let compliance_res = clients - .compliance - .submit_flight_plan(FlightPlanRequest { - flight_plan_id: fp.id.clone(), - data: "".to_string(), - }) - .await? - .into_inner(); - grpc_info!( - "(confirm_draft_flight_plan) Compliance response for flight plan id : {} is submitted: {}", - &fp.id, - compliance_res.submitted - ); - - Ok(fp) -} - -/// Registers an itinerary with svc-storage. -/// -/// There's two steps involved with registering an itinerary: -/// 1) Register a new itinerary with the `itinerary` table in the database -/// 2) Link flight plan IDs to the itinerary (a separate `itinerary_flight_plan` table) -async fn create_itinerary( - draft_itinerary_id: String, - user_id: String, - confirmed_flight_plan_ids: Vec, -) -> Result { - let clients = get_clients().await; - // - // 1) Add itinerary to `itinerary` DB table - // - let data = itinerary::Data { - user_id: user_id.to_string(), - status: itinerary::ItineraryStatus::Active as i32, - }; - - let Some(db_itinerary) = clients.storage.itinerary - .insert(data) - .await? - .into_inner() - .object - else { - return Err(Status::internal("Couldn't add itinerary to storage.")); - }; - - // - // 2) Link flight plans to itinerary in `itinerary_flight_plan` - // - clients - .storage - .itinerary_flight_plan_link - .link(itinerary::ItineraryFlightPlans { - id: db_itinerary.id.clone(), - other_id_list: Some(IdList { - ids: confirmed_flight_plan_ids, - }), - }) - .await?; - - // At this point all draft flight plans have been confirmed and can be - // removed from local memory along with the draft itinerary - let _ = remove_draft_itinerary_by_id(&draft_itinerary_id); - Ok(db_itinerary) -} - -//------------------------------------------------------------------- -// API Functions -//------------------------------------------------------------------- - -/// Initializes the router from vertiports in the database. -/// TODO(R3): The routing may be moved to a SQL Graph Database. -/// This function will be removed in that case. -pub async fn init_router() { - let clients = get_clients().await; - let result = clients - .storage - .vertiport - .search(AdvancedSearchFilter { - filters: vec![], - page_number: 0, - results_per_page: 50, - order_by: vec![], - }) - .await; - - let vertiports = match result { - Ok(vertiports) => vertiports, - Err(e) => { - let error_msg = format!("Failed to get vertiports from storage service: {}", e); - grpc_debug!("{}", error_msg); - let result = clients - .storage - .vertiport - .search(AdvancedSearchFilter { - filters: vec![], - page_number: 0, - results_per_page: 50, - order_by: vec![], - }) - .await; - let Ok(vertiports) = result else { - panic!("Could not get vertiports, retry failed.") - }; - vertiports - } //panic!("{}", error_msg); - }; - - let vertiports = vertiports.into_inner().list; - grpc_info!("Initializing router with {} vertiports ", vertiports.len()); - if !is_router_initialized() { - let res = init_router_from_vertiports(&vertiports).await; - if let Err(res) = res { - grpc_error!("Failed to initialize router: {}", res); - } - } -} - -/// Finds the first possible flight for customer location, flight type and requested time. -pub async fn query_flight( - request: Request, -) -> Result, Status> { - let clients = get_clients().await; - let flight_request = request.into_inner(); - // 1. get vertiports - grpc_info!( - "(query_flight) with vertiport depart, arrive ids: {}, {}", - &flight_request.vertiport_depart_id, - &flight_request.vertiport_arrive_id - ); - let depart_vertiport = clients - .storage - .vertiport - .get_by_id(StorageId { - id: flight_request.vertiport_depart_id.clone(), - }) - .await? - .into_inner(); - let arrive_vertiport = clients - .storage - .vertiport - .get_by_id(StorageId { - id: flight_request.vertiport_arrive_id.clone(), - }) - .await? - .into_inner(); - grpc_debug!( - "(query_flight) depart_vertiport: {:?}, arrive_vertiport: {:?}", - &depart_vertiport, - &arrive_vertiport - ); - let depart_vertipads = clients - .storage - .vertipad - .search( - AdvancedSearchFilter::search_equals( - "vertiport_id".to_owned(), - flight_request.vertiport_depart_id.clone(), - ) - .and_is_null("deleted_at".to_owned()), - ) - .await? - .into_inner() - .list; - grpc_debug!("(query_flight) depart_vertipads: {:?}", depart_vertipads); - - let arrive_vertipads = clients - .storage - .vertipad - .search( - AdvancedSearchFilter::search_equals( - "vertiport_id".to_owned(), - flight_request.vertiport_arrive_id.clone(), - ) - .and_is_null("deleted_at".to_owned()), - ) - .await? - .into_inner() - .list; - grpc_debug!("(query_flight) arrive_vertipads: {:?}", arrive_vertipads); - - //2. get all aircraft - let aircraft = clients - .storage - .vehicle - .search(AdvancedSearchFilter { - filters: vec![], - page_number: 0, - results_per_page: 0, - order_by: vec![], - }) - .await? - .into_inner() - .list; - - grpc_debug!("(query_flight) found vehicles: {:?}", aircraft); - - //3. get all flight plans from this time to latest departure time (including partially fitting flight plans) - //- this assumes that all landed flights have updated vehicle.last_vertiport_id (otherwise we would need to look in to the past) - //Plans are used by lib_router to find aircraft and vertiport availability and aircraft predicted location - let Some(latest_arrival_time) = flight_request.latest_arrival_time.clone() else { - grpc_warn!("(query_flight) latest arrival time not provided."); - return Err(Status::invalid_argument("Routing failed; latest arrival time not provided.")); - }; - let existing_flight_plans = query_flight_plans_for_latest_arrival(latest_arrival_time).await?; - grpc_debug!( - "(query_flight) found existing flight plans: {:?}", - existing_flight_plans - ); - - //4. get all possible flight plans from router - let Ok(flight_plans) = get_possible_flights( - depart_vertiport, - arrive_vertiport, - depart_vertipads, - arrive_vertipads, - flight_request.earliest_departure_time, - flight_request.latest_arrival_time, - aircraft, - existing_flight_plans, - clients - ).await else { - let error = String::from("Routing failed; No flight plans available."); - grpc_error!("(query_flight) {}", error); - return Err(Status::internal(error)); - }; - - if flight_plans.is_empty() { - let error = String::from("No flight plans available."); - grpc_info!("(query_flight) {}", error); - return Err(Status::not_found(error)); - } - - grpc_info!( - "(query_flight) Found {} flight plans from router", - &flight_plans.len() - ); - - //5. create draft itinerary and flight plans (in memory) - let mut itineraries: Vec = vec![]; - for (fp, deadhead_fps) in &flight_plans { - let fp_id = Uuid::new_v4().to_string(); - let Ok(item) = fp_from_storage(fp_id.clone(), fp.clone()) else { - grpc_warn!("(query_flight) invalid flight plan ({:?}), skipping.", fp_id); - continue; - }; - - grpc_info!( - "(query_flight) Adding draft flight plan with temporary id: {}", - &fp_id - ); - unconfirmed_flight_plans() - .lock() - .expect("Mutex Lock Error inserting flight plan into temp storage") - .insert(fp_id.clone(), fp.clone()); - - let deadhead_flight_plans: Vec = deadhead_fps - .iter() - .filter_map(|fp| fp_from_storage(Uuid::new_v4().to_string(), fp.clone()).ok()) - .collect(); - grpc_debug!("(query_flight) flight plan: {:?}", &item); - - // - // Create Itinerary - // - // TODO(R3) account for existing flight plans combined with draft flight plans - let mut total_fps = vec![]; - total_fps.push(ItineraryFlightPlan { - fp_type: FlightPlanType::Draft, - fp_id: item.id.clone(), - }); - - for deadhead_flight_plan in &deadhead_flight_plans { - total_fps.push(ItineraryFlightPlan { - fp_type: FlightPlanType::Draft, - fp_id: deadhead_flight_plan.id.clone(), - }); - } - - let itinerary_id = Uuid::new_v4().to_string(); - unconfirmed_itineraries() - .lock() - .expect("Mutex Lock Error inserting flight plan into temp storage") - .insert(itinerary_id.clone(), total_fps); - cancel_itinerary_after_timeout(itinerary_id.clone()); - - itineraries.push(Itinerary { - id: itinerary_id, - flight_plan: Some(item), - deadhead_flight_plans, - }); - } - - //6. return response - let response = QueryFlightResponse { itineraries }; - grpc_info!( - "(query_flight) query_flight returning: {} flight plans", - &response.itineraries.len() - ); - Ok(Response::new(response)) -} - -/// Get all flight plans from current time to latest departure time (including partially fitting flight plans) -pub async fn query_flight_plans_for_latest_arrival( - latest_arrival_time: Timestamp, -) -> Result, Status> { - let clients = get_clients().await; - - Ok(clients - .storage - .flight_plan - .search( - AdvancedSearchFilter::search_less_or_equal( - "scheduled_arrival".to_owned(), - latest_arrival_time.to_string(), - ) - .and_is_null("deleted_at".to_owned()) - .and_not_in( - "flight_status".to_owned(), - vec![ - (flight_plan::FlightStatus::Finished as i32).to_string(), - (flight_plan::FlightStatus::Cancelled as i32).to_string(), - ], - ), - ) - .await? - .into_inner() - .list) -} - -/// Confirms an itinerary -pub async fn confirm_itinerary( - request: Request, -) -> Result, Status> { - // - // Return if the itinerary has expired in cache - // - let request = request.into_inner(); - let draft_itinerary_id = request.id; - grpc_info!("(confirm_itinerary) with id {}", &draft_itinerary_id); - - let Some(draft_itinerary_flights) = get_draft_itinerary_by_id(&draft_itinerary_id) else { - return Err(Status::not_found("Itinerary ID not found or timed out.")); - }; - - // - // For each Draft flight in the itinerary, push to svc-storage - // - let mut confirmed_flight_plan_ids: Vec = vec![]; - for fp in draft_itinerary_flights { - if fp.fp_type == FlightPlanType::Existing { - // TODO(R3) update svc-storage flight plan with new passenger count - // let data = flight_plan::UpdateObject { ... } - - confirmed_flight_plan_ids.push(fp.fp_id); - continue; - } - - let confirmation = confirm_draft_flight_plan(fp.fp_id).await; - - let Ok(confirmed_fp) = confirmation else { - break; - }; - - confirmed_flight_plan_ids.push(confirmed_fp.id); - } - - // - // Create and insert itinerary - // - let result = create_itinerary( - draft_itinerary_id, - request.user_id, - confirmed_flight_plan_ids, - ) - .await; - - match result { - Ok(itinerary) => { - let response = ConfirmItineraryResponse { - id: itinerary.id, - confirmed: true, - confirmation_time: Some(Timestamp::from(SystemTime::now())), - }; - - Ok(Response::new(response)) - } - Err(e) => Err(e), - } -} - -/// Cancels a draft or confirmed flight plan -pub async fn cancel_itinerary( - request: Request, -) -> Result, Status> { - let clients = get_clients().await; - let itinerary_id = request.into_inner().id; - grpc_info!("(cancel_itinerary) for id {}.", &itinerary_id); - - // - // Look within unconfirmed itineraries - // - if remove_draft_itinerary_by_id(&itinerary_id) { - let sys_time = SystemTime::now(); - let response = CancelItineraryResponse { - id: itinerary_id, - cancelled: true, - cancellation_time: Some(Timestamp::from(sys_time)), - reason: "user cancelled".into(), - }; - return Ok(Response::new(response)); - } - - // - // Look within confirmed itineraries - // - clients - .storage - .itinerary - .get_by_id(StorageId { - id: itinerary_id.clone(), - }) - .await?; - - // - // TODO(R3) Don't allow cancellations within X minutes of the first flight - // - - // - // Remove itinerary - // - let update_object = itinerary::UpdateObject { - id: itinerary_id.clone(), - data: Option::from(itinerary::Data { - user_id: "".to_string(), // will be masked - status: itinerary::ItineraryStatus::Cancelled as i32, - }), - mask: Some(FieldMask { - paths: vec!["status".to_string()], - }), - }; - clients.storage.itinerary.update(update_object).await?; - - grpc_info!( - "(cancel_itinerary) cancel_itinerary with id {} cancelled in storage.", - &itinerary_id - ); - - let response = clients - .storage - .itinerary_flight_plan_link - .get_linked_ids(StorageId { - id: itinerary_id.clone(), - }) - .await?; - - // - // Cancel associated flight plans - // - //TODO: svc-storage currently doesn't check the FieldMask, so we'll - //have to provide it with the right data object for now. Will now be handled - //with temp code in for loop, but should be: - //let mut flight_plan_data = flight_plan::Data::default(); - //flight_plan_data.flight_status = flight_plan::FlightStatus::Cancelled as i32; - for id in response.into_inner().ids { - // begin temp code - let flight_plan = clients - .storage - .flight_plan - .get_by_id(StorageId { id: id.clone() }) - .await?; - - let Ok(mut flight_plan_data) = flight_plan.into_inner().data else { - grpc_warn!("(cancel_itinerary) WARNING: Could not cancel flight plan with ID: {}", id); - continue; - }; - - flight_plan_data.flight_status = flight_plan::FlightStatus::Cancelled as i32; - // end temp code - - // - // TODO(R4) Don't cancel flight plan if it exists in another itinerary - // - let request = flight_plan::UpdateObject { - id: id.clone(), - data: Some(flight_plan_data.clone()), - mask: Some(FieldMask { - paths: vec!["flight_status".to_string()], - }), - }; - let result = clients.storage.flight_plan.update(request).await; - - // Keep going even if there's a warning - if result.is_err() { - grpc_warn!("(cancel_itinerary) WARNING: Could not cancel flight plan with ID: {}", id); - } - } - - // - // Reply - // - let sys_time = SystemTime::now(); - let response = CancelItineraryResponse { - id: itinerary_id, - cancelled: true, - cancellation_time: Some(Timestamp::from(sys_time)), - reason: "user cancelled".into(), - }; - Ok(Response::new(response)) -} - -#[cfg(test)] -mod tests { - use crate::test_util::{ensure_storage_mock_data, get_vertiports_from_storage}; - use crate::{init_logger, Config}; - - use super::*; - use chrono::{TimeZone, Utc}; - - #[tokio::test] - async fn test_query_flight_plans_for_latest_arrival() { - init_logger(&Config::try_from_env().unwrap_or_default()); - unit_test_info!("(test_query_flight_plans_for_latest_arrival) start"); - ensure_storage_mock_data().await; - init_router().await; - - let latest_arrival_time: Timestamp = Utc - .datetime_from_str("2022-10-26 14:30:00", "%Y-%m-%d %H:%M:%S") - .unwrap() - .into(); - // our mock setup inserts only 3 flight_plans with an arrival date before "2022-10-26 14:30:00" - let expected_number_returned = 3; - - let res = query_flight_plans_for_latest_arrival(latest_arrival_time).await; - unit_test_debug!( - "(test_query_flight_plans_for_latest_arrival) flight_plans returned: {:#?}", - res - ); - - assert!(res.is_ok()); - assert_eq!(res.unwrap().len(), expected_number_returned); - unit_test_info!("(test_query_flight_plans_for_latest_arrival) success"); - } - - #[tokio::test] - async fn test_query_flight() { - init_logger(&Config::try_from_env().unwrap_or_default()); - unit_test_info!("(test_query_flight) start"); - ensure_storage_mock_data().await; - init_router().await; - - let vertiports = get_vertiports_from_storage().await; - let res = query_flight(Request::new(QueryFlightRequest { - is_cargo: false, - persons: None, - weight_grams: None, - earliest_departure_time: Some( - Utc.datetime_from_str("2022-10-25 11:20:00", "%Y-%m-%d %H:%M:%S") - .unwrap() - .into(), - ), - latest_arrival_time: Some( - Utc.datetime_from_str("2022-10-25 12:15:00", "%Y-%m-%d %H:%M:%S") - .unwrap() - .into(), - ), - vertiport_depart_id: vertiports[0].id.clone(), - vertiport_arrive_id: vertiports[1].id.clone(), - })) - .await; - unit_test_debug!("(test_query_flight) query_flight result: {:?}", res); - assert!(res.is_ok()); - assert_eq!(res.unwrap().into_inner().itineraries.len(), 5); - unit_test_info!("(test_query_flight) success"); - } - - #[tokio::test] - async fn test_confirm_and_cancel_itinerary() { - init_logger(&Config::try_from_env().unwrap_or_default()); - unit_test_info!("(test_confirm_and_cancel_itinerary) start"); - ensure_storage_mock_data().await; - init_router().await; - let res = confirm_itinerary(Request::new(ConfirmItineraryRequest { - id: "itinerary1".to_string(), - user_id: "".to_string(), - })) - .await; - //test confirming a flight that does not exist will return an error - assert_eq!(res.is_err(), true); - - let vertiports = get_vertiports_from_storage().await; - let res = query_flight(Request::new(QueryFlightRequest { - is_cargo: false, - persons: None, - weight_grams: None, - earliest_departure_time: Some( - Utc.datetime_from_str("2022-10-25 11:20:00", "%Y-%m-%d %H:%M:%S") - .unwrap() - .into(), - ), - latest_arrival_time: Some( - Utc.datetime_from_str("2022-10-25 12:15:00", "%Y-%m-%d %H:%M:%S") - .unwrap() - .into(), - ), - vertiport_depart_id: vertiports[0].id.clone(), - vertiport_arrive_id: vertiports[1].id.clone(), - })) - .await; - unit_test_debug!( - "(test_confirm_and_cancel_itinerary) query_flight result: {:#?}", - res - ); - assert!(res.is_ok()); - let id = res.unwrap().into_inner().itineraries[0].id.clone(); - let res = confirm_itinerary(Request::new(ConfirmItineraryRequest { - id, - user_id: "".to_string(), - })) - .await; - assert!(res.is_ok()); - let confirm_response: ConfirmItineraryResponse = res.unwrap().into_inner(); - //test confirming a flight that does exist will return a success - assert_eq!(confirm_response.confirmed, true); - - let id = confirm_response.id.clone(); - let res = cancel_itinerary(Request::new(Id { id })).await; - assert!(res.is_ok()); - assert_eq!(res.unwrap().into_inner().cancelled, true); - - unit_test_info!("(test_confirm_and_cancel_itinerary) success"); - } - - ///4. destination vertiport is available for about 15 minutes, no other restrictions - /// - returns 2 flights (assuming 10 minutes needed for unloading, this can fit 2 flights - /// if first is exactly at the beginning of 15 minute gap and second is exactly after 5 minutes) - #[tokio::test] - async fn test_query_flight_4_dest_vertiport_tight_availability_should_return_two_flights() { - init_logger(&Config::try_from_env().unwrap_or_default()); - unit_test_info!("(test_query_flight_4_dest_vertiport_tight_availability_should_return_two_flights) start"); - ensure_storage_mock_data().await; - init_router().await; - - let vertiports = get_vertiports_from_storage().await; - let res = query_flight(Request::new(QueryFlightRequest { - is_cargo: false, - persons: None, - weight_grams: None, - earliest_departure_time: Some( - Utc.datetime_from_str("2022-10-25 14:20:00", "%Y-%m-%d %H:%M:%S") - .unwrap() - .into(), - ), - latest_arrival_time: Some( - Utc.datetime_from_str("2022-10-25 15:10:00", "%Y-%m-%d %H:%M:%S") - .unwrap() - .into(), - ), - vertiport_depart_id: vertiports[2].id.clone(), - vertiport_arrive_id: vertiports[1].id.clone(), - })) - .await - .unwrap(); - - unit_test_debug!("(test_query_flight_4_dest_vertiport_tight_availability_should_return_two_flights) query_flight result: {:#?}", res); - assert_eq!(res.into_inner().itineraries.len(), 2); - unit_test_info!("(test_query_flight_4_dest_vertiport_tight_availability_should_return_two_flights) success"); - } - - ///5. source or destination vertiport doesn't have any vertipad free for the time range - ///no flight plans returned - #[tokio::test] - async fn test_query_flight_5_dest_vertiport_no_availability_should_return_zero_flights() { - init_logger(&Config::try_from_env().unwrap_or_default()); - unit_test_info!( - "(test_query_flight_5_dest_vertiport_no_availability_should_return_zero_flights) start" - ); - ensure_storage_mock_data().await; - init_router().await; - - let vertiports = get_vertiports_from_storage().await; - let res = query_flight(Request::new(QueryFlightRequest { - is_cargo: false, - persons: None, - weight_grams: None, - earliest_departure_time: Some( - Utc.datetime_from_str("2022-10-26 14:00:00", "%Y-%m-%d %H:%M:%S") - .unwrap() - .into(), - ), - latest_arrival_time: Some( - Utc.datetime_from_str("2022-10-26 14:40:00", "%Y-%m-%d %H:%M:%S") - .unwrap() - .into(), - ), - vertiport_depart_id: vertiports[1].id.clone(), - vertiport_arrive_id: vertiports[0].id.clone(), - })) - .await; - - unit_test_debug!("(test_query_flight_5_dest_vertiport_no_availability_should_return_zero_flights) query_flight result: {:#?}", res); - assert_eq!( - res.unwrap_err() - .message() - .contains("No flight plans available"), - true - ); - unit_test_info!( - "(test_query_flight_5_dest_vertiport_no_availability_should_return_zero_flights) success" - ); - } - - ///6. vertiports are available but aircraft are not at the vertiport for the requested time - /// but at least one aircraft is IN FLIGHT to requested vertiport for that time and has availability for a next flight. - /// - skips all unavailable time slots (4) and returns only time slots from when aircraft is available (1) - #[tokio::test] - async fn test_query_flight_6_no_aircraft_at_vertiport() { - init_logger(&Config::try_from_env().unwrap_or_default()); - unit_test_info!("(test_query_flight_6_no_aircraft_at_vertiport) start"); - ensure_storage_mock_data().await; - init_router().await; - - let vertiports = get_vertiports_from_storage().await; - let res = query_flight(Request::new(QueryFlightRequest { - is_cargo: false, - persons: None, - weight_grams: None, - earliest_departure_time: Some( - Utc.datetime_from_str("2022-10-26 14:15:00", "%Y-%m-%d %H:%M:%S") - .unwrap() - .into(), - ), - latest_arrival_time: Some( - Utc.datetime_from_str("2022-10-26 15:00:00", "%Y-%m-%d %H:%M:%S") - .unwrap() - .into(), - ), - vertiport_depart_id: vertiports[0].id.clone(), - vertiport_arrive_id: vertiports[2].id.clone(), - })) - .await - .unwrap() - .into_inner(); - - unit_test_debug!( - "(test_query_flight_6_no_aircraft_at_vertiport) query_flight result: {:#?}", - res - ); - assert_eq!(res.itineraries.len(), 1); - assert_eq!(res.itineraries[0].deadhead_flight_plans.len(), 0); - unit_test_info!("(test_query_flight_6_no_aircraft_at_vertiport) success"); - } - - /// 7. vertiports are available but aircraft are not at the vertiport for the requested time - /// but at least one aircraft is PARKED at other vertiport for the "requested time - N minutes" - #[tokio::test] - async fn test_query_flight_7_deadhead_flight_of_parked_vehicle() { - init_logger(&Config::try_from_env().unwrap_or_default()); - unit_test_info!("(test_query_flight_7_deadhead_flight_of_parked_vehicle) start"); - ensure_storage_mock_data().await; - init_router().await; - - let vertiports = get_vertiports_from_storage().await; - let res = query_flight(Request::new(QueryFlightRequest { - is_cargo: false, - persons: None, - weight_grams: None, - earliest_departure_time: Some( - Utc.datetime_from_str("2022-10-26 16:00:00", "%Y-%m-%d %H:%M:%S") - .unwrap() - .into(), - ), - latest_arrival_time: Some( - Utc.datetime_from_str("2022-10-26 16:30:00", "%Y-%m-%d %H:%M:%S") - .unwrap() - .into(), - ), - vertiport_depart_id: vertiports[2].id.clone(), - vertiport_arrive_id: vertiports[0].id.clone(), - })) - .await - .unwrap() - .into_inner(); - - unit_test_debug!( - "(test_query_flight_7_deadhead_flight_of_parked_vehicle) query_flight result: {:#?}", - res - ); - assert_eq!(res.itineraries.len(), 1); - assert_eq!(res.itineraries[0].deadhead_flight_plans.len(), 1); - unit_test_info!("(test_query_flight_7_deadhead_flight_of_parked_vehicle) success"); - } - - /// 8. vertiports are available but aircraft are not at the vertiport for the requested time - /// but at least one aircraft is EN ROUTE to another vertiport for the "requested time - N minutes - M minutes" - #[tokio::test] - async fn test_query_flight_8_deadhead_flight_of_in_flight_vehicle() { - init_logger(&Config::try_from_env().unwrap_or_default()); - unit_test_info!("(test_query_flight_8_deadhead_flight_of_in_flight_vehicle) start"); - ensure_storage_mock_data().await; - init_router().await; - - let vertiports = get_vertiports_from_storage().await; - let res = query_flight(Request::new(QueryFlightRequest { - is_cargo: false, - persons: None, - weight_grams: None, - earliest_departure_time: Some( - Utc.datetime_from_str("2022-10-27 12:30:00", "%Y-%m-%d %H:%M:%S") - .unwrap() - .into(), - ), - latest_arrival_time: Some( - Utc.datetime_from_str("2022-10-27 13:30:00", "%Y-%m-%d %H:%M:%S") - .unwrap() - .into(), - ), - vertiport_depart_id: vertiports[1].id.clone(), - vertiport_arrive_id: vertiports[0].id.clone(), - })) - .await - .unwrap() - .into_inner(); - - unit_test_debug!( - "(test_query_flight_8_deadhead_flight_of_in_flight_vehicle) query_flight result: {:#?}", - res - ); - assert_eq!(res.itineraries.len(), 2); - assert_eq!(res.itineraries[0].deadhead_flight_plans.len(), 1); - unit_test_info!("(test_query_flight_8_deadhead_flight_of_in_flight_vehicle) success"); - } - - /* TODO: R4 refactor code and re-implement this test - /// 9. destination vertiport is not available because of capacity - /// - if at requested time all pads are occupied and at least one is parked (not loading/unloading), - /// a extra flight plan should be created to move idle aircraft to the nearest unoccupied vertiport - /// (or to preferred vertiport in hub and spoke model). - #[tokio::test] - async fn test_query_flight_9_deadhead_destination_flight_no_capacity_at_destination_vertiport() - { - init_logger(&Config::try_from_env().unwrap_or_default()); - unit_test_info!("(test_query_flight_9_deadhead_destination_flight_no_capacity_at_destination_vertiport) start"); - ensure_storage_mock_data().await; - init_router().await; - - let vertiports = get_vertiports_from_storage().await; - let res = query_flight(Request::new(QueryFlightRequest { - is_cargo: false, - persons: None, - weight_grams: None, - earliest_departure_time: Some( - Utc.datetime_from_str("2022-10-27 15:10:00", "%Y-%m-%d %H:%M:%S") - .unwrap() - .into(), - ), - latest_arrival_time: Some( - Utc.datetime_from_str("2022-10-27 16:00:00", "%Y-%m-%d %H:%M:%S") - .unwrap() - .into(), - ), - vertiport_depart_id: vertiports[1].id.clone(), - vertiport_arrive_id: vertiports[3].id.clone(), - })) - .await - .unwrap() - .into_inner(); - - unit_test_debug!( - "(test_query_flight_9_deadhead_destination_flight_no_capacity_at_destination_vertiport) query_flight result: {:#?}", - res - ); - assert_eq!(res.itineraries.len(), 1); - assert_eq!(res.itineraries[0].deadhead_flight_plans.len(), 1); - unit_test_info!("(test_query_flight_9_deadhead_destination_flight_no_capacity_at_destination_vertiport) success"); - } - */ -} diff --git a/server/src/grpc/server.rs b/server/src/grpc/server.rs index 8d609db..8e30bdd 100644 --- a/server/src/grpc/server.rs +++ b/server/src/grpc/server.rs @@ -33,10 +33,13 @@ impl RpcService for ServerImpl { ) -> Result, Status> { grpc_info!("(query_flight) scheduler server."); grpc_debug!("(query_flight) request: {:?}", request); - let res = super::queries::query_flight(request).await; - if res.is_err() { - grpc_error!("{}", res.as_ref().unwrap_err()); + let res = super::api::query_flight::query_flight(request).await; + if let Err(e) = res { + grpc_error!("(query_flight) error: {}", e); + return Err(e); } + + res } ///Confirms the draft itinerary by id. @@ -46,10 +49,13 @@ impl RpcService for ServerImpl { ) -> Result, Status> { grpc_info!("(confirm_itinerary) scheduler server."); grpc_debug!("(confirm_itinerary) request: {:?}", request); - let res = super::queries::confirm_itinerary(request).await; - if res.is_err() { - grpc_error!("{}", res.as_ref().unwrap_err()); + let res = super::api::confirm_itinerary::confirm_itinerary(request).await; + if let Err(e) = res { + grpc_error!("(confirm_itinerary) error: {}", e); + return Err(e); } + + res } /// Cancels the itinerary by id. @@ -59,10 +65,13 @@ impl RpcService for ServerImpl { ) -> Result, Status> { grpc_info!("(cancel_itinerary) scheduler server."); grpc_debug!("(cancel_itinerary) request: {:?}", request); - let res = super::queries::cancel_itinerary(request).await; - if res.is_err() { - grpc_error!("{}", res.as_ref().unwrap_err()); + let res = super::api::cancel_itinerary::cancel_itinerary(request).await; + if let Err(e) = res { + grpc_error!("(cancel_itinerary) error: {}", e); + return Err(e); } + + res } /// Returns ready:true when service is available @@ -145,9 +154,8 @@ impl RpcService for ServerImpl { }; let itineraries = vec![Itinerary { - id: uuid::Uuid::new_v4().to_string(), - flight_plan: Some(flight_plan), - deadhead_flight_plans: vec![], + itinerary_id: uuid::Uuid::new_v4().to_string(), + flight_plans: vec![flight_plan], }]; Ok(tonic::Response::new(QueryFlightResponse { itineraries })) diff --git a/server/src/main.rs b/server/src/main.rs index 9619e1d..8302b9c 100755 --- a/server/src/main.rs +++ b/server/src/main.rs @@ -13,21 +13,6 @@ async fn main() -> Result<(), Box> { init_logger(&config); - // Spawn the loop for the router re-initialization - // (Dirty hack) - // TODO(R3): Refactor to respond to grpc trigger or - // move routing to SQL Graph database - tokio::spawn(async move { - // Every 10 seconds - let duration = std::time::Duration::new(10, 0); - - loop { - grpc::queries::init_router().await; - - std::thread::sleep(duration); - } - }); - // Spawn the GRPC server for this service tokio::spawn(grpc::server::grpc_server(config, None)).await?; diff --git a/server/src/router/flight_plan.rs b/server/src/router/flight_plan.rs new file mode 100644 index 0000000..398e28a --- /dev/null +++ b/server/src/router/flight_plan.rs @@ -0,0 +1,255 @@ +//! Helper Functions for Flight Plans + +use crate::grpc::client::GrpcClients; +use chrono::{DateTime, Utc}; +use prost_wkt_types::Timestamp; +use svc_storage_client_grpc::prelude::*; +use uuid::Uuid; + +#[derive(Debug, Clone)] +pub struct FlightPlanSchedule { + pub departure_vertiport_id: String, + pub departure_vertipad_id: String, + pub departure_time: DateTime, + pub arrival_vertiport_id: String, + pub arrival_vertipad_id: String, + pub arrival_time: DateTime, + pub vehicle_id: String, +} + +#[derive(Debug, Copy, Clone, PartialEq)] +pub enum FlightPlanError { + ClientError, + InvalidData, +} + +impl std::fmt::Display for FlightPlanError { + fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result { + match self { + FlightPlanError::ClientError => write!(f, "ClientError"), + FlightPlanError::InvalidData => write!(f, "InvalidData"), + } + } +} + +impl TryFrom for FlightPlanSchedule { + type Error = FlightPlanError; + + fn try_from(flight_plan: flight_plan::Object) -> Result { + let Some(data) = flight_plan.data else { + router_error!( + "(get_flight_plan_schedule) Flight plan [{}] has no data.", + flight_plan.id + ); + return Err(FlightPlanError::InvalidData) + }; + + // + // Must have valid departure and arrival times + // + let departure_time = match data.scheduled_departure { + Some(departure_time) => departure_time.into(), + None => { + router_error!( + "(get_flight_plan_schedule) Flight plan [{}] has no scheduled departure.", + flight_plan.id + ); + return Err(FlightPlanError::InvalidData); + } + }; + + let arrival_time = match data.scheduled_arrival { + Some(arrival_time) => arrival_time.into(), + None => { + router_error!( + "(get_flight_plan_schedule) Flight plan [{}] has no scheduled arrival.", + flight_plan.id + ); + return Err(FlightPlanError::InvalidData); + } + }; + + // + // Must have valid departure and arrival vertiports in UUID format + // + let Some(departure_vertiport_id) = data.departure_vertiport_id else { + router_error!( + "(get_flight_plan_schedule) Flight plan [{}] has no departure vertiport.", + flight_plan.id + ); + return Err(FlightPlanError::InvalidData) + }; + + let departure_vertiport_id = match Uuid::parse_str(&departure_vertiport_id) { + Ok(id) => id.to_string(), + Err(e) => { + router_error!( + "(get_flight_plan_schedule) Flight plan [{}] has invalid departure vertiport id: {}", + flight_plan.id, + e + ); + return Err(FlightPlanError::InvalidData); + } + }; + + let Some(arrival_vertiport_id) = data.destination_vertiport_id else { + router_error!( + "(get_flight_plan_schedule) Flight plan [{}] has no arrival vertiport.", + flight_plan.id + ); + return Err(FlightPlanError::InvalidData) + }; + + let arrival_vertiport_id = match Uuid::parse_str(&arrival_vertiport_id) { + Ok(id) => id.to_string(), + Err(e) => { + router_error!( + "(get_flight_plan_schedule) Flight plan [{}] has invalid arrival vertiport id: {}", + flight_plan.id, + e + ); + return Err(FlightPlanError::InvalidData); + } + }; + + // + // Must have a valid vehicle id in UUID format + // + let Ok(vehicle_id) = Uuid::parse_str(&data.vehicle_id) else { + router_error!( + "(get_flight_plan_schedule) Flight plan [{}] has no vehicle.", + flight_plan.id + ); + return Err(FlightPlanError::InvalidData) + }; + + Ok(FlightPlanSchedule { + departure_vertiport_id, + departure_vertipad_id: data.departure_vertipad_id, + departure_time, + arrival_vertiport_id, + arrival_vertipad_id: data.destination_vertipad_id, + arrival_time, + vehicle_id: vehicle_id.to_string(), + }) + } +} + +/// Gets all flight plans from storage in sorted order from +/// earliest to latest arrival time +pub async fn get_sorted_flight_plans( + latest_arrival_time: &DateTime, + clients: &GrpcClients, +) -> Result, FlightPlanError> { + let latest_arrival_time: Timestamp = (*latest_arrival_time).into(); + + // TODO(R4): Further filter by vehicle type, etc. + // With hundreds of vehicles in the air, this will be a lot of data + // on each call. + let mut filter = AdvancedSearchFilter::search_less_or_equal( + "scheduled_arrival".to_owned(), + latest_arrival_time.to_string(), + ) + .and_is_null("deleted_at".to_owned()) + .and_not_in( + "flight_status".to_owned(), + vec![ + (flight_plan::FlightStatus::Finished as i32).to_string(), + (flight_plan::FlightStatus::Cancelled as i32).to_string(), + ], + ); + + filter.order_by = vec![ + SortOption { + sort_field: "vehicle_id".to_string(), + sort_order: SortOrder::Asc as i32, + }, + SortOption { + sort_field: "scheduled_departure".to_owned(), + sort_order: SortOrder::Asc as i32, + }, + ]; + + let response = match clients.storage.flight_plan.search(filter).await { + Ok(response) => response.into_inner(), + Err(e) => { + router_error!( + "(get_sorted_flight_plans) Failed to get flight plans from storage: {}", + e + ); + return Err(FlightPlanError::ClientError); + } + }; + + Ok(response + .list + .into_iter() + .filter_map(|fp| FlightPlanSchedule::try_from(fp).ok()) + .collect::>()) +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn test_flight_plan_schedule_try_from() { + let expected_departure_vertiport_id = Uuid::new_v4().to_string(); + let expected_departure_vertipad_id = Uuid::new_v4().to_string(); + let expected_arrival_vertiport_id = Uuid::new_v4().to_string(); + let expected_arrival_vertipad_id = Uuid::new_v4().to_string(); + let expected_vehicle_id = Uuid::new_v4().to_string(); + + let flight_plan = flight_plan::Object { + id: Uuid::new_v4().to_string(), + data: Some(flight_plan::Data { + departure_vertiport_id: Some(expected_departure_vertiport_id.clone()), + departure_vertipad_id: expected_departure_vertipad_id.clone(), + destination_vertiport_id: Some(expected_arrival_vertiport_id.clone()), + destination_vertipad_id: expected_arrival_vertipad_id.clone(), + scheduled_departure: Some(Timestamp { + seconds: 0, + nanos: 0, + }), + scheduled_arrival: Some(Timestamp { + seconds: 0, + nanos: 0, + }), + vehicle_id: expected_vehicle_id.clone(), + ..Default::default() + }), + ..Default::default() + }; + + let flight_plan_schedule = FlightPlanSchedule::try_from(flight_plan).unwrap(); + assert_eq!( + flight_plan_schedule.departure_vertiport_id, + expected_departure_vertiport_id + ); + assert_eq!( + flight_plan_schedule.departure_vertipad_id, + expected_departure_vertipad_id + ); + assert_eq!( + flight_plan_schedule.arrival_vertiport_id, + expected_arrival_vertiport_id + ); + assert_eq!( + flight_plan_schedule.arrival_vertipad_id, + expected_arrival_vertipad_id + ); + assert_eq!(flight_plan_schedule.vehicle_id, expected_vehicle_id); + } + + #[test] + fn test_flight_plan_schedule_try_from_invalid_data() { + let flight_plan = flight_plan::Object { + id: "test".to_owned(), + data: None, + ..Default::default() + }; + + let e = FlightPlanSchedule::try_from(flight_plan).unwrap_err(); + assert_eq!(e, FlightPlanError::InvalidData); + } +} diff --git a/server/src/router/itinerary.rs b/server/src/router/itinerary.rs new file mode 100644 index 0000000..8fd1987 --- /dev/null +++ b/server/src/router/itinerary.rs @@ -0,0 +1,927 @@ +//! Build an itinerary given aircraft availability and the flight window + +use super::schedule::*; +use super::vehicle::*; +use super::vertiport::TimeslotPair; +use super::{best_path, BestPathError, BestPathRequest}; +use crate::grpc::client::GrpcClients; +use svc_gis_client_grpc::prelude::gis::*; +use svc_storage_client_grpc::prelude::*; + +use chrono::{DateTime, Duration, Utc}; +use std::cmp::max; +use std::collections::HashMap; +use std::fmt::{Display, Formatter, Result as FmtResult}; + +#[derive(Debug, Clone, PartialEq)] +pub enum ItineraryError { + ClientError, + InvalidData, + NoPathFound, + ScheduleConflict, +} + +impl Display for ItineraryError { + fn fmt(&self, f: &mut Formatter<'_>) -> FmtResult { + match self { + ItineraryError::ClientError => write!(f, "Could not contact dependency."), + ItineraryError::InvalidData => write!(f, "Invalid data."), + ItineraryError::NoPathFound => write!(f, "No path found."), + ItineraryError::ScheduleConflict => write!(f, "Schedule conflict."), + } + } +} + +/// Given timeslot pairs for departure and arrival vertiport and the +/// availabilities of the aircraft, get possible itineraries for each +/// aircraft. +/// Returns a maximum of 1 itinerary per aircraft. +pub async fn get_itineraries( + required_loading_time: &Duration, + required_unloading_time: &Duration, + timeslot_pairs: &[TimeslotPair], + aircraft_gaps: &HashMap>, + clients: &GrpcClients, +) -> Result>, ItineraryError> { + let mut itineraries: Vec> = vec![]; + + // For each available aircraft, see if it can do the flight + for (aircraft_id, aircraft_availability) in aircraft_gaps { + // Try different timeslots for the aircraft + for pair in timeslot_pairs { + // TODO(R4): Include vehicle model to improve estimate + let flight_duration = estimate_flight_time_seconds(&pair.distance_meters); + + let flight_window = Timeslot { + time_start: pair.depart_timeslot.time_start, + time_end: pair.arrival_timeslot.time_end, + }; + + let flight_plan = svc_storage_client_grpc::prelude::flight_plan::Data { + departure_vertiport_id: Some(pair.depart_port_id.clone()), + destination_vertiport_id: Some(pair.arrival_port_id.clone()), + departure_vertipad_id: pair.depart_pad_id.clone(), + destination_vertipad_id: pair.arrival_pad_id.clone(), + path: Some(pair.path.clone()), + vehicle_id: aircraft_id.clone(), + ..Default::default() + }; + + match aircraft_selection( + flight_plan, + aircraft_availability, + &flight_duration, + required_loading_time, + required_unloading_time, + &flight_window, + clients, + ) + .await + { + Ok(itinerary) => itineraries.push(itinerary), + Err(ItineraryError::ClientError) => { + // exit immediately if svc-gis is down, don't allow new flights + router_error!( + "(get_vehicle_availability) Could not determine path; client error." + ); + return Err(ItineraryError::ClientError); + } + _ => { + continue; + } + } + } + } + + Ok(itineraries) +} + +/// Iterate through an aircraft's available timeslots +/// and see if it can do the requested flight. +/// TODO(R4): Return more than one itinerary per aircraft +async fn aircraft_selection( + flight_plan: flight_plan::Data, + availability: &[Availability], + flight_duration: &Duration, + required_loading_time: &Duration, + required_unloading_time: &Duration, + flight_window: &Timeslot, + clients: &GrpcClients, +) -> Result, ItineraryError> { + for gap in availability.iter() { + match get_itinerary( + flight_plan.clone(), + gap, + flight_duration, + required_loading_time, + required_unloading_time, + flight_window, + clients, + ) + .await + { + Ok(itinerary) => { + // only return the first valid itinerary for an aircraft + return Ok(itinerary); + } + Err(ItineraryError::ClientError) => { + // exit immediately if svc-gis is down, don't allow new flights + router_error!("(get_vehicle_availability) Could not determine path; client error."); + return Err(ItineraryError::ClientError); + } + _ => { + continue; + } + } + } + + Err(ItineraryError::ScheduleConflict) +} + +/// Determines if the aircraft is available for the requested flight, +/// given that it may require multiple deadhead trips. +async fn get_itinerary( + flight_plan: flight_plan::Data, + availability: &Availability, + flight_duration: &Duration, + _required_loading_time: &Duration, + _required_unloading_time: &Duration, + flight_window: &Timeslot, + clients: &GrpcClients, +) -> Result, ItineraryError> { + // Must be some overlap between the flight window and the available timeslot + let Ok(overlap) = availability.timeslot.overlap(flight_window) else { + router_debug!( + "(is_aircraft_available) No overlap between flight window and available timeslot." + ); + + return Err(ItineraryError::ScheduleConflict); + }; + + let Some(ref departure_vertiport_id) = flight_plan.departure_vertiport_id else { + router_error!( + "(get_vehicle_itinerary) Flight plan doesn't have departure_vertiport_id.", + ); + + return Err(ItineraryError::InvalidData); + }; + + let Some(ref arrival_vertiport_id) = flight_plan.destination_vertiport_id else { + router_error!( + "(get_vehicle_itinerary) Flight plan doesn't have destination_vertiport_id.", + ); + + return Err(ItineraryError::InvalidData); + }; + + let vehicle_id = flight_plan.vehicle_id.clone(); + + // + // Create the flight plan for the deadhead flight to the requested departure vertiport + // + let mut flight_plans = vec![]; + if *departure_vertiport_id != availability.vertiport_id { + // See what the path and cost would be for a flight between the starting + // available timeslot and the ending flight time + let best_path_request = BestPathRequest { + start_type: NodeType::Vertiport as i32, + node_start_id: availability.vertiport_id.clone(), + node_uuid_end: departure_vertiport_id.clone(), + time_start: Some(availability.timeslot.time_start.into()), + time_end: Some(overlap.time_end.into()), + }; + + let (deadhead_path, pre_deadhead_distance_meters) = + match best_path(&best_path_request, clients).await { + Ok((deadhead_path, d)) => (deadhead_path, d as f32), + Err(BestPathError::NoPathFound) => { + // no path found, perhaps temporary no-fly zone + // is blocking journeys from this depart timeslot + // Break out and try the next depart timeslot + router_debug!( + "(get_vehicle_availability) No path found from vertiport {} + to vertiport {} (from {} to {}).", + availability.vertiport_id, + departure_vertiport_id, + availability.timeslot.time_start, + availability.timeslot.time_end + ); + + return Err(ItineraryError::NoPathFound); + } + Err(BestPathError::ClientError) => { + // exit immediately if svc-gis is down, don't allow new flights + router_error!("(get_vehicle_availability) Could not determine path."); + return Err(ItineraryError::ClientError); + } + }; + + let pre_deadhead_duration = estimate_flight_time_seconds(&pre_deadhead_distance_meters); + + // leave at earliest possible time + let scheduled_departure = max( + availability.timeslot.time_start, + flight_window.time_start - pre_deadhead_duration, + ); + let scheduled_arrival = scheduled_departure + pre_deadhead_duration; + if scheduled_arrival > availability.timeslot.time_end { + // This flight plan would end after the available timeslot + // Break out and try the next available timeslot + router_debug!( + "(get_vehicle_availability) Flight plan would end after available timeslot." + ); + + return Err(ItineraryError::ScheduleConflict); + } + + // TODO(R4): Get last vertipad for departure_vertipad_id + // less important than knowing where you're going to land + flight_plans.push(flight_plan::Data { + scheduled_departure: Some(scheduled_departure.into()), + scheduled_arrival: Some(scheduled_arrival.into()), + // go from current location, known from availability, to the departure vertiport for the requested flight + departure_vertiport_id: Some(availability.vertiport_id.clone()), + destination_vertiport_id: Some(departure_vertiport_id.clone()), + destination_vertipad_id: flight_plan.departure_vertipad_id.clone(), + vehicle_id: vehicle_id.clone(), + path: Some(deadhead_path), + ..Default::default() + }); + } + + // + // Create the flight plan for the requested flight + // + { + let scheduled_departure: DateTime = match flight_plans.last() { + Some(last) => match &last.scheduled_arrival { + Some(s) => s.clone().into(), + None => { + router_error!( + "(get_vehicle_availability) Last flight plan has no scheduled arrival." + ); + + return Err(ItineraryError::InvalidData); + } + }, + // leave at earliest possible time + None => max(flight_window.time_start, availability.timeslot.time_start), + }; + + let scheduled_arrival = scheduled_departure + *flight_duration; + if scheduled_arrival > availability.timeslot.time_end { + // This flight plan would end after the available timeslot + // Break out and try the next available timeslot + router_debug!( + "(get_vehicle_availability) Flight plan would end after available timeslot." + ); + + return Err(ItineraryError::ScheduleConflict); + } + + if scheduled_arrival > flight_window.time_end { + // This flight plan would end after the flight window + // Break out and try the next available timeslot + router_debug!("(get_vehicle_availability) Flight plan would end after flight window."); + + return Err(ItineraryError::ScheduleConflict); + } + + // Flight requested by user + let mut flight_plan = flight_plan.clone(); + flight_plan.scheduled_departure = Some(scheduled_departure.into()); + flight_plan.scheduled_arrival = Some(scheduled_arrival.into()); + flight_plans.push(flight_plan); + } + + // + // Create the post deadhead flight to take the aircraft away from the pad + // when flight is completed + // + if *arrival_vertiport_id != availability.vertiport_id { + // TODO(R4) - Get nearest open rest stop/hangar, direct to it + // right now it boomerangs back to its original last_vertiport_id + let Some(last) = flight_plans.last() else { + router_error!( + "(get_vehicle_availability) No flight plans found for vehicle {}.", + vehicle_id + ); + + return Err(ItineraryError::InvalidData); + }; + + let Some(last_arrival) = &last.scheduled_arrival else { + router_error!( + "(get_vehicle_availability) Last flight plan has no scheduled arrival." + ); + + return Err(ItineraryError::InvalidData); + }; + + // See what the path would cost from the flight plan's destination port + // to the next flight plan's departure port + let best_path_request = BestPathRequest { + start_type: NodeType::Vertiport as i32, + node_start_id: arrival_vertiport_id.clone(), + node_uuid_end: availability.vertiport_id.clone(), + time_start: Some(last_arrival.clone()), + time_end: Some(availability.timeslot.time_end.into()), + }; + + let (deadhead_path, post_deadhead_distance_meters) = + match best_path(&best_path_request, clients).await { + Ok((deadhead_path, d)) => (deadhead_path, d as f32), + Err(BestPathError::NoPathFound) => { + // no path found, perhaps temporary no-fly zone + // is blocking journeys from this depart timeslot + // Break out and try the next depart timeslot + router_debug!( + "(get_vehicle_availability) No path found from vertiport {} + to vertiport {} (from {} to {}).", + arrival_vertiport_id, + availability.vertiport_id, + availability.timeslot.time_start, + availability.timeslot.time_end + ); + + return Err(ItineraryError::NoPathFound); + } + Err(BestPathError::ClientError) => { + // exit immediately if svc-gis is down, don't allow new flights + router_error!("(get_vehicle_availability) Could not determine path."); + return Err(ItineraryError::ClientError); + } + }; + + let post_deadhead_duration = estimate_flight_time_seconds(&post_deadhead_distance_meters); + + let scheduled_departure: DateTime = last_arrival.clone().into(); + let scheduled_arrival = scheduled_departure + post_deadhead_duration; + if scheduled_arrival > availability.timeslot.time_end { + // This flight plan would end after the available timeslot + // Break out and try the next available timeslot + router_debug!( + "(get_vehicle_availability) Flight plan would end after available timeslot." + ); + + return Err(ItineraryError::ScheduleConflict); + } + + // TODO(R4): Get open vertipad for deadhead to rest stop/hangar + + flight_plans.push(flight_plan::Data { + scheduled_departure: Some(scheduled_departure.into()), + scheduled_arrival: Some(scheduled_arrival.into()), + // go from current location, known from availability, to the departure vertiport for the requested flight + departure_vertiport_id: last.destination_vertiport_id.clone(), + departure_vertipad_id: last.destination_vertipad_id.clone(), + destination_vertiport_id: Some(availability.vertiport_id.clone()), + vehicle_id: vehicle_id.clone(), + path: Some(deadhead_path), + ..Default::default() + }); + } + + Ok(flight_plans) +} + +#[cfg(test)] +mod tests { + use super::*; + use crate::grpc::client::get_clients; + use uuid::Uuid; + + #[tokio::test] + #[cfg(feature = "stub_backends")] + async fn test_get_itinerary_valid_pre_post_deadheads() { + let clients = get_clients().await; + let time_start = Utc::now(); + let time_end = Utc::now() + Duration::seconds(100); + let vertiport_1 = Uuid::new_v4().to_string(); + let vertiport_2 = Uuid::new_v4().to_string(); + let vertiport_3 = Uuid::new_v4().to_string(); + let vertipad_1 = Uuid::new_v4().to_string(); + let vertipad_2 = Uuid::new_v4().to_string(); + let vehicle_id = Uuid::new_v4().to_string(); + let required_loading_time = Duration::seconds(30); + let required_unloading_time = Duration::seconds(30); + + let aircraft_availability = Availability { + vertiport_id: vertiport_1.clone(), + timeslot: Timeslot { + time_start: time_start - Duration::seconds(100), + time_end: time_end + Duration::seconds(100), + }, + }; + + let distance_meters = 50.0; + let flight_duration = estimate_flight_time_seconds(&distance_meters); + let flight_window = Timeslot { + time_end, + time_start, + }; + + let flight_plan = flight_plan::Data { + departure_vertiport_id: Some(vertiport_3.clone()), + destination_vertiport_id: Some(vertiport_2.clone()), + departure_vertipad_id: vertipad_1.clone(), + destination_vertipad_id: vertipad_2.clone(), + vehicle_id, + path: Some(GeoLineString { points: vec![] }), + ..Default::default() + }; + + let itinerary = get_itinerary( + flight_plan, + &aircraft_availability, + &flight_duration, + &required_loading_time, + &required_unloading_time, + &flight_window, + &clients, + ) + .await + .unwrap(); + + // 3 flight plans: deadhead to vertiport_3, flight to vertiport_2, deadhead to vertiport_1 + assert_eq!(itinerary.len(), 3); + assert_eq!( + itinerary[0].departure_vertiport_id.clone().unwrap(), + vertiport_1 + ); + assert_eq!( + itinerary[0].destination_vertiport_id.clone().unwrap(), + vertiport_3 + ); + assert_eq!(itinerary[0].destination_vertipad_id.clone(), vertipad_1); + + assert_eq!( + itinerary[1].departure_vertiport_id.clone().unwrap(), + vertiport_3 + ); + assert_eq!(itinerary[1].departure_vertipad_id.clone(), vertipad_1); + assert_eq!( + itinerary[1].destination_vertiport_id.clone().unwrap(), + vertiport_2 + ); + assert_eq!(itinerary[1].destination_vertipad_id.clone(), vertipad_2); + + assert_eq!( + itinerary[2].departure_vertiport_id.clone().unwrap(), + vertiport_2 + ); + assert_eq!(itinerary[2].departure_vertipad_id.clone(), vertipad_2); + assert_eq!( + itinerary[2].destination_vertiport_id.clone().unwrap(), + vertiport_1 + ); + + // Land at earliest possible time + assert_eq!( + itinerary[0].scheduled_arrival.clone().unwrap(), + time_start.into() + ); + assert_eq!( + itinerary[1].scheduled_departure.clone().unwrap(), + time_start.into() + ); + assert_eq!( + itinerary[1].scheduled_arrival.clone().unwrap(), + (time_start + flight_duration).into() + ); + assert_eq!( + itinerary[2].scheduled_departure.clone().unwrap(), + (time_start + flight_duration).into() + ); + } + + #[tokio::test] + #[cfg(feature = "stub_backends")] + async fn test_get_itinerary_valid_pre_deadhead() { + let clients = get_clients().await; + let time_start = Utc::now(); + let time_end = Utc::now() + Duration::seconds(100); + let vertiport_1 = Uuid::new_v4().to_string(); + let vertiport_3 = Uuid::new_v4().to_string(); + let vertipad_1 = Uuid::new_v4().to_string(); + let vertipad_2 = Uuid::new_v4().to_string(); + let vehicle_id = Uuid::new_v4().to_string(); + let required_loading_time = Duration::seconds(30); + let required_unloading_time = Duration::seconds(30); + + let aircraft_availability = Availability { + vertiport_id: vertiport_1.clone(), + timeslot: Timeslot { + time_start: time_start - Duration::seconds(100), + time_end: time_end + Duration::seconds(100), + }, + }; + + let distance_meters = 50.0; + let flight_duration = estimate_flight_time_seconds(&distance_meters); + let flight_window = Timeslot { + time_end, + time_start, + }; + + let flight_plan = flight_plan::Data { + departure_vertiport_id: Some(vertiport_3.clone()), + destination_vertiport_id: Some(vertiport_1.clone()), + departure_vertipad_id: vertipad_1.clone(), + destination_vertipad_id: vertipad_2.clone(), + vehicle_id, + path: Some(GeoLineString { points: vec![] }), + ..Default::default() + }; + + let itinerary = get_itinerary( + flight_plan, + &aircraft_availability, + &flight_duration, + &required_loading_time, + &required_unloading_time, + &flight_window, + &clients, + ) + .await + .unwrap(); + + // 2 flight plans: deadhead to vertiport_3, flight to vertiport_1 + assert_eq!(itinerary.len(), 2); + assert_eq!( + itinerary[0].departure_vertiport_id.clone().unwrap(), + vertiport_1 + ); + assert_eq!( + itinerary[0].destination_vertiport_id.clone().unwrap(), + vertiport_3 + ); + assert_eq!(itinerary[0].destination_vertipad_id.clone(), vertipad_1); + + assert_eq!( + itinerary[1].departure_vertiport_id.clone().unwrap(), + vertiport_3 + ); + assert_eq!(itinerary[1].departure_vertipad_id.clone(), vertipad_1); + assert_eq!( + itinerary[1].destination_vertiport_id.clone().unwrap(), + vertiport_1 + ); + assert_eq!(itinerary[1].destination_vertipad_id.clone(), vertipad_2); + + // Land at earliest possible time + assert_eq!( + itinerary[0].scheduled_arrival.clone().unwrap(), + time_start.into() + ); + assert_eq!( + itinerary[1].scheduled_departure.clone().unwrap(), + time_start.into() + ); + assert_eq!( + itinerary[1].scheduled_arrival.clone().unwrap(), + (time_start + flight_duration).into() + ); + } + + #[tokio::test] + #[cfg(feature = "stub_backends")] + async fn test_get_itinerary_valid_post_deadhead() { + let clients = get_clients().await; + let time_start = Utc::now(); + let time_end = Utc::now() + Duration::seconds(100); + let vertiport_1 = Uuid::new_v4().to_string(); + let vertiport_3 = Uuid::new_v4().to_string(); + let vertipad_1 = Uuid::new_v4().to_string(); + let vertipad_2 = Uuid::new_v4().to_string(); + let vehicle_id = Uuid::new_v4().to_string(); + let required_loading_time = Duration::seconds(30); + let required_unloading_time = Duration::seconds(30); + + let aircraft_availability = Availability { + vertiport_id: vertiport_1.clone(), + timeslot: Timeslot { + time_start: time_start - Duration::seconds(100), + time_end: time_end + Duration::seconds(100), + }, + }; + + let distance_meters = 50.0; + let flight_duration = estimate_flight_time_seconds(&distance_meters); + let flight_window = Timeslot { + time_end, + time_start, + }; + + let flight_plan = flight_plan::Data { + departure_vertiport_id: Some(vertiport_1.clone()), + destination_vertiport_id: Some(vertiport_3.clone()), + departure_vertipad_id: vertipad_1.clone(), + destination_vertipad_id: vertipad_2.clone(), + vehicle_id, + path: Some(GeoLineString { points: vec![] }), + ..Default::default() + }; + + let itinerary = get_itinerary( + flight_plan, + &aircraft_availability, + &flight_duration, + &required_loading_time, + &required_unloading_time, + &flight_window, + &clients, + ) + .await + .unwrap(); + + // 2 flight plans: flight to vertiport_3, deadhead to vertiport_1 + assert_eq!(itinerary.len(), 2); + assert_eq!( + itinerary[0].departure_vertiport_id.clone().unwrap(), + vertiport_1 + ); + assert_eq!(itinerary[0].departure_vertipad_id.clone(), vertipad_1); + assert_eq!( + itinerary[0].destination_vertiport_id.clone().unwrap(), + vertiport_3 + ); + assert_eq!(itinerary[0].destination_vertipad_id.clone(), vertipad_2); + + assert_eq!( + itinerary[1].departure_vertiport_id.clone().unwrap(), + vertiport_3 + ); + assert_eq!(itinerary[1].departure_vertipad_id.clone(), vertipad_2); + assert_eq!( + itinerary[1].destination_vertiport_id.clone().unwrap(), + vertiport_1 + ); + + // Land at earliest possible time + assert_eq!( + itinerary[0].scheduled_departure.clone().unwrap(), + time_start.into() + ); + assert_eq!( + itinerary[0].scheduled_arrival.clone().unwrap(), + (time_start + flight_duration).into() + ); + assert_eq!( + itinerary[1].scheduled_departure.clone().unwrap(), + (time_start + flight_duration).into() + ); + } + + #[tokio::test] + #[cfg(feature = "stub_backends")] + async fn test_get_itinerary_valid_later_flight_window() { + let clients = get_clients().await; + let time_start = Utc::now(); + let time_end = Utc::now() + Duration::hours(1); + let vertiport_1 = Uuid::new_v4().to_string(); + let vertiport_2 = Uuid::new_v4().to_string(); + let vertiport_3 = Uuid::new_v4().to_string(); + let vertipad_1 = Uuid::new_v4().to_string(); + let vertipad_2 = Uuid::new_v4().to_string(); + let vehicle_id = Uuid::new_v4().to_string(); + let required_loading_time = Duration::seconds(30); + let required_unloading_time = Duration::seconds(30); + + // | flight window | + // | takeoff and land time window | + // + + let aircraft_availability = Availability { + vertiport_id: vertiport_1.clone(), + timeslot: Timeslot { + time_start: time_start + Duration::minutes(10), + time_end: time_end - Duration::minutes(20), + }, + }; + + let distance_meters = 50.0; + let flight_duration = estimate_flight_time_seconds(&distance_meters); + let flight_window = Timeslot { + time_end, + time_start, + }; + + let flight_plan = flight_plan::Data { + departure_vertiport_id: Some(vertiport_3.clone()), + destination_vertiport_id: Some(vertiport_2.clone()), + departure_vertipad_id: vertipad_1.clone(), + destination_vertipad_id: vertipad_2.clone(), + vehicle_id, + path: Some(GeoLineString { points: vec![] }), + ..Default::default() + }; + + let itinerary = get_itinerary( + flight_plan, + &aircraft_availability, + &flight_duration, + &required_loading_time, + &required_unloading_time, + &flight_window, + &clients, + ) + .await + .unwrap(); + + // 3 flight plans: deadhead to vertiport_3, flight to vertiport_2, deadhead to vertiport_1 + assert_eq!(itinerary.len(), 3); + assert_eq!( + itinerary[0].departure_vertiport_id.clone().unwrap(), + vertiport_1 + ); + assert_eq!( + itinerary[0].destination_vertiport_id.clone().unwrap(), + vertiport_3 + ); + assert_eq!(itinerary[0].destination_vertipad_id.clone(), vertipad_1); + + assert_eq!( + itinerary[1].departure_vertiport_id.clone().unwrap(), + vertiport_3 + ); + assert_eq!(itinerary[1].departure_vertipad_id.clone(), vertipad_1); + assert_eq!( + itinerary[1].destination_vertiport_id.clone().unwrap(), + vertiport_2 + ); + assert_eq!(itinerary[1].destination_vertipad_id.clone(), vertipad_2); + + assert_eq!( + itinerary[2].departure_vertiport_id.clone().unwrap(), + vertiport_2 + ); + assert_eq!(itinerary[2].departure_vertipad_id.clone(), vertipad_2); + assert_eq!( + itinerary[2].destination_vertiport_id.clone().unwrap(), + vertiport_1 + ); + + // First itinerary for aircraft leaves at earliest aircraft convenience + assert_eq!( + itinerary[0].scheduled_departure.clone().unwrap(), + aircraft_availability.timeslot.time_start.into() + ); + } + + #[tokio::test] + #[cfg(feature = "stub_backends")] + async fn test_get_itinerary_valid_incompatible_flight_window() { + let clients = get_clients().await; + let time_start = Utc::now(); + let time_end = Utc::now() + Duration::hours(1); + let vertiport_1 = Uuid::new_v4().to_string(); + let vertiport_2 = Uuid::new_v4().to_string(); + let vertiport_3 = Uuid::new_v4().to_string(); + let vertipad_1 = Uuid::new_v4().to_string(); + let vertipad_2 = Uuid::new_v4().to_string(); + let vehicle_id = Uuid::new_v4().to_string(); + let required_loading_time = Duration::seconds(30); + let required_unloading_time = Duration::seconds(30); + + // | flight window | + // | takeoff and land time window | + // + + let aircraft_availability = Availability { + vertiport_id: vertiport_1.clone(), + timeslot: Timeslot { + time_start: time_end - Duration::seconds(30), + time_end: time_end + Duration::minutes(20), + }, + }; + + let distance_meters = 1000.0; // too far to fly + let flight_duration = estimate_flight_time_seconds(&distance_meters); + let flight_window = Timeslot { + time_end, + time_start, + }; + + let flight_plan = flight_plan::Data { + departure_vertiport_id: Some(vertiport_3.clone()), + destination_vertiport_id: Some(vertiport_2.clone()), + departure_vertipad_id: vertipad_1.clone(), + destination_vertipad_id: vertipad_2.clone(), + vehicle_id, + path: Some(GeoLineString { points: vec![] }), + ..Default::default() + }; + + let e = get_itinerary( + flight_plan, + &aircraft_availability, + &flight_duration, + &required_loading_time, + &required_unloading_time, + &flight_window, + &clients, + ) + .await + .unwrap_err(); + assert_eq!(e, ItineraryError::ScheduleConflict); + } + + #[tokio::test] + #[cfg(feature = "stub_backends")] + async fn test_get_itineraries() { + let clients = get_clients().await; + let time_start = Utc::now(); + let time_end = Utc::now() + Duration::seconds(100); + let vertiport_1 = Uuid::new_v4().to_string(); + let vertiport_2 = Uuid::new_v4().to_string(); + let vertiport_3 = Uuid::new_v4().to_string(); + let vertipad_1 = Uuid::new_v4().to_string(); + let _vertipad_2 = Uuid::new_v4().to_string(); + let vehicle_1 = Uuid::new_v4().to_string(); + let vehicle_2 = Uuid::new_v4().to_string(); + let required_loading_time = Duration::seconds(30); + let required_unloading_time = Duration::seconds(30); + + let availabilities = HashMap::from([ + ( + vehicle_1.clone(), + vec![Availability { + vertiport_id: vertiport_1.clone(), + timeslot: Timeslot { + time_start: time_start - Duration::hours(1), + time_end: time_end + Duration::hours(1), + }, + }], + ), + ( + vehicle_2.clone(), + vec![Availability { + vertiport_id: vertiport_3.clone(), + timeslot: Timeslot { + time_start: time_end + Duration::hours(1), + time_end: time_end + Duration::hours(2), + }, + }], + ), + ]); + + let distance_meters = 50.0; + let flight_duration = estimate_flight_time_seconds(&distance_meters); + let timeslot_pairs = vec![ + TimeslotPair { + depart_port_id: vertiport_1.clone(), + depart_pad_id: vertipad_1.clone(), + depart_timeslot: Timeslot { + time_start: time_start.clone(), + time_end: time_end.clone(), + }, + arrival_port_id: vertiport_2.clone(), + arrival_pad_id: vertiport_2.clone(), + arrival_timeslot: Timeslot { + time_start: time_start + flight_duration, + time_end: time_end + flight_duration, + }, + path: GeoLineString { points: vec![] }, + distance_meters, + }, + TimeslotPair { + depart_port_id: vertiport_1.clone(), + depart_pad_id: vertipad_1.clone(), + depart_timeslot: Timeslot { + time_start: time_end + Duration::hours(1), + time_end: time_end + Duration::hours(2), + }, + arrival_port_id: vertiport_2.clone(), + arrival_pad_id: vertiport_2.clone(), + arrival_timeslot: Timeslot { + time_start: time_end + Duration::hours(1) + flight_duration, + time_end: time_end + Duration::hours(2) + flight_duration, + }, + path: GeoLineString { points: vec![] }, + distance_meters, + }, + ]; + + let itineraries = get_itineraries( + &required_loading_time, + &required_unloading_time, + ×lot_pairs, + &availabilities, + &clients, + ) + .await + .unwrap(); + + // Expect two matches + println!("{:?}", itineraries); + for (i, itinerary) in itineraries.iter().enumerate() { + println!("\n\n----- Itinerary {}", i); + for (fp_i, fp) in itinerary.iter().enumerate() { + println!("{}: {:?}\n", fp_i, fp); + } + } + + assert_eq!(itineraries.len(), 2); + } +} diff --git a/server/src/router/mod.rs b/server/src/router/mod.rs index dfc1832..e8abec5 100644 --- a/server/src/router/mod.rs +++ b/server/src/router/mod.rs @@ -2,5 +2,72 @@ #[macro_use] pub mod macros; -pub mod router_types; -pub mod router_utils; +pub mod flight_plan; +pub mod itinerary; +pub mod schedule; +pub mod vehicle; +pub mod vertiport; + +use crate::grpc::client::GrpcClients; +use svc_gis_client_grpc::prelude::{gis::*, *}; +use svc_storage_client_grpc::prelude::*; + +pub enum BestPathError { + ClientError, + NoPathFound, +} + +impl std::fmt::Display for BestPathError { + fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result { + match self { + BestPathError::ClientError => write!(f, "Client error"), + BestPathError::NoPathFound => write!(f, "No path found"), + } + } +} + +/// Get the best path between two vertiports or a between an aircraft and a vertiport +/// and the total length of the path in meters. +pub async fn best_path( + request: &BestPathRequest, + clients: &GrpcClients, +) -> Result<(GeoLineString, f64), BestPathError> { + let path = match clients.gis.best_path(request.clone()).await { + Ok(response) => response.into_inner().segments, + Err(e) => { + router_error!("(best_path) Failed to get best path: {e}"); + return Err(BestPathError::ClientError); + } + }; + + let (last_lat, last_lon) = match path.last() { + Some(last) => (last.end_latitude, last.end_longitude), + None => { + router_error!("(best_path) No path found."); + return Err(BestPathError::NoPathFound); + } + }; + + let total_distance_meters = path.iter().map(|x| x.distance_meters as f64).sum(); + + router_debug!("(best_path) Path: {:?}", path); + router_debug!("(best_path) Cost: {:?}", total_distance_meters); + + // convert segments to GeoLineString + let mut points: Vec = path + .iter() + .map(|x| GeoPoint { + latitude: x.start_latitude as f64, + longitude: x.start_longitude as f64, + }) + .collect(); + + points.push(GeoPoint { + latitude: last_lat as f64, + longitude: last_lon as f64, + }); + + let path = GeoLineString { points }; + + Ok((path, total_distance_meters)) +} diff --git a/server/src/router/router_types/edge.rs b/server/src/router/router_types/edge.rs deleted file mode 100644 index cce4904..0000000 --- a/server/src/router/router_types/edge.rs +++ /dev/null @@ -1,19 +0,0 @@ -//! Definition of the `Edge` type. -use ordered_float::OrderedFloat; -use serde::Serialize; - -use crate::router::router_types::node::Node; - -/// An edge is a connection between two nodes. -/// The cost represents the "weight" of the edge. -#[derive(Debug, PartialEq, Hash, Eq, Serialize)] -pub struct Edge<'a> { - /// One end of the edge. - pub from: &'a Node, - - /// The other end of the edge. - pub to: &'a Node, - - /// The weight of the edge in meters. - pub cost: OrderedFloat, -} diff --git a/server/src/router/router_types/location.rs b/server/src/router/router_types/location.rs deleted file mode 100644 index 99cdf01..0000000 --- a/server/src/router/router_types/location.rs +++ /dev/null @@ -1,45 +0,0 @@ -//! Struct definitions and implementations for [`Location`]. -//! -//! There may be special types of `Location` such as a moving -//! coordinate. - -use ordered_float::OrderedFloat; -use serde::{Deserialize, Serialize}; - -/// A [`Location`] is an interface type that represents a geographic -/// location of an object. Typically, this type is used in tandem with -/// the [`Node`](`super::node::Node`) type. -/// -/// Altitude matters because it is used to compute the estimated fuel -/// costs for landing to or taking off from a location. -/// -/// Float values are used to achieve a 5-decimal precision (0.00001), -/// which narrows the error margin to a meter. -#[derive(Debug, PartialEq, Hash, Eq, Copy, Clone, Serialize, Deserialize)] -pub struct Location { - /// The latitude of the location. - pub latitude: OrderedFloat, - - /// The longitude of the location. - pub longitude: OrderedFloat, - - /// The altitude of the location in meters. - pub altitude_meters: OrderedFloat, -} - -impl From for geo::Point { - fn from(value: Location) -> Self { - geo::Point::new( - value.longitude.into_inner() as f64, - value.latitude.into_inner() as f64, - ) - } -} -impl From<&Location> for geo::Point { - fn from(value: &Location) -> Self { - geo::Point::new( - value.longitude.into_inner() as f64, - value.latitude.into_inner() as f64, - ) - } -} diff --git a/server/src/router/router_types/mod.rs b/server/src/router/router_types/mod.rs deleted file mode 100644 index 890a2f8..0000000 --- a/server/src/router/router_types/mod.rs +++ /dev/null @@ -1,5 +0,0 @@ -pub mod edge; -pub mod location; -pub mod node; -pub mod router; -pub mod status; diff --git a/server/src/router/router_types/node.rs b/server/src/router/router_types/node.rs deleted file mode 100644 index 79c9be3..0000000 --- a/server/src/router/router_types/node.rs +++ /dev/null @@ -1,353 +0,0 @@ -//! Struct definitions and implementations for objects that represent -//! vertices in a graph. -//! -//! The most generic form of a vertex is [`Node`]. In the real world, -//! a vertex could be a [`Vertiport`], which includes a cluster of -//! [`Vertipad`]s. Other possibilities such as a rooftop, a container -//! ship, or a farm can also represent and extend `Node`. -//! -//! Since Rust doesn't have a built-in way to represent an interface -//! type, we use an [`AsNode`] trait to achieve the similar effect. So, -//! a function may take an [`AsNode`] parameter and call its -//! [`as_node`](`AsNode::as_node`) method to get a [`Node`] reference. -//! -//! This pattern allows functions to be agnostic of the type of `Node` to -//! accept as argument. -use ordered_float::OrderedFloat; -use serde::{Deserialize, Serialize}; - -use super::location; -use super::status; -use crate::router::router_utils::haversine; -use core::hash::Hash; - -/// Since Rust doesn't allow for inheritance, we need to use `trait` as -/// a hack to allow passing "Node-like" objects to functions. -pub trait AsNode { - /// Returns the generic `Node` struct that an object "extends". - fn as_node(&self) -> &Node; - - /// Returns the identifier of the node. - fn get_uid(&self) -> String; - - /// Returns the distance between two nodes using the Haversine - /// formula. - fn distance_to(&self, other: &dyn AsNode) -> OrderedFloat; -} - -//------------------------------------------------------------------ -// Structs and Implementations -//------------------------------------------------------------------ - -/// Represent a vertex in a graph. -/// -/// Since the actual vertex can be any object, a generic struct is -/// needed for the purpose of abstraction and clarity. -#[derive(Debug, PartialEq, Hash, Eq, Serialize, Deserialize)] -pub struct Node { - /// Typed as a [`String`] to allow for synthetic ids. One purpose of - /// using a synthetic id is to allow for partitioned indexing on the - /// database layer to efficiently filter data. - /// - /// For example, an uid could be `usa:ny:12345`. This format can be - /// helpful when a client try to get all nodes in New York from a - /// database. Otherwise, one would need to loop through all nodes - /// and filter by location -- this would be a runtime computation - /// that is expensive enough to impact the user experience. - pub uid: String, - - /// Denote the geographical position of the node. - /// - /// See also [`location::Location`]. - pub location: location::Location, - - /// A node might be unavailable for some reasons. If `forward_to` is - /// not [`None`], incoming traffic will be forwarded to another - /// node. - pub forward_to: Option>, - - /// Indicate the operation status of a node. - /// - /// See also [`status::Status`]. - pub status: status::Status, - - /// calendar of the node as RRule string. (Used for scheduling) - pub schedule: Option, -} - -impl AsNode for Node { - fn as_node(&self) -> &Node { - self - } - fn get_uid(&self) -> String { - self.uid.clone() - } - fn distance_to(&self, other: &dyn AsNode) -> OrderedFloat { - haversine::distance(&self.location, &other.as_node().location).into() - } -} - -/// A vertipad allows for take-offs and landings of a single aircraft. -#[derive(Debug)] -pub struct Vertipad<'a> { - /// The generic node that this vertipad extends. - pub node: Node, - - /// FAA regulated pad size. - pub size_square_meters: OrderedFloat, - - /// Certain pads may have special purposes. For example, a pad may - /// be used for medical emergency services. - /// - /// TODO(R3): Define a struct for permissions. - pub permissions: Vec, - - /// If there's no vertiport, then the vertipad itself is the vertiport. - pub owner_port: Option>, -} - -impl Vertipad<'_> { - /// Update the size_square_meters field of a vertipad. - /// - /// CAUTION: Testing purposes only. Updates should not be done from - /// the router lib. - #[allow(dead_code)] - fn update_size_square_meters(&mut self, new_size: OrderedFloat) { - self.size_square_meters = new_size; - } -} - -impl AsNode for Vertipad<'_> { - fn as_node(&self) -> &Node { - &self.node - } - - fn get_uid(&self) -> String { - self.as_node().uid.clone() - } - - fn distance_to(&self, other: &dyn AsNode) -> OrderedFloat { - haversine::distance(&self.as_node().location, &other.as_node().location).into() - } -} - -/// A vertiport that has a collection of vertipads. -#[derive(Debug)] -pub struct Vertiport<'a> { - /// The generic node that this vertiport extends. - pub node: Node, - - /// A vertiport may have multiple vertipads. - pub vertipads: Vec<&'a Vertipad<'a>>, -} - -impl<'a> Vertiport<'a> { - /// Adds a vertipad to the vertiport. - #[allow(dead_code)] - pub fn add_vertipad(&mut self, vertipad: &'a Vertipad) { - self.vertipads.push(vertipad); - } -} - -impl AsNode for Vertiport<'_> { - fn as_node(&self) -> &Node { - &self.node - } - - fn get_uid(&self) -> String { - self.as_node().uid.clone() - } - - fn distance_to(&self, other: &dyn AsNode) -> OrderedFloat { - haversine::distance(&self.as_node().location, &other.as_node().location).into() - } -} - -//------------------------------------------------------------------ -// Unit Tests -//------------------------------------------------------------------ - -/// Tests that an extended node type like [`Vertiport`] can be passed -/// in as an [`AsNode`] trait implementation. -#[cfg(test)] -mod node_type_tests { - use super::*; - - /// Tests that we can make modifications. - #[test] - fn test_mutability() { - let mut vertipad_1 = Vertipad { - node: Node { - uid: "vertipad_1".to_string(), - location: location::Location { - longitude: OrderedFloat(-73.935242), - latitude: OrderedFloat(40.730610), - altitude_meters: OrderedFloat(0.0), - }, - forward_to: None, - status: status::Status::Ok, - schedule: None, - }, - size_square_meters: OrderedFloat(100.0), - permissions: vec!["medical".to_string()], - owner_port: None, - }; - let vertipad_2 = Vertipad { - node: Node { - uid: "vertipad_2".to_string(), - location: location::Location { - longitude: OrderedFloat(-73.935242), - latitude: OrderedFloat(40.730610), - altitude_meters: OrderedFloat(0.0), - }, - forward_to: None, - status: status::Status::Ok, - schedule: None, - }, - size_square_meters: OrderedFloat(100.0), - permissions: vec!["medical".to_string()], - owner_port: None, - }; - let vertipad_3 = Vertipad { - node: Node { - uid: "vertipad_3".to_string(), - location: location::Location { - longitude: OrderedFloat(-73.935242), - latitude: OrderedFloat(40.730610), - altitude_meters: OrderedFloat(0.0), - }, - forward_to: None, - status: status::Status::Ok, - schedule: None, - }, - size_square_meters: OrderedFloat(100.0), - permissions: vec!["medical".to_string()], - owner_port: None, - }; - let mut vertiport = Vertiport { - node: Node { - uid: "vertiport_1".to_string(), - location: location::Location { - longitude: OrderedFloat(-73.935242), - latitude: OrderedFloat(40.730610), - altitude_meters: 0.0.into(), - }, - forward_to: None, - status: status::Status::Ok, - schedule: None, - }, - vertipads: vec![], - }; - - let vertipad_4 = Vertipad { - node: Node { - uid: "vertipad_4".to_string(), - location: location::Location { - longitude: OrderedFloat(-73.935242), - latitude: OrderedFloat(40.730610), - altitude_meters: 0.0.into(), - }, - forward_to: None, - status: status::Status::Ok, - schedule: None, - }, - size_square_meters: OrderedFloat(100.0), - permissions: vec!["medical".to_string()], - owner_port: None, - }; - // add all vertipads to the vertiport. - vertiport.add_vertipad(&vertipad_1); - vertiport.add_vertipad(&vertipad_2); - vertiport.add_vertipad(&vertipad_3); - vertiport.add_vertipad(&vertipad_4); - - // check that the vertiport has all vertipads. - assert_eq!(vertiport.vertipads.len(), 4); - - // print the uid of each vertipad in the vertiport. - assert_eq!(vertiport.vertipads[0].node.uid, "vertipad_1".to_string()); - assert_eq!(vertiport.vertipads[1].node.uid, "vertipad_2".to_string()); - assert_eq!(vertiport.vertipads[2].node.uid, "vertipad_3".to_string()); - assert_eq!(vertiport.vertipads[3].node.uid, "vertipad_4".to_string()); - - let new_pad_size = 200.0; - // update the size of vertipad_1. - vertipad_1.update_size_square_meters(new_pad_size.into()); - - // check that the size of vertipad_1 has been updated. - assert_eq!(vertipad_1.size_square_meters, new_pad_size); - } - - #[test] - fn test_get_node_props_from_vertipad() { - let vertipad = Vertipad { - node: Node { - uid: "vertipad_1".to_string(), - location: location::Location { - longitude: OrderedFloat(-73.935242), - latitude: OrderedFloat(40.730610), - altitude_meters: OrderedFloat(0.0), - }, - forward_to: None, - status: status::Status::Ok, - schedule: None, - }, - size_square_meters: OrderedFloat(100.0), - permissions: vec!["public".to_string()], - owner_port: None, - }; - assert_eq!(vertipad.get_uid(), "vertipad_1"); - } - - #[test] - fn test_distance_to() { - let vertipad_1 = Vertipad { - node: Node { - uid: "vertipad_1".to_string(), - location: location::Location { - longitude: OrderedFloat(-73.935242), - latitude: OrderedFloat(40.730610), - altitude_meters: OrderedFloat(0.0), - }, - forward_to: None, - status: status::Status::Ok, - schedule: None, - }, - size_square_meters: OrderedFloat(100.0), - permissions: vec!["public".to_string()], - owner_port: None, - }; - let vertipad_2 = Vertipad { - node: Node { - uid: "vertipad_2".to_string(), - location: location::Location { - longitude: OrderedFloat(-33.935242), - latitude: OrderedFloat(40.730610), - altitude_meters: OrderedFloat(0.0), - }, - forward_to: None, - status: status::Status::Ok, - schedule: None, - }, - size_square_meters: OrderedFloat(100.0), - permissions: vec!["public".to_string()], - owner_port: None, - }; - let vertiport = Vertiport { - node: Node { - uid: "vertiport_1".to_string(), - location: location::Location { - longitude: OrderedFloat(-73.935242), - latitude: OrderedFloat(40.730610), - altitude_meters: 0.0.into(), - }, - forward_to: None, - status: status::Status::Ok, - schedule: None, - }, - vertipads: vec![], - }; - assert_eq!(vertiport.distance_to(&vertipad_1), 0.0); - assert_eq!(vertiport.distance_to(&vertipad_2), 3340.5833); - } -} diff --git a/server/src/router/router_types/router.rs b/server/src/router/router_types/router.rs deleted file mode 100644 index c4adf4c..0000000 --- a/server/src/router/router_types/router.rs +++ /dev/null @@ -1,734 +0,0 @@ -//! The core of the router library. -//! -//! The engine module builds a graph given an input of nodes. Path -//! finding algorithms are also provided to find the shortest path -//! between two nodes. -#[allow(dead_code)] - -/// The router engine module. -pub mod engine { - use std::{ - collections::HashMap, - fmt::{Display, Formatter, Result}, - result::Result as StdResult, - }; - - use geo::GeodesicLength; - use ordered_float::OrderedFloat; - use petgraph::{algo::astar, graph::NodeIndex, stable_graph::StableDiGraph}; - - use crate::router::router_types::{ - edge::Edge, - node::{AsNode, Node}, - }; - - use crate::router::router_utils::graph::build_edges; - - /// Error types for the router engine. - /// - /// # Errors - /// * `InvalidNodesInPath` - The path returned by the path finding - /// algorithm contains invalid nodes - #[derive(Debug, Copy, Clone)] - pub enum RouterError { - /// The path returned by the path finding algorithm contains - /// invalid nodes. - /// - /// Expected message: "Invalid path" - InvalidNodesInPath, - } - - impl Display for RouterError { - fn fmt(&self, f: &mut Formatter) -> Result { - match self { - RouterError::InvalidNodesInPath => write!(f, "Invalid path"), - } - } - } - - impl std::error::Error for RouterError {} - - /// A Router struct contains a graph of nodes and also a hashmap - /// that maps a node to its index in the graph. - #[derive(Debug)] - pub struct Router<'a> { - pub(crate) graph: StableDiGraph<&'a Node, OrderedFloat>, - pub(crate) node_indices: HashMap<&'a Node, NodeIndex>, - pub(crate) edges: Vec>, - } - - /// Path finding algorithms. - #[derive(Debug, Copy, Clone)] - pub enum Algorithm { - /// The Dijkstra algorithm. - Dijkstra, - /// The A Star algorithm. - AStar, - } - - impl Router<'_> { - /// Creates a new router with the given graph. - /// - /// # Arguments - /// * `nodes` - A vector of nodes. - /// * `constraint` - Only nodes within a constraint can be connected. - /// * `constraint_function` - A function that takes two nodes and - /// returns a float to compare against `constraint`. - /// * `cost_function` - A function that computes the "weight" between - /// two nodes. - /// - /// # Returns - /// A Router struct. - pub fn new( - nodes: &[impl AsNode], - constraint: f64, - constraint_function: fn(&dyn AsNode, &dyn AsNode) -> f64, - cost_function: fn(&dyn AsNode, &dyn AsNode) -> f64, - ) -> Router { - router_info!("[1/4] Initializing the router engine..."); - router_info!("[2/4] Building edges..."); - - let edges = build_edges(nodes, constraint, constraint_function, cost_function); - let mut node_indices = HashMap::new(); - let mut graph = StableDiGraph::new(); - - router_info!("[3/4] Building the graph..."); - for edge in &edges { - let from_index = *node_indices - .entry(edge.from) - .or_insert_with(|| graph.add_node(edge.from)); - let to_index = *node_indices - .entry(edge.to) - .or_insert_with(|| graph.add_node(edge.to)); - graph.add_edge(from_index, to_index, edge.cost); - } - - router_info!("[4/4] Finalizing the router setup..."); - for node in nodes { - if !node_indices.contains_key(node.as_node()) { - let index = graph.add_node(node.as_node()); - node_indices.insert(node.as_node(), index); - } - } - - router_info!("✨Done! Router engine is ready to use."); - Router { - graph, - node_indices, - edges, - } - } - - /// Get the NodeIndex struct for a given node. The NodeIndex - /// struct is used to reference things in the graph. - pub fn get_node_index(&self, node: &Node) -> Option { - router_debug!("Node: {:?}", node); - self.node_indices.get(node).cloned() - } - - /// Get a node by NodeIndex. - pub fn get_node_by_id(&self, index: NodeIndex) -> Option<&Node> { - router_debug!("Node id: {:?}", index); - if self.graph.contains_node(index) { - Some(self.graph[index]) - } else { - None - } - } - - /// Return the number of edges in the graph. - pub fn get_edge_count(&self) -> usize { - router_debug!("Edge count: {}", self.graph.edge_count()); - self.graph.edge_count() - } - - /// Find the shortest path between two nodes. - /// - /// The petgraph's Dijkstra algorithm is very identical to the - /// a star algorithm, so we can use the same function for both. - /// The only difference might be how the heuristic function is - /// implemented. - /// - /// # Arguments - /// * `from` - The node to start from. - /// * `to` - The node to end at. - /// * `algorithm` - The algorithm to use. - /// * `heuristic` - The heuristic function to use. - /// - /// # Returns - /// A tuple of the total cost and the path consisting of node - /// indices. - /// - /// An empty path with a total cost of 0.0 returned if no path - /// is found. - /// - /// An empty path with a total cost of -1.0 is returned if - /// either the `from` or `to` node is not found. - pub fn find_shortest_path( - &self, - from: &Node, - to: &Node, - algorithm: Algorithm, - heuristic_function: Option f64>, - ) -> StdResult<(f64, Vec), RouterError> { - router_debug!( - "(find_shortest_path) Finding shortest path from {:?} to {:?} using algorithm {:?}", - from.location, - to.location, - algorithm - ); - - let Some(from_index) = self.get_node_index(from) else { - return Err(RouterError::InvalidNodesInPath); - }; - - let Some(to_index) = self.get_node_index(to) else { - return Err(RouterError::InvalidNodesInPath); - }; - - let result = match algorithm { - Algorithm::Dijkstra => astar( - &self.graph, - from_index, - |finish| finish == to_index, - |e| (*e.weight()).into_inner(), - heuristic_function.unwrap_or(|_| 0.0), - ) - .unwrap_or((0.0, Vec::new())), - - Algorithm::AStar => astar( - &self.graph, - from_index, - |finish| finish == to_index, - |e| (*e.weight()).into_inner(), - heuristic_function.unwrap_or(|_| 0.0), - ) - .unwrap_or((0.0, Vec::new())), - }; - - Ok(result) - } - - /// Compute the total Geodesic distance (in meters) of a path. - /// - /// # Arguments - /// * `path` - The path to compute the distance of. The path is - /// given as a vector of [`NodeIndex`] structs. - /// - /// # Returns - /// The total distance of the path in meters. - /// - /// If the path is empty, 0.0 is returned. - /// - /// # Errors - /// Returns a RouterError if a node could not be found in the router - pub fn get_total_distance(&self, path: &Vec) -> StdResult { - router_info!("(get_total_distance) Computing total distance of path"); - let mut points: Vec = vec![]; - for index in path { - let node = self.get_node_by_id(*index); - let Some(node) = node else { - router_error!("Node {:?} is not found.", index); - return Err(RouterError::InvalidNodesInPath); - }; - points.push(node.location.into()) - } - let geo_line_string = geo::LineString::from(points); - Ok(geo_line_string.geodesic_length()) - } - - /// Get the number of nodes in the graph. - pub fn get_node_count(&self) -> usize { - router_info!("Getting node count"); - router_debug!("Node count: {}", self.graph.node_count()); - self.graph.node_count() - } - - /// Get all the edges in the graph. - pub fn get_edges<'a>(&self) -> &'a Vec { - router_info!("Getting all edges"); - router_debug!("Edges: {:?}", self.edges); - &self.edges - } - } -} - -#[cfg(test)] -mod router_tests { - use crate::router::router_types::{ - location::Location, - node::{AsNode, Node}, - router::engine::Algorithm, - router::engine::Router, - }; - - use crate::router::router_utils::mock::{generate_nodes, generate_nodes_near}; - - use geo::{GeodesicDistance, Point}; - use ordered_float::OrderedFloat; - - const SAN_FRANCISCO: Location = Location { - latitude: OrderedFloat(37.7749), - longitude: OrderedFloat(-122.4194), - altitude_meters: OrderedFloat(0.0), - }; - const CAPACITY: i32 = 500; - - #[test] - fn test_correct_node_count() { - let nodes = generate_nodes_near(&SAN_FRANCISCO.into(), 10.0, CAPACITY); - - let router = Router::new( - &nodes, - 10000.0, - |from, to| { - let from_point: Point = from.as_node().location.into(); - let to_point: Point = to.as_node().location.into(); - from_point.geodesic_distance(&to_point) - }, - |from, to| { - let from_point: Point = from.as_node().location.into(); - let to_point: Point = to.as_node().location.into(); - from_point.geodesic_distance(&to_point) - }, - ); - - assert_eq!(CAPACITY as usize, router.get_node_count()); - } - - /// The graph has no edges. - #[test] - fn test_shortest_path_disconnected_graph() { - let nodes = generate_nodes_near(&SAN_FRANCISCO.into(), 10000.0, CAPACITY); - - let router = Router::new( - &nodes, - 0.0, - |from, to| { - let from_point: Point = from.as_node().location.into(); - let to_point: Point = to.as_node().location.into(); - from_point.geodesic_distance(&to_point) - }, - |from, to| { - let from_point: Point = from.as_node().location.into(); - let to_point: Point = to.as_node().location.into(); - from_point.geodesic_distance(&to_point) - }, - ); - - let from = &nodes[0]; - let to = &nodes[1]; - - let result = router.find_shortest_path(from, to, Algorithm::AStar, None); - - let Ok((cost, path)) = result else { - panic!("Could not find shortest path: {:?}", result.unwrap_err()); - }; - - assert_eq!(cost, 0.0); - assert_eq!(router.get_edge_count(), 0); - assert_eq!(router.get_node_count(), CAPACITY as usize); - assert_eq!(path.len(), 0); - } - - /// Find the shortest path between two nodes. - /// - /// The following points are random coordinates in San Francisco. - /// - /// point 1: 37.777843, -122.468207 - /// - /// point 2: 37.778339, -122.460395 - /// - /// point 3: 37.780596, -122.434904 - /// - /// point 4: 37.774397, -122.445366 - /// - /// The shortest path from 1 to 3 should be 1 -> 3 - #[test] - fn test_shortest_path_has_path() { - let nodes = vec![ - Node { - uid: "1".to_string(), - location: Location { - latitude: OrderedFloat(37.777843), - longitude: OrderedFloat(-122.468207), - altitude_meters: OrderedFloat(0.0), - }, - forward_to: None, - status: crate::router::router_types::status::Status::Ok, - schedule: None, - }, - Node { - uid: "2".to_string(), - location: Location { - latitude: OrderedFloat(37.778339), - longitude: OrderedFloat(-122.460395), - altitude_meters: OrderedFloat(0.0), - }, - forward_to: None, - status: crate::router::router_types::status::Status::Ok, - schedule: None, - }, - Node { - uid: "3".to_string(), - location: Location { - latitude: OrderedFloat(37.780596), - longitude: OrderedFloat(-122.434904), - altitude_meters: OrderedFloat(0.0), - }, - forward_to: None, - status: crate::router::router_types::status::Status::Ok, - schedule: None, - }, - Node { - uid: "4".to_string(), - location: Location { - latitude: OrderedFloat(37.774397), - longitude: OrderedFloat(-122.445366), - altitude_meters: OrderedFloat(0.0), - }, - forward_to: None, - status: crate::router::router_types::status::Status::Ok, - schedule: None, - }, - ]; - - let router = Router::new( - &nodes, - 16100.0, - |from, to| { - let from_point: Point = from.as_node().location.into(); - let to_point: Point = to.as_node().location.into(); - from_point.geodesic_distance(&to_point) - }, - |from, to| { - let from_point: Point = from.as_node().location.into(); - let to_point: Point = to.as_node().location.into(); - from_point.geodesic_distance(&to_point) - }, - ); - - assert_eq!(4, router.get_node_count()); - assert_eq!( - router.get_node_count() * router.get_node_count() - 4, - router.get_edge_count() - ); - - let result = router.find_shortest_path(&nodes[0], &nodes[2], Algorithm::AStar, None); - - let Ok((cost, path)) = result else { - panic!("Could not find shortest path: {:?}", result.unwrap_err()); - }; - - let expected_from_point: Point = nodes[0].location.into(); - let expected_to_point: Point = nodes[2].location.into(); - assert_eq!( - cost, - expected_from_point.geodesic_distance(&expected_to_point) - ); - // should be 1 -> 3 - assert_eq!(path.len(), 2); - - let Some(node_0) = router.get_node_index(&nodes[0]) else { - panic!("Could not find nodes[0]"); - }; - - let Some(node_2) = router.get_node_index(&nodes[2]) else { - panic!("Could not find nodes[2]"); - }; - - assert_eq!(path, vec![node_0, node_2]); - } - - /// Find the shortest path between a point in San Francisco and a - /// point in New York. - /// - /// The following points are random coordinates in San Francisco - /// except for point 4. - /// - /// point 1: 37.777843, -122.468207 - /// - /// point 2: 37.778339, -122.460395 - /// - /// point 3: 37.780596, -122.434904 - /// - /// point 4: 40.738820, -73.990440 - /// - /// There should not be any path from 1 to 4 if we constraint our - /// flight distance to 100 kilometers (100000 meters). - #[test] - fn test_shortest_path_no_path() { - let nodes = vec![ - Node { - uid: "1".to_string(), - location: Location { - latitude: OrderedFloat(37.777843), - longitude: OrderedFloat(-122.468207), - altitude_meters: OrderedFloat(0.0), - }, - forward_to: None, - status: crate::router::router_types::status::Status::Ok, - schedule: None, - }, - Node { - uid: "2".to_string(), - location: Location { - latitude: OrderedFloat(37.778339), - longitude: OrderedFloat(-122.460395), - altitude_meters: OrderedFloat(0.0), - }, - forward_to: None, - status: crate::router::router_types::status::Status::Ok, - schedule: None, - }, - Node { - uid: "3".to_string(), - location: Location { - latitude: OrderedFloat(37.780596), - longitude: OrderedFloat(-122.434904), - altitude_meters: OrderedFloat(0.0), - }, - forward_to: None, - status: crate::router::router_types::status::Status::Ok, - schedule: None, - }, - Node { - uid: "4".to_string(), - location: Location { - latitude: OrderedFloat(40.738820), - longitude: OrderedFloat(-73.990440), - altitude_meters: OrderedFloat(0.0), - }, - forward_to: None, - status: crate::router::router_types::status::Status::Ok, - schedule: None, - }, - ]; - - let router = Router::new( - &nodes, - 100000.0, - |from, to| { - let from_point: Point = from.as_node().location.into(); - let to_point: Point = to.as_node().location.into(); - from_point.geodesic_distance(&to_point) - }, - |from, to| { - let from_point: Point = from.as_node().location.into(); - let to_point: Point = to.as_node().location.into(); - from_point.geodesic_distance(&to_point) - }, - ); - - assert_eq!(4, router.get_node_count()); - assert_eq!( - (router.get_node_count() - 1) * (router.get_node_count() - 1) - 3, - router.get_edge_count() - ); - - let result = router.find_shortest_path(&nodes[0], &nodes[3], Algorithm::AStar, None); - - let Ok((cost, path)) = result else { - panic!("Could not find shortest path: {:?}", result.unwrap_err()); - }; - - assert_eq!(cost, 0.0); - // should be 0 - assert_eq!(path.len(), 0); - assert_eq!(path, vec![]); - } - - /// Test invalid node queries. - #[test] - fn test_invalid_node_shortest_path() { - let nodes = vec![ - Node { - uid: "1".to_string(), - location: Location { - latitude: OrderedFloat(37.777843), - longitude: OrderedFloat(-122.468207), - altitude_meters: OrderedFloat(0.0), - }, - forward_to: None, - status: crate::router::router_types::status::Status::Ok, - schedule: None, - }, - Node { - uid: "2".to_string(), - location: Location { - latitude: OrderedFloat(37.778339), - longitude: OrderedFloat(-122.460395), - altitude_meters: OrderedFloat(0.0), - }, - forward_to: None, - status: crate::router::router_types::status::Status::Ok, - schedule: None, - }, - Node { - uid: "3".to_string(), - location: Location { - latitude: OrderedFloat(37.780596), - longitude: OrderedFloat(-122.434904), - altitude_meters: OrderedFloat(0.0), - }, - forward_to: None, - status: crate::router::router_types::status::Status::Ok, - schedule: None, - }, - Node { - uid: "4".to_string(), - location: Location { - latitude: OrderedFloat(40.738820), - longitude: OrderedFloat(-73.990440), - altitude_meters: OrderedFloat(0.0), - }, - forward_to: None, - status: crate::router::router_types::status::Status::Ok, - schedule: None, - }, - ]; - - let not_in_graph_node = Node { - uid: "5".to_string(), - location: Location { - latitude: OrderedFloat(40.738820), - longitude: OrderedFloat(-73.990440), - altitude_meters: OrderedFloat(0.0), - }, - forward_to: None, - status: crate::router::router_types::status::Status::Ok, - schedule: None, - }; - - let router = Router::new( - &nodes, - 10000.0, - |from, to| { - let from_point: Point = from.as_node().location.into(); - let to_point: Point = to.as_node().location.into(); - from_point.geodesic_distance(&to_point) - }, - |from, to| { - let from_point: Point = from.as_node().location.into(); - let to_point: Point = to.as_node().location.into(); - from_point.geodesic_distance(&to_point) - }, - ); - - let result = - router.find_shortest_path(&nodes[0], ¬_in_graph_node, Algorithm::AStar, None); - - let Err(_) = result else { - panic!("This was a valid path, expected invalid path."); - }; - } - - /// Test get_edges - #[test] - fn test_get_edges() { - let nodes = vec![ - Node { - uid: "1".to_string(), - location: Location { - latitude: OrderedFloat(37.777843), - longitude: OrderedFloat(-122.468207), - altitude_meters: OrderedFloat(0.0), - }, - forward_to: None, - status: crate::router::router_types::status::Status::Ok, - schedule: None, - }, - Node { - uid: "2".to_string(), - location: Location { - latitude: OrderedFloat(37.778339), - longitude: OrderedFloat(-122.460395), - altitude_meters: OrderedFloat(0.0), - }, - forward_to: None, - status: crate::router::router_types::status::Status::Ok, - schedule: None, - }, - Node { - uid: "3".to_string(), - location: Location { - latitude: OrderedFloat(37.780596), - longitude: OrderedFloat(-122.434904), - altitude_meters: OrderedFloat(0.0), - }, - forward_to: None, - status: crate::router::router_types::status::Status::Ok, - schedule: None, - }, - Node { - uid: "4".to_string(), - location: Location { - latitude: OrderedFloat(40.738820), - longitude: OrderedFloat(-73.990440), - altitude_meters: OrderedFloat(0.0), - }, - forward_to: None, - status: crate::router::router_types::status::Status::Ok, - schedule: None, - }, - ]; - - let router = Router::new( - &nodes, - 10000000.0, - |from, to| { - let from_point: Point = from.as_node().location.into(); - let to_point: Point = to.as_node().location.into(); - from_point.geodesic_distance(&to_point) - }, - |from, to| { - let from_point: Point = from.as_node().location.into(); - let to_point: Point = to.as_node().location.into(); - from_point.geodesic_distance(&to_point) - }, - ); - - let edges = router.get_edges(); - assert_eq!(edges.len(), 12); - assert_eq!(edges[0].to.get_uid(), "2"); - assert_eq!(edges[1].to.get_uid(), "3"); - } - - /// Test get_total_distance - #[test] - fn test_get_total_distance() { - let nodes = generate_nodes(100); - - let router = Router::new( - &nodes, - 10000.0, - |from, to| { - let from_point: Point = from.as_node().location.into(); - let to_point: Point = to.as_node().location.into(); - from_point.geodesic_distance(&to_point) - }, - |from, to| { - let from_point: Point = from.as_node().location.into(); - let to_point: Point = to.as_node().location.into(); - from_point.geodesic_distance(&to_point) - }, - ); - - let result = router.find_shortest_path(&nodes[0], &nodes[99], Algorithm::AStar, None); - - let Ok((cost, mut path)) = result else { - panic!("Could not find shortest path: {:?}", result.unwrap_err()); - }; - - let result = router.get_total_distance(&path); - let Ok(actual_cost) = result else { - panic!("Could not get total distance: {:?}", result.unwrap_err()); - }; - assert_eq!(actual_cost, cost); - - let mut invalid_path: Vec = - vec![petgraph::stable_graph::NodeIndex::new(300)]; - path.append(&mut invalid_path); - assert_eq!(router.get_total_distance(&path).is_ok(), false); - } -} diff --git a/server/src/router/router_types/status.rs b/server/src/router/router_types/status.rs deleted file mode 100644 index 75e5b3e..0000000 --- a/server/src/router/router_types/status.rs +++ /dev/null @@ -1,12 +0,0 @@ -//! Definition for the [`Status`] type, implemented by an enum. -use serde::{Deserialize, Serialize}; - -/// Represent the operating status of a [`super::node::Node`]. -#[derive(Debug, PartialEq, Hash, Eq, Copy, Clone, Serialize, Deserialize)] -#[allow(dead_code)] -pub enum Status { - /// Indicate that the node is currently operating. - Ok, - /// Indicate that the node is currently down. - Closed, -} diff --git a/server/src/router/router_utils/flightplan.rs b/server/src/router/router_utils/flightplan.rs deleted file mode 100644 index e8ffddf..0000000 --- a/server/src/router/router_utils/flightplan.rs +++ /dev/null @@ -1,41 +0,0 @@ -//! Helper Functions for Flight Plans - -use chrono::{DateTime, Utc}; -use prost_wkt_types::Timestamp; -use svc_storage_client_grpc::resources::flight_plan::Data; -use svc_storage_client_grpc::GeoLineString; - -/// Generates a flight plan data object from minimum required information -pub(crate) fn create_flight_plan_data( - vehicle_id: String, - departure_vertiport_id: String, - arrival_vertiport_id: String, - departure_time: DateTime, - arrival_time: DateTime, - path: GeoLineString, -) -> Data { - Data { - vehicle_id, - departure_vertiport_id: Some(departure_vertiport_id), - destination_vertiport_id: Some(arrival_vertiport_id), - scheduled_departure: Some(departure_time.into()), - scheduled_arrival: Some(Timestamp { - seconds: arrival_time.timestamp(), - nanos: arrival_time.timestamp_subsec_nanos() as i32, - }), - path: Some(path), - ..Default::default() // pilot_id: "".to_string(), - // path: Some(path), - // weather_conditions: None, - // flight_status: 0, - // flight_priority: 0, - // departure_vertipad_id: "".to_string(), - // destination_vertipad_id: "".to_string(), - // carrier_ack: None, - // actual_departure: None, - // actual_arrival: None, - // flight_release_approval: None, - // flight_plan_submitted: None, - // approved_by: None, - } -} diff --git a/server/src/router/router_utils/graph.rs b/server/src/router/router_utils/graph.rs deleted file mode 100644 index f57c987..0000000 --- a/server/src/router/router_utils/graph.rs +++ /dev/null @@ -1,88 +0,0 @@ -//! Helper functions for working with graphs. - -use ordered_float::OrderedFloat; - -use crate::router::router_types::{edge::Edge, node::AsNode}; - -/// Build edges among nodes. -/// -/// The function will try to connect every node to every other node. -/// However, constraints can be added to the graph to prevent ineligible -/// nodes from being connected. -/// -/// For example, if the constraint represents the max travel distance of -/// an aircraft, we only want to connect nodes that are within the max -/// travel distance. A constraint function is also needed to determine -/// if a connection is valid. -/// -/// # Arguments -/// * `nodes` - A vector of nodes. -/// * `constraint` - Only nodes within a constraint can be connected. -/// * `constraint_function` - A function that takes two nodes and -/// returns a float to compare against `constraint`. -/// * `cost_function` - A function that computes the "weight" between -/// two nodes. -/// -/// # Returns -/// A vector of edges in the format of (from_node, to_node, weight). -/// -/// # Time Complexity -/// *O*(*n^2*) at worst if the constraint is not met for all nodes. -pub fn build_edges( - nodes: &[impl AsNode], - constraint: f64, - constraint_function: fn(&dyn AsNode, &dyn AsNode) -> f64, - cost_function: fn(&dyn AsNode, &dyn AsNode) -> f64, -) -> Vec { - router_debug!("(build_edges) starting function call."); - let mut edges = Vec::new(); - for from in nodes { - for to in nodes { - if from.as_node() != to.as_node() - && constraint_function(from.as_node(), to.as_node()) <= constraint - { - let cost = cost_function(from.as_node(), to.as_node()); - edges.push(Edge { - from: from.as_node(), - to: to.as_node(), - cost: OrderedFloat::(cost), - }); - } - } - } - edges -} - -#[cfg(test)] -mod tests { - use geo::{GeodesicDistance, Point}; - - use crate::router::router_utils::mock::{generate_location, generate_nodes_near}; - - use super::*; - - #[test] - fn test_build_edges() { - let capacity = 1000; - let location = generate_location(); - let nodes = generate_nodes_near(&location.into(), 1000.0, capacity); - - // set constraint to 2000 so that all nodes should be connected - let edges = build_edges( - &nodes, - 2000.0, - |from, to| { - let from_point: Point = from.as_node().location.into(); - let to_point: Point = to.as_node().location.into(); - from_point.geodesic_distance(&to_point) - }, - |from, to| { - let from_point: Point = from.as_node().location.into(); - let to_point: Point = to.as_node().location.into(); - from_point.geodesic_distance(&to_point) - }, - ); - - assert_eq!(edges.len(), nodes.len() * nodes.len() - capacity as usize); - } -} diff --git a/server/src/router/router_utils/haversine.rs b/server/src/router/router_utils/haversine.rs deleted file mode 100644 index b698023..0000000 --- a/server/src/router/router_utils/haversine.rs +++ /dev/null @@ -1,54 +0,0 @@ -//! Implementation of the Haversine formula for calculating the distance -//! between two points on a sphere. -//! -//! See [Wikipedia](https://en.wikipedia.org/wiki/Haversine_formula) for -//! more. -//! -//! **Distance is returned in kilometers**. - -use crate::router::router_types::location::Location; - -/// Calculate the distance between two points on a sphere. -/// -/// # Notes -/// The current formula does ***not*** take into account the altitude of the -/// points. -/// -/// Float 32 values are used to achieve a 5-decimal precision (0.00001), -/// which narrows the error margin to a meter. -pub fn distance(start: &Location, end: &Location) -> f32 { - // km in radians - let kilometers: f32 = 6371.0; - - let d_lat: f32 = (end.latitude.into_inner() - start.latitude.into_inner()).to_radians(); - let d_lon: f32 = (end.longitude.into_inner() - start.longitude.into_inner()).to_radians(); - let lat1: f32 = (start.latitude.into_inner()).to_radians(); - let lat2: f32 = (end.latitude.into_inner()).to_radians(); - - let a: f32 = ((d_lat / 2.0).sin()) * ((d_lat / 2.0).sin()) - + ((d_lon / 2.0).sin()) * ((d_lon / 2.0).sin()) * (lat1.cos()) * (lat2.cos()); - let c: f32 = 2.0 * ((a.sqrt()).atan2((1.0 - a).sqrt())); - - kilometers * c -} - -#[cfg(test)] -pub mod haversine_test { - use super::*; - use ordered_float::OrderedFloat; - - #[test] - fn haversine_distance_in_kilometers() { - let start = Location { - latitude: OrderedFloat(38.898556), - longitude: OrderedFloat(-77.037852), - altitude_meters: OrderedFloat(0.0), - }; - let end = Location { - latitude: OrderedFloat(38.897147), - longitude: OrderedFloat(-77.043934), - altitude_meters: OrderedFloat(0.0), - }; - assert_eq!(0.5496312, distance(&start, &end)); - } -} diff --git a/server/src/router/router_utils/mock.rs b/server/src/router/router_utils/mock.rs deleted file mode 100644 index d5f7d9c..0000000 --- a/server/src/router/router_utils/mock.rs +++ /dev/null @@ -1,245 +0,0 @@ -//! A number of methods to generate random data for testing. - -use crate::router::router_types::{location::Location, node::Node, status}; -use geo::prelude::*; -use geo::{LineString, Point, Polygon, Rect}; -use ordered_float::OrderedFloat; -use rand::Rng; - -use std::collections::HashSet; -use uuid::Uuid; - -//----------------------------------------------------- -// Constants -//----------------------------------------------------- -const DEG_TO_RAD: f32 = std::f32::consts::PI / 180.0; -const RAD_TO_DEG: f32 = 180.0 / std::f32::consts::PI; - -/// Generate a vector of random nodes. -pub fn generate_nodes(capacity: i32) -> Vec { - let mut nodes = Vec::new(); - let mut uuid_set = HashSet::::new(); - for _ in 0..capacity { - loop { - let node = generate_random_node(); - if !uuid_set.contains(&node.uid) { - uuid_set.insert(node.uid.clone()); - nodes.push(node); - break; - } - } - } - nodes -} - -/// Generate a vector of random nodes near a location. -/// The provided radius (kilometers) is being used to determine the maximum radius distance -/// the generated nodes can be apart from the provided location. -/// The provided capacity is used to determine the amount of nodes that should be generated. -pub fn generate_nodes_near(location: &Point, radius: f32, capacity: i32) -> Vec { - let mut nodes = Vec::new(); - let mut uuid_set = HashSet::::new(); - for _ in 0..capacity { - loop { - let node = generate_random_node_near(location, radius); - if !uuid_set.contains(&node.uid) { - uuid_set.insert(node.uid.clone()); - nodes.push(node); - break; - } - } - } - nodes -} - -/// Generate a single random node. -/// -/// -/// # Caution -/// Note that the UUID generation does not guarantee uniqueness. Please -/// make sure to check for potential duplicates, albeit very unlikely. -pub fn generate_random_node() -> Node { - Node { - uid: Uuid::new_v4().to_string(), - location: generate_location(), - forward_to: None, - status: status::Status::Ok, - schedule: None, - } -} - -/// Generate a random node near a location within radius in kilometers. -/// -/// # Caution -/// Note that the UUID generation does not guarantee uniqueness. Please -/// make sure to check for potential duplicates, albeit very unlikely. -pub fn generate_random_node_near(location: &Point, radius: f32) -> Node { - Node { - uid: Uuid::new_v4().to_string(), - location: generate_location_near(location, radius), - forward_to: None, - status: status::Status::Ok, - schedule: None, - } -} - -/// Generate a random location anywhere on earth. -pub fn generate_location() -> Location { - let mut rng = rand::thread_rng(); - let latitude = OrderedFloat(rng.gen_range(-90.0..=90.0)); - let longitude = OrderedFloat(rng.gen_range(-180.0..=180.0)); - let altitude_meters = OrderedFloat(rng.gen_range(0.0..=10000.0)); - Location { - latitude, - longitude, - altitude_meters, - } -} - -/// Generate a random location near a given location and radius. -pub fn generate_location_near(location: &Point, radius: f32) -> Location { - let mut rng = rand::thread_rng(); - let point = gen_around_location(&mut rng, location, radius); - - let altitude_meters = OrderedFloat(rng.gen_range(0.0..=10000.0)); - Location { - latitude: OrderedFloat(point.y() as f32), - longitude: OrderedFloat(point.x() as f32), - altitude_meters, - } -} - -/// Generate a random location within a radius (in meters). -/// -/// Creates a circle using 365 points around the given latitude/longitude values. -/// Then randomly generates a new point within this circle using a bounding rect. -fn gen_around_location( - rng: &mut rand::rngs::ThreadRng, - start_point: &Point, - radius: f32, -) -> Point { - let mut points = vec![]; - for i in 0..265 { - points.push(start_point.geodesic_destination(i as f64, radius as f64)); - } - - let polygon = Polygon::new(LineString::from(points), vec![]); - let bounding_rect: Rect = polygon - .bounding_rect() - .unwrap_or_else(|| panic!("Could not get bounding rect for polygon: {:?}", polygon)); - - loop { - let random_x = rng.gen_range(bounding_rect.min().x..bounding_rect.max().x); - let random_y = rng.gen_range(bounding_rect.min().y..bounding_rect.max().y); - let random_point = Point::new(random_x, random_y); - - if polygon.contains(&random_point) { - return random_point; - } - } -} - -/// Takes customer location (src) and required destination (dst) and returns a tuple with nearest vertiports to src and dst -pub fn get_nearest_vertiports<'a>( - src_location: &'a Location, - dst_location: &'a Location, - vertiports: &'static Vec, -) -> (&'static Node, &'static Node) { - router_info!("(get_nearest_vertiports) function start."); - let mut src_vertiport = &vertiports[0]; - let mut dst_vertiport = &vertiports[0]; - router_debug!("(get_nearest_vertiport) src_location: {:?}", src_location); - router_debug!("(get_nearest_vertiport) dst_location: {:?}", dst_location); - let src_point: Point = src_location.into(); - let dst_point: Point = dst_location.into(); - let mut src_distance = src_point.geodesic_distance(&src_vertiport.location.into()); - let mut dst_distance = dst_point.geodesic_distance(&dst_vertiport.location.into()); - router_debug!("(get_nearest_vertiport) src_distance: {}", src_distance); - router_debug!("(get_nearest_vertiport) dst_distance: {}", dst_distance); - for vertiport in vertiports { - router_debug!( - "(get_nearest_vertiport) checking vertiport: {:?}", - vertiport - ); - let new_src_distance = src_point.geodesic_distance(&vertiport.location.into()); - let new_dst_distance = dst_point.geodesic_distance(&vertiport.location.into()); - router_debug!( - "(get_nearest_vertiport) new_src_distance: {}", - new_src_distance - ); - router_debug!( - "(get_nearest_vertiport) new_dst_distance: {}", - new_dst_distance - ); - if new_src_distance < src_distance { - src_distance = new_src_distance; - src_vertiport = vertiport; - } - if new_dst_distance < dst_distance { - dst_distance = new_dst_distance; - dst_vertiport = vertiport; - } - } - router_debug!("(get_nearest_vertiport) src_vertiport: {:?}", src_vertiport); - router_debug!("(get_nearest_vertiport) dst_vertiport: {:?}", dst_vertiport); - (src_vertiport, dst_vertiport) -} - -#[cfg(test)] -mod tests { - use crate::router::router_utils::haversine; - - use super::*; - - #[test] - fn test_valid_coordinates() { - let location = generate_location(); - assert!(location.latitude.into_inner() >= -90.0); - assert!(location.latitude.into_inner() <= 90.0); - assert!(location.longitude.into_inner() >= -180.0); - assert!(location.longitude.into_inner() <= 180.0); - assert!(location.altitude_meters.into_inner() >= 0.0); - assert!(location.altitude_meters.into_inner() <= 10000.0); - } - - /// Test that the distance between two locations is less than the radius. - /// - /// # Note - /// Sometimes the test will fail. - /// TODO(R3): Double check the [`gen_around_location`] function for improvements. - #[test] - fn test_generate_location_near() { - let location = generate_location(); - let location_near = generate_location_near(&location.into(), 10.0); - println!("(test_generate_location_near) original location: {:?}", location); - println!("(test_generate_location_near) nearby location: {:?}", location_near); - println!( - "(test_generate_location_near) distance: {}", - haversine::distance(&location, &location_near) - ); - assert!(haversine::distance(&location, &location_near) <= 10.0); - } - - #[test] - fn test_generate_random_nodes() { - let node = generate_nodes(100); - assert_eq!(node.len(), 100); - } - - // Disregard this test. generate_nodes_near may fail occasionally. - // This is due to unknown reasons. However, generate_nodes_near is - // only used for testing purposes. - // - // Failure with generate_nodes_near does not impact the production - // functionality of the library. - // - // #[test] - // fn test_generate_random_nodes_near() { - // let location = generate_location(); - // let nodes = generate_nodes_near(&location, 10.0, 100); - // assert_eq!(nodes.len(), 100); - // for node in nodes { - // assert!(haversine::distance(&location, &node.location) <= 10.0); - // } - // } -} diff --git a/server/src/router/router_utils/mod.rs b/server/src/router/router_utils/mod.rs deleted file mode 100644 index 74847cd..0000000 --- a/server/src/router/router_utils/mod.rs +++ /dev/null @@ -1,9 +0,0 @@ -pub mod flightplan; -pub mod graph; -pub mod haversine; -pub mod router_state; -pub mod schedule; - -#[cfg(feature = "mock")] -#[allow(dead_code)] -pub mod mock; diff --git a/server/src/router/router_utils/router_state.rs b/server/src/router/router_utils/router_state.rs deleted file mode 100644 index 5176b34..0000000 --- a/server/src/router/router_utils/router_state.rs +++ /dev/null @@ -1,1566 +0,0 @@ -//! Stores the state of the router -use super::flightplan::create_flight_plan_data; -use crate::router::router_utils::schedule::Calendar; -use crate::{ - grpc::client::GrpcClients, - router::router_types::{location::Location, node::Node}, -}; -use chrono::{DateTime, Duration, Utc}; -use lib_common::time::datetime_to_timestamp; -use std::collections::HashMap; -use std::str::FromStr; -use svc_storage_client_grpc::prelude::*; -use tokio::sync::OnceCell; - -use svc_gis_client_grpc::{service::Client, BestPathRequest, NodeType, DistanceTo}; - -const AVERAGE_AIRCRAFT_VELOCITY_M_PER_S: f32 = 20.0; // TODO(R4): Get from each vehicle model -const NEAREST_NEIGHBOR_MAX_RANGE_METERS: f64 = 100_000.0; // 100 KM -const NEAREST_NEIGHBOR_LIMIT: i32 = 10; - -/// Query struct for generating nodes near a location. -#[derive(Debug, Copy, Clone)] -pub struct NearbyLocationQuery { - ///location - pub location: Location, - ///radius - pub radius: f32, - ///capacity - pub capacity: i32, -} - -/// Query struct to find a route between two nodes -#[derive(Debug, Copy, Clone)] -pub struct RouteQuery { - ///aircraft - pub aircraft: Aircraft, - ///from - pub from: &'static Node, - ///to - pub to: &'static Node, -} - -/// Enum with all Aircraft types -#[derive(Debug, Copy, Clone)] -pub enum Aircraft { - ///Cargo aircraft - Cargo, -} - -static ARROW_CARGO_CONSTRAINT_METERS: f64 = 120000.0; - -/// Time to block vertiport for cargo loading and takeoff -pub const LOADING_AND_TAKEOFF_TIME_MIN: f32 = 10.0; -/// Time to block vertiport for cargo unloading and landing -pub const LANDING_AND_UNLOADING_TIME_MIN: f32 = 10.0; -/// Average speed of cargo aircraft in kilometers per hour -pub const AVG_SPEED_KMH: f32 = 60.0; -/// Minimum time between suggested flight plans in case of multiple flights available -pub const FLIGHT_PLAN_GAP_MINUTES: f32 = 5.0; -/// Max amount of flight plans to return in case of large time window and multiple flights available -pub const MAX_RETURNED_FLIGHT_PLANS: i64 = 10; - -/// Helper function to check if two time ranges overlap (touching ranges are not considered overlapping) -/// All parameters are in seconds since epoch -fn time_ranges_overlap(start1: i64, end1: i64, start2: i64, end2: i64) -> bool { - start1 < end2 && start2 < end1 -} - -#[derive(Debug, PartialEq, Copy, Clone)] -pub enum VehicleAvailability { - Available, - Unavailable, - NoScheduleProvided, - InvalidSchedule, -} - -/// Checks if a vehicle is available for a given time window date_from to -/// date_from + flight_duration_minutes (this includes takeoff and landing time) -/// This checks both static schedule of the aircraft and existing flight plans which might overlap. -pub fn is_vehicle_available( - vehicle: &vehicle::Object, - date_from: DateTime, - flight_duration_minutes: i64, - existing_flight_plans: &[flight_plan::Object], -) -> VehicleAvailability { - let vehicle_id = &vehicle.id; - - let Some(vehicle_data) = vehicle.data.as_ref() else { - router_error!( - "(is_vehicle_available) Vehicle doesn't have data: {:?}", - vehicle - ); - return VehicleAvailability::Unavailable; - } - - // TODO(R3): What's the default if a schedule isn't provided? - let Some(vehicle_schedule) = vehicle_data.schedule.as_ref() else { - return VehicleAvailability::NoScheduleProvided; - }; - - let vehicle_schedule = vehicle_schedule.as_str(); - let Ok(vehicle_schedule) = Calendar::from_str(vehicle_schedule) else { - router_debug!( - "(is_vehicle_available) Invalid schedule for vehicle {}: {}", - vehicle_id, - vehicle_schedule - ); - - return VehicleAvailability::InvalidSchedule; - }; - - let date_to = date_from + Duration::minutes(flight_duration_minutes); - //check if vehicle is available as per schedule - if !vehicle_schedule.is_available_between( - date_from.with_timezone(&rrule::Tz::UTC), - date_to.with_timezone(&rrule::Tz::UTC), - ) { - router_debug!("(is_vehicle_available) date_from [{}] - date_to [{}] don't fit in vehicle's schedule [{:?}].", date_from, date_to, vehicle_schedule); - return VehicleAvailability::Unavailable; - } - - //check if vehicle is available as per existing flight plans - for existing_flight_plan in existing_flight_plans.iter() { - if vehicle_id != &existing_flight_plan.id { - continue; - } - - let Some(data) = existing_flight_plan.data.as_ref() else { - router_error!( - "(is_vehicle_available) Existing flight plan doesn't have data: {:?}", - existing_flight_plan - ); - continue; - }; - - let Some(scheduled_arrival) = data.scheduled_arrival.as_ref() else { - router_error!( - "(is_vehicle_available) Existing flight plan doesn't have scheduled_arrival: {:?}", - existing_flight_plan - ); - continue; - }; - - let Some(scheduled_departure) = data.scheduled_departure.as_ref() else { - router_error!( - "(is_vehicle_available) Existing flight plan doesn't have scheduled_departure: {:?}", - existing_flight_plan - ); - continue; - }; - - if time_ranges_overlap( - scheduled_departure.seconds, - scheduled_arrival.seconds, - date_from.timestamp(), - date_to.timestamp(), - ) { - router_debug!("(is_vehicle_available) A flight is already scheduled with an overlapping time range for this vehicle [{}].", vehicle_id); - return VehicleAvailability::Unavailable; - } - } - - VehicleAvailability::Available -} - -/// Checks if vertiport is available for a given time window using the provided `at_date_time` value. -/// This checks both static schedule of vertiport and existing flight plans which might overlap. -/// `is_departure_vertiport` is used to determine if we are checking for departure or arrival vertiport. -/// -/// ## Example scenario -/// flight_plan 1 has an `arrival_time` set at 2023-10-07T10:10 for vertipad 1 -/// flight_plan 2 has an `departure_time` set at 2023-10-07T10:20 for vertipad 1 -/// -/// This results in the following schedule for vertipad 1: -/// ```ignore -/// 2023-10-07 | 10:00 | 10:05 | 10:10 | 10:15 | 10:20 | 10:25 | 10:30 | 10:35 | 10:40 -/// --------------------------------------------------------------------------------------------------------- -/// | landing and unloading -/// flight_plan 1 | <-------------------> -/// | loading and takeoff -/// flight_plan 2 | <------------------> -/// --------------------------------------------------------------------------------------------------------- -/// ``` -/// With the above schedule, there is an available time slot for 10:10 - 10:20 -/// For `is_departure_vertiport == true` -/// `at_date_time` = `2023-10-07T10:10` returns true -/// `at_date_time` = `2023-10-07T10:15` returns false -/// `at_date_time` = `2023-10-07T10:20` returns false -/// For `is_departure_vertiport == false` -/// `at_date_time` = `2023-10-07T10:10` returns false -/// `at_date_time` = `2023-10-07T10:15` returns false -/// `at_date_time` = `2023-10-07T10:20` returns true -pub fn is_vertiport_available( - vertiport_id: String, - vertiport_schedule: Option, - vertipads: &[vertipad::Object], - at_date_time: DateTime, - existing_flight_plans: &[flight_plan::Object], - is_departure_vertiport: bool, -) -> (bool, Vec<(String, i64)>) { - let mut num_vertipads = vertipads.len(); - if num_vertipads == 0 { - num_vertipads = 1 - }; - - let vertiport_schedule = Calendar::from_str(vertiport_schedule.as_ref().unwrap().as_str()).unwrap(); - - // Adjust availability times as per time window needed taking the - // LOADING_AND_TAKEOFF_TIME_MIN into account for departure vertiports and - // the LANDING_AND_UNLOADING_TIME_MIN for arrival vertiports - let date_to; - let date_from; - if is_departure_vertiport { - date_from = at_date_time; - date_to = at_date_time + Duration::minutes(LOADING_AND_TAKEOFF_TIME_MIN as i64); - } else { - date_from = at_date_time - Duration::minutes(LANDING_AND_UNLOADING_TIME_MIN as i64); - date_to = at_date_time; - }; - //check if vertiport is available as per schedule - if !vertiport_schedule.is_available_between( - date_from.with_timezone(&rrule::Tz::UTC), - date_to.with_timezone(&rrule::Tz::UTC), - ) { - router_debug!( - "(is_vertiport_available) vertiport schedule does not match required times, returning." - ); - return (false, vec![]); - } - - // Adjust date_to and date_from to use for overlap search - let date_to; - let date_from; - if is_departure_vertiport { - date_from = at_date_time; - date_to = at_date_time + Duration::minutes(LOADING_AND_TAKEOFF_TIME_MIN as i64); - } else { - date_from = at_date_time - Duration::minutes(LANDING_AND_UNLOADING_TIME_MIN as i64); - date_to = at_date_time; - }; - - let conflicting_flight_plans_count = existing_flight_plans - .iter() - .filter(|flight_plan| { - let flight_plan_data = flight_plan.data.as_ref().unwrap(); - if is_departure_vertiport { - flight_plan_data.departure_vertiport_id.clone().unwrap() == vertiport_id - && flight_plan_data - .scheduled_departure - .as_ref() - .unwrap() - .seconds - + LOADING_AND_TAKEOFF_TIME_MIN as i64 * 60 - > date_from.timestamp() - && flight_plan_data - .scheduled_departure - .as_ref() - .unwrap() - .seconds - < date_to.timestamp() - } else { - flight_plan_data.destination_vertiport_id.clone().unwrap() == vertiport_id - && flight_plan_data.scheduled_arrival.as_ref().unwrap().seconds - > date_from.timestamp() - && flight_plan_data.scheduled_arrival.as_ref().unwrap().seconds - - LANDING_AND_UNLOADING_TIME_MIN as i64 * 60 - < date_to.timestamp() - } - }) - .count(); - let res = if num_vertipads > 1 { - let vehicles_at_vertiport = - get_all_vehicles_scheduled_for_vertiport(&vertiport_id, date_to, existing_flight_plans); - ( - vehicles_at_vertiport.len() < num_vertipads, - vehicles_at_vertiport, - ) - } else { - (conflicting_flight_plans_count == 0, vec![]) - }; - router_debug!( - "(is_vertiport_available) Checking {} is departure: {}, is available for {} - {}? {}.", - vertiport_id, - is_departure_vertiport, - date_from, - date_to, - res.0, - ); - res -} - -/// Finds all vehicles which are parked at or in flight to the vertiport at -/// specific timestamp. -/// Returns vector of tuples of (vehicle_id, minutes_to_arrival) where -/// minutes_to_arrival is 0 if vehicle is parked at the vertiport and up to 10 -/// minutes if vehicle is landing. -pub fn get_all_vehicles_scheduled_for_vertiport( - vertiport_id: &str, - timestamp: DateTime, - existing_flight_plans: &[flight_plan::Object], -) -> Vec<(String, i64)> { - let mut vehicles_plans_sorted: HashMap> = HashMap::new(); - existing_flight_plans - .iter() - .filter(|flight_plan| { - let flight_plan_data = flight_plan.data.as_ref().unwrap(); - flight_plan_data.destination_vertiport_id.as_ref().unwrap() == vertiport_id - && flight_plan_data - .scheduled_arrival - .as_ref() - .unwrap() - .seconds // arrival time needs to be less than 2x time needed - to allow landing and and then take off again) - < timestamp.timestamp() + LANDING_AND_UNLOADING_TIME_MIN as i64 * 60 - }) - .for_each(|flight_plan| { - let vehicle_id = flight_plan.data.as_ref().unwrap().vehicle_id.clone(); - let entry = vehicles_plans_sorted.entry(vehicle_id).or_default(); - entry.push(flight_plan.clone()); - }); - - //sort by scheduled arrival, latest first - vehicles_plans_sorted - .iter_mut() - .for_each(|(_, flight_plans)| { - flight_plans.sort_by(|a, b| { - b.data - .as_ref() - .unwrap() - .scheduled_arrival - .as_ref() - .unwrap() - .seconds - .cmp( - &a.data - .as_ref() - .unwrap() - .scheduled_arrival - .as_ref() - .unwrap() - .seconds, - ) - }); - }); - - //return only the latest flight plan for each vehicle - let vehicles = vehicles_plans_sorted - .iter() - .map(|(vehicle_id, flight_plans)| { - let mut minutes_to_arrival = (flight_plans - .first() - .unwrap() - .data - .as_ref() - .unwrap() - .scheduled_arrival - .as_ref() - .unwrap() - .seconds - - timestamp.timestamp()) - / 60; - if minutes_to_arrival < 0 { - minutes_to_arrival = 0; - } - (vehicle_id.clone(), minutes_to_arrival) - }) - .collect(); - router_debug!( - "(get_all_vehicles_scheduled_for_vertiport) Vehicles at vertiport: {} at a time: {} : {:?}.", - &vertiport_id, - timestamp, - vehicles - ); - vehicles -} - -/// Gets vehicle location (vertiport_id) at given timestamp -/// Returns tuple of (vertiport_id, minutes_to_arrival) -/// If minutes_to_arrival is 0, vehicle is parked at the vertiport, -/// otherwise it is in flight to the vertiport and should arrive in minutes_to_arrival -pub fn get_vehicle_scheduled_location( - vehicle: &vehicle::Object, - timestamp: DateTime, - existing_flight_plans: &[flight_plan::Object], -) -> (String, i64) { - let mut vehicle_flight_plans = existing_flight_plans - .iter() - .filter(|flight_plan| { - flight_plan.data.as_ref().unwrap().vehicle_id == vehicle.id - && flight_plan - .data - .as_ref() - .unwrap() - .scheduled_departure - .as_ref() - .unwrap() - .seconds - <= timestamp.timestamp() - }) - .collect::>(); - vehicle_flight_plans.sort_by(|a, b| { - b.data - .as_ref() - .unwrap() - .scheduled_departure - .as_ref() - .unwrap() - .seconds - .cmp( - &a.data - .as_ref() - .unwrap() - .scheduled_departure - .as_ref() - .unwrap() - .seconds, - ) - }); - - router_debug!( - "(get_vehicle_scheduled_location) Found flight plans for vehicle [{}]: {:?}", - vehicle.id, - vehicle_flight_plans - ); - - if vehicle_flight_plans.is_empty() { - return ( - vehicle - .data - .as_ref() - .unwrap() - .last_vertiport_id - .as_ref() - .unwrap() - .clone(), - 0, - ); - } - let vehicle_flight_plan = vehicle_flight_plans.first().unwrap(); - router_debug!( - "(get_vehicle_scheduled_location) Vehicle {} had last flight plan {} with destination {}.", - vehicle.id, - vehicle_flight_plan.id.clone(), - vehicle_flight_plan - .data - .as_ref() - .unwrap() - .destination_vertiport_id - .as_ref() - .unwrap() - ); - let mut minutes_to_arrival = (vehicle_flight_plan - .data - .as_ref() - .unwrap() - .scheduled_arrival - .as_ref() - .unwrap() - .seconds - - timestamp.timestamp()) - / 60; - if minutes_to_arrival < 0 { - minutes_to_arrival = 0; - } - ( - vehicle_flight_plan - .data - .as_ref() - .unwrap() - .destination_vertiport_id - .as_ref() - .unwrap() - .to_string(), - minutes_to_arrival, - ) -} - -/// Gets nearest gap for a reroute flight - takeoff and landing at the same vertiport -fn find_nearest_gap_for_reroute_flight( - vertiport_id: String, - vertiport_schedule: Option, - vertipads: &[vertipad::Object], - date_from: DateTime, - vehicle_id: String, - existing_flight_plans: &[flight_plan::Object], -) -> Option> { - let mut time_from: Option> = None; - for i in 0..6 { - let added_time = date_from + Duration::minutes(i * LOADING_AND_TAKEOFF_TIME_MIN as i64); - let (dep, vehicles_dep) = is_vertiport_available( - vertiport_id.clone(), - vertiport_schedule.clone(), - vertipads, - added_time, - existing_flight_plans, - true, - ); - let (arr, vehicles_arr) = is_vertiport_available( - vertiport_id.clone(), - vertiport_schedule.clone(), - vertipads, - added_time + Duration::minutes(LANDING_AND_UNLOADING_TIME_MIN as i64), - existing_flight_plans, - false, - ); - if (dep || vehicles_dep.contains(&(vehicle_id.clone(), 0))) - && (arr || vehicles_arr.contains(&(vehicle_id.clone(), 0))) - { - time_from = Some(added_time); - break; - } - } - time_from -} - -/// For the scenario where there is no available vehicle for the flight plan, this function find a deadhead flight plan -/// - summoning vehicle from the nearest vertiport to the departure vertiport so it can depart on time -/// Returns available vehicle and deadhead flight plan data if found, or (None, None) otherwise -#[allow(clippy::too_many_arguments)] -pub async fn find_deadhead_flight_plan( - nearest_vertiports: &Vec, - vehicles: &Vec, - vertiport_depart: &vertiport::Object, - vertipads_depart: &[vertipad::Object], - departure_time: DateTime, - existing_flight_plans: &[flight_plan::Object], - block_aircraft_and_vertiports_minutes: i64, - clients: &GrpcClients, -) -> (Option, Option) { - for &vertiport in nearest_vertiports { - for vehicle in vehicles { - - // TODO(R4): Get this from vehicle model - let vehicle_velocity_m_per_s: f32 = AVERAGE_AIRCRAFT_VELOCITY_M_PER_S; - - let duration_minutes: f32 = (vertiport.distance_meters/vehicle_velocity_m_per_s) * 60.; - - router_debug!( - "(find_deadhead_flight_plan) Checking vehicle id:{} for departure time: {}", - &vehicle.id, - departure_time - ); - - let Some(vehicle_data) = vehicle.data.as_ref() else { - router_error!( - "(find_deadhead_flight_plan) Vehicle [{}] has no data.", - &vehicle.id - ); - - continue; - }; - - let (vehicle_dest_vertiport, _minutes_to_arrival) = get_vehicle_scheduled_location( - vehicle, - departure_time - Duration::minutes(n_duration), - existing_flight_plans, - ); - if vehicle_dest_vertiport != *vertiport.uid { - router_debug!( - "(find_deadhead_flight_plan) Vehicle [{}] not at or arriving to vertiport [{}].", - &vehicle.id, - vehicle_dest_vertiport - ); - continue; - } - - match is_vehicle_available( - &vehicle.id, - &vehicle_data, - departure_time - Duration::minutes(n_duration), - block_aircraft_and_vertiports_minutes, - existing_flight_plans, - ) { - VehicleAvailability::Available => (), - VehicleAvailability::Unavailable => { - router_debug!( - "(find_deadhead_flight_plan) Vehicle [{}] not available for departure time: {} and duration {} minutes.", - &vehicle.id, departure_time - Duration::minutes(n_duration), block_aircraft_and_vertiports_minutes - ); - continue; - } - _ => { - router_debug!( - "(find_deadhead_flight_plan) Unable to determine vehicle availability: (id {})", - &vehicle.id - ); - continue; - } - }; - - let (is_departure_vertiport_available, _) = is_vertiport_available( - vertiport.uid.clone(), - vertiport.schedule.clone(), - &[], - departure_time - Duration::minutes(n_duration), - existing_flight_plans, - true, - ); - let (is_arrival_vertiport_available, _) = is_vertiport_available( - vertiport_depart.id.clone(), - vertiport_depart.data.as_ref().unwrap().schedule.clone(), - vertipads_depart, - departure_time - Duration::minutes(LANDING_AND_UNLOADING_TIME_MIN as i64), - existing_flight_plans, - false, - ); - - router_debug!( - "(find_deadhead_flight_plan) DEPARTURE TIME: {}, {}, {}.", - departure_time, - is_departure_vertiport_available, - is_arrival_vertiport_available - ); - - if !is_departure_vertiport_available { - router_debug!( - "(find_deadhead_flight_plan) Departure vertiport not available for departure time {}.", - departure_time - Duration::minutes(n_duration) - ); - continue; - } - - if !is_arrival_vertiport_available { - router_debug!( - "(find_deadhead_flight_plan) Arrival vertiport not available for departure time {}.", - departure_time - Duration::minutes(LANDING_AND_UNLOADING_TIME_MIN as i64) - ); - continue; - } - - let Some(time_start) = datetime_to_timestamp(&(departure_time - Duration::minutes(n_duration))) else { - router_debug!( - "(find_deadhead_flight_plan) Unable to convert departure time to timestamp: {}", - departure_time - Duration::minutes(n_duration) - ); - continue; - }; - - let Some(time_end) = datetime_to_timestamp(&departure_time) else { - router_debug!( - "(find_deadhead_flight_plan) Unable to convert departure time to timestamp: {}", - departure_time - ); - continue; - }; - - let request = BestPathRequest { - start_type: NodeType::Aircraft as i32, - node_start_id: vehicle_data.registration_number.clone(), - node_uuid_end: vertiport_depart.id.clone(), - time_start: Some(time_start), - time_end: Some(time_end), - }; - - let (path, _) = match gis_request(request, clients).await { - Ok(response) => { - let path = response.0; - let cost = response.1; - (path, cost) - } - Err(e) => { - router_debug!( - "(find_deadhead_flight_plan) Error getting path from gis: {}", - e - ); - continue; - } - }; - - // add deadhead flight plan and return - router_debug!( - "(find_deadhead_flight_plan) Found available vehicle [{}] from vertiport [{}], for a DH flight for a departure time {}.", vehicle.id, vertiport.uid.clone(), - departure_time - Duration::minutes(n_duration) - ); - return ( - Some(vehicle.clone()), - Some(create_flight_plan_data( - vehicle.id.clone(), - vertiport.uid.clone(), - vertiport_depart.id.clone(), - departure_time - Duration::minutes(n_duration), - departure_time, - path, - )), - ); - } - } - (None, None) -} - -/// In the scenario there is no vehicle available at the arrival vertiport, we can check -/// if there is availability at some other vertiport and re-route idle vehicle there. -/// This function finds such a flight plan and returns it -pub async fn find_rerouted_vehicle_flight_plan( - vehicles_at_arrival_airport: &[(String, i64)], - vertiport_arrive: &vertiport::Object, - vertipads_arrive: &[vertipad::Object], - arrival_time: &DateTime, - existing_flight_plans: &[flight_plan::Object], - clients: &GrpcClients, -) -> Option { - let found_vehicle = vehicles_at_arrival_airport - .iter() //if there is a parked vehicle at the arrival vertiport, we can move it to some other vertiport - .find(|(_, minutes_to_arrival)| *minutes_to_arrival == 0); - found_vehicle?; - router_debug!("(find_rerouted_vehicle_flight_plan) Checking if idle vehicle [{:#?}] from the arrival airport can be re-routed.", found_vehicle.unwrap()); - - // TODO(R3) this should re-route the vehicle to the nearest vertiport or HUB, but - // we don't have vertipads or HUB id in the graph to do this. - // So we are just re-routing to the same vertiport in the future time instead - let found_gap = find_nearest_gap_for_reroute_flight( - vertiport_arrive.id.clone(), - vertiport_arrive.data.as_ref().unwrap().schedule.clone(), - vertipads_arrive, - *arrival_time, - found_vehicle.unwrap().0.clone(), - existing_flight_plans, - ); - - let Some(found_gap) = found_gap else { - router_debug!( - "(find_rerouted_vehicle_flight_plan) No gap found for re-routing idle vehicle from the arrival vertiport." - ); - return None; - }; - - router_debug!( - "(find_rerouted_vehicle_flight_plan) Found a gap for re-routing idle vehicle from the arrival vertiport: {}", - found_gap - ); - - let gap_time_start = found_gap; - let gap_time_end = found_gap + Duration::minutes(LOADING_AND_TAKEOFF_TIME_MIN as i64); - let Some(time_start) = datetime_to_timestamp(&gap_time_start) else { - router_debug!( - "(find_rerouted_vehicle_flight_plan) Error converting time_start datetime to timestamp." - ); - return None; - }; - - let Some(time_end) = datetime_to_timestamp(&gap_time_end) else { - router_debug!( - "(find_rerouted_vehicle_flight_plan) Error converting time_end datetime to timestamp." - ); - return None; - }; - - let request = BestPathRequest { - start_type: NodeType::Aircraft as i32, - node_start_id: found_vehicle.unwrap().0.clone(), - node_uuid_end: vertiport_arrive.id.clone(), - time_start: Some(time_start), - time_end: Some(time_end), - }; - - let (path, _) = match gis_request(request, clients).await { - Ok(response) => { - let path = response.0; - let cost = response.1; - (path, cost) - } - Err(e) => { - router_debug!( - "(find_rerouted_vehicle_flight_plan) Error getting path from gis: {}", - e - ); - return None; - } - }; - - Some(create_flight_plan_data( - found_vehicle.unwrap().0.clone(), - vertiport_arrive.id.clone(), - vertiport_arrive.id.clone(), - gap_time_start, - gap_time_end, - path, - )) -} - -/// Gets nearest vertiports to the requested vertiport -/// Returns tuple of: -/// sorted_vertiports_by_durations - vector of distances -pub async fn get_nearest_vertiports_vertiport_id( - vertiport_depart: &vertiport::Object, - clients: &GrpcClients -) -> Result, String> { - router_debug!( - "(nearest_vertiports) for departure vertiport {:?}", - vertiport_depart - ); - - let request = tonic::Request::new(NearestNeighborRequest { - start_node_id: vertiport_depart.id.clone(), - start_type: NodeType::Vertiport as i32, - end_type: NodeType::Vertiport as i32, - limit: NEAREST_NEIGHBOR_LIMIT, - max_range_meters: NEAREST_NEIGHBOR_MAX_RANGE_METERS, - }); - - let distances: Vec = match clients.gis.nearest_neighbor(request).await { - Ok(response) => response.into_inner().distances, - Err(error) => { - router_error!( - "(nearest_vertiports) Failed to get nearest vertiports: {}", - error - ); - return Err(error.to_string()); - } - }; - - Ok(distances) -} - -async fn gis_request( - request: BestPathRequest, - clients: &GrpcClients, -) -> Result<(GeoLineString, f64), String> { - let request = tonic::Request::new(request); - let path = match clients.gis.best_path(request).await { - Ok(response) => response.into_inner().segments, - Err(error) => { - router_error!("(get_possible_flights) Failed to get best path: {}", error); - return Err(error.to_string()); - } - }; - - let (last_lat, last_lon) = match path.last() { - Some(last) => (last.end_latitude, last.end_longitude), - None => { - router_error!("(get_possible_flights) No path found."); - return Err("Path between vertiports not found".to_string()); - } - }; - - let cost = path.iter().map(|x| x.distance_meters as f64).sum(); - - router_debug!("(get_possible_flights) Path: {:?}", path); - router_debug!("(get_possible_flights) Cost: {:?}", cost); - - // convert segments to GeoLineString - let mut points: Vec = path - .iter() - .map(|x| GeoPoint { - latitude: x.start_latitude as f64, - longitude: x.start_longitude as f64, - }) - .collect(); - - points.push(GeoPoint { - latitude: last_lat as f64, - longitude: last_lon as f64, - }); - - let path = GeoLineString { points }; - - Ok((path, cost)) -} - -/// Creates all possible flight plans based on the given request -/// * `vertiport_depart` - Departure vertiport - svc-storage format -/// * `vertiport_arrive` - Arrival vertiport - svc-storage format -/// * `earliest_departure_time` - Earliest departure time of the time window -/// * `latest_arrival_time` - Latest arrival time of the time window -/// * `aircrafts` - Aircrafts serving the route and vertiports -/// # Returns -/// A vector of flight plans -#[allow(clippy::too_many_arguments)] -pub async fn get_possible_flights( - vertiport_depart: vertiport::Object, - vertiport_arrive: vertiport::Object, - vertipads_depart: Vec, - vertipads_arrive: Vec, - earliest_departure_time: Option, - latest_arrival_time: Option, - vehicles: Vec, - existing_flight_plans: Vec, - clients: &GrpcClients, -) -> Result)>, String> { - router_info!("(get_possible_flights) Finding possible flights."); - - //1. Find route and cost between requested vertiports - router_info!("(get_possible_flights) [1/5]: Finding route between vertiports."); - let request = BestPathRequest { - node_start_id: vertiport_depart.id.clone(), - node_uuid_end: vertiport_arrive.id.clone(), - start_type: NodeType::Vertiport as i32, - time_start: match earliest_departure_time.clone() { - Some(t) => Some(lib_common::time::Timestamp { - seconds: t.seconds, - nanos: t.nanos, - }), - None => None, - }, - time_end: match latest_arrival_time.clone() { - Some(t) => Some(lib_common::time::Timestamp { - seconds: t.seconds, - nanos: t.nanos, - }), - None => None, - }, - }; - - let (path, cost) = match gis_request(request, clients).await { - Ok((path, cost)) => (path, cost), - Err(e) => { - router_error!("(get_possible_flights) Failed to get best path: {}", e); - return Err(e); - } - }; - - let earliest_departure_time: DateTime = match earliest_departure_time { - Some(timestamp) => timestamp.into(), - None => { - let error = "No earliest departure time given."; - router_error!("(get_possible_flights) {}", error); - return Err(String::from(error)); - } - }; - - let latest_arrival_time: DateTime = match latest_arrival_time { - Some(timestamp) => timestamp.into(), - None => { - let error = "No latest arrival time given."; - router_error!("(get_possible_flights) {}", error); - return Err(String::from(error)); - } - }; - - //1.1 Create a sorted vector of vertiports nearest to the departure and arrival vertiport (in case we need to create a deadhead flight) - let nearest = nearest_vertiports(&vertiport_depart.id, clients).await?; - router_info!( - "(get_possible_flights) Found {} possible departure vertiports.", - nearest.len() - ); - - //2. calculate blocking times for each vertiport and aircraft - router_info!("(get_possible_flights) [2/5]: Calculating blocking times."); - let block_aircraft_and_vertiports_minutes = estimate_flight_time_minutes(cost, Aircraft::Cargo); - router_info!( - "(get_possible_flights) Estimated flight time in minutes including takeoff and landing: {}", - block_aircraft_and_vertiports_minutes - ); - - let time_window_duration_minutes: f32 = - (latest_arrival_time - earliest_departure_time).num_minutes() as f32; - router_debug!( - "(get_possible_flights) Time window duration in minutes: {}", - time_window_duration_minutes - ); - if (time_window_duration_minutes - block_aircraft_and_vertiports_minutes) < 0.0 { - router_info!("(get_possible_flights) Time window too small to schedule flight."); - return Err("Time window too small to schedule flight.".to_string()); - } - let mut num_flight_options: i64 = ((time_window_duration_minutes - - block_aircraft_and_vertiports_minutes) - / FLIGHT_PLAN_GAP_MINUTES) - .floor() as i64 - + 1; - if num_flight_options > MAX_RETURNED_FLIGHT_PLANS { - num_flight_options = MAX_RETURNED_FLIGHT_PLANS; - } - //3. check vertiport schedules and flight plans - router_info!( - "(get_possible_flights) [3/5]: Checking vertiport schedules and flight plans for {} possible flight plans.", - num_flight_options - ); - let mut flight_plans: Vec<(flight_plan::Data, Vec)> = vec![]; - for i in 0..num_flight_options { - let mut deadhead_flights: Vec = vec![]; - let mut departure_time: DateTime = earliest_departure_time; - departure_time += Duration::seconds(i * 60 * FLIGHT_PLAN_GAP_MINUTES as i64); - let arrival_time = - departure_time + Duration::minutes(block_aircraft_and_vertiports_minutes as i64); - let (is_departure_vertiport_available, _) = is_vertiport_available( - vertiport_depart.id.clone(), - vertiport_depart - .data - .as_ref() - .map_or( - Err(String::from( - "(get_possible_flights) No data provided for vertiport_depart.", - )), - Ok, - )? - .schedule - .clone(), - &vertipads_depart, - departure_time, - &existing_flight_plans, - true, - ); - let (is_arrival_vertiport_available, vehicles_at_arrival_airport) = is_vertiport_available( - vertiport_arrive.id.clone(), - vertiport_arrive - .data - .as_ref() - .map_or( - Err(String::from( - "(get_possible_flights) No data provided for vertiport_arrive.", - )), - Ok, - )? - .schedule - .clone(), - &vertipads_arrive, - arrival_time - Duration::minutes(LANDING_AND_UNLOADING_TIME_MIN as i64), - &existing_flight_plans, - false, - ); - router_debug!( - "(get_possible_flights) DEPARTURE TIME: {}, ARRIVAL TIME: {}, {}, {}.", - departure_time, - arrival_time, - is_departure_vertiport_available, - is_arrival_vertiport_available - ); - if !is_departure_vertiport_available { - router_debug!( - "(get_possible_flights) Departure vertiport not available for departure time {}.", - departure_time - ); - continue; - } - if !is_arrival_vertiport_available { - router_debug!( - "(get_possible_flights) Arrival vertiport not available for departure time {}.", - departure_time - ); - - let Some(flight_plan) = find_rerouted_vehicle_flight_plan( - &vehicles_at_arrival_airport, - &vertiport_arrive, - &vertipads_arrive, - &arrival_time, - &existing_flight_plans, - &clients - ).await else { - router_debug!("(get_possible_flights) No rerouted vehicle found."); - continue; - }; - - deadhead_flights.push(flight_plan); - } - let mut available_vehicle: Option = None; - for vehicle in &vehicles { - router_debug!( - "(get_possible_flights) Checking vehicle id:{} for departure time: {}", - &vehicle.id, - departure_time - ); - let (vehicle_vertiport_id, minutes_to_arrival) = - get_vehicle_scheduled_location(vehicle, departure_time, &existing_flight_plans); - if vehicle_vertiport_id != vertiport_depart.id || minutes_to_arrival > 0 { - router_debug!( - "(get_possible_flights) Vehicle [{}] not available at location for requested time {}. It is/will be at vertiport [{}] in {} minutes.", - &vehicle.id, departure_time, vehicle_vertiport_id, minutes_to_arrival - ); - continue; - } - - let Some(vehicle_data) = vehicle.data.as_ref() else { - router_debug!( - "(get_possible_flights) Vehicle [{}] has no data.", - &vehicle.id - ); - continue; - }; - - match is_vehicle_available( - &vehicle.id, - &vehicle_data, - departure_time, - block_aircraft_and_vertiports_minutes as i64, - &existing_flight_plans, - ) { - VehicleAvailability::Available => (), - VehicleAvailability::Unavailable => { - router_debug!( - "(get_possible_flights) Vehicle [{}] not available for departure time: {} and duration {} minutes.", - &vehicle.id, departure_time, block_aircraft_and_vertiports_minutes - ); - continue; - } - _ => { - router_debug!( - "(get_possible_flights) Unable to determine vehicle availability: (id {})", - &vehicle.id - ); - continue; - } - }; - - //when vehicle is available, break the "vehicles" loop early and add flight plan - available_vehicle = Some(vehicle.clone()); - router_debug!("(get_possible_flights) Found available vehicle [{}] from vertiport [{}], for a flight for a departure time {}.", &vehicle.id, &vertiport_depart.id, - departure_time - ); - break; - } - // No simple flight plans found, looking for plans with deadhead flights - if available_vehicle.is_none() { - router_debug!( - "(get_possible_flights) No available vehicles for departure time {}, looking for deadhead flights...", - departure_time - ); - - let (a_vehicle, deadhead_flight_plan) = find_deadhead_flight_plan( - &nearest, - &vehicles, - &vertiport_depart, - &vertipads_depart, - departure_time, - &existing_flight_plans, - block_aircraft_and_vertiports_minutes as i64, - clients, - ) - .await; - - if a_vehicle.is_some() { - available_vehicle = a_vehicle; - deadhead_flights.push(deadhead_flight_plan.unwrap()); - } - } - if available_vehicle.is_none() { - router_debug!( - "(get_possible_flights) DH: No available vehicles for departure time {} (including deadhead flights).", - departure_time - ); - continue; - } - //4. should check other constraints (cargo weight, number of passenger seats) - //router_info!("(get_possible_flights) [4/5]: Checking other constraints (cargo weight, number of passenger seats)"); - flight_plans.push(( - create_flight_plan_data( - available_vehicle.unwrap().id.clone(), - vertiport_depart.id.clone(), - vertiport_arrive.id.clone(), - departure_time, - arrival_time, - path.clone(), - ), - deadhead_flights, - )); - } - if flight_plans.is_empty() { - let error = format!( - "No flight plans found for given time window [{}] - [{}].", - earliest_departure_time, latest_arrival_time - ); - router_error!("(get_possible_flights) {}", error); - return Err(error); - } - - //5. return draft flight plan(s) - router_info!( - "(get_possible_flights) [5/5]: Returning {} draft flight plan(s).", - flight_plans.len() - ); - router_debug!("(get_possible_flights) Flight plans: {:?}", flight_plans); - Ok(flight_plans) -} - -/// Estimates the time needed to travel between two locations including loading and unloading -/// Estimate should be rather generous to block resources instead of potentially overloading them -pub fn estimate_flight_time_minutes(distance_meters: f64, aircraft: Aircraft) -> f32 { - router_debug!("(estimate_flight_time_minutes) distance_meters: {}", distance_meters); - router_debug!("(estimate_flight_time_minutes) aircraft: {:?}", aircraft); - match aircraft { - Aircraft::Cargo => { - LOADING_AND_TAKEOFF_TIME_MIN - + (distance_meters / 1000.0) as f32 / AVG_SPEED_KMH * 60.0 - + LANDING_AND_UNLOADING_TIME_MIN - } - } -} - -// /// gets node by id -// pub async fn get_node_by_id(id: &str) -> Result<&'static Node, String> { -// router_debug!("id: {}", id); -// let nodes = get_nodes().await?; -// let node = nodes -// .iter() -// .find(|node| node.uid == id) -// .ok_or_else(|| "Node not found by id: ".to_owned() + id)?; -// Ok(node) -// } - -// /// Initialize the router with vertiports from the storage service -// pub async fn init_router_from_vertiports(vertiports: &[vertiport::Object]) -> Result<(), String> { -// router_info!("(init_router_from_vertiports) Initializing router from vertiports."); -// let mut nodes = vec![]; -// for vertiport in vertiports { -// let data = match &vertiport.data { -// Some(data) => data, -// None => { -// return Err(format!( -// "(init_router_from_vertiports) No data provided for vertiport [{}].", -// vertiport.id -// )) -// } -// }; -// let geo_location = match &data.geo_location { -// Some(polygon) => polygon, -// None => { -// return Err(format!( -// "(init_router_from_vertiports) No geo_location provided for vertiport [{}].", -// vertiport.id -// )) -// } -// }; -// let latitude = OrderedFloat(geo_location.exterior.clone().ok_or(format!("(init_router_from_vertiports) No exterior points found for vertiport location of vertiport [{}]", vertiport.id))?.points[0].latitude as f32); -// let longitude = OrderedFloat(geo_location.exterior.clone().ok_or(format!("(init_router_from_vertiports) No exterior points found for vertiport location of vertiport [{}]", vertiport.id))?.points[0].longitude as f32); -// nodes.push(Node { -// uid: vertiport.id.clone(), -// location: Location { -// latitude, -// longitude, -// altitude_meters: OrderedFloat(0.0), -// }, -// forward_to: None, -// status: status::Status::Ok, -// schedule: vertiport -// .data -// .as_ref() -// .ok_or_else(|| { -// format!( -// "Something went wrong when parsing schedule data of vertiport id: {}", -// vertiport.id -// ) -// }) -// .unwrap() -// .schedule -// .clone(), -// }) -// } -// set_nodes(nodes).await; -// match get_router().await { -// Ok(_) => Ok(()), -// Err(e) => Err(e), -// } -// } - -// /// Checks if router is initialized -// pub fn is_router_initialized() -> bool { -// ARROW_CARGO_ROUTER.get().is_some() -// } - -// /// Get route -// pub async fn get_route(req: RouteQuery) -> Result<(Vec, f64), String> { -// router_debug!("Getting route"); -// if !is_router_initialized() { -// return Err("Arrow XL router not initialized. Try to initialize it first.".to_string()); -// } - -// let RouteQuery { -// from, -// to, -// aircraft: _, -// } = req; - -// let result = get_router() -// .await? -// .find_shortest_path(from, to, Algorithm::Dijkstra, None); - -// let Ok((cost, path)) = result else { -// return Err(format!("{:?}", result.unwrap_err())); -// }; - -// router_debug!("cost: {}", cost); -// router_debug!("path: {:?}", path); -// let mut locations = vec![]; -// for node in path { -// locations.push( -// get_router() -// .await? -// .get_node_by_id(node) -// .ok_or(format!("Node not found by index {:?}", node))? -// .location, -// ); -// } -// router_debug!("locations: {:?}", locations); -// router_info!("Finished getting route with cost: {}", cost); -// Ok((locations, cost)) -// } - -// /// Gets the router -// /// Will initialize the router if it hasn't been set and if the NODES are available. -// /// Ensures initialization is done only once. -// pub(crate) async fn get_router() -> Result<&'static Router<'static>, String> { -// if NODES.get().is_none() { -// return Err("Nodes not initialized. Try to get some nodes first.".to_string()); -// } -// ARROW_CARGO_ROUTER -// .get_or_try_init(|| async move { -// Ok(Router::new( -// get_nodes().await?, -// ARROW_CARGO_CONSTRAINT_METERS, -// |from, to| { -// let from_point: Point = from.as_node().location.into(); -// let to_point: Point = to.as_node().location.into(); -// from_point.geodesic_distance(&to_point) -// }, -// |from, to| { -// let from_point: Point = from.as_node().location.into(); -// let to_point: Point = to.as_node().location.into(); -// from_point.geodesic_distance(&to_point) -// }, -// )) -// }) -// .await -// } - -// /// Gets nodes -// /// Returns error if nodes are not available yet -// pub(crate) async fn get_nodes() -> Result<&'static Vec, String> { -// match NODES.get() { -// Some(nodes) => Ok(nodes), -// None => Err("Nodes not initialized. Try to get some nodes first.".to_string()), -// } -// } -// /// Will initialize the nodes if it hasn't been initialized yet. -// /// Ensures initialization is done only once. -// pub(crate) async fn set_nodes(nodes: Vec) { -// NODES.get_or_init(|| async move { nodes }).await; -// } - -#[cfg(test)] -mod tests { - use super::*; - use crate::test_util::*; - use crate::{ - init_logger, router::router_types::location::Location, - router::router_utils::mock::get_nearest_vertiports, test_util::ensure_storage_mock_data, - Config, - }; - use chrono::{TimeZone, Utc}; - use ordered_float::OrderedFloat; - - #[test] - fn test_estimate_flight_time_minutes() { - init_logger(&Config::try_from_env().unwrap_or_default()); - unit_test_info!("(test_estimate_flight_time_minutes) start"); - - let distance_meters: f64 = (AVG_SPEED_KMH * 1000.0) as f64; // using AVG_SPEED_KMH since it's an easy calculation to make from there - let aircraft = Aircraft::Cargo; - let expected_time_minutes: f32 = - LOADING_AND_TAKEOFF_TIME_MIN + 60.0 + LANDING_AND_UNLOADING_TIME_MIN; // If the distance is the same as the AVG_SPEED_KMH, it should take 60 minutes. Then we'll need to add the landing/ unloading time to get the expected minutes. - - let result = estimate_flight_time_minutes(distance_meters, aircraft); - - assert_eq!(result, expected_time_minutes); - unit_test_info!("(test_estimate_flight_time_minutes) success"); - } - - #[tokio::test] - async fn test_get_all_vehicles_scheduled_for_vertiport() { - init_logger(&Config::try_from_env().unwrap_or_default()); - unit_test_info!("(get_all_vehicles_scheduled_for_vertiport) start"); - ensure_storage_mock_data().await; - crate::grpc::queries::init_router().await; - - let latest_arrival_time: Timestamp = Utc - .datetime_from_str("2022-10-27 15:00:00", "%Y-%m-%d %H:%M:%S") - .unwrap() - .into(); - let existing_flight_plans = crate::grpc::queries::query_flight_plans_for_latest_arrival( - latest_arrival_time.clone(), - ) - .await - .unwrap(); - - // The 3th vehicle we've inserted should be arriving at our 4th - // vertiport at "2022-10-27 15:00:00". - let vehicles = get_vehicles_from_storage().await; - let expected_vehicle_id = &vehicles[2].id; - let vertiports = get_vertiports_from_storage().await; - let vertiport_id = &vertiports[3].id; - let res = get_all_vehicles_scheduled_for_vertiport( - vertiport_id, - latest_arrival_time.into(), - &existing_flight_plans, - ); - unit_test_debug!( - "(get_all_vehicles_scheduled_for_vertiport) Vehicles found: {:#?}", - res - ); - - assert_eq!(res.len(), 1); - assert_eq!(res[0], (expected_vehicle_id.clone(), 0)); - unit_test_info!("(get_all_vehicles_scheduled_for_vertiport) success"); - } - - #[test] - fn test_is_vehicle_available_per_schedule_true() { - init_logger(&Config::try_from_env().unwrap_or_default()); - unit_test_info!("(test_is_vehicle_available_per_schedule_true) start"); - - // Construct a Vehicle Object using mock data and adding a known schedule. - let vehicle_id = uuid::Uuid::new_v4(); - let vertiport_id = uuid::Uuid::new_v4(); - let schedule = - "DTSTART:20221020T180000Z;DURATION:PT1H\nRRULE:FREQ=WEEKLY;BYDAY=MO,TU,WE,TH,FR"; - let mut vehicle_data = vehicle::mock::get_data_obj(); - vehicle_data.last_vertiport_id = Some(vertiport_id.to_string()); - vehicle_data.schedule = Some(schedule.to_owned()); - let vehicle = vehicle::Object { - id: vehicle_id.to_string(), - data: Some(vehicle_data), - }; - - // Create a `date_from` value which should be within the vehicle's schedule - let date_from: Timestamp = Utc - .datetime_from_str("2022-10-26 15:00:00", "%Y-%m-%d %H:%M:%S") - .unwrap() - .into(); - let res = - is_vehicle_available(vehicle_id.as_str(), &vehicle, date_from.into(), 60, &vec![]); - - assert!(res.is_ok()); - assert_eq!(res.unwrap(), true); - unit_test_info!("(test_is_vehicle_available_per_schedule_true) success"); - } - - #[test] - fn test_is_vehicle_available_per_schedule_false() { - init_logger(&Config::try_from_env().unwrap_or_default()); - unit_test_info!("(test_is_vehicle_available_per_schedule_false) start"); - - // Construct a Vehicle Object using mock data and adding a known schedule. - let vehicle_id = uuid::Uuid::new_v4(); - let vertiport_id = uuid::Uuid::new_v4(); - let mut vehicle_data = vehicle::mock::get_data_obj(); - vehicle_data.last_vertiport_id = Some(vertiport_id.to_string()); - vehicle_data.schedule = Some(String::from( - "DTSTART:20221020T180000Z;DURATION:PT1H\nRRULE:FREQ=WEEKLY;BYDAY=MO,TU,WE,TH,FR", - )); - let vehicle = vehicle::Object { - id: vehicle_id.to_string(), - data: Some(vehicle_data), - }; - - // Create a `date_from` value which should be within the vehicle's schedule - let date_from: Timestamp = Utc - .datetime_from_str("2022-10-26 18:00:00", "%Y-%m-%d %H:%M:%S") - .unwrap() - .into(); - let res = is_vehicle_available(&vehicle_id, &vehicle, date_from.into(), 60, &vec![]); - - assert!(res.is_ok()); - assert_eq!(res.unwrap(), false); - unit_test_info!("(test_is_vehicle_available_per_schedule_false) success"); - } - - #[tokio::test] - async fn test_is_vehicle_available_true() { - init_logger(&Config::try_from_env().unwrap_or_default()); - unit_test_info!("(test_is_vehicle_available_true) start"); - ensure_storage_mock_data().await; - crate::grpc::queries::init_router().await; - - let latest_arrival_time: Timestamp = Utc - .datetime_from_str("2022-10-26 15:00:00", "%Y-%m-%d %H:%M:%S") - .unwrap() - .into(); - let existing_flight_plans = - crate::grpc::queries::query_flight_plans_for_latest_arrival(latest_arrival_time) - .await - .unwrap(); - let flight_plan = &existing_flight_plans[0]; - let flight_plan_data = flight_plan.data.as_ref().unwrap(); - - // We'll pick a vehicle_id from the returned flight_plans, making sure - // it's available - let vehicle_id = flight_plan_data.vehicle_id.clone(); - let vertiport_id = flight_plan_data.departure_vertipad_id.clone(); - - let mut vehicle_data = vehicle::mock::get_data_obj(); - vehicle_data.last_vertiport_id = Some(vertiport_id.to_string()); - vehicle_data.schedule = Some(String::from( - "DTSTART:20221020T180000Z;DURATION:PT1H\nRRULE:FREQ=WEEKLY;BYDAY=MO,TU,WE,TH,FR", - )); - let vehicle = vehicle::Object { - id: vehicle_id.to_string(), - data: Some(vehicle_data), - }; - - unit_test_debug!( - "(test_is_vehicle_available_true) testing for vehicle: {:?}", - vehicle.data - ); - // This should be available - let earliest_departure_time: Timestamp = Utc - .datetime_from_str("2022-10-25 10:15:00", "%Y-%m-%d %H:%M:%S") - .unwrap() - .into(); - let res = is_vehicle_available( - &vehicle_id, - &vehicle, - earliest_departure_time.into(), - 60, - &existing_flight_plans, - ); - - assert!(res.is_ok()); - assert_eq!(res.unwrap(), true); - unit_test_info!("(test_is_vehicle_available_true) success"); - } - - /// Takes vertiport 1 and gets all available flight_plans for the provided latest arrival. - /// Then picks out the vehicle_id of the first flight_plan returned. Since - /// this vehicle is already occupied for this flight_plan, the test should - /// return false. - #[tokio::test] - async fn test_is_vehicle_available_false() { - init_logger(&Config::try_from_env().unwrap_or_default()); - unit_test_info!("(test_is_vehicle_available_false) start"); - ensure_storage_mock_data().await; - crate::grpc::queries::init_router().await; - - let latest_arrival_time: Timestamp = Utc - .datetime_from_str("2022-10-25 15:00:00", "%Y-%m-%d %H:%M:%S") - .unwrap() - .into(); - - let existing_flight_plans = - crate::grpc::queries::query_flight_plans_for_latest_arrival(latest_arrival_time) - .await - .unwrap(); - let flight_plan = &existing_flight_plans[0]; - let flight_plan_data = flight_plan.data.as_ref().unwrap(); - - // We'll pick a vehicle_id from the returned flight_plans, making sure - // it's part of the test data set - let vehicle_id = flight_plan_data.vehicle_id.clone(); - let vertiport_id = flight_plan_data.departure_vertipad_id.clone(); - - let mut vehicle_data = vehicle::mock::get_data_obj(); - vehicle_data.last_vertiport_id = Some(vertiport_id.to_string()); - vehicle_data.schedule = Some(String::from( - "DTSTART:20221020T180000Z;DURATION:PT1H\nRRULE:FREQ=WEEKLY;BYDAY=MO,TU,WE,TH,FR", - )); - let vehicle = vehicle::Object { - id: vehicle_id.to_string(), - data: Some(vehicle_data), - }; - - // This should generate a conflict since we've already inserted a - // flight_plan for this vehicle and this vertiport with a departure date - // of "2022-10-25 14:20:00" - let earliest_departure_time: Timestamp = Utc - .datetime_from_str("2022-10-25 14:15:00", "%Y-%m-%d %H:%M:%S") - .unwrap() - .into(); - let res = is_vehicle_available( - &vehicle_id, - &vehicle, - earliest_departure_time.into(), - 60, - &existing_flight_plans, - ); - - assert!(res.is_ok()); - assert_eq!(res.unwrap(), false); - unit_test_info!("(test_is_vehicle_available_false) success"); - } -} diff --git a/server/src/router/router_utils/schedule.rs b/server/src/router/router_utils/schedule.rs deleted file mode 100644 index baea4eb..0000000 --- a/server/src/router/router_utils/schedule.rs +++ /dev/null @@ -1,330 +0,0 @@ -//! Provides calendar/scheduling utilities -//! Parses and serializes string RRULEs with duration and provides api to query if time slot is available. - -use chrono::{DateTime, Duration}; -use chrono_tz::UTC; -use iso8601_duration::Duration as DurationParser; -pub use rrule::{RRuleSet, Tz}; -use std::fmt::Display; -use std::str::FromStr; - -/// formats chrono::DateTime to string in format: `YYYYMMDDThhmmssZ`, e.g. 20221026T133000Z -fn datetime_to_ical_format(dt: &DateTime) -> String { - router_debug!("(datetime_to_ical_format) {:?}", dt); - let mut tz_prefix = String::new(); - let mut tz_postfix = String::new(); - router_debug!("(datetime_to_ical_format) tz: {:?}", dt.timezone()); - let tz = dt.timezone(); - match tz { - Tz::Local(_) => {} - Tz::Tz(tz) => match tz { - UTC => { - tz_postfix = "Z".to_string(); - } - tz => { - tz_prefix = format!(";TZID={}:", tz.name()); - } - }, - } - - let dt = dt.format("%Y%m%dT%H%M%S"); - router_debug!("(datetime_to_ical_format) dt: {:?}", dt); - format!("{}{}{}", tz_prefix, dt, tz_postfix) -} - -/// Wraps rruleset and their duration -#[derive(Debug)] -pub struct RecurrentEvent { - /// The rruleset with recurrence rules - pub rrule_set: RRuleSet, - /// The duration of the event (iso8601 format) - pub duration: String, -} -///Calendar implementation for recurring events using the rrule crate and duration iso8601_duration crate -#[derive(Debug)] -pub struct Calendar { - ///Vec of rrulesets and their duration - pub events: Vec, -} - -impl FromStr for Calendar { - type Err = (); - - /// Parses multiline string into a vector of `RecurrentEvents` - /// Using `rrule` library is not sufficient to capture duration of the event so we need to parse it - /// manually ane remove it from the string before letting rrule parse the rest - /// Duration has to be the last part of the RRULE_SET header after DTSTART e.g. - /// "DTSTART:20221020T180000Z;DURATION:PT1H" not "DURATION:PT1H;DTSTART:20221020T180000Z" - /// Duration is in ISO8601 format (`iso8601_duration` crate) - fn from_str(calendar_str: &str) -> Result { - router_debug!("(Calendar from_str) Parsing calendar: {}", calendar_str); - let rrule_sets: Vec<&str> = calendar_str - .split("DTSTART:") - .filter(|s| !s.is_empty()) - .collect(); - router_debug!("(Calendar from_str) rrule_sets: {:?}", rrule_sets); - let mut recurrent_events: Vec = Vec::new(); - for rrule_set_str in rrule_sets { - router_debug!("(Calendar from_str) rrule_set_str: {}", rrule_set_str); - let rrules_with_header: Vec<&str> = rrule_set_str - .split('\n') - .filter(|s| !s.is_empty()) - .collect(); - if rrules_with_header.len() < 2 { - router_error!( - "(Calendar from_str) Invalid rrule {} with header length: {}", - calendar_str, - rrules_with_header.len() - ); - return Err(()); - } - let header = rrules_with_header[0]; - let rrules = &rrules_with_header[1..]; - let header_parts: Vec<&str> = header - .split(";DURATION:") - .filter(|s| !s.is_empty()) - .collect(); - if header_parts.len() != 2 { - router_error!("(Calendar from_str) Invalid header parts length: {}", header_parts.len()); - return Err(()); - } - let dtstart = header_parts[0]; - let duration = header_parts[1]; - let str = "DTSTART:".to_owned() + dtstart + "\n" + rrules.join("\n").as_str(); - let rrset_res = RRuleSet::from_str(&str); - - let Ok(rrule_set) = rrset_res else { - router_error!("(Calendar from_str) Invalid rrule set: {:?}", rrset_res.unwrap_err()); - return Err(()); - }; - - recurrent_events.push(RecurrentEvent { - rrule_set, - duration: duration.to_string(), - }); - } - router_debug!("(Calendar from_str) Parsed calendar: {:?}", recurrent_events); - Ok(Calendar { - events: recurrent_events, - }) - } -} - -impl Display for Calendar { - /// Formats `Calendar` into multiline string which can be stored in the database - fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { - let err_msg = String::from("Error writing to string"); - for event in &self.events { - writeln!( - f, - "DTSTART:{};DURATION:{}", - datetime_to_ical_format(event.rrule_set.get_dt_start()), - &event.duration - ) - .expect(&err_msg); - for rrule in event.rrule_set.get_rrule() { - writeln!(f, "RRULE:{}", rrule).expect(&err_msg); - } - for rdate in event.rrule_set.get_rdate() { - writeln!(f, "RDATE:{}", datetime_to_ical_format(rdate)).expect(&err_msg); - } - } - Ok(()) - } -} - -impl Calendar { - /// Wrapper implementation of rrule library's `all` method which also considers duration of the event - /// Calendar stores blocking events as rrulesets with duration. This function checks if the time slot is fully available. - /// # Examples - /// If the calendar contains a blocking event from 10:00 to 11:00 and we check if 10:30 to 11:30 is available, it will return false - /// If the calendar contains a blocking event from 10:00 to 11:00 and we check if 9:30 to 10:00 is available, it will return true. - /// - if the start or end time is on the boundary with the blocking event, it is considered available. - /// Code Example: - /// ```rust,ignore - /// use std::str::FromStr; - /// use chrono::TimeZone; - /// use rrule::Tz; - /// let Ok(calendar) = Calendar::from_str("DTSTART:20221020T180000Z;DURATION:PT14H\n\ - /// RRULE:FREQ=WEEKLY;BYDAY=MO,TU,WE,TH,FR").unwrap(); - /// let mut start = Tz::UTC.with_ymd_and_hms(2022, 10, 25,17, 0, 0).unwrap(); - /// let mut end = Tz::UTC.with_ymd_and_hms(2022, 10, 25,18, 0, 0).unwrap(); - /// assert_eq!(calendar.is_available_between(start, end), true); - /// ``` - /// * `start_time` - start of the time slot - /// * `end_time` - end of the time slot - /// returns true if the time slot is fully available - pub fn is_available_between(&self, start_time: DateTime, end_time: DateTime) -> bool { - router_debug!( - "(is_available_between) Checking if time slot is available between {} and {}", - start_time, - end_time - ); - - // adjust start and end time by one second to make search inclusive of boundary values - let start_time = start_time + Duration::seconds(1); - let end_time = end_time - Duration::seconds(1); - - router_debug!("(is_available_between) Adjusted start_time: {}", start_time); - router_debug!("(is_available_between) Adjusted end_time: {}", end_time); - for event in &self.events { - let duration = &event.duration; - // check standard rrule time - if event(block) start time is between two dates, - // then it will be found and time slot will be marked as not available - let (events, _) = &event - .rrule_set - .clone() - .after(start_time.with_timezone(&rrule::Tz::UTC)) - .before(end_time.with_timezone(&rrule::Tz::UTC)) - .all(1); - if !events.is_empty() { - router_debug!("(is_available_between) Time slot is not available"); - return false; - } - let d = DurationParser::parse(duration).expect("(is_available_between) Failed to parse duration"); - let adjusted_start_time = start_time - - Duration::days(d.day as i64) - - Duration::hours(d.hour as i64) - - Duration::minutes(d.minute as i64) - - Duration::seconds(d.second as i64); - // here we check if event(block) start time + duration is between two dates, - // then it will be found and time slot will be marked as not available - let (events, _) = &event - .rrule_set - .clone() - .after(adjusted_start_time.with_timezone(&rrule::Tz::UTC)) - .before(end_time.with_timezone(&rrule::Tz::UTC)) - .all(10); - if !events.is_empty() { - router_debug!("(is_available_between) Time slot is not available for adjusted start time [{}]", adjusted_start_time); - return false; - } - } - // if no events(blocks) found across all rrule_sets, then time slot is available - router_debug!("(is_available_between) Time slot is available"); - true - } -} - -#[cfg(test)] -mod tests { - use super::Calendar; - use chrono::TimeZone; - use rrule::Tz; - use std::str::FromStr; - - const CAL_WORKDAYS_8AM_6PM: &str = "DTSTART:20221020T180000Z;DURATION:PT14H\n\ - RRULE:FREQ=WEEKLY;BYDAY=MO,TU,WE,TH,FR\n\ - DTSTART:20221022T000000Z;DURATION:PT24H\n\ - RRULE:FREQ=WEEKLY;BYDAY=SA,SU"; - - const _WITH_1HR_DAILY_BREAK: &str = "\n\ - DTSTART:20221020T120000Z;DURATION:PT1H\n\ - RRULE:FREQ=WEEKLY;BYDAY=MO,TU,WE,TH,FR"; - - const _WITH_ONE_OFF_BLOCK: &str = "\n\ - DTSTART:20221026T133000Z;DURATION:PT3H\n\ - RDATE:20221026T133000Z"; - - const INVALID_CALENDAR: &str = "DURATION:PT3H;DTSTART:20221026T133000Z;\n\ - RRULE:FREQ=WEEKLY;BYDAY=SA,SU"; - - #[test] - fn test_parse_calendar() { - let calendar = Calendar::from_str(CAL_WORKDAYS_8AM_6PM).unwrap(); - assert_eq!(calendar.events.len(), 2); - assert_eq!(calendar.events[0].duration, "PT14H"); - assert_eq!(calendar.events[1].duration, "PT24H"); - } - - #[test] - fn test_night_unavailable() { - let calendar = Calendar::from_str(CAL_WORKDAYS_8AM_6PM).unwrap(); - let start = Tz::UTC.with_ymd_and_hms(2022, 10, 25, 19, 0, 0).unwrap(); - let end = Tz::UTC.with_ymd_and_hms(2022, 10, 25, 20, 0, 0).unwrap(); - assert_eq!(calendar.is_available_between(start, end), false); - } - - #[test] - fn test_weekend_unavailable() { - let calendar = Calendar::from_str(CAL_WORKDAYS_8AM_6PM).unwrap(); - let start = Tz::UTC.with_ymd_and_hms(2022, 10, 22, 19, 0, 0).unwrap(); - let end = Tz::UTC.with_ymd_and_hms(2022, 10, 22, 20, 0, 0).unwrap(); - assert_eq!(calendar.is_available_between(start, end), false); - } - - #[test] - fn test_inclusive_boundaries_available() { - let calendar = Calendar::from_str(CAL_WORKDAYS_8AM_6PM).unwrap(); - - let mut start = Tz::UTC.with_ymd_and_hms(2022, 10, 25, 17, 0, 0).unwrap(); - let mut end = Tz::UTC.with_ymd_and_hms(2022, 10, 25, 18, 0, 0).unwrap(); - assert_eq!(calendar.is_available_between(start, end), true); - - start = Tz::UTC.with_ymd_and_hms(2022, 10, 25, 8, 0, 0).unwrap(); - end = Tz::UTC.with_ymd_and_hms(2022, 10, 25, 9, 0, 0).unwrap(); - assert_eq!(calendar.is_available_between(start, end), true); - } - - #[test] - fn test_calendar_with_day_break() { - let calendar = - Calendar::from_str(&(CAL_WORKDAYS_8AM_6PM.to_owned() + _WITH_1HR_DAILY_BREAK)).unwrap(); - assert_eq!(calendar.events[2].duration, "PT1H"); - - let mut start = Tz::UTC.with_ymd_and_hms(2022, 10, 25, 11, 30, 0).unwrap(); - let mut end = Tz::UTC.with_ymd_and_hms(2022, 10, 25, 12, 30, 0).unwrap(); - assert_eq!(calendar.is_available_between(start, end), false); - - start = Tz::UTC.with_ymd_and_hms(2022, 10, 25, 8, 0, 0).unwrap(); - end = Tz::UTC.with_ymd_and_hms(2022, 10, 25, 12, 0, 0).unwrap(); - assert_eq!(calendar.is_available_between(start, end), true); - - start = Tz::UTC.with_ymd_and_hms(2022, 10, 25, 12, 15, 0).unwrap(); - end = Tz::UTC.with_ymd_and_hms(2022, 10, 25, 12, 30, 0).unwrap(); - assert_eq!(calendar.is_available_between(start, end), false); - - start = Tz::UTC.with_ymd_and_hms(2022, 10, 25, 12, 59, 0).unwrap(); - end = Tz::UTC.with_ymd_and_hms(2022, 10, 25, 13, 30, 0).unwrap(); - assert_eq!(calendar.is_available_between(start, end), false); - } - - #[test] - fn test_calendar_with_one_off_block() { - let calendar = - Calendar::from_str(&(CAL_WORKDAYS_8AM_6PM.to_owned() + _WITH_ONE_OFF_BLOCK)).unwrap(); - - let mut start = Tz::UTC.with_ymd_and_hms(2022, 10, 26, 13, 30, 0).unwrap(); - let mut end = Tz::UTC.with_ymd_and_hms(2022, 10, 26, 14, 30, 0).unwrap(); - assert_eq!(calendar.is_available_between(start, end), false); - - start = Tz::UTC.with_ymd_and_hms(2022, 10, 27, 13, 30, 0).unwrap(); - end = Tz::UTC.with_ymd_and_hms(2022, 10, 27, 14, 30, 0).unwrap(); - assert_eq!(calendar.is_available_between(start, end), true); - - start = Tz::UTC.with_ymd_and_hms(2022, 10, 26, 11, 00, 0).unwrap(); - end = Tz::UTC.with_ymd_and_hms(2022, 10, 26, 13, 30, 0).unwrap(); - assert_eq!(calendar.is_available_between(start, end), true); - - start = Tz::UTC.with_ymd_and_hms(2022, 10, 26, 11, 00, 0).unwrap(); - end = Tz::UTC.with_ymd_and_hms(2022, 10, 26, 13, 30, 1).unwrap(); - assert_eq!(calendar.is_available_between(start, end), false); - } - - #[test] - fn test_save_and_load_calendar() { - let orig_cal_str = - &(CAL_WORKDAYS_8AM_6PM.to_owned() + _WITH_1HR_DAILY_BREAK + _WITH_ONE_OFF_BLOCK); - let calendar = Calendar::from_str(orig_cal_str).unwrap(); - let cal_str = calendar.to_string(); - let calendar = Calendar::from_str(&cal_str).unwrap(); - assert_eq!(calendar.events.len(), 4); - assert_eq!(calendar.events[0].duration, "PT14H"); - } - - #[test] - #[should_panic] - fn test_invalid_input() { - let _calendar = Calendar::from_str(INVALID_CALENDAR).unwrap(); - } -} diff --git a/server/src/router/schedule.rs b/server/src/router/schedule.rs new file mode 100644 index 0000000..0eb4dd1 --- /dev/null +++ b/server/src/router/schedule.rs @@ -0,0 +1,624 @@ +//! Provides calendar/scheduling utilities +//! Parses and serializes string RRULEs with duration and provides api to query if time slot is available. + +use chrono::{DateTime, Duration, Utc}; +use chrono_tz::Tz; +use iso8601_duration::Duration as Iso8601Duration; +pub use rrule::{RRuleResult, RRuleSet, Tz as RRuleTz}; +use std::cmp::{max, min}; +use std::fmt::{Display, Formatter, Result as FmtResult}; +use std::ops::Sub; +use std::str::FromStr; + +#[derive(Debug, Copy, Clone, Eq, PartialEq)] +pub struct Timeslot { + pub time_start: DateTime, + pub time_end: DateTime, +} + +#[derive(Debug, Copy, Clone, Eq, PartialEq)] +pub enum TimeslotError { + NoOverlap, +} + +impl Timeslot { + pub fn duration(&self) -> Duration { + self.time_end - self.time_start + } + + pub fn split(&self, min_duration: &Duration, max_duration: &Duration) -> Vec { + let mut slots = vec![]; + let mut current_time = self.time_start; + + while current_time < self.time_end { + let next_time = min(current_time + *max_duration, self.time_end); + let timeslot = Timeslot { + time_start: current_time, + time_end: next_time, + }; + + if timeslot.duration() >= *min_duration { + slots.push(timeslot); + } + + current_time = next_time; + } + + slots + } + + pub fn overlap(&self, other: &Self) -> Result { + // + // | self | + // | other | + // | overlap | + let slot = Self { + time_start: max(self.time_start, other.time_start), + time_end: min(self.time_end, other.time_end), + }; + + if slot.time_start >= slot.time_end { + return Err(TimeslotError::NoOverlap); + } + + Ok(slot) + } +} + +impl Sub for Timeslot { + type Output = Vec; + + fn sub(self, other: Self) -> Self::Output { + // Occupied slot ends before available slot starts + // or occupied slot starts after available slot ends + if self.time_end <= other.time_start || self.time_start >= other.time_end { + return vec![self]; + } + + // Occupied slot starts before and ends after available slot + // | OCCUPIED | + // + + // | Available | Available | + // = + // (no available slots) + if other.time_start <= self.time_start && other.time_end >= self.time_end { + // other timeslot obliterates this available timeslot + return vec![]; + } + + // Occupied slot right in the middle of the available slot, so we need to split the available slot + // | Occupied | + // + + // | Available | + // = + // | Av. | | Av. | + if self.time_start < other.time_start && self.time_end > other.time_end { + return vec![ + Timeslot { + time_start: self.time_start, + time_end: other.time_start, + }, + Timeslot { + time_start: other.time_end, + time_end: self.time_end, + }, + ]; + } + + // | Occupied | + // + + // | Available | + // = + // | Av. | + if self.time_start < other.time_start && self.time_end <= other.time_end { + return vec![Timeslot { + time_start: self.time_start, + time_end: other.time_start, + }]; + } + + // + // | Occupied | + // + + // | Available | + // = + // | Av. | + if self.time_start >= other.time_start && self.time_end > other.time_end { + return vec![Timeslot { + time_start: other.time_end, + time_end: self.time_end, + }]; + } + + router_warn!( + "(timeslot_collision) Unhandled case: {:?} {:?}", + self, + other + ); + + vec![] + } +} + +// /// formats chrono::DateTime to string in format: `YYYYMMDDThhmmssZ`, e.g. 20221026T133000Z +fn datetime_to_ical_format(dt: &DateTime) -> String { + router_debug!("(datetime_to_ical_format) {:?}", dt); + let mut tz_prefix = String::new(); + let mut tz_postfix = String::new(); + router_debug!("(datetime_to_ical_format) tz: {:?}", dt.timezone()); + let tz = dt.timezone(); + match tz { + RRuleTz::Local(_) => {} + RRuleTz::Tz(tz) => match tz { + Tz::UTC => { + tz_postfix = "Z".to_string(); + } + tz => { + tz_prefix = format!(";TZID={}:", tz.name()); + } + }, + } + + let dt = dt.format("%Y%m%dT%H%M%S"); + router_debug!("(datetime_to_ical_format) dt: {:?}", dt); + format!("{}{}{}", tz_prefix, dt, tz_postfix) +} + +/// Wraps rruleset and their duration +#[derive(Debug, Clone)] +pub struct RecurrentEvent { + /// The rruleset with recurrence rules + pub rrule_set: RRuleSet, + /// The duration of the event + pub duration: Duration, +} +///Calendar implementation for recurring events using the rrule crate and duration iso8601_duration crate +#[derive(Debug, Clone)] +pub struct Calendar { + ///Vec of rrulesets and their duration + pub events: Vec, +} + +#[derive(Debug, Copy, Clone)] +pub enum CalendarError { + Rrule, + RruleSet, + HeaderPartsLength, + Duration, +} + +impl Display for CalendarError { + fn fmt(&self, f: &mut Formatter) -> FmtResult { + match self { + CalendarError::Rrule => write!(f, "Invalid rrule"), + CalendarError::RruleSet => write!(f, "Invalid rrule set"), + CalendarError::HeaderPartsLength => write!(f, "Invalid header parts length"), + CalendarError::Duration => write!(f, "Invalid duration"), + } + } +} + +impl FromStr for Calendar { + type Err = CalendarError; + + /// Parses multiline string into a vector of `RecurrentEvents` + /// Using `rrule` library is not sufficient to capture duration of the event so we need to parse it + /// manually ane remove it from the string before letting rrule parse the rest + /// Duration has to be the last part of the RRULE_SET header after DTSTART e.g. + /// "DTSTART:20221020T180000Z;DURATION:PT1H" not "DURATION:PT1H;DTSTART:20221020T180000Z" + /// Duration is in ISO8601 format (`iso8601_duration` crate) + fn from_str(calendar_str: &str) -> Result { + router_debug!("(Calendar from_str) Parsing calendar: {}", calendar_str); + let rrule_sets: Vec<&str> = calendar_str + .split("DTSTART:") + .filter(|s| !s.is_empty()) + .collect(); + router_debug!("(Calendar from_str) rrule_sets: {:?}", rrule_sets); + let mut recurrent_events: Vec = Vec::new(); + for rrule_set_str in rrule_sets { + router_debug!("(Calendar from_str) rrule_set_str: {}", rrule_set_str); + let rrules_with_header: Vec<&str> = rrule_set_str + .split('\n') + .filter(|s| !s.is_empty()) + .collect(); + if rrules_with_header.len() < 2 { + router_error!( + "(Calendar from_str) Invalid rrule {} with header length: {}", + calendar_str, + rrules_with_header.len() + ); + return Err(CalendarError::Rrule); + } + let header = rrules_with_header[0]; + let rrules = &rrules_with_header[1..]; + let header_parts: Vec<&str> = header + .split(";DURATION:") + .filter(|s| !s.is_empty()) + .collect(); + if header_parts.len() != 2 { + router_error!( + "(Calendar from_str) Invalid header parts length: {}", + header_parts.len() + ); + return Err(CalendarError::HeaderPartsLength); + } + + let dtstart = header_parts[0]; + let duration: &str = header_parts[1]; + let Ok(duration) = duration.parse::() else { + router_error!("(Calendar from_str) Invalid duration: {:?}", duration); + return Err(CalendarError::Duration); + }; + + let Some(duration) = duration.to_chrono() else { + router_error!("(Calendar from_str) Could not convert duration to chrono::DateTime: {:?}", duration); + return Err(CalendarError::Duration); + }; + + let str = "DTSTART:".to_owned() + dtstart + "\n" + rrules.join("\n").as_str(); + let rrset_res = RRuleSet::from_str(&str); + + let Ok(rrule_set) = rrset_res else { + router_error!("(Calendar from_str) Invalid rrule set: {:?}", rrset_res.unwrap_err()); + return Err(CalendarError::RruleSet); + }; + + recurrent_events.push(RecurrentEvent { + rrule_set, + duration, + }); + } + router_debug!( + "(Calendar from_str) Parsed calendar: {:?}", + recurrent_events + ); + Ok(Calendar { + events: recurrent_events, + }) + } +} + +impl Display for Calendar { + /// Formats `Calendar` into multiline string which can be stored in the database + fn fmt(&self, f: &mut Formatter<'_>) -> FmtResult { + let err_msg = String::from("Error writing to string"); + for event in &self.events { + writeln!( + f, + "DTSTART:{};DURATION:{}", + datetime_to_ical_format(event.rrule_set.get_dt_start()), + &event.duration + ) + .expect(&err_msg); + for rrule in event.rrule_set.get_rrule() { + writeln!(f, "RRULE:{}", rrule).expect(&err_msg); + } + for rdate in event.rrule_set.get_rdate() { + writeln!(f, "RDATE:{}", datetime_to_ical_format(rdate)).expect(&err_msg); + } + } + Ok(()) + } +} + +impl Calendar { + /// Converts a date into a sorted list of timeslots for a given date + pub fn to_timeslots(&self, start: &DateTime, end: &DateTime) -> Vec { + let mut timeslots = vec![]; + for event in &self.events { + let rrule_set = &event.rrule_set; + let duration = &event.duration; + + let time_start: DateTime = rrule_set.get_dt_start().with_timezone(&Utc); + let time_end: DateTime = time_start + *duration; + + let slot = Timeslot { + time_start, + time_end, + }; + + if slot.time_start >= *end || slot.time_end <= *start { + continue; + } + + timeslots.push(Timeslot { + time_start: max(slot.time_start, *start), + time_end: min(slot.time_end, *end), + }) + } + + timeslots + } + + // /// Wrapper implementation of rrule library's `all` method which also considers duration of the event + // /// Calendar stores blocking events as rrulesets with duration. This function checks if the time slot is fully available. + // /// # Examples + // /// If the calendar contains a blocking event from 10:00 to 11:00 and we check if 10:30 to 11:30 is available, it will return false + // /// If the calendar contains a blocking event from 10:00 to 11:00 and we check if 9:30 to 10:00 is available, it will return true. + // /// - if the start or end time is on the boundary with the blocking event, it is considered available. + // /// Code Example: + // /// ```rust,ignore + // /// use std::str::FromStr; + // /// use chrono::TimeZone; + // /// use rrule::Tz; + // /// let Ok(calendar) = Calendar::from_str("DTSTART:20221020T180000Z;DURATION:PT14H\n\ + // /// RRULE:FREQ=WEEKLY;BYDAY=MO,TU,WE,TH,FR").unwrap(); + // /// let mut start = Tz::UTC.with_ymd_and_hms(2022, 10, 25,17, 0, 0).unwrap(); + // /// let mut end = Tz::UTC.with_ymd_and_hms(2022, 10, 25,18, 0, 0).unwrap(); + // /// assert_eq!(calendar.is_available_between(start, end), true); + // /// ``` + // /// * `start_time` - start of the time slot + // /// * `end_time` - end of the time slot + // /// returns true if the time slot is fully available + // pub fn is_available_between(&self, start_time: DateTime, end_time: DateTime) -> bool { + // router_debug!( + // "(is_available_between) Checking if time slot is available between {} and {}", + // start_time, + // end_time + // ); + + // // adjust start and end time by one second to make search inclusive of boundary values + // let start_time = start_time + Duration::seconds(1); + // let end_time = end_time - Duration::seconds(1); + + // router_debug!("(is_available_between) Adjusted start_time: {}", start_time); + // router_debug!("(is_available_between) Adjusted end_time: {}", end_time); + // for event in &self.events { + // let duration = &event.duration; + // // check standard rrule time - if event(block) start time is between two dates, + // // then it will be found and time slot will be marked as not available + // let RRuleResult { dates: events, limited: _ } = &event + // .rrule_set + // .clone() + // .after(start_time.with_timezone(&rrule::Tz::UTC)) + // .before(end_time.with_timezone(&rrule::Tz::UTC)) + // .all(1) else { + // router_error!("(is_available_between) Failed to parse rrule set"); + // return false; + // }; + + // if !events.is_empty() { + // router_debug!("(is_available_between) Time slot is not available"); + // return false; + // } + + // let adjusted_start_time = start_time - duration; + + // // here we check if event(block) start time + duration is between two dates, + // // then it will be found and time slot will be marked as not available + // let RRuleResult { dates: events, limited: _ } = &event + // .rrule_set + // .clone() + // .after(adjusted_start_time.with_timezone(&rrule::Tz::UTC)) + // .before(end_time.with_timezone(&rrule::Tz::UTC)) + // .all(10) else { + // router_error!("(is_available_between) Failed to parse rrule set"); + // return false; + // }; + + // if !events.is_empty() { + // router_debug!("(is_available_between) Time slot is not available for adjusted start time [{}]", adjusted_start_time); + // return false; + // } + // } + // // if no events(blocks) found across all rrule_sets, then time slot is available + // router_debug!("(is_available_between) Time slot is available"); + // true + // } +} + +#[cfg(test)] +mod tests { + use super::*; + use chrono::{Duration, TimeZone, Utc}; + use std::str::FromStr; + + const CAL_WORKDAYS_8AM_6PM: &str = "DTSTART:20221020T180000Z;DURATION:PT14H\n\ + RRULE:FREQ=WEEKLY;BYDAY=MO,TU,WE,TH,FR\n\ + DTSTART:20221022T000000Z;DURATION:PT24H\n\ + RRULE:FREQ=WEEKLY;BYDAY=SA,SU"; + + const _WITH_1HR_DAILY_BREAK: &str = "\n\ + DTSTART:20221020T120000Z;DURATION:PT1H\n\ + RRULE:FREQ=WEEKLY;BYDAY=MO,TU,WE,TH,FR"; + + const _WITH_ONE_OFF_BLOCK: &str = "\n\ + DTSTART:20221026T133000Z;DURATION:PT3H\n\ + RDATE:20221026T133000Z"; + + const INVALID_CALENDAR: &str = "DURATION:PT3H;DTSTART:20221026T133000Z;\n\ + RRULE:FREQ=WEEKLY;BYDAY=SA,SU"; + + #[test] + fn test_parse_calendar() { + let calendar = Calendar::from_str(CAL_WORKDAYS_8AM_6PM).unwrap(); + assert_eq!(calendar.events.len(), 2); + assert_eq!(calendar.events[0].duration, Duration::hours(14)); + assert_eq!(calendar.events[1].duration, Duration::hours(24)); + } + + #[test] + fn test_save_and_load_calendar() { + let orig_cal_str = + &(CAL_WORKDAYS_8AM_6PM.to_owned() + _WITH_1HR_DAILY_BREAK + _WITH_ONE_OFF_BLOCK); + let calendar = Calendar::from_str(orig_cal_str).unwrap(); + let cal_str = calendar.to_string(); + let calendar = Calendar::from_str(&cal_str).unwrap(); + assert_eq!(calendar.events.len(), 4); + assert_eq!(calendar.events[0].duration, Duration::hours(14)); + } + + #[test] + #[should_panic] + fn test_invalid_input() { + let _calendar = Calendar::from_str(INVALID_CALENDAR).unwrap(); + } + + #[test] + fn test_calendar_to_timeslots() { + // 8AM to 12PM, 2PM to 6PM + let calendar = "DTSTART:20221020T080000Z;DURATION:PT4H\n\ + RRULE:FREQ=WEEKLY;BYDAY=MO,TU,WE,TH,FR\n\ + DTSTART:20221020T140000Z;DURATION:PT4H\n\ + RRULE:FREQ=WEEKLY;BYDAY=MO,TU,WE,TH,FR\n"; + + let calendar = Calendar::from_str(calendar).unwrap(); + assert_eq!(calendar.events.len(), 2); + + let cal_start = Utc.with_ymd_and_hms(2022, 10, 20, 8, 0, 0).unwrap(); + let expected_timeslots = vec![ + Timeslot { + time_start: cal_start, // 8AM + time_end: cal_start + Duration::hours(4), // 12PM + }, + Timeslot { + time_start: cal_start + Duration::hours(6), // 2PM + time_end: cal_start + Duration::hours(10), // 6PM + }, + ]; + + // Get full day schedule + let timeslots = calendar.to_timeslots( + &(cal_start - Duration::hours(1)), + &(cal_start + Duration::hours(12)), + ); + assert_eq!(timeslots.len(), 2); + assert_eq!(timeslots, expected_timeslots); + } + + #[test] + fn test_calendar_to_timeslots_cropped() { + // 8AM to 12PM, 2PM to 6PM + let calendar = "DTSTART:20221020T080000Z;DURATION:PT4H\n\ + RRULE:FREQ=WEEKLY;BYDAY=MO,TU,WE,TH,FR\n\ + DTSTART:20221020T140000Z;DURATION:PT4H\n\ + RRULE:FREQ=WEEKLY;BYDAY=MO,TU,WE,TH,FR\n"; + + let calendar = Calendar::from_str(calendar).unwrap(); + assert_eq!(calendar.events.len(), 2); + + let cal_start = Utc.with_ymd_and_hms(2022, 10, 20, 8, 0, 0).unwrap(); + + // Crop to 10AM to 6PM + let start: DateTime = cal_start + Duration::hours(2); + let end: DateTime = cal_start + Duration::hours(8); + + let expected_timeslots = vec![ + Timeslot { + time_start: start, // 10 AM + time_end: cal_start + Duration::hours(4), // 12PM + }, + Timeslot { + time_start: cal_start + Duration::hours(6), // 2PM + time_end: end, // 4PM + }, + ]; + + // Get full day schedule + let timeslots = calendar.to_timeslots(&start, &end); + assert_eq!(timeslots.len(), 2); + assert_eq!(timeslots, expected_timeslots); + } + + #[test] + fn test_calendar_to_timeslots_cropped_to_single() { + // 8AM to 12PM, 2PM to 6PM + let calendar = "DTSTART:20221020T080000Z;DURATION:PT4H\n\ + RRULE:FREQ=WEEKLY;BYDAY=MO,TU,WE,TH,FR\n\ + DTSTART:20221020T140000Z;DURATION:PT4H\n\ + RRULE:FREQ=WEEKLY;BYDAY=MO,TU,WE,TH,FR\n"; + + let calendar = Calendar::from_str(calendar).unwrap(); + assert_eq!(calendar.events.len(), 2); + + let cal_start = Utc.with_ymd_and_hms(2022, 10, 20, 8, 0, 0).unwrap(); + + // Crop to 10AM to 6PM + let start: DateTime = cal_start + Duration::hours(2); + let end: DateTime = cal_start + Duration::hours(3); + + let expected_timeslots = vec![Timeslot { + time_start: start, // 10 AM + time_end: end, // 11AM + }]; + + // Get full day schedule + let timeslots = calendar.to_timeslots(&start, &end); + assert_eq!(timeslots.len(), 1); + assert_eq!(timeslots, expected_timeslots); + } + + // #[test] + // fn test_night_unavailable() { + // let calendar = Calendar::from_str(CAL_WORKDAYS_8AM_6PM).unwrap(); + // let start = Tz::UTC.with_ymd_and_hms(2022, 10, 25, 19, 0, 0).unwrap(); + // let end = Tz::UTC.with_ymd_and_hms(2022, 10, 25, 20, 0, 0).unwrap(); + // assert_eq!(calendar.is_available_between(start, end), false); + // } + + // #[test] + // fn test_weekend_unavailable() { + // let calendar = Calendar::from_str(CAL_WORKDAYS_8AM_6PM).unwrap(); + // let start = Tz::UTC.with_ymd_and_hms(2022, 10, 22, 19, 0, 0).unwrap(); + // let end = Tz::UTC.with_ymd_and_hms(2022, 10, 22, 20, 0, 0).unwrap(); + // assert_eq!(calendar.is_available_between(start, end), false); + // } + + // #[test] + // fn test_inclusive_boundaries_available() { + // let calendar = Calendar::from_str(CAL_WORKDAYS_8AM_6PM).unwrap(); + + // let mut start = Tz::UTC.with_ymd_and_hms(2022, 10, 25, 17, 0, 0).unwrap(); + // let mut end = Tz::UTC.with_ymd_and_hms(2022, 10, 25, 18, 0, 0).unwrap(); + // assert_eq!(calendar.is_available_between(start, end), true); + + // start = Tz::UTC.with_ymd_and_hms(2022, 10, 25, 8, 0, 0).unwrap(); + // end = Tz::UTC.with_ymd_and_hms(2022, 10, 25, 9, 0, 0).unwrap(); + // assert_eq!(calendar.is_available_between(start, end), true); + // } + + // #[test] + // fn test_calendar_with_day_break() { + // let calendar = + // Calendar::from_str(&(CAL_WORKDAYS_8AM_6PM.to_owned() + _WITH_1HR_DAILY_BREAK)).unwrap(); + // assert_eq!(calendar.events[2].duration, "PT1H"); + + // let mut start = Tz::UTC.with_ymd_and_hms(2022, 10, 25, 11, 30, 0).unwrap(); + // let mut end = Tz::UTC.with_ymd_and_hms(2022, 10, 25, 12, 30, 0).unwrap(); + // assert_eq!(calendar.is_available_between(start, end), false); + + // start = Tz::UTC.with_ymd_and_hms(2022, 10, 25, 8, 0, 0).unwrap(); + // end = Tz::UTC.with_ymd_and_hms(2022, 10, 25, 12, 0, 0).unwrap(); + // assert_eq!(calendar.is_available_between(start, end), true); + + // start = Tz::UTC.with_ymd_and_hms(2022, 10, 25, 12, 15, 0).unwrap(); + // end = Tz::UTC.with_ymd_and_hms(2022, 10, 25, 12, 30, 0).unwrap(); + // assert_eq!(calendar.is_available_between(start, end), false); + + // start = Tz::UTC.with_ymd_and_hms(2022, 10, 25, 12, 59, 0).unwrap(); + // end = Tz::UTC.with_ymd_and_hms(2022, 10, 25, 13, 30, 0).unwrap(); + // assert_eq!(calendar.is_available_between(start, end), false); + // } + + // #[test] + // fn test_calendar_with_one_off_block() { + // let calendar = + // Calendar::from_str(&(CAL_WORKDAYS_8AM_6PM.to_owned() + _WITH_ONE_OFF_BLOCK)).unwrap(); + + // let mut start = Tz::UTC.with_ymd_and_hms(2022, 10, 26, 13, 30, 0).unwrap(); + // let mut end = Tz::UTC.with_ymd_and_hms(2022, 10, 26, 14, 30, 0).unwrap(); + // assert_eq!(calendar.is_available_between(start, end), false); + + // start = Tz::UTC.with_ymd_and_hms(2022, 10, 27, 13, 30, 0).unwrap(); + // end = Tz::UTC.with_ymd_and_hms(2022, 10, 27, 14, 30, 0).unwrap(); + // assert_eq!(calendar.is_available_between(start, end), true); + + // start = Tz::UTC.with_ymd_and_hms(2022, 10, 26, 11, 00, 0).unwrap(); + // end = Tz::UTC.with_ymd_and_hms(2022, 10, 26, 13, 30, 0).unwrap(); + // assert_eq!(calendar.is_available_between(start, end), true); + + // start = Tz::UTC.with_ymd_and_hms(2022, 10, 26, 11, 00, 0).unwrap(); + // end = Tz::UTC.with_ymd_and_hms(2022, 10, 26, 13, 30, 1).unwrap(); + // assert_eq!(calendar.is_available_between(start, end), false); + // } +} diff --git a/server/src/router/vehicle.rs b/server/src/router/vehicle.rs new file mode 100644 index 0000000..e7a38f0 --- /dev/null +++ b/server/src/router/vehicle.rs @@ -0,0 +1,484 @@ +use crate::grpc::client::GrpcClients; +use crate::router::flight_plan::*; +use crate::router::schedule::*; +use svc_storage_client_grpc::prelude::*; + +use chrono::{Duration, Utc}; +use std::collections::HashMap; +use std::str::FromStr; +use uuid::Uuid; + +/// Enum with all Aircraft types +#[derive(Debug, Copy, Clone)] +pub enum AircraftType { + /// Cargo aircraft + Cargo, +} + +/// TODO(R4): Hardcoded for the demo. This is solely used to +/// estimate a duration of a flight. +const AVERAGE_CARGO_AIRCRAFT_CRUISE_VELOCITY_M_PER_S: f32 = 10.0; +/// Ignore aircraft that haven't reported in the last hour +const AIRCRAFT_TIMEOUT_MINUTES: i64 = 60; + +/// Reasons for unavailable aircraft +#[derive(Debug, PartialEq, Copy, Clone)] +pub enum VehicleError { + ClientError, + InvalidData, + NoScheduleProvided, + InvalidSchedule, +} + +impl std::fmt::Display for VehicleError { + fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result { + match self { + VehicleError::ClientError => write!(f, "Vehicle client error"), + VehicleError::InvalidData => write!(f, "Vehicle data is corrupt or invalid"), + VehicleError::NoScheduleProvided => write!(f, "Vehicle doesn't have a schedule"), + VehicleError::InvalidSchedule => write!(f, "Vehicle has an invalid schedule"), + } + } +} + +#[derive(Debug, Clone)] +pub struct Aircraft { + vehicle_uuid: String, + vehicle_calendar: Calendar, + last_vertiport_id: String, +} + +#[derive(Clone, Debug)] +pub struct Availability { + pub timeslot: Timeslot, + pub vertiport_id: String, + // TODO(R4): Add vertipad occupied during this timeslot + // vertipad_id: String, +} + +impl Availability { + fn subtract(&self, flight_plan: &FlightPlanSchedule) -> Result, VehicleError> { + let mut slots = vec![]; + + let flight_plan_timeslot = Timeslot { + time_start: flight_plan.departure_time, + time_end: flight_plan.arrival_time, + }; + + for timeslot in self.timeslot - flight_plan_timeslot { + let vertiport_id = if self.timeslot.time_start < flight_plan_timeslot.time_start { + self.vertiport_id.clone() + } else { + flight_plan.arrival_vertiport_id.clone() + }; + + slots.push(Availability { + timeslot, + vertiport_id, + }); + } + + Ok(slots) + } +} + +impl TryFrom for Aircraft { + type Error = VehicleError; + + fn try_from(vehicle: vehicle::Object) -> Result { + let vehicle_uuid = match Uuid::parse_str(&vehicle.id) { + Ok(uuid) => uuid.to_string(), + Err(e) => { + router_error!( + "(Aircraft::try_from) Vehicle {} has invalid UUID: {}", + vehicle.id, + e + ); + + return Err(VehicleError::InvalidData); + } + }; + + let Some(data) = vehicle.data else { + router_error!( + "(Aircraft::try_from) Vehicle doesn't have data: {:?}", + vehicle + ); + + return Err(VehicleError::InvalidData); + }; + + let Some(last_vertiport_id) = data.last_vertiport_id else { + router_error!( + "(Aircraft::try_from) Vehicle {} doesn't have last_vertiport_id", + vehicle_uuid + ); + + return Err(VehicleError::InvalidData); + }; + + let last_vertiport_id = match Uuid::parse_str(&last_vertiport_id) { + Ok(uuid) => uuid.to_string(), + Err(e) => { + router_error!( + "(Aircraft::try_from) Vehicle {} has invalid last_vertiport_id: {}", + vehicle_uuid, + e + ); + + return Err(VehicleError::InvalidData); + } + }; + + let Some(calendar) = data.schedule else { + // If vehicle doesn't have a schedule, it is not available + // MUST have a schedule to be a valid aircraft choice, even if the + // schedule is 24/7. Must be explicit. + return Err(VehicleError::NoScheduleProvided); + }; + + let Ok(vehicle_calendar) = Calendar::from_str(&calendar) else { + router_debug!( + "(is_vehicle_available) Invalid schedule for vehicle {}: {}", + vehicle_uuid, + calendar + ); + + return Err(VehicleError::InvalidSchedule); + }; + + Ok(Aircraft { + vehicle_uuid, + vehicle_calendar, + last_vertiport_id, + }) + } +} + +/// Request a list of all aircraft from svc-storage +async fn get_aircraft(clients: &GrpcClients) -> Result, VehicleError> { + // TODO(R4): Private aircraft, disabled aircraft, etc. should be filtered out here + // This is a lot of aircraft. Possible filters: + // geographical area within N kilometers of request departure vertiport + // private aircraft + // disabled aircraft + + // Ignore aircraft that haven't been updated recently + let mut filter = AdvancedSearchFilter::search_greater( + "updated_at".to_owned(), + (Utc::now() - Duration::minutes(AIRCRAFT_TIMEOUT_MINUTES)).to_string(), + ); + + // We should further limit this, but for now we'll just get all aircraft + // Need something to sort by, ascending distance from the + // departure vertiport or charge level before cutting off the list + filter.results_per_page = 1000; + let Ok(response) = clients + .storage + .vehicle + .search(filter) + .await + else { + router_error!("(get_aircraft) request to svc-storage failed."); + return Err(VehicleError::ClientError); + }; + + Ok(response + .into_inner() + .list + .into_iter() + .filter_map(|v| Aircraft::try_from(v).ok()) + .collect()) +} + +/// Estimates the time needed to travel between two locations including loading and unloading +/// Estimate should be rather generous to block resources instead of potentially overloading them +pub fn estimate_flight_time_seconds(distance_meters: &f32) -> Duration { + router_debug!( + "(estimate_flight_time_minutes) distance_meters: {}", + *distance_meters + ); + + let aircraft = AircraftType::Cargo; // TODO(R4): Hardcoded for demo + router_debug!("(estimate_flight_time_minutes) aircraft: {:?}", aircraft); + + match aircraft { + AircraftType::Cargo => { + let liftoff_duration_s: f32 = 10.0; // TODO(R4): Calculate from altitude of corridor + let landing_duration_s: f32 = 10.0; // TODO(R4): Calculate from altitude of corridor + + let cruise_duration_s: f32 = + *distance_meters / AVERAGE_CARGO_AIRCRAFT_CRUISE_VELOCITY_M_PER_S; + + Duration::seconds((liftoff_duration_s + cruise_duration_s + landing_duration_s) as i64) + } + } +} + +/// From an aircraft's calendar and list of busy timeslots, determine +/// the aircraft's availability and location at a given time. +fn get_aircraft_availability( + aircraft: Aircraft, + aircraft_schedule: &[FlightPlanSchedule], +) -> Vec { + // Get timeslots from vehicle's general calendar + // e.g. 8AM to 12PM, 2PM to 6PM + let mut availability = aircraft + .vehicle_calendar + .to_timeslots( + // 2 hours before earliest departure time + &(Utc::now() - Duration::hours(2)), + // 2 hours after latest arrival time + &(Utc::now() + Duration::hours(2)), + ) + .into_iter() + .map(|slot| { + Availability { + timeslot: slot, + vertiport_id: aircraft.last_vertiport_id.clone(), + // vertipad_id: aircraft.last_vertipad_id.clone(), + } + }) + .collect::>(); + + // Existing flight plans modify availability + for fp in aircraft_schedule { + availability = availability + .into_iter() + // Remove any slots that overlap with the occupied slot + .filter_map(|availability| availability.subtract(fp).ok()) + .flatten() + .collect::>() + } + + availability +} + +/// Build out a list of available aircraft (and their scheduled locations) +/// given a list of existing flight plans. +pub async fn get_aircraft_gaps( + existing_flight_plans: &[FlightPlanSchedule], + clients: &GrpcClients, +) -> Result>, VehicleError> { + let aircraft: Vec = get_aircraft(clients).await?; + let mut aircraft_schedules = aircraft + .iter() + .map(|a| (a.vehicle_uuid.clone(), vec![])) + .collect::>>(); + + // Group flight plans by vehicle_id + existing_flight_plans.iter().for_each(|fp| { + // only push flight plans for aircraft that we have in our list + // don't want to schedule new flights for removed aircraft + if let Some(schedule) = aircraft_schedules.get_mut(&fp.vehicle_id) { + schedule.push(fp.clone()); + } else { + router_warn!( + "(query_flight) Flight plan for unknown aircraft: {}", + fp.vehicle_id + ); + } + }); + + // Convert to a hashmap of vehicle_id to list of availabilities + let mut gaps = HashMap::new(); + for vehicle in aircraft.into_iter() { + let Some(schedule) = aircraft_schedules.get_mut(&vehicle.vehicle_uuid) else { + router_warn!( + "(query_flight) Flight plan for unknown aircraft: {}", + vehicle.vehicle_uuid + ); + + continue; + }; + + gaps.insert( + vehicle.vehicle_uuid.clone(), + get_aircraft_availability(vehicle, schedule), + ); + } + + Ok(gaps) +} + +// #[cfg(test)] +// mod tests { +// #[test] +// fn test_estimate_flight_time_minutes() { +// init_logger(&Config::try_from_env().unwrap_or_default()); +// unit_test_info!("(test_estimate_flight_time_minutes) start"); + +// let distance_meters: f64 = (AVG_SPEED_KMH * 1000.0) as f64; // using AVG_SPEED_KMH since it's an easy calculation to make from there +// let aircraft = Aircraft::Cargo; +// let expected_time_minutes: f32 = +// LOADING_AND_TAKEOFF_TIME_MIN + 60.0 + LANDING_AND_UNLOADING_TIME_MIN; // If the distance is the same as the AVG_SPEED_KMH, it should take 60 minutes. Then we'll need to add the landing/ unloading time to get the expected minutes. + +// let result = estimate_flight_time_minutes(distance_meters, aircraft); + +// assert_eq!(result, expected_time_minutes); +// unit_test_info!("(test_estimate_flight_time_minutes) success"); +// } + +// #[test] +// fn test_is_vehicle_available_per_schedule_true() { +// init_logger(&Config::try_from_env().unwrap_or_default()); +// unit_test_info!("(test_is_vehicle_available_per_schedule_true) start"); + +// // Construct a Vehicle Object using mock data and adding a known schedule. +// let vehicle_id = uuid::Uuid::new_v4(); +// let vertiport_id = uuid::Uuid::new_v4(); +// let schedule = +// "DTSTART:20221020T180000Z;DURATION:PT1H\nRRULE:FREQ=WEEKLY;BYDAY=MO,TU,WE,TH,FR"; +// let mut vehicle_data = vehicle::mock::get_data_obj(); +// vehicle_data.last_vertiport_id = Some(vertiport_id.to_string()); +// vehicle_data.schedule = Some(schedule.to_owned()); +// let vehicle = vehicle::Object { +// id: vehicle_id.to_string(), +// data: Some(vehicle_data), +// }; + +// // Create a `date_from` value which should be within the vehicle's schedule +// let date_from: Timestamp = Utc +// .datetime_from_str("2022-10-26 15:00:00", "%Y-%m-%d %H:%M:%S") +// .unwrap() +// .into(); + +// is_vehicle_available(&vehicle, date_from.into(), 60, &vec![]).unwrap(); +// unit_test_info!("(test_is_vehicle_available_per_schedule_true) success"); +// } + +// #[test] +// fn test_is_vehicle_available_per_schedule_false() { +// init_logger(&Config::try_from_env().unwrap_or_default()); +// unit_test_info!("(test_is_vehicle_available_per_schedule_false) start"); + +// // Construct a Vehicle Object using mock data and adding a known schedule. +// let vehicle_id = uuid::Uuid::new_v4(); +// let vertiport_id = uuid::Uuid::new_v4(); +// let mut vehicle_data = vehicle::mock::get_data_obj(); +// vehicle_data.last_vertiport_id = Some(vertiport_id.to_string()); +// vehicle_data.schedule = Some(String::from( +// "DTSTART:20221020T180000Z;DURATION:PT1H\nRRULE:FREQ=WEEKLY;BYDAY=MO,TU,WE,TH,FR", +// )); +// let vehicle = vehicle::Object { +// id: vehicle_id.to_string(), +// data: Some(vehicle_data), +// }; + +// // Create a `date_from` value which should be within the vehicle's schedule +// let date_from: Timestamp = Utc +// .datetime_from_str("2022-10-26 18:00:00", "%Y-%m-%d %H:%M:%S") +// .unwrap() +// .into(); +// let e = is_vehicle_available(&vehicle, date_from.into(), 60, &vec![]).unwrap_err(); +// assert_eq!(e, VehicleError::ScheduleConflict); +// unit_test_info!("(test_is_vehicle_available_per_schedule_false) success"); +// } + +// #[tokio::test] +// async fn test_is_vehicle_available_true() { +// init_logger(&Config::try_from_env().unwrap_or_default()); +// unit_test_info!("(test_is_vehicle_available_true) start"); +// ensure_storage_mock_data().await; +// crate::grpc::queries::init_router().await; + +// let latest_arrival_time: Timestamp = Utc +// .datetime_from_str("2022-10-26 15:00:00", "%Y-%m-%d %H:%M:%S") +// .unwrap() +// .into(); +// let existing_flight_plans = +// crate::grpc::queries::query_flight_plans_for_latest_arrival(latest_arrival_time) +// .await +// .unwrap(); +// let flight_plan = &existing_flight_plans[0]; +// let flight_plan_data = flight_plan.data.as_ref().unwrap(); + +// // We'll pick a vehicle_id from the returned flight_plans, making sure +// // it's available +// let vehicle_id = flight_plan_data.vehicle_id.clone(); +// let vertiport_id = flight_plan_data.departure_vertipad_id.clone(); + +// let mut vehicle_data = vehicle::mock::get_data_obj(); +// vehicle_data.last_vertiport_id = Some(vertiport_id.to_string()); +// vehicle_data.schedule = Some(String::from( +// "DTSTART:20221020T180000Z;DURATION:PT1H\nRRULE:FREQ=WEEKLY;BYDAY=MO,TU,WE,TH,FR", +// )); +// let vehicle = vehicle::Object { +// id: vehicle_id.to_string(), +// data: Some(vehicle_data), +// }; + +// unit_test_debug!( +// "(test_is_vehicle_available_true) testing for vehicle: {:?}", +// vehicle.data +// ); +// // This should be available +// let earliest_departure_time: Timestamp = Utc +// .datetime_from_str("2022-10-25 10:15:00", "%Y-%m-%d %H:%M:%S") +// .unwrap() +// .into(); +// is_vehicle_available( +// &vehicle, +// earliest_departure_time.into(), +// 60, +// &existing_flight_plans, +// ).unwrap(); +// unit_test_info!("(test_is_vehicle_available_true) success"); +// } + +// /// Takes vertiport 1 and gets all available flight_plans for the provided latest arrival. +// /// Then picks out the vehicle_id of the first flight_plan returned. Since +// /// this vehicle is already occupied for this flight_plan, the test should +// /// return false. +// #[tokio::test] +// async fn test_is_vehicle_available_false() { +// init_logger(&Config::try_from_env().unwrap_or_default()); +// unit_test_info!("(test_is_vehicle_available_false) start"); +// ensure_storage_mock_data().await; +// crate::grpc::queries::init_router().await; + +// let latest_arrival_time: Timestamp = Utc +// .datetime_from_str("2022-10-25 15:00:00", "%Y-%m-%d %H:%M:%S") +// .unwrap() +// .into(); + +// let existing_flight_plans = +// crate::grpc::queries::query_flight_plans_for_latest_arrival(latest_arrival_time) +// .await +// .unwrap(); +// let flight_plan = &existing_flight_plans[0]; +// let flight_plan_data = flight_plan.data.as_ref().unwrap(); + +// // We'll pick a vehicle_id from the returned flight_plans, making sure +// // it's part of the test data set +// let vehicle_id = flight_plan_data.vehicle_id.clone(); +// let vertiport_id = flight_plan_data.departure_vertipad_id.clone(); + +// let mut vehicle_data = vehicle::mock::get_data_obj(); +// vehicle_data.last_vertiport_id = Some(vertiport_id.to_string()); +// vehicle_data.schedule = Some(String::from( +// "DTSTART:20221020T180000Z;DURATION:PT1H\nRRULE:FREQ=WEEKLY;BYDAY=MO,TU,WE,TH,FR", +// )); +// let vehicle = vehicle::Object { +// id: vehicle_id.to_string(), +// data: Some(vehicle_data), +// }; + +// // This should generate a conflict since we've already inserted a +// // flight_plan for this vehicle and this vertiport with a departure date +// // of "2022-10-25 14:20:00" +// let earliest_departure_time: Timestamp = Utc +// .datetime_from_str("2022-10-25 14:15:00", "%Y-%m-%d %H:%M:%S") +// .unwrap() +// .into(); +// let e = is_vehicle_available( +// &vehicle, +// earliest_departure_time.into(), +// 60, +// &existing_flight_plans, +// ).unwrap_err(); + +// assert_eq!(e, VehicleError::ScheduleConflict); +// unit_test_info!("(test_is_vehicle_available_false) success"); +// } +// } diff --git a/server/src/router/vertiport.rs b/server/src/router/vertiport.rs new file mode 100644 index 0000000..9221703 --- /dev/null +++ b/server/src/router/vertiport.rs @@ -0,0 +1,817 @@ +//! Vertiport-related utilities + +use super::flight_plan::*; +use super::schedule::*; +use super::vehicle::*; +use super::{best_path, BestPathError, BestPathRequest}; +use crate::grpc::client::GrpcClients; +use chrono::Duration; +use std::cmp::{max, min}; +use std::collections::HashMap; +use std::str::FromStr; +use svc_gis_client_grpc::prelude::gis::*; +use svc_storage_client_grpc::prelude::*; + +/// Chop up larger timeslots into smaller durations to avoid temporary no-fly zones +const MAX_DURATION_TIMESLOT_MINUTES: i64 = 30; + +/// Error type for vertiport-related errors +#[derive(Debug, Copy, Clone)] +pub enum VertiportError { + ClientError, + InvalidData, + NoVertipads, + NoSchedule, + InvalidSchedule, +} + +impl std::fmt::Display for VertiportError { + fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result { + match self { + VertiportError::ClientError => write!(f, "Client error"), + VertiportError::InvalidData => write!(f, "Invalid data"), + VertiportError::NoVertipads => write!(f, "No vertipads"), + VertiportError::NoSchedule => write!(f, "No schedule"), + VertiportError::InvalidSchedule => write!(f, "Invalid schedule"), + } + } +} + +/// Gets all vertipads for a vertiport +pub async fn get_vertipads( + vertiport_id: &String, + clients: &GrpcClients, +) -> Result, VertiportError> { + let Ok(response) = clients + .storage + .vertipad + .search( + AdvancedSearchFilter::search_equals( + "vertiport_id".to_string(), + vertiport_id.to_string(), + ) + .and_is_null("deleted_at".to_owned()), + ) + .await + else { + let error_str = format!( + "Failed to get vertipads for vertiport {}.", + vertiport_id + ); + router_error!("(get_vertipads) {}", error_str); + return Err(VertiportError::NoVertipads); + }; + + Ok(response + .into_inner() + .list + .into_iter() + .map(|vp| vp.id) + .collect::>()) +} + +/// Get pairs of timeslots where a flight can leave within the departure timeslot +/// and land within the arrival timeslot +pub async fn get_timeslot_pairs( + departure_vertiport_id: &String, + arrival_vertiport_id: &String, + departure_time_block: &Duration, + arrival_time_block: &Duration, + timeslot: &Timeslot, + existing_flight_plans: &[FlightPlanSchedule], + clients: &GrpcClients, +) -> Result, VertiportError> { + let departure_timeslots = get_available_timeslots( + departure_vertiport_id, + existing_flight_plans, + timeslot, + departure_time_block, + clients, + ) + .await?; + + let arrival_timeslots = get_available_timeslots( + arrival_vertiport_id, + existing_flight_plans, + timeslot, + arrival_time_block, + clients, + ) + .await?; + + get_vertipad_timeslot_pairs( + departure_vertiport_id, + arrival_vertiport_id, + departure_timeslots, + arrival_timeslots, + clients, + ) + .await +} + +/// Return a map of vertipad ids to available timeslots for that vertipad +/// +/// TODO(R4): This will be replaced with a call to svc-storage vertipad_timeslots to +/// return a list of available timeslots for each vertipad, so we don't +/// need to rebuild each pad's schedule from flight plans each time +async fn get_available_timeslots( + vertiport_id: &String, + existing_flight_plans: &[FlightPlanSchedule], + timeslot: &Timeslot, + minimum_duration: &Duration, + clients: &GrpcClients, +) -> Result>, VertiportError> { + // Get vertiport schedule + let calendar = get_vertiport_calendar(vertiport_id, clients).await?; + let base_timeslots = calendar.to_timeslots(×lot.time_start, ×lot.time_end); + + // TODO: This is currently hardcoded, get the duration of the timeslot + // try min and max both the necessary landing time + let max_duration = Duration::minutes(MAX_DURATION_TIMESLOT_MINUTES); + + // Prepare a list of slots for each vertipad + // For now, each vertipad shares the same schedule as the vertiport itself + let mut timeslots = get_vertipads(vertiport_id, clients) + .await? + .into_iter() + .map(|id| (id, base_timeslots.clone())) + .collect::>>(); + + // Get occupied slots + // TODO(R4): This will be replaced with a call to svc-storage vertipad_timeslots to + // return a list of occupied timeslots for each vertipad, so we don't + // need to rebuild each pad's schedule from flight plans each time + let occupied_slots = build_timeslots_from_flight_plans(vertiport_id, existing_flight_plans); + + // For each occupied slot, remove it from the list of available slots + for (vertipad_id, occupied_slot) in occupied_slots.iter() { + let Some(vertipad_slots) = timeslots.get_mut(vertipad_id) else { + router_error!("(get_available_timeslots) Vertipad {} (from a flight plan) not found in list of vertipads from storage.", vertipad_id); + continue; + }; + + *vertipad_slots = vertipad_slots + .iter_mut() + // Subtract the occupation slot from the available slots + .flat_map(|slot| *slot - *occupied_slot) + // Split any slots that are too long. A short temporary no-fly zone overlapping + // any part of the timeslot will invalidate the entire timeslot, so we split it + // into smaller timeslots to avoid this. + .flat_map(|slot| slot.split(minimum_duration, &max_duration)) + .collect::>(); + } + + Ok(timeslots) +} + +/// Gets vertiport schedule from storage and converts it to a Calendar object. +async fn get_vertiport_calendar( + vertiport_id: &String, + clients: &GrpcClients, +) -> Result { + let vertiport_object = match clients + .storage + .vertiport + .get_by_id(Id { + id: vertiport_id.clone(), + }) + .await + { + Ok(response) => response.into_inner(), + Err(e) => { + let error_str = format!("Could not retrieve data for vertiport {vertiport_id}."); + + router_error!("(get_vertiport_calendar) {}: {e}", error_str); + return Err(VertiportError::ClientError); + } + }; + + let vertiport_data = match vertiport_object.data { + Some(d) => d, + None => { + let error_str = format!("Date invalid for vertiport {}.", vertiport_id); + router_error!("(get_vertiport_calendar) {}", error_str); + return Err(VertiportError::InvalidData); + } + }; + + let Some(vertiport_schedule) = vertiport_data.schedule else { + let error_str = format!( + "No schedule for vertiport {}.", + vertiport_id + ); + router_error!("(get_vertiport_calendar) {}", error_str); + return Err(VertiportError::NoSchedule); + }; + + match Calendar::from_str(&vertiport_schedule) { + Ok(calendar) => Ok(calendar), + Err(e) => { + let error_str = format!("Schedule invalid for vertiport {}.", vertiport_id); + router_error!("(get_vertiport_calendar) {}: {}", error_str, e); + Err(VertiportError::InvalidSchedule) + } + } +} + +/// Gets all occupied vertipad time slots given flight plans. +/// If `invert` is true, returns all unoccupied time slots. +/// +/// TODO(R4): Remove in favor of read from storage vertipad_timeslot table +/// where the duration of the timeslot is stored +fn build_timeslots_from_flight_plans( + vertiport_id: &String, + flight_plans: &[FlightPlanSchedule], +) -> Vec<(String, Timeslot)> { + // TODO(R4): This is currently hardcoded, get the duration of the timeslot + // directly from the vertipad_timeslot object + let required_loading_time = + Duration::seconds(crate::grpc::api::query_flight::LOADING_AND_TAKEOFF_TIME_SECONDS); + let required_unloading_time = + Duration::seconds(crate::grpc::api::query_flight::LANDING_AND_UNLOADING_TIME_SECONDS); + + flight_plans + .iter() + .filter_map(|fp| { + if *vertiport_id == fp.departure_vertiport_id { + let timeslot = Timeslot { + time_start: fp.departure_time, + // TODO(R4): duration should be retrieved from flight plan object + // instead of being hardcoded + time_end: fp.departure_time + required_loading_time, + }; + + Some((fp.departure_vertipad_id.clone(), timeslot)) + } else if *vertiport_id == fp.arrival_vertiport_id { + let timeslot = Timeslot { + time_start: fp.arrival_time, + // TODO(R4): duration should be retrieved from flight plan object + // instead of being hardcoded + time_end: fp.arrival_time + required_unloading_time, + }; + + Some((fp.arrival_vertipad_id.clone(), timeslot)) + } else { + None + } + }) + .collect::>() +} + +/// Gets all available timeslot pairs and a path for each pair +#[derive(Debug, Clone)] +pub struct TimeslotPair { + pub depart_port_id: String, + pub depart_pad_id: String, + pub depart_timeslot: Timeslot, + pub arrival_port_id: String, + pub arrival_pad_id: String, + pub arrival_timeslot: Timeslot, + pub path: GeoLineString, + pub distance_meters: f32, +} + +/// Attempts to find a pairing of departure and arrival pad +/// timeslots wherein a flight could occur. +pub async fn get_vertipad_timeslot_pairs( + depart_vertiport_id: &String, + arrival_vertiport_id: &String, + mut depart_vertipads: HashMap>, + mut arrive_vertipads: HashMap>, + clients: &GrpcClients, +) -> Result, VertiportError> { + let mut pairs = vec![]; + + let mut best_path_request = BestPathRequest { + node_start_id: depart_vertiport_id.clone(), + node_uuid_end: arrival_vertiport_id.clone(), + start_type: NodeType::Vertiport as i32, + time_start: None, + time_end: None, + }; + + // Iterate through departure pads and their schedules + for (depart_pad_id, depart_schedule) in depart_vertipads.iter_mut() { + depart_schedule.sort_by(|a, b| a.time_end.cmp(&b.time_end)); + + // Iterate through the available timeslots for this pad + 'depart_timeslots: for dts in depart_schedule.iter() { + // Iterate through arrival pads and their schedules + for (arrival_pad_id, arrival_schedule) in arrive_vertipads.iter_mut() { + arrival_schedule.sort_by(|a, b| a.time_start.cmp(&b.time_start)); + + // Iterate through available timeslots for this pad + // There will be several opportunities to break out without + // excess work + for ats in arrival_schedule.iter() { + // no timeslot overlap possible + // | departure timeslot | + // | arrival timeslot | + if dts.time_start >= ats.time_end { + continue; + } + + // Temporary no-fly zones make checking the same route + // multiple times necessary for different timeslots + best_path_request.time_start = Some(dts.time_start.into()); + best_path_request.time_end = Some(ats.time_end.into()); + + let (path, distance_meters) = match best_path(&best_path_request, clients).await + { + Ok((path, distance_meters)) => (path, distance_meters as f32), + Err(BestPathError::NoPathFound) => { + // no path found, perhaps temporary no-fly zone + // is blocking journeys from this depart timeslot + // Break out and try the next depart timeslot + router_debug!( + "(get_vertipad_timeslot_pairs) No path found from vertiport {} + to vertiport {} (from {} to {}).", + depart_vertiport_id, + arrival_vertiport_id, + dts.time_start, + ats.time_end + ); + + break 'depart_timeslots; + } + Err(BestPathError::ClientError) => { + // exit immediately if svc-gis is down, don't allow new flights + router_error!( + "(get_vertipad_timeslot_pairs) Could not determine path." + ); + return Err(VertiportError::ClientError); + } + }; + + let estimated_duration_s = estimate_flight_time_seconds(&distance_meters); + + // Since both schedules are sorted, we can break early once + // departure end time + flight time is less than the arrival timeslot's start time + // and not look at the other timeslots for that pad + // | departure timeslot | + // ---->x + // | arrival timeslot 1 | arrival timeslot 2 | + // (the next arrival timeslot start to be checked would be even further away) + if dts.time_end + estimated_duration_s < ats.time_start { + break; + } + + // + // | dts | (depart timeslot) + // -----> -----> (flight time) + // | ats | (arrival timeslot) + // | actual dts | (actual depart timeslot) + // + // The actual depart_timeslot is the timeslot within which departure + // will result in landing in the arrival timeslot. + let depart_timeslot = Timeslot { + time_start: max(dts.time_start, ats.time_start - estimated_duration_s), + time_end: min(dts.time_end, ats.time_end - estimated_duration_s), + }; + + // + // | dts | (depart timeslot) + // -----> -----> (flight time) + // | ats | (arrival timeslot) + // | actual ats | + // The actual arrival_timeslot is the timeslot within which arrival is possible + // given a departure from the actual depart timeslot. + let arrival_timeslot = Timeslot { + time_start: max( + ats.time_start, + depart_timeslot.time_start + estimated_duration_s, + ), + time_end: min( + ats.time_end, + depart_timeslot.time_end + estimated_duration_s, + ), + }; + + pairs.push(TimeslotPair { + depart_port_id: depart_vertiport_id.clone(), + depart_pad_id: depart_pad_id.clone(), + depart_timeslot, + arrival_port_id: arrival_vertiport_id.clone(), + arrival_pad_id: arrival_pad_id.clone(), + arrival_timeslot, + path, + distance_meters, + }); + } + } + } + } + + // Sort available options by shortest distance first + pairs.sort_by( + |a, b| match a.distance_meters.partial_cmp(&b.distance_meters) { + Some(ord) => ord, + None => { + router_error!( + "(get_vertipad_timeslot_pairs) Could not compare distances: {}, {}", + a.distance_meters, + b.distance_meters + ); + std::cmp::Ordering::Equal + } + }, + ); + + Ok(pairs) +} + +#[cfg(test)] +mod tests { + use super::*; + use crate::grpc::client::get_clients; + use crate::router::vehicle::estimate_flight_time_seconds; + use chrono::DateTime; + use uuid::Uuid; + + #[tokio::test] + #[cfg(feature = "stub_backends")] + async fn ut_get_vertipad_pairs_no_overlap() { + let depart_vertiport_id: String = Uuid::new_v4().to_string(); + let arrive_vertiport_id: String = Uuid::new_v4().to_string(); + let depart_vertipad_id: String = Uuid::new_v4().to_string(); + let arrive_vertipad_id: String = Uuid::new_v4().to_string(); + let clients = get_clients().await; + + // + // CASE 1: No overlap, even leaving at the last minute of the departure window + // |-----v2----| + // >>>>>>>>>>x + // |-----v1----| + // | | | | + // 3 6 10 13 + + let depart_start = DateTime::from_str("2021-01-01T03:00:00Z").unwrap(); + let depart_end = DateTime::from_str("2021-01-01T06:00:00Z").unwrap(); + let depart_vertipads = HashMap::from([( + depart_vertipad_id.clone(), + vec![Timeslot { + time_start: depart_start, + time_end: depart_end, + }], + )]); + + let arrival_vertipads = HashMap::from([( + arrive_vertipad_id.clone(), + vec![Timeslot { + time_start: DateTime::from_str("2021-01-01T10:00:00Z").unwrap(), + time_end: DateTime::from_str("2021-01-01T13:00:00Z").unwrap(), + }], + )]); + + let pairs = get_vertipad_timeslot_pairs( + &depart_vertiport_id, + &arrive_vertiport_id, + depart_vertipads, + arrival_vertipads, + &clients, + ) + .await + .unwrap(); + + assert!(pairs.is_empty()); + } + + #[tokio::test] + #[cfg(feature = "stub_backends")] + async fn ut_get_vertipad_pairs_no_overlap_arrival_lead() { + let depart_vertiport_id: String = Uuid::new_v4().to_string(); + let arrive_vertiport_id: String = Uuid::new_v4().to_string(); + let depart_vertipad_id: String = Uuid::new_v4().to_string(); + let arrive_vertipad_id: String = Uuid::new_v4().to_string(); + let clients = get_clients().await; + + // + // No overlap, arrival window is earlier + // |-----v1----| + // + // |-----v2----| + // | | | | + // 3 6 10 13 + + let depart_start = DateTime::from_str("2021-01-01T06:00:00Z").unwrap(); + let depart_end = DateTime::from_str("2021-01-01T10:00:00Z").unwrap(); + let depart_vertipads = HashMap::from([( + depart_vertipad_id.clone(), + vec![Timeslot { + time_start: depart_start, + time_end: depart_end, + }], + )]); + + let arrival_vertipads = HashMap::from([( + arrive_vertipad_id.clone(), + vec![Timeslot { + time_start: DateTime::from_str("2021-01-01T03:00:00Z").unwrap(), + time_end: DateTime::from_str("2021-01-01T06:00:00Z").unwrap(), + }], + )]); + + let pairs = get_vertipad_timeslot_pairs( + &depart_vertiport_id, + &arrive_vertiport_id, + depart_vertipads, + arrival_vertipads, + &clients, + ) + .await + .unwrap(); + + println!("{:?}", pairs); + assert!(pairs.is_empty()); + } + + #[tokio::test] + #[cfg(feature = "stub_backends")] + async fn ut_get_vertipad_pairs_some_overlap() { + let depart_vertiport_id: String = Uuid::new_v4().to_string(); + let arrive_vertiport_id: String = Uuid::new_v4().to_string(); + let depart_vertipad_id: String = Uuid::new_v4().to_string(); + let arrive_vertipad_id: String = Uuid::new_v4().to_string(); + let clients = get_clients().await; + + // + // Some overlap + // |-----v2----| + // >>>>> Leave at end of depart window + // >>>>> Middle case + // >>>>> Arrive at start of arrival window + // |-----v1----| + // | | | + // 3 6 9 + + let depart_start = DateTime::from_str("2021-01-01T03:00:00Z").unwrap(); + let depart_end = DateTime::from_str("2021-01-01T06:00:00Z").unwrap(); + let depart_vertipads = HashMap::from([( + depart_vertipad_id.clone(), + vec![Timeslot { + time_start: depart_start, + time_end: depart_end, + }], + )]); + + let arrival_start = DateTime::from_str("2021-01-01T06:00:00Z").unwrap(); + let arrival_end = DateTime::from_str("2021-01-01T09:00:00Z").unwrap(); + let arrival_vertipads = HashMap::from([( + arrive_vertipad_id.clone(), + vec![Timeslot { + time_start: arrival_start, + time_end: arrival_end, + }], + )]); + + let pairs = get_vertipad_timeslot_pairs( + &depart_vertiport_id, + &arrive_vertiport_id, + depart_vertipads, + arrival_vertipads, + &clients, + ) + .await + .unwrap(); + + assert_eq!(pairs.len(), 1); + let pair = pairs.last().unwrap(); + let flight_duration = estimate_flight_time_seconds(&pair.distance_meters); + + assert_eq!(pair.depart_pad_id, depart_vertipad_id); + assert_eq!(pair.arrival_pad_id, arrive_vertipad_id); + assert_eq!( + pair.depart_timeslot.time_start, + arrival_start - flight_duration + ); + + assert_eq!(pair.depart_timeslot.time_end, depart_end); + assert_eq!(pair.arrival_timeslot.time_start, arrival_start); + assert_eq!(pair.arrival_timeslot.time_end, depart_end + flight_duration); + } + + #[tokio::test] + #[cfg(feature = "stub_backends")] + async fn ut_get_vertipad_pairs_overlap_nested() { + let depart_vertiport_id: String = Uuid::new_v4().to_string(); + let arrive_vertiport_id: String = Uuid::new_v4().to_string(); + let depart_vertipad_id: String = Uuid::new_v4().to_string(); + let arrive_vertipad_id: String = Uuid::new_v4().to_string(); + let clients = get_clients().await; + // + // Some overlap + // |-----v2---| + // >>> Arrive at end of arrival window + // >>> Middle case + // >>> Arrive at start of arrival window + // |-----v1----------------| + // | | | + // 3 6 9 + + let depart_start = DateTime::from_str("2021-01-01T03:00:00Z").unwrap(); + let depart_end = DateTime::from_str("2021-01-01T09:00:00Z").unwrap(); + let depart_vertipads = HashMap::from([( + depart_vertipad_id.clone(), + vec![Timeslot { + time_start: depart_start, + time_end: depart_end, + }], + )]); + + let arrival_start = DateTime::from_str("2021-01-01T05:00:00Z").unwrap(); + let arrival_end = DateTime::from_str("2021-01-01T07:00:00Z").unwrap(); + let arrival_vertipads = HashMap::from([( + arrive_vertipad_id.clone(), + vec![Timeslot { + time_start: arrival_start, + time_end: arrival_end, + }], + )]); + + let pairs = get_vertipad_timeslot_pairs( + &depart_vertiport_id, + &arrive_vertiport_id, + depart_vertipads, + arrival_vertipads, + &clients, + ) + .await + .unwrap(); + + assert_eq!(pairs.len(), 1); + let pair = pairs.last().unwrap(); + let flight_duration = estimate_flight_time_seconds(&pair.distance_meters); + + assert_eq!(pair.depart_pad_id, depart_vertipad_id); + assert_eq!(pair.arrival_pad_id, arrive_vertipad_id); + assert_eq!( + pair.depart_timeslot.time_start, + arrival_start - flight_duration + ); + assert_eq!(pair.depart_timeslot.time_end, arrival_end - flight_duration); + assert_eq!(pair.arrival_timeslot.time_start, arrival_start); + assert_eq!(pair.arrival_timeslot.time_end, arrival_end); + } + + #[tokio::test] + #[cfg(feature = "stub_backends")] + async fn ut_get_vertipad_pairs_overlap_arrival_window_lead() { + let depart_vertiport_id: String = Uuid::new_v4().to_string(); + let arrive_vertiport_id: String = Uuid::new_v4().to_string(); + let depart_vertipad_id: String = Uuid::new_v4().to_string(); + let arrive_vertipad_id: String = Uuid::new_v4().to_string(); + let clients = get_clients().await; + + // + // Some overlap, arrival window leads + // |-------v2-----| + // >>> + // |-----------| + // | | | + // 3 6 9 + + let depart_start = DateTime::from_str("2021-01-01T06:00:00Z").unwrap(); + let depart_end = DateTime::from_str("2021-01-01T09:00:00Z").unwrap(); + let depart_vertipads = HashMap::from([( + depart_vertipad_id.clone(), + vec![Timeslot { + time_start: depart_start, + time_end: depart_end, + }], + )]); + + let arrival_start = DateTime::from_str("2021-01-01T03:00:00Z").unwrap(); + let arrival_end = DateTime::from_str("2021-01-01T07:00:00Z").unwrap(); + let arrival_vertipads = HashMap::from([( + arrive_vertipad_id.clone(), + vec![Timeslot { + time_start: arrival_start, + time_end: arrival_end, + }], + )]); + + let pairs = get_vertipad_timeslot_pairs( + &depart_vertiport_id, + &arrive_vertiport_id, + depart_vertipads, + arrival_vertipads, + &clients, + ) + .await + .unwrap(); + + assert_eq!(pairs.len(), 1); + let pair = pairs.last().unwrap(); + let flight_duration = estimate_flight_time_seconds(&pair.distance_meters); + + assert_eq!(pair.depart_pad_id, depart_vertipad_id); + assert_eq!(pair.arrival_pad_id, arrive_vertipad_id); + assert_eq!(pair.depart_timeslot.time_start, depart_start); + assert_eq!(pair.depart_timeslot.time_end, arrival_end - flight_duration); + assert_eq!( + pair.arrival_timeslot.time_start, + depart_start + flight_duration + ); + assert_eq!(pair.arrival_timeslot.time_end, arrival_end); + } + + #[tokio::test] + #[cfg(feature = "stub_backends")] + async fn ut_get_vertipad_pairs_overlap_multiple() { + let depart_vertiport_id: String = Uuid::new_v4().to_string(); + let arrive_vertiport_id: String = Uuid::new_v4().to_string(); + let depart_vertipad_id: String = Uuid::new_v4().to_string(); + let arrive_vertipad_id: String = Uuid::new_v4().to_string(); + let clients = get_clients().await; + + // + // Some overlap + // |-----v2-p1--| |-----v2-p2--| + // + // |-----v1----------------| + // | | | + // 3 6 9 + + let depart_start = DateTime::from_str("2021-01-01T03:00:00Z").unwrap(); + let depart_end = DateTime::from_str("2021-01-01T09:00:00Z").unwrap(); + let depart_vertipads = HashMap::from([( + depart_vertipad_id.clone(), + vec![Timeslot { + time_start: depart_start, + time_end: depart_end, + }], + )]); + + let arrival_timeslot_1 = Timeslot { + time_start: DateTime::from_str("2021-01-01T05:00:00Z").unwrap(), + time_end: DateTime::from_str("2021-01-01T07:00:00Z").unwrap(), + }; + + let arrival_timeslot_2 = Timeslot { + time_start: DateTime::from_str("2021-01-01T09:00:00Z").unwrap(), + time_end: DateTime::from_str("2021-01-01T10:00:00Z").unwrap(), + }; + + let arrival_vertipads = HashMap::from([( + arrive_vertipad_id.clone(), + vec![arrival_timeslot_1, arrival_timeslot_2], + )]); + + let pairs = get_vertipad_timeslot_pairs( + &depart_vertiport_id, + &arrive_vertiport_id, + depart_vertipads, + arrival_vertipads, + &clients, + ) + .await + .unwrap(); + + assert_eq!(pairs.len(), 2); + + { + let pair = pairs[0].clone(); + let arrival_timeslot = arrival_timeslot_1; + assert_eq!(pair.depart_pad_id, depart_vertipad_id); + assert_eq!(pair.arrival_pad_id, arrive_vertipad_id); + + let flight_duration = estimate_flight_time_seconds(&pair.distance_meters); + assert_eq!( + pair.depart_timeslot.time_start, + arrival_timeslot.time_start - flight_duration + ); + + assert_eq!( + pair.depart_timeslot.time_end, + arrival_timeslot.time_end - flight_duration + ); + + assert_eq!( + pair.arrival_timeslot.time_start, + arrival_timeslot.time_start + ); + assert_eq!(pair.arrival_timeslot.time_end, arrival_timeslot.time_end); + } + + { + let pair = pairs[1].clone(); + let arrival_timeslot = arrival_timeslot_2; + assert_eq!(pair.depart_pad_id, depart_vertipad_id); + assert_eq!(pair.arrival_pad_id, arrive_vertipad_id); + + let flight_duration = estimate_flight_time_seconds(&pair.distance_meters); + assert_eq!( + pair.depart_timeslot.time_start, + arrival_timeslot.time_start - flight_duration + ); + + assert_eq!(pair.depart_timeslot.time_end, depart_end); + + assert_eq!( + pair.arrival_timeslot.time_start, + arrival_timeslot.time_start + ); + assert_eq!(pair.arrival_timeslot.time_end, depart_end + flight_duration); + } + } +} diff --git a/server/src/test_util.rs b/server/src/test_util.rs index c6cb75a..f2c65cf 100644 --- a/server/src/test_util.rs +++ b/server/src/test_util.rs @@ -1,6 +1,5 @@ /// test utilities. Provides functions to inject mock data. use crate::grpc::client::get_clients; -use crate::router::router_types::location::Location; use chrono::{TimeZone, Utc}; use lib_common::log_macros; use ordered_float::OrderedFloat; @@ -9,19 +8,6 @@ use tokio::sync::OnceCell; log_macros!("unit_test", "test::unit"); -/// SF central location -pub static SAN_FRANCISCO: Location = Location { - latitude: OrderedFloat(37.7749), - longitude: OrderedFloat(-122.4194), - altitude_meters: OrderedFloat(0.0), -}; -/// Montara central location -pub static MONTARA: Location = Location { - latitude: OrderedFloat(37.52123), - longitude: OrderedFloat(-122.50892), - altitude_meters: OrderedFloat(0.0), -}; - /* static VERTIPORTS_MOCK: OnceCell> = tokio::sync::OnceCell::const_new(); static VERTIPADS_MOCK: OnceCell> = tokio::sync::OnceCell::const_new();