1use std::sync::Arc;
22
23use axioval_ir::{Evidence, SourceId};
24use thiserror::Error;
25
26use crate::services::reviewable_exact_evidence;
27use crate::{MetricDirection, SnapshotBoundService, SourceSnapshot};
28
29#[derive(Clone, Debug, Error, PartialEq, Eq)]
31pub enum CoordinateSystemError {
32 #[error("coordinate-system service does not cover source `{0}`")]
34 UncoveredSource(SourceId),
35 #[error("coordinate system is stated ambiguously: {0}")]
38 Ambiguous(String),
39 #[error("coordinate system unsupported: {0}")]
42 Unsupported(String),
43 #[error("coordinate system cannot be read exactly: {0}")]
46 Unreadable(String),
47 #[error("coordinate system is invalid")]
50 InvalidMeasurement,
51 #[error("coordinate-system evidence is not exact and reviewable")]
53 InexactEvidence,
54 #[error("coordinate-system service answered for another source")]
56 ResponseRequestMismatch,
57}
58
59#[derive(Clone, Copy, Debug, PartialEq)]
61pub struct CoordinateFrame {
62 origin_metres: [f64; 3],
63 axes: [MetricDirection; 3],
64}
65
66impl CoordinateFrame {
67 pub fn try_new(
70 origin_metres: [f64; 3],
71 x: MetricDirection,
72 y: MetricDirection,
73 z: MetricDirection,
74 ) -> Result<Self, CoordinateSystemError> {
75 const ORTHOGONAL_TOLERANCE: f64 = 1.0e-9;
76 if !origin_metres.iter().all(|value| value.is_finite()) {
77 return Err(CoordinateSystemError::InvalidMeasurement);
78 }
79 let dot = |a: [f64; 3], b: [f64; 3]| a[0] * b[0] + a[1] * b[1] + a[2] * b[2];
80 let [first, second, third] = [x.components(), y.components(), z.components()];
81 let cross = [
82 first[1] * second[2] - first[2] * second[1],
83 first[2] * second[0] - first[0] * second[2],
84 first[0] * second[1] - first[1] * second[0],
85 ];
86 if dot(first, second).abs() > ORTHOGONAL_TOLERANCE
87 || dot(first, third).abs() > ORTHOGONAL_TOLERANCE
88 || dot(second, third).abs() > ORTHOGONAL_TOLERANCE
89 || dot(cross, third) < 1.0 - ORTHOGONAL_TOLERANCE
90 {
91 return Err(CoordinateSystemError::InvalidMeasurement);
92 }
93 Ok(Self {
94 origin_metres,
95 axes: [x, y, z],
96 })
97 }
98
99 #[must_use]
101 pub fn origin_metres(&self) -> [f64; 3] {
102 self.origin_metres
103 }
104
105 #[must_use]
107 pub fn axes(&self) -> [MetricDirection; 3] {
108 self.axes
109 }
110}
111
112#[derive(Clone, Debug, PartialEq)]
114pub struct MapConversion {
115 target: Option<String>,
116 offset: [f64; 3],
117 x_axis: [f64; 2],
118 scale: f64,
119 metres_per_map_unit: Option<f64>,
120}
121
122impl MapConversion {
123 pub fn try_new(
131 target: Option<String>,
132 offset: [f64; 3],
133 x_axis: [f64; 2],
134 scale: f64,
135 metres_per_map_unit: Option<f64>,
136 ) -> Result<Self, CoordinateSystemError> {
137 let norm = x_axis[0].hypot(x_axis[1]);
138 let valid = offset.iter().all(|value| value.is_finite())
139 && norm.is_finite()
140 && norm > f64::EPSILON
141 && scale.is_finite()
142 && scale > 0.0
143 && metres_per_map_unit.is_none_or(|unit| unit.is_finite() && unit > 0.0)
144 && target.as_deref().is_none_or(|name| !name.trim().is_empty());
145 if !valid {
146 return Err(CoordinateSystemError::InvalidMeasurement);
147 }
148 Ok(Self {
149 target,
150 offset,
151 x_axis: [x_axis[0] / norm, x_axis[1] / norm],
152 scale,
153 metres_per_map_unit,
154 })
155 }
156
157 #[must_use]
159 pub fn target(&self) -> Option<&str> {
160 self.target.as_deref()
161 }
162
163 #[must_use]
165 pub fn offset(&self) -> [f64; 3] {
166 self.offset
167 }
168
169 #[must_use]
171 pub fn x_axis(&self) -> [f64; 2] {
172 self.x_axis
173 }
174
175 #[must_use]
177 pub fn scale(&self) -> f64 {
178 self.scale
179 }
180
181 #[must_use]
183 pub fn metres_per_map_unit(&self) -> Option<f64> {
184 self.metres_per_map_unit
185 }
186
187 #[must_use]
189 pub fn offset_metres(&self) -> Option<[f64; 3]> {
190 self.metres_per_map_unit
191 .map(|unit| self.offset.map(|value| value * unit))
192 }
193}
194
195#[derive(Clone, Debug, PartialEq)]
197pub struct SourceCoordinateSystem {
198 source: SourceId,
199 world: Option<CoordinateFrame>,
200 true_north: Option<[f64; 2]>,
201 map: Option<MapConversion>,
202 evidence: Evidence,
203}
204
205impl SourceCoordinateSystem {
206 pub fn try_new(
212 source: SourceId,
213 world: Option<CoordinateFrame>,
214 true_north: Option<[f64; 2]>,
215 map: Option<MapConversion>,
216 evidence: Evidence,
217 ) -> Result<Self, CoordinateSystemError> {
218 let true_north = match true_north {
219 Some([x, y]) => {
220 let norm = x.hypot(y);
221 if !norm.is_finite() || norm <= f64::EPSILON {
222 return Err(CoordinateSystemError::InvalidMeasurement);
223 }
224 Some([x / norm, y / norm])
225 }
226 None => None,
227 };
228 if !reviewable_exact_evidence(&evidence) || evidence.source != source {
229 return Err(CoordinateSystemError::InexactEvidence);
230 }
231 Ok(Self {
232 source,
233 world,
234 true_north,
235 map,
236 evidence,
237 })
238 }
239
240 #[must_use]
242 pub fn source(&self) -> &SourceId {
243 &self.source
244 }
245
246 #[must_use]
248 pub fn world(&self) -> Option<&CoordinateFrame> {
249 self.world.as_ref()
250 }
251
252 #[must_use]
254 pub fn true_north(&self) -> Option<[f64; 2]> {
255 self.true_north
256 }
257
258 #[must_use]
260 pub fn map(&self) -> Option<&MapConversion> {
261 self.map.as_ref()
262 }
263
264 #[must_use]
266 pub fn evidence(&self) -> &Evidence {
267 &self.evidence
268 }
269}
270
271pub trait CoordinateSystemService: Send + Sync + 'static {
273 fn source_snapshots(&self) -> &[SourceSnapshot];
275 fn coordinate_system(
277 &self,
278 source: &SourceId,
279 ) -> Result<SourceCoordinateSystem, CoordinateSystemError>;
280}
281
282#[derive(Clone)]
284pub struct CoordinateSystemServiceHandle(Arc<dyn CoordinateSystemService>);
285
286impl CoordinateSystemServiceHandle {
287 #[must_use]
289 pub fn new(service: Arc<dyn CoordinateSystemService>) -> Self {
290 Self(service)
291 }
292
293 pub fn coordinate_system(
296 &self,
297 source: &SourceId,
298 ) -> Result<SourceCoordinateSystem, CoordinateSystemError> {
299 if !self
300 .0
301 .source_snapshots()
302 .iter()
303 .any(|snapshot| snapshot.source() == source)
304 {
305 return Err(CoordinateSystemError::UncoveredSource(source.clone()));
306 }
307 let answer = self.0.coordinate_system(source)?;
308 if answer.source() != source {
309 return Err(CoordinateSystemError::ResponseRequestMismatch);
310 }
311 Ok(answer)
312 }
313}
314
315impl SnapshotBoundService for CoordinateSystemServiceHandle {
316 fn source_snapshots(&self) -> &[SourceSnapshot] {
317 self.0.source_snapshots()
318 }
319}
320
321#[cfg(test)]
322mod tests {
323 use super::*;
324
325 fn source(name: &str) -> SourceId {
326 SourceId::new("cad", name).unwrap()
327 }
328
329 fn direction(vector: [f64; 3]) -> MetricDirection {
330 MetricDirection::try_new(vector).unwrap()
331 }
332
333 fn identity() -> [MetricDirection; 3] {
334 [
335 direction([1.0, 0.0, 0.0]),
336 direction([0.0, 1.0, 0.0]),
337 direction([0.0, 0.0, 1.0]),
338 ]
339 }
340
341 #[test]
342 fn a_frame_must_be_right_handed_and_finite() {
343 let [x, y, z] = identity();
344 assert!(CoordinateFrame::try_new([0.0; 3], x, y, z).is_ok());
345 assert_eq!(
346 CoordinateFrame::try_new([0.0; 3], direction([-1.0, 0.0, 0.0]), y, z),
347 Err(CoordinateSystemError::InvalidMeasurement)
348 );
349 assert_eq!(
350 CoordinateFrame::try_new([f64::NAN, 0.0, 0.0], x, y, z),
351 Err(CoordinateSystemError::InvalidMeasurement)
352 );
353 }
354
355 #[test]
356 #[allow(clippy::float_cmp)] fn a_map_conversion_needs_a_positive_scale_and_a_direction() {
358 assert!(MapConversion::try_new(None, [1.0, 2.0, 3.0], [1.0, 0.0], 1.0, None).is_ok());
359 for (axis, scale, unit) in [
360 ([0.0, 0.0], 1.0, None),
361 ([1.0, 0.0], 0.0, None),
362 ([1.0, 0.0], 1.0, Some(-1.0)),
363 ] {
364 assert_eq!(
365 MapConversion::try_new(None, [0.0; 3], axis, scale, unit),
366 Err(CoordinateSystemError::InvalidMeasurement)
367 );
368 }
369 let map =
370 MapConversion::try_new(None, [1.0, 2.0, 3.0], [0.0, 2.0], 1.0, Some(0.001)).unwrap();
371 assert_eq!(map.x_axis(), [0.0, 1.0]);
372 assert_eq!(map.offset_metres(), Some([0.001, 0.002, 0.003]));
373 }
374
375 #[test]
376 fn evidence_must_be_exact_and_from_the_source() {
377 let foreign = Evidence::exact(source("b"), "crs");
378 assert_eq!(
379 SourceCoordinateSystem::try_new(source("a"), None, None, None, foreign),
380 Err(CoordinateSystemError::InexactEvidence)
381 );
382 let system = SourceCoordinateSystem::try_new(
383 source("a"),
384 None,
385 Some([0.0, 3.0]),
386 None,
387 Evidence::exact(source("a"), "crs"),
388 )
389 .unwrap();
390 assert_eq!(system.true_north(), Some([0.0, 1.0]));
391 }
392
393 struct Fixed(Vec<SourceSnapshot>, SourceCoordinateSystem);
394 impl CoordinateSystemService for Fixed {
395 fn source_snapshots(&self) -> &[SourceSnapshot] {
396 &self.0
397 }
398 fn coordinate_system(
399 &self,
400 _: &SourceId,
401 ) -> Result<SourceCoordinateSystem, CoordinateSystemError> {
402 Ok(self.1.clone())
403 }
404 }
405
406 #[test]
407 fn the_handle_binds_answers_to_the_request() {
408 let snapshots = vec![
409 SourceSnapshot::try_new(source("a"), "r", "sha256:a").unwrap(),
410 SourceSnapshot::try_new(source("b"), "r", "sha256:b").unwrap(),
411 ];
412 let answer = SourceCoordinateSystem::try_new(
413 source("a"),
414 None,
415 None,
416 None,
417 Evidence::exact(source("a"), "crs"),
418 )
419 .unwrap();
420 let handle = CoordinateSystemServiceHandle::new(Arc::new(Fixed(snapshots, answer)));
421 assert!(handle.coordinate_system(&source("a")).is_ok());
422 assert_eq!(
423 handle.coordinate_system(&source("b")),
424 Err(CoordinateSystemError::ResponseRequestMismatch)
425 );
426 assert_eq!(
427 handle.coordinate_system(&source("c")),
428 Err(CoordinateSystemError::UncoveredSource(source("c")))
429 );
430 }
431}