Skip to content

Commit c20aaf4

Browse files
authored
Merge pull request #1017 from hove-io/feat/DHUB-2348-waiting-time-by-modes
feat: DHUB-2248 - fn generates_transfers - Add waiting_time_by_modes parameter
2 parents 8572c09 + 36f4551 commit c20aaf4

8 files changed

Lines changed: 358 additions & 15 deletions

File tree

Cargo.toml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
[package]
22
authors = ["Hove <core@hove.com>", "Guillaume Pinot <texitoi@texitoi.eu>"]
33
name = "transit_model"
4-
version = "0.78.6"
4+
version = "0.79.0"
55
license = "AGPL-3.0-only"
66
description = "Transit data management"
77
repository = "https://github.com/hove-io/transit_model"

gtfs2ntfs/src/main.rs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -143,6 +143,7 @@ fn run(opt: Opt) -> Result<()> {
143143
opt.walking_speed,
144144
opt.waiting_time,
145145
None,
146+
None,
146147
)?;
147148
transit_model::Model::new(collections)?
148149
};

ntfs2ntfs/src/main.rs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -102,6 +102,7 @@ fn run(opt: Opt) -> Result<()> {
102102
opt.walking_speed,
103103
opt.waiting_time,
104104
None,
105+
None,
105106
)?;
106107
transit_model::Model::new(collections)?
107108
};

src/lib.rs

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -69,6 +69,7 @@ pub mod ntfs;
6969
pub(crate) mod parser;
7070
#[cfg(feature = "parser")]
7171
pub mod parser;
72+
pub mod physical_modes_utils;
7273
#[doc(hidden)]
7374
pub mod test_utils;
7475
pub mod transfers;
@@ -92,7 +93,7 @@ pub const TRANSFER_MAX_DISTANCE: &str = "300";
9293
pub const TRANSFER_WALKING_SPEED: &str = "0.785";
9394

9495
/// Waiting time at stop in second
95-
pub const TRANSFER_WAITING_TIME: &str = "60";
96+
pub const TRANSFER_WAITING_TIME: &str = "120";
9697

