glint-mask-tools 0.1.1

Rust implementation of glint mask generation tools for UAV and aerial imagery
Documentation
/// Configuration file management for sensor definitions.
///
/// This module provides functionality to load sensor configurations
/// from TOML files, allowing users to define custom sensors without
/// modifying the library code.
use serde::{Deserialize, Serialize};
use std::collections::HashMap;
use std::path::{Path, PathBuf};

use crate::core::sensor::{Band, Sensor, SensorRegistry};
use crate::error::{GlintError, Result};

/// Configuration file format for sensor definitions
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct SensorConfigFile {
    /// Version of the configuration file format
    pub version: String,
    /// List of sensor definitions
    pub sensors: Vec<SensorConfig>,
}

/// Sensor configuration as defined in a config file
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct SensorConfig {
    /// Sensor ID
    pub id: String,
    /// Human-readable name
    pub name: String,
    /// Sensor description
    pub description: Option<String>,
    /// Bit depth
    pub bit_depth: u8,
    /// Loader type
    pub loader_type: String,
    /// Band definitions
    pub bands: Vec<BandConfig>,
    /// Loader-specific configuration
    pub loader_config: Option<HashMap<String, String>>,
}

/// Band configuration as defined in a config file
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct BandConfig {
    /// Band name
    pub name: String,
    /// Default threshold value
    pub default_threshold: f64,
    /// Optional wavelength in nanometers
    pub wavelength: Option<f64>,
    /// Optional description
    pub description: Option<String>,
}

impl From<BandConfig> for Band {
    fn from(config: BandConfig) -> Self {
        Band {
            name: config.name,
            default_threshold: config.default_threshold,
            wavelength: config.wavelength,
            description: config.description,
        }
    }
}

impl From<Band> for BandConfig {
    fn from(band: Band) -> Self {
        BandConfig {
            name: band.name,
            default_threshold: band.default_threshold,
            wavelength: band.wavelength,
            description: band.description,
        }
    }
}

impl From<SensorConfig> for Sensor {
    fn from(config: SensorConfig) -> Self {
        let bands: Vec<Band> = config.bands.into_iter().map(Band::from).collect();
        let mut sensor = Sensor::new(
            config.id,
            config.name,
            bands,
            config.bit_depth,
            config.loader_type,
        );

        if let Some(description) = config.description {
            sensor = sensor.with_description(description);
        }

        if let Some(loader_config) = config.loader_config {
            for (key, value) in loader_config {
                sensor = sensor.with_loader_config(key, value);
            }
        }

        sensor
    }
}

impl From<Sensor> for SensorConfig {
    fn from(sensor: Sensor) -> Self {
        let bands: Vec<BandConfig> = sensor.bands.into_iter().map(BandConfig::from).collect();
        let loader_config = if sensor.loader_config.is_empty() {
            None
        } else {
            Some(sensor.loader_config)
        };

        SensorConfig {
            id: sensor.id,
            name: sensor.name,
            description: sensor.description,
            bit_depth: sensor.bit_depth,
            loader_type: sensor.loader_type,
            bands,
            loader_config,
        }
    }
}

/// Configuration manager for handling sensor configuration files
#[derive(Debug)]
pub struct ConfigManager {
    /// Default configuration directory
    config_dir: Option<PathBuf>,
}

impl ConfigManager {
    /// Create a new configuration manager
    pub fn new() -> Self {
        Self { config_dir: None }
    }

    /// Create a configuration manager with a specific config directory
    pub fn with_config_dir(config_dir: PathBuf) -> Self {
        Self {
            config_dir: Some(config_dir),
        }
    }

    /// Get the default configuration directory
    pub fn default_config_dir() -> Result<PathBuf> {
        // Try to find a reasonable default config directory
        if let Some(config_dir) = dirs::config_dir() {
            Ok(config_dir.join("glint-mask-tools"))
        } else {
            // Fallback to current directory
            Ok(PathBuf::from("."))
        }
    }

    /// Get the configuration directory to use
    pub fn get_config_dir(&self) -> Result<PathBuf> {
        if let Some(ref dir) = self.config_dir {
            Ok(dir.clone())
        } else {
            Self::default_config_dir()
        }
    }

    /// Load sensor configurations from a TOML file
    pub fn load_from_file(&self, path: &Path) -> Result<Vec<Sensor>> {
        let content = std::fs::read_to_string(path).map_err(GlintError::Io)?;

        let config_file: SensorConfigFile =
            toml::from_str(&content).map_err(GlintError::Deserialization)?;

        // Validate version (for future compatibility)
        if config_file.version != "1.0" {
            return Err(GlintError::config(format!(
                "Unsupported configuration file version: {}",
                config_file.version
            )));
        }

        // Convert to sensors and validate
        let mut sensors = Vec::new();
        for sensor_config in config_file.sensors {
            let sensor = Sensor::from(sensor_config);
            sensor.validate()?;
            sensors.push(sensor);
        }

        Ok(sensors)
    }

    /// Save sensor configurations to a TOML file
    pub fn save_to_file(&self, sensors: &[Sensor], path: &Path) -> Result<()> {
        let sensor_configs: Vec<SensorConfig> = sensors
            .iter()
            .map(|s| SensorConfig::from(s.clone()))
            .collect();

        let config_file = SensorConfigFile {
            version: "1.0".to_string(),
            sensors: sensor_configs,
        };

        let content = toml::to_string_pretty(&config_file).map_err(GlintError::Serialization)?;

        // Ensure parent directory exists
        if let Some(parent) = path.parent() {
            std::fs::create_dir_all(parent).map_err(GlintError::Io)?;
        }

        std::fs::write(path, content).map_err(GlintError::Io)?;

        Ok(())
    }

