Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

lrs: expose planar coordinate system #86

Open
wants to merge 1 commit into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
51 changes: 26 additions & 25 deletions python/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -3,9 +3,9 @@

use std::path::PathBuf;

use liblrs::lrs::LrmHandle;
use liblrs::builder::Properties;
use liblrs::lrs::{self, LrmHandle};
use liblrs::lrs_ext::*;
use liblrs::{builder::Properties, lrs::LrsBase};
use pyo3::{exceptions::PyTypeError, prelude::*};

/// Holds the whole Linear Referencing System.
Expand Down Expand Up @@ -230,6 +230,17 @@ impl Anchor {
}
}

impl From<&liblrs::lrm_scale::Anchor> for Anchor {
fn from(value: &liblrs::lrm_scale::Anchor) -> Self {
Self {
name: value.clone().id.unwrap_or_else(|| "-".to_owned()),
position: value.point.map(|p| p.into()),
curve_position: value.curve_position,
scale_position: value.scale_position,
}
}
}

#[pyclass]
/// The result of a projection onto an [`LrmScale`].
pub struct LrmProjection {
Expand All @@ -241,13 +252,11 @@ pub struct LrmProjection {
pub orthogonal_offset: f64,
}

impl From<&liblrs::lrm_scale::Anchor> for Anchor {
fn from(value: &liblrs::lrm_scale::Anchor) -> Self {
impl From<lrs::LrmProjection> for LrmProjection {
fn from(value: lrs::LrmProjection) -> Self {
Self {
name: value.clone().id.unwrap_or_else(|| "-".to_owned()),
position: value.point.map(|p| p.into()),
curve_position: value.curve_position,
scale_position: value.scale_position,
measure: (&value.measure.measure).into(),
orthogonal_offset: value.orthogonal_offset,
}
}
}
Expand All @@ -256,8 +265,8 @@ impl From<&liblrs::lrm_scale::Anchor> for Anchor {
impl Lrs {
/// Load the data.
#[new]
pub fn load(data: &[u8]) -> PyResult<Lrs> {
ExtLrs::load(data)
pub fn load(data: &[u8], planar: bool) -> PyResult<Lrs> {
ExtLrs::load(data, planar)
.map(|lrs| Self { lrs })
.map_err(|e| PyTypeError::new_err(e.to_string()))
}
Expand Down Expand Up @@ -300,9 +309,8 @@ impl Lrs {
/// Get the positon along the curve given a [`LrmScaleMeasure`]
/// The value will be between 0.0 and 1.0, both included
pub fn locate_point(&self, lrm_index: usize, measure: &LrmScaleMeasure) -> PyResult<f64> {
self.lrs.lrs.lrms[lrm_index]
.scale
.locate_point(&measure.into())
self.lrs
.locate_point(lrm_index, &(measure.into()))
.map_err(|e| PyTypeError::new_err(e.to_string()))
}

Expand All @@ -321,24 +329,17 @@ impl Lrs {

/// Given a ID returns the corresponding lrs index (or None if not found)
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
/// Given a ID returns the corresponding lrs index (or None if not found)
/// Given an ID, returns the corresponding lrs index (or None if not found)

pub fn find_lrm(&self, lrm_id: &str) -> Option<usize> {
self.lrs.lrs.get_lrm(lrm_id).map(|handle| handle.0)
self.lrs.find_lrm(lrm_id)
}

/// Projects a [`Point`] on all applicable [`Traversal`]s to a given [`Lrm`].
/// The [`Point`] must be in the bounding box of the [`Curve`] of the [`Traversal`].
/// The result is sorted by `orthogonal_offset`: the nearest [`Lrm`] to the [`Point`] is the first item.
fn lookup(&self, point: Point, lrm_handle: usize) -> Vec<LrmProjection> {
self.lrs
.lrs
.lookup(point.into(), LrmHandle(lrm_handle))
.iter()
.map(|p| LrmProjection {
measure: LrmScaleMeasure {
anchor_name: p.measure.measure.anchor_name.to_owned(),
scale_offset: p.measure.measure.scale_offset,
},
orthogonal_offset: p.orthogonal_offset,
})
.into_iter()
.map(|p| p.into())
.collect()
}
}
Expand Down Expand Up @@ -457,10 +458,10 @@ impl Builder {
}

/// Builds the lrs to be used directly
pub fn build_lrs(&mut self, properties: Properties) -> PyResult<Lrs> {
pub fn build_lrs(&mut self, properties: Properties, planar: bool) -> PyResult<Lrs> {
let lrs = self
.inner
.build_lrs(properties)
.build_lrs(properties, planar)
.map_err(|e| PyTypeError::new_err(e.to_string()))?;
Ok(Lrs { lrs })
}
Expand Down
6 changes: 3 additions & 3 deletions src/builder.rs
Original file line number Diff line number Diff line change
Expand Up @@ -407,8 +407,8 @@ impl<'fbb> Builder<'fbb> {
}

/// Builds the LRS from the data.
pub fn build_lrs(&mut self, properties: Properties) -> Result<ExtLrs, String> {
ExtLrs::load(self.build_data(properties))
pub fn build_lrs(&mut self, properties: Properties, planar: bool) -> Result<ExtLrs, String> {
ExtLrs::load(self.build_data(properties), planar)
}

/// Return the mapping between a traversal id and its index in the builder.
Expand Down Expand Up @@ -643,7 +643,7 @@ mod tests {
};
b.add_lrm("lrm", traversal, &[aol, aol2], properties!());

let lrs = b.build_lrs(properties!()).unwrap();
let lrs = b.build_lrs(properties!(), true).unwrap();
let lrm = lrs
.resolve(
0,
Expand Down
105 changes: 79 additions & 26 deletions src/lrs_ext.rs
Original file line number Diff line number Diff line change
Expand Up @@ -3,62 +3,88 @@

use geo::{Coord, Point};

use crate::curves::{Curve, SphericalLineStringCurve};
use crate::lrm_scale::Anchor;
use crate::curves::{Curve, PlanarLineStringCurve, SphericalLineStringCurve};
use crate::lrm_scale::LrmScaleMeasure;
use crate::lrs::{self, TraversalPosition};
use crate::lrs::{LrsBase, LrsError};

type Lrs = lrs::Lrs<SphericalLineStringCurve>;
use crate::lrm_scale::{Anchor, LrmScaleError};
use crate::lrs::{Lrm, LrmHandle, LrmProjection, Lrs, LrsError};
use crate::lrs::{LrsBase, TraversalPosition};

/// Struct exposed to js.
pub struct ExtLrs {
/// The linear referencing system
pub lrs: Lrs,
pub enum ExtLrs {
/// LRS with spherical coordinates.
Spherical(Lrs<SphericalLineStringCurve>),
/// LRS with planar coordinates.
Planar(Lrs<PlanarLineStringCurve>),
}

impl ExtLrs {
/// Load the data.
pub fn load(data: &[u8]) -> Result<ExtLrs, String> {
Lrs::from_bytes(data)
.map(|lrs| Self { lrs })
.map_err(|err| err.to_string())
pub fn load(data: &[u8], planar: bool) -> Result<ExtLrs, String> {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The serialized data stores what kind of coordinates we are dealing with: https://github.com/OpenRailAssociation/liblrs/blob/main/schema/lrs.fbs#L8 (from the wording, we can see that it was a different use case that was initially though of, but it still work).

This will however draw a lot of small issues. I’m not sure how we could get a nice overview of all the fiddling we’ll have to do

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don't really see the issues, could you elaborate ?

Is it that

  • we need to set it through the builder (which my PR doesn't do, I reckon)
  • we need to read it when deserializing (which my PR doesn't do either)

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It’s both.

load shouldn’t take a parameter as it’s defined in the datafile. Loading planar graph as spherical would result in unexpected results (plus the more complicated API).

if planar {
Lrs::<PlanarLineStringCurve>::from_bytes(data).map(ExtLrs::Planar)
} else {
Lrs::<SphericalLineStringCurve>::from_bytes(data).map(ExtLrs::Spherical)
}
.map_err(|err| err.to_string())
}

/// How many LRMs compose the LRS.
pub fn lrm_len(&self) -> usize {
self.lrs.lrm_len()
match self {
ExtLrs::Spherical(lrs) => lrs.lrm_len(),
ExtLrs::Planar(lrs) => lrs.lrm_len(),
}
}

/// Given a ID returns the corresponding lrs index (or None if not found)
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
/// Given a ID returns the corresponding lrs index (or None if not found)
/// Given an ID, returns the corresponding lrs index (or None if not found)

pub fn find_lrm(&self, lrm_id: &str) -> Option<usize> {
match self {
ExtLrs::Spherical(lrs) => lrs.get_lrm(lrm_id).map(|handle| handle.0),
ExtLrs::Planar(lrs) => lrs.get_lrm(lrm_id).map(|handle| handle.0),
}
}

fn get_lrm(&self, index: usize) -> &Lrm {
match self {
ExtLrs::Spherical(lrs) => &lrs.lrms[index],
ExtLrs::Planar(lrs) => &lrs.lrms[index],
}
}

/// Return the geometry of the LRM.
pub fn get_lrm_geom(&self, index: usize) -> Result<Vec<geo::Coord>, String> {
let lrm = self.lrs.lrms.get(index).ok_or("Invalid index")?;
self.lrs
.get_linestring(lrm.reference_traversal)
.map_err(|err| err.to_string())
.map(|linestring| linestring.0)
let lrm = self.get_lrm(index);
match self {
ExtLrs::Spherical(lrs) => lrs.get_linestring(lrm.reference_traversal),
ExtLrs::Planar(lrs) => lrs.get_linestring(lrm.reference_traversal),
}
.map_err(|err| err.to_string())
.map(|linestring| linestring.0)
}

/// `id` of the [`LrmScale`].
pub fn get_lrm_scale_id(&self, index: usize) -> String {
self.lrs.lrms[index].scale.id.clone()
self.get_lrm(index).scale.id.clone()
}

/// All the [`Anchor`]s of a LRM.
pub fn get_anchors(&self, lrm_index: usize) -> Vec<Anchor> {
self.lrs.lrms[lrm_index].scale.anchors.to_vec()
self.get_lrm(lrm_index).scale.anchors.to_vec()
}

/// Get the position given a [`LrmScaleMeasure`].
pub fn resolve(&self, lrm_index: usize, measure: &LrmScaleMeasure) -> Result<Point, LrsError> {
let lrm = &self.lrs.lrms[lrm_index];
let lrm = self.get_lrm(lrm_index);
let curve_position = lrm.scale.locate_point(measure)?.clamp(0., 1.0);

let traversal_position = TraversalPosition {
curve_position,
traversal: lrm.reference_traversal,
};
self.lrs.locate_traversal(traversal_position)
match self {
ExtLrs::Spherical(lrs) => lrs.locate_traversal(traversal_position),
ExtLrs::Planar(lrs) => lrs.locate_traversal(traversal_position),
}
}

/// Given two [`LrmScaleMeasure`]s, return a range of [`LineString`].
Expand All @@ -68,9 +94,8 @@ impl ExtLrs {
from: &LrmScaleMeasure,
to: &LrmScaleMeasure,
) -> Result<Vec<Coord>, String> {
let lrm = &self.lrs.lrms[lrm_index];
let lrm = self.get_lrm(lrm_index);
let scale = &lrm.scale;
let curve = &self.lrs.traversals[lrm.reference_traversal.0].curve;
let from = scale
.locate_point(from)
.map_err(|e| e.to_string())?
Expand All @@ -80,9 +105,37 @@ impl ExtLrs {
.map_err(|e| e.to_string())?
.clamp(0., 1.);

match curve.sublinestring(from, to) {
let sublinestring = match self {
ExtLrs::Spherical(lrs) => lrs.traversals[lrm.reference_traversal.0]
.curve
.sublinestring(from, to),
ExtLrs::Planar(lrs) => lrs.traversals[lrm.reference_traversal.0]
.curve
.sublinestring(from, to),
};

match sublinestring {
Some(linestring) => Ok(linestring.0),
None => Err("Could not find sublinestring".to_string()),
}
}

/// Given a point, return the [`LrmProjection`]s.
pub fn lookup(&self, point: Point, lrm_handle: LrmHandle) -> Vec<LrmProjection> {
match self {
ExtLrs::Spherical(lrs) => lrs.lookup(point, lrm_handle),
ExtLrs::Planar(lrs) => lrs.lookup(point, lrm_handle),
}
}

/// Get the positon along the curve given a [`LrmScaleMeasure`]
Copy link
Contributor

@SergeCroise SergeCroise Oct 28, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

a [`LrmScaleMeasure`]
or
an [`LrmScaleMeasure`]

/// The value will be between 0.0 and 1.0, both included
pub fn locate_point(
&self,
lrm_index: usize,
measure: &LrmScaleMeasure,
) -> Result<f64, LrmScaleError> {
let lrm = self.get_lrm(lrm_index);
lrm.scale.locate_point(measure)
}
}
2 changes: 1 addition & 1 deletion wasm/html_demo/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@ set_panic_hook()
async function file_selected(el) {
const [file] = el.target.files;
const data = await file.arrayBuffer()
const lrs = await Lrs.load(new Uint8Array(data));
const lrs = await Lrs.load(new Uint8Array(data), planar=false);

const curves_features = []
const anchors_features = []
Expand Down
10 changes: 3 additions & 7 deletions wasm/src/lib.rs
Original file line number Diff line number Diff line change
@@ -1,10 +1,7 @@
//! High level extensions meant for an easy usage
//! Those functions are exposed in wasm-bindings
Copy link
Contributor

@SergeCroise SergeCroise Oct 28, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

 `wasm-bindings` 
or
[`wasm-bindings`]


use liblrs::{
lrs::{LrmHandle, LrsBase},
lrs_ext::*,
};
use liblrs::{lrs::LrmHandle, lrs_ext::*};
use wasm_bindgen::prelude::*;

#[wasm_bindgen]
Expand Down Expand Up @@ -135,8 +132,8 @@ pub struct LrmProjection {
#[wasm_bindgen]
impl Lrs {
/// Load the data.
pub fn load(data: &[u8]) -> Result<Lrs, String> {
ExtLrs::load(data).map(|lrs| Self { lrs })
pub fn load(data: &[u8], planar: bool) -> Result<Lrs, String> {
ExtLrs::load(data, planar).map(|lrs| Self { lrs })
}

/// How many LRMs compose the LRS.
Expand Down Expand Up @@ -191,7 +188,6 @@ impl Lrs {
/// The result is sorted by `orthogonal_offset`: the nearest [`Lrm`] to the [`Point`] is the first item.
pub fn lookup(&self, point: Point, lrm_handle: usize) -> Vec<LrmProjection> {
self.lrs
.lrs
.lookup(point.into(), LrmHandle(lrm_handle))
.iter()
.map(|p| LrmProjection {
Expand Down
Loading