|
| 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 | +} |
0 commit comments