-
Notifications
You must be signed in to change notification settings - Fork 2
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Add low level LrmScale implementation
Closes #3
- Loading branch information
Showing
2 changed files
with
389 additions
and
0 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,387 @@ | ||
//! A LRM (linear reference model) is an abstract representation | ||
//! where the geometry and real distances are not considered | ||
/* | ||
On doit avoir au moins deux ancres | ||
produits en croix entre nommées et non nommées | ||
si on est avant, on peut être négatif | ||
*/ | ||
use thiserror::Error; | ||
|
||
/// Measurement along the curve. Typically in meters | ||
pub type CurvePosition = f64; | ||
|
||
/// Measurement along the scale. Often in meters, but it could be anything | ||
pub type ScalePosition = f64; | ||
|
||
/// Errors when manipulating a LrmScale | ||
#[derive(Error, Debug, PartialEq)] | ||
pub enum LrmScaleError { | ||
/// Returned when building a scale from a builder and less than 2 named anchors were provided | ||
#[error("a scale needs at least two named anchor")] | ||
NoEnoughNamedAnchor, | ||
/// All the named anchors must be unique within a same scale | ||
#[error("duplicated anchor: {0}")] | ||
DuplicatedAnchorName(String), | ||
/// Could not find the position on the curve as the anchor is not known | ||
#[error("anchor is unknown in the LrmScale")] | ||
UnknownAnchorName, | ||
/// Could not find an anchor that matches a given offset | ||
#[error("no anchor found")] | ||
NoAnchorFound, | ||
} | ||
|
||
/// An anchor is a reference point that is well known from which the location is computed | ||
#[derive(PartialEq, Debug)] | ||
|
||
pub struct Anchor { | ||
/// Some anchors might not be named | ||
/// e.g. the first anchor of the LRM | ||
pub id: Option<String>, | ||
|
||
/// Distance from the start of the scale in the scale space. Can be negative | ||
pub scale_position: ScalePosition, | ||
|
||
/// Real distance from the start of the curve | ||
/// The curve might not start at the same 0 (e.g. the curve is longer than the scale) | ||
/// or the curve might not progress at the same rate (e.g. the curve is a schematic representation that distorts distances) | ||
pub curve_position: CurvePosition, | ||
} | ||
|
||
impl Anchor { | ||
/// Build a named anchor | ||
pub fn new(name: &str, scale_position: ScalePosition, curve_position: CurvePosition) -> Self { | ||
Self { | ||
id: Some(name.to_owned()), | ||
scale_position, | ||
curve_position, | ||
} | ||
} | ||
|
||
/// Build an unnamed anchor | ||
pub fn new_unnamed(scale_position: ScalePosition, curve_position: CurvePosition) -> Self { | ||
Self { | ||
id: None, | ||
scale_position, | ||
curve_position, | ||
} | ||
} | ||
|
||
fn as_named(&self) -> Option<NamedAnchor> { | ||
self.id.as_ref().map(|id| NamedAnchor { | ||
id: id.to_owned(), | ||
scale_position: self.scale_position, | ||
curve_position: self.curve_position, | ||
}) | ||
} | ||
} | ||
|
||
// Private struct to be used when we only deal with anchor that have names | ||
struct NamedAnchor { | ||
id: String, | ||
scale_position: ScalePosition, | ||
curve_position: CurvePosition, | ||
} | ||
|
||
/// A helper to build a scale by adding consecutives anchors with relative distances | ||
/// When having all the anchors and their distances in both scale and real position, | ||
/// it is simpler to directly build the LrmScale from an Vec<Anchor> | ||
/// Using the builder will however ensure that the scale is valid | ||
pub struct ScaleBuilder { | ||
anchors: Vec<Anchor>, | ||
} | ||
|
||
impl ScaleBuilder { | ||
/// Create a new scale with an initial anchor | ||
pub fn new(anchor: Anchor) -> Self { | ||
Self { | ||
anchors: vec![anchor], | ||
} | ||
} | ||
|
||
/// Builds a named anchor and adds it to the scale builder | ||
/// Distances are relative to previous anchor | ||
pub fn add_named(self, id: &str, scale_dist: ScalePosition, curve_dist: CurvePosition) -> Self { | ||
self.add(Some(id.to_owned()), scale_dist, curve_dist) | ||
} | ||
|
||
/// Builds an unnamed anchor and adds it to the scale builder | ||
/// Distances are relative to previous anchor | ||
pub fn add_unnamed(self, scale_dist: ScalePosition, curve_dist: CurvePosition) -> Self { | ||
self.add(None, scale_dist, curve_dist) | ||
} | ||
|
||
/// Builds an anchor and adds it to the scale builder | ||
/// Distances are relative to previous anchor | ||
pub fn add( | ||
mut self, | ||
id: Option<String>, | ||
scale_dist: ScalePosition, | ||
curve_dist: CurvePosition, | ||
) -> Self { | ||
let last_anchor = self | ||
.anchors | ||
.last() | ||
.expect("The builder should have at least one anchor"); | ||
|
||
self.anchors.push(Anchor { | ||
id, | ||
scale_position: last_anchor.scale_position + scale_dist, | ||
curve_position: last_anchor.curve_position + curve_dist, | ||
}); | ||
self | ||
} | ||
|
||
/// Requires at least one named anchor | ||
/// Will fail if none is present and if the anchors names are duplicated | ||
/// This will consume the builder that can not be used after | ||
pub fn build(self) -> Result<LrmScale, LrmScaleError> { | ||
let mut names = std::collections::HashSet::new(); | ||
for anchor in self.anchors.iter() { | ||
if let Some(name) = &anchor.id { | ||
if names.contains(&name) { | ||
return Err(LrmScaleError::DuplicatedAnchorName(name.to_string())); | ||
} else { | ||
names.insert(name); | ||
} | ||
} | ||
} | ||
|
||
if names.is_empty() { | ||
Err(LrmScaleError::NoEnoughNamedAnchor) | ||
} else { | ||
Ok(LrmScale { | ||
anchors: self.anchors, | ||
}) | ||
} | ||
} | ||
} | ||
|
||
/// A measure defines a location on the LRM scale | ||
/// It is given as an anchor name and an offset on that scale | ||
/// It is often represented as 12+100 to say “100 scale units after the anchor 12” | ||
pub struct LrmMeasure { | ||
/// Name of the anchor. While it is often named after a kilometer position | ||
/// it can be anything (a letter, a landmark) | ||
anchor_name: String, | ||
/// The offset from the anchor in the scale units | ||
/// there is no guarantee that its value matches actual distance on the curve and is defined in scale units | ||
scale_offset: ScalePosition, | ||
} | ||
|
||
impl LrmMeasure { | ||
/// Build a new LrmMeasure from an anchor name and the offset on the scale | ||
pub fn new(anchor_name: &str, scale_offset: ScalePosition) -> Self { | ||
Self { | ||
anchor_name: anchor_name.to_owned(), | ||
scale_offset, | ||
} | ||
} | ||
} | ||
|
||
/// Represents an a LRM Scale and allows to map [Measure] to a position along a curve | ||
#[derive(PartialEq, Debug)] | ||
pub struct LrmScale { | ||
anchors: Vec<Anchor>, | ||
} | ||
|
||
impl LrmScale { | ||
/// Locates a point along a curve given an anchor and a offset | ||
/// The offset might be negative | ||
pub fn locate_point(&self, measure: &LrmMeasure) -> Result<CurvePosition, LrmScaleError> { | ||
let named_anchor = self | ||
.iter_named() | ||
.find(|anchor| anchor.id == measure.anchor_name) | ||
.ok_or(LrmScaleError::UnknownAnchorName)?; | ||
let nearest_anchor = self | ||
.next_anchor(&named_anchor.id) | ||
.or_else(|| self.previous_anchor(&named_anchor.id)) | ||
.ok_or(LrmScaleError::NoAnchorFound)?; | ||
|
||
let scale_interval = named_anchor.scale_position - nearest_anchor.scale_position; | ||
let curve_interval = named_anchor.curve_position - nearest_anchor.curve_position; | ||
Ok(named_anchor.curve_position + curve_interval * measure.scale_offset / scale_interval) | ||
} | ||
|
||
/// Returns a measure given a distance along the curve | ||
/// The corresponding anchor is the named anchor that gives the smallest positive offset | ||
/// If such an anchor does not exists, the first named anchor is used | ||
pub fn locate_anchor( | ||
&self, | ||
curve_position: CurvePosition, | ||
) -> Result<LrmMeasure, LrmScaleError> { | ||
// First we find the nearest named anchor to the curve | ||
let named_anchor = self | ||
.nearest_named(curve_position) | ||
.ok_or(LrmScaleError::NoAnchorFound)?; | ||
|
||
// Then we search the nearest anchor that will be the reference | ||
// to convert from curve units to scale units | ||
let nearest_anchor = if named_anchor.curve_position < curve_position { | ||
self.next_anchor(&named_anchor.id) | ||
.or(self.previous_anchor(&named_anchor.id)) | ||
} else { | ||
self.previous_anchor(&named_anchor.id) | ||
.or(self.next_anchor(&named_anchor.id)) | ||
} | ||
.ok_or(LrmScaleError::NoAnchorFound)?; | ||
|
||
let ratio = (nearest_anchor.scale_position - named_anchor.scale_position) | ||
/ (nearest_anchor.curve_position - named_anchor.curve_position); | ||
|
||
Ok(LrmMeasure { | ||
anchor_name: named_anchor.id, | ||
scale_offset: (curve_position - named_anchor.curve_position) * ratio, | ||
}) | ||
} | ||
|
||
fn nearest_named(&self, curve_position: CurvePosition) -> Option<NamedAnchor> { | ||
// Tries to find the anchor whose curve_position is the biggest possible, yet smaller than curve position | ||
// Otherwise take the first named | ||
// Anchor names ----A----B---- | ||
// Curve positions 2 3 | ||
// With curve position = 2.5, we want A | ||
// 3.5, we want B | ||
// 1.5, we want A | ||
self.iter_named() | ||
.rev() | ||
.find(|anchor| anchor.curve_position <= curve_position) | ||
.or_else(|| self.iter_named().next()) | ||
} | ||
|
||
// Find the closest anchor before the anchor having the name `name` | ||
fn previous_anchor(&self, name: &str) -> Option<&Anchor> { | ||
self.anchors | ||
.iter() | ||
.rev() | ||
.skip_while(|anchor| anchor.id.as_deref() != Some(name)) | ||
.nth(1) | ||
} | ||
|
||
// Find the closest anchor after the anchor having the name `name` | ||
fn next_anchor(&self, name: &str) -> Option<&Anchor> { | ||
self.anchors | ||
.iter() | ||
.skip_while(|anchor| anchor.id.as_deref() != Some(name)) | ||
.nth(1) | ||
} | ||
|
||
// Iterates only on named anchors | ||
fn iter_named(&self) -> impl DoubleEndedIterator<Item = NamedAnchor> + '_ { | ||
self.anchors.iter().filter_map(|anchor| anchor.as_named()) | ||
} | ||
} | ||
|
||
#[cfg(test)] | ||
mod tests { | ||
use super::*; | ||
fn scale() -> LrmScale { | ||
ScaleBuilder::new(Anchor::new("a", 0., 0.)) | ||
.add_named("b", 10., 100.) | ||
.build() | ||
.unwrap() | ||
} | ||
|
||
#[test] | ||
fn builder() { | ||
// Everything as planed | ||
let scale = scale(); | ||
assert_eq!(scale.anchors[0].curve_position, 0.); | ||
assert_eq!(scale.anchors[1].curve_position, 100.); | ||
|
||
// Missing named anchor | ||
let b = ScaleBuilder::new(Anchor::new_unnamed(0., 0.)); | ||
let scale = b.build(); | ||
assert_eq!(scale, Err(LrmScaleError::NoEnoughNamedAnchor)); | ||
|
||
// Duplicated name | ||
let scale = ScaleBuilder::new(Anchor::new("a", 0., 0.)) | ||
.add_named("a", 100., 100.) | ||
.build(); | ||
assert_eq!( | ||
scale, | ||
Err(LrmScaleError::DuplicatedAnchorName("a".to_string())) | ||
); | ||
} | ||
|
||
#[test] | ||
fn locate_point() { | ||
let scale = scale(); | ||
|
||
// Everything a usual | ||
assert_eq!(scale.locate_point(&LrmMeasure::new("a", 5.)), Ok(50.)); | ||
assert_eq!(scale.locate_point(&LrmMeasure::new("b", 5.)), Ok(150.)); | ||
|
||
// Negative offsets | ||
assert_eq!(scale.locate_point(&LrmMeasure::new("a", -5.)), Ok(-50.)); | ||
|
||
// Unknown anchor | ||
assert_eq!( | ||
scale.locate_point(&LrmMeasure::new("c", 5.)), | ||
Err(LrmScaleError::UnknownAnchorName) | ||
); | ||
} | ||
|
||
#[test] | ||
fn nearest_named() { | ||
let scale = ScaleBuilder::new(Anchor::new("a", 0., 2.)) | ||
.add_named("b", 10., 1.) | ||
.build() | ||
.unwrap(); | ||
|
||
assert_eq!(scale.nearest_named(2.5).unwrap().id, "a"); | ||
assert_eq!(scale.nearest_named(1.5).unwrap().id, "a"); | ||
assert_eq!(scale.nearest_named(3.5).unwrap().id, "b"); | ||
} | ||
|
||
#[test] | ||
fn locate_anchor() { | ||
let scale = ScaleBuilder::new(Anchor::new("a", 0., 0.)) | ||
.add_named("b", 10., 100.) | ||
.build() | ||
.unwrap(); | ||
|
||
let measure = scale.locate_anchor(40.).unwrap(); | ||
assert_eq!(measure.anchor_name, "a"); | ||
assert_eq!(measure.scale_offset, 4.); | ||
|
||
let measure = scale.locate_anchor(150.).unwrap(); | ||
assert_eq!(measure.anchor_name, "b"); | ||
assert_eq!(measure.scale_offset, 5.); | ||
|
||
let measure = scale.locate_anchor(-10.).unwrap(); | ||
assert_eq!(measure.anchor_name, "a"); | ||
assert_eq!(measure.scale_offset, -1.); | ||
} | ||
|
||
#[test] | ||
fn locate_anchor_with_unnamed() { | ||
// ----Unnamed(100)----A(200)----B(300)----Unnamed(400)--- | ||
let scale = ScaleBuilder::new(Anchor::new_unnamed(0., 100.)) | ||
.add_named("a", 1., 100.) | ||
.add_named("b", 1., 100.) | ||
.add_unnamed(1., 100.) | ||
.build() | ||
.unwrap(); | ||
|
||
// Unamed----position----Named | ||
let measure = scale.locate_anchor(150.).unwrap(); | ||
assert_eq!(measure.anchor_name, "a"); | ||
assert_eq!(measure.scale_offset, -0.5); | ||
|
||
// position----Unamed----Named | ||
let measure = scale.locate_anchor(50.).unwrap(); | ||
assert_eq!(measure.anchor_name, "a"); | ||
assert_eq!(measure.scale_offset, -1.5); | ||
|
||
// Unamed----Named----position----Unamed | ||
let measure = scale.locate_anchor(350.).unwrap(); | ||
assert_eq!(measure.anchor_name, "b"); | ||
assert_eq!(measure.scale_offset, 0.5); | ||
|
||
// Unamed----Named----Unamed----position | ||
let measure = scale.locate_anchor(500.).unwrap(); | ||
assert_eq!(measure.anchor_name, "b"); | ||
assert_eq!(measure.scale_offset, 2.); | ||
} | ||
} |