use super::{Mapping, MappingFrame};
use serde::{Deserialize, Serialize};
#[derive(Debug, Clone, Deserialize, Serialize)]
#[serde(rename_all = "camelCase", deny_unknown_fields)]
pub struct PlaneCalibrationRequest {
pub raster_to_source: [f64; 6],
pub raster_size: [u32; 2],
pub source_points: [[f64; 2]; 2],
pub distance_metres: f64,
pub world_anchor: [f64; 3],
pub world_direction: [f64; 3],
pub plane_normal: [f64; 3],
}
#[derive(Debug, Clone, Serialize)]
#[serde(rename_all = "camelCase")]
pub struct CalibratedPlane {
pub mapping: Mapping,
pub raster_corners: [[f64; 3]; 4],
pub metres_per_source_unit: f64,
}
fn dot(a: [f64; 3], b: [f64; 3]) -> f64 {
a.iter().zip(b).map(|(x, y)| x * y).sum()
}
fn length(v: [f64; 3]) -> f64 { v[0].hypot(v[1]).hypot(v[2]) }
fn preserves_vector(start: [f64; 3], end: [f64; 3], intended: [f64; 3]) -> bool {
let error = length(std::array::from_fn(|i| (end[i] - start[i]) - intended[i]));
error.is_finite() && error <= length(intended) * 1e-6
}
fn unit(v: [f64; 3]) -> Result<[f64; 3], String> {
let len = length(v);
if !v.iter().all(|v| v.is_finite()) || !len.is_finite() || len <= 0. {
return Err("Calibration needs finite, nonzero plane directions".into());
}
Ok(v.map(|value| value / len))
}
pub fn calibrate_appearance_plane(request: &PlaneCalibrationRequest) -> Result<CalibratedPlane, String> {
let r = request;
if !r.raster_to_source.iter().chain(r.source_points.iter().flatten())
.chain(r.world_anchor.iter()).all(|v| v.is_finite())
|| !r.distance_metres.is_finite() || r.distance_metres <= 0.
|| r.raster_size.iter().any(|&v| v == 0 || v > 8192)
|| u64::from(r.raster_size[0]) * u64::from(r.raster_size[1]) > 16 * 1024 * 1024 {
return Err("Calibration needs finite coordinates, a positive measured distance and a bounded raster".into());
}
let dx = r.source_points[1][0] - r.source_points[0][0];
let dy = r.source_points[1][1] - r.source_points[0][1];
let span = dx.hypot(dy);
if !span.is_finite() || span <= 0. {
return Err("Choose two distinct source points for calibration".into());
}
let scale = r.distance_metres / span;
if !scale.is_finite() || scale <= 0. {
return Err("The measured source scale cannot be represented".into());
}
let direction = unit(r.world_direction)?;
let normal = unit(r.plane_normal)?;
if dot(direction, normal).abs() > 1e-10 {
return Err("The measured direction must lie in the chosen plane".into());
}
let sideways = unit([
normal[1] * direction[2] - normal[2] * direction[1],
normal[2] * direction[0] - normal[0] * direction[2],
normal[0] * direction[1] - normal[1] * direction[0],
])?;
let sx = dx / span;
let sy = dy / span;
let vector = |x: f64, y: f64| -> [f64; 3] {
let along = (x * sx + y * sy) * scale;
let across = (-x * sy + y * sx) * scale;
std::array::from_fn(|i| direction[i] * along + sideways[i] * across)
};
let [a, b, c, d, e, f] = r.raster_to_source;
let raster_x = vector(a, b);
let raster_y = vector(c, d);
let axis_u = unit(raster_x)?;
let axis_v = unit(raster_y.map(|v| -v))?;
if dot(axis_u, axis_v).abs() > 1e-10 {
return Err("A sheared raster needs rectification before planar projection".into());
}
let width = length(raster_x) * f64::from(r.raster_size[0]);
let height = length(raster_y) * f64::from(r.raster_size[1]);
let top_offset = vector(e - r.source_points[0][0], f - r.source_points[0][1]);
let top_left = std::array::from_fn(|i| r.world_anchor[i] + top_offset[i]);
let top_right = std::array::from_fn(|i| top_left[i] + axis_u[i] * width);
let bottom_left = std::array::from_fn(|i| top_left[i] - axis_v[i] * height);
let bottom_right = std::array::from_fn(|i| bottom_left[i] + axis_u[i] * width);
let corners = [top_left, top_right, bottom_right, bottom_left];
if !corners.iter().flatten().all(|v| v.is_finite())
|| !width.is_finite() || !height.is_finite() || width <= 0. || height <= 0.
|| !preserves_vector(r.world_anchor, top_left, top_offset)
|| !preserves_vector(top_left, top_right, axis_u.map(|v| v * width))
|| !preserves_vector(bottom_left, bottom_right, axis_u.map(|v| v * width))
|| !preserves_vector(bottom_left, top_left, axis_v.map(|v| v * height))
|| !preserves_vector(bottom_right, top_right, axis_v.map(|v| v * height)) {
return Err("The calibrated plane exceeds coordinate precision; use a local world frame".into());
}
Ok(CalibratedPlane {
mapping: Mapping::Planar { frame: MappingFrame::World, origin: bottom_left,
axis_u, axis_v, metres_per_tile: [width, height] },
raster_corners: corners,
metres_per_source_unit: scale,
})
}
#[cfg(test)]
#[path = "calibration_tests.rs"]
mod tests;