9798
lazy_static::lazy_static! {
9899
/// Current datetime

src/physical_modes_utils.rs

Lines changed: 145 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,145 @@
1+
// Copyright (C) 2017 Hove and/or its affiliates.
2+
//
3+
// This program is free software: you can redistribute it and/or modify it
4+
// under the terms of the GNU Affero General Public License as published by the
5+
// Free Software Foundation, version 3.
6+
7+
// This program is distributed in the hope that it will be useful, but WITHOUT
8+
// ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
9+
// FOR A PARTICULAR PURPOSE. See the GNU Affero General Public License for more
10+
// details.
11+
12+
// You should have received a copy of the GNU Affero General Public License
13+
// along with this program. If not, see <https://www.gnu.org/licenses/>
14+
15+
//! Utilities for physical modes
16+
17+
use crate::{
18+
model::{
19+
Model, AIR_PHYSICAL_MODE, BOAT_PHYSICAL_MODE, BUS_PHYSICAL_MODE,
20+
BUS_RAPID_TRANSIT_PHYSICAL_MODE, COACH_PHYSICAL_MODE, FERRY_PHYSICAL_MODE,
21+
FUNICULAR_PHYSICAL_MODE, LOCAL_TRAIN_PHYSICAL_MODE, LONG_DISTANCE_TRAIN_PHYSICAL_MODE,
22+
METRO_PHYSICAL_MODE, RAIL_SHUTTLE_PHYSICAL_MODE, RAPID_TRANSIT_PHYSICAL_MODE,
23+
SHUTTLE_PHYSICAL_MODE, SUSPENDED_CABLE_CAR_PHYSICAL_MODE, TAXI_PHYSICAL_MODE,
24+
TRAIN_PHYSICAL_MODE, TRAMWAY_PHYSICAL_MODE,
25+
},
26+
objects::{PhysicalMode, StopPoint},
27+
};
28+
use std::collections::HashMap;
29+
use typed_index_collection::Idx;
30+
31+
/// Returns a priority order for a physical mode, used to pick the most relevant mode
32+
/// when a stop point is served by multiple ones.
33+
/// Lower value means higher priority (e.g. Train = 3, Bus = 7).
34+
///
35+
/// Priority order follows the NTFS specification:
36+
/// see https://github.com/hove-io/ntfs-specification/blob/master/ntfs_fr.md#physical_modestxt-requis
37+
pub fn get_physical_mode_order(physical_mode: &PhysicalMode) -> u8 {
38+
match physical_mode.id.as_str() {
39+
AIR_PHYSICAL_MODE => 1,
40+
BOAT_PHYSICAL_MODE | FERRY_PHYSICAL_MODE => 2,
41+
LOCAL_TRAIN_PHYSICAL_MODE
42+
| LONG_DISTANCE_TRAIN_PHYSICAL_MODE
43+
| RAPID_TRANSIT_PHYSICAL_MODE
44+
| RAIL_SHUTTLE_PHYSICAL_MODE
45+
| TRAIN_PHYSICAL_MODE => 3,
46+
METRO_PHYSICAL_MODE => 4,
47+
TRAMWAY_PHYSICAL_MODE => 5,
48+
FUNICULAR_PHYSICAL_MODE | SUSPENDED_CABLE_CAR_PHYSICAL_MODE => 6,
49+
BUS_PHYSICAL_MODE
50+
| BUS_RAPID_TRANSIT_PHYSICAL_MODE
51+
| COACH_PHYSICAL_MODE
52+
| SHUTTLE_PHYSICAL_MODE
53+
| TAXI_PHYSICAL_MODE => 7,
54+
_ => 8,
55+
}
56+
}
57+
58+
/// Builds a map from each `StopPoint` index to its highest priority `PhysicalMode` index.
59+
///
60+
/// When a stop point is served by multiple physical modes, the one with the lowest order
61+
/// value from `get_physical_mode_order` is selected (e.g. Train wins over Bus).
62+
/// In practice these are often similar modes with the same hierarchy level (e.g. Train, LocalTrain, RapidTransit).
63+
/// Stop points with no associated vehicle journey are absent from the returned map.
64+
pub fn build_stop_point_physical_mode_map(
65+
model: &Model,
66+
) -> HashMap<Idx<StopPoint>, Idx<PhysicalMode>> {
67+
model
68+
.stop_points
69+
.iter()
70+
.filter_map(|(stop_point_idx, _)| {
71+
let physical_mode_idx = model
72+
.get_corresponding_from_idx::<StopPoint, PhysicalMode>(stop_point_idx)
73+
.into_iter()
74+
.min_by_key(|&physical_mode_idx| {
75+
get_physical_mode_order(&model.physical_modes[physical_mode_idx])
76+
})?;
77+
Some((stop_point_idx, physical_mode_idx))
78+
})
79+
.collect()
80+
}
81+
82+
#[cfg(test)]
83+
mod tests {
84+
use super::build_stop_point_physical_mode_map;
85+
use crate::physical_modes_utils::{
86+
BUS_RAPID_TRANSIT_PHYSICAL_MODE, RAPID_TRANSIT_PHYSICAL_MODE,
87+
};
88+
use crate::ModelBuilder;
89+
90+
#[test]
91+
fn test_build_stop_point_physical_mode_map_single_mode() {
92+
// Stop point A served only by Bus
93+
let model = ModelBuilder::default()
94+
.vj("vj1", |vj| {
95+
vj.route("route1")
96+
.physical_mode(BUS_RAPID_TRANSIT_PHYSICAL_MODE)
97+
.st("A", "10:00:00")
98+
.st("B", "10:10:00");
99+
})
100+
.build();
101+
102+
let map = build_stop_point_physical_mode_map(&model);
103+
104+
let sp_a_idx = model.stop_points.get_idx("A").unwrap();
105+
let sp_b_idx = model.stop_points.get_idx("B").unwrap();
106+
let bus_idx = model
107+
.physical_modes
108+
.get_idx(BUS_RAPID_TRANSIT_PHYSICAL_MODE)
109+
.unwrap();
110+
111+
assert_eq!(map.get(&sp_a_idx), Some(&bus_idx));
112+
assert_eq!(map.get(&sp_b_idx), Some(&bus_idx));
113+
}
114+
115+
#[test]
116+
fn test_build_stop_point_physical_mode_map_picks_highest_priority() {
117+
// Stop point A is served by both BusRapidTransit and RapidTransit — RapidTransit should win (order 3 < 7)
118+
let model = ModelBuilder::default()
119+
.vj("vj1", |vj| {
120+
vj.route("route1")
121+
.physical_mode(RAPID_TRANSIT_PHYSICAL_MODE)
122+
.st("A", "10:00:00")
123+
.st("B", "10:10:00");
124+
})
125+
.vj("vj2", |vj| {
126+
vj.route("route2")
127+
.physical_mode(BUS_RAPID_TRANSIT_PHYSICAL_MODE)
128+
.st("A", "11:00:00")
129+
.st("B", "11:10:00");
130+
})
131+
.build();
132+
133+
let map = build_stop_point_physical_mode_map(&model);
134+
135+
assert_eq!(model.vehicle_journeys.len(), 2);
136+
137+
let sp_a_idx = model.stop_points.get_idx("A").unwrap();
138+
let rapid_transit_idx = model
139+
.physical_modes
140+
.get_idx(RAPID_TRANSIT_PHYSICAL_MODE)
141+
.unwrap();
142+
143+
assert_eq!(map.get(&sp_a_idx), Some(&rapid_transit_idx));
144+
}
145+
}

src/serde_utils.rs

Lines changed: 52 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -150,6 +150,27 @@ where
150150
})
151151
}
152152

