use std::path::Path;
use nom::{
IResult, Parser,
branch::alt,
bytes::{complete::tag, streaming::take_until},
character::{
char,
complete::{multispace0, multispace1, space1},
},
combinator::{map, opt},
number::complete::double,
sequence::{delimited, preceded},
};
use rustc_hash::{FxHashMap, FxHashSet};
use crate::{
JourneyId, Version,
error::{HResult, HrdfError},
models::{CoordinateSystem, Coordinates, JourneyPlatform, Model, Platform},
parsing::{
error::{PResult, ParsingError},
helpers::{
i32_from_n_digits_parser, optional_i32_from_n_digits_parser, read_lines,
string_from_n_chars_parser, string_till_eol_parser,
},
},
storage::ResourceStorage,
utils::{AutoIncrement, create_time_from_value},
};
enum PlatformLine {
JourneyPlatform {
stop_id: i32,
journey_id: i32,
administration: String,
index: i32,
time: Option<i32>,
bit_field_id: Option<i32>,
},
Platform {
stop_id: i32,
index: i32,
platform_name: String,
code: Option<String>,
},
Section {
#[allow(unused)]
stop_id: i32,
#[allow(unused)]
index: i32,
#[allow(unused)]
section_data: String,
},
Sloid {
stop_id: i32,
index: i32,
sloid: String,
},
Coord {
stop_id: i32,
index: i32,
x: f64,
y: f64,
#[allow(unused)]
altitude: Option<f64>,
},
}
fn journey_platform_combinator(input: &str) -> IResult<&str, PlatformLine> {
map(
(
i32_from_n_digits_parser(7),
preceded(char(' '), i32_from_n_digits_parser(6)),
preceded(char(' '), string_from_n_chars_parser(6)),
preceded((space1, tag("#")), i32_from_n_digits_parser(7)),
preceded(char(' '), optional_i32_from_n_digits_parser(4)),
preceded(char(' '), optional_i32_from_n_digits_parser(6)),
),
|(stop_id, journey_id, administration, index, time, bit_field_id)| {
PlatformLine::JourneyPlatform {
stop_id,
journey_id,
administration,
index,
time,
bit_field_id,
}
},
)
.parse(input)
}
fn platform_combinator(input: &str) -> IResult<&str, PlatformLine> {
map(
(
i32_from_n_digits_parser(7),
preceded(tag(" #"), i32_from_n_digits_parser(7)),
preceded(tag(" G "), delimited(tag("'"), take_until("'"), tag("'"))),
preceded(
opt(tag(" A ")),
opt(delimited(tag("'"), take_until("'"), tag("'"))),
),
),
|(stop_id, index, platform_name, code)| PlatformLine::Platform {
stop_id,
index,
platform_name: platform_name.to_string(),
code: code.map(String::from),
},
)
.parse(input)
}
fn section_combinator(input: &str) -> IResult<&str, PlatformLine> {
map(
(
i32_from_n_digits_parser(7),
preceded(tag(" #"), i32_from_n_digits_parser(7)),
preceded(tag(" A "), string_till_eol_parser),
),
|(stop_id, index, section_data)| PlatformLine::Section {
stop_id,
index,
section_data,
},
)
.parse(input)
}
fn coord_combinator(input: &str) -> IResult<&str, PlatformLine> {
map(
(
i32_from_n_digits_parser(7),
preceded(tag(" #"), i32_from_n_digits_parser(7)),
preceded(
alt((tag(" k"), tag(" K"))),
(
preceded(multispace0, double),
preceded(multispace1, double),
opt(preceded(multispace1, double)),
),
),
),
|(stop_id, index, (x, y, altitude))| PlatformLine::Coord {
stop_id,
index,
x,
y,
altitude,
},
)
.parse(input)
}
fn sloid_combinator(input: &str) -> IResult<&str, PlatformLine> {
map(
(
i32_from_n_digits_parser(7),
preceded(tag(" #"), i32_from_n_digits_parser(7)),
alt((
preceded(tag(" g A "), string_till_eol_parser),
preceded(tag(" I A "), string_till_eol_parser),
)),
),
|(stop_id, index, sloid)| PlatformLine::Sloid {
stop_id,
index,
sloid,
},
)
.parse(input)
}
fn parse_line(
line: &str,
platforms: &mut FxHashMap<i32, Platform>,
journey_platform: &mut FxHashMap<(i32, i32), JourneyPlatform>,
platforms_pk_type_converter: &mut FxHashMap<(i32, i32), i32>,
journeys_pk_type_converter: &FxHashSet<JourneyId>,
auto_increment: &AutoIncrement,
coordinate_system: CoordinateSystem,
) -> PResult<()> {
let (_, platform_row) = alt((
journey_platform_combinator,
platform_combinator,
section_combinator,
sloid_combinator,
coord_combinator,
))
.parse(line)?;
match platform_row {
PlatformLine::JourneyPlatform {
stop_id,
journey_id,
administration,
index,
time,
bit_field_id,
} => {
let key = (journey_id, administration.clone());
let _journey_id = journeys_pk_type_converter.get(&key).ok_or_else(|| {
ParsingError::UnknownId(format!(
"Journey Legacy Id (journey_id, administration): ({journey_id}, {administration})"
))
})?;
if !platforms_pk_type_converter.is_empty() {
let platform_id = *platforms_pk_type_converter
.get(&(stop_id, index))
.ok_or_else(|| {
ParsingError::UnknownId(format!(
"Legacy Platform Id (stop_id, index): ({stop_id}, {index})"
))
})?;
let time = time.map(|x| create_time_from_value(x as u32)).transpose()?;
let jp_instance = JourneyPlatform::new(
journey_id,
administration,
platform_id,
time,
bit_field_id,
);
journey_platform.insert(jp_instance.id(), jp_instance);
}
}
PlatformLine::Section {
stop_id: _,
index: _,
section_data: _,
} => {
}
PlatformLine::Platform {
stop_id,
index,
platform_name,
code,
} => {
let id = auto_increment.next();
let id = platforms_pk_type_converter
.entry((stop_id, index))
.or_insert(id);
platforms
.entry(*id)
.or_insert(Platform::new(*id, platform_name, code, stop_id));
}
PlatformLine::Sloid {
stop_id,
index,
sloid,
} => {
let id = platforms_pk_type_converter
.get(&(stop_id, index))
.ok_or_else(|| {
ParsingError::UnknownId(format!(
"Legacy Platform Id (stop_id, index): ({stop_id}, {index})"
))
})?;
platforms
.get_mut(id)
.ok_or_else(|| ParsingError::UnknownId(format!("Unknown platforms Id: {id}")))?
.set_sloid(sloid);
}
PlatformLine::Coord {
stop_id,
index,
x,
y,
altitude: _,
} => {
let id = platforms_pk_type_converter
.get(&(stop_id, index))
.ok_or_else(|| {
ParsingError::UnknownId(format!(
"Legacy Platform Id (stop_id, index): ({stop_id}, {index})"
))
})?;
let platform = platforms
.get_mut(id)
.ok_or_else(|| ParsingError::UnknownId(format!("Unknown platforms Id: {id}")))?;
match coordinate_system {
c @ CoordinateSystem::LV95 => {
let value = Coordinates::new(c, x, y);
platform.set_lv95_coordinates(value);
}
c @ CoordinateSystem::WGS84 => {
let value = Coordinates::new(c, y, x);
platform.set_wgs84_coordinates(value);
}
}
}
}
Ok(())
}
pub fn parse(
version: Version,
path: &Path,
journeys_pk_type_converter: &FxHashSet<JourneyId>,
) -> HResult<(ResourceStorage<JourneyPlatform>, ResourceStorage<Platform>)> {
let prefix = match version {
Version::V_5_40_41_2_0_7 => Ok("GLEISE"),
Version::V_5_40_41_2_0_4 | Version::V_5_40_41_2_0_5 | Version::V_5_40_41_2_0_6 => {
Ok("GLEIS")
}
_ => Err(HrdfError::SupportedVersion(version)),
}?;
let auto_increment = AutoIncrement::new();
let mut platforms = FxHashMap::default();
let mut platforms_pk_type_converter = FxHashMap::default();
let mut journey_platform = FxHashMap::default();
log::info!("Parsing {prefix}_LV95...");
let file = path.join(format!("{prefix}_LV95"));
let platforms_lv95 = read_lines(&file, 0)?;
platforms_lv95
.into_iter()
.enumerate()
.filter(|(_, line)| !line.trim().is_empty())
.try_for_each(|(line_number, line)| {
parse_line(
&line,
&mut platforms,
&mut journey_platform,
&mut platforms_pk_type_converter,
journeys_pk_type_converter,
&auto_increment,
CoordinateSystem::LV95,
)
.map_err(|e| HrdfError::Parsing {
error: e,
file: String::from(file.to_string_lossy()),
line,
line_number,
})
})?;
log::info!("Parsing {prefix}_WGS...");
let file = path.join(format!("{prefix}_WGS"));
let platforms_wgs84 = read_lines(&file, 0)?;
platforms_wgs84
.into_iter()
.enumerate()
.filter(|(_, line)| !line.trim().is_empty())
.try_for_each(|(line_number, line)| {
parse_line(
&line,
&mut platforms,
&mut journey_platform,
&mut platforms_pk_type_converter,
journeys_pk_type_converter,
&auto_increment,
CoordinateSystem::WGS84,
)
.map_err(|e| HrdfError::Parsing {
error: e,
file: String::from(file.to_string_lossy()),
line,
line_number,
})
})?;
Ok((
ResourceStorage::new(journey_platform),
ResourceStorage::new(platforms),
))
}
#[cfg(test)]
mod tests {
use crate::parsing::tests::get_json_values;
use super::*;
use pretty_assertions::assert_eq;
#[test]
fn test_journey_platform_combinator_basic() {
let input = "8500010 000003 000011 #0000001 053751";
let result = journey_platform_combinator(input);
assert!(result.is_ok());
let (_, platform_line) = result.unwrap();
match platform_line {
PlatformLine::JourneyPlatform {
stop_id,
journey_id,
administration,
index,
time,
bit_field_id,
} => {
assert_eq!(stop_id, 8500010);
assert_eq!(journey_id, 3);
assert_eq!(administration, "000011");
assert_eq!(index, 1);
assert_eq!(time, None);
assert_eq!(bit_field_id, Some(53751));
}
_ => panic!("Expected JourneyPlatform variant"),
}
}
#[test]
fn test_journey_platform_combinator_with_time() {
let input = "8014331 005338 8006C5 #0000003 0025 049496";
let result = journey_platform_combinator(input);
assert!(result.is_ok());
let (_, platform_line) = result.unwrap();
match platform_line {
PlatformLine::JourneyPlatform {
stop_id,
journey_id,
administration,
index,
time,
bit_field_id,
} => {
assert_eq!(stop_id, 8014331);
assert_eq!(journey_id, 5338);
assert_eq!(administration, "8006C5");
assert_eq!(index, 3);
assert_eq!(time, Some(25));
assert_eq!(bit_field_id, Some(49496));
}
_ => panic!("Expected JourneyPlatform variant"),
}
}
#[test]
fn test_journey_platform_combinator_spaces_for_optional_fields() {
let input = "8500010 000003 000011 #0000002 000000";
let result = journey_platform_combinator(input);
assert!(result.is_ok());
let (_, platform_line) = result.unwrap();
match platform_line {
PlatformLine::JourneyPlatform {
stop_id,
journey_id,
administration,
index,
time,
bit_field_id,
} => {
assert_eq!(stop_id, 8500010);
assert_eq!(journey_id, 3);
assert_eq!(administration, "000011");
assert_eq!(index, 2);
assert_eq!(time, None);
assert_eq!(bit_field_id, Some(0));
}
_ => panic!("Expected JourneyPlatform variant"),
}
}
#[test]
fn test_platform_combinator_basic() {
let input = "8500010 #0000004 G '9'";
let result = platform_combinator(input);
assert!(result.is_ok());
let (_, platform_line) = result.unwrap();
match platform_line {
PlatformLine::Platform {
stop_id,
index,
platform_name,
code,
} => {
assert_eq!(stop_id, 8500010);
assert_eq!(index, 4);
assert_eq!(platform_name, "9");
assert_eq!(code, None);
}
_ => panic!("Expected Platform variant"),
}
}
#[test]
fn test_platform_combinator_double_digit() {
let input = "8500010 #0000001 G '11'";
let result = platform_combinator(input);
assert!(result.is_ok());
let (_, platform_line) = result.unwrap();
match platform_line {
PlatformLine::Platform {
stop_id,
index,
platform_name,
code,
} => {
assert_eq!(stop_id, 8500010);
assert_eq!(index, 1);
assert_eq!(platform_name, "11");
assert_eq!(code, None);
}
_ => panic!("Expected Platform variant"),
}
}
#[test]
fn test_platform_combinator_with_section() {
let input = "8500207 #0000001 G '1' A 'AB'";
let result = platform_combinator(input);
assert!(result.is_ok());
let (_, platform_line) = result.unwrap();
match platform_line {
PlatformLine::Platform {
stop_id,
index,
platform_name,
code,
} => {
assert_eq!(stop_id, 8500207);
assert_eq!(index, 1);
assert_eq!(platform_name, "1");
assert_eq!(code, Some("AB".to_string()));
}
_ => panic!("Expected Platform variant"),
}
}
#[test]
fn test_platform_combinator_empty_name() {
let input = "8574200 #0000003 G ''";
let result = platform_combinator(input);
assert!(result.is_ok());
let (_, platform_line) = result.unwrap();
match platform_line {
PlatformLine::Platform {
stop_id,
index,
platform_name,
code,
} => {
assert_eq!(stop_id, 8574200);
assert_eq!(index, 3);
assert_eq!(platform_name, "");
assert_eq!(code, None);
}
_ => panic!("Expected Platform variant"),
}
}
#[test]
fn test_section_combinator() {
let input = "8500207 #0000001 A 'AB'";
let result = section_combinator(input);
assert!(result.is_ok());
let (_, platform_line) = result.unwrap();
match platform_line {
PlatformLine::Section {
stop_id,
index,
section_data,
} => {
assert_eq!(stop_id, 8500207);
assert_eq!(index, 1);
assert_eq!(section_data, "'AB'");
}
_ => panic!("Expected Section variant"),
}
}
#[test]
fn test_sloid_combinator_lowercase() {
let input = "8574200 #0000003 g A ch:1:sloid:74200:1:3";
let result = sloid_combinator(input);
assert!(result.is_ok());
let (_, platform_line) = result.unwrap();
match platform_line {
PlatformLine::Sloid {
stop_id,
index,
sloid,
} => {
assert_eq!(stop_id, 8574200);
assert_eq!(index, 3);
assert_eq!(sloid, "ch:1:sloid:74200:1:3");
}
_ => panic!("Expected Sloid variant"),
}
}
#[test]
fn test_sloid_combinator_uppercase() {
let input = "8574200 #0000003 I A ch:1:sloid:74200:1:3";
let result = sloid_combinator(input);
assert!(result.is_ok());
let (_, platform_line) = result.unwrap();
match platform_line {
PlatformLine::Sloid {
stop_id,
index,
sloid,
} => {
assert_eq!(stop_id, 8574200);
assert_eq!(index, 3);
assert_eq!(sloid, "ch:1:sloid:74200:1:3");
}
_ => panic!("Expected Sloid variant"),
}
}
#[test]
fn test_coord_combinator_with_altitude() {
let input = "8574200 #0000003 k 2692827 1247287 680";
let result = coord_combinator(input);
assert!(result.is_ok());
let (_, platform_line) = result.unwrap();
match platform_line {
PlatformLine::Coord {
stop_id,
index,
x,
y,
altitude,
} => {
assert_eq!(stop_id, 8574200);
assert_eq!(index, 3);
assert_eq!(x, 2692827.0);
assert_eq!(y, 1247287.0);
assert_eq!(altitude, Some(680.0));
}
_ => panic!("Expected Coord variant"),
}
}
#[test]
fn test_coord_combinator_without_altitude() {
let input = "8574200 #0000003 k 2692827.5 1247287.2";
let result = coord_combinator(input);
assert!(result.is_ok());
let (_, platform_line) = result.unwrap();
match platform_line {
PlatformLine::Coord {
stop_id,
index,
x,
y,
altitude,
} => {
assert_eq!(stop_id, 8574200);
assert_eq!(index, 3);
assert_eq!(x, 2692827.5);
assert_eq!(y, 1247287.2);
assert_eq!(altitude, None);
}
_ => panic!("Expected Coord variant"),
}
}
#[test]
fn test_coord_combinator_uppercase_k() {
let input = "8574200 #0000003 K 2692827 1247287 680";
let result = coord_combinator(input);
assert!(result.is_ok());
let (_, platform_line) = result.unwrap();
match platform_line {
PlatformLine::Coord {
stop_id,
index,
x,
y,
altitude,
} => {
assert_eq!(stop_id, 8574200);
assert_eq!(index, 3);
assert_eq!(x, 2692827.0);
assert_eq!(y, 1247287.0);
assert_eq!(altitude, Some(680.0));
}
_ => panic!("Expected Coord variant"),
}
}
#[test]
fn test_parse_line_platform_creation() {
let mut platforms = FxHashMap::default();
let mut journey_platform = FxHashMap::default();
let mut platforms_pk_type_converter = FxHashMap::default();
let journeys_pk_type_converter = FxHashSet::default();
let auto_increment = AutoIncrement::new();
parse_line(
"8500010 #0000001 G '11'",
&mut platforms,
&mut journey_platform,
&mut platforms_pk_type_converter,
&journeys_pk_type_converter,
&auto_increment,
CoordinateSystem::LV95,
)
.unwrap();
assert_eq!(platforms.len(), 1);
assert_eq!(platforms_pk_type_converter.len(), 1);
let platform = platforms.get(&1).unwrap();
println!("{}", serde_json::to_string(&platform).unwrap());
let reference = r#"
{
"id":1,
"name":"11",
"sectors":null,
"stop_id":8500010,
"sloid":"",
"lv95_coordinates":{"coordinate_system":"LV95","x":0.0,"y":0.0},
"wgs84_coordinates":{"coordinate_system":"LV95","x":0.0,"y":0.0}
}"#;
let (platform, reference) = get_json_values(platform, reference).unwrap();
assert_eq!(platform, reference);
}
#[test]
#[should_panic]
fn test_parse_line_sloid_requires_existing_platform() {
let mut platforms = FxHashMap::default();
let mut journey_platform = FxHashMap::default();
let mut platforms_pk_type_converter = FxHashMap::default();
let journeys_pk_type_converter = FxHashSet::default();
let auto_increment = AutoIncrement::new();
parse_line(
"8574200 #0000003 g A ch:1:sloid:74200:1:3",
&mut platforms,
&mut journey_platform,
&mut platforms_pk_type_converter,
&journeys_pk_type_converter,
&auto_increment,
CoordinateSystem::LV95,
)
.unwrap();
}
#[test]
#[should_panic]
fn test_parse_line_coord_requires_existing_platform() {
let mut platforms = FxHashMap::default();
let mut journey_platform = FxHashMap::default();
let mut platforms_pk_type_converter = FxHashMap::default();
let journeys_pk_type_converter = FxHashSet::default();
let auto_increment = AutoIncrement::new();
parse_line(
"8574200 #0000003 k 2692827 1247287 680",
&mut platforms,
&mut journey_platform,
&mut platforms_pk_type_converter,
&journeys_pk_type_converter,
&auto_increment,
CoordinateSystem::LV95,
)
.unwrap();
}
#[test]
fn test_parse_line_complete_platform_sequence() {
let mut platforms = FxHashMap::default();
let mut journey_platform = FxHashMap::default();
let mut platforms_pk_type_converter = FxHashMap::default();
let journeys_pk_type_converter = FxHashSet::default();
let auto_increment = AutoIncrement::new();
parse_line(
"8574200 #0000003 G '5'",
&mut platforms,
&mut journey_platform,
&mut platforms_pk_type_converter,
&journeys_pk_type_converter,
&auto_increment,
CoordinateSystem::LV95,
)
.unwrap();
parse_line(
"8574200 #0000003 g A ch:1:sloid:74200:1:3",
&mut platforms,
&mut journey_platform,
&mut platforms_pk_type_converter,
&journeys_pk_type_converter,
&auto_increment,
CoordinateSystem::LV95,
)
.unwrap();
parse_line(
"8574200 #0000003 k 2692827 1247287 680",
&mut platforms,
&mut journey_platform,
&mut platforms_pk_type_converter,
&journeys_pk_type_converter,
&auto_increment,
CoordinateSystem::LV95,
)
.unwrap();
assert_eq!(platforms.len(), 1);
let platform_id = *platforms_pk_type_converter.get(&(8574200, 3)).unwrap();
assert_eq!(platform_id, 1);
let platform = platforms.get(&platform_id).unwrap();
assert_eq!(platform.id(), platform_id);
println!("{}", serde_json::to_string(&platform).unwrap());
let reference = r#"
{
"id":1,
"name":"5",
"sectors":null,
"stop_id":8574200,
"sloid":"ch:1:sloid:74200:1:3",
"lv95_coordinates":{"coordinate_system":"LV95","x":2692827.0,"y":1247287.0},
"wgs84_coordinates":{"coordinate_system":"LV95","x":0.0,"y":0.0}
}"#;
let (platform, reference) = get_json_values(platform, reference).unwrap();
assert_eq!(platform, reference);
}
#[test]
fn test_coordinate_system_wgs84_reverses_coordinates() {
let mut platforms = FxHashMap::default();
let mut journey_platform = FxHashMap::default();
let mut platforms_pk_type_converter = FxHashMap::default();
let journeys_pk_type_converter = FxHashSet::default();
let auto_increment = AutoIncrement::new();
parse_line(
"8500010 #0000001 G '1'",
&mut platforms,
&mut journey_platform,
&mut platforms_pk_type_converter,
&journeys_pk_type_converter,
&auto_increment,
CoordinateSystem::WGS84,
)
.unwrap();
parse_line(
"8500010 #0000001 k 47.123 8.456",
&mut platforms,
&mut journey_platform,
&mut platforms_pk_type_converter,
&journeys_pk_type_converter,
&AutoIncrement::new(),
CoordinateSystem::WGS84,
)
.unwrap();
}
#[test]
#[should_panic]
fn test_journey_platform_requires_valid_journey() {
let mut platforms = FxHashMap::default();
let mut journey_platform = FxHashMap::default();
let mut platforms_pk_type_converter = FxHashMap::default();
let journeys_pk_type_converter = FxHashSet::default(); let auto_increment = AutoIncrement::new();
parse_line(
"8500010 000003 000011 #0000001 053751",
&mut platforms,
&mut journey_platform,
&mut platforms_pk_type_converter,
&journeys_pk_type_converter,
&auto_increment,
CoordinateSystem::LV95,
)
.unwrap();
}
}