Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

editoast: infra/{id}/pathfinding starts in both directions #5904

Merged
merged 5 commits into from
Dec 5, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 1 addition & 6 deletions editoast/openapi.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -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:
Expand Down
7 changes: 1 addition & 6 deletions editoast/openapi_legacy.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -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:
Expand Down
14 changes: 14 additions & 0 deletions editoast/src/map/bounding_box.rs
Original file line number Diff line number Diff line change
Expand Up @@ -54,6 +54,20 @@ impl BoundingBox {
pub fn from_geometry(value: Geometry) -> Result<Self> {
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)]
Expand Down
143 changes: 109 additions & 34 deletions editoast/src/views/infra/pathfinding.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand All @@ -57,7 +49,7 @@ struct PathfindingTrackLocationInput {

#[derive(Debug, Clone, Deserialize)]
struct PathfindingInput {
starting: PathfindingTrackLocationDirInput,
starting: PathfindingTrackLocationInput,
ending: PathfindingTrackLocationInput,
}

Expand Down Expand Up @@ -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<Box<PathfindingStep>>,
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,
}
}

Expand All @@ -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,
}
}

Expand All @@ -177,14 +177,57 @@ fn compute_path(
k: u8,
) -> Vec<PathfindingOutput> {
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
Expand All @@ -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(),
Expand All @@ -201,8 +246,9 @@ fn compute_path(
None,
true,
step.clone(),
cost,
),
into_cost((step.position - input.ending.position).abs()),
cost,
)];
}

Expand All @@ -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![];
Expand Down Expand Up @@ -244,6 +295,7 @@ fn compute_path(
switch.map(|s| (s.obj_id.clone().into(), neighbour_group.clone())),
false,
step.clone(),
cost,
),
cost,
));
Expand All @@ -264,7 +316,8 @@ fn compute_path(
fn build_path_output(path: &Vec<PathfindingStep>, 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()
Expand Down Expand Up @@ -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<DirectionalTrackRange> {
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<Identifier, Identifier> {
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(),
Expand All @@ -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());
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -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' },
Expand All @@ -57,7 +56,6 @@ export const EditRoutePathLeftPanel: FC<{ state: EditRoutePathState }> = ({ stat
const payload = await getCompatibleRoutesPayload(
infraID as number,
entryPoint,
entryPointDirection,
exitPoint,
dispatch
);
Expand Down Expand Up @@ -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;
Expand All @@ -108,12 +106,7 @@ export const EditRoutePathLeftPanel: FC<{ state: EditRoutePathState }> = ({ stat
<button
className="btn btn-primary btn-sm mr-2 d-block w-100 text-center"
type="submit"
disabled={
!entryPoint ||
!entryPointDirection ||
!exitPoint ||
state.optionsState.type === 'loading'
}
disabled={!entryPoint || !exitPoint || state.optionsState.type === 'loading'}
>
<FiSearch /> {t('Editor.tools.routes-edition.search-routes')}
</button>
Expand Down Expand Up @@ -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
Expand Down
Loading