153+
/// deserialize String as non empty String
154+
/// returns an error if empty String
155+
pub fn de_non_empty_string<'de, D>(deserializer: D) -> Result<String, D::Error>
156+
where
157+
D: serde::Deserializer<'de>,
158+
{
159+
use serde::{
160+
de::{Error, Unexpected::Other},
161+
Deserialize,
162+
};
163+
let s = String::deserialize(deserializer)?;
164+
if s.is_empty() {
165+
Err(D::Error::invalid_value(
166+
Other("empty string"),
167+
&"non empty string",
168+
))
169+
} else {
170+
Ok(s)
171+
}
172+
}
173+
153174
/// deserialize String by removing slashes
154175
pub fn de_without_slashes<'de, D>(deserializer: D) -> Result<String, D::Error>
155176
where
@@ -445,4 +466,35 @@ mod tests {
445466
);
446467
}
447468
}
469+
470+
mod serde_non_empty_string {
471+
use super::*;
472+
use pretty_assertions::assert_eq;
473+
use serde::Deserialize;
474+
475+
#[derive(Debug, Deserialize)]
476+
struct WithNonEmptyString {
477+
#[serde(deserialize_with = "de_non_empty_string")]
478+
name: String,
479+
}
480+
481+
#[test]
482+
fn with_string() {
483+
let json = r#"{"name": "baz"}"#;
484+
let object: WithNonEmptyString = serde_json::from_str(json).unwrap();
485+
assert_eq!(object.name, "baz");
486+
}
487+
488+
#[test]
489+
fn with_empty_string() {
490+
let json = r#"{"name": ""}"#;
491+
let err = serde_json::from_str::<WithNonEmptyString>(json).unwrap_err();
492+
let err_str = err.to_string();
493+
assert!(
494+
err_str.contains("invalid value: empty string, expected non empty string"),
495+
"unexpected error message: {err_str}",
496+
err_str = err_str
497+
);
498+
}
499+
}
448500
}

0 commit comments

Comments
 (0)