Skip to content

Commit 5e722a3

Browse files
committed
Add low level LrmScale implementation
1 parent 43fc0d4 commit 5e722a3

File tree

2 files changed

+245
-0
lines changed

2 files changed

+245
-0
lines changed

src/lib.rs

+2
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,8 @@ mod lrs_generated;
77

88
#[deny(missing_docs)]
99
pub mod curves;
10+
#[deny(missing_docs)]
11+
pub mod lrm;
1012
pub use lrs_generated::*;
1113

1214
#[test]

src/lrm.rs

+243
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,243 @@
1+
//! A LRM (linear reference model) is an abstract representation
2+
//! where the geometry and real distances are not considered
3+
4+
use thiserror::Error;
5+
6+
/// Errors when manipulating an LrmScale
7+
#[derive(Error, Debug, PartialEq)]
8+
pub enum LrmScaleError {
9+
/// Returned when building a scale from a builder and no name anchor was provided
10+
#[error("a scale needs at least one named anchor")]
11+
NoNamedAnchor,
12+
/// All the named anchors must be unique within a same scale
13+
#[error("duplicated anchor: {0}")]
14+
DuplicatedAnchorName(String),
15+
/// Could not find the position on the curve as the anchor is not known
16+
#[error("anchor is unknown in the LrmScale")]
17+
UnknownAnchorName,
18+
/// Could not find an anchor that matches a given offset
19+
#[error("no anchor found")]
20+
NoAnchorFound,
21+
}
22+
23+
/// An anchor is a reference point that is well known from which the location is computed
24+
#[derive(PartialEq, Debug)]
25+
26+
pub struct Anchor {
27+
/// Some anchors might not be named
28+
/// e.g. the first anchor of the LRM
29+
pub id: Option<String>,
30+
/// Offset to the start of the scale. Can be negative
31+
pub offset: f64,
32+
}
33+
34+
impl Anchor {
35+
/// Build a named anchor
36+
pub fn new(name: &str, offset: f64) -> Self {
37+
Self {
38+
id: Some(name.to_owned()),
39+
offset,
40+
}
41+
}
42+
43+
/// Build an unamed anchor
44+
pub fn new_unamed(offset: f64) -> Self {
45+
Self { id: None, offset }
46+
}
47+
}
48+
49+
/// A helper to build a scale by adding consecutives anchors
50+
51+
pub struct ScaleBuilder {
52+
anchors: Vec<Anchor>,
53+
cummulative_distances: Vec<f64>,
54+
}
55+
56+
impl ScaleBuilder {
57+
/// Create a new builder with the initial anchor
58+
pub fn new(start: Anchor) -> Self {
59+
Self {
60+
anchors: vec![start],
61+
cummulative_distances: vec![0.],
62+
}
63+
}
64+
/// Distance_to_previous is measured along a curve
65+
/// The curve might not start at the same 0 (e.g. the curve is longer than the scale)
66+
/// or the curve might not progress at the same rate (e.g. the curve is a schematic representation that distorts distances)
67+
pub fn add(&mut self, anchor: Anchor, distance_to_previous: f64) -> &mut Self {
68+
self.anchors.push(anchor);
69+
let last_distance = self.cummulative_distances.last().unwrap_or(&0.);
70+
self.cummulative_distances
71+
.push(distance_to_previous + last_distance);
72+
self
73+
}
74+
75+
/// Requires at least one named anchor
76+
/// Will fail if none is present and if the anchors names are duplicated
77+
/// This will consume the builder that can not be used after
78+
pub fn build(self) -> Result<LrmScale, LrmScaleError> {
79+
let mut names = std::collections::HashSet::new();
80+
for anchor in self.anchors.iter() {
81+
if let Some(name) = &anchor.id {
82+
if names.contains(&name) {
83+
return Err(LrmScaleError::DuplicatedAnchorName(name.to_string()));
84+
} else {
85+
names.insert(name);
86+
}
87+
}
88+
}
89+
90+
if names.is_empty() {
91+
Err(LrmScaleError::NoNamedAnchor)
92+
} else {
93+
Ok(LrmScale {
94+
anchors: self.anchors,
95+
cummulated_distances: self.cummulative_distances,
96+
})
97+
}
98+
}
99+
}
100+
101+
/// A measure defines a location on the LRM scale
102+
/// It is given as an anchor name and an offset on that scale
103+
/// It is often represented as 12+100 to say “100 meters after the anchor 12”
104+
pub struct LrmMeasure {
105+
/// Name of the anchor. While it is often named after a kilometer position
106+
/// it can be anything (a letter, a landmark) and where it is named after a position,
107+
/// there is no guarantee that its value matches actual distance
108+
anchor_name: String,
109+
/// The offset from the anchor
110+
offset: f64,
111+
}
112+
113+
/// Represents an a LRM Scale and allows to map [Measure] to a position along a curve
114+
#[derive(PartialEq, Debug)]
115+
pub struct LrmScale {
116+
// The anchors are milestones from which positions are measured
117+
anchors: Vec<Anchor>,
118+
// The distances between anchors
119+
// Implementation detail: the first distance is 0.
120+
// This allows to have two vectors of the same size and iterate by zipping them
121+
cummulated_distances: Vec<f64>,
122+
}
123+
124+
impl LrmScale {
125+
/// Locates a point along a curve given an anchor and a offset
126+
/// The offset might be negative
127+
pub fn locate_point(&self, measure: LrmMeasure) -> Result<f64, LrmScaleError> {
128+
self.iter_named()
129+
.find(|(anchor_id, _distance)| anchor_id == &&measure.anchor_name)
130+
.map(|(_anchor_id, distance)| distance + measure.offset)
131+
.ok_or(LrmScaleError::UnknownAnchorName)
132+
}
133+
134+
/// Returns a measure given a distance along the curve
135+
/// The corresponding anchor is the named anchor that gives the smallest positive offset
136+
/// If such an anchor does not exists, the first named anchor is used
137+
pub fn locate_anchor(&self, offset: f64) -> Result<LrmMeasure, LrmScaleError> {
138+
let smallest_measure = self
139+
.iter_named()
140+
.rev()
141+
.find(|(_anchor_id, distance)| distance < &&offset)
142+
.map(|(anchor_id, distance)| LrmMeasure {
143+
anchor_name: anchor_id.to_owned(),
144+
offset: offset - distance,
145+
});
146+
147+
// If we found a solution, let’s return it
148+
// Otherwise we use the first named anchor with a negative offset
149+
if let Some(measure) = smallest_measure {
150+
Ok(measure)
151+
} else {
152+
self.iter_named()
153+
.next()
154+
.map(|(anchor_id, distance)| LrmMeasure {
155+
anchor_name: anchor_id.to_owned(),
156+
offset: offset - distance,
157+
})
158+
.ok_or(LrmScaleError::NoAnchorFound)
159+
}
160+
}
161+
162+
// Private helper function that iterates only over named anchors names and their cummulated distances
163+
fn iter_named(&self) -> impl DoubleEndedIterator<Item = (&String, &f64)> {
164+
self.anchors
165+
.iter()
166+
.zip(self.cummulated_distances.iter())
167+
.filter_map(|(anchor, distance)| anchor.id.as_ref().map(|id| (id, distance)))
168+
}
169+
}
170+
171+
#[cfg(test)]
172+
mod tests {
173+
use super::*;
174+
fn scale() -> LrmScale {
175+
let mut b = ScaleBuilder::new(Anchor::new("a", 0.));
176+
b.add(Anchor::new("b", 100.), 100.);
177+
b.build().unwrap()
178+
}
179+
180+
#[test]
181+
fn builder() {
182+
// Everything as planed
183+
let scale = scale();
184+
assert_eq!(scale.cummulated_distances, vec![0., 100.]);
185+
186+
// Missing named anchor
187+
let b = ScaleBuilder::new(Anchor::new_unamed(0.));
188+
let scale = b.build();
189+
assert_eq!(scale, Err(LrmScaleError::NoNamedAnchor));
190+
191+
// Duplicated name
192+
let mut b = ScaleBuilder::new(Anchor::new("a", 0.));
193+
b.add(Anchor::new("a", 100.), 100.);
194+
let scale = b.build();
195+
assert_eq!(
196+
scale,
197+
Err(LrmScaleError::DuplicatedAnchorName("a".to_string()))
198+
);
199+
}
200+
201+
#[test]
202+
fn locate_point() {
203+
let scale = scale();
204+
205+
// Everything a usual
206+
let p = scale.locate_point(LrmMeasure {
207+
anchor_name: "b".to_string(),
208+
offset: 50.,
209+
});
210+
assert_eq!(p, Ok(150.));
211+
212+
// Negative offsets
213+
let p = scale.locate_point(LrmMeasure {
214+
anchor_name: "a".to_string(),
215+
offset: -50.,
216+
});
217+
assert_eq!(p, Ok(-50.));
218+
219+
// Unknown anchor
220+
let p = scale.locate_point(LrmMeasure {
221+
anchor_name: "c".to_string(),
222+
offset: 50.,
223+
});
224+
assert_eq!(p, Err(LrmScaleError::UnknownAnchorName));
225+
}
226+
227+
#[test]
228+
fn locate_anchor() {
229+
let scale = scale();
230+
231+
let anchor = scale.locate_anchor(150.).unwrap();
232+
assert_eq!(anchor.anchor_name, "b");
233+
assert_eq!(anchor.offset, 50.);
234+
235+
let anchor = scale.locate_anchor(40.).unwrap();
236+
assert_eq!(anchor.anchor_name, "a");
237+
assert_eq!(anchor.offset, 40.);
238+
239+
let anchor = scale.locate_anchor(-10.).unwrap();
240+
assert_eq!(anchor.anchor_name, "a");
241+
assert_eq!(anchor.offset, -10.);
242+
}
243+
}

0 commit comments

Comments
 (0)