Skip to content

Commit

Permalink
Add low level LrmScale implementation
Browse files Browse the repository at this point in the history
Closes #3
  • Loading branch information
Tristramg committed Mar 13, 2024
1 parent 6d96c8d commit a0b89c4
Show file tree
Hide file tree
Showing 2 changed files with 389 additions and 0 deletions.
2 changes: 2 additions & 0 deletions src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,8 @@ mod lrs_generated;

#[deny(missing_docs)]
pub mod curves;
#[deny(missing_docs)]
pub mod lrm;
pub use lrs_generated::*;

#[test]
Expand Down
387 changes: 387 additions & 0 deletions src/lrm.rs
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.);
}
}

0 comments on commit a0b89c4

Please sign in to comment.