use std::ops::RangeBounds;
use geo::{Contains, Polygon};
use crate::{
math::QuadraticCurve,
observer::Observer,
spectrum::{NS, SPECTRUM_WAVELENGTH_RANGE},
xyz::XYZ,
};
pub struct SpectralLocus {
observer: Observer,
polygon: Polygon,
}
impl SpectralLocus {
pub fn new(observer: Observer) -> Self {
let obs_data = observer.data().data;
let mut v = Vec::with_capacity(NS + 1);
for i in 0..NS {
let xyz = obs_data.column(i).into();
let chromaticity = XYZ::from_vec(xyz, observer).chromaticity();
v.push(chromaticity.to_array());
}
v.push(v[0]); let polygon = vec_to_polygon(v);
SpectralLocus { observer, polygon }
}
pub fn contains(&self, point: [f64; 2]) -> bool {
self.polygon.contains(&geo::Point::new(point[0], point[1]))
}
pub fn observer(&self) -> Observer {
self.observer
}
pub fn iter_range_with_slope<'a>(
&'a self,
range: impl RangeBounds<usize>,
step: usize,
) -> SpectralLocusSlopeIterator<'a> {
let min_wavelength = *SPECTRUM_WAVELENGTH_RANGE.start();
let clip = |s, lim| {
if s > min_wavelength {
s - min_wavelength
} else {
lim
}
};
let start = match range.start_bound() {
std::ops::Bound::Included(&s) => clip(s, 0),
std::ops::Bound::Excluded(&s) => clip(s + 1, 0),
std::ops::Bound::Unbounded => 0,
};
let end = match range.end_bound() {
std::ops::Bound::Included(&e) => clip(e + 1, NS - 1),
std::ops::Bound::Excluded(&e) => clip(e, NS - 1),
std::ops::Bound::Unbounded => NS - 1,
};
if end <= start {
panic!("End of range must be greater than start");
}
SpectralLocusSlopeIterator {
locus: self,
index: start,
step,
end,
}
}
pub fn polygon(&self) -> &Polygon {
&self.polygon
}
}
pub struct SpectralLocusIterator<'a> {
locus: &'a SpectralLocus,
index: usize,
}
impl<'a> IntoIterator for &'a SpectralLocus {
type Item = (f64, f64);
type IntoIter = SpectralLocusIterator<'a>;
fn into_iter(self) -> Self::IntoIter {
SpectralLocusIterator {
locus: self,
index: 0,
}
}
}
impl<'a> Iterator for SpectralLocusIterator<'a> {
type Item = (f64, f64);
fn next(&mut self) -> Option<Self::Item> {
if self.index < self.locus.polygon.exterior().points().count() {
let coord = self.locus.polygon.exterior()[self.index];
self.index += 1;
Some((coord.x, coord.y))
} else {
None
}
}
}
pub struct SpectralLocusSlopeIterator<'a> {
locus: &'a SpectralLocus,
index: usize,
step: usize,
end: usize,
}
impl Iterator for SpectralLocusSlopeIterator<'_> {
type Item = ((f64, f64), f64);
fn next(&mut self) -> Option<Self::Item> {
if self.index < self.end {
let coord = self.locus.polygon.exterior()[self.index];
let angle = match self.index {
0 => {
let coord_plus = self.locus.polygon.exterior()[1];
(coord_plus.y - coord.y).atan2(coord_plus.x - coord.x)
}
i if i < NS - 1 => {
let coord_min = self.locus.polygon.exterior()[i - 1];
let coord_plus = self.locus.polygon.exterior()[i + 1];
let qurve = QuadraticCurve::new(
(coord_min.x, coord_min.y),
(coord.x, coord.y),
(coord_plus.x, coord_plus.y),
)
.unwrap();
qurve.slope_angle(0.5)
}
n if n == NS - 1 => {
let coord_min = self.locus.polygon.exterior()[n - 1];
(coord.y - coord_min.y).atan2(coord.x - coord_min.x)
}
_ => 0.0, };
self.index += self.step;
Some(((coord.x, coord.y), angle))
} else {
None
}
}
}
impl<'a> DoubleEndedIterator for SpectralLocusSlopeIterator<'a> {
fn next_back(&mut self) -> Option<<Self as Iterator>::Item> {
if self.index < self.end && self.end > 0 {
self.end = self.end.saturating_sub(self.step);
let coord = self.locus.polygon.exterior()[self.end];
let angle = match self.end {
0 => {
let coord_plus = self.locus.polygon.exterior()[1];
(coord_plus.y - coord.y).atan2(coord_plus.x - coord.x)
}
i if i < NS - 1 => {
let coord_min = self.locus.polygon.exterior()[i - 1];
let coord_plus = self.locus.polygon.exterior()[i + 1];
let qurve = QuadraticCurve::new(
(coord_min.x, coord_min.y),
(coord.x, coord.y),
(coord_plus.x, coord_plus.y),
)
.unwrap();
qurve.slope_angle(0.5)
}
n if n == NS - 1 => {
let coord_min = self.locus.polygon.exterior()[n - 1];
(coord.y - coord_min.y).atan2(coord.x - coord_min.x)
}
_ => 0.0,
};
Some(((coord.x, coord.y), angle))
} else {
None
}
}
}
use geo::{Coord, LineString};
fn vec_to_polygon(coords: Vec<[f64; 2]>) -> Polygon {
let linestring = LineString::from(
coords
.into_iter()
.map(|[x, y]| Coord { x, y })
.collect::<Vec<_>>(),
);
Polygon::new(linestring, vec![]) }
#[cfg(test)]
mod test {
use approx::assert_abs_diff_eq;
use super::*;
use crate::observer::Observer::Cie1931;
#[test]
fn test_spectral_locus() {
let locus = SpectralLocus::new(Cie1931);
assert_eq!(locus.observer(), Cie1931);
assert!(locus.contains([0.3, 0.3]));
assert!(!locus.contains([0.05, 0.05]));
assert!(!locus.contains([0.7, 0.7]));
let points: Vec<(f64, f64)> = locus.into_iter().collect();
assert_eq!(points.len(), NS + 1); }
#[test]
fn test_spectral_locus_iterator() {
let locus = SpectralLocus::new(Cie1931);
let mut iter = locus.into_iter();
let point = iter.next().unwrap();
assert_abs_diff_eq!(point.0, 0.17411, epsilon = 0.00005);
assert_abs_diff_eq!(point.1, 0.00496, epsilon = 0.00005);
let last = iter.last().unwrap();
assert_abs_diff_eq!(last.0, 0.17411, epsilon = 0.00005);
assert_abs_diff_eq!(last.1, 0.00496, epsilon = 0.00005);
}
#[test]
fn test_spectral_locus_slope_iterator() {
let locus = SpectralLocus::new(Cie1931);
let mut iter = locus.iter_range_with_slope(400..700, 10);
let (point, angle) = iter.next().unwrap();
assert_abs_diff_eq!(point.0, 0.173336, epsilon = 0.00005);
assert_abs_diff_eq!(point.1, 0.0047967, epsilon = 0.00005);
assert_abs_diff_eq!(angle, -2.84106, epsilon = 0.00005);
let (point, angle) = iter.next_back().unwrap();
assert_abs_diff_eq!(point.0, 0.734390, epsilon = 0.00005);
assert_abs_diff_eq!(point.1, 0.265609, epsilon = 0.00005);
assert_abs_diff_eq!(angle, -0.785398, epsilon = 0.00005); }
}