diff --git a/.cargo/config.toml b/.cargo/config.toml index 1b0ab74..85241c0 100644 --- a/.cargo/config.toml +++ b/.cargo/config.toml @@ -7,7 +7,7 @@ rustdocflags = ["--cfg", "zerocopy_derive_union_into_bytes"] linker = "s390x-linux-gnu-gcc" # Use QEMU to run cross-compiled test binaries in CI -# qemu-s390x-static is provided by the qemu-user-static package +# The qemu-user package provides qemu-s390x # The -L path points to the s390x sysroot installed by libc6-*-s390x-cross -runner = "qemu-s390x-static -L /usr/s390x-linux-gnu" +runner = "qemu-s390x -L /usr/s390x-linux-gnu" diff --git a/.github/workflows/rust.yml b/.github/workflows/rust.yml index fc06189..91c7917 100644 --- a/.github/workflows/rust.yml +++ b/.github/workflows/rust.yml @@ -65,7 +65,7 @@ jobs: # .cargo/config.toml uses these packages as the s390x linker and test runner. - name: Install s390x cross toolchain and QEMU - run: sudo apt-get update && sudo apt-get install -y gcc-s390x-linux-gnu g++-s390x-linux-gnu libc6-dev-s390x-cross qemu-user-static + run: sudo apt-get update && sudo apt-get install -y gcc-s390x-linux-gnu g++-s390x-linux-gnu libc6-dev-s390x-cross qemu-user - name: Run tests (big-endian s390x) run: cargo nextest run --no-fail-fast --target s390x-unknown-linux-gnu diff --git a/valhalla-graphtile/fixtures/andorra-transit-tiles/2/000/763/926.gph b/valhalla-graphtile/fixtures/andorra-transit-tiles/2/000/763/926.gph new file mode 100644 index 0000000..c888535 Binary files /dev/null and b/valhalla-graphtile/fixtures/andorra-transit-tiles/2/000/763/926.gph differ diff --git a/valhalla-graphtile/fixtures/andorra-transit-tiles/3/000/763/926.gph b/valhalla-graphtile/fixtures/andorra-transit-tiles/3/000/763/926.gph new file mode 100644 index 0000000..c150e6b Binary files /dev/null and b/valhalla-graphtile/fixtures/andorra-transit-tiles/3/000/763/926.gph differ diff --git a/valhalla-graphtile/src/graph_id.rs b/valhalla-graphtile/src/graph_id.rs index fa765b9..5c6707e 100644 --- a/valhalla-graphtile/src/graph_id.rs +++ b/valhalla-graphtile/src/graph_id.rs @@ -1,4 +1,4 @@ -use crate::tile_hierarchy::{STANDARD_LEVELS, TRANSIT_LEVEL}; +use crate::tile_hierarchy::tile_level; #[cfg(feature = "serde")] use serde::{Serialize, Serializer}; use std::fmt::{Display, Formatter}; @@ -44,8 +44,8 @@ pub enum InvalidGraphIdError { /// # Hierarchy (level + Tile ID) /// /// Valhalla organizes tiles into several levels. -/// For a description of the common ones, see [`STANDARD_LEVELS`] -/// and [`TRANSIT_LEVEL`]. +/// For a description of the common ones, see [`STANDARD_LEVELS`](crate::tile_hierarchy::STANDARD_LEVELS) +/// and [`TRANSIT_LEVEL`](crate::tile_hierarchy::TRANSIT_LEVEL). /// Each level includes progressively more "local" roads. /// This helps improve efficiency. /// @@ -213,16 +213,12 @@ impl GraphId { /// /// # Errors /// - /// This will fail if the tile is invalid for this level of tiling. + /// This will fail if the level is not a standard level or the transit level, + /// or if the tile is invalid for this level of tiling. /// TODO: It seems like we could do this check at tile creation time? pub fn file_path(&self, extension: &str) -> Result { // This is quite ugly and prone to failure; this is a pretty literal C++ translation - let level_number = self.level(); - let level = if level_number == TRANSIT_LEVEL.level { - &*TRANSIT_LEVEL - } else { - &STANDARD_LEVELS[level_number as usize] - }; + let level = tile_level(self.level()).ok_or(InvalidGraphIdError::Level)?; let max_id = level.tiling_system.n_cols * level.tiling_system.n_rows - 1; let tile_id = self.tile_id(); @@ -423,4 +419,16 @@ mod tests { Ok("3/001/000/000.gph".into()) ); } + + #[test] + fn test_graph_id_file_path_unknown_level() { + for level in 4..=MAX_HIERARCHY_LEVEL { + assert_eq!( + GraphId::try_from_components(level, 0, 0) + .unwrap() + .file_path("gph"), + Err(InvalidGraphIdError::Level) + ); + } + } } diff --git a/valhalla-graphtile/src/graph_tile.rs b/valhalla-graphtile/src/graph_tile.rs index 567a8ec..1faccf2 100644 --- a/valhalla-graphtile/src/graph_tile.rs +++ b/valhalla-graphtile/src/graph_tile.rs @@ -42,6 +42,7 @@ use crate::graph_tile::predicted_speeds::{ COEFFICIENT_COUNT, PredictedSpeedCodecError, PredictedSpeeds, }; use crate::spatial::DistanceApproximator; +use crate::tile_hierarchy::tile_level; pub use crate::{ Access, graph_id::{GraphId, InvalidGraphIdError}, @@ -258,8 +259,8 @@ pub trait GraphTile { /// /// # Panics /// - /// This function assumes that the level in the graph tile header is a standard Valhalla level. - /// Non-standard values will cause a panic. + /// Assumes that the level is one of the [`STANDARD_LEVELS`](crate::tile_hierarchy::STANDARD_LEVELS) + /// or the [`TRANSIT_LEVEL`](crate::tile_hierarchy::TRANSIT_LEVEL). /// /// This function also assumes that the `x` and `y` values are within the accepted range /// (specifically, they must each be <= the square root of [`crate::BIN_COUNT`]). @@ -484,9 +485,6 @@ pub struct GraphTileView<'a> { text_memory: &'a [u8], lane_connectivity_memory: &'a [u8], predicted_speeds: Option>, - // TODO: Stop one stops(?) - // TODO: Route one stops(?) - // TODO: Operator one stops(?) } impl GraphTile for GraphTileView<'_> { @@ -651,12 +649,9 @@ impl GraphTile for GraphTileView<'_> { ); let level = self.header.graph_id().level(); - // Find the subdivision count for this level (defaults to 5 if unknown) - let n_subdivisions = crate::tile_hierarchy::STANDARD_LEVELS - .iter() - .find(|lvl| lvl.level == level) - .map(|lvl| lvl.tiling_system.n_subdivisions as usize) - .expect("Only Valhalla standard tile levels are supported"); + let n_subdivisions = tile_level(level) + .map(|lvl| usize::from(lvl.tiling_system.n_subdivisions)) + .expect("A decoded tile always has a known level"); y * n_subdivisions + x } } @@ -673,8 +668,7 @@ impl<'a> TryFrom<&'a [u8]> for GraphTileView<'a> { })?; let level = header.graph_id().level(); - if level > 2 { - // No transit tile support yet (see the end of this function for some more specific TODOs) + if tile_level(level).is_none() { return Err(GraphTileDecodingError::UnsupportedTileLevel(level)); } @@ -875,11 +869,6 @@ impl<'a> TryFrom<&'a [u8]> for GraphTileView<'a> { (None, bytes) }; - // TODO: Transit info - // - Stop one stops(?) - // - Route one stops(?) - // - Operator one stops(?) - if bytes.is_empty() { Ok(Self { header, @@ -980,6 +969,39 @@ static TEST_GRAPH_TILE_WITH_FLOW: LazyLock = LazyLock::new OwnedGraphTileHandle::try_from(bytes).expect("Unable to get tile") }); +/// The tile ID (at levels 2 and 3) of the tiles in the `andorra-transit-tiles` fixture. +#[cfg(test)] +const TEST_TRANSIT_FIXTURE_TILE_ID: u64 = 763_926; + +#[cfg(test)] +fn transit_fixture_dir() -> std::path::PathBuf { + std::path::PathBuf::from(env!("CARGO_MANIFEST_DIR")) + .join("fixtures") + .join("andorra-transit-tiles") +} + +#[cfg(test)] +fn load_transit_fixture_tile(level: u8) -> OwnedGraphTileHandle { + let relative_path = GraphId::try_from_components(level, TEST_TRANSIT_FIXTURE_TILE_ID, 0) + .expect("Invalid graph ID") + .file_path("gph") + .expect("Unable to get relative path"); + let bytes = + std::fs::read(transit_fixture_dir().join(relative_path)).expect("Unable to read file"); + + OwnedGraphTileHandle::try_from(bytes).expect("Unable to get tile") +} + +/// The local tile under [`TEST_TRANSIT_TILE_L3`], from the same build. +#[cfg(test)] +static TEST_TRANSIT_TILE_L2: LazyLock = + LazyLock::new(|| load_transit_fixture_tile(2)); + +/// A transit tile that Valhalla 3.8.2 built from a small GTFS feed. +#[cfg(test)] +static TEST_TRANSIT_TILE_L3: LazyLock = + LazyLock::new(|| load_transit_fixture_tile(3)); + #[cfg(test)] mod tests { use super::*; @@ -987,10 +1009,13 @@ mod tests { use crate::graph_tile::predicted_speeds::BUCKETS_PER_WEEK; use crate::graph_tile::{ GraphTile, TEST_GRAPH_TILE_ID_L0, TEST_GRAPH_TILE_L0, TEST_GRAPH_TILE_L2, - TEST_GRAPH_TILE_WITH_FLOW, + TEST_GRAPH_TILE_WITH_FLOW, TEST_TRANSIT_TILE_L2, TEST_TRANSIT_TILE_L3, }; + use crate::{AsCowStr, RoadUse}; use enumset::{EnumSet, enum_set}; use rand::{Rng, rng}; + use std::borrow::Cow; + use walkdir::WalkDir; #[test] fn test_get_opp_edge_index() { @@ -1406,4 +1431,318 @@ mod tests { ); } } + + /// Values of the `node_type` field for transit nodes. + const TRANSIT_EGRESS_NODE_TYPE: u8 = 4; + const TRANSIT_STATION_NODE_TYPE: u8 = 5; + const TRANSIT_PLATFORM_NODE_TYPE: u8 = 6; + + fn text_at<'a>(tile_view: &'a GraphTileView<'_>, offset: u32) -> Cow<'a, str> { + tile_view.text_memory[offset as usize..].as_cow_str() + } + + #[test] + fn test_decode_all_transit_fixture_tiles() { + let transit_level_dir = transit_fixture_dir().join("3"); + let mut tile_count = 0; + for entry in WalkDir::new(transit_level_dir) { + let entry = entry.expect("Unable to read fixture directory"); + if entry.path().extension().is_none_or(|ext| ext != "gph") { + continue; + } + + let bytes = std::fs::read(entry.path()).expect("Unable to read file"); + let tile_view = GraphTileView::try_from(bytes.as_slice()) + .unwrap_or_else(|e| panic!("Unable to decode {}: {e}", entry.path().display())); + let header = tile_view.header(); + assert_eq!(header.graph_id().level(), 3); + assert_eq!( + tile_view.transit_departures.len(), + header.departure_count() as usize + ); + assert_eq!(tile_view.transit_stops.len(), header.stop_count() as usize); + assert_eq!( + tile_view.transit_routes.len(), + header.route_count() as usize + ); + assert_eq!( + tile_view.transit_schedules.len(), + header.schedule_count() as usize + ); + assert_eq!( + tile_view.transit_transfers.len(), + header.transfer_count() as usize + ); + tile_count += 1; + } + + assert_eq!(tile_count, 1, "Expected one transit tile in the fixture"); + } + + #[test] + fn test_unsupported_tile_level() { + let mut bytes = TEST_TRANSIT_TILE_L3.borrow_owner().clone(); + // The level is in the 3 low bits of the graph ID at the start of the header. + bytes[0] = (bytes[0] & !0b111) | 4; + + assert!(matches!( + GraphTileView::try_from(bytes.as_slice()), + Err(GraphTileDecodingError::UnsupportedTileLevel(4)) + )); + } + + #[test] + fn test_transit_stops() { + let tile = &*TEST_TRANSIT_TILE_L3; + let tile_view = tile.borrow_dependent(); + let sw = tile_view.header().sw_corner(); + + // Valhalla makes a station node and an egress node for each GTFS stop + // that has no parent station. + assert_eq!(tile_view.nodes().len(), 12); + assert_eq!(tile_view.transit_stops.len(), 12); + + let mut platforms = Vec::new(); + for node in tile_view.nodes() { + let stop = &tile_view.transit_stops[node.transition_index() as usize]; + let name = text_at(tile_view, stop.name_offset()); + let one_stop_id = text_at(tile_view, stop.one_stop_offset()); + match node.node_type() { + TRANSIT_PLATFORM_NODE_TYPE => { + assert!(!stop.generated()); + let coordinate = node.coordinate(sw); + platforms.push((one_stop_id, name, coordinate.y, coordinate.x)); + } + TRANSIT_STATION_NODE_TYPE => { + assert!(stop.generated()); + assert!(one_stop_id.ends_with("_transit_station")); + } + TRANSIT_EGRESS_NODE_TYPE => { + assert!(stop.generated()); + assert!(one_stop_id.ends_with("_transit_egress")); + } + other => panic!("Unexpected node type {other} in a transit tile"), + } + } + platforms.sort_by(|a, b| a.0.cmp(&b.0)); + + // The stops in the GTFS feed that built the fixture + let expected = [ + ("andorra-test_S1", "Plaça U", 42.50655, 1.52100), + ("andorra-test_S2", "Plaça Dos", 42.50655, 1.52300), + ("andorra-test_S3", "Plaça Tres", 42.50655, 1.52500), + ("andorra-test_S4", "Plaça Quatre", 42.50725, 1.52605), + ]; + assert_eq!(platforms.len(), expected.len()); + for ((one_stop_id, name, lat, lon), (exp_id, exp_name, exp_lat, exp_lon)) in + platforms.iter().zip(expected) + { + assert_eq!(one_stop_id, exp_id); + assert_eq!(name, exp_name); + // Node coordinates have 7 decimal digits, but f32 loses some of them at these values. + assert!( + (lat - exp_lat).abs() < 1e-5, + "{name}: lat {lat} != {exp_lat}" + ); + assert!( + (lon - exp_lon).abs() < 1e-5, + "{name}: lon {lon} != {exp_lon}" + ); + } + + // insta internally does a fork operation, which is not supported under Miri + if !cfg!(miri) { + insta::assert_debug_snapshot!(tile_view.transit_stops); + } + } + + #[test] + fn test_transit_routes() { + let tile = &*TEST_TRANSIT_TILE_L3; + let tile_view = tile.borrow_dependent(); + + // The route and the agency in the GTFS feed that built the fixture + let [route] = tile_view.transit_routes else { + panic!("Expected one route in the transit tile") + }; + assert_eq!(text_at(tile_view, route.short_name_offset()), "L1"); + assert_eq!( + text_at(tile_view, route.long_name_offset()), + "Línia de prova" + ); + assert_eq!( + text_at(tile_view, route.description_offset()), + "Test route over the synthetic grid" + ); + assert_eq!( + text_at(tile_view, route.one_stop_offset()), + "andorra-test_L1" + ); + assert_eq!( + text_at(tile_view, route.operator_one_stop_offset()), + "andorra-test_VTA" + ); + assert_eq!( + text_at(tile_view, route.operator_name_offset()), + "Valinor Test Agency" + ); + assert_eq!( + text_at(tile_view, route.operator_website_offset()), + "https://example.com/valinor-test-agency" + ); + // Bus + assert_eq!(route.route_type(), 3); + assert_eq!(route.route_color(), 0x00_55_A4); + assert_eq!(route.route_text_color(), 0xFF_FF_FF); + + // insta internally does a fork operation, which is not supported under Miri + if !cfg!(miri) { + insta::assert_debug_snapshot!(tile_view.transit_routes); + } + } + + #[test] + fn test_transit_departures() { + let tile = &*TEST_TRANSIT_TILE_L3; + let tile_view = tile.borrow_dependent(); + + let mut departures: Vec<_> = tile_view + .transit_departures + .iter() + .map(|departure| { + assert_eq!(departure.route_index(), 0); + assert!( + usize::from(departure.schedule_index()) < tile_view.transit_schedules.len() + ); + ( + departure.departure_time(), + departure.elapsed_time(), + departure.departure_type(), + departure.end_time(), + departure.frequency(), + text_at(tile_view, departure.headsign_offset()), + departure.wheelchair_accessible(), + departure.bicycle_accessible(), + ) + }) + .collect(); + departures.sort_by_key(|departure| departure.0); + + // The trips in the GTFS feed that built the fixture. + // The last stop of each trip has no departure. + let fixed = |time, headsign, bicycle_accessible| { + let headsign = Cow::from(headsign); + (time, 120, 0, None, None, headsign, true, bicycle_accessible) + }; + // Each stop of a frequency trip has the end time of the frequency entry. + let frequency = |time| { + let headsign = Cow::from("Plaça Quatre"); + ( + time, + 120, + 1, + Some(12 * 3600), + Some(900), + headsign, + false, + true, + ) + }; + let expected = vec![ + fixed(8 * 3600, "Plaça Quatre", true), + fixed(8 * 3600 + 150, "Plaça Quatre", true), + fixed(8 * 3600 + 300, "Plaça Quatre", true), + fixed(8 * 3600 + 1800, "Plaça U", false), + fixed(8 * 3600 + 1920, "Plaça U", false), + fixed(8 * 3600 + 2040, "Plaça U", false), + frequency(9 * 3600), + frequency(9 * 3600 + 120), + frequency(9 * 3600 + 240), + ]; + assert_eq!(departures, expected); + + // insta internally does a fork operation, which is not supported under Miri + if !cfg!(miri) { + insta::assert_debug_snapshot!(tile_view.transit_departures); + } + } + + #[test] + fn test_transit_schedules_and_transfers() { + let tile = &*TEST_TRANSIT_TILE_L3; + let tile_view = tile.borrow_dependent(); + + assert!(!tile_view.transit_schedules.is_empty()); + for schedule in tile_view.transit_schedules { + // Monday to Friday, from the calendar of the GTFS feed + assert_eq!(schedule.days_of_week(), 0b011_1110); + + // The day bits start at the build date, so check the weekly pattern only. + let end_day = u32::from(schedule.end_day()); + assert!((6..64).contains(&end_day)); + let days = schedule.days(); + let is_valid = |day: u32| days & (1 << day) != 0; + assert_eq!((0..7).filter(|&day| is_valid(day)).count(), 5); + for day in 7..=end_day { + assert_eq!(is_valid(day), is_valid(day - 7), "Day {day}"); + } + assert_eq!(days.checked_shr(end_day + 1).unwrap_or(0), 0); + } + + // Valhalla does not write transfers into transit tiles + assert!(tile_view.transit_transfers.is_empty()); + + // insta internally does a fork operation, which is not supported under Miri + if !cfg!(miri) { + insta::assert_debug_snapshot!(tile_view.transit_schedules); + } + } + + #[test] + fn test_transit_tile_bins() { + let tile = &*TEST_TRANSIT_TILE_L3; + let tile_view = tile.borrow_dependent(); + assert_eq!(tile_view.edge_bins, []); + + assert_eq!(tile_view.bin_index_xy(0, 0), 0); + assert_eq!(tile_view.bin_index_xy(1, 2), 11); + assert_eq!(tile_view.bin_index_xy(4, 4), crate::BIN_COUNT - 1); + for bin_index in 0..crate::BIN_COUNT { + assert_eq!(tile_view.edges_in_bin(bin_index), []); + } + } + + #[test] + fn test_local_tile_from_transit_build() { + let local_tile = &*TEST_TRANSIT_TILE_L2; + let transit_tile = &*TEST_TRANSIT_TILE_L3; + assert_eq!(local_tile.header().graph_id().level(), 2); + + // Valhalla connects each of the 4 transit egress nodes + // to both end nodes of the closest road edge, in both directions. + let mut egress_ids: Vec<_> = local_tile + .directed_edges() + .iter() + .filter(|edge| edge.road_use() == RoadUse::TransitConnection) + .map(|edge| { + let end_node = transit_tile + .get_node(edge.end_node_id()) + .expect("A transit connection must end in the transit tile"); + assert_eq!(end_node.node_type(), TRANSIT_EGRESS_NODE_TYPE); + edge.end_node_id() + }) + .collect(); + assert_eq!(egress_ids.len(), 8); + egress_ids.sort_by_key(GraphId::value); + egress_ids.dedup(); + assert_eq!(egress_ids.len(), 4); + + let egress_to_road = transit_tile + .directed_edges() + .iter() + .filter(|edge| edge.road_use() == RoadUse::TransitConnection) + .inspect(|edge| assert!(local_tile.may_contain_id(edge.end_node_id()))) + .count(); + assert_eq!(egress_to_road, 8); + } } diff --git a/valhalla-graphtile/src/graph_tile/transit.rs b/valhalla-graphtile/src/graph_tile/transit.rs index 587c0de..bd2dd32 100644 --- a/valhalla-graphtile/src/graph_tile/transit.rs +++ b/valhalla-graphtile/src/graph_tile/transit.rs @@ -1,47 +1,544 @@ //! # Transit data structures //! -//! TODO: The authors don't actively use transit data structures, so these are just (correctly sized) stubs for now. +//! These records are in transit (level 3) tiles. +//! Text offsets point into the text list of the same tile. +//! Raw enum values (such as the route type) use the numbering in Valhalla's +//! [`graphconstants.h`](https://github.com/valhalla/valhalla/blob/b77d5987ed13562454681fc06a81b39d33d97abf/valhalla/baldr/graphconstants.h). -use zerocopy::{LE, U32, U64}; +use bitfield_struct::bitfield; +use core::fmt; +use zerocopy::{LE, U16, U32, U64}; use zerocopy_derive::{FromBytes, Immutable, IntoBytes, Unaligned}; -#[derive(FromBytes, IntoBytes, Immutable, Unaligned, Debug, Clone)] +const FIXED_DEPARTURE_TYPE: u8 = 0; + +#[bitfield(u64, + repr = U64, + from = bit_twiddling_helpers::conv_u64le::from_inner, + into = bit_twiddling_helpers::conv_u64le::into_inner +)] +#[derive(FromBytes, IntoBytes, Immutable, Unaligned)] +struct DepartureFirstBitfield { + #[bits(20, from = bit_twiddling_helpers::conv_u32le::from_inner, into = bit_twiddling_helpers::conv_u32le::into_inner)] + line_id: U32, + #[bits(12, from = bit_twiddling_helpers::conv_u16le::from_inner, into = bit_twiddling_helpers::conv_u16le::into_inner)] + route_index: U16, + #[bits(32, from = bit_twiddling_helpers::conv_u32le::from_inner, into = bit_twiddling_helpers::conv_u32le::into_inner)] + trip_id: U32, +} + +#[bitfield(u64, + repr = U64, + from = bit_twiddling_helpers::conv_u64le::from_inner, + into = bit_twiddling_helpers::conv_u64le::into_inner +)] +#[derive(FromBytes, IntoBytes, Immutable, Unaligned)] +struct DepartureSecondBitfield { + #[bits(20, from = bit_twiddling_helpers::conv_u32le::from_inner, into = bit_twiddling_helpers::conv_u32le::into_inner)] + block_id: U32, + #[bits(12, from = bit_twiddling_helpers::conv_u16le::from_inner, into = bit_twiddling_helpers::conv_u16le::into_inner)] + schedule_index: U16, + #[bits(24, from = bit_twiddling_helpers::conv_u32le::from_inner, into = bit_twiddling_helpers::conv_u32le::into_inner)] + headsign_offset: U32, + #[bits(2)] + departure_type: u8, + #[bits(1)] + wheelchair_accessible: u8, + #[bits(1)] + bicycle_accessible: u8, + #[bits(4)] + _spare: u8, +} + +/// The departure times of a [`TransitDeparture`]. +/// +/// A fixed departure uses only `departure_time`, +/// and stores its elapsed time in the bits of `end_time`. +#[bitfield(u64, + repr = U64, + from = bit_twiddling_helpers::conv_u64le::from_inner, + into = bit_twiddling_helpers::conv_u64le::into_inner +)] +#[derive(FromBytes, IntoBytes, Immutable, Unaligned)] +struct DepartureTimes { + #[bits(17, from = bit_twiddling_helpers::conv_u32le::from_inner, into = bit_twiddling_helpers::conv_u32le::into_inner)] + departure_time: U32, + #[bits(17, from = bit_twiddling_helpers::conv_u32le::from_inner, into = bit_twiddling_helpers::conv_u32le::into_inner)] + end_time: U32, + #[bits(13, from = bit_twiddling_helpers::conv_u16le::from_inner, into = bit_twiddling_helpers::conv_u16le::into_inner)] + frequency: U16, + #[bits(17, from = bit_twiddling_helpers::conv_u32le::from_inner, into = bit_twiddling_helpers::conv_u32le::into_inner)] + frequency_elapsed_time: U32, +} + +/// A scheduled departure of a transit line from a stop. +/// +/// All times are in seconds. +/// Departure and end times count from midnight local time. +#[derive(FromBytes, IntoBytes, Immutable, Unaligned, Clone)] #[repr(C)] pub struct TransitDeparture { - _bitfield1: U64, - _bitfield2: U64, - _departure_times: U64, + first_bitfield: DepartureFirstBitfield, + second_bitfield: DepartureSecondBitfield, + departure_times: DepartureTimes, } +impl TransitDeparture { + /// Gets the line ID. + /// + /// A line is a unique pair of a route and a destination stop. + #[inline] + pub const fn line_id(&self) -> u32 { + self.first_bitfield.line_id().get() + } + + /// Gets the index of the route in the [`TransitRoute`] list of the tile. + #[inline] + pub const fn route_index(&self) -> u16 { + self.first_bitfield.route_index().get() + } + + /// Gets the internal trip ID. + #[inline] + pub const fn trip_id(&self) -> u32 { + self.first_bitfield.trip_id().get() + } + + /// Gets the block ID. + #[inline] + pub const fn block_id(&self) -> u32 { + self.second_bitfield.block_id().get() + } + + /// Gets the index of the [`TransitSchedule`] in the tile + /// that tells on which days this departure is valid. + #[inline] + pub const fn schedule_index(&self) -> u16 { + self.second_bitfield.schedule_index().get() + } + + /// Gets the offset of the headsign in the text list of the tile. + #[inline] + pub const fn headsign_offset(&self) -> u32 { + self.second_bitfield.headsign_offset().get() + } + + /// Gets the raw departure type (0 = fixed, 1 = frequency). + /// + /// A fixed departure leaves at one time. + /// A frequency departure leaves at an interval from the departure time to the end time. + #[inline] + pub const fn departure_type(&self) -> u8 { + self.second_bitfield.departure_type() + } + + /// Can a wheelchair use this departure? + #[inline] + pub const fn wheelchair_accessible(&self) -> bool { + self.second_bitfield.wheelchair_accessible() != 0 + } + + /// Can a passenger take a bicycle on this departure? + #[inline] + pub const fn bicycle_accessible(&self) -> bool { + self.second_bitfield.bicycle_accessible() != 0 + } + + /// Gets the departure time. + /// + /// For a frequency departure, this is the time of the first departure. + #[inline] + pub const fn departure_time(&self) -> u32 { + self.departure_times.departure_time().get() + } + + /// Gets the time from the departure to the arrival at the next stop. + #[inline] + pub const fn elapsed_time(&self) -> u32 { + if self.departure_type() == FIXED_DEPARTURE_TYPE { + self.departure_times.end_time().get() + } else { + self.departure_times.frequency_elapsed_time().get() + } + } + + /// Gets the time of the last departure. + /// + /// Returns `None` for a fixed departure. + #[inline] + pub const fn end_time(&self) -> Option { + if self.departure_type() == FIXED_DEPARTURE_TYPE { + None + } else { + Some(self.departure_times.end_time().get()) + } + } + + /// Gets the interval between departures. + /// + /// Returns `None` for a fixed departure. + #[inline] + pub const fn frequency(&self) -> Option { + if self.departure_type() == FIXED_DEPARTURE_TYPE { + None + } else { + Some(self.departure_times.frequency().get()) + } + } +} + +// The frequency bits of a fixed departure can hold stale data, +// so the output shows only the fields of the departure type. +impl fmt::Debug for TransitDeparture { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + let mut debug = f.debug_struct("TransitDeparture"); + debug + .field("line_id", &self.line_id()) + .field("route_index", &self.route_index()) + .field("trip_id", &self.trip_id()) + .field("block_id", &self.block_id()) + .field("schedule_index", &self.schedule_index()) + .field("headsign_offset", &self.headsign_offset()) + .field("departure_type", &self.departure_type()) + .field("wheelchair_accessible", &self.wheelchair_accessible()) + .field("bicycle_accessible", &self.bicycle_accessible()) + .field("departure_time", &self.departure_time()) + .field("elapsed_time", &self.elapsed_time()); + if let (Some(end_time), Some(frequency)) = (self.end_time(), self.frequency()) { + debug + .field("end_time", &end_time) + .field("frequency", &frequency); + } + debug.finish() + } +} + +#[bitfield(u64, + repr = U64, + from = bit_twiddling_helpers::conv_u64le::from_inner, + into = bit_twiddling_helpers::conv_u64le::into_inner +)] +#[derive(FromBytes, IntoBytes, Immutable, Unaligned)] +struct StopBitfield { + #[bits(24, from = bit_twiddling_helpers::conv_u32le::from_inner, into = bit_twiddling_helpers::conv_u32le::into_inner)] + one_stop_offset: U32, + #[bits(24, from = bit_twiddling_helpers::conv_u32le::from_inner, into = bit_twiddling_helpers::conv_u32le::into_inner)] + name_offset: U32, + #[bits(1)] + generated: u8, + #[bits(2)] + traversability: u8, + #[bits(13)] + _spare: u16, +} + +/// A transit stop. +/// +/// Each node in a transit tile has one stop. +/// The stop index of a node is in [`NodeInfo::transition_index`](super::NodeInfo::transition_index). #[derive(FromBytes, IntoBytes, Immutable, Unaligned, Debug, Clone)] #[repr(C)] pub struct TransitStop { - _data: U64, + bitfield: StopBitfield, +} + +impl TransitStop { + /// Gets the offset of the stop ID (a Onestop ID) in the text list of the tile. + #[inline] + pub const fn one_stop_offset(&self) -> u32 { + self.bitfield.one_stop_offset().get() + } + + /// Gets the offset of the stop name in the text list of the tile. + #[inline] + pub const fn name_offset(&self) -> u32 { + self.bitfield.name_offset().get() + } + + /// Did the tile builder make this stop (and not the transit feed)? + #[inline] + pub const fn generated(&self) -> bool { + self.bitfield.generated() != 0 + } + + /// Gets the raw traversability value (0 = none, 1 = forward, 2 = backward, 3 = both). + #[inline] + pub const fn traversability(&self) -> u8 { + self.bitfield.traversability() + } +} + +#[bitfield(u64, + repr = U64, + from = bit_twiddling_helpers::conv_u64le::from_inner, + into = bit_twiddling_helpers::conv_u64le::into_inner +)] +#[derive(FromBytes, IntoBytes, Immutable, Unaligned)] +struct RouteFirstBitfield { + #[bits(8)] + route_type: u8, + #[bits(24, from = bit_twiddling_helpers::conv_u32le::from_inner, into = bit_twiddling_helpers::conv_u32le::into_inner)] + one_stop_offset: U32, + #[bits(32)] + _spare: u32, } +/// A pair of text offsets in one 64-bit word. +#[bitfield(u64, + repr = U64, + from = bit_twiddling_helpers::conv_u64le::from_inner, + into = bit_twiddling_helpers::conv_u64le::into_inner +)] +#[derive(FromBytes, IntoBytes, Immutable, Unaligned)] +struct RouteOffsetPair { + #[bits(24, from = bit_twiddling_helpers::conv_u32le::from_inner, into = bit_twiddling_helpers::conv_u32le::into_inner)] + first: U32, + #[bits(24, from = bit_twiddling_helpers::conv_u32le::from_inner, into = bit_twiddling_helpers::conv_u32le::into_inner)] + second: U32, + #[bits(16)] + _spare: u16, +} + +/// A transit route. +/// +/// All offsets point into the text list of the tile. #[derive(FromBytes, IntoBytes, Immutable, Unaligned, Debug, Clone)] #[repr(C)] pub struct TransitRoute { - _route_color: U32, - _route_text_color: U32, + route_color: U32, + route_text_color: U32, + first_bitfield: RouteFirstBitfield, + /// The operator Onestop ID and the operator name. + operator_offsets: RouteOffsetPair, + /// The operator website and the short route name. + website_and_short_name_offsets: RouteOffsetPair, + /// The long route name and the route description. + long_name_and_description_offsets: RouteOffsetPair, +} + +impl TransitRoute { + /// Gets the route color as `0xRRGGBB`. + #[inline] + pub const fn route_color(&self) -> u32 { + self.route_color.get() + } + + /// Gets the route text color as `0xRRGGBB`. + #[inline] + pub const fn route_text_color(&self) -> u32 { + self.route_text_color.get() + } + + /// Gets the raw route type. + /// + /// The value is a Valhalla `TransitType` (0 = tram, 1 = metro, 2 = rail, 3 = bus, + /// 4 = ferry, 5 = cable car, 6 = gondola, 7 = funicular). + #[inline] + pub const fn route_type(&self) -> u8 { + self.first_bitfield.route_type() + } + + /// Gets the offset of the route Onestop ID. + #[inline] + pub const fn one_stop_offset(&self) -> u32 { + self.first_bitfield.one_stop_offset().get() + } + + /// Gets the offset of the Onestop ID of the operator. + #[inline] + pub const fn operator_one_stop_offset(&self) -> u32 { + self.operator_offsets.first().get() + } + + /// Gets the offset of the operator name. + #[inline] + pub const fn operator_name_offset(&self) -> u32 { + self.operator_offsets.second().get() + } - _bitfield1: U64, - _bitfield2: U64, - _bitfield3: U64, - _bitfield4: U64, + /// Gets the offset of the operator website. + #[inline] + pub const fn operator_website_offset(&self) -> u32 { + self.website_and_short_name_offsets.first().get() + } + + /// Gets the offset of the short route name. + #[inline] + pub const fn short_name_offset(&self) -> u32 { + self.website_and_short_name_offsets.second().get() + } + + /// Gets the offset of the long route name. + #[inline] + pub const fn long_name_offset(&self) -> u32 { + self.long_name_and_description_offsets.first().get() + } + + /// Gets the offset of the route description. + #[inline] + pub const fn description_offset(&self) -> u32 { + self.long_name_and_description_offsets.second().get() + } +} + +#[bitfield(u64, + repr = U64, + from = bit_twiddling_helpers::conv_u64le::from_inner, + into = bit_twiddling_helpers::conv_u64le::into_inner +)] +#[derive(FromBytes, IntoBytes, Immutable, Unaligned)] +struct ScheduleBitfield { + #[bits(7)] + days_of_week: u8, + #[bits(6)] + end_day: u8, + #[bits(51)] + _spare: u64, } +/// The days on which a group of departures is valid. #[derive(FromBytes, IntoBytes, Immutable, Unaligned, Debug, Clone)] #[repr(C)] pub struct TransitSchedule { - _days: U64, - _bitfield1: U64, + days: U64, + bitfield: ScheduleBitfield, } +impl TransitSchedule { + /// Gets the bit mask of valid days. + /// + /// Bit `n` is for the day `n` days after the creation date of the tile. + #[inline] + pub const fn days(&self) -> u64 { + self.days.get() + } + + /// Gets the bit mask of valid days of the week. + /// + /// Bit 0 is Sunday and bit 6 is Saturday. + #[inline] + pub const fn days_of_week(&self) -> u8 { + self.bitfield.days_of_week() + } + + /// Gets the last day in [`days`](Self::days) that has data. + #[inline] + pub const fn end_day(&self) -> u8 { + self.bitfield.end_day() + } +} + +#[bitfield(u32, + repr = U32, + from = bit_twiddling_helpers::conv_u32le::from_inner, + into = bit_twiddling_helpers::conv_u32le::into_inner +)] +#[derive(FromBytes, IntoBytes, Immutable, Unaligned)] +struct TransferBitfield { + #[bits(4)] + transfer_type: u8, + #[bits(16, from = bit_twiddling_helpers::conv_u16le::from_inner, into = bit_twiddling_helpers::conv_u16le::into_inner)] + min_time: U16, + #[bits(12)] + _spare: u16, +} + +/// A transfer between two stops. #[derive(FromBytes, IntoBytes, Immutable, Unaligned, Debug, Clone)] #[repr(C)] pub struct TransitTransfer { from_stop_id: U32, to_stop_id: U32, - _bitfield_1: U32, + bitfield: TransferBitfield, +} + +impl TransitTransfer { + /// Gets the internal ID of the stop where the transfer starts. + #[inline] + pub const fn from_stop_id(&self) -> u32 { + self.from_stop_id.get() + } + + /// Gets the internal ID of the stop where the transfer ends. + #[inline] + pub const fn to_stop_id(&self) -> u32 { + self.to_stop_id.get() + } + + /// Gets the raw transfer type (0 = recommended, 1 = timed, 2 = minimum time, + /// 3 = not possible). + #[inline] + pub const fn transfer_type(&self) -> u8 { + self.bitfield.transfer_type() + } + + /// Gets the minimum transfer time in seconds. + #[inline] + pub const fn min_time(&self) -> u16 { + self.bitfield.min_time().get() + } +} + +#[cfg(test)] +mod tests { + use super::*; + use core::mem::size_of; + use zerocopy::FromBytes; + + #[test] + fn test_record_sizes() { + assert_eq!(size_of::(), 24); + assert_eq!(size_of::(), 8); + assert_eq!(size_of::(), 40); + assert_eq!(size_of::(), 16); + assert_eq!(size_of::(), 12); + } + + fn departure_from_words(words: [u64; 3]) -> TransitDeparture { + let bytes: Vec = words.iter().flat_map(|w| w.to_le_bytes()).collect(); + TransitDeparture::read_from_bytes(&bytes).expect("Invalid departure bytes") + } + + #[test] + fn test_fixed_departure() { + let (line_id, route_index, trip_id): (u64, u64, u64) = (7, 11, 123_456); + let first = line_id | (route_index << 20) | (trip_id << 32); + let (block_id, schedule_index, headsign_offset): (u64, u64, u64) = (5, 3, 99); + let second = block_id | (schedule_index << 20) | (headsign_offset << 32) | (0b11 << 58); + // The spare bits (34 to 63) are set to make sure that no accessor reads them. + let (departure_time, elapsed_time): (u64, u64) = (28_800, 120); + let times = departure_time | (elapsed_time << 17) | (u64::MAX << 34); + let departure = departure_from_words([first, second, times]); + + assert_eq!(departure.line_id(), 7); + assert_eq!(departure.route_index(), 11); + assert_eq!(departure.trip_id(), 123_456); + assert_eq!(departure.block_id(), 5); + assert_eq!(departure.schedule_index(), 3); + assert_eq!(departure.headsign_offset(), 99); + assert_eq!(departure.departure_type(), FIXED_DEPARTURE_TYPE); + assert!(departure.wheelchair_accessible()); + assert!(departure.bicycle_accessible()); + assert_eq!(departure.departure_time(), 28_800); + assert_eq!(departure.elapsed_time(), 120); + assert_eq!(departure.end_time(), None); + assert_eq!(departure.frequency(), None); + } + + #[test] + fn test_frequency_departure() { + let second = 1 << 56; + let (departure_time, end_time, frequency, elapsed_time): (u64, u64, u64, u64) = + (32_400, 43_200, 900, 150); + let times = departure_time | (end_time << 17) | (frequency << 34) | (elapsed_time << 47); + let departure = departure_from_words([0, second, times]); + + assert_eq!(departure.departure_type(), 1); + assert!(!departure.wheelchair_accessible()); + assert!(!departure.bicycle_accessible()); + assert_eq!(departure.departure_time(), 32_400); + assert_eq!(departure.elapsed_time(), 150); + assert_eq!(departure.end_time(), Some(43_200)); + assert_eq!(departure.frequency(), Some(900)); + } } diff --git a/valhalla-graphtile/src/snapshots/valhalla_graphtile__graph_tile__tests__transit_departures.snap b/valhalla-graphtile/src/snapshots/valhalla_graphtile__graph_tile__tests__transit_departures.snap new file mode 100644 index 0000000..147c7ad --- /dev/null +++ b/valhalla-graphtile/src/snapshots/valhalla_graphtile__graph_tile__tests__transit_departures.snap @@ -0,0 +1,129 @@ +--- +source: valhalla-graphtile/src/graph_tile.rs +expression: tile_view.transit_departures +--- +[ + TransitDeparture { + line_id: 1, + route_index: 0, + trip_id: 2, + block_id: 0, + schedule_index: 0, + headsign_offset: 311, + departure_type: 0, + wheelchair_accessible: true, + bicycle_accessible: false, + departure_time: 30600, + elapsed_time: 120, + }, + TransitDeparture { + line_id: 2, + route_index: 0, + trip_id: 3, + block_id: 0, + schedule_index: 0, + headsign_offset: 37, + departure_type: 0, + wheelchair_accessible: true, + bicycle_accessible: true, + departure_time: 29100, + elapsed_time: 120, + }, + TransitDeparture { + line_id: 2, + route_index: 0, + trip_id: 1, + block_id: 0, + schedule_index: 0, + headsign_offset: 37, + departure_type: 1, + wheelchair_accessible: false, + bicycle_accessible: true, + departure_time: 32640, + elapsed_time: 120, + end_time: 43200, + frequency: 900, + }, + TransitDeparture { + line_id: 3, + route_index: 0, + trip_id: 2, + block_id: 0, + schedule_index: 0, + headsign_offset: 311, + departure_type: 0, + wheelchair_accessible: true, + bicycle_accessible: false, + departure_time: 30720, + elapsed_time: 120, + }, + TransitDeparture { + line_id: 4, + route_index: 0, + trip_id: 3, + block_id: 0, + schedule_index: 0, + headsign_offset: 37, + departure_type: 0, + wheelchair_accessible: true, + bicycle_accessible: true, + departure_time: 28950, + elapsed_time: 120, + }, + TransitDeparture { + line_id: 4, + route_index: 0, + trip_id: 1, + block_id: 0, + schedule_index: 0, + headsign_offset: 37, + departure_type: 1, + wheelchair_accessible: false, + bicycle_accessible: true, + departure_time: 32520, + elapsed_time: 120, + end_time: 43200, + frequency: 900, + }, + TransitDeparture { + line_id: 5, + route_index: 0, + trip_id: 2, + block_id: 0, + schedule_index: 0, + headsign_offset: 311, + departure_type: 0, + wheelchair_accessible: true, + bicycle_accessible: false, + departure_time: 30840, + elapsed_time: 120, + }, + TransitDeparture { + line_id: 6, + route_index: 0, + trip_id: 3, + block_id: 0, + schedule_index: 0, + headsign_offset: 37, + departure_type: 0, + wheelchair_accessible: true, + bicycle_accessible: true, + departure_time: 28800, + elapsed_time: 120, + }, + TransitDeparture { + line_id: 6, + route_index: 0, + trip_id: 1, + block_id: 0, + schedule_index: 0, + headsign_offset: 37, + departure_type: 1, + wheelchair_accessible: false, + bicycle_accessible: true, + departure_time: 32400, + elapsed_time: 120, + end_time: 43200, + frequency: 900, + }, +] diff --git a/valhalla-graphtile/src/snapshots/valhalla_graphtile__graph_tile__tests__transit_routes.snap b/valhalla-graphtile/src/snapshots/valhalla_graphtile__graph_tile__tests__transit_routes.snap new file mode 100644 index 0000000..2f219cb --- /dev/null +++ b/valhalla-graphtile/src/snapshots/valhalla_graphtile__graph_tile__tests__transit_routes.snap @@ -0,0 +1,44 @@ +--- +source: valhalla-graphtile/src/graph_tile.rs +expression: tile_view.transit_routes +--- +[ + TransitRoute { + route_color: U32( + 21924, + ), + route_text_color: U32( + 16777215, + ), + first_bitfield: RouteFirstBitfield { + route_type: 3, + one_stop_offset: U32( + 368, + ), + }, + operator_offsets: RouteOffsetPair { + first: U32( + 384, + ), + second: U32( + 401, + ), + }, + website_and_short_name_offsets: RouteOffsetPair { + first: U32( + 421, + ), + second: U32( + 461, + ), + }, + long_name_and_description_offsets: RouteOffsetPair { + first: U32( + 464, + ), + second: U32( + 480, + ), + }, + }, +] diff --git a/valhalla-graphtile/src/snapshots/valhalla_graphtile__graph_tile__tests__transit_schedules_and_transfers.snap b/valhalla-graphtile/src/snapshots/valhalla_graphtile__graph_tile__tests__transit_schedules_and_transfers.snap new file mode 100644 index 0000000..d7c79bc --- /dev/null +++ b/valhalla-graphtile/src/snapshots/valhalla_graphtile__graph_tile__tests__transit_schedules_and_transfers.snap @@ -0,0 +1,15 @@ +--- +source: valhalla-graphtile/src/graph_tile.rs +expression: tile_view.transit_schedules +--- +[ + TransitSchedule { + days: U64( + 281421784589072883, + ), + bitfield: ScheduleBitfield { + days_of_week: 62, + end_day: 59, + }, + }, +] diff --git a/valhalla-graphtile/src/snapshots/valhalla_graphtile__graph_tile__tests__transit_stops.snap b/valhalla-graphtile/src/snapshots/valhalla_graphtile__graph_tile__tests__transit_stops.snap new file mode 100644 index 0000000..fbd7910 --- /dev/null +++ b/valhalla-graphtile/src/snapshots/valhalla_graphtile__graph_tile__tests__transit_stops.snap @@ -0,0 +1,150 @@ +--- +source: valhalla-graphtile/src/graph_tile.rs +expression: tile_view.transit_stops +--- +[ + TransitStop { + bitfield: StopBitfield { + one_stop_offset: U32( + 6, + ), + name_offset: U32( + 37, + ), + generated: 1, + traversability: 3, + }, + }, + TransitStop { + bitfield: StopBitfield { + one_stop_offset: U32( + 51, + ), + name_offset: U32( + 37, + ), + generated: 1, + traversability: 0, + }, + }, + TransitStop { + bitfield: StopBitfield { + one_stop_offset: U32( + 83, + ), + name_offset: U32( + 37, + ), + generated: 0, + traversability: 0, + }, + }, + TransitStop { + bitfield: StopBitfield { + one_stop_offset: U32( + 99, + ), + name_offset: U32( + 130, + ), + generated: 1, + traversability: 3, + }, + }, + TransitStop { + bitfield: StopBitfield { + one_stop_offset: U32( + 142, + ), + name_offset: U32( + 130, + ), + generated: 1, + traversability: 0, + }, + }, + TransitStop { + bitfield: StopBitfield { + one_stop_offset: U32( + 174, + ), + name_offset: U32( + 130, + ), + generated: 0, + traversability: 0, + }, + }, + TransitStop { + bitfield: StopBitfield { + one_stop_offset: U32( + 190, + ), + name_offset: U32( + 221, + ), + generated: 1, + traversability: 3, + }, + }, + TransitStop { + bitfield: StopBitfield { + one_stop_offset: U32( + 232, + ), + name_offset: U32( + 221, + ), + generated: 1, + traversability: 0, + }, + }, + TransitStop { + bitfield: StopBitfield { + one_stop_offset: U32( + 264, + ), + name_offset: U32( + 221, + ), + generated: 0, + traversability: 0, + }, + }, + TransitStop { + bitfield: StopBitfield { + one_stop_offset: U32( + 280, + ), + name_offset: U32( + 311, + ), + generated: 1, + traversability: 3, + }, + }, + TransitStop { + bitfield: StopBitfield { + one_stop_offset: U32( + 320, + ), + name_offset: U32( + 311, + ), + generated: 1, + traversability: 0, + }, + }, + TransitStop { + bitfield: StopBitfield { + one_stop_offset: U32( + 352, + ), + name_offset: U32( + 311, + ), + generated: 0, + traversability: 0, + }, + }, +] diff --git a/valhalla-graphtile/src/tile_hierarchy.rs b/valhalla-graphtile/src/tile_hierarchy.rs index be774ca..e4978c0 100644 --- a/valhalla-graphtile/src/tile_hierarchy.rs +++ b/valhalla-graphtile/src/tile_hierarchy.rs @@ -204,6 +204,21 @@ pub static TRANSIT_LEVEL: LazyLock = LazyLock::new(|| TileLevel { ), }); +/// Gets the definition of hierarchy level `level`. +/// +/// Returns `None` if `level` is not one of the [`STANDARD_LEVELS`] or the [`TRANSIT_LEVEL`]. +#[inline] +#[must_use] +pub fn tile_level(level: u8) -> Option<&'static TileLevel> { + if level == TRANSIT_LEVEL.level { + Some(&TRANSIT_LEVEL) + } else { + STANDARD_LEVELS + .iter() + .find(|tile_level| tile_level.level == level) + } +} + #[cfg(test)] mod tests { use super::*; @@ -378,4 +393,18 @@ mod tests { GraphId::try_from_components(1, 361, 0).unwrap() ); } + + #[test] + fn test_tile_level() { + for level in 0..=3 { + let tile_level = tile_level(level).expect("Missing tile level"); + assert_eq!(tile_level.level, level); + } + assert_eq!(tile_level(2).map(|l| l.name), Some("local")); + assert_eq!(tile_level(3).map(|l| l.name), Some("transit")); + + for level in 4..=u8::MAX { + assert!(tile_level(level).is_none(), "Unexpected tile level {level}"); + } + } } diff --git a/valhalla-graphtile/src/tile_provider/directory.rs b/valhalla-graphtile/src/tile_provider/directory.rs index 01212a3..553196e 100644 --- a/valhalla-graphtile/src/tile_provider/directory.rs +++ b/valhalla-graphtile/src/tile_provider/directory.rs @@ -200,10 +200,10 @@ impl GraphTileProvider for DirectoryGraphTileProvider { #[cfg(test)] mod test { use super::DirectoryGraphTileProvider; - use crate::GraphId; use crate::graph_tile::GraphTile; use crate::tile_hierarchy::STANDARD_LEVELS; use crate::tile_provider::GraphTileProvider; + use crate::{GraphId, RoadUse}; use core::num::NonZeroUsize; use rand::{ distr::{Distribution, Uniform}, @@ -228,6 +228,48 @@ mod test { assert_eq!(tile.header().graph_id().value(), graph_id.value()); } + #[test] + fn test_transit_connections_across_levels() { + let base = PathBuf::from(env!("CARGO_MANIFEST_DIR")) + .join("fixtures") + .join("andorra-transit-tiles"); + let provider = DirectoryGraphTileProvider::new(base, NonZeroUsize::new(2).unwrap()); + let transit_tile_id = + GraphId::try_from_components(3, 763_926, 0).expect("Unable to create graph ID"); + let transit_tile = provider + .get_handle_for_tile_containing(transit_tile_id) + .expect("Unable to get transit tile"); + assert_eq!(transit_tile.header().graph_id(), transit_tile_id); + + // Each connection from a transit egress to a road has an opposing edge + // in the local tile that goes back to the transit tile. + let mut connection_count = 0; + for (index, edge) in transit_tile.directed_edges().iter().enumerate() { + if edge.road_use() != RoadUse::TransitConnection { + continue; + } + connection_count += 1; + + let edge_id = transit_tile_id + .with_feature_index(index as u64) + .expect("Invalid graph ID"); + let opp_edge_id = provider + .get_opposing_edge_id(edge_id, transit_tile.borrow_dependent()) + .expect("Unable to get opposing edge"); + assert_eq!(opp_edge_id.level(), 2); + + let local_tile = provider + .get_handle_for_tile_containing(opp_edge_id) + .expect("Unable to get local tile"); + let opp_edge = local_tile + .get_directed_edge(opp_edge_id) + .expect("Unable to get opposing edge"); + assert_eq!(opp_edge.road_use(), RoadUse::TransitConnection); + assert!(transit_tile.may_contain_id(opp_edge.end_node_id())); + } + assert_eq!(connection_count, 8); + } + #[test] fn test_get_opp_edge() { let mut rng = rng(); diff --git a/valhalla-graphtile/src/tile_provider/mod.rs b/valhalla-graphtile/src/tile_provider/mod.rs index d068fd3..46c7acf 100644 --- a/valhalla-graphtile/src/tile_provider/mod.rs +++ b/valhalla-graphtile/src/tile_provider/mod.rs @@ -519,7 +519,7 @@ impl LockTable { mod tests { use crate::GraphId; use crate::graph_tile::GraphTile; - use crate::tile_hierarchy::STANDARD_LEVELS; + use crate::tile_hierarchy::tile_level; use crate::tile_provider::{DirectoryGraphTileProvider, GraphTileProvider}; use geo::{Destination, HasDimensions, Haversine, LineString, Validation, coord, point}; use rand::rngs::ThreadRng; @@ -624,7 +624,7 @@ mod tests { let query_point = if let Ok(node) = tile_view.get_node(edge.end_node_id()) { node.coordinate(sw).into() } else { - let level_info = &STANDARD_LEVELS[tile_view.header().graph_id().level() as usize]; + let level_info = tile_level(tile_view.header().graph_id().level()).expect("Unknown tile level"); let tile_size = level_info.tiling_system.tile_size as f32; let ne = coord! { x: sw.x + tile_size, y: sw.y + tile_size };