Skip to content

Commit a070314

Browse files
committed
[doc] Add better documentation
Especially in the course module. This also renames CourseBuilder to RouteBuilder for clarity.
1 parent 26faa56 commit a070314

7 files changed

Lines changed: 157 additions & 92 deletions

File tree

README.md

Lines changed: 7 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -70,8 +70,8 @@ doesn't mean they're necessarily located *along* the route.
7070

7171
In contrast, the course points in a FIT course file are specifically points on
7272
the course. In addition to their latitude and longitude, they also specify
73-
the distance at which they appear along the course, used by the Up Ahead
74-
feature to compute distance remaining.
73+
the distance at which they appear along the course, which is used by the Up
74+
Ahead feature to compute distance remaining.
7575

7676
So given a GPX route and a set of waypoints, coursepointer:
7777

@@ -113,10 +113,10 @@ route-planning applications.
113113
However, for routes or tracks exported by [Gaia GPS](https://gaiagps.com/) or
114114
[Ride with GPS](https://ridewithgps.com/), coursepointer additionally attempts
115115
to map the waypoint or POI type (respectively) to a relevant course point type
116-
instead of `generic`, so that you'll see a relevant course point icon instead
117-
of a generic pin. See [docs/point_types.md](docs/point_types.md) for more
118-
information about how point types from these apps are interpreted as course
119-
point types.
116+
instead of `generic`, so that you'll see a meaningful course point icon
117+
instead of a generic pin. See [docs/point_types.md](docs/point_types.md) for
118+
more information about how point types from these apps are interpreted as
119+
course point types.
120120

121121
Currently, GPX input files must contain exactly one route or track, and zero
122122
or more waypoints. Routes and tracks are treated identically.
@@ -142,7 +142,7 @@ delete](https://help.gaiagps.com/hc/en-us/articles/360004157214-Archiving-Unarch
142142
it and then re-export. To avoid it, move routes or waypoints out of your
143143
folder before archiving them.
144144

145-
## Development
145+
## Development and contributing
146146

147147
See [docs/development.md](docs/development.md).
148148

docs/development.md

Lines changed: 7 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -24,16 +24,18 @@ uv run --package integration pytest
2424
```
2525

2626
Some of the integration tests use the nested `integration-stub` binary crate
27-
by passing JSON specifications to it and then examining its output. Most
28-
others build and test against the `coursepointer` command-line binary.
27+
by passing JSON specifications to it and then examining its output. (This was
28+
how I bootstrapped my early FIT encoding implementation, which is the first
29+
part I wrote.) Most other integration tests build and test against the main
30+
`coursepointer` command-line binary.
2931

3032
## Formatting
3133

32-
Though the project builds with the stable Rust toolchain, it uses nightly for
33-
`cargo fmt` for access to unstable features:
34+
Though the project targets the stable Rust toolchain, it uses nightly for
35+
`cargo fmt` to access unstable features:
3436

3537
```
36-
rustup toolchain install nightly
38+
rustup toolchain install --profile minimal -c rustfmt nightly
3739
cargo +nightly fmt
3840
```
3941

integration-stub/src/main.rs

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@ use std::path::PathBuf;
55
use anyhow::Result;
66
use chrono::{DateTime, Utc};
77
use clap::{Parser, Subcommand};
8-
use coursepointer::CourseOptions;
8+
use coursepointer::CourseSetOptions;
99
use coursepointer::internal::{CourseFile, CourseSetBuilder, DEG, GeoPoint};
1010
use dimensioned::f64prefixes::KILO;
1111
use dimensioned::si::M;
@@ -66,7 +66,7 @@ fn write_fit(spec: PathBuf, out: PathBuf) -> Result<()> {
6666
let spec: CourseSpec = serde_json::from_reader(spec_file)?;
6767

6868
let mut fit_file = BufWriter::new(File::create(&out)?);
69-
let mut builder = CourseSetBuilder::new(CourseOptions::default());
69+
let mut builder = CourseSetBuilder::new(CourseSetOptions::default());
7070
builder.add_course();
7171
builder.last_course_mut()?.with_name(spec.name);
7272
for point in &spec.records {

src/bin/cli.rs

Lines changed: 3 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -8,10 +8,9 @@ use anyhow::{Context, Result, bail};
88
use clap::builder::styling::Styles;
99
use clap::{Args, ColorChoice, Parser, Subcommand, ValueEnum, command};
1010
use clap_cargo::style::{ERROR, HEADER, INVALID, LITERAL, PLACEHOLDER, USAGE, VALID};
11+
use coursepointer::course::{CourseSetOptions, InterceptStrategy};
1112
use coursepointer::internal::{CoursePointType, Kilometer, Mile};
12-
use coursepointer::{
13-
ConversionInfo, CourseOptions, CoursePointerError, FitEncodeError, InterceptStrategy,
14-
};
13+
use coursepointer::{ConversionInfo, CoursePointerError, FitEncodeError};
1514
use dimensioned::f64prefixes::KILO;
1615
use dimensioned::si::{HR, M, Meter};
1716
use strum::Display;
@@ -211,7 +210,7 @@ fn convert_gpx_cmd(args: &Cli, sub_args: &ConvertArgs) -> Result<String> {
211210
);
212211
info!("Created FIT output file: {:?}", absolute(output)?);
213212

214-
let course_options = CourseOptions::default()
213+
let course_options = CourseSetOptions::default()
215214
.with_threshold(sub_args.threshold * M)
216215
.with_strategy(sub_args.strategy);
217216
let fit_speed = sub_args.speed * KILO * M / HR;

src/course.rs

Lines changed: 96 additions & 48 deletions
Original file line numberDiff line numberDiff line change
@@ -1,27 +1,72 @@
1-
//! Abstract representation of courses and waypoints
1+
//! Types for composing courses that contain course points
22
//!
3-
//! Provides [`Course`], an abstract representation of a course with its records
4-
//! and course points (if any). Courses are created by obtaining a
5-
//! [`CourseSetBuilder`] and adding data to it.
3+
//! # Course points and courses, waypoints and routes
64
//!
7-
//! Once all the data has been added (for example, by parsing it from a GPX
8-
//! file), [`CourseSetBuilder::build`] returns a [`CourseSet`].
5+
//! This module borrows terminology from GPX and Garmin FIT. Here a route, like
6+
//! a GPX route, consists of a sequence of route points with latitude,
7+
//! longitude, and optionally elevation. And a waypoint is just a named
8+
//! location, which may or may not be associated with any given route.
9+
//!
10+
//! (GPX also has a notion of tracks and track points, but here they are
11+
//! represented as routes and route points. This module has no concept
12+
//! corresponding to a GPX track segment.)
13+
//!
14+
//! These GPX-based concepts have FIT analogues, which contain additional
15+
//! distance information:
16+
//!
17+
//! | GPX term | FIT term | FIT additional data |
18+
//! | ----------- | ------------ | ------------------------------------------ |
19+
//! | route | course | Total distance |
20+
//! | route point | record | Distance along the course |
21+
//! | waypoint | course point | Association with and distance along course |
22+
//!
23+
//! The role of this module is to calculate this additional information needed
24+
//! to turn a set of routes and waypoints into courses and associated course
25+
//! points.
26+
//!
27+
//! Unlike in a Garmin FIT course file, here course records do not contain
28+
//! timestamps, but those can be trivially computed from records' distances
29+
//! based on a given start time and speed.
30+
//!
31+
//! # Module overview
32+
//!
33+
//! This module provides [`CourseSetBuilder`], which is used to build a set of
34+
//! routes and waypoints into a set of courses and their now-associated course
35+
//! points.
36+
//!
37+
//! Individual routes are added with [`CourseSetBuilder::add_route`], and their
38+
//! [`RouteBuilder::with_name`] and [`RouteBuilder::with_route_point`] methods
39+
//! are used to add names and route points to them.
40+
//!
41+
//! Waypoints are added to the set (not to a particular route) with
42+
//! [`CourseSetBuilder::add_waypoint`].
43+
//!
44+
//! Once your routes and waypoints have been assembled, running
45+
//! [`CourseSetBuilder::build`] does the work needed to determine which
46+
//! waypoints qualify as course points along any of the given routes. It
47+
//! returns a [`CourseSet`] containing [`Course`] instances, which in turn will
48+
//! contain any identified [`CoursePoint`] instances.
49+
//!
50+
//! If route points are given optional elevation data, this will simply be
51+
//! passed through to the corresponding course points. No additional geoid data
52+
//! is applied by this module, and elevations are not used in distance
53+
//! calculations.
954
1055
use std::cmp::Ordering;
1156
use std::convert::Infallible;
1257

13-
use dimensioned::si::{M, Meter};
58+
use dimensioned::si::{Meter, M};
1459
#[cfg(feature = "rayon")]
1560
use rayon::prelude::*;
1661
use thiserror::Error;
1762
use tracing::{debug, error, info};
1863

1964
use crate::algorithm::{
20-
AlgorithmError, FromGeoPoints, NearbySegment, find_nearby_segments, intercept_distance_floor,
21-
karney_interception,
65+
find_nearby_segments, intercept_distance_floor, karney_interception, AlgorithmError,
66+
FromGeoPoints, NearbySegment,
2267
};
2368
use crate::fit::CoursePointType;
24-
use crate::geographic::{GeographicError, geodesic_inverse};
69+
use crate::geographic::{geodesic_inverse, GeographicError};
2570
use crate::types::{GeoAndXyzPoint, GeoPoint, GeoSegment, HasGeoPoint};
2671

2772
#[derive(Error, Debug)]
@@ -54,7 +99,7 @@ macro_rules! iter_work {
5499
};
55100
}
56101

57-
/// Strategy for handling duplicate intercepts from a waypoint.
102+
/// A strategy for handling duplicate intercepts from a waypoint.
58103
///
59104
/// Duplicate interception can happen in an out-and-back course, for example.
60105
/// This strategy determines what to do in the case that duplicate intercepts
@@ -74,19 +119,19 @@ pub enum InterceptStrategy {
74119
All,
75120
}
76121

77-
/// Options for building a course.
78-
pub struct CourseOptions {
79-
/// The threshold distance of a waypoint from the segments of a course,
80-
/// below which the course will be considered "intercepted" by the waypoint,
81-
/// turning it into a course point.
122+
/// Options for building a course set
123+
pub struct CourseSetOptions {
124+
/// The maximum distance between a waypoint and a route, within which the
125+
/// waypoint will be considered to be a course point along the corresponding
126+
/// course.
82127
pub threshold: Meter<f64>,
83128

84-
/// What strategy should be applied in the case of duplicate course
85-
/// intercepts from a waypoint.
129+
/// What strategy to apply when a waypoint intercepts a single route
130+
/// multiple times.
86131
pub strategy: InterceptStrategy,
87132
}
88133

89-
impl Default for CourseOptions {
134+
impl Default for CourseSetOptions {
90135
fn default() -> Self {
91136
Self {
92137
threshold: 35.0 * M,
@@ -95,7 +140,7 @@ impl Default for CourseOptions {
95140
}
96141
}
97142

98-
impl CourseOptions {
143+
impl CourseSetOptions {
99144
pub fn with_threshold(self, threshold: Meter<f64>) -> Self {
100145
Self {
101146
threshold,
@@ -115,27 +160,30 @@ pub struct CourseSet {
115160
pub courses: Vec<Course>,
116161
}
117162

163+
/// Builds routes and waypoints into courses with associated course points
118164
pub struct CourseSetBuilder {
119-
options: CourseOptions,
120-
course_builders: Vec<CourseBuilder>,
165+
options: CourseSetOptions,
166+
course_builders: Vec<RouteBuilder>,
121167
waypoints: Vec<Waypoint>,
122168
}
123169

124170
impl CourseSetBuilder {
125-
pub fn new(options: CourseOptions) -> Self {
171+
/// Returns a new builder using the given [`CourseSetOptions`].
172+
pub fn new(options: CourseSetOptions) -> Self {
126173
Self {
127174
options,
128175
course_builders: Vec::new(),
129176
waypoints: Vec::new(),
130177
}
131178
}
132179

133-
pub fn add_course(&mut self) -> &mut CourseBuilder {
134-
self.course_builders.push(CourseBuilder::new());
180+
/// Adds a new [`RouteBuilder`] to this set builder.
181+
pub fn add_route(&mut self) -> &mut RouteBuilder {
182+
self.course_builders.push(RouteBuilder::new());
135183
self.last_course_mut().unwrap()
136184
}
137185

138-
pub fn last_course_mut(&mut self) -> Result<&mut CourseBuilder> {
186+
pub fn last_course_mut(&mut self) -> Result<&mut RouteBuilder> {
139187
match self.course_builders.last_mut() {
140188
Some(course) => Ok(course),
141189
None => Err(CourseError::MissingCourse),
@@ -294,7 +342,7 @@ impl CourseSetBuilder {
294342
}
295343

296344
#[derive(Clone, Copy, Debug)]
297-
pub struct NearIntercept {
345+
struct NearIntercept {
298346
/// The point of interception.
299347
intercept_point: GeoPoint,
300348

@@ -320,7 +368,7 @@ impl PartialOrd for NearIntercept {
320368
}
321369

322370
#[derive(Clone, Copy, PartialEq, Debug)]
323-
pub enum InterceptSolution {
371+
enum InterceptSolution {
324372
Near(NearIntercept),
325373
Far,
326374
}
@@ -349,7 +397,7 @@ impl NearbySegment<Meter<f64>> for &InterceptSolution {
349397
}
350398
}
351399

352-
/// A course for navigation.
400+
/// A navigation course
353401
///
354402
/// This contains the distance data needed as input for a FIT course file, but
355403
/// it does not represent speeds or timestamps.
@@ -358,9 +406,9 @@ pub struct Course {
358406
/// course, in order of physical traversal.
359407
pub records: Vec<Record>,
360408

361-
/// The course points and their cumulative distances. These are derived from
362-
/// the subset of waypoints provided to [`CourseSetBuilder`] that are
363-
/// located near the course.
409+
/// The course points and their course distances. These are derived from the
410+
/// subset of waypoints provided to [`CourseSetBuilder`] that are located
411+
/// near the course.
364412
pub course_points: Vec<CoursePoint>,
365413

366414
/// The name of the course, if given.
@@ -386,16 +434,16 @@ impl Course {
386434
///
387435
/// Used as part of [`CourseSetBuilder`] to allow composing a course.
388436
/// Within that context, obtain a new instance with
389-
/// [`CourseSetBuilder::add_course`].
390-
pub struct CourseBuilder {
437+
/// [`CourseSetBuilder::add_route`].
438+
pub struct RouteBuilder {
391439
route_points: Vec<GeoPoint>,
392440
xyz_points: Vec<GeoAndXyzPoint>,
393441
name: Option<String>,
394442
num_repeated_points_skipped: usize,
395443
}
396444

397445
#[allow(clippy::new_without_default)]
398-
impl CourseBuilder {
446+
impl RouteBuilder {
399447
fn new() -> Self {
400448
Self {
401449
route_points: Vec::new(),
@@ -464,7 +512,7 @@ impl CourseBuilder {
464512

465513
/// A segmented [`Course`] builder.
466514
///
467-
/// This represents an intermediate stage of building a [`CourseBuilder`]: The
515+
/// This represents an intermediate stage of building a [`RouteBuilder`]: The
468516
/// initial work of processing route points into geodesic segments along with
469517
/// distance information, as well as possibly [`XyPoint`] values, has been done.
470518
///
@@ -574,25 +622,25 @@ pub struct CoursePoint {
574622
mod tests {
575623
use anyhow::Result;
576624
use approx::assert_relative_eq;
577-
use dimensioned::si::{M, Meter};
625+
use dimensioned::si::{Meter, M};
578626

579627
use crate::course::{
580-
CourseBuilder, CourseSetBuilder, InterceptSolution, NearIntercept, Waypoint,
628+
CourseSetBuilder, InterceptSolution, NearIntercept, RouteBuilder, Waypoint,
581629
};
582630
use crate::fit::CoursePointType;
583631
use crate::types::GeoPoint;
584-
use crate::{CourseOptions, geo_point, geo_points};
632+
use crate::{geo_point, geo_points, CourseSetOptions};
585633

586634
#[test]
587-
fn test_course_builder_empty() -> Result<()> {
588-
let course = CourseBuilder::new().segment()?.build()?;
635+
fn test_route_builder_empty() -> Result<()> {
636+
let course = RouteBuilder::new().segment()?.build()?;
589637
assert_eq!(course.records, vec![]);
590638
Ok(())
591639
}
592640

593641
#[test]
594-
fn test_course_builder_single_point() -> Result<()> {
595-
let mut builder = CourseBuilder::new();
642+
fn test_route_builder_single_point() -> Result<()> {
643+
let mut builder = RouteBuilder::new();
596644
builder.with_route_point(geo_point!(1.0, 2.0));
597645
let record_points = builder
598646
.segment()?
@@ -609,8 +657,8 @@ mod tests {
609657
}
610658

611659
#[test]
612-
fn test_course_builder_two_points() -> Result<()> {
613-
let mut builder = CourseBuilder::new();
660+
fn test_route_builder_two_points() -> Result<()> {
661+
let mut builder = RouteBuilder::new();
614662
builder
615663
.with_route_point(geo_point!(1.0, 2.0))
616664
.with_route_point(geo_point!(1.1, 2.2));
@@ -630,7 +678,7 @@ mod tests {
630678

631679
#[test]
632680
fn test_repeated_points() -> Result<()> {
633-
let mut builder = CourseBuilder::new();
681+
let mut builder = RouteBuilder::new();
634682
builder
635683
.with_route_point(geo_point!(1.0, 2.0))
636684
.with_route_point(geo_point!(1.0, 2.0))
@@ -651,9 +699,9 @@ mod tests {
651699

652700
#[test]
653701
fn test_intercept_long_segments() -> Result<()> {
654-
let mut builder = CourseSetBuilder::new(CourseOptions::default());
702+
let mut builder = CourseSetBuilder::new(CourseSetOptions::default());
655703
builder
656-
.add_course()
704+
.add_route()
657705
.with_route_point(geo_point!(35.5252717091331, -101.2856451853322))
658706
.with_route_point(geo_point!(36.05200980326534, -90.02610043506964))
659707
.with_route_point(geo_point!(38.13369722302025, -78.51238236506529));

0 commit comments

Comments
 (0)