From da0cb7a851878a56bbed56ad62a4b68b20ffc103 Mon Sep 17 00:00:00 2001 From: Loup Federico <16464925+Sh099078@users.noreply.github.com> Date: Mon, 28 Oct 2024 14:46:16 +0100 Subject: [PATCH] editoast: split temporary_speed_limit_group into two endpoints - One endpoint creates temporary speed limits and their group. - Another endpoint returns the list of track ranges inside an area delimited by a list of entries and exits (directed locations on the tracks, quite similar to the position and direction of signs). Signed-off-by: Loup Federico <16464925+Sh099078@users.noreply.github.com> --- editoast/openapi.yaml | 91 +- editoast/src/views/infra/delimited_area.rs | 837 +++++++++++++++ editoast/src/views/infra/mod.rs | 2 + editoast/src/views/temporary_speed_limits.rs | 1005 +----------------- front/src/common/api/generatedEditoastApi.ts | 36 +- 5 files changed, 1000 insertions(+), 971 deletions(-) create mode 100644 editoast/src/views/infra/delimited_area.rs diff --git a/editoast/openapi.yaml b/editoast/openapi.yaml index 0d296861c6d..82523ddb437 100644 --- a/editoast/openapi.yaml +++ b/editoast/openapi.yaml @@ -674,6 +674,83 @@ paths: minimum: 0 '404': description: Infra ID not found + /infra/{infra_id}/delimited_area: + post: + tags: + - delimited_area + parameters: + - name: infra_id + in: path + description: An existing infra ID + required: true + schema: + type: integer + format: int64 + requestBody: + content: + application/json: + schema: + type: object + required: + - track_ranges + properties: + track_ranges: + type: array + items: + type: object + required: + - track + - begin + - end + - direction + properties: + begin: + type: number + format: double + direction: + $ref: '#/components/schemas/Direction' + end: + type: number + format: double + track: + type: string + maxLength: 255 + minLength: 1 + additionalProperties: false + required: true + responses: + '200': + description: The track ranges between a list entries and exits. + content: + application/json: + schema: + type: object + required: + - track_ranges + properties: + track_ranges: + type: array + items: + type: object + required: + - track + - begin + - end + - direction + properties: + begin: + type: number + format: double + direction: + $ref: '#/components/schemas/Direction' + end: + type: number + format: double + track: + type: string + maxLength: 255 + minLength: 1 + additionalProperties: false /infra/{infra_id}/errors: get: tags: @@ -2336,12 +2413,8 @@ paths: type: object required: - speed_limit_group_name - - infra_id - speed_limits properties: - infra_id: - type: integer - format: int64 speed_limit_group_name: type: string speed_limits: @@ -2351,7 +2424,7 @@ paths: required: - start_date_time - end_date_time - - signals + - track_ranges - speed_limit - obj_id properties: @@ -2360,16 +2433,16 @@ paths: format: date-time obj_id: type: string - signals: - type: array - items: - $ref: '#/components/schemas/Sign' speed_limit: type: number format: double start_date_time: type: string format: date-time + track_ranges: + type: array + items: + $ref: '#/components/schemas/DirectionalTrackRange' required: true responses: '201': diff --git a/editoast/src/views/infra/delimited_area.rs b/editoast/src/views/infra/delimited_area.rs new file mode 100644 index 00000000000..5801cd09c79 --- /dev/null +++ b/editoast/src/views/infra/delimited_area.rs @@ -0,0 +1,837 @@ +use crate::error::Result; +use crate::infra_cache::{Graph, InfraCache}; +use crate::models::Infra; +use crate::views::infra::{InfraApiError, InfraIdParam}; +use crate::views::{AuthenticationExt, AuthorizationError}; +use crate::AppState; +use crate::Retrieve; +use axum::extract::{Path, State}; +use axum::{Extension, Json}; +use editoast_authz::BuiltinRole; +use editoast_models::DbConnectionPoolV2; +use editoast_schemas::{ + infra::{Direction, DirectionalTrackRange, Endpoint, Sign, TrackEndpoint}, + primitives::Identifier, +}; +use itertools::Itertools; +use serde::{Deserialize, Serialize}; +use std::{ + cmp::Ordering, + collections::{HashMap, HashSet}, +}; +use utoipa::ToSchema; + +crate::routes! { + "/delimited_area" => delimited_area, +} + +// Maximum distance the graph can be explored from a speed limit execution signal +// without finding any legitimate ending to the speed limit before it is considered +// there is not valid limit on the portion of the graph that is being explored. +// TODO Magic number for now. Make it configurable ? +#[cfg(not(test))] +const MAXIMUM_DISTANCE: f64 = 80000.; +#[cfg(test)] +const MAXIMUM_DISTANCE: f64 = 5000.; + +#[derive(Deserialize, ToSchema)] +struct DelimitedAreaForm { + #[schema(inline)] + entries: Vec, + #[schema(inline)] + exits: Vec, +} + +#[derive(Deserialize, Serialize, ToSchema)] +struct DelimitedAreaResponse { + #[schema(inline)] + track_ranges: Vec, +} + +#[derive(Debug, Deserialize, ToSchema)] +struct DirectedLocation { + pub track: Identifier, + pub position: f64, + pub direction: Direction, +} + +impl From for DirectedLocation { + fn from(value: Sign) -> Self { + let Sign { + track, + position, + direction, + .. + } = value; + DirectedLocation { + track, + position, + direction, + } + } +} + +#[utoipa::path( + post, path = "", + tag = "delimited_area", + params(InfraIdParam), + request_body = inline(DelimitedAreaResponse), + responses( + (status = 200, body = inline(DelimitedAreaResponse), description = "The track ranges between a list entries and exits." ), + ) +)] +async fn delimited_area( + Extension(auth): AuthenticationExt, + State(db_pool): State, + State(app_state): State, + Path(InfraIdParam { infra_id }): Path, + Json(DelimitedAreaForm { entries, exits }): Json, +) -> Result> { + let authorized = auth + .check_roles([BuiltinRole::InfraWrite].into()) + .await + .map_err(AuthorizationError::AuthError)?; + if !authorized { + return Err(AuthorizationError::Unauthorized.into()); + } + + // Retrieve the infra + + let conn = &mut db_pool.get().await?; + let infra_caches = app_state.infra_caches.clone(); + let infra = + Infra::retrieve_or_fail(conn, infra_id, || InfraApiError::NotFound { infra_id }).await?; + let infra_cache = InfraCache::get_or_load(conn, &infra_caches, &infra).await?; + let graph = Graph::load(&infra_cache); + + // Retrieve the track ranges + + Ok(Json(DelimitedAreaResponse { + track_ranges: track_ranges_from_locations(entries, exits, &graph, &infra_cache), + })) +} + +fn track_ranges_from_locations( + entries: Vec, + exits: Vec, + graph: &Graph, + infra_cache: &InfraCache, +) -> Vec { + let mut track_ranges: Vec = Vec::new(); + entries.iter().for_each(|entry| { + track_ranges.extend(impacted_tracks( + entry, + exits.iter().collect::>(), + graph, + infra_cache, + MAXIMUM_DISTANCE, + )) + }); + track_ranges +} + +fn impacted_tracks( + entry: &DirectedLocation, + exits: Vec<&DirectedLocation>, + graph: &Graph, + infra_cache: &InfraCache, + max_distance: f64, +) -> Vec { + // Map track identifiers to their list of associated exits: + let mut tracks_to_exits: HashMap<&Identifier, Vec<&DirectedLocation>> = HashMap::new(); + exits.into_iter().for_each(|exit| { + let track_id = &exit.track; + if let Some(track_exits) = tracks_to_exits.get_mut(track_id) { + track_exits.push(exit); + } else { + let track_exits = vec![exit]; + tracks_to_exits.insert(track_id, track_exits); + }; + }); + let exits = tracks_to_exits; + + // Directional track ranges reachable from `entry` during the graph exploration. + let mut related_track_ranges: Vec = Vec::new(); + + // TrackEndpoint right after the entry location (in the correct direction): + let first_track_endpoint = TrackEndpoint { + endpoint: match entry.direction { + Direction::StartToStop => Endpoint::End, + Direction::StopToStart => Endpoint::Begin, + }, + track: entry.track.clone(), + }; + + if let Some(immediate_exit) = closest_exit_from_entry(entry, exits.get(&entry.track)) { + if let Some(only_track_range) = track_range_between_two_locations(entry, immediate_exit) { + return vec![only_track_range]; + } else { + return vec![]; + } + } else if let Some(first_track_range) = + track_range_between_endpoint_and_location(entry, &first_track_endpoint, infra_cache, true) + { + related_track_ranges.push(first_track_range); + } else { + return vec![]; + }; + + // Identifiers of the track sections that have already been reached and should be ignored: + let mut visited_tracks: HashSet<&TrackEndpoint> = HashSet::new(); + + // Neighbors of the explored tracks, i.e. the tracks that should be visited next: + let mut next_tracks: Vec<(&TrackEndpoint, f64)> = Vec::new(); + let remaining_distance = + max_distance - (related_track_ranges[0].end - related_track_ranges[0].begin); + if 0. < remaining_distance { + let neighbours = graph + .get_all_neighbours(&first_track_endpoint) + .into_iter() + .map(|neighbour| (neighbour, remaining_distance)) + .collect::>(); + next_tracks.extend(neighbours); + } + + while let Some((curr_track_endpoint, remaining_distance)) = next_tracks.pop() { + let curr_track_id = &curr_track_endpoint.track; + + if !visited_tracks.insert(curr_track_endpoint) { + // Track already visited + continue; + } + + // Check if there is an exit location on that track range + if let Some(exit) = + closest_exit_from_endpoint(curr_track_endpoint, exits.get(&curr_track_id)) + { + // End the search on that track, add the current track with the correct offset + let track_range = track_range_between_endpoint_and_location( + exit, + curr_track_endpoint, + infra_cache, + false, + ) + .unwrap_or_else(|| { + panic!( + "Cannot form a track from {:?} and {:?}", + exit, curr_track_endpoint + ) + }); + related_track_ranges.push(track_range); + } else { + let track_range = + track_range_from_endpoint(curr_track_endpoint, remaining_distance, infra_cache) + .unwrap_or_else(|| { + panic!("Cannot form a track from {:?}", curr_track_endpoint) + }); + let neighbours_remaining_distance = + remaining_distance - (track_range.end - track_range.begin); + related_track_ranges.push(track_range); + if 0. < neighbours_remaining_distance { + let opposite_track_endpoint = TrackEndpoint { + endpoint: match curr_track_endpoint.endpoint { + Endpoint::Begin => Endpoint::End, + Endpoint::End => Endpoint::Begin, + }, + track: curr_track_endpoint.track.clone(), + }; + let neighbours = graph + .get_all_neighbours(&opposite_track_endpoint) + .into_iter() + .map(|neighbour| (neighbour, neighbours_remaining_distance)) + .collect::>(); + next_tracks.extend(neighbours); + } + } + } + related_track_ranges +} + +/// Return the closest exit that applies on a track from a starting endpoint. +/// To be applicable, an exit must be in the correct direction. +fn closest_exit_from_endpoint<'a>( + track_endpoint: &TrackEndpoint, + exits: Option<&'a Vec<&DirectedLocation>>, +) -> Option<&'a DirectedLocation> { + if let Some(exits) = exits { + exits + .iter() + .filter(|exit| exit.track == track_endpoint.track) + .filter(|exit| match track_endpoint.endpoint { + Endpoint::Begin => exit.direction == Direction::StartToStop, + Endpoint::End => exit.direction == Direction::StopToStart, + }) + .sorted_by( + |e_1, e_2| match (track_endpoint.endpoint, e_1.position < e_2.position) { + (Endpoint::Begin, true) | (Endpoint::End, false) => Ordering::Less, + (Endpoint::Begin, false) | (Endpoint::End, true) => Ordering::Greater, + }, + ) + .map(|exit| &**exit) + .next() + } else { + None + } +} + +/// Return the closest applicable exit that is on the same track as the `entry`, or `None`. +/// if there is none. +fn closest_exit_from_entry<'a>( + entry: &DirectedLocation, + exits: Option<&'a Vec<&DirectedLocation>>, +) -> Option<&'a DirectedLocation> { + if let Some(exits) = exits { + exits + .iter() + .filter(|exit| exit.track == entry.track) + .filter(|exit| entry.direction == exit.direction) + .filter(|exit| match entry.direction { + Direction::StartToStop => entry.position < exit.position, + Direction::StopToStart => exit.position < entry.position, + }) + .sorted_by( + |e_1, e_2| match (entry.direction, e_1.position < e_2.position) { + (Direction::StartToStop, true) | (Direction::StopToStart, false) => { + Ordering::Less + } + (Direction::StartToStop, false) | (Direction::StopToStart, true) => { + Ordering::Greater + } + }, + ) + .map(|exit| &**exit) + .next() + } else { + None + } +} + +/// Return the directional track range starting at `entry` finishing at `exit`, or `None` +/// if no track range can be built from them. +fn track_range_between_two_locations( + entry: &DirectedLocation, + exit: &DirectedLocation, +) -> Option { + let exit_before_entry = match entry.direction { + Direction::StartToStop => exit.position < entry.position, + Direction::StopToStart => entry.position < exit.position, + }; + if entry.direction != exit.direction || entry.track != exit.track || exit_before_entry { + return None; + } + Some(DirectionalTrackRange { + track: entry.track.clone(), + begin: f64::min(entry.position, exit.position), + end: f64::max(entry.position, exit.position), + direction: entry.direction, + }) +} + +/// Return the directional track range delimited by a location and a track endpoint, or `None` if no +/// track range can be built from them. +fn track_range_between_endpoint_and_location( + location: &DirectedLocation, + endpoint: &TrackEndpoint, + infra_cache: &InfraCache, + entry: bool, +) -> Option { + let mut location_on_correct_direction = matches!( + (location.direction, endpoint.endpoint), + (Direction::StartToStop, Endpoint::End) | (Direction::StopToStart, Endpoint::Begin) + ); + if entry { + location_on_correct_direction = !location_on_correct_direction; + } + + if location_on_correct_direction { + None + } else if let Some(track) = infra_cache.track_sections().get(&location.track.0) { + let track_length = track.unwrap_track_section().length; + let (begin_offset, end_offset) = match endpoint.endpoint { + Endpoint::Begin => (0., location.position), + Endpoint::End => (location.position, track_length), + }; + let track_range = DirectionalTrackRange { + track: location.track.clone(), + begin: begin_offset, + end: end_offset, + direction: location.direction, + }; + Some(track_range) + } else { + None + } +} + +/// Build a directional track range starting at `track_endpoint` and stopping at the end of the track +/// range if it is shorter than `remaining_distance`, or at `remaining_distance` from `track_endpoint` +/// otherwise. Returns: the built track range or `None` if the track does not exist in `infra_cache`. +fn track_range_from_endpoint( + track_endpoint: &TrackEndpoint, + remaining_distance: f64, + infra_cache: &InfraCache, +) -> Option { + if let Some(track) = infra_cache.track_sections().get(&track_endpoint.track.0) { + let track_length = track.unwrap_track_section().length; + let direction = match track_endpoint.endpoint { + Endpoint::Begin => Direction::StartToStop, + Endpoint::End => Direction::StopToStart, + }; + let track_range_length = if track_length < remaining_distance { + track_length + } else { + remaining_distance + }; + let (begin_offset, end_offset) = match direction { + Direction::StartToStop => (0., track_range_length), + Direction::StopToStart => (track_length - track_range_length, track_length), + }; + Some(DirectionalTrackRange { + track: track_endpoint.track.clone(), + begin: begin_offset, + end: end_offset, + direction, + }) + } else { + None + } +} + +#[cfg(test)] +mod tests { + use crate::models::fixtures::create_small_infra; + use crate::models::Infra; + use crate::views::infra::delimited_area::DelimitedAreaResponse; + use crate::views::test_app::TestAppBuilder; + use axum::http::StatusCode; + use editoast_schemas::infra::{Direction, DirectionalTrackRange}; + use rstest::rstest; + use serde_json::{json, Value}; + + /// Create a temporary speed limit through with a given signal list and `small_infra` id through + /// the creation endpoint, then retrieve from the database the persisted track sections for that + /// speed limit. + async fn get_track_ranges_request(entries: Value, exits: Value) -> Vec { + let app = TestAppBuilder::default_app(); + let pool = app.db_pool(); + let Infra { id: infra_id, .. } = create_small_infra(&mut pool.get_ok()).await; + let request = app + .post(&format!("/infra/{infra_id}/delimited_area")) + .json(&json!({ + "infra_id": infra_id, + "entries": entries, + "exits": exits, + } + )); + let DelimitedAreaResponse { track_ranges } = + app.fetch(request).assert_status(StatusCode::OK).json_into(); + + track_ranges + } + + #[rstest] + async fn same_track_start_to_stop() { + let entries = json!([ + { + "track": "TH1", + "position": 100., + "direction": "START_TO_STOP", + }, + ]); + let exits = json!([ + { + "track": "TH1", + "position": 200., + "direction": "START_TO_STOP", + }, + ]); + let retrieved_track_ranges = get_track_ranges_request(entries, exits).await; + let expected_track_ranges = vec![DirectionalTrackRange { + track: "TH1".into(), + begin: 100., + end: 200., + direction: Direction::StartToStop, + }]; + assert_eq!(expected_track_ranges, retrieved_track_ranges); + } + + #[rstest] + async fn same_track_stop_to_start() { + let entries = json!([ + { + "track": "TH1", + "position": 200., + "direction": "STOP_TO_START", + }, + ]); + let exits = json!([ + { + "track": "TH1", + "position": 100., + "direction": "STOP_TO_START", + } + ]); + let retrieved_track_ranges = get_track_ranges_request(entries, exits).await; + let expected_track_ranges = vec![DirectionalTrackRange { + track: "TH1".into(), + begin: 100., + end: 200., + direction: Direction::StopToStart, + }]; + assert_eq!(expected_track_ranges, retrieved_track_ranges); + } + + #[rstest] + async fn tunnel_on_two_tracks() { + let entries = json!([ + { + "track": "TF1", + "position": 100., + "direction": "STOP_TO_START", + }, + ]); + let exits = json!([ + { + "track": "TF0", + "position": 2., + "direction": "STOP_TO_START", + }, + ]); + let retrieved_track_ranges = get_track_ranges_request(entries, exits).await; + let expected_track_ranges = vec![ + DirectionalTrackRange { + track: "TF1".into(), + begin: 0., + end: 100., + direction: Direction::StopToStart, + }, + DirectionalTrackRange { + track: "TF0".into(), + begin: 2., + end: 3., + direction: Direction::StopToStart, + }, + ]; + assert_eq!(expected_track_ranges, retrieved_track_ranges); + } + + #[rstest] + async fn both_point_switch_directions_get_explored() { + let entries = json!([ + { + "track": "TG1", + "position": 100., + "direction": "START_TO_STOP", + }, + ]); + let exits = json!([ + { + "track": "TG3", + "position": 50., + "direction": "START_TO_STOP", + }, + { + "track": "TG4", + "position": 150., + "direction": "START_TO_STOP", + }, + ]); + let mut retrieved_track_ranges = get_track_ranges_request(entries, exits).await; + let mut expected_track_ranges = vec![ + DirectionalTrackRange { + track: "TG1".into(), + begin: 100., + end: 4000., + direction: Direction::StartToStop, + }, + DirectionalTrackRange { + track: "TG3".into(), + begin: 0., + end: 50., + direction: Direction::StartToStop, + }, + DirectionalTrackRange { + track: "TG4".into(), + begin: 0., + end: 150., + direction: Direction::StartToStop, + }, + ]; + expected_track_ranges.sort_by(|lhs, rhs| lhs.track.0.cmp(&rhs.track.0)); + retrieved_track_ranges.sort_by(|lhs, rhs| lhs.track.0.cmp(&rhs.track.0)); + assert_eq!(expected_track_ranges, retrieved_track_ranges); + } + + #[rstest] + async fn multiple_isolated_entry_signals() { + let entries = json!([ + { + "track": "TF1", + "position": 100., + "direction": "STOP_TO_START", + }, + { + "track": "TG1", + "position": 100., + "direction": "START_TO_STOP", + }, + ]); + let exits = json!([ + { + "track": "TF0", + "position": 2., + "direction": "STOP_TO_START", + }, + { + "track": "TG3", + "position": 50., + "direction": "START_TO_STOP", + }, + { + "track": "TG4", + "position": 150., + "direction": "START_TO_STOP", + }, + ]); + let mut retrieved_track_ranges = get_track_ranges_request(entries, exits).await; + let mut expected_track_ranges = vec![ + DirectionalTrackRange { + track: "TF1".into(), + begin: 0., + end: 100., + direction: Direction::StopToStart, + }, + DirectionalTrackRange { + track: "TF0".into(), + begin: 2., + end: 3., + direction: Direction::StopToStart, + }, + DirectionalTrackRange { + track: "TG1".into(), + begin: 100., + end: 4000., + direction: Direction::StartToStop, + }, + DirectionalTrackRange { + track: "TG3".into(), + begin: 0., + end: 50., + direction: Direction::StartToStop, + }, + DirectionalTrackRange { + track: "TG4".into(), + begin: 0., + end: 150., + direction: Direction::StartToStop, + }, + ]; + expected_track_ranges.sort_by(|lhs, rhs| lhs.track.0.cmp(&rhs.track.0)); + retrieved_track_ranges.sort_by(|lhs, rhs| lhs.track.0.cmp(&rhs.track.0)); + assert_eq!(expected_track_ranges, retrieved_track_ranges); + } + + #[rstest] + async fn signals_facing_opposite_direction_are_ignored() { + let entries = json!([ + { + "track": "TF1", + "position": 100., + "direction": "STOP_TO_START", + }, + ]); + let exits = json!([ + { + "track": "TF0", + "position": 2., + "direction": "START_TO_STOP", + }, + { + "track": "TF0", + "position": 1., + "direction": "STOP_TO_START", + }, + ]); + let retrieved_track_ranges = get_track_ranges_request(entries, exits).await; + let expected_track_ranges = vec![ + DirectionalTrackRange { + track: "TF1".into(), + begin: 0., + end: 100., + direction: Direction::StopToStart, + }, + DirectionalTrackRange { + track: "TF0".into(), + begin: 1., + end: 3., + direction: Direction::StopToStart, + }, + ]; + assert_eq!(expected_track_ranges, retrieved_track_ranges); + } + + #[rstest] + async fn track_range_is_built_from_the_closest_exit() { + let entries = json!([ + { + "track": "TF1", + "position": 100., + "direction": "STOP_TO_START", + }, + ]); + let exits = json!([ + { + "track": "TF0", + "position": 2., + "direction": "STOP_TO_START", + }, + { + "track": "TF0", + "position": 1., + "direction": "STOP_TO_START", + }, + ]); + let retrieved_track_ranges = get_track_ranges_request(entries, exits).await; + let expected_track_ranges = vec![ + DirectionalTrackRange { + track: "TF1".into(), + begin: 0., + end: 100., + direction: Direction::StopToStart, + }, + DirectionalTrackRange { + track: "TF0".into(), + begin: 2., + end: 3., + direction: Direction::StopToStart, + }, + ]; + assert_eq!(expected_track_ranges, retrieved_track_ranges); + } + + #[rstest] + async fn exit_before_entry_is_ignored() { + // The graph exploration should not stop if there is an exit signal on the same track + // as the entry signal when the exit signal is behind the entry signal. + let entries = json!([ + { + "track": "TF1", + "position": 100., + "direction": "STOP_TO_START", + }, + ]); + let exits = json!([ + { + "track": "TF1", + "position": 150., + "direction": "STOP_TO_START", + }, + { + "track": "TF0", + "position": 2., + "direction": "STOP_TO_START", + }, + ]); + let retrieved_track_ranges = get_track_ranges_request(entries, exits).await; + let expected_track_ranges = vec![ + DirectionalTrackRange { + track: "TF1".into(), + begin: 0., + end: 100., + direction: Direction::StopToStart, + }, + DirectionalTrackRange { + track: "TF0".into(), + begin: 2., + end: 3., + direction: Direction::StopToStart, + }, + ]; + assert_eq!(expected_track_ranges, retrieved_track_ranges); + } + + #[rstest] + async fn closest_exit_ignores_exits_before_entry() { + // If the LTV is a single track range, it should ignore the signals behind it when + // checking which one is the closest. + let entries = json!([ + { + "track": "TF1", + "position": 400., + "direction": "STOP_TO_START", + }, + ]); + let exits = json!([ + { + "track": "TF1", + "position": 500., + "direction": "STOP_TO_START", + }, + { + "track": "TF1", + "position": 100., + "direction": "STOP_TO_START", + }, + ]); + let retrieved_track_ranges = get_track_ranges_request(entries, exits).await; + let expected_track_ranges = vec![DirectionalTrackRange { + track: "TF1".into(), + begin: 100., + end: 400., + direction: Direction::StopToStart, + }]; + assert_eq!(expected_track_ranges, retrieved_track_ranges); + } + + #[rstest] + async fn exploration_stops_when_resume_signal_is_missing_and_maximum_distance_is_reached() { + let entries = json!([ + { + "track": "TE0", + "position": 500., + "direction": "START_TO_STOP", + "kp": String::new(), + }, + ]); + let mut retrieved_track_ranges = get_track_ranges_request(entries, json!([])).await; + let mut expected_track_ranges = vec![ + DirectionalTrackRange { + track: "TE0".into(), + begin: 500., + end: 1500., + direction: Direction::StartToStop, + }, + DirectionalTrackRange { + track: "TF0".into(), + begin: 0., + end: 3., + direction: Direction::StartToStop, + }, + DirectionalTrackRange { + track: "TF1".into(), + begin: 0., + end: 3997., + direction: Direction::StartToStop, + }, + ]; + expected_track_ranges.sort_by(|lhs, rhs| lhs.track.0.cmp(&rhs.track.0)); + retrieved_track_ranges.sort_by(|lhs, rhs| lhs.track.0.cmp(&rhs.track.0)); + assert_eq!(expected_track_ranges, retrieved_track_ranges); + } + + #[rstest] + #[ignore] + async fn track_section_can_be_explored_in_both_directions() { + // TODO find a way to test it on small_infra or make a specific infra for this test + todo!() + } + + #[rstest] + #[ignore] + async fn adjacent_track_ranges_are_merged() { + // If two directional track ranges are adjacent and have the same direction, + // they should be merged into a single bigger directional track range. + // N.B. This is mostly a performance issue. + unimplemented!(); + } +} diff --git a/editoast/src/views/infra/mod.rs b/editoast/src/views/infra/mod.rs index 0e12f47ab9b..46fc56ea116 100644 --- a/editoast/src/views/infra/mod.rs +++ b/editoast/src/views/infra/mod.rs @@ -1,5 +1,6 @@ mod attached; mod auto_fixes; +mod delimited_area; mod edition; mod errors; mod lines; @@ -60,6 +61,7 @@ crate::routes! { &attached, &edition, &errors, + &delimited_area, get, "/load" => load, diff --git a/editoast/src/views/temporary_speed_limits.rs b/editoast/src/views/temporary_speed_limits.rs index d61e5e2642c..957d9563e6a 100644 --- a/editoast/src/views/temporary_speed_limits.rs +++ b/editoast/src/views/temporary_speed_limits.rs @@ -5,61 +5,31 @@ use chrono::NaiveDateTime; use chrono::Utc; use editoast_derive::EditoastError; use editoast_models::DbConnectionPoolV2; -use editoast_schemas::infra::{Direction, Endpoint, TrackEndpoint}; -use editoast_schemas::infra::{DirectionalTrackRange, Sign}; -use editoast_schemas::primitives::Identifier; -use itertools::Itertools; +use editoast_schemas::infra::DirectionalTrackRange; use serde::de::Error as SerdeError; use serde::{Deserialize, Serialize}; -use std::cmp::Ordering; -use std::collections::{HashMap, HashSet}; -use std::iter::Extend; use std::result::Result as StdResult; use thiserror::Error; use utoipa::ToSchema; use crate::error::InternalError; use crate::error::Result; -use crate::infra_cache::Graph; -use crate::infra_cache::InfraCache; use crate::models::temporary_speed_limits::TemporarySpeedLimit; use crate::models::temporary_speed_limits::TemporarySpeedLimitGroup; use crate::models::Changeset; -use crate::models::Infra; use crate::views::AuthenticationExt; use crate::views::AuthorizationError; -use crate::AppState; use crate::Create; use crate::CreateBatch; use crate::Model; -use crate::Retrieve; use editoast_authz::BuiltinRole; -use super::infra::InfraApiError; - crate::routes! { "/temporary_speed_limit_group" => create_temporary_speed_limit_group, } -// Maximum distance the graph can be explored from a speed limit execution signal -// without finding any legitimate ending to the speed limit before it is considered -// there is not valid limit on the portion of the graph that is being explored. -// TODO Magic number for now. Make it configurable ? -#[cfg(not(test))] -const MAXIMUM_DISTANCE: f64 = 80000.; -#[cfg(test)] -const MAXIMUM_DISTANCE: f64 = 5000.; - #[derive(Serialize, ToSchema)] -struct TemporarySpeedLimitItemForm { - start_date_time: NaiveDateTime, - end_date_time: NaiveDateTime, - signals: Vec, - speed_limit: f64, - obj_id: String, -} - -struct TemporarySpeedLimitImport { +struct TemporarySpeedLimitItem { start_date_time: NaiveDateTime, end_date_time: NaiveDateTime, track_ranges: Vec, @@ -70,9 +40,8 @@ struct TemporarySpeedLimitImport { #[derive(Serialize, Deserialize, ToSchema)] struct TemporarySpeedLimitCreateForm { speed_limit_group_name: String, - infra_id: i64, #[schema(inline)] - speed_limits: Vec, + speed_limits: Vec, } #[derive(Serialize, Deserialize, ToSchema)] @@ -80,7 +49,7 @@ struct TemporarySpeedLimitCreateResponse { group_id: i64, } -impl TemporarySpeedLimitImport { +impl TemporarySpeedLimitItem { fn into_temporary_speed_limit_changeset( self, temporary_speed_limit_group_id: i64, @@ -93,334 +62,10 @@ impl TemporarySpeedLimitImport { .obj_id(self.obj_id) .temporary_speed_limit_group_id(temporary_speed_limit_group_id) } - - fn from_temporary_speed_limit_item( - speed_limit: TemporarySpeedLimitItemForm, - graph: &Graph, - infra_cache: &InfraCache, - ) -> Self { - let track_ranges: Vec = - track_ranges_from_signals(speed_limit.signals, graph, infra_cache); - TemporarySpeedLimitImport { - start_date_time: speed_limit.start_date_time, - end_date_time: speed_limit.end_date_time, - track_ranges, - speed_limit: speed_limit.speed_limit, - obj_id: speed_limit.obj_id, - } - } -} - -/// Retrieve from the infrastructure the track sections which match a list of temporary speed limit -/// signals. The input signals vector is expected to contain valid temporary speed limit signals, -/// i.e. signals which sign type is either `E` (for `Execution`) or `R` (for `Resume`). -fn track_ranges_from_signals( - signals: Vec, - graph: &Graph, - infra_cache: &InfraCache, -) -> Vec { - // TODO merge adjacent directional track ranges - let (execution_signals, resume_signals): (Vec<_>, Vec<_>) = signals - .into_iter() - .filter(|s| s.sign_type == "E".into() || s.sign_type == "R".into()) - .partition(|s| s.sign_type == "E".into()); - let mut speed_limit_track_ranges: Vec = Vec::new(); - execution_signals.into_iter().for_each(|start_signal| { - speed_limit_track_ranges.extend(impacted_tracks( - &start_signal, - resume_signals.iter().collect::>(), - graph, - infra_cache, - MAXIMUM_DISTANCE, - )) - }); - speed_limit_track_ranges } -/// Do a graph traversal from `entry` up to any of the `exits` and return the visited track ranges -/// which correspond to the track ranges impacted by the temporary speed limit. -/// For a given resume signal, a valid exit is: -/// - A resume signal. -/// - Another execution signal (which will define another temporary speed limit and void the -/// current one). -/// The algorithm stops looking for more track ranges if the distance from `entry` to the track -/// range being currently explored track range exceeds the maximum distance defined, so that a -/// missing `exit` signal in a given direction does not cause the temporary speed limit to keep -/// exploring the whole infrastructure. -/// # Parameters: -/// - `entry`: The entry point of the LTV. -/// - `exits`: any valid exit for the LTV, i.e. any signal that exists in the infrastructure (does -/// this mean we need to persist signals ? If we take any signal as a valid stop case we need to -/// consider the signals related to already imported LTVs, and they are not present anymore in the -/// current import request.) -/// - `max_distance`: The maximum distance (in meters ?) after which we consider the exit signal is -/// missing and we stop adding new track ranges from the current path. -fn impacted_tracks( - entry: &Sign, - exits: Vec<&Sign>, - graph: &Graph, - infra_cache: &InfraCache, - max_distance: f64, -) -> Vec { - // TODO allow exploration of tracks in both directions (when coming back to a track already - // explored in the other direction) - - // Map track identifiers to their list of associated exits: - let mut tracks_to_exits: HashMap<&Identifier, Vec<&Sign>> = HashMap::new(); - exits.into_iter().for_each(|exit| { - let track_id = &exit.track; - if let Some(track_exits) = tracks_to_exits.get_mut(track_id) { - track_exits.push(exit); - } else { - let track_exits = vec![exit]; - tracks_to_exits.insert(track_id, track_exits); - }; - }); - let exits = tracks_to_exits; - - // Directional track ranges reachable from `entry` during the graph exploration. - let mut related_track_ranges: Vec = Vec::new(); - - // TrackEndpoint right after the entry sign (in the correct direction): - let first_track_endpoint = TrackEndpoint { - endpoint: match entry.direction { - Direction::StartToStop => Endpoint::End, - Direction::StopToStart => Endpoint::Begin, - }, - track: entry.track.clone(), - }; - - if let Some(immediate_exit) = closest_exit_from_entry(entry, exits.get(&entry.track)) { - if let Some(only_track_range) = track_range_between_two_signs(entry, immediate_exit) { - return vec![only_track_range]; - } else { - return vec![]; - } - } else if let Some(first_track_range) = - track_range_between_endpoint_and_sign(entry, &first_track_endpoint, infra_cache, true) - { - related_track_ranges.push(first_track_range); - } else { - return vec![]; - }; - - // Identifiers of the track sections that have already been reached and should be ignored: - let mut visited_tracks: HashSet<&TrackEndpoint> = HashSet::new(); - - // Neighbors of the explored tracks, i.e. the tracks that should be visited next: - let mut next_tracks: Vec<(&TrackEndpoint, f64)> = Vec::new(); - let remaining_distance = - max_distance - (related_track_ranges[0].end - related_track_ranges[0].begin); - if 0. < remaining_distance { - let neighbours = graph - .get_all_neighbours(&first_track_endpoint) - .into_iter() - .map(|neighbour| (neighbour, remaining_distance)) - .collect::>(); - next_tracks.extend(neighbours); - } - - while let Some((curr_track_endpoint, remaining_distance)) = next_tracks.pop() { - let curr_track_id = &curr_track_endpoint.track; - - if !visited_tracks.insert(curr_track_endpoint) { - // Track already visited - continue; - } - - // Check if there is a resume signal on that track range - if let Some(exit) = - closest_exit_from_endpoint(curr_track_endpoint, exits.get(&curr_track_id)) - { - // End the search on that track, add the current track with the correct offset - let track_range = track_range_between_endpoint_and_sign( - exit, - curr_track_endpoint, - infra_cache, - false, - ) - .unwrap_or_else(|| { - panic!( - "Cannot form a track from {:?} and {:?}", - exit, curr_track_endpoint - ) - }); - related_track_ranges.push(track_range); - } else { - let track_range = - track_range_from_endpoint(curr_track_endpoint, remaining_distance, infra_cache) - .unwrap_or_else(|| { - panic!("Cannot form a track from {:?}", curr_track_endpoint) - }); - let neighbours_remaining_distance = - remaining_distance - (track_range.end - track_range.begin); - related_track_ranges.push(track_range); - if 0. < neighbours_remaining_distance { - let opposite_track_endpoint = TrackEndpoint { - endpoint: match curr_track_endpoint.endpoint { - Endpoint::Begin => Endpoint::End, - Endpoint::End => Endpoint::Begin, - }, - track: curr_track_endpoint.track.clone(), - }; - let neighbours = graph - .get_all_neighbours(&opposite_track_endpoint) - .into_iter() - .map(|neighbour| (neighbour, neighbours_remaining_distance)) - .collect::>(); - next_tracks.extend(neighbours); - } - } - } - related_track_ranges -} - -/// Return the closest exit that applies on a track from a starting endpoint. -/// To be applicable, an exit must be in the correct direction. -fn closest_exit_from_endpoint<'a>( - track_endpoint: &TrackEndpoint, - exits: Option<&'a Vec<&Sign>>, -) -> Option<&'a Sign> { - if let Some(exits) = exits { - exits - .iter() - .filter(|exit| exit.track == track_endpoint.track) - .filter(|exit| match track_endpoint.endpoint { - Endpoint::Begin => exit.direction == Direction::StartToStop, - Endpoint::End => exit.direction == Direction::StopToStart, - }) - .sorted_by( - |e_1, e_2| match (track_endpoint.endpoint, e_1.position < e_2.position) { - (Endpoint::Begin, true) | (Endpoint::End, false) => Ordering::Less, - (Endpoint::Begin, false) | (Endpoint::End, true) => Ordering::Greater, - }, - ) - .map(|sign| &**sign) - .next() - } else { - None - } -} - -/// Return the closest applicable exit that is on the same track as the `entry` sign or `None` -/// if there is none. -fn closest_exit_from_entry<'a>(entry: &Sign, exits: Option<&'a Vec<&Sign>>) -> Option<&'a Sign> { - if let Some(exits) = exits { - exits - .iter() - .filter(|exit| exit.track == entry.track) - .filter(|exit| entry.direction == exit.direction) - .filter(|exit| match entry.direction { - Direction::StartToStop => entry.position < exit.position, - Direction::StopToStart => exit.position < entry.position, - }) - .sorted_by( - |e_1, e_2| match (entry.direction, e_1.position < e_2.position) { - (Direction::StartToStop, true) | (Direction::StopToStart, false) => { - Ordering::Less - } - (Direction::StartToStop, false) | (Direction::StopToStart, true) => { - Ordering::Greater - } - }, - ) - .map(|sign| &**sign) - .next() - } else { - None - } -} - -/// Return the directional track range starting at `entry` finishing at `exit`, or `None` -/// if no track range can be built from them. -fn track_range_between_two_signs(entry: &Sign, exit: &Sign) -> Option { - let exit_before_entry = match entry.direction { - Direction::StartToStop => exit.position < entry.position, - Direction::StopToStart => entry.position < exit.position, - }; - if entry.direction != exit.direction || entry.track != exit.track || exit_before_entry { - return None; - } - Some(DirectionalTrackRange { - track: entry.track.clone(), - begin: f64::min(entry.position, exit.position), - end: f64::max(entry.position, exit.position), - direction: entry.direction, - }) -} - -/// Return the directional track range delimited by a sign and a track endpoint, or `None` if no -/// track range can be built from them. -fn track_range_between_endpoint_and_sign( - sign: &Sign, - endpoint: &TrackEndpoint, - infra_cache: &InfraCache, - entry: bool, -) -> Option { - let mut sign_on_correct_direction = matches!( - (sign.direction, endpoint.endpoint), - (Direction::StartToStop, Endpoint::End) | (Direction::StopToStart, Endpoint::Begin) - ); - if entry { - sign_on_correct_direction = !sign_on_correct_direction; - } - - if sign_on_correct_direction { - None - } else if let Some(track) = infra_cache.track_sections().get(&sign.track.0) { - let track_length = track.unwrap_track_section().length; - let (begin_offset, end_offset) = match endpoint.endpoint { - Endpoint::Begin => (0., sign.position), - Endpoint::End => (sign.position, track_length), - }; - let track_range = DirectionalTrackRange { - track: sign.track.clone(), - begin: begin_offset, - end: end_offset, - direction: sign.direction, - }; - Some(track_range) - } else { - None - } -} - -/// Build a directional track range starting at `track_endpoint` and stopping at the end of the track -/// range if it is shorter than `remaining_distance`, or at `remaining_distance` from `track_endpoint` -/// otherwise. Returns: the built track range or `None` if the track does not exist in `infra_cache`. -fn track_range_from_endpoint( - track_endpoint: &TrackEndpoint, - remaining_distance: f64, - infra_cache: &InfraCache, -) -> Option { - if let Some(track) = infra_cache.track_sections().get(&track_endpoint.track.0) { - let track_length = track.unwrap_track_section().length; - let direction = match track_endpoint.endpoint { - Endpoint::Begin => Direction::StartToStop, - Endpoint::End => Direction::StopToStart, - }; - let track_range_length = if track_length < remaining_distance { - track_length - } else { - remaining_distance - }; - let (begin_offset, end_offset) = match direction { - Direction::StartToStop => (0., track_range_length), - Direction::StopToStart => (track_length - track_range_length, track_length), - }; - Some(DirectionalTrackRange { - track: track_endpoint.track.clone(), - begin: begin_offset, - end: end_offset, - direction, - }) - } else { - None - } -} - -impl<'de> Deserialize<'de> for TemporarySpeedLimitItemForm { - fn deserialize(deserializer: D) -> StdResult +impl<'de> Deserialize<'de> for TemporarySpeedLimitItem { + fn deserialize(deserializer: D) -> StdResult where D: serde::Deserializer<'de>, { @@ -429,14 +74,14 @@ impl<'de> Deserialize<'de> for TemporarySpeedLimitItemForm { struct Internal { start_date_time: NaiveDateTime, end_date_time: NaiveDateTime, - signals: Vec, + track_ranges: Vec, speed_limit: f64, obj_id: String, } let Internal { start_date_time, end_date_time, - signals, + track_ranges, speed_limit, obj_id, } = Internal::deserialize(deserializer)?; @@ -450,16 +95,10 @@ impl<'de> Deserialize<'de> for TemporarySpeedLimitItemForm { ))); } - if !signals.iter().any(|signal| signal.sign_type == "E".into()) { - return Err(SerdeError::custom( - "The temporary speed limit signals list must contain at least one execution signal.", - )); - } - - Ok(TemporarySpeedLimitItemForm { + Ok(TemporarySpeedLimitItem { start_date_time, end_date_time, - signals, + track_ranges, speed_limit, obj_id, }) @@ -498,10 +137,8 @@ fn map_diesel_error(e: InternalError, name: impl AsRef) -> InternalError { async fn create_temporary_speed_limit_group( State(db_pool): State, Extension(auth): AuthenticationExt, - State(app_state): State, Json(TemporarySpeedLimitCreateForm { speed_limit_group_name, - infra_id, speed_limits, }): Json, ) -> Result> { @@ -523,24 +160,9 @@ async fn create_temporary_speed_limit_group( .await .map_err(|e| map_diesel_error(e, speed_limit_group_name))?; - // Retrieve the infra - - let infra_caches = app_state.infra_caches.clone(); - let infra = - Infra::retrieve_or_fail(conn, infra_id, || InfraApiError::NotFound { infra_id }).await?; - let infra_cache = InfraCache::get_or_load(conn, &infra_caches, &infra).await?; - let graph = Graph::load(&infra_cache); - // Create the speed limits let speed_limits_changesets = speed_limits .into_iter() - .map(|speed_limit| { - TemporarySpeedLimitImport::from_temporary_speed_limit_item( - speed_limit, - &graph, - &infra_cache, - ) - }) .map(|speed_limit| speed_limit.into_temporary_speed_limit_changeset(group_id)) .collect::>(); let _: Vec<_> = TemporarySpeedLimit::create_batch(conn, speed_limits_changesets).await?; @@ -559,8 +181,6 @@ mod tests { use axum::http::StatusCode; use axum_test::TestRequest; use chrono::{Duration, NaiveDateTime, Utc}; - use editoast_schemas::infra::{Direction, DirectionalTrackRange}; - use rand; use rstest::rstest; use serde_json::{json, Value}; use uuid::Uuid; @@ -591,7 +211,7 @@ mod tests { start_date_time, end_date_time, }, - signals, + track_sections: signals, } = parameters; self.post("/temporary_speed_limit_group").json(&json!( { @@ -614,7 +234,7 @@ mod tests { infra_id: i64, obj_id: String, time_period: TimePeriod, - signals: Value, + track_sections: Value, } impl RequestParameters { @@ -627,25 +247,19 @@ mod tests { start_date_time: Utc::now().naive_utc(), end_date_time: Utc::now().naive_utc() + Duration::days(1), }, - signals: json!( + track_sections: json!( [ { "track": Uuid::new_v4(), - "position": rand::random::(), - "side": "LEFT", + "begin": 0., + "end": 100., "direction": "START_TO_STOP", - "type": "E", - "value": Uuid::new_v4(), - "kp": "147+292", }, { "track": Uuid::new_v4(), - "position": rand::random::(), - "side": "LEFT", + "begin": 0., + "end": 100, "direction": "START_TO_STOP", - "type": "R", - "value": Uuid::new_v4(), - "kp": "147+292", } ]), } @@ -666,8 +280,8 @@ mod tests { self } - fn with_signals(mut self, signals: Value) -> Self { - self.signals = signals; + fn with_track_ranges(mut self, track_ranges: Value) -> Self { + self.track_sections = track_ranges; self } } @@ -758,14 +372,14 @@ mod tests { } #[rstest] - async fn create_ltv_with_no_signals_fails() { + async fn create_ltv_with_no_track_ranges_fails() { let app = TestAppBuilder::default_app(); let pool = app.db_pool(); let Infra { id: infra_id, .. } = create_small_infra(&mut pool.get_ok()).await; let request = app.create_temporary_speed_limit_group_request( - RequestParameters::new(infra_id).with_signals(json!([])), + RequestParameters::new(infra_id).with_track_ranges(json!([])), ); let _ = app @@ -773,557 +387,28 @@ mod tests { .assert_status(StatusCode::UNPROCESSABLE_ENTITY); } - #[rstest] - async fn create_ltv_with_no_execution_signals_fails() { - let app = TestAppBuilder::default_app(); - let pool = app.db_pool(); - - let Infra { id: infra_id, .. } = create_small_infra(&mut pool.get_ok()).await; - - let request = app.create_temporary_speed_limit_group_request( - RequestParameters::new(infra_id).with_signals(json!([ - { - "track": Uuid::new_v4(), - "position": rand::random::(), - "side": "LEFT", - "direction": "START_TO_STOP", - "type": "R", - "value": Uuid::new_v4(), - "kp": "147+292", - - }, - ])), - ); - - let _ = app - .fetch(request) - .assert_status(StatusCode::UNPROCESSABLE_ENTITY); - } - - // Signals to tracks conversion tests - - /// Create a temporary speed limit through with a given signal list and `small_infra` id through - /// the creation endpoint, then retrieve from the database the persisted track sections for that - /// speed limit. - async fn retrieve_track_ranges_from_signals(signals: Value) -> Vec { - let app = TestAppBuilder::default_app(); - let pool = app.db_pool(); - - let Infra { id: infra_id, .. } = create_small_infra(&mut pool.get_ok()).await; - let speed_limit_obj_id = Uuid::new_v4().to_string(); - let request = app.create_temporary_speed_limit_group_request( - RequestParameters::new(infra_id) - .with_obj_id(speed_limit_obj_id.clone()) - .with_signals(signals), - ); - - let TemporarySpeedLimitCreateResponse { group_id } = - app.fetch(request).assert_status(StatusCode::OK).json_into(); - - let selection_settings: SelectionSettings = SelectionSettings::new() - .filter(move || TemporarySpeedLimit::TEMPORARY_SPEED_LIMIT_GROUP_ID.eq(group_id)); - let mut created_speed_limits: Vec = - TemporarySpeedLimit::list(&mut pool.get_ok(), selection_settings) - .await - .expect("Failed to retrieve temporary speed limits from the database"); - - assert_eq!(created_speed_limits.len(), 1); - let TemporarySpeedLimit { track_ranges, .. } = created_speed_limits.pop().unwrap(); - track_ranges - } - - #[rstest] - async fn same_track_start_to_stop() { - let retrieved_track_ranges = retrieve_track_ranges_from_signals(json!([ - { - "track": "TH1", - "position": 100., - "side": "LEFT", - "direction": "START_TO_STOP", - "type": "E", - "value": Uuid::new_v4(), - "kp": String::new(), - }, - { - "track": "TH1", - "position": 200., - "side": "LEFT", - "direction": "START_TO_STOP", - "type": "R", - "value": Uuid::new_v4(), - "kp": String::new(), - } - ])) - .await; - let expected_track_ranges = vec![DirectionalTrackRange { - track: "TH1".into(), - begin: 100., - end: 200., - direction: Direction::StartToStop, - }]; - assert_eq!(expected_track_ranges, retrieved_track_ranges); - } - - #[rstest] - async fn same_track_stop_to_start() { - let retrieved_track_ranges = retrieve_track_ranges_from_signals(json!([ - { - "track": "TH1", - "position": 200., - "side": "LEFT", - "direction": "STOP_TO_START", - "type": "E", - "value": Uuid::new_v4(), - "kp": String::new(), - }, - { - "track": "TH1", - "position": 100., - "side": "LEFT", - "direction": "STOP_TO_START", - "type": "R", - "value": Uuid::new_v4(), - "kp": String::new(), - } - ])) - .await; - let expected_track_ranges = vec![DirectionalTrackRange { - track: "TH1".into(), - begin: 100., - end: 200., - direction: Direction::StopToStart, - }]; - assert_eq!(expected_track_ranges, retrieved_track_ranges); - } - - #[rstest] - async fn tunnel_on_two_tracks() { - let retrieved_track_ranges = retrieve_track_ranges_from_signals(json!([ - { - "track": "TF1", - "position": 100., - "side": "LEFT", - "direction": "STOP_TO_START", - "type": "E", - "value": Uuid::new_v4(), - "kp": String::new(), - }, - { - "track": "TF0", - "position": 2., - "side": "LEFT", - "direction": "STOP_TO_START", - "type": "R", - "value": Uuid::new_v4(), - "kp": String::new(), - }, - ])) - .await; - let expected_track_ranges = vec![ - DirectionalTrackRange { - track: "TF1".into(), - begin: 0., - end: 100., - direction: Direction::StopToStart, - }, - DirectionalTrackRange { - track: "TF0".into(), - begin: 2., - end: 3., - direction: Direction::StopToStart, - }, - ]; - assert_eq!(expected_track_ranges, retrieved_track_ranges); - } - - #[rstest] - async fn both_point_switch_directions_get_explored() { - let mut retrieved_track_ranges = retrieve_track_ranges_from_signals(json!([ - { - "track": "TG1", - "position": 100., - "side": "LEFT", - "direction": "START_TO_STOP", - "type": "E", - "value": Uuid::new_v4(), - "kp": String::new(), - }, - { - "track": "TG3", - "position": 50., - "side": "LEFT", - "direction": "START_TO_STOP", - "type": "R", - "value": Uuid::new_v4(), - "kp": String::new(), - }, - { - "track": "TG4", - "position": 150., - "side": "LEFT", - "direction": "START_TO_STOP", - "type": "R", - "value": Uuid::new_v4(), - "kp": String::new(), - }, - ])) - .await; - let mut expected_track_ranges = vec![ - DirectionalTrackRange { - track: "TG1".into(), - begin: 100., - end: 4000., - direction: Direction::StartToStop, - }, - DirectionalTrackRange { - track: "TG3".into(), - begin: 0., - end: 50., - direction: Direction::StartToStop, - }, - DirectionalTrackRange { - track: "TG4".into(), - begin: 0., - end: 150., - direction: Direction::StartToStop, - }, - ]; - expected_track_ranges.sort_by(|lhs, rhs| lhs.track.0.cmp(&rhs.track.0)); - retrieved_track_ranges.sort_by(|lhs, rhs| lhs.track.0.cmp(&rhs.track.0)); - assert_eq!(expected_track_ranges, retrieved_track_ranges); - } - - #[rstest] - async fn multiple_isolated_entry_signals() { - let mut retrieved_track_ranges = retrieve_track_ranges_from_signals(json!([ - { - "track": "TF1", - "position": 100., - "side": "LEFT", - "direction": "STOP_TO_START", - "type": "E", - "value": Uuid::new_v4(), - "kp": String::new(), - }, - { - "track": "TF0", - "position": 2., - "side": "LEFT", - "direction": "STOP_TO_START", - "type": "R", - "value": Uuid::new_v4(), - "kp": String::new(), - }, - { - "track": "TG1", - "position": 100., - "side": "LEFT", - "direction": "START_TO_STOP", - "type": "E", - "value": Uuid::new_v4(), - "kp": String::new(), - }, - { - "track": "TG3", - "position": 50., - "side": "LEFT", - "direction": "START_TO_STOP", - "type": "R", - "value": Uuid::new_v4(), - "kp": String::new(), - }, - { - "track": "TG4", - "position": 150., - "side": "LEFT", - "direction": "START_TO_STOP", - "type": "R", - "value": Uuid::new_v4(), - "kp": String::new(), - }, - ])) - .await; - let mut expected_track_ranges = vec![ - DirectionalTrackRange { - track: "TF1".into(), - begin: 0., - end: 100., - direction: Direction::StopToStart, - }, - DirectionalTrackRange { - track: "TF0".into(), - begin: 2., - end: 3., - direction: Direction::StopToStart, - }, - DirectionalTrackRange { - track: "TG1".into(), - begin: 100., - end: 4000., - direction: Direction::StartToStop, - }, - DirectionalTrackRange { - track: "TG3".into(), - begin: 0., - end: 50., - direction: Direction::StartToStop, - }, - DirectionalTrackRange { - track: "TG4".into(), - begin: 0., - end: 150., - direction: Direction::StartToStop, - }, - ]; - expected_track_ranges.sort_by(|lhs, rhs| lhs.track.0.cmp(&rhs.track.0)); - retrieved_track_ranges.sort_by(|lhs, rhs| lhs.track.0.cmp(&rhs.track.0)); - assert_eq!(expected_track_ranges, retrieved_track_ranges); - } - - #[rstest] - async fn signals_facing_opposite_direction_are_ignored() { - let retrieved_track_ranges = retrieve_track_ranges_from_signals(json!([ - { - "track": "TF1", - "position": 100., - "side": "LEFT", - "direction": "STOP_TO_START", - "type": "E", - "value": Uuid::new_v4(), - "kp": String::new(), - }, - { - "track": "TF0", - "position": 2., - "side": "LEFT", - "direction": "START_TO_STOP", - "type": "R", - "value": Uuid::new_v4(), - "kp": String::new(), - }, - { - "track": "TF0", - "position": 1., - "side": "LEFT", - "direction": "STOP_TO_START", - "type": "R", - "value": Uuid::new_v4(), - "kp": String::new(), - }, - ])) - .await; - let expected_track_ranges = vec![ - DirectionalTrackRange { - track: "TF1".into(), - begin: 0., - end: 100., - direction: Direction::StopToStart, - }, - DirectionalTrackRange { - track: "TF0".into(), - begin: 1., - end: 3., - direction: Direction::StopToStart, - }, - ]; - assert_eq!(expected_track_ranges, retrieved_track_ranges); - } - - #[rstest] - async fn track_range_is_built_from_the_closest_exit() { - let retrieved_track_ranges = retrieve_track_ranges_from_signals(json!([ - { - "track": "TF1", - "position": 100., - "side": "LEFT", - "direction": "STOP_TO_START", - "type": "E", - "value": Uuid::new_v4(), - "kp": String::new(), - }, - { - "track": "TF0", - "position": 2., - "side": "LEFT", - "direction": "STOP_TO_START", - "type": "R", - "value": Uuid::new_v4(), - "kp": String::new(), - }, - { - "track": "TF0", - "position": 1., - "side": "LEFT", - "direction": "STOP_TO_START", - "type": "R", - "value": Uuid::new_v4(), - "kp": String::new(), - }, - ])) - .await; - let expected_track_ranges = vec![ - DirectionalTrackRange { - track: "TF1".into(), - begin: 0., - end: 100., - direction: Direction::StopToStart, - }, - DirectionalTrackRange { - track: "TF0".into(), - begin: 2., - end: 3., - direction: Direction::StopToStart, - }, - ]; - assert_eq!(expected_track_ranges, retrieved_track_ranges); - } - - #[rstest] - async fn exit_before_entry_is_ignored() { - // The graph exploration should not stop if there is an exit signal on the same track - // as the entry signal when the exit signal is behind the entry signal. - let retrieved_track_ranges = retrieve_track_ranges_from_signals(json!([ - { - "track": "TF1", - "position": 100., - "side": "LEFT", - "direction": "STOP_TO_START", - "type": "E", - "value": Uuid::new_v4(), - "kp": String::new(), - }, - { - "track": "TF1", - "position": 150., - "side": "LEFT", - "direction": "STOP_TO_START", - "type": "R", - "value": Uuid::new_v4(), - "kp": String::new(), - }, - { - "track": "TF0", - "position": 2., - "side": "LEFT", - "direction": "STOP_TO_START", - "type": "R", - "value": Uuid::new_v4(), - "kp": String::new(), - }, - ])) - .await; - let expected_track_ranges = vec![ - DirectionalTrackRange { - track: "TF1".into(), - begin: 0., - end: 100., - direction: Direction::StopToStart, - }, - DirectionalTrackRange { - track: "TF0".into(), - begin: 2., - end: 3., - direction: Direction::StopToStart, - }, - ]; - assert_eq!(expected_track_ranges, retrieved_track_ranges); - } - - #[rstest] - async fn closest_exit_ignores_exits_before_entry() { - // If the LTV is a single track range, it should ignore the signals behind it when - // checking which one is the closest. - let retrieved_track_ranges = retrieve_track_ranges_from_signals(json!([ - { - "track": "TF1", - "position": 400., - "side": "LEFT", - "direction": "STOP_TO_START", - "type": "E", - "value": Uuid::new_v4(), - "kp": String::new(), - }, - { - "track": "TF1", - "position": 500., - "side": "LEFT", - "direction": "STOP_TO_START", - "type": "R", - "value": Uuid::new_v4(), - "kp": String::new(), - }, - { - "track": "TF1", - "position": 100., - "side": "LEFT", - "direction": "STOP_TO_START", - "type": "R", - "value": Uuid::new_v4(), - "kp": String::new(), - }, - ])) - .await; - let expected_track_ranges = vec![DirectionalTrackRange { - track: "TF1".into(), - begin: 100., - end: 400., - direction: Direction::StopToStart, - }]; - assert_eq!(expected_track_ranges, retrieved_track_ranges); - } - - #[rstest] - async fn exploration_stops_when_resume_signal_is_missing_and_maximum_distance_is_reached() { - let mut retrieved_track_ranges = retrieve_track_ranges_from_signals(json!([ - { - "track": "TE0", - "position": 500., - "side": "LEFT", - "direction": "START_TO_STOP", - "type": "E", - "value": Uuid::new_v4(), - "kp": String::new(), - }, - ])) - .await; - let mut expected_track_ranges = vec![ - DirectionalTrackRange { - track: "TE0".into(), - begin: 500., - end: 1500., - direction: Direction::StartToStop, - }, - DirectionalTrackRange { - track: "TF0".into(), - begin: 0., - end: 3., - direction: Direction::StartToStop, - }, - DirectionalTrackRange { - track: "TF1".into(), - begin: 0., - end: 3997., - direction: Direction::StartToStop, - }, - ]; - expected_track_ranges.sort_by(|lhs, rhs| lhs.track.0.cmp(&rhs.track.0)); - retrieved_track_ranges.sort_by(|lhs, rhs| lhs.track.0.cmp(&rhs.track.0)); - assert_eq!(expected_track_ranges, retrieved_track_ranges); - } - - #[rstest] - #[ignore] - async fn track_section_can_be_explored_in_both_directions() { - // TODO find a way to test it on small_infra or make a specific infra for this test - todo!() - } - - #[rstest] - #[ignore] - async fn adjacent_track_ranges_are_merged() { - // If two directional track ranges are adjacent and have the same direction, - // they should be merged into a single bigger directional track range. - // N.B. This is mostly a performance issue. - unimplemented!(); - } + // #[rstest] + // #[ignore] + // async fn create_ltv_with_no_execution_signals_fails() { + // let app = TestAppBuilder::default_app(); + // let pool = app.db_pool(); + // + // let Infra { id: infra_id, .. } = create_small_infra(&mut pool.get_ok()).await; + // + // let request = app.create_temporary_speed_limit_group_request( + // RequestParameters::new(infra_id).with_track_ranges(json!([ + // { + // "track": Uuid::new_v4(), + // "begin":todo!(), + // "end":todo!(), + // "direction": todo!(), + // + // }, + // ])), + // ); + // + // let _ = app + // .fetch(request) + // .assert_status(StatusCode::UNPROCESSABLE_ENTITY); + // } } diff --git a/front/src/common/api/generatedEditoastApi.ts b/front/src/common/api/generatedEditoastApi.ts index bdacd544464..eb784271961 100644 --- a/front/src/common/api/generatedEditoastApi.ts +++ b/front/src/common/api/generatedEditoastApi.ts @@ -6,6 +6,7 @@ export const addTagTypes = [ 'electrical_profiles', 'infra', 'rolling_stock', + 'delimited_area', 'pathfinding', 'routes', 'layers', @@ -219,6 +220,17 @@ const injectedRtkApi = api }), invalidatesTags: ['infra'], }), + postInfraByInfraIdDelimitedArea: build.mutation< + PostInfraByInfraIdDelimitedAreaApiResponse, + PostInfraByInfraIdDelimitedAreaApiArg + >({ + query: (queryArg) => ({ + url: `/infra/${queryArg.infraId}/delimited_area`, + method: 'POST', + body: queryArg.body, + }), + invalidatesTags: ['delimited_area'], + }), getInfraByInfraIdErrors: build.query< GetInfraByInfraIdErrorsApiResponse, GetInfraByInfraIdErrorsApiArg @@ -1083,6 +1095,27 @@ export type PostInfraByInfraIdCloneApiArg = { /** The name of the new infra */ name: string; }; +export type PostInfraByInfraIdDelimitedAreaApiResponse = + /** status 200 The track ranges between a list entries and exits. */ { + track_ranges: { + begin: number; + direction: Direction; + end: number; + track: string; + }[]; + }; +export type PostInfraByInfraIdDelimitedAreaApiArg = { + /** An existing infra ID */ + infraId: number; + body: { + track_ranges: { + begin: number; + direction: Direction; + end: number; + track: string; + }[]; + }; +}; export type GetInfraByInfraIdErrorsApiResponse = /** status 200 A paginated list of errors */ PaginationStats & { results: { @@ -1476,14 +1509,13 @@ export type PostTemporarySpeedLimitGroupApiResponse = }; export type PostTemporarySpeedLimitGroupApiArg = { body: { - infra_id: number; speed_limit_group_name: string; speed_limits: { end_date_time: string; obj_id: string; - signals: Sign[]; speed_limit: number; start_date_time: string; + track_ranges: DirectionalTrackRange[]; }[]; }; };