use std::sync::Arc;
use axioval_ir::{Evidence, SourceId};
use thiserror::Error;
use crate::services::reviewable_exact_evidence;
use crate::{MetricDirection, SnapshotBoundService, SourceSnapshot};
#[derive(Clone, Debug, Error, PartialEq, Eq)]
pub enum CoordinateSystemError {
#[error("coordinate-system service does not cover source `{0}`")]
UncoveredSource(SourceId),
#[error("coordinate system is stated ambiguously: {0}")]
Ambiguous(String),
#[error("coordinate system unsupported: {0}")]
Unsupported(String),
#[error("coordinate system cannot be read exactly: {0}")]
Unreadable(String),
#[error("coordinate system is invalid")]
InvalidMeasurement,
#[error("coordinate-system evidence is not exact and reviewable")]
InexactEvidence,
#[error("coordinate-system service answered for another source")]
ResponseRequestMismatch,
}
#[derive(Clone, Copy, Debug, PartialEq)]
pub struct CoordinateFrame {
origin_metres: [f64; 3],
axes: [MetricDirection; 3],
}
impl CoordinateFrame {
pub fn try_new(
origin_metres: [f64; 3],
x: MetricDirection,
y: MetricDirection,
z: MetricDirection,
) -> Result<Self, CoordinateSystemError> {
const ORTHOGONAL_TOLERANCE: f64 = 1.0e-9;
if !origin_metres.iter().all(|value| value.is_finite()) {
return Err(CoordinateSystemError::InvalidMeasurement);
}
let dot = |a: [f64; 3], b: [f64; 3]| a[0] * b[0] + a[1] * b[1] + a[2] * b[2];
let [first, second, third] = [x.components(), y.components(), z.components()];
let cross = [
first[1] * second[2] - first[2] * second[1],
first[2] * second[0] - first[0] * second[2],
first[0] * second[1] - first[1] * second[0],
];
if dot(first, second).abs() > ORTHOGONAL_TOLERANCE
|| dot(first, third).abs() > ORTHOGONAL_TOLERANCE
|| dot(second, third).abs() > ORTHOGONAL_TOLERANCE
|| dot(cross, third) < 1.0 - ORTHOGONAL_TOLERANCE
{
return Err(CoordinateSystemError::InvalidMeasurement);
}
Ok(Self {
origin_metres,
axes: [x, y, z],
})
}
#[must_use]
pub fn origin_metres(&self) -> [f64; 3] {
self.origin_metres
}
#[must_use]
pub fn axes(&self) -> [MetricDirection; 3] {
self.axes
}
}
#[derive(Clone, Debug, PartialEq)]
pub struct MapConversion {
target: Option<String>,
offset: [f64; 3],
x_axis: [f64; 2],
scale: f64,
metres_per_map_unit: Option<f64>,
}
impl MapConversion {
pub fn try_new(
target: Option<String>,
offset: [f64; 3],
x_axis: [f64; 2],
scale: f64,
metres_per_map_unit: Option<f64>,
) -> Result<Self, CoordinateSystemError> {
let norm = x_axis[0].hypot(x_axis[1]);
let valid = offset.iter().all(|value| value.is_finite())
&& norm.is_finite()
&& norm > f64::EPSILON
&& scale.is_finite()
&& scale > 0.0
&& metres_per_map_unit.is_none_or(|unit| unit.is_finite() && unit > 0.0)
&& target.as_deref().is_none_or(|name| !name.trim().is_empty());
if !valid {
return Err(CoordinateSystemError::InvalidMeasurement);
}
Ok(Self {
target,
offset,
x_axis: [x_axis[0] / norm, x_axis[1] / norm],
scale,
metres_per_map_unit,
})
}
#[must_use]
pub fn target(&self) -> Option<&str> {
self.target.as_deref()
}
#[must_use]
pub fn offset(&self) -> [f64; 3] {
self.offset
}
#[must_use]
pub fn x_axis(&self) -> [f64; 2] {
self.x_axis
}
#[must_use]
pub fn scale(&self) -> f64 {
self.scale
}
#[must_use]
pub fn metres_per_map_unit(&self) -> Option<f64> {
self.metres_per_map_unit
}
#[must_use]
pub fn offset_metres(&self) -> Option<[f64; 3]> {
self.metres_per_map_unit
.map(|unit| self.offset.map(|value| value * unit))
}
}
#[derive(Clone, Debug, PartialEq)]
pub struct SourceCoordinateSystem {
source: SourceId,
world: Option<CoordinateFrame>,
true_north: Option<[f64; 2]>,
map: Option<MapConversion>,
evidence: Evidence,
}
impl SourceCoordinateSystem {
pub fn try_new(
source: SourceId,
world: Option<CoordinateFrame>,
true_north: Option<[f64; 2]>,
map: Option<MapConversion>,
evidence: Evidence,
) -> Result<Self, CoordinateSystemError> {
let true_north = match true_north {
Some([x, y]) => {
let norm = x.hypot(y);
if !norm.is_finite() || norm <= f64::EPSILON {
return Err(CoordinateSystemError::InvalidMeasurement);
}
Some([x / norm, y / norm])
}
None => None,
};
if !reviewable_exact_evidence(&evidence) || evidence.source != source {
return Err(CoordinateSystemError::InexactEvidence);
}
Ok(Self {
source,
world,
true_north,
map,
evidence,
})
}
#[must_use]
pub fn source(&self) -> &SourceId {
&self.source
}
#[must_use]
pub fn world(&self) -> Option<&CoordinateFrame> {
self.world.as_ref()
}
#[must_use]
pub fn true_north(&self) -> Option<[f64; 2]> {
self.true_north
}
#[must_use]
pub fn map(&self) -> Option<&MapConversion> {
self.map.as_ref()
}
#[must_use]
pub fn evidence(&self) -> &Evidence {
&self.evidence
}
}
pub trait CoordinateSystemService: Send + Sync + 'static {
fn source_snapshots(&self) -> &[SourceSnapshot];
fn coordinate_system(
&self,
source: &SourceId,
) -> Result<SourceCoordinateSystem, CoordinateSystemError>;
}
#[derive(Clone)]
pub struct CoordinateSystemServiceHandle(Arc<dyn CoordinateSystemService>);
impl CoordinateSystemServiceHandle {
#[must_use]
pub fn new(service: Arc<dyn CoordinateSystemService>) -> Self {
Self(service)
}
pub fn coordinate_system(
&self,
source: &SourceId,
) -> Result<SourceCoordinateSystem, CoordinateSystemError> {
if !self
.0
.source_snapshots()
.iter()
.any(|snapshot| snapshot.source() == source)
{
return Err(CoordinateSystemError::UncoveredSource(source.clone()));
}
let answer = self.0.coordinate_system(source)?;
if answer.source() != source {
return Err(CoordinateSystemError::ResponseRequestMismatch);
}
Ok(answer)
}
}
impl SnapshotBoundService for CoordinateSystemServiceHandle {
fn source_snapshots(&self) -> &[SourceSnapshot] {
self.0.source_snapshots()
}
}
#[cfg(test)]
mod tests {
use super::*;
fn source(name: &str) -> SourceId {
SourceId::new("cad", name).unwrap()
}
fn direction(vector: [f64; 3]) -> MetricDirection {
MetricDirection::try_new(vector).unwrap()
}
fn identity() -> [MetricDirection; 3] {
[
direction([1.0, 0.0, 0.0]),
direction([0.0, 1.0, 0.0]),
direction([0.0, 0.0, 1.0]),
]
}
#[test]
fn a_frame_must_be_right_handed_and_finite() {
let [x, y, z] = identity();
assert!(CoordinateFrame::try_new([0.0; 3], x, y, z).is_ok());
assert_eq!(
CoordinateFrame::try_new([0.0; 3], direction([-1.0, 0.0, 0.0]), y, z),
Err(CoordinateSystemError::InvalidMeasurement)
);
assert_eq!(
CoordinateFrame::try_new([f64::NAN, 0.0, 0.0], x, y, z),
Err(CoordinateSystemError::InvalidMeasurement)
);
}
#[test]
#[allow(clippy::float_cmp)] fn a_map_conversion_needs_a_positive_scale_and_a_direction() {
assert!(MapConversion::try_new(None, [1.0, 2.0, 3.0], [1.0, 0.0], 1.0, None).is_ok());
for (axis, scale, unit) in [
([0.0, 0.0], 1.0, None),
([1.0, 0.0], 0.0, None),
([1.0, 0.0], 1.0, Some(-1.0)),
] {
assert_eq!(
MapConversion::try_new(None, [0.0; 3], axis, scale, unit),
Err(CoordinateSystemError::InvalidMeasurement)
);
}
let map =
MapConversion::try_new(None, [1.0, 2.0, 3.0], [0.0, 2.0], 1.0, Some(0.001)).unwrap();
assert_eq!(map.x_axis(), [0.0, 1.0]);
assert_eq!(map.offset_metres(), Some([0.001, 0.002, 0.003]));
}
#[test]
fn evidence_must_be_exact_and_from_the_source() {
let foreign = Evidence::exact(source("b"), "crs");
assert_eq!(
SourceCoordinateSystem::try_new(source("a"), None, None, None, foreign),
Err(CoordinateSystemError::InexactEvidence)
);
let system = SourceCoordinateSystem::try_new(
source("a"),
None,
Some([0.0, 3.0]),
None,
Evidence::exact(source("a"), "crs"),
)
.unwrap();
assert_eq!(system.true_north(), Some([0.0, 1.0]));
}
struct Fixed(Vec<SourceSnapshot>, SourceCoordinateSystem);
impl CoordinateSystemService for Fixed {
fn source_snapshots(&self) -> &[SourceSnapshot] {
&self.0
}
fn coordinate_system(
&self,
_: &SourceId,
) -> Result<SourceCoordinateSystem, CoordinateSystemError> {
Ok(self.1.clone())
}
}
#[test]
fn the_handle_binds_answers_to_the_request() {
let snapshots = vec![
SourceSnapshot::try_new(source("a"), "r", "sha256:a").unwrap(),
SourceSnapshot::try_new(source("b"), "r", "sha256:b").unwrap(),
];
let answer = SourceCoordinateSystem::try_new(
source("a"),
None,
None,
None,
Evidence::exact(source("a"), "crs"),
)
.unwrap();
let handle = CoordinateSystemServiceHandle::new(Arc::new(Fixed(snapshots, answer)));
assert!(handle.coordinate_system(&source("a")).is_ok());
assert_eq!(
handle.coordinate_system(&source("b")),
Err(CoordinateSystemError::ResponseRequestMismatch)
);
assert_eq!(
handle.coordinate_system(&source("c")),
Err(CoordinateSystemError::UncoveredSource(source("c")))
);
}
}