Skip to content

Commit e86c8af

Browse files
committed
Add low level LrmScale implementation
1 parent 6d96c8d commit e86c8af

File tree

2 files changed

+286
-0
lines changed

2 files changed

+286
-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

+284
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,284 @@
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+
31+
/// Distance from the start of the scale in the scale space. Can be negative
32+
pub scale_position: f64,
33+
34+
/// Real distance from the start of the curve
35+
/// The curve might not start at the same 0 (e.g. the curve is longer than the scale)
36+
/// or the curve might not progress at the same rate (e.g. the curve is a schematic representation that distorts distances)
37+
pub curve_position: f64,
38+
}
39+
40+
// Private struct to be used when we only deal with anchor that have names
41+
struct NamedAnchor {
42+
id: String,
43+
scale_position: f64,
44+
curve_position: f64,
45+
}
46+
47+
impl Anchor {
48+
/// Build a named anchor
49+
pub fn new(name: &str, scale_position: f64, curve_position: f64) -> Self {
50+
Self {
51+
id: Some(name.to_owned()),
52+
scale_position,
53+
curve_position,
54+
}
55+
}
56+
57+
/// Build an unamed anchor
58+
pub fn new_unamed(scale_position: f64, curve_position: f64) -> Self {
59+
Self {
60+
id: None,
61+
scale_position,
62+
curve_position,
63+
}
64+
}
65+
}
66+
67+
/// A helper to build a scale by adding consecutives anchors with relative distances
68+
/// When having all the anchors and their distances in both scale and real position,
69+
/// it is simpler to directly build the LrmScale from an Vec<Anchor>
70+
71+
pub struct ScaleBuilder {
72+
anchors: Vec<Anchor>,
73+
}
74+
75+
impl ScaleBuilder {
76+
/// Create a new scale with an initial anchor
77+
pub fn new(anchor: Anchor) -> Self {
78+
Self {
79+
anchors: vec![anchor],
80+
}
81+
}
82+
83+
/// Create a new builder with an initial named anchor, and initial positions at 0
84+
pub fn new_named(first_anchor_name: &str) -> Self {
85+
Self::new(Anchor {
86+
id: Some(first_anchor_name.to_string()),
87+
scale_position: 0.,
88+
curve_position: 0.,
89+
})
90+
}
91+
92+
/// distance_to_previous is the value in the scale it might differ from the real distance measured
93+
/// When real_distance is None, they are considered to be equal
94+
pub fn add(
95+
&mut self,
96+
anchor_name: &str,
97+
distance_to_previous: f64,
98+
real_distance: Option<f64>,
99+
) -> &mut Self {
100+
let (last_distance, last_real_distance) = self
101+
.anchors
102+
.last()
103+
.map(|anchor| (anchor.scale_position, anchor.curve_position))
104+
.unwrap_or((0., 0.));
105+
106+
self.anchors.push(Anchor {
107+
id: Some(anchor_name.to_owned()),
108+
scale_position: last_distance + distance_to_previous,
109+
curve_position: last_real_distance + real_distance.unwrap_or(distance_to_previous),
110+
});
111+
self
112+
}
113+
114+
/// Requires at least one named anchor
115+
/// Will fail if none is present and if the anchors names are duplicated
116+
/// This will consume the builder that can not be used after
117+
pub fn build(self) -> Result<LrmScale, LrmScaleError> {
118+
let mut names = std::collections::HashSet::new();
119+
for anchor in self.anchors.iter() {
120+
if let Some(name) = &anchor.id {
121+
if names.contains(&name) {
122+
return Err(LrmScaleError::DuplicatedAnchorName(name.to_string()));
123+
} else {
124+
names.insert(name);
125+
}
126+
}
127+
}
128+
129+
if names.is_empty() {
130+
Err(LrmScaleError::NoNamedAnchor)
131+
} else {
132+
Ok(LrmScale {
133+
anchors: self.anchors,
134+
})
135+
}
136+
}
137+
}
138+
139+
/// A measure defines a location on the LRM scale
140+
/// It is given as an anchor name and an offset on that scale
141+
/// It is often represented as 12+100 to say “100 meters after the anchor 12”
142+
pub struct LrmMeasure {
143+
/// Name of the anchor. While it is often named after a kilometer position
144+
/// it can be anything (a letter, a landmark) and where it is named after a position,
145+
/// there is no guarantee that its value matches actual distance
146+
anchor_name: String,
147+
/// The offset from the anchor
148+
offset: f64,
149+
}
150+
151+
/// Represents an a LRM Scale and allows to map [Measure] to a position along a curve
152+
#[derive(PartialEq, Debug)]
153+
pub struct LrmScale {
154+
// The anchors are milestones from which positions are measured
155+
anchors: Vec<Anchor>,
156+
}
157+
158+
impl LrmScale {
159+
/// Locates a point along a curve given an anchor and a offset
160+
/// The offset might be negative
161+
pub fn locate_point(&self, measure: LrmMeasure) -> Result<f64, LrmScaleError> {
162+
self.iter_named()
163+
.find(|anchor| anchor.id == measure.anchor_name)
164+
.map(|anchor| anchor.curve_position + measure.offset)
165+
.ok_or(LrmScaleError::UnknownAnchorName)
166+
}
167+
168+
/// Returns a measure given a distance along the curve
169+
/// The corresponding anchor is the named anchor that gives the smallest positive offset
170+
/// If such an anchor does not exists, the first named anchor is used
171+
pub fn locate_anchor(&self, curve_position: f64) -> Result<LrmMeasure, LrmScaleError> {
172+
let smallest_measure = self
173+
.iter_named()
174+
.rev()
175+
.find(|anchor| anchor.curve_position < curve_position)
176+
.map(|anchor| LrmMeasure {
177+
anchor_name: anchor.id,
178+
offset: curve_position - anchor.curve_position,
179+
});
180+
181+
// If we found a solution, let’s return it
182+
// Otherwise we use the first named anchor with a negative offset
183+
if let Some(measure) = smallest_measure {
184+
Ok(measure)
185+
} else {
186+
self.iter_named()
187+
.next()
188+
.map(|anchor| LrmMeasure {
189+
anchor_name: anchor.id,
190+
offset: curve_position - anchor.curve_position,
191+
})
192+
.ok_or(LrmScaleError::NoAnchorFound)
193+
}
194+
}
195+
196+
// Private helper function that iterates only over named anchors names and their cummulated distances
197+
fn iter_named(&self) -> impl DoubleEndedIterator<Item = NamedAnchor> + '_ {
198+
self.anchors.iter().filter_map(|anchor| {
199+
anchor.id.as_ref().map(|id| NamedAnchor {
200+
id: id.to_owned(),
201+
scale_position: anchor.scale_position,
202+
curve_position: anchor.curve_position,
203+
})
204+
})
205+
}
206+
}
207+
208+
#[cfg(test)]
209+
mod tests {
210+
use super::*;
211+
fn scale() -> LrmScale {
212+
let mut b = ScaleBuilder::new(Anchor::new("a", 0., 0.));
213+
b.add("b", 100., None);
214+
b.build().unwrap()
215+
}
216+
217+
#[test]
218+
fn builder() {
219+
// Everything as planed
220+
let scale = scale();
221+
assert_eq!(scale.anchors[0].curve_position, 0.);
222+
assert_eq!(scale.anchors[1].curve_position, 100.);
223+
224+
// Missing named anchor
225+
let b = ScaleBuilder::new(Anchor::new_unamed(0., 0.));
226+
let scale = b.build();
227+
assert_eq!(scale, Err(LrmScaleError::NoNamedAnchor));
228+
229+
// Duplicated name
230+
let mut b = ScaleBuilder::new(Anchor::new("a", 0., 0.));
231+
b.add("a", 100., None);
232+
let scale = b.build();
233+
assert_eq!(
234+
scale,
235+
Err(LrmScaleError::DuplicatedAnchorName("a".to_string()))
236+
);
237+
}
238+
239+
#[test]
240+
fn locate_point() {
241+
let scale = scale();
242+
243+
// Everything a usual
244+
let p = scale.locate_point(LrmMeasure {
245+
anchor_name: "b".to_string(),
246+
offset: 50.,
247+
});
248+
assert_eq!(p, Ok(150.));
249+
250+
// Negative offsets
251+
let p = scale.locate_point(LrmMeasure {
252+
anchor_name: "a".to_string(),
253+
offset: -50.,
254+
});
255+
assert_eq!(p, Ok(-50.));
256+
257+
// Unknown anchor
258+
let p = scale.locate_point(LrmMeasure {
259+
anchor_name: "c".to_string(),
260+
offset: 50.,
261+
});
262+
assert_eq!(p, Err(LrmScaleError::UnknownAnchorName));
263+
}
264+
265+
#[test]
266+
fn locate_anchor() {
267+
let mut b = ScaleBuilder::new(Anchor::new("a", 0., 0.));
268+
b.add("b", 100., None);
269+
b.add("c", 200., Some(1000.));
270+
let scale = b.build().unwrap();
271+
272+
let measure = scale.locate_anchor(150.).unwrap();
273+
assert_eq!(measure.anchor_name, "b");
274+
assert_eq!(measure.offset, 50.);
275+
276+
let measure = scale.locate_anchor(40.).unwrap();
277+
assert_eq!(measure.anchor_name, "a");
278+
assert_eq!(measure.offset, 40.);
279+
280+
let measure = scale.locate_anchor(-10.).unwrap();
281+
assert_eq!(measure.anchor_name, "a");
282+
assert_eq!(measure.offset, -10.);
283+
}
284+
}

0 commit comments

Comments
 (0)