Skip to content

Commit e555501

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

File tree

2 files changed

+391
-0
lines changed

2 files changed

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

+389
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,389 @@
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 an 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+
91+
pub struct ScaleBuilder {
92+
anchors: Vec<Anchor>,
93+
}
94+
95+
impl ScaleBuilder {
96+
/// Create a new scale with an initial anchor
97+
pub fn new(anchor: Anchor) -> Self {
98+
Self {
99+
anchors: vec![anchor],
100+
}
101+
}
102+
103+
/// Builds a named anchor and adds it to the scale builder
104+
/// Distances are relative to previous anchor
105+
pub fn add_named(self, id: &str, scale_dist: ScalePosition, curve_dist: CurvePosition) -> Self {
106+
self.add(Some(id.to_owned()), scale_dist, curve_dist)
107+
}
108+
109+
/// Builds an unnamed anchor and adds it to the scale builder
110+
/// Distances are relative to previous anchor
111+
pub fn add_unnamed(self, scale_dist: ScalePosition, curve_dist: CurvePosition) -> Self {
112+
self.add(None, scale_dist, curve_dist)
113+
}
114+
115+
/// Builds an anchor and adds it to the scale builder
116+
/// Distances are relative to previous anchor
117+
pub fn add(
118+
mut self,
119+
id: Option<String>,
120+
scale_dist: ScalePosition,
121+
curve_dist: CurvePosition,
122+
) -> Self {
123+
let last_anchor = self
124+
.anchors
125+
.last()
126+
.expect("The builder should have at least one anchor");
127+
128+
self.anchors.push(Anchor {
129+
id,
130+
scale_position: last_anchor.scale_position + scale_dist,
131+
curve_position: last_anchor.curve_position + curve_dist,
132+
});
133+
self
134+
}
135+
136+
/// Requires at least one named anchor
137+
/// Will fail if none is present and if the anchors names are duplicated
138+
/// This will consume the builder that can not be used after
139+
pub fn build(self) -> Result<LrmScale, LrmScaleError> {
140+
let mut names = std::collections::HashSet::new();
141+
for anchor in self.anchors.iter() {
142+
if let Some(name) = &anchor.id {
143+
if names.contains(&name) {
144+
return Err(LrmScaleError::DuplicatedAnchorName(name.to_string()));
145+
} else {
146+
names.insert(name);
147+
}
148+
}
149+
}
150+
151+
if names.is_empty() {
152+
Err(LrmScaleError::NoEnoughNamedAnchor)
153+
} else {
154+
Ok(LrmScale {
155+
anchors: self.anchors,
156+
})
157+
}
158+
}
159+
}
160+
161+
/// A measure defines a location on the LRM scale
162+
/// It is given as an anchor name and an offset on that scale
163+
/// It is often represented as 12+100 to say “100 scale units after the anchor 12”
164+
pub struct LrmMeasure {
165+
/// Name of the anchor. While it is often named after a kilometer position
166+
/// it can be anything (a letter, a landmark) and where it is named after a position,
167+
/// there is no guarantee that its value matches actual distance
168+
anchor_name: String,
169+
/// The offset from the anchor in the scale units
170+
scale_offset: ScalePosition,
171+
}
172+
173+
impl LrmMeasure {
174+
/// Build a new LrmMeasure from an anchor name and the offset on the scale
175+
pub fn new(anchor_name: &str, scale_offset: ScalePosition) -> Self {
176+
Self {
177+
anchor_name: anchor_name.to_owned(),
178+
scale_offset,
179+
}
180+
}
181+
}
182+
183+
/// Represents an a LRM Scale and allows to map [Measure] to a position along a curve
184+
#[derive(PartialEq, Debug)]
185+
pub struct LrmScale {
186+
// The anchors are milestones from which positions are measured
187+
anchors: Vec<Anchor>,
188+
}
189+
190+
impl LrmScale {
191+
/// Locates a point along a curve given an anchor and a offset
192+
/// The offset might be negative
193+
pub fn locate_point(&self, measure: &LrmMeasure) -> Result<CurvePosition, LrmScaleError> {
194+
let named_anchor = self
195+
.iter_named()
196+
.find(|anchor| anchor.id == measure.anchor_name)
197+
.ok_or(LrmScaleError::UnknownAnchorName)?;
198+
let nearest_anchor = self
199+
.next_anchor(&named_anchor.id)
200+
.or_else(|| self.previous_anchor(&named_anchor.id))
201+
.ok_or(LrmScaleError::NoAnchorFound)?;
202+
203+
let scale_interval = named_anchor.scale_position - nearest_anchor.scale_position;
204+
let curve_interval = named_anchor.curve_position - nearest_anchor.curve_position;
205+
Ok(named_anchor.curve_position + curve_interval * measure.scale_offset / scale_interval)
206+
}
207+
208+
/// Returns a measure given a distance along the curve
209+
/// The corresponding anchor is the named anchor that gives the smallest positive offset
210+
/// If such an anchor does not exists, the first named anchor is used
211+
pub fn locate_anchor(
212+
&self,
213+
curve_position: CurvePosition,
214+
) -> Result<LrmMeasure, LrmScaleError> {
215+
// First we find the nearest named anchor to the curve
216+
let named_anchor = self
217+
.nearest_named(curve_position)
218+
.ok_or(LrmScaleError::NoAnchorFound)?;
219+
220+
// Then we search the nearest anchor that will be the reference
221+
// to convert from curve units to scale units
222+
let nearest_anchor = if named_anchor.curve_position < curve_position {
223+
self.next_anchor(&named_anchor.id)
224+
.or(self.previous_anchor(&named_anchor.id))
225+
} else {
226+
self.previous_anchor(&named_anchor.id)
227+
.or(self.next_anchor(&named_anchor.id))
228+
}
229+
.ok_or(LrmScaleError::NoAnchorFound)?;
230+
231+
let ratio = (nearest_anchor.scale_position - named_anchor.scale_position)
232+
/ (nearest_anchor.curve_position - named_anchor.curve_position);
233+
234+
Ok(LrmMeasure {
235+
anchor_name: named_anchor.id,
236+
scale_offset: (curve_position - named_anchor.curve_position) * ratio,
237+
})
238+
}
239+
240+
fn nearest_named(&self, curve_position: CurvePosition) -> Option<NamedAnchor> {
241+
// Tries to find the anchor whose curve_position is the biggest possible, yet smaller than curve position
242+
// Otherwise take the first named
243+
// Anchor names ----A----B----
244+
// Curve positions 2 3
245+
// With curve position = 2.5, we want A
246+
// 3.5, we want B
247+
// 1.5, we want A
248+
self.iter_named()
249+
.rev()
250+
.find(|anchor| anchor.curve_position <= curve_position)
251+
.or_else(|| self.iter_named().next())
252+
}
253+
254+
// Find the closest anchor before the anchor having the name `name`
255+
fn previous_anchor(&self, name: &str) -> Option<&Anchor> {
256+
self.anchors
257+
.iter()
258+
.rev()
259+
.skip_while(|anchor| anchor.id.as_deref() != Some(name))
260+
.nth(1)
261+
}
262+
263+
// Find the closest anchor after the anchor having the name `name`
264+
fn next_anchor(&self, name: &str) -> Option<&Anchor> {
265+
self.anchors
266+
.iter()
267+
.skip_while(|anchor| anchor.id.as_deref() != Some(name))
268+
.nth(1)
269+
}
270+
271+
// Iterates only on named anchors
272+
fn iter_named(&self) -> impl DoubleEndedIterator<Item = NamedAnchor> + '_ {
273+
self.anchors.iter().filter_map(|anchor| anchor.as_named())
274+
}
275+
}
276+
277+
#[cfg(test)]
278+
mod tests {
279+
use super::*;
280+
fn scale() -> LrmScale {
281+
ScaleBuilder::new(Anchor::new("a", 0., 0.))
282+
.add_named("b", 10., 100.)
283+
.build()
284+
.unwrap()
285+
}
286+
287+
#[test]
288+
fn builder() {
289+
// Everything as planed
290+
let scale = scale();
291+
assert_eq!(scale.anchors[0].curve_position, 0.);
292+
assert_eq!(scale.anchors[1].curve_position, 100.);
293+
294+
// Missing named anchor
295+
let b = ScaleBuilder::new(Anchor::new_unnamed(0., 0.));
296+
let scale = b.build();
297+
assert_eq!(scale, Err(LrmScaleError::NoEnoughNamedAnchor));
298+
299+
// Duplicated name
300+
let scale = ScaleBuilder::new(Anchor::new("a", 0., 0.))
301+
.add_named("a", 100., 100.)
302+
.build();
303+
assert_eq!(
304+
scale,
305+
Err(LrmScaleError::DuplicatedAnchorName("a".to_string()))
306+
);
307+
}
308+
309+
#[test]
310+
fn locate_point() {
311+
let scale = scale();
312+
313+
// Everything a usual
314+
assert_eq!(scale.locate_point(&LrmMeasure::new("a", 5.)), Ok(50.));
315+
assert_eq!(scale.locate_point(&LrmMeasure::new("b", 5.)), Ok(150.));
316+
317+
// Negative offsets
318+
assert_eq!(scale.locate_point(&LrmMeasure::new("a", -5.)), Ok(-50.));
319+
320+
// Unknown anchor
321+
assert_eq!(
322+
scale.locate_point(&LrmMeasure::new("c", 5.)),
323+
Err(LrmScaleError::UnknownAnchorName)
324+
);
325+
}
326+
327+
#[test]
328+
fn nearest_named() {
329+
let scale = ScaleBuilder::new(Anchor::new("a", 0., 2.))
330+
.add_named("b", 10., 1.)
331+
.build()
332+
.unwrap();
333+
334+
assert_eq!(scale.nearest_named(2.5).unwrap().id, "a");
335+
assert_eq!(scale.nearest_named(1.5).unwrap().id, "a");
336+
assert_eq!(scale.nearest_named(3.5).unwrap().id, "b");
337+
}
338+
339+
#[test]
340+
fn locate_anchor() {
341+
let scale = ScaleBuilder::new(Anchor::new("a", 0., 0.))
342+
.add_named("b", 10., 100.)
343+
.build()
344+
.unwrap();
345+
346+
let measure = scale.locate_anchor(40.).unwrap();
347+
assert_eq!(measure.anchor_name, "a");
348+
assert_eq!(measure.scale_offset, 4.);
349+
350+
let measure = scale.locate_anchor(150.).unwrap();
351+
assert_eq!(measure.anchor_name, "b");
352+
assert_eq!(measure.scale_offset, 5.);
353+
354+
let measure = scale.locate_anchor(-10.).unwrap();
355+
assert_eq!(measure.anchor_name, "a");
356+
assert_eq!(measure.scale_offset, -1.);
357+
}
358+
359+
#[test]
360+
fn locate_anchor_with_unnamed() {
361+
// ----Unnamed(100)----A(200)----B(300)----Unnamed(400)---
362+
let scale = ScaleBuilder::new(Anchor::new_unnamed(0., 100.))
363+
.add_named("a", 1., 100.)
364+
.add_named("b", 1., 100.)
365+
.add_unnamed(1., 100.)
366+
.build()
367+
.unwrap();
368+
369+
// Unamed----position----Named
370+
let measure = scale.locate_anchor(150.).unwrap();
371+
assert_eq!(measure.anchor_name, "a");
372+
assert_eq!(measure.scale_offset, -0.5);
373+
374+
// position----Unamed----Named
375+
let measure = scale.locate_anchor(50.).unwrap();
376+
assert_eq!(measure.anchor_name, "a");
377+
assert_eq!(measure.scale_offset, -1.5);
378+
379+
// Unamed----Named----position----Unamed
380+
let measure = scale.locate_anchor(350.).unwrap();
381+
assert_eq!(measure.anchor_name, "b");
382+
assert_eq!(measure.scale_offset, 0.5);
383+
384+
// Unamed----Named----Unamed----position
385+
let measure = scale.locate_anchor(500.).unwrap();
386+
assert_eq!(measure.anchor_name, "b");
387+
assert_eq!(measure.scale_offset, 2.);
388+
}
389+
}

0 commit comments

Comments
 (0)