Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[feature] Export Networks for NeTEx France #524

Merged
merged 1 commit into from
Jan 29, 2020
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
23 changes: 13 additions & 10 deletions src/documentation/ntfs_to_netex_france_specs.md
Original file line number Diff line number Diff line change
Expand Up @@ -247,17 +247,20 @@ Example:
```xml
<?xml version="1.0" encoding="UTF-8"?>
<CompositeFrame
id="FR:CompositeFrame:NETEX-20190709T093214Z:LOC"
version="1.8">
<TypeOfFrameRef ref="FR:TypeOfFrame:NETEX:" />
id="FR:CompositeFrame:NETEX_LIGNE:<stop_provider_code>"
version="any">
<frames>
<!-- Repeat a ServiceFrame node for each Network -->
<ServiceFrame version="any" id="">
<Network />
</ServiceFrame>
<ServiceFrame version="any" id="">
<lines><!-- One node Line for each Line of the dataset--></lines>
</ServiceFrame>
<!-- Repeat a ServiceFrame node for each Network -->
<ServiceFrame
id="FR:ServiceFrame:<network_id>:<stop_provider_code>"
version="any">
<Network />
</ServiceFrame>
<ServiceFrame
id="FR:ServiceFrame:lines:<stop_provider_code>"
version="any">
<lines><!-- One node Line for each Line of the dataset--></lines>
</ServiceFrame>
</frames>
</CompositeFrame>
```
Expand Down
90 changes: 80 additions & 10 deletions src/netex_france/exporter.rs
Original file line number Diff line number Diff line change
Expand Up @@ -13,9 +13,15 @@
// along with this program. If not, see <https://www.gnu.org/licenses/>

//! Exporter for Netex France profile
use crate::{minidom_utils::ElementWriter, model::Model, netex_france::StopExporter, Result};
use crate::{
minidom_utils::ElementWriter,
model::Model,
netex_france::{NetworkExporter, StopExporter},
netex_utils::FrameType,
Result,
};
use chrono::prelude::*;
use minidom::Element;
use minidom::{Element, Node};
use std::{
convert::AsRef,
fmt::{self, Display, Formatter},
Expand All @@ -24,16 +30,19 @@ use std::{
};

const NETEX_FRANCE_STOPS_FILENAME: &str = "arrets.xml";
const NETEX_FRANCE_LINES_FILENAME: &str = "lignes.xml";

enum VersionType {
Stops,
Lines,
}

impl Display for VersionType {
fn fmt(&self, fmt: &mut Formatter) -> std::result::Result<(), fmt::Error> {
use VersionType::*;
match self {
Stops => write!(fmt, "ARRET"),
Lines => write!(fmt, "LIGNE"),
}
}
}
Expand All @@ -42,7 +51,7 @@ impl Display for VersionType {
pub struct Exporter<'a> {
model: &'a Model,
participant_ref: String,
stop_provider_code: Option<String>,
stop_provider_code: String,
timestamp: NaiveDateTime,
}

Expand All @@ -57,6 +66,7 @@ impl<'a> Exporter<'a> {
stop_provider_code: Option<String>,
timestamp: NaiveDateTime,
) -> Self {
let stop_provider_code = stop_provider_code.unwrap_or_else(|| String::from("LOC"));
Exporter {
model,
participant_ref,
Expand All @@ -70,7 +80,8 @@ impl<'a> Exporter<'a> {
where
P: AsRef<Path>,
{
self.write_stops(path)?;
self.write_lines(&path)?;
self.write_stops(&path)?;
Ok(())
}
}
Expand Down Expand Up @@ -110,6 +121,68 @@ impl Exporter<'_> {
Ok(root)
}

fn generate_frame_id(&self, frame_type: FrameType, id: &str) -> String {
format!("FR:{}:{}:{}", frame_type, id, self.stop_provider_code)
}

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("CompositeFrame")
.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 mut file = File::create(filepath)?;
let network_frames = self.create_networks_frames()?;
let composite_frame_id = self.generate_frame_id(
FrameType::Composite,
&format!("NETEX_{}", VersionType::Lines),
);
let composite_frame = Self::create_composite_frame(composite_frame_id, network_frames);
let netex = self.wrap_frame(composite_frame, VersionType::Lines)?;
let writer = ElementWriter::new(netex, true);
writer.write(&mut file)?;
Ok(())
}

// Returns a list of 'ServiceFrame' each containing a 'Network'
fn create_networks_frames(&self) -> Result<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("ServiceFrame")
.attr("id", service_frame_id)
.attr("version", "any")
.append(network_element)
.build()
})
.collect();
Ok(frames)
}

