diff --git a/editoast/openapi.yaml b/editoast/openapi.yaml index 46bffc39676..bccedab71c2 100644 --- a/editoast/openapi.yaml +++ b/editoast/openapi.yaml @@ -4144,12 +4144,7 @@ paths: ending: $ref: '#/components/schemas/TrackLocation' starting: - allOf: - - $ref: '#/components/schemas/TrackLocation' - - properties: - direction: - $ref: '#/components/schemas/Direction' - type: object + $ref: '#/components/schemas/TrackLocation' type: object description: Starting and ending track location responses: diff --git a/editoast/openapi_legacy.yaml b/editoast/openapi_legacy.yaml index 9e6344ef425..acc8b23cf84 100644 --- a/editoast/openapi_legacy.yaml +++ b/editoast/openapi_legacy.yaml @@ -642,12 +642,7 @@ paths: type: object properties: starting: - allOf: - - $ref: "#/components/schemas/TrackLocation" - - type: object - properties: - direction: - $ref: "#/components/schemas/Direction" + $ref: "#/components/schemas/TrackLocation" ending: $ref: "#/components/schemas/TrackLocation" responses: diff --git a/editoast/src/map/bounding_box.rs b/editoast/src/map/bounding_box.rs index 8bb5cf8e8c3..263c136c9b9 100644 --- a/editoast/src/map/bounding_box.rs +++ b/editoast/src/map/bounding_box.rs @@ -54,6 +54,20 @@ impl BoundingBox { pub fn from_geometry(value: Geometry) -> Result { Self::from_geojson(value.value) } + + /// Computes the length (in meters) of the diagonal + /// It represents the longest distance as the crow flies between two points in the box + pub fn diagonal_length(&self) -> f64 { + let a = osm4routing::Coord { + lon: self.0 .0, + lat: self.0 .1, + }; + let b = osm4routing::Coord { + lon: self.1 .0, + lat: self.1 .1, + }; + a.distance_to(b) + } } #[derive(Debug, Error, EditoastError)] diff --git a/editoast/src/views/infra/pathfinding.rs b/editoast/src/views/infra/pathfinding.rs index 3c6ab4d5d8a..992d07b64d6 100644 --- a/editoast/src/views/infra/pathfinding.rs +++ b/editoast/src/views/infra/pathfinding.rs @@ -40,14 +40,6 @@ enum PathfindingViewErrors { InvalidNumberOfPaths(u8), } -#[derive(Debug, Clone, Deserialize)] -#[serde(deny_unknown_fields)] -struct PathfindingTrackLocationDirInput { - track: Identifier, - position: f64, - direction: Direction, -} - #[derive(Debug, Clone, Deserialize)] #[serde(deny_unknown_fields)] struct PathfindingTrackLocationInput { @@ -57,7 +49,7 @@ struct PathfindingTrackLocationInput { #[derive(Debug, Clone, Deserialize)] struct PathfindingInput { - starting: PathfindingTrackLocationDirInput, + starting: PathfindingTrackLocationInput, ending: PathfindingTrackLocationInput, } @@ -121,19 +113,23 @@ struct PathfindingStep { direction: Direction, switch_direction: Option<(Identifier, Identifier)>, found: bool, + starting_step: bool, #[derivative(Hash = "ignore", PartialEq = "ignore")] previous: Option>, + total_length: u64, } impl PathfindingStep { - fn new_init(track: String, position: f64, direction: Direction) -> Self { + fn new_init(track: String, position: f64) -> Self { Self { track, position, - direction, + direction: Direction::StartToStop, // Ignored for initial node switch_direction: None, found: false, + starting_step: true, previous: None, + total_length: 0, } } @@ -144,14 +140,18 @@ impl PathfindingStep { switch_direction: Option<(Identifier, Identifier)>, found: bool, previous: PathfindingStep, + length: u64, ) -> Self { + let total_length = previous.total_length + length; Self { track, position, direction, switch_direction, found, + starting_step: false, previous: Some(Box::new(previous)), + total_length, } } @@ -177,14 +177,57 @@ fn compute_path( k: u8, ) -> Vec { let start = &input.starting; - let start = PathfindingStep::new_init(start.track.0.clone(), start.position, start.direction); + let start = PathfindingStep::new_init(start.track.0.clone(), start.position); let track_sections = infra_cache.track_sections(); // Transform a length (in m) into a cost (in mm). This provide the Ord implementation for our cost using u64. let into_cost = |length: f64| (length * 100.).round() as u64; let get_length = |track: &String| track_sections[track].unwrap_track_section().length; let success = |step: &PathfindingStep| step.found; + + let starting_track = track_sections[&input.starting.track.0].unwrap_track_section(); + let ending_track = track_sections[&input.ending.track.0].unwrap_track_section(); + let best_distance = starting_track + .bbox_geo + .clone() + .union(&ending_track.bbox_geo) + .diagonal_length(); + // We build an upper bound that is the diagonal of the bounding box covering start and end + // During the path search, we prune any route that is twice that distance + // We set an upper bound of at least 10 km to avoid problems on very short distances + let mut best_distance = into_cost(best_distance.max(10_000.0)); + let successors = |step: &PathfindingStep| { + // We initially don’t know in which direction start searching the path + // So the first step as two successors, at the same track-position, but in opposite directions + if step.starting_step { + return vec![ + ( + PathfindingStep::new( + step.track.clone(), + step.position, + Direction::StartToStop, + None, + false, + step.clone(), + 0, + ), + 0, + ), + ( + PathfindingStep::new( + step.track.clone(), + step.position, + Direction::StopToStart, + None, + false, + step.clone(), + 0, + ), + 0, + ), + ]; + } // The successor is our on ending track if step.track == input.ending.track.0 { // If we aren't in the good direction to reach the ending position, it's a dead end @@ -193,6 +236,8 @@ fn compute_path( { return vec![]; } + let cost = into_cost((step.position - input.ending.position).abs()); + best_distance = best_distance.min(step.total_length + cost); return vec![( PathfindingStep::new( step.track.clone(), @@ -201,8 +246,9 @@ fn compute_path( None, true, step.clone(), + cost, ), - into_cost((step.position - input.ending.position).abs()), + cost, )]; } @@ -213,6 +259,11 @@ fn compute_path( } else { into_cost(step.position) }; + // We search for k-shortest path. However, we want to prune routes that are too long compared to the shortest + // We can’t do best_distance * 3, as initially it is u64::MAX + if (step.total_length + cost) / 3 > best_distance { + return vec![]; + } // Find neighbours let mut successors = vec![]; @@ -244,6 +295,7 @@ fn compute_path( switch.map(|s| (s.obj_id.clone().into(), neighbour_group.clone())), false, step.clone(), + cost, ), cost, )); @@ -264,7 +316,8 @@ fn compute_path( fn build_path_output(path: &Vec, infra_cache: &InfraCache) -> PathfindingOutput { // Fill track ranges let mut track_ranges = Vec::new(); - (0..(path.len() - 2)).for_each(|i| { + // We ignore the first element of path, as it is a virtual step to handle going in both directions + (1..(path.len() - 2)).for_each(|i| { let end = if path[i].direction == Direction::StartToStop { infra_cache.track_sections()[&path[i].track] .unwrap_track_section() @@ -326,20 +379,55 @@ mod tests { use super::compute_path; use crate::infra_cache::tests::create_small_infra_cache; use crate::infra_cache::Graph; + use crate::schema::utils::Identifier; use crate::schema::{Direction, DirectionalTrackRange}; - use crate::views::infra::pathfinding::{ - PathfindingInput, PathfindingTrackLocationDirInput, PathfindingTrackLocationInput, - }; + use crate::views::infra::pathfinding::{PathfindingInput, PathfindingTrackLocationInput}; + + fn expected_path() -> Vec { + vec![ + DirectionalTrackRange::new("A", 30., 500., Direction::StartToStop), + DirectionalTrackRange::new("B", 0., 500., Direction::StartToStop), + DirectionalTrackRange::new("C", 0., 470., Direction::StartToStop), + ] + } + fn expected_switches() -> HashMap { + HashMap::from([ + ("link".into(), "LINK".into()), + ("switch".into(), "A_B1".into()), + ]) + } #[test] fn test_compute_path() { let infra_cache = create_small_infra_cache(); let graph = Graph::load(&infra_cache); let input = PathfindingInput { - starting: PathfindingTrackLocationDirInput { + starting: PathfindingTrackLocationInput { + track: "A".into(), + position: 30.0, + }, + ending: PathfindingTrackLocationInput { + track: "C".into(), + position: 470.0, + }, + }; + let mut paths = compute_path(&input, &infra_cache, &graph, 1); + + assert_eq!(paths.len(), 1); + let path = paths.pop().unwrap(); + assert_eq!(path.track_ranges, expected_path()); + assert_eq!(path.detectors, vec!["D1".into()]); + assert_eq!(path.switches_directions, expected_switches()); + } + + #[test] + fn test_compute_path_opposite_direction() { + let infra_cache = create_small_infra_cache(); + let graph = Graph::load(&infra_cache); + let input = PathfindingInput { + starting: PathfindingTrackLocationInput { track: "A".into(), position: 30.0, - direction: Direction::StartToStop, }, ending: PathfindingTrackLocationInput { track: "C".into(), @@ -350,21 +438,8 @@ mod tests { assert_eq!(paths.len(), 1); let path = paths.pop().unwrap(); - assert_eq!( - path.track_ranges, - vec![ - DirectionalTrackRange::new("A", 30., 500., Direction::StartToStop), - DirectionalTrackRange::new("B", 0., 500., Direction::StartToStop), - DirectionalTrackRange::new("C", 0., 470., Direction::StartToStop), - ] - ); + assert_eq!(path.track_ranges, expected_path()); assert_eq!(path.detectors, vec!["D1".into()]); - assert_eq!( - path.switches_directions, - HashMap::from([ - ("link".into(), "LINK".into()), - ("switch".into(), "A_B1".into()) - ]) - ); + assert_eq!(path.switches_directions, expected_switches()); } } diff --git a/front/src/applications/editor/tools/routeEdition/components/EditRoutePath.tsx b/front/src/applications/editor/tools/routeEdition/components/EditRoutePath.tsx index 6e0fc43ac4b..93e12b18f99 100644 --- a/front/src/applications/editor/tools/routeEdition/components/EditRoutePath.tsx +++ b/front/src/applications/editor/tools/routeEdition/components/EditRoutePath.tsx @@ -42,13 +42,12 @@ export const EditRoutePathLeftPanel: FC<{ state: EditRoutePathState }> = ({ stat const infraID = useSelector(getInfraID); const [isSaving, setIsSaving] = useState(false); const [includeReleaseDetectors, setIncludeReleaseDetectors] = useState(true); - const { entryPoint, exitPoint, entryPointDirection } = state.routeState; + const { entryPoint, exitPoint } = state.routeState; const [postPathfinding] = osrdEditoastApi.endpoints.postInfraByIdPathfinding.useMutation(); const searchCandidates = useCallback(async () => { - if (!entryPoint || !exitPoint || !entryPointDirection || state.optionsState.type === 'loading') - return; + if (!entryPoint || !exitPoint || state.optionsState.type === 'loading') return; setState({ optionsState: { type: 'loading' }, @@ -57,7 +56,6 @@ export const EditRoutePathLeftPanel: FC<{ state: EditRoutePathState }> = ({ stat const payload = await getCompatibleRoutesPayload( infraID as number, entryPoint, - entryPointDirection, exitPoint, dispatch ); @@ -87,7 +85,7 @@ export const EditRoutePathLeftPanel: FC<{ state: EditRoutePathState }> = ({ stat })), }, }); - }, [entryPoint, entryPointDirection, exitPoint, infraID, setState, state.optionsState.type]); + }, [entryPoint, exitPoint, infraID, setState, state.optionsState.type]); const focusedOptionIndex = state.optionsState.type === 'options' ? state.optionsState.focusedOptionIndex : null; @@ -108,12 +106,7 @@ export const EditRoutePathLeftPanel: FC<{ state: EditRoutePathState }> = ({ stat @@ -221,8 +214,8 @@ export const EditRoutePathLeftPanel: FC<{ state: EditRoutePathState }> = ({ stat properties: { id: '', // will be replaced by entityToCreateOperation entry_point: omit(entryPoint, 'position'), + entry_point_direction: candidate.data.track_ranges[0].direction, exit_point: omit(exitPoint, 'position'), - entry_point_direction: entryPointDirection, switches_directions: candidate.data.switches_directions, release_detectors: includeReleaseDetectors ? candidate.data.detectors diff --git a/front/src/applications/editor/tools/routeEdition/components/Endpoints.tsx b/front/src/applications/editor/tools/routeEdition/components/Endpoints.tsx index 758b0ffffe9..57543dc442a 100644 --- a/front/src/applications/editor/tools/routeEdition/components/Endpoints.tsx +++ b/front/src/applications/editor/tools/routeEdition/components/Endpoints.tsx @@ -1,5 +1,4 @@ -import Select from 'react-select'; -import React, { FC, useContext, useMemo } from 'react'; +import React, { FC, useContext } from 'react'; import { BsArrowBarRight, BsBoxArrowInRight } from 'react-icons/bs'; import { HiSwitchVertical } from 'react-icons/hi'; import { FaFlagCheckered } from 'react-icons/fa'; @@ -7,7 +6,7 @@ import { useTranslation } from 'react-i18next'; import { useDispatch, useSelector } from 'react-redux'; import { getInfraID } from 'reducers/osrdconf/selectors'; -import { BufferStopEntity, DetectorEntity, DIRECTIONS, WayPoint, WayPointEntity } from 'types'; +import { BufferStopEntity, DetectorEntity, WayPoint, WayPointEntity } from 'types'; import EditorContext from 'applications/editor/context'; import { getEntity } from 'applications/editor/data/api'; import EntitySumUp from 'applications/editor/components/EntitySumUp'; @@ -20,20 +19,7 @@ export const EditEndpoints: FC<{ state: RouteState; onChange: (newState: RouteSt onChange, }) => { const { t } = useTranslation(); - const { entryPoint, exitPoint, entryPointDirection } = state; - - const options = useMemo( - () => - DIRECTIONS.map((s) => ({ - value: s, - label: t(`Editor.tools.routes-edition.directions.${s}`), - })), - [t] - ); - const option = useMemo( - () => options.find((o) => o.value === entryPointDirection) || options[0], - [options, entryPointDirection] - ); + const { entryPoint, exitPoint } = state; return ( <> @@ -45,22 +31,6 @@ export const EditEndpoints: FC<{ state: RouteState; onChange: (newState: RouteSt wayPoint={entryPoint} onChange={(wayPoint) => onChange({ ...state, entryPoint: wayPoint })} /> - {entryPoint && ( -
- {t('Editor.tools.routes-edition.start_direction')} -