From 21906b8c9f9e5c1db1de2fe937f9ed6dec8e91cc Mon Sep 17 00:00:00 2001 From: Benoit Simard Date: Fri, 19 Apr 2024 17:19:02 +0200 Subject: [PATCH] editoast: split track section endpoint --- .../src/infra/neutral_section.rs | 8 +- editoast/openapi.yaml | 58 ++ .../sql/insert_neutral_sign_layer.sql | 8 +- editoast/src/infra_cache/mod.rs | 42 +- editoast/src/infra_cache/object_cache.rs | 8 +- .../object_cache/neutral_section_cache.rs | 21 + editoast/src/infra_cache/operation/create.rs | 8 + editoast/src/main.rs | 2 - .../infra/auto_fixes/electrifications.rs | 13 +- .../views/infra/auto_fixes/speed_section.rs | 13 +- editoast/src/views/infra/edition.rs | 908 +++++++++++++++++- editoast/src/views/infra/mod.rs | 10 +- front/public/locales/en/errors.json | 19 +- front/public/locales/fr/errors.json | 15 +- front/src/common/api/osrdEditoastApi.ts | 28 +- 15 files changed, 1104 insertions(+), 57 deletions(-) create mode 100644 editoast/src/infra_cache/object_cache/neutral_section_cache.rs diff --git a/editoast/editoast_schemas/src/infra/neutral_section.rs b/editoast/editoast_schemas/src/infra/neutral_section.rs index 4c010d99e6a..39e568fa2bb 100644 --- a/editoast/editoast_schemas/src/infra/neutral_section.rs +++ b/editoast/editoast_schemas/src/infra/neutral_section.rs @@ -43,10 +43,10 @@ pub struct NeutralSectionExtensions { #[derive(Debug, Default, Clone, Deserialize, Serialize, PartialEq, ToSchema)] #[serde(deny_unknown_fields)] pub struct NeutralSectionNeutralSncfExtension { - announcement: Vec, - exe: Sign, - end: Vec, - rev: Vec, + pub announcement: Vec, + pub exe: Sign, + pub end: Vec, + pub rev: Vec, } impl OSRDTyped for NeutralSection { diff --git a/editoast/openapi.yaml b/editoast/openapi.yaml index 97feca8fe4d..e3fac17b84c 100644 --- a/editoast/openapi.yaml +++ b/editoast/openapi.yaml @@ -759,6 +759,36 @@ components: - status - message type: object + EditoastEditionErrorSplitTrackSectionBadOffset: + properties: + context: + properties: + infra_id: + type: integer + tracksection_id: + type: string + tracksection_length: + type: number + required: + - infra_id + - tracksection_id + - tracksection_length + type: object + message: + type: string + status: + enum: + - 400 + type: integer + type: + enum: + - editoast:infra:edition:SplitTrackSectionBadOffset + type: string + required: + - type + - status + - message + type: object EditoastEditoastUrlErrorInvalidUrl: properties: context: @@ -828,6 +858,7 @@ components: - $ref: '#/components/schemas/EditoastCoreErrorUnparsableErrorOutput' - $ref: '#/components/schemas/EditoastDocumentErrorsNotFound' - $ref: '#/components/schemas/EditoastEditionErrorInfraIsLocked' + - $ref: '#/components/schemas/EditoastEditionErrorSplitTrackSectionBadOffset' - $ref: '#/components/schemas/EditoastEditoastUrlErrorInvalidUrl' - $ref: '#/components/schemas/EditoastElectricalProfilesErrorNotFound' - $ref: '#/components/schemas/EditoastGeometryErrorUnexpectedGeometry' @@ -9989,6 +10020,33 @@ paths: summary: Returns the set of speed limit tags for a given infra tags: - infra + /infra/{infra_id}/split_track_section/: + post: + parameters: + - description: An existing infra ID + in: path + name: infra_id + required: true + schema: + format: int64 + type: integer + requestBody: + content: + application/json: + schema: + $ref: '#/components/schemas/TrackOffset' + required: true + responses: + '200': + content: + application/json: + schema: + items: + type: string + type: array + description: ID of the trackSections created + tags: + - infra /infra/{infra_id}/switch_types/: get: parameters: diff --git a/editoast/src/generated_data/sql/insert_neutral_sign_layer.sql b/editoast/src/generated_data/sql/insert_neutral_sign_layer.sql index 41099831ef0..89b4ca15f5a 100644 --- a/editoast/src/generated_data/sql/insert_neutral_sign_layer.sql +++ b/editoast/src/generated_data/sql/insert_neutral_sign_layer.sql @@ -19,7 +19,7 @@ WITH signs AS ( FROM infra_object_neutral_section WHERE infra_id = $1 AND obj_id = ANY($2) - AND infra_object_neutral_section.data @ ? '$.extensions.neutral_sncf.announcement' + AND infra_object_neutral_section.data @? '$.extensions.neutral_sncf.announcement' UNION SELECT obj_id AS sc_id, ( @@ -41,7 +41,7 @@ WITH signs AS ( FROM infra_object_neutral_section WHERE infra_id = $1 AND obj_id = ANY($2) - AND infra_object_neutral_section.data @ ? '$.extensions.neutral_sncf.rev' + AND infra_object_neutral_section.data @? '$.extensions.neutral_sncf.rev' UNION SELECT obj_id AS sc_id, ( @@ -57,7 +57,7 @@ WITH signs AS ( FROM infra_object_neutral_section WHERE infra_id = $1 AND obj_id = ANY($2) - AND infra_object_neutral_section.data @ ? '$.extensions.neutral_sncf.end' + AND infra_object_neutral_section.data @? '$.extensions.neutral_sncf.end' UNION SELECT obj_id AS sc_id, ( @@ -71,7 +71,7 @@ WITH signs AS ( FROM infra_object_neutral_section WHERE infra_id = $1 AND obj_id = ANY($2) - AND infra_object_neutral_section.data @ ? '$.extensions.neutral_sncf.exe' + AND infra_object_neutral_section.data @? '$.extensions.neutral_sncf.exe' ), collect AS ( SELECT signs.sc_id, diff --git a/editoast/src/infra_cache/mod.rs b/editoast/src/infra_cache/mod.rs index 75df2ff7681..704a497a37d 100644 --- a/editoast/src/infra_cache/mod.rs +++ b/editoast/src/infra_cache/mod.rs @@ -25,6 +25,7 @@ use editoast_schemas::infra::DoubleSlipSwitch; use editoast_schemas::infra::Electrification; use editoast_schemas::infra::Endpoint; use editoast_schemas::infra::Link; +use editoast_schemas::infra::NeutralSection; use editoast_schemas::infra::OperationalPointPart; use editoast_schemas::infra::PointSwitch; use editoast_schemas::infra::Route; @@ -89,6 +90,7 @@ pub enum ObjectCache { OperationalPoint(OperationalPointCache), SwitchType(SwitchType), Electrification(Electrification), + NeutralSection(NeutralSection), } impl From for ObjectCache { @@ -96,7 +98,7 @@ impl From for ObjectCache { match railjson { RailjsonObject::TrackSection { railjson } => ObjectCache::TrackSection(railjson.into()), RailjsonObject::Signal { railjson } => ObjectCache::Signal(railjson.into()), - RailjsonObject::NeutralSection { .. } => unimplemented!(), + RailjsonObject::NeutralSection { railjson } => ObjectCache::NeutralSection(railjson), RailjsonObject::SpeedSection { railjson } => ObjectCache::SpeedSection(railjson), RailjsonObject::Switch { railjson } => ObjectCache::Switch(railjson.into()), RailjsonObject::SwitchType { railjson } => ObjectCache::SwitchType(railjson), @@ -130,6 +132,7 @@ impl OSRDIdentified for ObjectCache { ObjectCache::OperationalPoint(obj) => obj.get_id(), ObjectCache::SwitchType(obj) => obj.get_id(), ObjectCache::Electrification(obj) => obj.get_id(), + ObjectCache::NeutralSection(obj) => obj.get_id(), } } } @@ -147,6 +150,7 @@ impl OSRDObject for ObjectCache { ObjectCache::OperationalPoint(_) => ObjectType::OperationalPoint, ObjectCache::SwitchType(_) => ObjectType::SwitchType, ObjectCache::Electrification(_) => ObjectType::Electrification, + ObjectCache::NeutralSection(_) => ObjectType::NeutralSection, } } } @@ -166,6 +170,9 @@ impl ObjectCache { ObjectCache::Electrification(electrification) => { electrification.get_track_referenced_id() } + ObjectCache::NeutralSection(neutral_section) => { + neutral_section.get_track_referenced_id() + } } } @@ -248,6 +255,14 @@ impl ObjectCache { _ => panic!("ObjectCache is not a Electrification"), } } + + /// Unwrap a neutral section from the object cache + pub fn unwrap_neutral_section(&self) -> &NeutralSection { + match self { + ObjectCache::NeutralSection(neutral_section) => neutral_section, + _ => panic!("ObjectCache is not a NeutralSection"), + } + } } #[derive(QueryableByName, Debug, Clone)] @@ -376,6 +391,11 @@ impl InfraCache { &self.objects[ObjectType::SpeedSection] } + /// Retrieve the cache of neutral sections + pub fn neutral_sections(&self) -> &HashMap { + &self.objects[ObjectType::NeutralSection] + } + /// Retrieve the cache of routes pub fn routes(&self) -> &HashMap { &self.objects[ObjectType::Route] @@ -442,6 +462,12 @@ impl InfraCache { .into_iter() .try_for_each(|speed| infra_cache.add(speed))?; + // Load speed sections tracks references + find_all_schemas::>(conn, infra_id) + .await? + .into_iter() + .try_for_each(|neutralsection| infra_cache.add(neutralsection))?; + // Load routes tracks references find_all_schemas::<_, Vec>(conn, infra_id) .await? @@ -594,6 +620,9 @@ impl InfraCache { ObjectCache::Electrification(electrification) => { self.add::(electrification)? } + ObjectCache::NeutralSection(neutral_section) => { + self.add::(neutral_section)? + } } Ok(()) } @@ -643,6 +672,17 @@ impl InfraCache { .unwrap_speed_section()) } + pub fn get_neutral_section(&self, neutral_section_id: &str) -> Result<&NeutralSection> { + Ok(self + .neutral_sections() + .get(neutral_section_id) + .ok_or_else(|| InfraCacheEditoastError::ObjectNotFound { + obj_type: ObjectType::NeutralSection.to_string(), + obj_id: neutral_section_id.to_string(), + })? + .unwrap_neutral_section()) + } + pub fn get_detector(&self, detector_id: &str) -> Result<&DetectorCache> { Ok(self .detectors() diff --git a/editoast/src/infra_cache/object_cache.rs b/editoast/src/infra_cache/object_cache.rs index a970547d068..a8fbf78e451 100644 --- a/editoast/src/infra_cache/object_cache.rs +++ b/editoast/src/infra_cache/object_cache.rs @@ -1,6 +1,7 @@ mod buffer_stop_cache; mod detector_cache; mod electrification_cache; +mod neutral_section_cache; mod operational_point_cache; mod route_cache; mod signal_cache; @@ -12,12 +13,7 @@ mod track_section_cache; pub use buffer_stop_cache::BufferStopCache; pub use detector_cache::DetectorCache; pub use operational_point_cache::OperationalPointCache; +pub use operational_point_cache::OperationalPointPartCache; pub use signal_cache::SignalCache; pub use switch_cache::SwitchCache; pub use track_section_cache::TrackSectionCache; - -cfg_if! { - if #[cfg(test)] { - pub use operational_point_cache::OperationalPointPartCache; - } -} diff --git a/editoast/src/infra_cache/object_cache/neutral_section_cache.rs b/editoast/src/infra_cache/object_cache/neutral_section_cache.rs new file mode 100644 index 00000000000..636a3d9b162 --- /dev/null +++ b/editoast/src/infra_cache/object_cache/neutral_section_cache.rs @@ -0,0 +1,21 @@ +use crate::infra_cache::Cache; +use crate::infra_cache::ObjectCache; +use editoast_schemas::infra::NeutralSection; + +impl Cache for NeutralSection { + fn get_track_referenced_id(&self) -> Vec<&String> { + let mut res: Vec<_> = self.track_ranges.iter().map(|tr| &*tr.track).collect(); + res.extend(self.announcement_track_ranges.iter().map(|tr| &*tr.track)); + if let Some(ext) = &self.extensions.neutral_sncf { + res.push(&*ext.exe.track); + res.extend(ext.announcement.iter().map(|sign| &*sign.track)); + res.extend(ext.end.iter().map(|sign| &*sign.track)); + res.extend(ext.rev.iter().map(|sign| &*sign.track)); + } + res + } + + fn get_object_cache(&self) -> ObjectCache { + ObjectCache::NeutralSection(self.clone()) + } +} diff --git a/editoast/src/infra_cache/operation/create.rs b/editoast/src/infra_cache/operation/create.rs index 4de1d0f3908..a281b11c162 100644 --- a/editoast/src/infra_cache/operation/create.rs +++ b/editoast/src/infra_cache/operation/create.rs @@ -199,6 +199,14 @@ impl From for RailjsonObject { } } +impl From for RailjsonObject { + fn from(neutralsection: NeutralSection) -> Self { + RailjsonObject::NeutralSection { + railjson: neutralsection, + } + } +} + impl From for RailjsonObject { fn from(switch: Switch) -> Self { RailjsonObject::Switch { railjson: switch } diff --git a/editoast/src/main.rs b/editoast/src/main.rs index 05972f9b9ea..c6eb237ad75 100644 --- a/editoast/src/main.rs +++ b/editoast/src/main.rs @@ -1,7 +1,5 @@ #[macro_use] extern crate diesel; -#[macro_use] -extern crate cfg_if; mod client; mod core; diff --git a/editoast/src/views/infra/auto_fixes/electrifications.rs b/editoast/src/views/infra/auto_fixes/electrifications.rs index 75afcb5275c..b5354342972 100644 --- a/editoast/src/views/infra/auto_fixes/electrifications.rs +++ b/editoast/src/views/infra/auto_fixes/electrifications.rs @@ -1,6 +1,7 @@ use itertools::Itertools; -use serde_json::from_value; -use serde_json::json; +use json_patch::Patch; +use json_patch::PatchOperation; +use json_patch::RemoveOperation; use std::collections::HashMap; use tracing::debug; use tracing::error; @@ -53,11 +54,9 @@ pub fn fix_electrification( Operation::Update(UpdateOperation { obj_id: electrification.get_id().clone(), obj_type: electrification.get_type(), - railjson_patch: from_value(json!([{ - "op": "remove", - "path": format!("/track_ranges/{track_range_idx}"), - }])) - .unwrap(), + railjson_patch: Patch(vec![PatchOperation::Remove(RemoveOperation { + path: format!("/track_ranges/{track_range_idx}").parse().unwrap(), + })]), }) } OrderedOperation::Delete => { diff --git a/editoast/src/views/infra/auto_fixes/speed_section.rs b/editoast/src/views/infra/auto_fixes/speed_section.rs index 9ce32205faf..a067e8f3d5c 100644 --- a/editoast/src/views/infra/auto_fixes/speed_section.rs +++ b/editoast/src/views/infra/auto_fixes/speed_section.rs @@ -1,6 +1,7 @@ use itertools::Itertools; -use serde_json::from_value; -use serde_json::json; +use json_patch::Patch; +use json_patch::PatchOperation; +use json_patch::RemoveOperation; use std::collections::HashMap; use tracing::debug; use tracing::error; @@ -53,11 +54,9 @@ pub fn fix_speed_section( Operation::Update(UpdateOperation { obj_id: speed_section.get_id().clone(), obj_type: speed_section.get_type(), - railjson_patch: from_value(json!([{ - "op": "remove", - "path": format!("/track_ranges/{track_range_idx}"), - }])) - .unwrap(), + railjson_patch: Patch(vec![PatchOperation::Remove(RemoveOperation { + path: format!("/track_ranges/{track_range_idx}").parse().unwrap(), + })]), }) } OrderedOperation::Delete => { diff --git a/editoast/src/views/infra/edition.rs b/editoast/src/views/infra/edition.rs index 5ea86f2e13a..91250bd5847 100644 --- a/editoast/src/views/infra/edition.rs +++ b/editoast/src/views/infra/edition.rs @@ -3,25 +3,60 @@ use actix_web::web::Data; use actix_web::web::Json; use actix_web::web::Path; use chashmap::CHashMap; +use diesel::sql_query; +use diesel::sql_types::BigInt; +use diesel::sql_types::Double; +use diesel::sql_types::Jsonb; +use diesel::sql_types::Text; +use diesel::QueryableByName; +use diesel_async::RunQueryDsl; use editoast_derive::EditoastError; +use editoast_schemas::infra::ApplicableDirectionsTrackRange; +use editoast_schemas::infra::DirectionalTrackRange; +use editoast_schemas::infra::Endpoint; +use editoast_schemas::infra::Sign; +use editoast_schemas::infra::Switch; +use editoast_schemas::infra::TrackEndpoint; +use editoast_schemas::infra::TrackOffset; +use editoast_schemas::infra::TrackSection; +use editoast_schemas::primitives::Identifier; +use editoast_schemas::primitives::OSRDIdentified; +use editoast_schemas::primitives::ObjectType; +use itertools::Itertools; +use json_patch::{AddOperation, Patch, PatchOperation, RemoveOperation, ReplaceOperation}; +use serde::Deserialize; +use serde::Serialize; +use serde_json::json; +use std::collections::HashMap; use thiserror::Error; +use tracing::error; +use uuid::Uuid; use crate::error::Result; use crate::generated_data; +use crate::infra_cache::object_cache::OperationalPointPartCache; use crate::infra_cache::operation::CacheOperation; +use crate::infra_cache::operation::DeleteOperation; use crate::infra_cache::operation::Operation; use crate::infra_cache::operation::RailjsonObject; +use crate::infra_cache::operation::UpdateOperation; use crate::infra_cache::InfraCache; use crate::infra_cache::ObjectCache; +use crate::map; use crate::map::MapLayers; -use crate::map::{self}; +use crate::modelsv2::get_table; use crate::modelsv2::prelude::*; use crate::modelsv2::DbConnection; use crate::modelsv2::DbConnectionPool; use crate::modelsv2::Infra; use crate::views::infra::InfraApiError; +use crate::views::infra::InfraIdParam; use crate::RedisClient; +crate::routes! { + split_track_section, +} + /// CRUD for edit an infrastructure. Takes a batch of operations. #[post("")] pub async fn edit<'a>( @@ -54,6 +89,741 @@ pub async fn edit<'a>( Ok(Json(operation_results)) } +#[derive(QueryableByName, Debug, Clone, Serialize, Deserialize)] +pub struct SplitedTrackSectionWithData { + #[diesel(sql_type = Text)] + obj_id: String, + #[diesel(sql_type = Jsonb)] + railjson: diesel_json::Json, + #[diesel(sql_type = Jsonb)] + left_geo: diesel_json::Json, + #[diesel(sql_type = Jsonb)] + right_geo: diesel_json::Json, +} + +#[utoipa::path( + tag = "infra", + params(InfraIdParam), + request_body = TrackOffset, + responses( + (status = 200, body = inline(Vec), description = "ID of the trackSections created") + ), +)] +#[post("/split_track_section")] +pub async fn split_track_section<'a>( + infra: Path, + payload: Json, + db_pool: Data, + infra_caches: Data>, + redis_client: Data, + map_layers: Data, +) -> Result>> { + let payload = payload.into_inner(); + let infra_id = infra.into_inner(); + let mut conn = db_pool.get().await?; + + // Check the infra + let mut infra = + Infra::retrieve_or_fail(&mut conn, infra_id, || InfraApiError::NotFound { infra_id }) + .await?; + let mut infra_cache = InfraCache::get_or_load_mut(&mut conn, &infra_caches, &infra).await?; + + // Get tracks cache if it exists + let tracksection_cached = infra_cache.get_track_section(&payload.track)?.clone(); + + // Check if the distance is compatible with the length of the TrackSection + let distance = (payload.offset / 1000) as f64; + let distance_fraction = distance / tracksection_cached.length; + if distance <= 0.0 || distance >= tracksection_cached.length { + return Err(EditionError::SplitTrackSectionBadOffset { + infra_id, + tracksection_id: payload.track.to_string(), + tracksection_length: tracksection_cached.length, + } + .into()); + } + + // Calling the DB to get the full object and also the splitted geo + let query = format!("SELECT + object_table.obj_id as obj_id, + object_table.data as railjson, + ST_AsGeoJSON(ST_LineSubstring(ST_GeomFromGeoJSON(object_table.data->'geo'), 0, $3))::jsonb as left_geo, + ST_AsGeoJSON(ST_LineSubstring(ST_GeomFromGeoJSON(object_table.data->'geo'), $3, 1))::jsonb as right_geo + FROM {} AS object_table + WHERE object_table.infra_id = $1 AND object_table.obj_id = $2", + get_table(&ObjectType::TrackSection), + ); + let result: Vec = sql_query(query) + .bind::(infra_id) + .bind::(payload.track.to_string()) + .bind::(distance_fraction) + .load(&mut conn) + .await?; + let tracksection_data = result[0].clone(); + let tracksection = tracksection_data.railjson.as_ref().clone(); + + // Building the two newly tracksections from the splitted one + // ~~~~~~~~~~~~~~~ + // left + let left_tracksection_id = Uuid::new_v4(); + let left_tracksection = TrackSection { + id: Identifier::from(left_tracksection_id), + length: distance, + geo: tracksection_data.left_geo.as_ref().clone(), + sch: tracksection_data.left_geo.as_ref().clone(), + slopes: tracksection + .slopes + .iter() + .filter(|e| e.begin <= distance) + .map(|e| { + let mut item = e.clone(); + if item.end > distance { + item.end = distance; + } + item + }) + .collect_vec(), + curves: tracksection + .curves + .iter() + .filter(|e| e.begin <= distance) + .map(|e| { + let mut item = e.clone(); + if item.end > distance { + item.end = distance; + } + item + }) + .collect_vec(), + loading_gauge_limits: tracksection + .loading_gauge_limits + .iter() + .filter(|e| e.begin <= distance) + .map(|e| { + let mut item = e.clone(); + if item.end > distance { + item.end = distance; + } + item + }) + .collect_vec(), + ..tracksection.clone() + }; + + // right + let right_tracksection_id = Uuid::new_v4(); + let right_tracksection = TrackSection { + id: Identifier::from(right_tracksection_id), + length: tracksection.length - distance, + geo: tracksection_data.right_geo.as_ref().clone(), + sch: tracksection_data.right_geo.as_ref().clone(), + slopes: tracksection + .slopes + .iter() + .filter(|e| e.end >= distance) + .map(|e| { + let mut item = e.clone(); + if item.begin < distance { + item.begin = distance; + } else { + item.begin -= distance; + } + item.end -= distance; + item + }) + .collect_vec(), + curves: tracksection + .curves + .iter() + .filter(|e| e.end >= distance) + .map(|e| { + let mut item = e.clone(); + if item.begin < distance { + item.begin = 0.0; + } else { + item.begin -= distance; + } + item.end -= distance; + item + }) + .collect_vec(), + loading_gauge_limits: tracksection + .loading_gauge_limits + .iter() + .filter(|e| e.end >= distance) + .map(|e| { + let mut item = e.clone(); + if item.begin < distance { + item.begin = distance; + } else { + item.begin -= distance; + } + item.end -= distance; + item + }) + .collect_vec(), + ..tracksection.clone() + }; + + // track link + let mut ports = HashMap::new(); + ports.insert( + "A".into(), + TrackEndpoint { + track: Identifier::from(left_tracksection_id), + endpoint: Endpoint::End, + }, + ); + ports.insert( + "B".into(), + TrackEndpoint { + track: Identifier::from(right_tracksection_id), + endpoint: Endpoint::Begin, + }, + ); + let track_link = Switch { + id: Identifier::from(Uuid::new_v4()), + switch_type: Identifier::from("link"), + group_change_delay: 0.0, + ports, + ..Switch::default() + }; + + // Compute operations + // ~~~~~~~~~~~~~~~~~~~~~~~ + // Firstly, we create the two newly tracks + let mut operations: Vec = [ + Operation::Create(Box::new(RailjsonObject::TrackSection { + railjson: left_tracksection, + })), + Operation::Create(Box::new(RailjsonObject::TrackSection { + railjson: right_tracksection, + })), + Operation::Create(Box::new(RailjsonObject::Switch { + railjson: track_link, + })), + ] + .to_vec(); + + operations.extend(get_splitted_operations_for_impacted( + &mut infra_cache, + &tracksection, + distance, + left_tracksection_id, + right_tracksection_id, + )); + + // last operation, we delete the given track + operations.push(Operation::Delete(DeleteOperation { + obj_type: ObjectType::TrackSection, + obj_id: payload.track.to_string(), + })); + + // Apply operations + apply_edit(&mut conn, &mut infra, &operations, &mut infra_cache).await?; + let mut conn = redis_client.get_connection().await?; + map::invalidate_all( + &mut conn, + &map_layers.layers.keys().cloned().collect(), + infra_id, + ) + .await?; + + // Return the result + Ok(Json( + [ + left_tracksection_id.to_string(), + right_tracksection_id.to_string(), + ] + .to_vec(), + )) +} + +/// Function used while splitting a track section. +/// It compute the impacted list of operations in the DB to do, following the split of the tracksection. +/// +/// # Example +/// * On Switch, we change the ports ref +/// * On electrification, we change the track ranges +/// * On Detector, BufferStop : we change the track and possibly its position +/// * .... +/// +/// # Arguments +/// * `tracksection_id` - ID of the original track (the splitted one) +/// * `distance` - Distance (in meters) where the tracksection is splitted +/// * `left_tracksection_id` - ID of the newly "left" tracksection +/// * `tracksection_id` - ID of the newly "right" tracksection +/// * `path` - JSON path for the operation +/// * `sign` - Sign to check +fn get_splitted_operations_for_impacted( + infra_cache: &mut InfraCache, + tracksection: &TrackSection, + distance: f64, + left_tracksection_id: Uuid, + right_tracksection_id: Uuid, +) -> Vec { + let mut operations: Vec = Vec::::new(); + for obj in infra_cache + .track_sections_refs + .get(tracksection.get_id()) + .unwrap() + { + match obj.obj_type { + ObjectType::Signal => { + let ponctual_item = infra_cache.get_signal(&obj.obj_id).unwrap(); + operations.push(Operation::Update(UpdateOperation { + obj_type: obj.obj_type, + obj_id: obj.obj_id.to_string(), + railjson_patch: Patch(vec![ + PatchOperation::Replace(ReplaceOperation { + path: "/track".to_string().parse().unwrap(), + value: if ponctual_item.position <= distance { + json!(Identifier::from(left_tracksection_id)) + } else { + json!(Identifier::from(right_tracksection_id)) + }, + }), + PatchOperation::Replace(ReplaceOperation { + path: "/position".to_string().parse().unwrap(), + value: if ponctual_item.position <= distance { + json!(ponctual_item.position) + } else { + json!(ponctual_item.position - distance) + }, + }), + ]), + })); + } + ObjectType::BufferStop => { + let ponctual_item = infra_cache.get_buffer_stop(&obj.obj_id).unwrap(); + operations.push(Operation::Update(UpdateOperation { + obj_type: obj.obj_type, + obj_id: obj.obj_id.to_string(), + railjson_patch: Patch(vec![ + PatchOperation::Replace(ReplaceOperation { + path: "/track".to_string().parse().unwrap(), + value: if ponctual_item.position <= distance { + json!(Identifier::from(left_tracksection_id)) + } else { + json!(Identifier::from(right_tracksection_id)) + }, + }), + PatchOperation::Replace(ReplaceOperation { + path: "/position".to_string().parse().unwrap(), + value: if ponctual_item.position <= distance { + json!(ponctual_item.position) + } else { + json!(ponctual_item.position - distance) + }, + }), + ]), + })); + } + ObjectType::Detector => { + let ponctual_item = infra_cache.get_detector(&obj.obj_id).unwrap(); + operations.push(Operation::Update(UpdateOperation { + obj_type: obj.obj_type, + obj_id: obj.obj_id.to_string(), + railjson_patch: Patch(vec![ + PatchOperation::Replace(ReplaceOperation { + path: "/track".to_string().parse().unwrap(), + value: if ponctual_item.position <= distance { + json!(Identifier::from(left_tracksection_id)) + } else { + json!(Identifier::from(right_tracksection_id)) + }, + }), + PatchOperation::Replace(ReplaceOperation { + path: "/position".to_string().parse().unwrap(), + value: if ponctual_item.position <= distance { + json!(ponctual_item.position) + } else { + json!(ponctual_item.position - distance) + }, + }), + ]), + })); + } + ObjectType::Switch => { + let switch = infra_cache.get_switch(&obj.obj_id).unwrap(); + let mut patch_operations: Vec = Vec::::new(); + // Check ports ref + for (key, value) in switch.ports.iter() { + if value.track == tracksection.id { + patch_operations.push(PatchOperation::Replace(ReplaceOperation { + path: format!("/ports/{}/track", key).parse().unwrap(), + value: if value.endpoint == Endpoint::Begin { + json!(Identifier::from(left_tracksection_id)) + } else { + json!(Identifier::from(right_tracksection_id)) + }, + })); + } + } + operations.push(Operation::Update(UpdateOperation { + obj_type: obj.obj_type, + obj_id: obj.obj_id.to_string(), + railjson_patch: Patch(patch_operations), + })); + } + ObjectType::Electrification => { + let electrification = infra_cache.get_electrification(&obj.obj_id).unwrap(); + // Check track ranges + operations.push(Operation::Update(UpdateOperation { + obj_type: obj.obj_type, + obj_id: obj.obj_id.to_string(), + railjson_patch: Patch(get_splitted_patch_operations_for_applicable_ranges( + tracksection.id.clone(), + distance, + left_tracksection_id, + right_tracksection_id, + "/track_ranges".to_string(), + &electrification.track_ranges, + )), + })); + } + ObjectType::SpeedSection => { + let speedsection = infra_cache.get_speed_section(&obj.obj_id).unwrap(); + let mut patch_operations: Vec = Vec::::new(); + // Check track ranges + patch_operations.extend(get_splitted_patch_operations_for_applicable_ranges( + tracksection.id.clone(), + distance, + left_tracksection_id, + right_tracksection_id, + "/track_ranges".to_string(), + &speedsection.track_ranges, + )); + // Check extensions for signs in extensions + if let Some(psl) = &speedsection.extensions.psl_sncf { + // check for `z`` + patch_operations.extend(get_splitted_patch_operations_for_sign( + tracksection.id.clone(), + distance, + left_tracksection_id, + right_tracksection_id, + "/extensions/psl_sncf/z/track".to_string(), + psl.z(), + )); + // check for `announcement` + for (index, sign) in psl.announcement().iter().enumerate() { + patch_operations.extend(get_splitted_patch_operations_for_sign( + tracksection.id.clone(), + distance, + left_tracksection_id, + right_tracksection_id, + format!("/extensions/psl_sncf/announcement/{}", index), + sign, + )); + } + // check for `r` + for (index, sign) in psl.r().iter().enumerate() { + patch_operations.extend(get_splitted_patch_operations_for_sign( + tracksection.id.clone(), + distance, + left_tracksection_id, + right_tracksection_id, + format!("/extensions/psl_sncf/r/{}", index), + sign, + )); + } + } + operations.push(Operation::Update(UpdateOperation { + obj_type: obj.obj_type, + obj_id: obj.obj_id.to_string(), + railjson_patch: Patch(patch_operations), + })); + } + ObjectType::OperationalPoint => { + let operationalpoint = infra_cache.get_operational_point(&obj.obj_id).unwrap(); + let mut patch_operations: Vec = Vec::::new(); + for (index, part) in operationalpoint.parts.iter().enumerate() { + if part.track == tracksection.id { + if part.position <= distance { + patch_operations.push(PatchOperation::Replace(ReplaceOperation { + path: format!("/parts/{}/track", index).parse().unwrap(), + value: json!(Identifier::from(left_tracksection_id)), + })); + } else { + patch_operations.push(PatchOperation::Replace(ReplaceOperation { + path: format!("/parts/{}", index).parse().unwrap(), + value: json!(OperationalPointPartCache { + track: Identifier::from(right_tracksection_id), + position: part.position - distance, + }), + })); + } + } + } + operations.push(Operation::Update(UpdateOperation { + obj_type: obj.obj_type, + obj_id: obj.obj_id.to_string(), + railjson_patch: Patch(patch_operations), + })); + } + ObjectType::NeutralSection => { + let neutralsection = infra_cache.get_neutral_section(&obj.obj_id).unwrap(); + let mut patch_operations: Vec = Vec::::new(); + // Check track ranges + patch_operations.extend(get_splitted_patch_operations_for_ranges( + tracksection.id.clone(), + distance, + left_tracksection_id, + right_tracksection_id, + "/track_ranges".to_string(), + &neutralsection.track_ranges, + )); + // Check extensions for signs in extensions + if let Some(neutral) = &neutralsection.extensions.neutral_sncf { + // Check for `z`` + patch_operations.extend(get_splitted_patch_operations_for_sign( + tracksection.id.clone(), + distance, + left_tracksection_id, + right_tracksection_id, + "/extensions/neutral_sncf/exe".to_string(), + &neutral.exe, + )); + // check for `announcement` + for (index, sign) in neutral.announcement.iter().enumerate() { + patch_operations.extend(get_splitted_patch_operations_for_sign( + tracksection.id.clone(), + distance, + left_tracksection_id, + right_tracksection_id, + format!("/extensions/neutral_sncf/announcement/{}", index), + sign, + )); + } + // check for `end` + for (index, sign) in neutral.end.iter().enumerate() { + patch_operations.extend(get_splitted_patch_operations_for_sign( + tracksection.id.clone(), + distance, + left_tracksection_id, + right_tracksection_id, + format!("/extensions/neutral_sncf/end/{}", index), + sign, + )); + } + // check for `rev` + for (index, sign) in neutral.rev.iter().enumerate() { + patch_operations.extend(get_splitted_patch_operations_for_sign( + tracksection.id.clone(), + distance, + left_tracksection_id, + right_tracksection_id, + format!("/extensions/neutral_sncf/rev/{}", index), + sign, + )); + } + } + operations.push(Operation::Update(UpdateOperation { + obj_type: obj.obj_type, + obj_id: obj.obj_id.to_string(), + railjson_patch: Patch(patch_operations), + })); + } + // TODO: route + ObjectType::Route => (), + // TrackSection doesn't depend on track + ObjectType::TrackSection => (), + // Switch type doesn't depend on track + ObjectType::SwitchType => (), + } + } + + operations +} + +/// Function used while splitting a track section. +/// It helps to generate a JSON patch operation for a `Sign`. +/// +/// # Arguments +/// * `tracksection_id` - ID of the original track (the splitted one) +/// * `distance` - Distance (in meters) where the tracksection is splitted +/// * `left_tracksection_id` - ID of the newly "left" tracksection +/// * `tracksection_id` - ID of the newly "right" tracksection +/// * `path` - JSON path for the operation +/// * `sign` - Sign to check +fn get_splitted_patch_operations_for_sign( + tracksection_id: Identifier, + distance: f64, + left_tracksection_id: Uuid, + right_tracksection_id: Uuid, + path: String, + sign: &Sign, +) -> Vec { + let mut patch_operations: Vec = Vec::::new(); + if sign.track == tracksection_id { + if sign.position <= distance { + patch_operations.push(PatchOperation::Replace(ReplaceOperation { + path: format!("{}/track", path).parse().unwrap(), + value: json!(Identifier::from(left_tracksection_id)), + })); + } else { + patch_operations.push(PatchOperation::Replace(ReplaceOperation { + path: format!("{}/track", path).parse().unwrap(), + value: json!(Identifier::from(right_tracksection_id)), + })); + patch_operations.push(PatchOperation::Replace(ReplaceOperation { + path: format!("{}/position", path).parse().unwrap(), + value: json!(sign.position - distance), + })); + } + } + patch_operations +} + +/// Function used while splitting a track section. +/// It helps to generate a JSON patch operation for a `Vec`. +/// +/// # Arguments +/// * `tracksection_id` - ID of the original track (the splitted one) +/// * `distance` - Distance (in meters) where the tracksection is splitted +/// * `left_tracksection_id` - ID of the newly "left" tracksection +/// * `right_tracksection_id` - ID of the newly "right" tracksection +/// * `path` - JSON path for the operation +/// * `ranges` - List of track section ranges +fn get_splitted_patch_operations_for_applicable_ranges( + tracksection_id: Identifier, + distance: f64, + left_tracksection_id: Uuid, + right_tracksection_id: Uuid, + path: String, + ranges: &[ApplicableDirectionsTrackRange], +) -> Vec { + let mut patch_operations: Vec = Vec::::new(); + for (index, range) in ranges.iter().enumerate() { + if range.track == tracksection_id { + // Case where the range is fully on left side + // so we just need to change the track + if range.end <= distance { + patch_operations.push(PatchOperation::Replace(ReplaceOperation { + path: format!("{}/{}/track", path, index).parse().unwrap(), + value: json!(Identifier::from(left_tracksection_id)), + })); + } else { + // Case where the range is fully on right side + // so we need to change the track and to substract the distance on begin & end + if range.begin >= distance { + patch_operations.push(PatchOperation::Replace(ReplaceOperation { + path: format!("{}/{}/track", path, index).parse().unwrap(), + value: json!(Identifier::from(right_tracksection_id)), + })); + patch_operations.push(PatchOperation::Replace(ReplaceOperation { + path: format!("{}/{}/begin", path, index).parse().unwrap(), + value: json!(range.begin - distance), + })); + patch_operations.push(PatchOperation::Replace(ReplaceOperation { + path: format!("{}/{}/end", path, index).parse().unwrap(), + value: json!(range.end - distance), + })); + } + // Case where the range is on left AND right side + else { + patch_operations.push(PatchOperation::Remove(RemoveOperation { + path: format!("{}/{}", path, index).parse().unwrap(), + })); + patch_operations.push(PatchOperation::Add(AddOperation { + path: format!("{}/-", path).parse().unwrap(), + value: json!(ApplicableDirectionsTrackRange { + track: Identifier::from(left_tracksection_id), + end: distance, + ..range.clone() + }), + })); + patch_operations.push(PatchOperation::Add(AddOperation { + path: format!("{}/-", path).parse().unwrap(), + value: json!(ApplicableDirectionsTrackRange { + track: Identifier::from(right_tracksection_id), + begin: 0.0, + end: range.end - distance, + ..range.clone() + }), + })); + } + } + } + } + patch_operations +} + +/// Function used while splitting a track section. +/// It helps to generate a JSON patch operation for a `Vec`. +/// /!\ It's the same function than the one above, but for `DirectionalTrackRange`` instead of `ApplicableDirectionsTrackRange``. +/// +/// # Arguments +/// * `tracksection_id` - ID of the original track (the splitted one) +/// * `distance` - Distance (in meters) where the tracksection is splitted +/// * `left_tracksection_id` - ID of the newly "left" tracksection +/// * `right_tracksection_id` - ID of the newly "right" tracksection +/// * `path` - JSON path for the operation +/// * `ranges` - List of track section ranges +fn get_splitted_patch_operations_for_ranges( + tracksection_id: Identifier, + distance: f64, + left_tracksection_id: Uuid, + right_tracksection_id: Uuid, + path: String, + ranges: &[DirectionalTrackRange], +) -> Vec { + let mut patch_operations: Vec = Vec::::new(); + for (index, range) in ranges.iter().enumerate() { + if range.track == tracksection_id { + // Case where the range is fully on left side + // so we just need to change the track + if range.end <= distance { + patch_operations.push(PatchOperation::Replace(ReplaceOperation { + path: format!("{}/{}/track", path, index).parse().unwrap(), + value: json!(Identifier::from(left_tracksection_id)), + })); + } else { + // Case where the range is fully on right side + // so we need to change the track and to substract the distance on begin & end + if range.begin >= distance { + patch_operations.push(PatchOperation::Replace(ReplaceOperation { + path: format!("{}/{}/track", path, index).parse().unwrap(), + value: json!(Identifier::from(right_tracksection_id)), + })); + patch_operations.push(PatchOperation::Replace(ReplaceOperation { + path: format!("{}/{}/begin", path, index).parse().unwrap(), + value: json!(range.begin - distance), + })); + patch_operations.push(PatchOperation::Replace(ReplaceOperation { + path: format!("{}/{}/end", path, index).parse().unwrap(), + value: json!(range.end - distance), + })); + } + // Case where the range is on left AND right side + else { + patch_operations.push(PatchOperation::Remove(RemoveOperation { + path: format!("{}/{}", path, index).parse().unwrap(), + })); + patch_operations.push(PatchOperation::Add(AddOperation { + path: format!("{}/-", path).parse().unwrap(), + value: json!(DirectionalTrackRange { + track: Identifier::from(left_tracksection_id), + end: distance, + ..range.clone() + }), + })); + patch_operations.push(PatchOperation::Add(AddOperation { + path: format!("{}/-", path).parse().unwrap(), + value: json!(DirectionalTrackRange { + track: Identifier::from(right_tracksection_id), + begin: 0.0, + end: range.end - distance, + ..range.clone() + }), + })); + } + } + } + } + patch_operations +} + async fn apply_edit( conn: &mut DbConnection, infra: &mut Infra, @@ -109,4 +879,140 @@ async fn apply_edit( enum EditionError { #[error("Infra {infra_id} is locked")] InfraIsLocked { infra_id: i64 }, + + #[error("Invalid split offset for track section '{tracksection_id}' in infra '{infra_id}'. Expected a value between 0 and {tracksection_length} meters")] + #[editoast_error(status = 400)] + SplitTrackSectionBadOffset { + infra_id: i64, + tracksection_id: String, + tracksection_length: f64, + }, +} + +#[cfg(test)] +pub mod tests { + use actix_web::http::StatusCode; + use actix_web::test::call_and_read_body_json; + use actix_web::test::call_service; + use actix_web::test::TestRequest; + use rstest::*; + + use super::*; + use crate::fixtures::tests::db_pool; + use crate::fixtures::tests::small_infra; + use crate::views::infra::errors::InfraError; + use crate::views::pagination::PaginatedResponse; + use crate::views::tests::create_test_service; + + #[rstest] + async fn split_track_section_should_return_404_with_bad_infra() { + // Init + let app = create_test_service().await; + + // Make a call with a bad infra ID + let req = TestRequest::post() + .uri("/infra/123456789/split_track_section/") + .set_json(json!({ + "track": String::from("INVALID-ID"), + "offset": 1, + })) + .to_request(); + let res = call_service(&app, req).await; + + // Check that we receive a 404 + assert_eq!(res.status(), StatusCode::NOT_FOUND); + } + + #[rstest] + async fn split_track_section_should_return_404_with_bad_id() { + // Init + let pg_db_pool = db_pool(); + let small_infra = small_infra(pg_db_pool.clone()).await; + let app = create_test_service().await; + + // Make a call with a bad ID + let req = TestRequest::post() + .uri(format!("/infra/{}/split_track_section", small_infra.id()).as_str()) + .set_json(json!({ + "track":"INVALID-ID", + "offset": 1, + })) + .to_request(); + let res = call_service(&app, req).await; + + // Check that we receive a 404 + assert_eq!(res.status(), StatusCode::NOT_FOUND); + } + + #[rstest] + async fn split_track_section_should_fail_with_bad_distance() { + // Init + let pg_db_pool = db_pool(); + let small_infra = small_infra(pg_db_pool.clone()).await; + let app = create_test_service().await; + + // Make a call with a bad distance + let req = TestRequest::post() + .uri(format!("/infra/{}/split_track_section", small_infra.id()).as_str()) + .set_json(json!({ + "track": "TA0", + "offset": 5000000, + })) + .to_request(); + let res = call_service(&app, req).await; + + // Check that we receive an error + assert_eq!(res.status(), StatusCode::BAD_REQUEST); + } + + #[rstest] + async fn split_track_section_should_work() { + // Init + let pg_db_pool = db_pool(); + let small_infra = small_infra(pg_db_pool.clone()).await; + let app = create_test_service().await; + + // Refresh the infra to get the good number of infra errors + let req_refresh = TestRequest::post() + .uri(format!("/infra/refresh/?infras={}&force=true", small_infra.id()).as_str()) + .to_request(); + call_service(&app, req_refresh).await; + + // Get infra errors + let req_init_errors = TestRequest::get() + .uri(format!("/infra/{}/errors", small_infra.id()).as_str()) + .to_request(); + let init_errors: PaginatedResponse = + call_and_read_body_json(&app, req_init_errors).await; + + // Make a call to split the track section + let req = TestRequest::post() + .uri(format!("/infra/{}/split_track_section", small_infra.id()).as_str()) + .set_json(json!({ + "track": "TA0", + "offset": 1000000, + })) + .to_request(); + let res: Vec = call_and_read_body_json(&app, req).await; + + // Check the response + assert_eq!(res.len(), 2); + + // Check that infra errors has not increased with the split (omit route error for now) + let req_errors = TestRequest::get() + .uri(format!("/infra/{}/errors", small_infra.id()).as_str()) + .to_request(); + let errors: PaginatedResponse = call_and_read_body_json(&app, req_errors).await; + let errors_without_routes: Vec = errors + .results + .into_iter() + .filter(|e| { + !e.information["error_type"] + .as_str() + .unwrap() + .ends_with("_route") + }) + .collect(); + assert_eq!(errors_without_routes.len() - init_errors.results.len(), 0); + } } diff --git a/editoast/src/views/infra/mod.rs b/editoast/src/views/infra/mod.rs index ac885d03c4a..348acf1bc3f 100644 --- a/editoast/src/views/infra/mod.rs +++ b/editoast/src/views/infra/mod.rs @@ -8,9 +8,6 @@ mod pathfinding; mod railjson; mod routes; -use std::collections::HashMap; -use std::sync::Arc; - use actix_web::delete; use actix_web::dev::HttpServiceFactory; use actix_web::get; @@ -32,10 +29,13 @@ use diesel::QueryableByName; use editoast_derive::EditoastError; use serde::Deserialize; use serde::Serialize; +use std::collections::HashMap; +use std::sync::Arc; use thiserror::Error; use utoipa::IntoParams; use self::edition::edit; +use self::edition::split_track_section; use super::params::List; use crate::core::infra_loading::InfraLoadRequest; use crate::core::infra_state::InfraStateRequest; @@ -45,8 +45,8 @@ use crate::core::CoreClient; use crate::error::Result; use crate::infra_cache::InfraCache; use crate::infra_cache::ObjectCache; +use crate::map; use crate::map::MapLayers; -use crate::map::{self}; use crate::models::List as ModelList; use crate::models::NoParams; use crate::modelsv2::prelude::*; @@ -65,6 +65,7 @@ crate::routes! { auto_fixes::routes(), pathfinding::routes(), attached::routes(), + edition::routes(), lock, unlock, get_speed_limit_tags, @@ -99,6 +100,7 @@ pub fn infra_routes() -> impl HttpServiceFactory { delete, clone, edit, + split_track_section, rename, lock, unlock, diff --git a/front/public/locales/en/errors.json b/front/public/locales/en/errors.json index 142bf6c3685..2ad2117f871 100644 --- a/front/public/locales/en/errors.json +++ b/front/public/locales/en/errors.json @@ -18,7 +18,7 @@ "auto_fixes": { "ConflictingFixesOnSameObject": "Conflicting fixes for the same object on the same fix-iteration", "FixTrialFailure": "Failed trying to apply fixes", - "MaximumIterationReached": "Reached maximum number of iterations to fix infra without providing every possible fixes", + "MaximumIterationReached": "Reached maximum number of iterations to fix infrastructure without providing every possible fixes", "MissingErrorObject": "Failed to find the error's object" }, "cache_operation": { @@ -49,7 +49,8 @@ "infra": { "NotFound": "", "edition": { - "InfraIsLocked": "Infra is locked" + "InfraIsLocked": "Infrastructure is locked", + "SplitTrackSectionBadOffset": "Distance to split track section '{{tracksection_id}}' in infrastructure '{{infra_id}}' is invalid. It must be between 0 and {{tracksection_length}} meters." }, "errors": { "WrongErrorTypeProvided": "Wrong Error type provided" @@ -86,11 +87,11 @@ }, "pathfinding": { "ElectricalProfilesOverlap": "Electrical Profile overlaps with others", - "ElectrificationOverlap": "Electrification {{electrification_id}} overlaps with other electrifications", - "InfraNotFound": "Infra {{infra_id}} does not exist", + "ElectrificationOverlap": "Electrification '{{electrification_id}}' overlaps with other electrifications", + "InfraNotFound": "Infrastructure '{{infra_id}}' does not exist", "NotFound": "Pathfinding {{pathfinding_id}} does not exist", "OperationalPointsNotFound": "Operational points do not exist: {{operational_points}}", - "RollingStockNotFound": "Rolling stock with id {{rolling_stock_id}} does not exist", + "RollingStockNotFound": "Rolling stock with id '{{rolling_stock_id}}' does not exist", "TrackSectionsNotFound": "Track sections do not exist: {{track_sections}}" }, "postgres": { @@ -151,7 +152,7 @@ "UnknownSignalingSystem": "Unknown signaling system" }, "stdcm": { - "InfraNotFound": "Infra {{infra_id}} does not exist" + "InfraNotFound": "Infrastructure '{{infra_id}}' does not exist" }, "stdcm_v2": { "InfraNotFound": "Infrastructure '{{infra_id}}' does not exist", @@ -164,8 +165,8 @@ "StartDateAfterEndDate": "The study start date must be before the end date" }, "timetable": { - "InfraNotLoaded": "Infra '{{infra_id}}' is not loaded", - "InfraNotFound": "Infra {{infra_id}} does not exist", + "InfraNotLoaded": "Infrastructure '{{infra_id}}' is not loaded", + "InfraNotFound": "Infrastructure '{{infra_id}}' does not exist", "NotFound": "Timetable '{{timetable_id}}' could not be found" }, "train_schedule": { @@ -182,7 +183,7 @@ "train_schedule_v2": { "BatchTrainScheduleNotFound": "'{{number}}' train schedule(s) could not be found", "NotFound": "Train Schedule '{{train_schedule_id}}' could not be found", - "InfraNotFound": "Infra '{{infra_id}}' could not be found" + "InfraNotFound": "Infrastructure '{{infra_id}}' could not be found" }, "url": { "InvalidUrl": "Invalid url '{{url}}'" diff --git a/front/public/locales/fr/errors.json b/front/public/locales/fr/errors.json index 38ce05652f5..56721779494 100644 --- a/front/public/locales/fr/errors.json +++ b/front/public/locales/fr/errors.json @@ -18,7 +18,7 @@ "auto_fixes": { "ConflictingFixesOnSameObject": "Correctifs conflictuels pour le même objet sur la même itération de correctif", "FixTrialFailure": "Echec de l'application des correctifs", - "MaximumIterationReached": "Nombre maximum d'itérations atteint pour corriger l'infra sans fournir tous les correctifs possibles", + "MaximumIterationReached": "Nombre maximum d'itérations atteint pour corriger l'infrastructure sans fournir tous les correctifs possibles", "MissingErrorObject": "Impossible de trouver l'objet de l'erreur" }, "cache_operation": { @@ -49,7 +49,8 @@ "infra": { "NotFound": "", "edition": { - "InfraIsLocked": "Infrastructure verrouillée" + "InfraIsLocked": "Infrastructure verrouillée", + "SplitTrackSectionBadOffset": "La distance pour scinder la section de ligne '{{tracksection_id}}' de l'infrastructure '{{infra_id}}' est invalide. La valeur doit être comprise entre 0 et {{tracksection_length}} mètres." }, "errors": { "WrongErrorTypeProvided": "Mauvais type d'erreur fourni" @@ -87,10 +88,10 @@ "pathfinding": { "ElectricalProfilesOverlap": "Des profils électriques se chevauchent", "ElectrificationOverlap": "Electrification {{electrification_id}} se supperpose avec d'autres", - "InfraNotFound": "Infrastructure {{infra_id}} non trouvée", - "NotFound": "Itinéraire {{pathfinding_id}} non trouvé", + "InfraNotFound": "Infrastructure '{{infra_id}}' non trouvée", + "NotFound": "Itinéraire '{{pathfinding_id}}' non trouvé", "OperationalPointsNotFound": "Points opérationnels {{operational_points}} non trouvés", - "RollingStockNotFound": "Matériel roulant {{rolling_stock_id}} non trouvé", + "RollingStockNotFound": "Matériel roulant '{{rolling_stock_id}}' non trouvé", "TrackSectionsNotFound": "Section de ligne {{track_sections}} non trouvée" }, "postgres": { @@ -167,7 +168,7 @@ }, "timetable": { "InfraNotLoaded": "L'infrastructure '{{infra_id}}' n'est pas chargée", - "InfraNotFound": "Infra '{{infra_id}}' non trouvée", + "InfraNotFound": "Infrastructure '{{infra_id}}' non trouvée", "NotFound": "Grille horaire '{{timetable_id}}' non trouvée" }, "train_schedule": { @@ -184,7 +185,7 @@ "train_schedule_v2": { "BatchTrainScheduleNotFound": "'{{number}}' circulation(s) n'ont pas pu être trouvée(s)", "NotFound": "Circulation '{{train_schedule_id}}' non trouvée", - "InfraNotFound": "Infra '{{infra_id}}' non trouvée" + "InfraNotFound": "Infrastructure '{{infra_id}}' non trouvée" }, "url": { "InvalidUrl": "Url invalide '{{url}}'" diff --git a/front/src/common/api/osrdEditoastApi.ts b/front/src/common/api/osrdEditoastApi.ts index 7dc28a007c4..924c7ec4c55 100644 --- a/front/src/common/api/osrdEditoastApi.ts +++ b/front/src/common/api/osrdEditoastApi.ts @@ -280,6 +280,17 @@ const injectedRtkApi = api query: (queryArg) => ({ url: `/infra/${queryArg.infraId}/speed_limit_tags/` }), providesTags: ['infra'], }), + postInfraByInfraIdSplitTrackSection: build.mutation< + PostInfraByInfraIdSplitTrackSectionApiResponse, + PostInfraByInfraIdSplitTrackSectionApiArg + >({ + query: (queryArg) => ({ + url: `/infra/${queryArg.infraId}/split_track_section/`, + method: 'POST', + body: queryArg.trackOffset, + }), + invalidatesTags: ['infra'], + }), getInfraByInfraIdSwitchTypes: build.query< GetInfraByInfraIdSwitchTypesApiResponse, GetInfraByInfraIdSwitchTypesApiArg @@ -1246,6 +1257,13 @@ export type GetInfraByInfraIdSpeedLimitTagsApiArg = { /** An existing infra ID */ infraId: number; }; +export type PostInfraByInfraIdSplitTrackSectionApiResponse = + /** status 200 ID of the trackSections created */ string[]; +export type PostInfraByInfraIdSplitTrackSectionApiArg = { + /** An existing infra ID */ + infraId: number; + trackOffset: TrackOffset; +}; export type GetInfraByInfraIdSwitchTypesApiResponse = /** status 200 A list of switch types */ SwitchType[]; export type GetInfraByInfraIdSwitchTypesApiArg = { @@ -2274,6 +2292,11 @@ export type PathfindingInput = { ending: PathfindingTrackLocationInput; starting: PathfindingTrackLocationInput; }; +export type TrackOffset = { + /** Offset in mm */ + offset: number; + track: string; +}; export type LightModeEffortCurves = { is_electric: boolean; }; @@ -3292,11 +3315,6 @@ export type PathfindingResultSuccess = { /** Path description as track ranges */ track_section_ranges: TrackRange[]; }; -export type TrackOffset = { - /** Offset in mm */ - offset: number; - track: string; -}; export type PathfindingResult = | (PathfindingResultSuccess & { status: 'success';