fn write_stops<P>(&self, path: P) -> Result<()>
where
P: AsRef<Path>,
Expand All @@ -125,13 +198,10 @@ impl Exporter<'_> {

// Returns a 'GeneralFrame' containing all 'StopArea' and 'Quay'
fn create_stops_frame(&self) -> Result<Element> {
let stop_point_exporter = StopExporter::new(
&self.model,
&self.participant_ref,
self.stop_provider_code.as_ref(),
)?;
let stop_point_exporter =
StopExporter::new(&self.model, &self.participant_ref, &self.stop_provider_code)?;
let stop_points = stop_point_exporter.export()?;
let members = Element::builder("members").append_all(stop_points).build();
let members = Self::create_members(stop_points);
let frame = Element::builder("GeneralFrame").append(members).build();
Ok(frame)
}
Expand Down
2 changes: 2 additions & 0 deletions src/netex_france/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -16,5 +16,7 @@

mod exporter;
pub use exporter::Exporter;
mod networks;
use networks::NetworkExporter;
mod stops;
use stops::StopExporter;
73 changes: 73 additions & 0 deletions src/netex_france/networks.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,73 @@
// Copyright (C) 2017 Kisio Digital and/or its affiliates.
//
// This program is free software: you can redistribute it and/or modify it
// under the terms of the GNU Affero General Public License as published by the
// Free Software Foundation, version 3.

// This program is distributed in the hope that it will be useful, but WITHOUT
// ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
// FOR A PARTICULAR PURPOSE. See the GNU Affero General Public License for more
// details.

// You should have received a copy of the GNU Affero General Public License
// along with this program. If not, see <https://www.gnu.org/licenses/>

use super::Exporter;
use crate::{
objects::{Line, Network},
Model, Result,
};
use minidom::{Element, Node};

pub struct NetworkExporter<'a> {
model: &'a Model,
}

// Publicly exposed methods
impl<'a> NetworkExporter<'a> {
pub fn new(model: &'a Model) -> Self {
NetworkExporter { model }
}
pub fn export(&self) -> Result<Vec<Element>> {
self.model
.networks
.values()
.map(|network| self.export_network(network))
.collect()
}
}

// Internal methods
impl<'a> NetworkExporter<'a> {
fn export_network(&self, network: &'a Network) -> Result<Element> {
let element_builder = Element::builder("Network")
.attr("id", self.generate_id(network))
.attr("version", "any");
let element_builder = element_builder.append(self.generate_name(network));
let line_ref_elements = self
.model
.lines
.values()
.filter(|line| line.network_id == network.id)
.map(|line| self.generate_line_ref(line));
let element_builder = element_builder.append(Exporter::create_members(line_ref_elements));
Ok(element_builder.build())
}

fn generate_id(&self, network: &'a Network) -> String {
let id = network.id.replace(':', "_");
format!("FR:network:{}:", id)
}

fn generate_name(&self, network: &'a Network) -> Element {
Element::builder("Name")
.append(Node::Text(network.name.to_owned()))
.build()
}

fn generate_line_ref(&self, line: &'a Line) -> Element {
let id = line.id.replace(':', "_");
let line_id = format!("FR:line:{}", id);
Element::builder("LineRef").attr("ref", line_id).build()
}
}
10 changes: 3 additions & 7 deletions src/netex_france/stops.rs
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@ use proj::Proj;
pub struct StopExporter<'a> {
model: &'a Model,
participant_ref: &'a str,
stop_provider_code: Option<&'a String>,
stop_provider_code: &'a str,
converter: Proj,
}

