diff --git a/editoast/openapi.yaml b/editoast/openapi.yaml index a6367c8b659..c8f3cadac6c 100644 --- a/editoast/openapi.yaml +++ b/editoast/openapi.yaml @@ -3340,6 +3340,10 @@ components: - properties: OperationalPointNotFound: properties: + missing_ids: + items: + type: string + type: array missing_uics: items: format: int64 @@ -3347,6 +3351,7 @@ components: type: array required: - missing_uics + - missing_ids type: object required: - OperationalPointNotFound @@ -3420,6 +3425,17 @@ components: - uic - type type: object + - properties: + id: + type: string + type: + enum: + - operational_point_id + type: string + required: + - id + - type + type: object TimetableImportPathSchedule: properties: arrival_time: diff --git a/editoast/src/models/infra_objects/operational_point.rs b/editoast/src/models/infra_objects/operational_point.rs index 3ee5af5d5b3..5450ff9aa27 100644 --- a/editoast/src/models/infra_objects/operational_point.rs +++ b/editoast/src/models/infra_objects/operational_point.rs @@ -3,7 +3,7 @@ use crate::error::Result; use crate::{schema::OperationalPoint, tables::infra_object_operational_point}; use derivative::Derivative; -use diesel::sql_types::{Array, BigInt}; +use diesel::sql_types::{Array, BigInt, Text}; use diesel::{result::Error as DieselError, sql_query}; use diesel::{ExpressionMethods, QueryDsl}; use diesel_async::{AsyncPgConnection as PgConnection, RunQueryDsl}; @@ -38,4 +38,19 @@ impl OperationalPointModel { .load(conn) .await?) } + + /// Retrieve a list of operational points from the database + pub async fn retrieve_from_obj_ids( + conn: &mut PgConnection, + infra_id: i64, + ids: &[String], + ) -> Result> { + let query = "SELECT * FROM infra_object_operational_point + WHERE infra_id = $1 AND infra_object_operational_point.obj_id = ANY($2)".to_string(); + Ok(sql_query(query) + .bind::(infra_id) + .bind::, _>(ids) + .load(conn) + .await?) + } } diff --git a/editoast/src/views/timetable/import.rs b/editoast/src/views/timetable/import.rs index 8edb2aca5e1..749ad19bbf5 100644 --- a/editoast/src/views/timetable/import.rs +++ b/editoast/src/views/timetable/import.rs @@ -64,9 +64,11 @@ pub struct TimetableImportPathSchedule { #[serde(tag = "type")] pub enum TimetableImportPathLocation { #[serde(rename = "track_offset")] - TrackOffsetLocation { track_section: String, offset: f64 }, + TrackOffset { track_section: String, offset: f64 }, #[serde(rename = "operational_point")] - OperationalPointLocation { uic: i64 }, + OperationalPoint { uic: i64 }, + #[serde(rename = "operational_point_id")] + OperationalPointId { id: String }, } #[derive(Debug, Serialize, ToSchema)] @@ -76,10 +78,19 @@ struct TimetableImportReport { #[derive(Debug, Clone, Serialize, ToSchema)] enum TimetableImportError { - RollingStockNotFound { name: String }, - OperationalPointNotFound { missing_uics: Vec }, - PathfindingError { cause: InternalError }, - SimulationError { cause: InternalError }, + RollingStockNotFound { + name: String, + }, + OperationalPointNotFound { + missing_uics: Vec, + missing_ids: Vec, + }, + PathfindingError { + cause: InternalError, + }, + SimulationError { + cause: InternalError, + }, } #[derive(Debug, Clone, Deserialize, ToSchema)] @@ -90,6 +101,11 @@ pub struct TimetableImportTrain { departure_time: DateTime, } +struct OperationalPointsToParts { + from_uic: HashMap>, + from_id: HashMap>, +} + /// Import a timetable #[utoipa::path( tag = "timetable", @@ -181,11 +197,11 @@ async fn import_item( // PATHFINDING let mut pf_request = PathfindingRequest::new(infra_id, infra_version.to_owned()); pf_request.with_rolling_stocks(&mut vec![rolling_stock.clone()]); - // List operational points uic needed for this import let ops_uic = ops_uic_from_path(&import_item.path); + let ops_id = ops_id_from_path(&import_item.path); // Retrieve operational points - let op_id_to_parts = match find_operation_points(&ops_uic, infra_id, conn).await? { - Ok(op_id_to_parts) => op_id_to_parts, + let op_to_parts = match find_operation_points(&ops_uic, &ops_id, infra_id, conn).await? { + Ok(op_to_parts) => op_to_parts, Err(err) => { return Ok(import_item .trains @@ -195,7 +211,7 @@ async fn import_item( } }; // Create waypoints - let mut waypoints = waypoints_from_steps(&import_item.path, &op_id_to_parts); + let mut waypoints = waypoints_from_steps(&import_item.path, &op_to_parts); pf_request.with_waypoints(&mut waypoints); // Run pathfinding @@ -295,37 +311,65 @@ async fn import_item( async fn find_operation_points( ops_uic: &[i64], + ops_id: &[String], infra_id: i64, conn: &mut AsyncPgConnection, -) -> Result>, TimetableImportError>> { +) -> Result> { // Retrieve operational points - let ops = OperationalPointModel::retrieve_from_uic(conn, infra_id, ops_uic).await?; + let ops_from_uic: Vec = + OperationalPointModel::retrieve_from_uic(conn, infra_id, ops_uic).await?; + let mut op_uic_to_parts = HashMap::<_, Vec<_>>::new(); + for op in ops_from_uic { + op_uic_to_parts + .entry(op.data.0.extensions.identifier.unwrap().uic) + .or_default() + .extend(op.data.0.parts); + } + let mut missing_uics: Vec = vec![]; + // If we didn't find all the operational points, we can't run the pathfinding + if op_uic_to_parts.len() != ops_uic.len() { + ops_uic.iter().for_each(|uic| { + if !op_uic_to_parts.contains_key(uic) { + missing_uics.push(*uic) + } + }); + } + + let ops_from_ids = OperationalPointModel::retrieve_from_obj_ids(conn, infra_id, ops_id).await?; let mut op_id_to_parts = HashMap::<_, Vec<_>>::new(); - for op in ops { + for op in ops_from_ids { op_id_to_parts - .entry(op.data.0.extensions.identifier.unwrap().uic) + .entry(op.obj_id) .or_default() .extend(op.data.0.parts); } // If we didn't find all the operational points, we can't run the pathfinding - if op_id_to_parts.len() != ops_uic.len() { - let missing_uics = ops_uic - .iter() - .filter(|uic| !op_id_to_parts.contains_key(uic)) - .cloned() - .collect(); + let mut missing_ids: Vec = vec![]; + if op_id_to_parts.len() != ops_id.len() { + ops_id.iter().for_each(|id| { + if !op_id_to_parts.contains_key(id) { + missing_ids.push(id.to_string()) + } + }); + } + if missing_uics.len() + missing_ids.len() > 0 { return Ok(Err(TimetableImportError::OperationalPointNotFound { missing_uics, + missing_ids, })); } - Ok(Ok(op_id_to_parts)) + + Ok(Ok(OperationalPointsToParts { + from_uic: op_uic_to_parts, + from_id: op_id_to_parts, + })) } fn ops_uic_from_path(path: &[TimetableImportPathStep]) -> Vec { let mut ops_uic = path .iter() .filter_map(|step| match &step.location { - TimetableImportPathLocation::OperationalPointLocation { uic } => Some(*uic), + TimetableImportPathLocation::OperationalPoint { uic } => Some(*uic), _ => None, }) .collect::>(); @@ -335,23 +379,45 @@ fn ops_uic_from_path(path: &[TimetableImportPathStep]) -> Vec { ops_uic } +fn ops_id_from_path(path: &[TimetableImportPathStep]) -> Vec { + let mut ops_id = path + .iter() + .filter_map(|step| match &step.location { + TimetableImportPathLocation::OperationalPointId { id } => Some(id.to_string()), + _ => None, + }) + .collect::>(); + // Remove duplicates + ops_id.sort(); + ops_id.dedup(); + ops_id +} + fn waypoints_from_steps( path: &Vec, - op_id_to_parts: &HashMap>, + op_to_parts: &OperationalPointsToParts, ) -> Vec> { let mut res = PathfindingWaypoints::new(); for step in path { res.push(match &step.location { - TimetableImportPathLocation::TrackOffsetLocation { + TimetableImportPathLocation::TrackOffset { track_section, offset, } => Vec::from(Waypoint::bidirectional(track_section, *offset)), - TimetableImportPathLocation::OperationalPointLocation { uic } => op_id_to_parts + TimetableImportPathLocation::OperationalPoint { uic } => op_to_parts + .from_uic .get(uic) .unwrap() .iter() .flat_map(|op_part| Waypoint::bidirectional(&op_part.track, op_part.position)) .collect(), + TimetableImportPathLocation::OperationalPointId { id } => op_to_parts + .from_id + .get(id) + .unwrap() + .iter() + .flat_map(|op_part| Waypoint::bidirectional(&op_part.track, op_part.position)) + .collect(), }); } res @@ -438,8 +504,8 @@ mod tests { #[test] fn test_waypoints_from_steps() { - let mut op_id_to_parts = HashMap::new(); - op_id_to_parts.insert( + let mut op_uic_to_parts = HashMap::new(); + op_uic_to_parts.insert( 1, vec![ OperationalPointPart { @@ -455,23 +521,52 @@ mod tests { ], ); + let mut op_id_to_parts = HashMap::new(); + op_id_to_parts.insert( + "a1".to_string(), + vec![ + OperationalPointPart { + track: Identifier("E".to_string()), + position: 0., + ..Default::default() + }, + OperationalPointPart { + track: Identifier("F".to_string()), + position: 100., + ..Default::default() + }, + ], + ); + let path = vec![ TimetableImportPathStep { - location: TimetableImportPathLocation::TrackOffsetLocation { + location: TimetableImportPathLocation::TrackOffset { track_section: "C".to_string(), offset: 50., }, schedule: HashMap::new(), }, TimetableImportPathStep { - location: TimetableImportPathLocation::OperationalPointLocation { uic: 1 }, + location: TimetableImportPathLocation::OperationalPoint { uic: 1 }, + schedule: HashMap::new(), + }, + TimetableImportPathStep { + location: TimetableImportPathLocation::OperationalPointId { + id: "a1".to_string(), + }, schedule: HashMap::new(), }, ]; - let waypoints = waypoints_from_steps(&path, &op_id_to_parts); + let waypoints = waypoints_from_steps( + &path, + &(OperationalPointsToParts { + from_uic: op_uic_to_parts, + from_id: op_id_to_parts, + }), + ); - assert_eq!(waypoints.len(), 2); + assert_eq!(waypoints.len(), 3); assert_eq!(waypoints[0], Waypoint::bidirectional("C", 50.)); assert_eq!( waypoints[1], @@ -481,35 +576,61 @@ mod tests { ] .concat() ); + assert_eq!( + waypoints[2], + [ + Waypoint::bidirectional("E", 0.), + Waypoint::bidirectional("F", 100.), + ] + .concat() + ); } #[test] - fn test_ops_uic_from_path() { + fn test_ops_uic_id_from_path() { let path = vec![ TimetableImportPathStep { - location: TimetableImportPathLocation::TrackOffsetLocation { + location: TimetableImportPathLocation::TrackOffset { track_section: "A".to_string(), offset: 0., }, schedule: HashMap::new(), }, TimetableImportPathStep { - location: TimetableImportPathLocation::OperationalPointLocation { uic: 1 }, + location: TimetableImportPathLocation::OperationalPoint { uic: 1 }, schedule: HashMap::new(), }, TimetableImportPathStep { - location: TimetableImportPathLocation::OperationalPointLocation { uic: 2 }, + location: TimetableImportPathLocation::OperationalPointId { + id: "a1".to_string(), + }, schedule: HashMap::new(), }, TimetableImportPathStep { - location: TimetableImportPathLocation::TrackOffsetLocation { + location: TimetableImportPathLocation::OperationalPoint { uic: 2 }, + schedule: HashMap::new(), + }, + TimetableImportPathStep { + location: TimetableImportPathLocation::TrackOffset { track_section: "B".to_string(), offset: 100., }, schedule: HashMap::new(), }, TimetableImportPathStep { - location: TimetableImportPathLocation::OperationalPointLocation { uic: 1 }, + location: TimetableImportPathLocation::OperationalPointId { + id: "a2".to_string(), + }, + schedule: HashMap::new(), + }, + TimetableImportPathStep { + location: TimetableImportPathLocation::OperationalPointId { + id: "a1".to_string(), + }, + schedule: HashMap::new(), + }, + TimetableImportPathStep { + location: TimetableImportPathLocation::OperationalPoint { uic: 1 }, schedule: HashMap::new(), }, ]; @@ -519,5 +640,11 @@ mod tests { assert_eq!(ops_uic.len(), 2); assert_eq!(ops_uic[0], 1); assert_eq!(ops_uic[1], 2); + + let ops_id = ops_id_from_path(&path); + + assert_eq!(ops_id.len(), 2); + assert_eq!(ops_id[0], "a1"); + assert_eq!(ops_id[1], "a2"); } } diff --git a/front/src/common/api/osrdEditoastApi.ts b/front/src/common/api/osrdEditoastApi.ts index a96af6ef587..552079446a8 100644 --- a/front/src/common/api/osrdEditoastApi.ts +++ b/front/src/common/api/osrdEditoastApi.ts @@ -2308,6 +2308,7 @@ export type TimetableImportError = } | { OperationalPointNotFound: { + missing_ids: string[]; missing_uics: number[]; }; } @@ -2335,6 +2336,10 @@ export type TimetableImportPathLocation = | { type: 'operational_point'; uic: number; + } + | { + id: string; + type: 'operational_point_id'; }; export type TimetableImportPathSchedule = { arrival_time: string;