    /// Load sensors from the default configuration file
    pub fn load_default_config(&self) -> Result<Vec<Sensor>> {
        let config_dir = self.get_config_dir()?;
        let config_path = config_dir.join("sensors.toml");

        if config_path.exists() {
            self.load_from_file(&config_path)
        } else {
            // Return empty vector if no config file exists
            Ok(Vec::new())
        }
    }

    /// Save sensors to the default configuration file
    pub fn save_default_config(&self, sensors: &[Sensor]) -> Result<()> {
        let config_dir = self.get_config_dir()?;
        let config_path = config_dir.join("sensors.toml");
        self.save_to_file(sensors, &config_path)
    }

    /// Create a sample configuration file with default sensors
    pub fn create_sample_config(&self, path: &Path) -> Result<()> {
        let registry = crate::core::sensor::SensorRegistry::with_defaults();
        let default_sensors: Vec<_> = registry.sensors().into_iter().cloned().collect();
        self.save_to_file(&default_sensors, path)
    }

    /// Load sensors into a registry from configuration files
    pub fn load_into_registry(&self, registry: &mut SensorRegistry) -> Result<()> {
        // Try to load from default config
        match self.load_default_config() {
            Ok(sensors) => {
                for sensor in sensors {
                    registry.register_sensor(sensor)?;
                }
            }
            Err(GlintError::Io(_)) => {
                // Config file doesn't exist, which is fine
            }
            Err(e) => return Err(e),
        }

        Ok(())
    }
}

impl Default for ConfigManager {
    fn default() -> Self {
        Self::new()
    }
}

#[cfg(test)]
mod tests {
    use super::*;
    use crate::core::sensor::bands;
    use tempfile::tempdir;

    #[test]
    fn test_sensor_config_conversion() {
        let sensor = Sensor::new(
            "test",
            "Test Sensor",
            vec![bands::red(), bands::green(), bands::blue()],
            8,
            "single_file",
        )
        .with_description("Test description");

        // Convert to config and back
        let config = SensorConfig::from(sensor.clone());
        let converted_sensor = Sensor::from(config);

        assert_eq!(sensor.id, converted_sensor.id);
        assert_eq!(sensor.name, converted_sensor.name);
        assert_eq!(sensor.description, converted_sensor.description);
        assert_eq!(sensor.bit_depth, converted_sensor.bit_depth);
        assert_eq!(sensor.loader_type, converted_sensor.loader_type);
        assert_eq!(sensor.bands.len(), converted_sensor.bands.len());
    }

    #[test]
    fn test_config_file_save_load() {
        let temp_dir = tempdir().unwrap();
        let config_path = temp_dir.path().join("sensors.toml");

        let registry = crate::core::sensor::SensorRegistry::with_defaults();
        let sensors = vec![
            registry.get_sensor("rgb").unwrap().clone(),
            registry.get_sensor("p4ms").unwrap().clone(),
        ];

        let manager = ConfigManager::new();

        // Save configuration
        manager.save_to_file(&sensors, &config_path).unwrap();
        assert!(config_path.exists());

        // Load configuration
        let loaded_sensors = manager.load_from_file(&config_path).unwrap();
        assert_eq!(loaded_sensors.len(), 2);

        // Verify sensor properties
        assert_eq!(loaded_sensors[0].id, "rgb");
        assert_eq!(loaded_sensors[1].id, "p4ms");
    }

    #[test]
    fn test_config_manager_with_directory() {
        let temp_dir = tempdir().unwrap();
        let manager = ConfigManager::with_config_dir(temp_dir.path().to_path_buf());

        let registry = crate::core::sensor::SensorRegistry::with_defaults();
        let sensors = vec![registry.get_sensor("rgb").unwrap().clone()];

        // Save and load default config
        manager.save_default_config(&sensors).unwrap();
        let loaded_sensors = manager.load_default_config().unwrap();

        assert_eq!(loaded_sensors.len(), 1);
        assert_eq!(loaded_sensors[0].id, "rgb");
    }

    #[test]
    fn test_sample_config_creation() {
        let temp_dir = tempdir().unwrap();
        let sample_path = temp_dir.path().join("sample_sensors.toml");

        let manager = ConfigManager::new();
        manager.create_sample_config(&sample_path).unwrap();

        assert!(sample_path.exists());

        // Verify we can load the sample config
        let sensors = manager.load_from_file(&sample_path).unwrap();
        assert_eq!(sensors.len(), 6); // Should have all default sensors
    }

    #[test]
    fn test_registry_integration() {
        let temp_dir = tempdir().unwrap();
        let manager = ConfigManager::with_config_dir(temp_dir.path().to_path_buf());

        // Save some sensors to config
        let registry = crate::core::sensor::SensorRegistry::with_defaults();
        let sensors = vec![
            registry.get_sensor("rgb").unwrap().clone(),
            registry.get_sensor("m3m").unwrap().clone(),
        ];
        manager.save_default_config(&sensors).unwrap();

        // Load into registry
        let mut registry = SensorRegistry::new();
        manager.load_into_registry(&mut registry).unwrap();

        assert!(registry.has_sensor("rgb"));
        assert!(registry.has_sensor("m3m"));
        assert!(!registry.has_sensor("p4ms")); // Not in our config
    }
}