Expand All @@ -29,7 +29,7 @@ impl<'a> StopExporter<'a> {
pub fn new(
model: &'a Model,
participant_ref: &'a str,
stop_provider_code: Option<&'a String>,
stop_provider_code: &'a str,
) -> Result<Self> {
// FIXME: String 'EPSG:4326' is failing at runtime (string below is equivalent but works)
let from = "+proj=longlat +datum=WGS84 +no_defs"; // See https://epsg.io/4326
Expand Down Expand Up @@ -79,13 +79,9 @@ impl<'a> StopExporter<'a> {

fn generate_id(&self, stop_point: &'a StopPoint) -> String {
let id = stop_point.id.replace(':', "_");
let provider_code = self
.stop_provider_code
.cloned()
.unwrap_or_else(|| String::from("LOC"));
// TODO: Find INSEE code from geolocation of the `stop_point`
let insee = "XXXXX";
format!("FR:{}:ZE:{}:{}", insee, id, provider_code)
format!("FR:{}:ZE:{}:{}", insee, id, self.stop_provider_code)
}

fn generate_name(&self, stop_point: &'a StopPoint) -> Element {
Expand Down
29 changes: 18 additions & 11 deletions src/netex_utils/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -23,32 +23,37 @@ use std::{

#[derive(Debug, Eq, Hash, PartialEq)]
pub enum FrameType {
Composite,
Fare,
General,
Resource,
Service,
Fare,
}
pub type Frames<'a> = HashMap<FrameType, Vec<&'a Element>>;

impl Display for FrameType {
fn fmt(&self, f: &mut Formatter) -> std::fmt::Result {
use FrameType::*;
match self {
FrameType::Fare => write!(f, "Fare"),
FrameType::General => write!(f, "General"),
FrameType::Resource => write!(f, "Resource"),
FrameType::Service => write!(f, "Service"),
Composite => write!(f, "CompositeFrame"),
Fare => write!(f, "FareFrame"),
General => write!(f, "GeneralFrame"),
Resource => write!(f, "ResourceFrame"),
Service => write!(f, "ServiceFrame"),
}
}
}

impl FromStr for FrameType {
type Err = Error;
fn from_str(s: &str) -> std::result::Result<Self, Self::Err> {
use FrameType::*;
match s {
"FareFrame" => Ok(FrameType::Fare),
"GeneralFrame" => Ok(FrameType::General),
"ResourceFrame" => Ok(FrameType::Resource),
"ServiceFrame" => Ok(FrameType::Service),
"CompositeFrame" => Ok(Composite),
"FareFrame" => Ok(Fare),
"GeneralFrame" => Ok(General),
"ResourceFrame" => Ok(Resource),
"ServiceFrame" => Ok(Service),
_ => bail!("Failed to convert '{}' into a FrameType", s),
}
}
Expand Down Expand Up @@ -168,14 +173,16 @@ mod tests {
}

#[test]
#[should_panic(expected = "Failed to find a \\'Service\\' frame in the Netex file")]
#[should_panic(expected = "Failed to find a \\'ServiceFrame\\' frame in the Netex file")]
fn no_frame() {
let frames = HashMap::new();
get_only_frame(&frames, FrameType::Service).unwrap();
}

#[test]
#[should_panic(expected = "Failed to find a unique \\'Resource\\' frame in the Netex file")]
#[should_panic(
expected = "Failed to find a unique \\'ResourceFrame\\' frame in the Netex file"
)]
fn multiple_frames() {
let mut frames = HashMap::new();
let frame: Element = r#"<frame xmlns="test" />"#.parse().unwrap();
Expand Down
24 changes: 24 additions & 0 deletions tests/fixtures/netex_france/lignes.xml
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
<?xml version="1.0" encoding="UTF-8"?>
<PublicationDelivery version="1.09:FR-NETEX_LIGNE-2.1-1.0" xmlns="http://www.netex.org.uk/netex" xmlns:core="http://www.govtalk.gov.uk/core" xmlns:gml="http://www.opengis.net/gml/3.2" xmlns:ifopt="http://www.ifopt.org.uk/ifopt" xmlns:siri="http://www.siri.org.uk/siri" xmlns:xlink="http://www.w3.org/1999/xlink" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://www.netex.org.uk/netex">
<PublicationTimestamp>2019-04-03T17:19:00</PublicationTimestamp>
<ParticipantRef>Participant</ParticipantRef>
<dataObjects>
<CompositeFrame id="FR:CompositeFrame:NETEX_LIGNE:ProviderCode" version="any">
<frames>
<ServiceFrame id="FR:ServiceFrame:TGN:ProviderCode" version="any">
<Network id="FR:network:TGN:" version="any">
<Name>The Great Network</Name>
<members>
<LineRef ref="FR:line:M1">
</LineRef>
<LineRef ref="FR:line:B42">
</LineRef>
<LineRef ref="FR:line:RERA">
</LineRef>
</members>
</Network>
</ServiceFrame>
</frames>
</CompositeFrame>
</dataObjects>
</PublicationDelivery>