import and export of routes, ref #192

This commit is contained in:
vcoppe committed 2026-10-09 09:26:38 +02:00
1 parent da9abbae5e
commit dc36de2a48
10 files changed
+620 -62

No files matched your search

+30
View File
@@ -0,0 +1,30 @@
<?xml version="1.0" encoding="UTF-8"?>
<gpx xmlns="http://www.topografix.com/GPX/1/1" xmlns:gpxx="http://www.garmin.com/xmlschemas/GpxExtensions/v3" version="1.1" creator="test">
<rte>
<name>Lausanne</name>
<cmt>comment</cmt>
<desc>description</desc>
<type>Cycling</type>
<rtept lat="46.544628096744418" lon="6.658079791814089">
<ele>500</ele>
<extensions>
<gpxx:RoutePointExtension>
<gpxx:rpt lat="46.544483928009868" lon="6.658018603920937"/>
<gpxx:rpt lat="46.544415950775146" lon="6.658358573913574"/>
<gpxx:rpt lat="46.544415950775146" lon="6.658358573913574"/>
</gpxx:RoutePointExtension>
</extensions>
</rtept>
<rtept lat="46.544100000000000" lon="6.659000000000000">
<ele>510</ele>
<extensions>
<gpxx:RoutePointExtension>
<gpxx:rpt lat="46.543900000000000" lon="6.659500000000000"/>
</gpxx:RoutePointExtension>
</extensions>
</rtept>
<rtept lat="46.543000000000000" lon="6.660000000000000">
<ele>505</ele>
</rtept>
</rte>
</gpx>
+215 -29
View File
@@ -1,6 +1,6 @@
use crate::{
Author, Chunk, File, Link, LngLat, Track, TrackSegment, Trackpoint, TrackpointCategories,
TrackpointChunk, Waypoint, WaypointChunk,
TrackpointChunk, Waypoint, WaypointChunk, distance,
};
use chrono::DateTime;
use quick_xml::Error;
@@ -300,6 +300,93 @@ fn attach_link(stack: &mut [GPXElement], gpx: &mut File, link: Link) {
}
}
/// Builds the segment of a route.
///
/// The route points are the anchors of the segment, and are the only ones that the file gives
/// (no anchor is computed). The points of the detailed path between a route point and the next one
/// (`gpxx:rpt`, which only have a position) are regular trackpoints, with the elevation worked out
/// from the one of the route points around them.
#[derive(Default)]
struct RouteReader {
segment: TrackSegment,
chunk: TrackpointChunk,
/// The intermediate points that follow the last route point, up to the next one.
pending: Vec<Trackpoint>,
/// Position and elevation of the last route point.
previous: Option<(LngLat, f64)>,
}
impl RouteReader {
fn push(&mut self, point: Trackpoint) {
self.chunk.trkpt.push(point);
if self.chunk.is_full() {
self.segment.push(std::mem::take(&mut self.chunk));
}
}
/// Adds a route point, after the intermediate points of the previous one. `following` are the
/// intermediate points that lead from it to the next one.
fn push_route_point(&mut self, mut rtept: Trackpoint, following: Vec<Trackpoint>) {
rtept.anchor = Some(0);
let here = (rtept.coordinates, rtept.ele);
self.flush(Some(here));
self.previous = Some(here);
self.push(rtept);
self.pending = following;
}
/// Adds the pending intermediate points, which lead to the route point `next` if there is one.
fn flush(&mut self, next: Option<(LngLat, f64)>) {
let mut pending = std::mem::take(&mut self.pending);
if let Some(previous) = self.previous {
interpolate_elevations(&mut pending, previous, next);
}
for point in pending {
self.push(point);
}
}
fn finish(mut self) -> TrackSegment {
self.flush(None);
self.segment.push(std::mem::take(&mut self.chunk));
self.segment.ensure_end_anchors();
self.segment
}
}
/// Gives the points of a path from `from` to `to` an elevation that goes from the one of the first
/// to the one of the second, in proportion to the distance along the path. Without `to`, they have
/// the elevation of `from`.
fn interpolate_elevations(
points: &mut [Trackpoint],
from: (LngLat, f64),
to: Option<(LngLat, f64)>,
) {
let Some((to_position, to_ele)) = to else {
points.iter_mut().for_each(|point| point.ele = from.1);
return;
};
let mut distances = Vec::with_capacity(points.len() + 1);
let mut total = 0.0;
let mut last = from.0;
for position in points
.iter()
.map(|point| point.coordinates)
.chain([to_position])
{
total += distance(last, position);
distances.push(total);
last = position;
}
for (point, along) in points.iter_mut().zip(distances) {
point.ele = if total > 0.0 {
from.1 + (to_ele - from.1) * along / total
} else {
from.1
};
}
}
/// Parses a GPX file. The surface, highway, SAC scale and MTB scale of the trackpoints are stored as codes of
/// `categories`, which learns the values it does not know yet.
pub fn parse(data: &[u8], categories: &mut TrackpointCategories) -> Result<File, Error> {
@@ -309,9 +396,9 @@ pub fn parse(data: &[u8], categories: &mut TrackpointCategories) -> Result<File,
let mut stack: Vec<GPXElement> = vec![];
let mut trkpt_chunk = TrackpointChunk::default();
let mut wpt_chunk = WaypointChunk::default();
// Routes are read as tracks of a single segment, which is what `rte_segment` collects the points of. A route
// point that comes with the detailed path of the route to the next one (`gpxx:rpt`) is replaced by it.
let mut rte_segment = TrackSegment::default();
// Routes are read as tracks of a single segment, see `RouteReader`. The points of the detailed
// path that a route point comes with (`gpxx:rpt`) are collected until the route point is read.
let mut route = RouteReader::default();
let mut rpt_points: Vec<Trackpoint> = vec![];
// the text of the element at the top of the stack, up to now
let mut text = String::new();
@@ -381,13 +468,13 @@ pub fn parse(data: &[u8], categories: &mut TrackpointCategories) -> Result<File,
}
"rtept" => {
if let Some(GPXElement::Track(_)) = stack.last() {
trkpt_chunk.trkpt.push(Trackpoint {
coordinates: parse_coordinates(e.attributes()),
..Default::default()
});
if trkpt_chunk.is_full() {
rte_segment.push(std::mem::take(&mut trkpt_chunk));
}
route.push_route_point(
Trackpoint {
coordinates: parse_coordinates(e.attributes()),
..Default::default()
},
vec![],
);
}
}
name if is_route_point_extension(name) => {
@@ -444,10 +531,8 @@ pub fn parse(data: &[u8], categories: &mut TrackpointCategories) -> Result<File,
}
"rte" => {
if let Some(GPXElement::Track(mut trk)) = stack.pop() {
rte_segment.push(std::mem::take(&mut trkpt_chunk));
if !rte_segment.is_empty() {
let mut trkseg = std::mem::take(&mut rte_segment);
trkseg.compute_anchors();
let trkseg = std::mem::take(&mut route).finish();
if !trkseg.is_empty() {
trk.trkseg.push(trkseg);
}
gpx.trk.push(trk);
@@ -457,17 +542,7 @@ pub fn parse(data: &[u8], categories: &mut TrackpointCategories) -> Result<File,
if let Some(GPXElement::Trackpoint(rtept)) = stack.pop()
&& let Some(GPXElement::Track(_)) = stack.last()
{
let points = if rpt_points.is_empty() {
vec![rtept]
} else {
std::mem::take(&mut rpt_points)
};
for point in points {
trkpt_chunk.trkpt.push(point);
if trkpt_chunk.is_full() {
rte_segment.push(std::mem::take(&mut trkpt_chunk));
}
}
route.push_route_point(rtept, std::mem::take(&mut rpt_points));
}
}
name if is_route_point_extension(name) => {
@@ -697,17 +772,128 @@ mod tests {
assert_eq!(trk.info.name.as_deref(), Some("with extensions"));
assert_eq!(trk.trkseg.len(), 1);
let points: Vec<_> = trk.trkseg[0].iter().collect();
// the detailed path replaces the route point it comes with
// the route points are kept, the points of the detailed path follow the one they come with
let coordinates: Vec<_> = points
.iter()
.map(|p| (p.coordinates.lat, p.coordinates.lng))
.collect();
assert_eq!(
coordinates,
[(1.1, 1.1), (1.2, 1.2), (2.0, 2.0), (3.0, 3.0)]
[(1.0, 1.0), (1.1, 1.1), (1.2, 1.2), (2.0, 2.0), (3.0, 3.0)]
);
assert_eq!(points[3].ele, 20.0);
assert!(points[3].time.is_some());
}
#[test]
fn test_route_points_are_the_anchors_and_nothing_else_is() {
let gpx = parse_data("with_route_extensions");
let anchors: Vec<_> = gpx.trk[0].trkseg[0].iter().map(|p| p.anchor).collect();
assert_eq!(anchors, [Some(0), None, None, Some(0), Some(0)]);
// no anchor is computed for the points of a route that has none in between
let gpx = parse_data("with_routes");
for trk in &gpx.trk {
let segment = &trk.trkseg[0];
assert!(segment.iter().all(|p| p.anchor == Some(0)));
}
// while a track gets the anchors that its shape needs
let track = parse_data("with_tracks");
let segment = &track.trk[0].trkseg[0];
let anchors = segment.iter().filter(|p| p.anchor.is_some()).count();
assert!(anchors >= 2 && anchors < segment.len());
}
#[test]
fn test_parse_the_points_of_the_detailed_path_of_a_route() {
let gpx = parse_data("with_route_points");
assert_eq!(gpx.trk.len(), 1);
let trk = &gpx.trk[0];
assert_eq!(trk.info.name.as_deref(), Some("Lausanne"));
assert_eq!(trk.info.cmt.as_deref(), Some("comment"));
assert_eq!(trk.info.desc.as_deref(), Some("description"));
assert_eq!(trk.info.type_.as_deref(), Some("Cycling"));
let points: Vec<_> = trk.trkseg[0].iter().cloned().collect();
// 3 route points and 4 points between them, the repeated one being kept
assert_eq!(points.len(), 7);
let kinds: Vec<_> = points.iter().map(|p| p.anchor.is_some()).collect();
assert_eq!(kinds, [true, false, false, false, true, false, true]);
assert_eq!(points[0].coordinates.lat, 46.54462809674442);
assert_eq!(points[0].coordinates.lng, 6.658079791814089);
assert_eq!(points[1].coordinates.lat, 46.54448392800987);
assert_eq!(points[3].coordinates.lng, points[2].coordinates.lng);
assert_eq!(points[4].coordinates.lat, 46.5441);
assert_eq!(points[5].coordinates.lat, 46.5439);
// the points in between have no elevation of their own: it goes from one route point to
// the next
let ele: Vec<_> = points.iter().map(|p| p.ele).collect();
assert_eq!((ele[0], ele[4], ele[6]), (500.0, 510.0, 505.0));
assert!(ele[0] < ele[1] && ele[1] < ele[2] && ele[2] <= ele[3] && ele[3] < ele[4]);
assert!(ele[5] < 510.0 && ele[5] > 505.0);
}
#[test]
fn test_the_last_point_of_a_route_is_an_anchor_even_after_a_path() {
// the last route point comes with a path that leads nowhere
let data = r#"<gpx xmlns:gpxx="x"><rte>
<rtept lat="1" lon="1"><ele>10</ele></rtept>
<rtept lat="2" lon="2"><ele>20</ele>
<extensions><gpxx:RoutePointExtension>
<gpxx:rpt lat="2.1" lon="2.1"/><gpxx:rpt lat="2.2" lon="2.2"/>
</gpxx:RoutePointExtension></extensions>
</rtept>
</rte></gpx>"#;
let gpx = parse(data.as_bytes(), &mut Default::default()).unwrap();
let points: Vec<_> = gpx.trk[0].trkseg[0].iter().cloned().collect();
let coordinates: Vec<_> = points.iter().map(|p| p.coordinates.lat).collect();
assert_eq!(coordinates, [1.0, 2.0, 2.1, 2.2]);
// the route point is an anchor, and so is the end of the path
let anchors: Vec<_> = points.iter().map(|p| p.anchor).collect();
assert_eq!(anchors, [Some(0), Some(0), None, Some(0)]);
// with the elevation of the last route point, as there is no next one
assert_eq!(points[2].ele, 20.0);
assert!(points[2].time.is_some());
assert_eq!(points[3].ele, 20.0);
// a path after a single route point
let data = r#"<gpx xmlns:gpxx="x"><rte><rtept lat="1" lon="1"><extensions>
<gpxx:RoutePointExtension><gpxx:rpt lat="1.5" lon="1.5"/></gpxx:RoutePointExtension>
</extensions></rtept></rte></gpx>"#;
let gpx = parse(data.as_bytes(), &mut Default::default()).unwrap();
let anchors: Vec<_> = gpx.trk[0].trkseg[0].iter().map(|p| p.anchor).collect();
assert_eq!(anchors, [Some(0), Some(0)]);
}
#[test]
fn test_elevations_between_route_points() {
let here = |lng: f64| LngLat { lng, lat: 0.0 };
let point = |lng: f64| Trackpoint {
coordinates: here(lng),
..Default::default()
};
// evenly spaced points go up evenly
let mut points = vec![point(1.0), point(2.0), point(3.0)];
interpolate_elevations(&mut points, (here(0.0), 100.0), Some((here(4.0), 200.0)));
let ele: Vec<_> = points.iter().map(|p| p.ele.round()).collect();
assert_eq!(ele, [125.0, 150.0, 175.0]);
// the distance counts, not the number of points
let mut points = vec![point(1.0)];
interpolate_elevations(&mut points, (here(0.0), 0.0), Some((here(4.0), 400.0)));
assert!((points[0].ele - 100.0).abs() < 1e-6);
// after the last route point, the elevation does not change
let mut points = vec![point(1.0), point(2.0)];
interpolate_elevations(&mut points, (here(0.0), 321.0), None);
assert!(points.iter().all(|p| p.ele == 321.0));
// route points at the same place
let mut points = vec![point(0.0)];
interpolate_elevations(&mut points, (here(0.0), 10.0), Some((here(0.0), 20.0)));
assert_eq!(points[0].ele, 10.0);
// nothing between them is fine
interpolate_elevations(&mut [], (here(0.0), 1.0), Some((here(1.0), 2.0)));
}
#[test]
+273 -28
View File
@@ -8,6 +8,9 @@ use crate::{File, Link, Track, Trackpoint, TrackpointCategories, Waypoint};
/// What the written file contains: the data of the trackpoints that are `false` are left out.
/// The same type tells which data a file has, see [`File::exportable_data`].
///
/// With `as_route`, the file is written with routes instead of tracks, see [`write`]. It is not
/// data: it is off in `ALL` and `NONE`, and in what [`File::exportable_data`] gives.
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct ExportOptions {
pub time: bool,
@@ -17,6 +20,8 @@ pub struct ExportOptions {
pub power: bool,
/// The OpenStreetMap data: surface, highway, SAC scale and MTB scale.
pub osm: bool,
/// Writes every segment as a route (`rte`) instead of a track.
pub as_route: bool,
}
impl ExportOptions {
@@ -31,6 +36,7 @@ impl ExportOptions {
atemp: value,
power: value,
osm: value,
as_route: false,
}
}
@@ -43,6 +49,7 @@ impl ExportOptions {
atemp: self.atemp || other.atemp,
power: self.power || other.power,
osm: self.osm || other.osm,
as_route: self.as_route || other.as_route,
}
}
}
@@ -81,6 +88,11 @@ type XmlWriter = Writer<Vec<u8>>;
/// Writes a file as GPX 1.1, in UTF-8. The surface, highway, SAC scale and MTB scale of the trackpoints are
/// the names of their codes in `categories`.
///
/// The segments are written as tracks, or with `options.as_route` as routes: a segment becomes a
/// `rte` that has the information of its track, its anchors are the `rtept` and the trackpoints
/// between two anchors are the `gpxx:rpt` of the `rtept` before them (which only have a
/// position, unlike the `rtept`). The first and last trackpoints always are `rtept`.
pub fn write(file: &File, categories: &TrackpointCategories, options: ExportOptions) -> Vec<u8> {
let mut writer = Writer::new_with_indent(Vec::new(), b' ', 4);
// writing to memory does not fail
@@ -131,7 +143,13 @@ fn write_file(
(_, 1) => Some(file.info.name.as_str()),
_ => None,
};
write_track(w, trk, name, categories, options)?;
if options.as_route {
for segment in trk.trkseg.iter().filter(|segment| !segment.is_empty()) {
write_route(w, trk, segment, name, categories, options)?;
}
} else {
write_track(w, trk, name, categories, options)?;
}
}
Ok(())
})?;
@@ -237,6 +255,38 @@ fn write_waypoint(w: &mut XmlWriter, wpt: &Waypoint, options: ExportOptions) ->
Ok(())
}
/// The information of a track that its routes have too: name, comment, description, source,
/// links, type and style.
fn write_track_info(
w: &mut XmlWriter,
name: Option<&str>,
info: &crate::TrackInfo,
) -> io::Result<()> {
text(w, "name", name.unwrap_or_default())?;
optional_text(w, "cmt", &info.cmt)?;
optional_text(w, "desc", &info.desc)?;
optional_text(w, "src", &info.src)?;
links(w, &info.links)?;
optional_text(w, "type", &info.type_)?;
if info.color.is_some() || info.opacity.is_some() || info.width.is_some() {
w.create_element("extensions").write_inner_content(|w| {
w.create_element("gpx_style:line")
.write_inner_content(|w| {
optional_text(w, "gpx_style:color", &info.color)?;
if let Some(opacity) = info.opacity {
number(w, "gpx_style:opacity", opacity)?;
}
if let Some(width) = info.width {
number(w, "gpx_style:width", width)?;
}
Ok(())
})?;
Ok(())
})?;
}
Ok(())
}
fn write_track(
w: &mut XmlWriter,
trk: &Track,
@@ -244,34 +294,12 @@ fn write_track(
categories: &TrackpointCategories,
options: ExportOptions,
) -> io::Result<()> {
let info = &trk.info;
w.create_element("trk").write_inner_content(|w| {
text(w, "name", name.unwrap_or_default())?;
optional_text(w, "cmt", &info.cmt)?;
optional_text(w, "desc", &info.desc)?;
optional_text(w, "src", &info.src)?;
links(w, &info.links)?;
optional_text(w, "type", &info.type_)?;
if info.color.is_some() || info.opacity.is_some() || info.width.is_some() {
w.create_element("extensions").write_inner_content(|w| {
w.create_element("gpx_style:line")
.write_inner_content(|w| {
optional_text(w, "gpx_style:color", &info.color)?;
if let Some(opacity) = info.opacity {
number(w, "gpx_style:opacity", opacity)?;
}
if let Some(width) = info.width {
number(w, "gpx_style:width", width)?;
}
Ok(())
})?;
Ok(())
})?;
}
write_track_info(w, name, &trk.info)?;
for segment in &trk.trkseg {
w.create_element("trkseg").write_inner_content(|w| {
for point in segment.iter() {
write_trackpoint(w, point, categories, options)?;
write_point(w, "trkpt", point, &[], categories, options)?;
}
Ok(())
})?;
@@ -281,9 +309,48 @@ fn write_track(
Ok(())
}
fn write_trackpoint(
fn write_route(
w: &mut XmlWriter,
trk: &Track,
segment: &crate::TrackSegment,
name: Option<&str>,
categories: &TrackpointCategories,
options: ExportOptions,
) -> io::Result<()> {
w.create_element("rte").write_inner_content(|w| {
write_track_info(w, name, &trk.info)?;
let points: Vec<&Trackpoint> = segment.iter().collect();
let last = points.len() - 1;
let is_anchor = |i: usize| i == 0 || i == last || points[i].anchor.is_some();
let mut i = 0;
while i < points.len() {
// the trackpoints up to the next anchor
let mut next = i + 1;
while next < points.len() && !is_anchor(next) {
next += 1;
}
write_point(
w,
"rtept",
points[i],
&points[i + 1..next],
categories,
options,
)?;
i = next;
}
Ok(())
})?;
Ok(())
}
/// A trackpoint or a route point (`tag`). A route point has the `path` that leads to the next one,
/// of which only the positions are written.
fn write_point(
w: &mut XmlWriter,
tag: &str,
point: &Trackpoint,
path: &[&Trackpoint],
categories: &TrackpointCategories,
options: ExportOptions,
) -> io::Result<()> {
@@ -306,12 +373,12 @@ fn write_trackpoint(
let has_osm = osm.iter().any(|(_, value)| value.is_some());
let has_track_point_extension = atemp.is_some() || hr.is_some() || cad.is_some() || has_osm;
write_coordinates(w, "trkpt", point.coordinates).write_inner_content(|w| {
write_coordinates(w, tag, point.coordinates).write_inner_content(|w| {
number(w, "ele", point.ele)?;
if options.time {
time(w, point.time)?;
}
if !has_track_point_extension && power.is_none() {
if !has_track_point_extension && power.is_none() && path.is_empty() {
return Ok(());
}
w.create_element("extensions").write_inner_content(|w| {
@@ -343,6 +410,15 @@ fn write_trackpoint(
w.create_element("gpxpx:PowerExtension")
.write_inner_content(|w| number(w, "gpxpx:PowerInWatts", power))?;
}
if !path.is_empty() {
w.create_element("gpxx:RoutePointExtension")
.write_inner_content(|w| {
for point in path {
write_coordinates(w, "gpxx:rpt", point.coordinates).write_empty()?;
}
Ok(())
})?;
}
Ok(())
})?;
Ok(())
@@ -518,4 +594,173 @@ mod tests {
let (file, _) = read("with_power_2");
assert!(file.exportable_data().power);
}
fn as_route() -> ExportOptions {
ExportOptions {
as_route: true,
..ExportOptions::ALL
}
}
fn count(text: &str, pattern: &str) -> usize {
text.matches(pattern).count()
}
#[test]
fn test_export_as_route_writes_routes_instead_of_tracks() {
let (file, categories) = read("with_route_points");
let text = written_text(&file, &categories, as_route());
assert_eq!(count(&text, "<rte>"), 1);
assert_eq!(count(&text, "<trk>"), 0);
assert_eq!(count(&text, "<trkseg>"), 0);
assert_eq!(count(&text, "<trkpt"), 0);
// the anchors are the route points, the trackpoints between them the points of their path
assert_eq!(count(&text, "<rtept"), 3);
assert_eq!(count(&text, "<gpxx:rpt"), 4);
assert_eq!(count(&text, "<gpxx:RoutePointExtension>"), 2);
// a route has the information of its track
assert!(text.contains("<name>Lausanne</name>"));
assert!(text.contains("<cmt>comment</cmt>"));
assert!(text.contains("<desc>description</desc>"));
assert!(text.contains("<type>Cycling</type>"));
// and its points only have a position in the path, and an elevation as route points
let first_rtept = &text[text.find("<rtept").unwrap()..];
let (lat, lng) = (46.54462809674442_f64, 6.658079791814089_f64);
assert!(first_rtept.starts_with(&format!("<rtept lat=\"{lat}\" lon=\"{lng}\">")));
assert!(first_rtept.contains("<ele>500</ele>"));
let (lat, lng) = (46.54441595077515_f64, 6.658358573913574_f64);
assert!(text.contains(&format!("<gpxx:rpt lat=\"{lat}\" lon=\"{lng}\"/>")));
}
#[test]
fn test_a_route_survives_being_exported_as_a_route() {
let (file, mut categories) = read("with_route_points");
let written = write(&file, &categories, as_route());
let again = parse(&written, &mut categories).unwrap();
assert_eq!(again.trk.len(), 1);
assert_eq!(again.trk[0].info, file.trk[0].info);
let (a, b) = (&file.trk[0].trkseg[0], &again.trk[0].trkseg[0]);
assert_eq!(a.len(), b.len());
for (a, b) in a.iter().zip(b.iter()) {
assert_eq!(a.coordinates.lat, b.coordinates.lat);
assert_eq!(a.coordinates.lng, b.coordinates.lng);
assert_eq!(a.anchor, b.anchor);
}
// the elevation of the route points is kept, the other ones are worked out again
let anchors = |s: &crate::TrackSegment| {
s.iter()
.filter(|p| p.anchor.is_some())
.map(|p| p.ele)
.collect::<Vec<_>>()
};
assert_eq!(anchors(a), anchors(b));
}
#[test]
fn test_tracks_become_one_route_per_segment_with_the_information_of_the_track() {
let (file, mut categories) = read("with_tracks_and_segments");
let segments: usize = file.trk.iter().map(|t| t.trkseg.len()).sum();
assert!(segments > 2);
let written = write(&file, &categories, as_route());
let text = String::from_utf8(written.clone()).unwrap();
assert_eq!(count(&text, "<rte>"), segments);
assert_eq!(count(&text, "<trk>"), 0);
// every route starts and ends with a route point
assert_eq!(count(&text, "</rte>"), segments);
let again = parse(&written, &mut categories).unwrap();
assert_eq!(again.trk.len(), segments);
let mut routes = again.trk.iter();
for track in &file.trk {
for segment in &track.trkseg {
let route = routes.next().unwrap();
// the information of the track, repeated for each of its segments
assert_eq!(route.info.name, track.info.name);
assert_eq!(route.info.type_, track.info.type_);
let (a, b) = (segment, &route.trkseg[0]);
assert_eq!(a.len(), b.len());
for (a, b) in a.iter().zip(b.iter()) {
assert_eq!(
(a.coordinates.lat, a.coordinates.lng),
(b.coordinates.lat, b.coordinates.lng)
);
}
// the anchors of the segment are the ones of its route
let anchors = |s: &crate::TrackSegment| {
s.iter()
.enumerate()
.filter(|(_, p)| p.anchor.is_some())
.map(|(i, _)| i)
.collect::<Vec<_>>()
};
assert_eq!(anchors(a), anchors(b));
}
}
}
#[test]
fn test_the_ends_of_a_route_are_route_points_even_without_anchors() {
let (mut file, categories) = read("simple");
file.trk[0].trkseg[0].update_all(|_, point| point.anchor = None);
let len = file.trk[0].trkseg[0].len();
let text = written_text(&file, &categories, as_route());
assert_eq!(count(&text, "<rtept"), 2);
assert_eq!(count(&text, "<gpxx:rpt"), len - 2);
// and a single trackpoint is a route of one point
file.trk[0].trkseg[0].splice(1, len, vec![]);
let text = written_text(&file, &categories, as_route());
assert_eq!(count(&text, "<rtept"), 1);
assert_eq!(count(&text, "<gpxx:rpt"), 0);
}
#[test]
fn test_empty_segments_are_not_routes() {
let (mut file, categories) = read("simple");
file.trk[0].trkseg.push(crate::TrackSegment::default());
let text = written_text(&file, &categories, as_route());
assert_eq!(count(&text, "<rte>"), 1);
}
#[test]
fn test_routes_have_the_style_of_their_track() {
let (file, categories) = read("with_style");
let text = written_text(&file, &categories, as_route());
assert!(count(&text, "<gpx_style:line>") >= 1);
assert_eq!(count(&text, "<gpx_style:line>"), count(&text, "<rte>"));
let again = parse(text.as_bytes(), &mut Default::default()).unwrap();
assert_eq!(again.trk[0].info.color, file.trk[0].info.color);
assert_eq!(again.trk[0].info.width, file.trk[0].info.width);
}
#[test]
fn test_route_points_keep_their_data_and_the_options_apply_to_them() {
let (file, categories) = read("with_hr");
let with_all = written_text(&file, &categories, as_route());
assert!(with_all.contains("<gpxtpx:hr>"));
let without = written_text(
&file,
&categories,
ExportOptions {
hr: false,
time: false,
as_route: true,
..ExportOptions::ALL
},
);
assert!(!without.contains("<gpxtpx:hr>"));
assert!(!without.contains("<time>"));
}
#[test]
fn test_the_route_option_is_not_data() {
let (file, _) = read("with_route_points");
assert!(!file.exportable_data().as_route);
// it is a way to write the data, never part of them
let all = std::hint::black_box(ExportOptions::ALL);
assert!(!all.as_route && !ExportOptions::NONE.as_route);
assert!(as_route().union(ExportOptions::NONE).as_route);
assert!(!ExportOptions::ALL.union(ExportOptions::NONE).as_route);
}
}
+5 -1
View File
@@ -56,8 +56,10 @@ pub fn selection_statistics() -> GlobalStatistics {
}
/// A file as GPX (UTF-8 bytes), `undefined` if it does not exist. The options say which data of the trackpoints
/// to keep.
/// to keep, and with `as_route` that the segments are written as routes (`rte`) instead of tracks:
/// the anchors are the route points, and the trackpoints between them the points of their path.
#[wasm_bindgen]
#[allow(clippy::too_many_arguments)]
pub fn export_file(
file_id: &str,
time: bool,
@@ -66,6 +68,7 @@ pub fn export_file(
atemp: bool,
power: bool,
osm: bool,
as_route: bool,
) -> Option<Vec<u8>> {
let id = parse_file_id(file_id)?;
let options = engine::ExportOptions {
@@ -75,6 +78,7 @@ pub fn export_file(
atemp,
power,
osm,
as_route,
};
with_engine(|e| e.export(&id, options)).flatten()
}
+23 -2
View File
@@ -112,7 +112,10 @@ fn test_unknown_file_has_no_structure() {
assert!(crate::queries::files::file_structure(&id).is_undefined());
assert!(crate::queries::files::file_structure("not an id").is_undefined());
assert!(crate::queries::files::file_statistics(&id).is_undefined());
assert!(crate::queries::files::export_file(&id, true, true, true, true, true, true).is_none());
assert!(
crate::queries::files::export_file(&id, true, true, true, true, true, true, false)
.is_none()
);
assert_eq!(
crate::queries::coordinates::segment_coordinates(&id).length(),
0
@@ -265,7 +268,8 @@ fn test_export_and_exportable_data() {
let ids = order();
let bytes =
crate::queries::files::export_file(&ids[0], true, true, true, true, true, true).unwrap();
crate::queries::files::export_file(&ids[0], true, true, true, true, true, true, false)
.unwrap();
assert!(String::from_utf8(bytes).unwrap().contains("<gpx"));
// the time is the first bit
@@ -284,6 +288,23 @@ fn test_export_and_exportable_data() {
assert_eq!(crate::queries::files::exportable_data(&[1, 2, 3]), 0);
}
#[wasm_bindgen_test]
fn test_export_as_route() {
started_with(&[(SIMPLE, "plain")]);
let id = order().remove(0);
let text = |as_route| {
let bytes =
crate::queries::files::export_file(&id, true, true, true, true, true, true, as_route)
.unwrap();
String::from_utf8(bytes).unwrap()
};
let tracks = text(false);
assert!(tracks.contains("<trk>") && !tracks.contains("<rte>"));
let routes = text(true);
assert!(routes.contains("<rte>") && !routes.contains("<trk>"));
assert!(routes.contains("<rtept"));
}
#[wasm_bindgen_test]
fn test_select_and_reorder_files() {
started_with(&[(SIMPLE, "a"), (WITH_TIME, "b")]);