Skip to content

Commit 980a125

Browse files
committed
Add low level LrmScale implementation
Closes #3
1 parent 6d96c8d commit 980a125

File tree

2 files changed

+384
-0
lines changed

2 files changed

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

+382
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,382 @@
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+
/// Measurement along the curve. Typically in meters
7+
pub type CurvePosition = f64;
8+
9+
/// Measurement along the scale. Often in meters, but it could be anything
10+
pub type ScalePosition = f64;
11+
12+
/// Errors when manipulating a LrmScale
13+
#[derive(Error, Debug, PartialEq)]
14+
pub enum LrmScaleError {
15+
/// Returned when building a scale from a builder and less than 2 named anchors were provided
16+
#[error("a scale needs at least two named anchor")]
17+
NoEnoughNamedAnchor,
18+
/// All the named anchors must be unique within a same scale
19+
#[error("duplicated anchor: {0}")]
20+
DuplicatedAnchorName(String),
21+
/// Could not find the position on the curve as the anchor is not known
22+
#[error("anchor is unknown in the LrmScale")]
23+
UnknownAnchorName,
24+
/// Could not find an anchor that matches a given offset
25+
#[error("no anchor found")]
26+
NoAnchorFound,
27+
}
28+
29+
/// An anchor is a reference point that is well known from which the location is computed
30+
#[derive(PartialEq, Debug)]
31+
32+
pub struct Anchor {
33+
/// Some anchors might not be named
34+
/// e.g. the first anchor of the LRM
35+
pub id: Option<String>,
36+
37+
/// Distance from the start of the scale in the scale space. Can be negative
38+
pub scale_position: ScalePosition,
39+
40+
/// Real distance from the start of the curve
41+
/// The curve might not start at the same 0 (e.g. the curve is longer than the scale)
42+
/// or the curve might not progress at the same rate (e.g. the curve is a schematic representation that distorts distances)
43+
pub curve_position: CurvePosition,
44+
}
45+
46+
impl Anchor {
47+
/// Build a named anchor
48+
pub fn new(name: &str, scale_position: ScalePosition, curve_position: CurvePosition) -> Self {
49+
Self {
50+
id: Some(name.to_owned()),
51+
scale_position,
52+
curve_position,
53+
}
54+
}
55+
56+
/// Build an unnamed anchor
57+
pub fn new_unnamed(scale_position: ScalePosition, curve_position: CurvePosition) -> Self {
58+
Self {
59+
id: None,
60+
scale_position,
61+
curve_position,
62+
}
63+
}
64+
65+
fn as_named(&self) -> Option<NamedAnchor> {
66+
self.id.as_ref().map(|id| NamedAnchor {
67+
id: id.to_owned(),
68+
scale_position: self.scale_position,
69+
curve_position: self.curve_position,
70+
})
71+
}
72+
}
73+
74+
// Private struct to be used when we only deal with anchor that have names
75+
struct NamedAnchor {
76+
id: String,
77+
scale_position: ScalePosition,
78+
curve_position: CurvePosition,
79+
}
80+
81+
/// A helper to build a scale by adding consecutives anchors with relative distances
82+
/// When having all the anchors and their distances in both scale and real position,
83+
/// it is simpler to directly build the LrmScale from an Vec<Anchor>
84+
/// Using the builder will however ensure that the scale is valid
85+
pub struct ScaleBuilder {
86+
anchors: Vec<Anchor>,
87+
}
88+
89+
impl ScaleBuilder {
90+
/// Create a new scale with an initial anchor
91+
pub fn new(anchor: Anchor) -> Self {
92+
Self {
93+
anchors: vec![anchor],
94+
}
95+
}
96+
97+
/// Builds a named anchor and adds it to the scale builder
98+
/// Distances are relative to previous anchor
99+
pub fn add_named(self, id: &str, scale_dist: ScalePosition, curve_dist: CurvePosition) -> Self {
100+
self.add(Some(id.to_owned()), scale_dist, curve_dist)
101+
}
102+
103+
/// Builds an unnamed anchor and adds it to the scale builder
104+
/// Distances are relative to previous anchor
105+
pub fn add_unnamed(self, scale_dist: ScalePosition, curve_dist: CurvePosition) -> Self {
106+
self.add(None, scale_dist, curve_dist)
107+
}
108+
109+
/// Builds an anchor and adds it to the scale builder
110+
/// Distances are relative to previous anchor
111+
pub fn add(
112+
mut self,
113+
id: Option<String>,
114+
scale_dist: ScalePosition,
115+
curve_dist: CurvePosition,
116+
) -> Self {
117+
let last_anchor = self
118+
.anchors
119+
.last()
120+
.expect("The builder should have at least one anchor");
121+
122+
self.anchors.push(Anchor {
123+
id,
124+
scale_position: last_anchor.scale_position + scale_dist,
125+
curve_position: last_anchor.curve_position + curve_dist,
126+
});
127+
self
128+
}
129+
130+
/// Requires at least one named anchor
131+
/// Will fail if none is present and if the anchors names are duplicated
132+
/// This will consume the builder that can not be used after
133+
pub fn build(self) -> Result<LrmScale, LrmScaleError> {
134+
let mut names = std::collections::HashSet::new();
135+
for anchor in self.anchors.iter() {
136+
if let Some(name) = &anchor.id {
137+
if names.contains(&name) {
138+
return Err(LrmScaleError::DuplicatedAnchorName(name.to_string()));
139+
} else {
140+
names.insert(name);
141+
}
142+
}
143+
}
144+
145+
if names.is_empty() {
146+
Err(LrmScaleError::NoEnoughNamedAnchor)
147+
} else {
148+
Ok(LrmScale {
149+
anchors: self.anchors,
150+
})
151+
}
152+
}
153+
}
154+
155+
/// A measure defines a location on the LRM scale
156+
/// It is given as an anchor name and an offset on that scale
157+
/// It is often represented as 12+100 to say “100 scale units after the anchor 12”
158+
pub struct LrmMeasure {
159+
/// Name of the anchor. While it is often named after a kilometer position
160+
/// it can be anything (a letter, a landmark)
161+
anchor_name: String,
162+
/// The offset from the anchor in the scale units
163+
/// there is no guarantee that its value matches actual distance on the curve and is defined in scale units
164+
scale_offset: ScalePosition,
165+
}
166+
167+
impl LrmMeasure {
168+
/// Build a new LrmMeasure from an anchor name and the offset on the scale
169+
pub fn new(anchor_name: &str, scale_offset: ScalePosition) -> Self {
170+
Self {
171+
anchor_name: anchor_name.to_owned(),
172+
scale_offset,
173+
}
174+
}
175+
}
176+
177+
/// Represents an a LRM Scale and allows to map [Measure] to a position along a curve
178+
#[derive(PartialEq, Debug)]
179+
pub struct LrmScale {
180+
anchors: Vec<Anchor>,
181+
}
182+
183+
impl LrmScale {
184+
/// Locates a point along a curve given an anchor and a offset
185+
/// The offset might be negative
186+
pub fn locate_point(&self, measure: &LrmMeasure) -> Result<CurvePosition, LrmScaleError> {
187+
let named_anchor = self
188+
.iter_named()
189+
.find(|anchor| anchor.id == measure.anchor_name)
190+
.ok_or(LrmScaleError::UnknownAnchorName)?;
191+
let nearest_anchor = self
192+
.next_anchor(&named_anchor.id)
193+
.or_else(|| self.previous_anchor(&named_anchor.id))
194+
.ok_or(LrmScaleError::NoAnchorFound)?;
195+
196+
let scale_interval = named_anchor.scale_position - nearest_anchor.scale_position;
197+
let curve_interval = named_anchor.curve_position - nearest_anchor.curve_position;
198+
Ok(named_anchor.curve_position + curve_interval * measure.scale_offset / scale_interval)
199+
}
200+
201+
/// Returns a measure given a distance along the curve
202+
/// The corresponding anchor is the named anchor that gives the smallest positive offset
203+
/// If such an anchor does not exists, the first named anchor is used
204+
pub fn locate_anchor(
205+
&self,
206+
curve_position: CurvePosition,
207+
) -> Result<LrmMeasure, LrmScaleError> {
208+
// First we find the nearest named anchor to the curve
209+
let named_anchor = self
210+
.nearest_named(curve_position)
211+
.ok_or(LrmScaleError::NoAnchorFound)?;
212+
213+
// Then we search the nearest anchor that will be the reference
214+
// to convert from curve units to scale units
215+
let nearest_anchor = if named_anchor.curve_position < curve_position {
216+
self.next_anchor(&named_anchor.id)
217+
.or(self.previous_anchor(&named_anchor.id))
218+
} else {
219+
self.previous_anchor(&named_anchor.id)
220+
.or(self.next_anchor(&named_anchor.id))
221+
}
222+
.ok_or(LrmScaleError::NoAnchorFound)?;
223+
224+
let ratio = (nearest_anchor.scale_position - named_anchor.scale_position)
225+
/ (nearest_anchor.curve_position - named_anchor.curve_position);
226+
227+
Ok(LrmMeasure {
228+
anchor_name: named_anchor.id,
229+
scale_offset: (curve_position - named_anchor.curve_position) * ratio,
230+
})
231+
}
232+
233+
fn nearest_named(&self, curve_position: CurvePosition) -> Option<NamedAnchor> {
234+
// Tries to find the anchor whose curve_position is the biggest possible, yet smaller than curve position
235+
// Otherwise take the first named
236+
// Anchor names ----A----B----
237+
// Curve positions 2 3
238+
// With curve position = 2.5, we want A
239+
// 3.5, we want B
240+
// 1.5, we want A
241+
self.iter_named()
242+
.rev()
243+
.find(|anchor| anchor.curve_position <= curve_position)
244+
.or_else(|| self.iter_named().next())
245+
}
246+
247+
// Find the closest anchor before the anchor having the name `name`
248+
fn previous_anchor(&self, name: &str) -> Option<&Anchor> {
249+
self.anchors
250+
.iter()
251+
.rev()
252+
.skip_while(|anchor| anchor.id.as_deref() != Some(name))
253+
.nth(1)
254+
}
255+
256+
// Find the closest anchor after the anchor having the name `name`
257+
fn next_anchor(&self, name: &str) -> Option<&Anchor> {
258+
self.anchors
259+
.iter()
260+
.skip_while(|anchor| anchor.id.as_deref() != Some(name))
261+
.nth(1)
262+
}
263+
264+
// Iterates only on named anchors
265+
fn iter_named(&self) -> impl DoubleEndedIterator<Item = NamedAnchor> + '_ {
266+
self.anchors.iter().filter_map(|anchor| anchor.as_named())
267+
}
268+
}
269+
270+
#[cfg(test)]
271+
mod tests {
272+
use super::*;
273+
fn scale() -> LrmScale {
274+
ScaleBuilder::new(Anchor::new("a", 0., 0.))
275+
.add_named("b", 10., 100.)
276+
.build()
277+
.unwrap()
278+
}
279+
280+
#[test]
281+
fn builder() {
282+
// Everything as planed
283+
let scale = scale();
284+
assert_eq!(scale.anchors[0].curve_position, 0.);
285+
assert_eq!(scale.anchors[1].curve_position, 100.);
286+
287+
// Missing named anchor
288+
let b = ScaleBuilder::new(Anchor::new_unnamed(0., 0.));
289+
let scale = b.build();
290+
assert_eq!(scale, Err(LrmScaleError::NoEnoughNamedAnchor));
291+
292+
// Duplicated name
293+
let scale = ScaleBuilder::new(Anchor::new("a", 0., 0.))
294+
.add_named("a", 100., 100.)
295+
.build();
296+
assert_eq!(
297+
scale,
298+
Err(LrmScaleError::DuplicatedAnchorName("a".to_string()))
299+
);
300+
}
301+
302+
#[test]
303+
fn locate_point() {
304+
let scale = scale();
305+
306+
// Everything a usual
307+
assert_eq!(scale.locate_point(&LrmMeasure::new("a", 5.)), Ok(50.));
308+
assert_eq!(scale.locate_point(&LrmMeasure::new("b", 5.)), Ok(150.));
309+
310+
// Negative offsets
311+
assert_eq!(scale.locate_point(&LrmMeasure::new("a", -5.)), Ok(-50.));
312+
313+
// Unknown anchor
314+
assert_eq!(
315+
scale.locate_point(&LrmMeasure::new("c", 5.)),
316+
Err(LrmScaleError::UnknownAnchorName)
317+
);
318+
}
319+
320+
#[test]
321+
fn nearest_named() {
322+
let scale = ScaleBuilder::new(Anchor::new("a", 0., 2.))
323+
.add_named("b", 10., 1.)
324+
.build()
325+
.unwrap();
326+
327+
assert_eq!(scale.nearest_named(2.5).unwrap().id, "a");
328+
assert_eq!(scale.nearest_named(1.5).unwrap().id, "a");
329+
assert_eq!(scale.nearest_named(3.5).unwrap().id, "b");
330+
}
331+
332+
#[test]
333+
fn locate_anchor() {
334+
let scale = ScaleBuilder::new(Anchor::new("a", 0., 0.))
335+
.add_named("b", 10., 100.)
336+
.build()
337+
.unwrap();
338+
339+
let measure = scale.locate_anchor(40.).unwrap();
340+
assert_eq!(measure.anchor_name, "a");
341+
assert_eq!(measure.scale_offset, 4.);
342+
343+
let measure = scale.locate_anchor(150.).unwrap();
344+
assert_eq!(measure.anchor_name, "b");
345+
assert_eq!(measure.scale_offset, 5.);
346+
347+
let measure = scale.locate_anchor(-10.).unwrap();
348+
assert_eq!(measure.anchor_name, "a");
349+
assert_eq!(measure.scale_offset, -1.);
350+
}
351+
352+
#[test]
353+
fn locate_anchor_with_unnamed() {
354+
// ----Unnamed(100)----A(200)----B(300)----Unnamed(400)---
355+
let scale = ScaleBuilder::new(Anchor::new_unnamed(0., 100.))
356+
.add_named("a", 1., 100.)
357+
.add_named("b", 1., 100.)
358+
.add_unnamed(1., 100.)
359+
.build()
360+
.unwrap();
361+
362+
// Unamed----position----Named
363+
let measure = scale.locate_anchor(150.).unwrap();
364+
assert_eq!(measure.anchor_name, "a");
365+
assert_eq!(measure.scale_offset, -0.5);
366+
367+
// position----Unamed----Named
368+
let measure = scale.locate_anchor(50.).unwrap();
369+
assert_eq!(measure.anchor_name, "a");
370+
assert_eq!(measure.scale_offset, -1.5);
371+
372+
// Unamed----Named----position----Unamed
373+
let measure = scale.locate_anchor(350.).unwrap();
374+
assert_eq!(measure.anchor_name, "b");
375+
assert_eq!(measure.scale_offset, 0.5);
376+
377+
// Unamed----Named----Unamed----position
378+
let measure = scale.locate_anchor(500.).unwrap();
379+
assert_eq!(measure.anchor_name, "b");
380+
assert_eq!(measure.scale_offset, 2.);
381+
}
382+
}

0 commit comments

Comments
 (0)