use crate::{
model::Model,
netex_france::{
CalendarExporter, CompanyExporter, LineExporter, NetworkExporter, OfferExporter,
StopExporter, TransferExporter,
},
netex_utils::FrameType,
objects::{Date, Line, Network},
Result,
};
use anyhow::anyhow;
use chrono::prelude::*;
use minidom::{Element, Node};
use minidom_writer::ElementWriter;
use proj::Proj;
use relational_types::IdxSet;
use std::{
convert::AsRef,
fmt::{self, Display, Formatter},
fs::{self, File},
iter,
path::Path,
};
use tracing::info;
use typed_index_collection::Idx;
const NETEX_FRANCE_CALENDARS_FILENAME: &str = "calendriers.xml";
const NETEX_FRANCE_TRANSFERS_FILENAME: &str = "correspondances.xml";
const NETEX_FRANCE_LINES_FILENAME: &str = "lignes.xml";
const NETEX_FRANCE_STOPS_FILENAME: &str = "arrets.xml";
pub(in crate::netex_france) enum ObjectType {
AccessibilityAssessment,
DayType,
DayTypeAssignment,
Line,
Network,
Operator,
PassengerStopAssignment,
PointOnRoute,
Quay,
Route,
RoutePoint,
ScheduledStopPoint,
ServiceJourney,
ServiceJourneyPattern,
SiteConnection,
StopPlace,
StopPlaceEntrance,
StopPointInJourneyPattern,
TimetabledPassingTime,
UicOperatingPeriod,
}
impl Display for ObjectType {
fn fmt(&self, f: &mut Formatter) -> std::result::Result<(), fmt::Error> {
use ObjectType::*;
match self {
AccessibilityAssessment => write!(f, "AccessibilityAssessment"),
DayType => write!(f, "DayType"),
DayTypeAssignment => write!(f, "DayTypeAssignment"),
Line => write!(f, "Line"),
Network => write!(f, "Network"),
Operator => write!(f, "Operator"),
PassengerStopAssignment => write!(f, "PassengerStopAssignment"),
PointOnRoute => write!(f, "PointOnRoute"),
Quay => write!(f, "Quay"),
Route => write!(f, "Route"),
RoutePoint => write!(f, "RoutePoint"),
ScheduledStopPoint => write!(f, "ScheduledStopPoint"),
ServiceJourney => write!(f, "ServiceJourney"),
ServiceJourneyPattern => write!(f, "ServiceJourneyPattern"),
SiteConnection => write!(f, "SiteConnection"),
StopPlace => write!(f, "StopPlace"),
StopPlaceEntrance => write!(f, "StopPlaceEntrance"),
StopPointInJourneyPattern => write!(f, "StopPointInJourneyPattern"),
TimetabledPassingTime => write!(f, "TimetabledPassingTime"),
UicOperatingPeriod => write!(f, "UicOperatingPeriod"),
}
}
}
enum VersionType {
Calendars,
Lines,
Schedule,
Stops,
Transfers,
}
impl Display for VersionType {
fn fmt(&self, fmt: &mut Formatter) -> std::result::Result<(), fmt::Error> {
use VersionType::*;
match self {
Calendars => write!(fmt, "CALENDRIER"),
Lines => write!(fmt, "LIGNE"),
Schedule => write!(fmt, "HORAIRE"),
Stops => write!(fmt, "ARRET"),
Transfers => write!(fmt, "RESEAU"),
}
}
}
fn only_alphanumeric(s: &str) -> String {
s.chars().filter(|c| c.is_alphanumeric()).collect()
}
pub struct Exporter<'a> {
model: &'a Model,
participant_ref: String,
_stop_provider_code: String,
timestamp: DateTime<FixedOffset>,
}
impl<'a> Exporter<'a> {
pub fn new(
model: &'a Model,
participant_ref: String,
stop_provider_code: Option<String>,
timestamp: DateTime<FixedOffset>,
) -> Self {
let _stop_provider_code = stop_provider_code.unwrap_or_else(|| String::from("LOC"));
Exporter {
model,
participant_ref,
_stop_provider_code,
timestamp,
}
}
pub fn write<P>(&self, path: P) -> Result<()>
where
P: AsRef<Path>,
{
std::fs::create_dir_all(&path)?;
self.write_lines(&path)?;
self.write_stops(&path)?;
self.write_calendars(&path)?;
if !self.model.transfers.is_empty() {
self.write_transfers(&path)?;
} else {
info!("Skipping '{}'", NETEX_FRANCE_TRANSFERS_FILENAME);
}
self.write_offers(&path)?;
Ok(())
}
pub(in crate::netex_france) fn generate_id(id: &'a str, object_type: ObjectType) -> String {
let id = id.replace(':', "_");
format!("FR:{}:{}:", object_type, id)
}
pub(in crate::netex_france) fn get_coordinates_converter() -> Result<Proj> {
let from = "+proj=longlat +datum=WGS84 +no_defs"; let to = "+proj=lcc +lat_1=49 +lat_2=44 +lat_0=46.5 +lon_0=3 +x_0=700000 +y_0=6600000 +ellps=GRS80 +towgs84=0,0,0,0,0,0,0 +units=m +no_defs"; Proj::new_known_crs(from, to, None)
.map_err(|_| anyhow!("Proj cannot build a converter from '{}' to '{}'", from, to))
}
}
impl Exporter<'_> {
fn wrap_frame(&self, frame: Element, version_type: VersionType) -> Element {
let publication_timestamp = Element::builder("PublicationTimestamp")
.ns("http://www.netex.org.uk/netex/")
.append(self.timestamp.to_rfc3339())
.build();
let participant_ref = Element::builder("ParticipantRef")
.ns("http://www.netex.org.uk/netex/")
.append(self.participant_ref.as_str())
.build();
let data_objects = Element::builder("dataObjects")
.ns("http://www.netex.org.uk/netex/")
.append(frame)
.build();
Element::builder("PublicationDelivery")
.attr("version", format!("1.09:FR-NETEX_{}-2.1-1.0", version_type))
.attr("xmlns:siri", "http://www.siri.org.uk/siri")
.attr("xmlns:core", "http://www.govtalk.gov.uk/core")
.attr("xmlns:gml", "http://www.opengis.net/gml/3.2")
.attr("xmlns:ifopt", "http://www.ifopt.org.uk/ifopt")
.attr("xmlns:xlink", "http://www.w3.org/1999/xlink")
.attr("xmlns", "http://www.netex.org.uk/netex")
.attr("xsi:schemaLocation", "http://www.netex.org.uk/netex")
.attr("xmlns:xsi", "http://www.w3.org/2001/XMLSchema-instance")
.append(publication_timestamp)
.append(participant_ref)
.append(data_objects)
.build()
}
fn generate_frame_id(&self, frame_type: FrameType, id: &str) -> String {
format!("FR:{}:{}:", frame_type, id)
}
fn create_composite_frame<I, T>(id: String, frames: I) -> Element
where
I: IntoIterator<Item = T>,
T: Into<Node>,
{
let frame_list = Element::builder("frames").append_all(frames).build();
Element::builder(FrameType::Composite.to_string())
.attr("id", id)
.attr("version", "any")
.append(frame_list)
.build()
}
pub(crate) fn create_members<I, T>(members: I) -> Element
where
I: IntoIterator<Item = T>,
T: Into<Node>,
{
Element::builder("members").append_all(members).build()
}
fn write_lines<P>(&self, path: P) -> Result<()>
where
P: AsRef<Path>,
{
let filepath = path.as_ref().join(NETEX_FRANCE_LINES_FILENAME);
let file = File::create(&filepath)?;
let network_frames = self.create_networks_frames();
let lines_frame = self.create_lines_frame()?;
let companies_frame = self.create_companies_frame();
let frames = network_frames
.into_iter()
.chain(iter::once(lines_frame))
.chain(iter::once(companies_frame));
let composite_frame_id = self.generate_frame_id(
FrameType::Composite,
&format!("NETEX_{}", VersionType::Lines),
);
let composite_frame = Self::create_composite_frame(composite_frame_id, frames);
let netex = self.wrap_frame(composite_frame, VersionType::Lines);
let mut writer = ElementWriter::pretty(file);
info!("Writing {:?}", &filepath);
writer.write(&netex)?;
Ok(())
}
fn create_networks_frames(&self) -> Vec<Element> {
let network_exporter = NetworkExporter::new(self.model);
let network_elements = network_exporter.export();
let frames = network_elements
.into_iter()
.zip(self.model.networks.values())
.map(|(network_element, network)| {
let service_frame_id = self.generate_frame_id(FrameType::Service, &network.id);
Element::builder(FrameType::Service.to_string())
.attr("id", service_frame_id)
.attr("version", "any")
.append(network_element)
.build()
})
.collect();
frames
}
fn create_lines_frame(&self) -> Result<Element> {
let line_exporter = LineExporter::new(self.model);
let lines = line_exporter.export()?;
let line_list = Element::builder("lines").append_all(lines).build();
let service_frame_id = self.generate_frame_id(FrameType::Service, "lines");
let frame = Element::builder(FrameType::Service.to_string())
.attr("id", service_frame_id)
.attr("version", "any")
.append(line_list)
.build();
Ok(frame)
}
fn create_companies_frame(&self) -> Element {
let company_exporter = CompanyExporter::new(self.model);
let companies = company_exporter.export();
let companies_list = Element::builder("organisations")
.append_all(companies)
.build();
let resource_frame_id = self.generate_frame_id(FrameType::Resource, "operators");
Element::builder(FrameType::Resource.to_string())
.attr("id", resource_frame_id)
.attr("version", "any")
.append(companies_list)
.build()
}
fn write_stops<P>(&self, path: P) -> Result<()>
where
P: AsRef<Path>,
{
let filepath = path.as_ref().join(NETEX_FRANCE_STOPS_FILENAME);
let file = File::create(&filepath)?;
let stop_frame = self.create_stops_frame()?;
let netex = self.wrap_frame(stop_frame, VersionType::Stops);
let mut writer = ElementWriter::pretty(file);
info!("Writing {:?}", &filepath);
writer.write(&netex)?;
Ok(())
}
fn create_stops_frame(&self) -> Result<Element> {
let stop_exporter = StopExporter::new(self.model, &self.participant_ref)?;
let stops = stop_exporter.export()?;
let members = Self::create_members(stops);
let general_frame_id =
self.generate_frame_id(FrameType::General, &format!("NETEX_{}", VersionType::Stops));
let frame = Element::builder(FrameType::General.to_string())
.attr("id", general_frame_id)
.attr("version", "any")
.append(members)
.build();
Ok(frame)
}
fn write_calendars<P>(&self, path: P) -> Result<()>
where
P: AsRef<Path>,
{
let filepath = path.as_ref().join(NETEX_FRANCE_CALENDARS_FILENAME);
let file = File::create(&filepath)?;
let calendars_frame = self.create_calendars_frame()?;
let netex = self.wrap_frame(calendars_frame, VersionType::Calendars);
let mut writer = ElementWriter::pretty(file);
info!("Writing {:?}", &filepath);
writer.write(&netex)?;
Ok(())
}
fn create_calendars_frame(&self) -> Result<Element> {
let calendar_exporter = CalendarExporter::new(self.model);
let calendars = calendar_exporter.export()?;
let valid_between = self.create_valid_between()?;
let members = Self::create_members(calendars);
let general_frame_id = self.generate_frame_id(
FrameType::General,
&format!("NETEX_{}", VersionType::Calendars),
);
let frame = Element::builder(FrameType::General.to_string())
.attr("id", general_frame_id)
.attr("version", "any")
.append(valid_between)
.append(members)
.build();
Ok(frame)
}
fn create_valid_between(&self) -> Result<Element> {
let format_date = |date: Date, hour, minute, second| -> String {
DateTime::<Utc>::from_utc(date.and_hms_opt(hour, minute, second).unwrap(), Utc)
.to_rfc3339()
};
let (start_date, end_date) = self.model.calculate_validity_period()?;
let from_date = Element::builder("FromDate")
.append(Node::Text(format_date(start_date, 0, 0, 0)))
.build();
let to_date = Element::builder("ToDate")
.append(Node::Text(format_date(end_date, 23, 59, 59)))
.build();
let valid_between = Element::builder("ValidBetween")
.append(from_date)
.append(to_date)
.build();
Ok(valid_between)
}
fn write_transfers<P>(&self, path: P) -> Result<()>
where
P: AsRef<Path>,
{
let filepath = path.as_ref().join(NETEX_FRANCE_TRANSFERS_FILENAME);
let file = File::create(&filepath)?;
let transfers_frame = self.create_transfers_frame()?;
let netex = self.wrap_frame(transfers_frame, VersionType::Transfers);
let mut writer = ElementWriter::pretty(file);
info!("Writing {:?}", &filepath);
writer.write(&netex)?;
Ok(())
}
fn create_transfers_frame(&self) -> Result<Element> {
let transfer_exporter = TransferExporter::new(self.model);
let transfers = transfer_exporter.export()?;
let members = Self::create_members(transfers);
let general_frame_id = self.generate_frame_id(
FrameType::General,
&format!("NETEX_{}", VersionType::Transfers),
);
let frame = Element::builder(FrameType::General.to_string())
.attr("id", general_frame_id)
.attr("version", "any")
.append(members)
.build();
Ok(frame)
}
fn write_offers<P>(&self, path: P) -> Result<()>
where
P: AsRef<Path>,
{
for network in self.model.networks.values() {
let network_id_md5 = md5::compute(network.id.as_bytes());
let folder_name = format!(
"reseau_{}_{:x}",
only_alphanumeric(&network.name),
network_id_md5
);
let network_path = path.as_ref().join(folder_name);
fs::create_dir(&network_path)?;
let network_idx = self.model.networks.get_idx(&network.id).unwrap();
self.write_network_offers(&network_path, network_idx)?;
}
Ok(())
}
fn write_network_offers<P>(&self, network_path: P, network_idx: Idx<Network>) -> Result<()>
where
P: AsRef<Path>,
{
let line_indexes: IdxSet<Line> = self.model.get_corresponding_from_idx(network_idx);
let offer_exporter = OfferExporter::new(self.model)?;
for line_idx in line_indexes {
let line = &self.model.lines[line_idx];
let line_id_md5 = md5::compute(line.id.as_bytes());
let line_code = if let Some(line_code) = line.code.as_ref() {
format!("{}_", only_alphanumeric(line_code))
} else {
String::new()
};
let file_name = format!("offre_{}{:x}.xml", line_code, line_id_md5);
let filepath = network_path.as_ref().join(file_name);
let file = File::create(&filepath)?;
let offer_frame = self.create_offer_frame(&offer_exporter, line_idx)?;
let netex = self.wrap_frame(offer_frame, VersionType::Schedule);
let mut writer = ElementWriter::pretty(file);
info!("Writing {:?}", &filepath);
writer.write(&netex)?;
}
Ok(())
}
fn create_offer_frame(
&self,
offer_exporter: &OfferExporter,
line_idx: Idx<Line>,
) -> Result<Element> {
let offer = offer_exporter.export(line_idx)?;
let members = Self::create_members(offer);
let general_frame_id = self.generate_frame_id(
FrameType::General,
&format!("NETEX_{}", VersionType::Schedule),
);
let frame = Element::builder(FrameType::General.to_string())
.attr("id", general_frame_id)
.attr("version", "any")
.append(members)
.build();
Ok(frame)
}
}