diff --git a/gpx-rs/engine/data/with_route_points.gpx b/gpx-rs/engine/data/with_route_points.gpx
new file mode 100644
index 000000000..41be56470
--- /dev/null
+++ b/gpx-rs/engine/data/with_route_points.gpx
@@ -0,0 +1,30 @@
+
+
+
+ Lausanne
+ comment
+ description
+ Cycling
+
+ 500
+
+
+
+
+
+
+
+
+
+ 510
+
+
+
+
+
+
+
+ 505
+
+
+
diff --git a/gpx-rs/engine/src/core/io/parse.rs b/gpx-rs/engine/src/core/io/parse.rs
index c5818056e..b8f8370b6 100644
--- a/gpx-rs/engine/src/core/io/parse.rs
+++ b/gpx-rs/engine/src/core/io/parse.rs
@@ -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,
+ /// 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) {
+ 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 {
@@ -309,9 +396,9 @@ pub fn parse(data: &[u8], categories: &mut TrackpointCategories) -> Result = 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 = 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 {
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 {
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 {
@@ -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#"
+ 10
+ 20
+
+
+
+
+ "#;
+ 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#"
+
+ "#;
+ 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]
diff --git a/gpx-rs/engine/src/core/io/write.rs b/gpx-rs/engine/src/core/io/write.rs
index 7e21a5d86..a9a967aed 100644
--- a/gpx-rs/engine/src/core/io/write.rs
+++ b/gpx-rs/engine/src/core/io/write.rs
@@ -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>;
/// 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 {
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, ""), 1);
+ assert_eq!(count(&text, ""), 0);
+ assert_eq!(count(&text, ""), 0);
+ assert_eq!(count(&text, ""), 2);
+ // a route has the information of its track
+ assert!(text.contains("Lausanne"));
+ assert!(text.contains("comment"));
+ assert!(text.contains("description"));
+ assert!(text.contains("Cycling"));
+ // and its points only have a position in the path, and an elevation as route points
+ let first_rtept = &text[text.find("")));
+ assert!(first_rtept.contains("500"));
+ let (lat, lng) = (46.54441595077515_f64, 6.658358573913574_f64);
+ assert!(text.contains(&format!("")));
+ }
+
+ #[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::>()
+ };
+ 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, ""), segments);
+ assert_eq!(count(&text, ""), 0);
+ // every route starts and ends with a route point
+ assert_eq!(count(&text, ""), 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::>()
+ };
+ 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, ""), 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, "") >= 1);
+ assert_eq!(count(&text, ""), count(&text, ""));
+ 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(""));
+ let without = written_text(
+ &file,
+ &categories,
+ ExportOptions {
+ hr: false,
+ time: false,
+ as_route: true,
+ ..ExportOptions::ALL
+ },
+ );
+ assert!(!without.contains(""));
+ assert!(!without.contains("