Skip to content

Commit a0b89c4

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

File tree

2 files changed

+389
-0
lines changed

2 files changed

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

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

0 commit comments

Comments
 (0)