#![allow(dead_code)]
#![forbid(unsafe_code)]
use std::collections::HashMap;
use thiserror::Error;
#[derive(Debug, Error)]
pub enum MultiAngleError {
#[error("angle group '{0}' not found")]
GroupNotFound(String),
#[error("angle index {index} is out of range for group '{group}' (max {max})")]
AngleOutOfRange {
group: String,
index: usize,
max: usize,
},
#[error("duplicate angle group name: '{0}'")]
DuplicateGroup(String),
#[error("angle group '{0}' has no angles")]
EmptyGroup(String),
#[error("switch at sample {0} is not a sync sample")]
NotSyncSample(u64),
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct AngleDescriptor {
pub track_index: usize,
pub label: String,
pub language: Option<String>,
pub is_default: bool,
pub metadata: HashMap<String, String>,
}
impl AngleDescriptor {
#[must_use]
pub fn new(track_index: usize, label: impl Into<String>) -> Self {
Self {
track_index,
label: label.into(),
language: None,
is_default: false,
metadata: HashMap::new(),
}
}
#[must_use]
pub fn with_language(mut self, lang: impl Into<String>) -> Self {
self.language = Some(lang.into());
self
}
#[must_use]
pub fn as_default(mut self) -> Self {
self.is_default = true;
self
}
#[must_use]
pub fn with_metadata(mut self, key: impl Into<String>, value: impl Into<String>) -> Self {
self.metadata.insert(key.into(), value.into());
self
}
}
#[derive(Debug, Clone)]
pub struct AngleGroup {
pub name: String,
pub angles: Vec<AngleDescriptor>,
active_index: usize,
pub time_range: Option<(u64, u64)>,
}
impl AngleGroup {
#[must_use]
pub fn new(name: impl Into<String>) -> Self {
Self {
name: name.into(),
angles: Vec::new(),
active_index: 0,
time_range: None,
}
}
#[must_use]
pub fn with_angle(mut self, angle: AngleDescriptor) -> Self {
self.angles.push(angle);
self
}
pub fn add_angle(&mut self, angle: AngleDescriptor) {
self.angles.push(angle);
}
#[must_use]
pub fn with_time_range(mut self, start: u64, end: u64) -> Self {
self.time_range = Some((start, end));
self
}
#[must_use]
pub fn angle_count(&self) -> usize {
self.angles.len()
}
#[must_use]
pub fn active_index(&self) -> usize {
self.active_index
}
#[must_use]
pub fn active_angle(&self) -> Option<&AngleDescriptor> {
self.angles.get(self.active_index)
}
#[must_use]
pub fn active_track_index(&self) -> Option<usize> {
self.active_angle().map(|a| a.track_index)
}
pub fn set_active(&mut self, index: usize) -> Result<(), MultiAngleError> {
if index >= self.angles.len() {
return Err(MultiAngleError::AngleOutOfRange {
group: self.name.clone(),
index,
max: self.angles.len().saturating_sub(1),
});
}
self.active_index = index;
Ok(())
}
#[must_use]
pub fn default_angle_index(&self) -> Option<usize> {
self.angles
.iter()
.position(|a| a.is_default)
.or(if self.angles.is_empty() {
None
} else {
Some(0)
})
}
#[must_use]
pub fn get_angle(&self, index: usize) -> Option<&AngleDescriptor> {
self.angles.get(index)
}
#[must_use]
pub fn find_by_track(&self, track_index: usize) -> Option<(usize, &AngleDescriptor)> {
self.angles
.iter()
.enumerate()
.find(|(_, a)| a.track_index == track_index)
}
#[must_use]
pub fn find_by_label(&self, label: &str) -> Option<(usize, &AngleDescriptor)> {
self.angles
.iter()
.enumerate()
.find(|(_, a)| a.label.eq_ignore_ascii_case(label))
}
#[must_use]
pub fn all_track_indices(&self) -> Vec<usize> {
self.angles.iter().map(|a| a.track_index).collect()
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct AngleSwitchPoint {
pub time_ticks: u64,
pub sample_number: u32,
pub from_angle: usize,
pub to_angle: usize,
}
#[derive(Debug, Clone, Default)]
pub struct AngleManager {
groups: HashMap<String, AngleGroup>,
group_order: Vec<String>,
switch_points: Vec<AngleSwitchPoint>,
}
impl AngleManager {
#[must_use]
pub fn new() -> Self {
Self::default()
}
pub fn add_group(&mut self, group: AngleGroup) -> Result<(), MultiAngleError> {
if self.groups.contains_key(&group.name) {
return Err(MultiAngleError::DuplicateGroup(group.name));
}
self.group_order.push(group.name.clone());
self.groups.insert(group.name.clone(), group);
Ok(())
}
#[must_use]
pub fn get_group(&self, name: &str) -> Option<&AngleGroup> {
self.groups.get(name)
}
#[must_use]
pub fn get_group_mut(&mut self, name: &str) -> Option<&mut AngleGroup> {
self.groups.get_mut(name)
}
#[must_use]
pub fn group_count(&self) -> usize {
self.groups.len()
}
#[must_use]
pub fn total_angles(&self, group_name: &str) -> Option<usize> {
self.groups.get(group_name).map(|g| g.angle_count())
}
pub fn set_active_angle(
&mut self,
group_name: &str,
angle_index: usize,
) -> Result<(), MultiAngleError> {
let group = self
.groups
.get_mut(group_name)
.ok_or_else(|| MultiAngleError::GroupNotFound(group_name.to_string()))?;
group.set_active(angle_index)
}
#[must_use]
pub fn active_track(&self, group_name: &str) -> Option<usize> {
self.groups
.get(group_name)
.and_then(|g| g.active_track_index())
}
pub fn schedule_switch(&mut self, point: AngleSwitchPoint) {
self.switch_points.push(point);
self.switch_points.sort_by_key(|p| p.time_ticks);
}
#[must_use]
pub fn switch_points(&self) -> &[AngleSwitchPoint] {
&self.switch_points
}
#[must_use]
pub fn next_switch_after(&self, time_ticks: u64) -> Option<&AngleSwitchPoint> {
self.switch_points
.iter()
.find(|p| p.time_ticks >= time_ticks)
}
#[must_use]
pub fn group_names(&self) -> &[String] {
&self.group_order
}
#[must_use]
pub fn all_track_indices(&self) -> Vec<usize> {
let mut indices: Vec<usize> = self
.groups
.values()
.flat_map(|g| g.all_track_indices())
.collect();
indices.sort_unstable();
indices.dedup();
indices
}
#[must_use]
pub fn group_for_track(&self, track_index: usize) -> Option<(&str, usize)> {
for group in self.groups.values() {
if let Some((angle_idx, _)) = group.find_by_track(track_index) {
return Some((&group.name, angle_idx));
}
}
None
}
pub fn clear_switch_points(&mut self) {
self.switch_points.clear();
}
}
#[cfg(test)]
mod tests {
use super::*;
fn sample_group() -> AngleGroup {
AngleGroup::new("scene1")
.with_angle(AngleDescriptor::new(0, "front").as_default())
.with_angle(AngleDescriptor::new(1, "side"))
.with_angle(AngleDescriptor::new(2, "overhead"))
}
#[test]
fn test_angle_descriptor_creation() {
let desc = AngleDescriptor::new(0, "Camera A")
.with_language("eng")
.as_default()
.with_metadata("camera_model", "Sony A7");
assert_eq!(desc.track_index, 0);
assert_eq!(desc.label, "Camera A");
assert_eq!(desc.language.as_deref(), Some("eng"));
assert!(desc.is_default);
assert_eq!(
desc.metadata.get("camera_model").map(|s| s.as_str()),
Some("Sony A7")
);
}
#[test]
fn test_angle_group_basics() {
let group = sample_group();
assert_eq!(group.angle_count(), 3);
assert_eq!(group.active_index(), 0);
assert_eq!(group.active_track_index(), Some(0));
assert_eq!(group.default_angle_index(), Some(0));
}
#[test]
fn test_angle_group_set_active() {
let mut group = sample_group();
assert!(group.set_active(1).is_ok());
assert_eq!(group.active_index(), 1);
assert_eq!(group.active_track_index(), Some(1));
assert!(group.set_active(5).is_err());
}
#[test]
fn test_angle_group_find_by_label() {
let group = sample_group();
let (idx, desc) = group.find_by_label("SIDE").expect("should find");
assert_eq!(idx, 1);
assert_eq!(desc.track_index, 1);
}
#[test]
fn test_angle_group_find_by_track() {
let group = sample_group();
let (idx, desc) = group.find_by_track(2).expect("should find");
assert_eq!(idx, 2);
assert_eq!(desc.label, "overhead");
assert!(group.find_by_track(99).is_none());
}
#[test]
fn test_angle_group_all_tracks() {
let group = sample_group();
assert_eq!(group.all_track_indices(), vec![0, 1, 2]);
}
#[test]
fn test_angle_group_time_range() {
let group = AngleGroup::new("timed")
.with_angle(AngleDescriptor::new(0, "a"))
.with_time_range(1000, 5000);
assert_eq!(group.time_range, Some((1000, 5000)));
}
#[test]
fn test_angle_manager_add_and_query() {
let mut manager = AngleManager::new();
assert!(manager.add_group(sample_group()).is_ok());
assert_eq!(manager.group_count(), 1);
assert_eq!(manager.total_angles("scene1"), Some(3));
assert_eq!(manager.active_track("scene1"), Some(0));
assert!(manager.add_group(AngleGroup::new("scene1")).is_err());
}
#[test]
fn test_angle_manager_set_active() {
let mut manager = AngleManager::new();
manager.add_group(sample_group()).expect("should succeed");
assert!(manager.set_active_angle("scene1", 2).is_ok());
assert_eq!(manager.active_track("scene1"), Some(2));
assert!(manager.set_active_angle("nonexistent", 0).is_err());
}
#[test]
fn test_angle_manager_switch_points() {
let mut manager = AngleManager::new();
manager.add_group(sample_group()).expect("should succeed");
manager.schedule_switch(AngleSwitchPoint {
time_ticks: 5000,
sample_number: 150,
from_angle: 0,
to_angle: 1,
});
manager.schedule_switch(AngleSwitchPoint {
time_ticks: 2000,
sample_number: 60,
from_angle: 0,
to_angle: 2,
});
assert_eq!(manager.switch_points().len(), 2);
assert_eq!(manager.switch_points()[0].time_ticks, 2000);
assert_eq!(manager.switch_points()[1].time_ticks, 5000);
let next = manager.next_switch_after(3000);
assert!(next.is_some());
assert_eq!(next.expect("should exist").time_ticks, 5000);
manager.clear_switch_points();
assert!(manager.switch_points().is_empty());
}
#[test]
fn test_angle_manager_all_tracks() {
let mut manager = AngleManager::new();
manager.add_group(sample_group()).expect("should succeed");
manager
.add_group(
AngleGroup::new("scene2")
.with_angle(AngleDescriptor::new(3, "wide"))
.with_angle(AngleDescriptor::new(1, "side_shared")),
)
.expect("should succeed");
let tracks = manager.all_track_indices();
assert_eq!(tracks, vec![0, 1, 2, 3]);
}
#[test]
fn test_angle_manager_group_for_track() {
let mut manager = AngleManager::new();
manager.add_group(sample_group()).expect("should succeed");
let (group_name, angle_idx) = manager.group_for_track(2).expect("should find");
assert_eq!(group_name, "scene1");
assert_eq!(angle_idx, 2);
assert!(manager.group_for_track(99).is_none());
}
#[test]
fn test_angle_manager_group_names_order() {
let mut manager = AngleManager::new();
manager
.add_group(AngleGroup::new("beta").with_angle(AngleDescriptor::new(0, "a")))
.expect("ok");
manager
.add_group(AngleGroup::new("alpha").with_angle(AngleDescriptor::new(1, "b")))
.expect("ok");
assert_eq!(manager.group_names(), &["beta", "alpha"]);
}
#[test]
fn test_angle_group_empty_default() {
let group = AngleGroup::new("empty");
assert_eq!(group.default_angle_index(), None);
assert!(group.active_angle().is_none());
}
}