guess my indentation is all over the place:
#!/usr/bin/env python3
# -*- coding: utf-8 -*-

"""
MMS Data Processing and Analysis Tool
-----------------------------------
Process and analyze Mobile Mapping System data with columns:
Time, Image, X, Y, Z, Roll, Pitch, Heading
"""

import os
import sys
import glob
import json
import logging
import argparse
from typing import Dict, List, Tuple, Any, Optional, Union
from datetime import datetime
from pathlib import Path

import numpy as np
import pandas as pd
import geopandas as gpd
from shapely.geometry import Point, LineString
import matplotlib.pyplot as plt
import seaborn as sns
from tqdm import tqdm

# Base configuration
DEFAULT_CONFIG = {
    'required_columns': ['Time', 'Image', 'X', 'Y', 'Z', 'Roll', 'Pitch', 'Heading'],
    'crs': 'EPSG:3857',  # Web Mercator as default
    'gap_threshold': 3.0,
    'output_folder': 'mms_output',
    'analyze': True,
    'visualize': True,
    'folders': {
        'plots': 'plots',
        'reports': 'reports',
        'data': 'data'
    },
    'quality_thresholds': {
        'point_spacing': {'min': 0.05, 'max': 5.0},
        'speed': {'min': 1.0, 'max': 50.0},
        'orientation': {'max_variation': 5.0}
    }
}

class MMSError(Exception):
    """Base exception for MMS processing."""
    pass

class InputError(MMSError):
    """Exception for input validation errors."""
    pass

class ProcessingError(MMSError):
    """Exception for processing errors."""
    pass

class OutputError(MMSError):
    """Exception for export errors."""
    pass

class MMSPlugin:
    """Base class for MMS processor plugins."""
    def __init__(self, name: str):
        self.name = name
        self.enabled = True
        self.logger = logging.getLogger(f'MMSProcessor.{name}')

    def initialize(self, processor: 'MMSProcessor') -> None:
        """Initialize plugin with processor instance."""
        pass

    def pre_process(self, processor: 'MMSProcessor') -> None:
        """Hook called before processing starts."""
        pass

    def post_merge(self, processor: 'MMSProcessor', 
                  merged_df: pd.DataFrame) -> pd.DataFrame:
        """Hook called after file merging."""
        return merged_df

    def post_path_creation(self, processor: 'MMSProcessor', 
                          paths: gpd.GeoDataFrame) -> gpd.GeoDataFrame:
        """Hook called after path creation."""
        return paths

    def pre_export(self, processor: 'MMSProcessor') -> None:
        """Hook called before exporting results."""
        pass

    def post_process(self, processor: 'MMSProcessor') -> None:
        """Hook called after processing completes."""
        pass

class MMSProcessor:
    """Main MMS data processor class."""
    def __init__(self, config: Dict[str, Any] = None):
        """
        Initialize MMS Processor.
        
        Args:
            config: Optional configuration dictionary to override defaults
        """
        self.config = self._initialize_config(config)
        self.logger = self._setup_logger()
        self.plugins: Dict[str, MMSPlugin] = {}
        self.data = None
        self.paths = None
        self.metrics = {}
        
        # Create output folders
        self._create_output_folders()
        
        self.logger.info("MMS Processor initialized")

    def _initialize_config(self, config: Optional[Dict[str, Any]]) -> Dict[str, Any]:
        """Initialize configuration with defaults and user overrides."""
        final_config = DEFAULT_CONFIG.copy()
        if config:
            self._validate_config(config)
            final_config.update(config)
        return final_config

    def _validate_config(self, config: Dict[str, Any]) -> None:
        """Validate configuration values."""
        if 'required_columns' in config:
            missing = set(['Time', 'Image', 'X', 'Y', 'Z', 'Roll', 'Pitch', 'Heading']) - \
                     set(config['required_columns'])
            if missing:
                raise InputError(f"Required columns missing from config: {missing}")

    def _setup_logger(self) -> logging.Logger:
        """Set up logging configuration."""
        logger = logging.getLogger('MMSProcessor')
        logger.setLevel(logging.INFO)
        
        if not logger.handlers:
            # File handler
            log_file = os.path.join(
                self.config.get('output_folder', '.'),
                'mms_processor.log'
            )
            os.makedirs(os.path.dirname(log_file), exist_ok=True)
            
            fh = logging.FileHandler(log_file)
            fh.setLevel(logging.DEBUG)
            
            # Console handler
            ch = logging.StreamHandler()
            ch.setLevel(logging.INFO)
            
            # Formatter
            formatter = logging.Formatter(
                '%(asctime)s - %(name)s - %(levelname)s - %(message)s'
            )
            fh.setFormatter(formatter)
            ch.setFormatter(formatter)
            
            logger.addHandler(fh)
            logger.addHandler(ch)
        
        return logger

    def _create_output_folders(self) -> None:
        """Create necessary output folders."""
        try:
            for folder_name, folder_path in self.config['folders'].items():
                full_path = os.path.join(self.config['output_folder'], folder_path)
                os.makedirs(full_path, exist_ok=True)
                self.logger.debug(f"Created output folder: {full_path}")
        except Exception as e:
            raise OutputError(f"Failed to create output folders: {e}")

    def register_plugin(self, plugin: MMSPlugin) -> None:
        """Register a new plugin."""
        try:
            plugin.initialize(self)
            self.plugins[plugin.name] = plugin
            self.logger.info(f"Registered plugin: {plugin.name}")
        except Exception as e:
            self.logger.error(f"Failed to register plugin {plugin.name}: {e}")

    def _run_plugin_hooks(self, hook_name: str, *args, **kwargs) -> Any:
        """Run all plugin hooks for a given hook point."""
        result = kwargs.get('data')
        for plugin in self.plugins.values():
            if plugin.enabled:
                try:
                    hook_method = getattr(plugin, hook_name)
                    if hook_method:
                        new_result = hook_method(self, result) if result is not None \
                                   else hook_method(self)
                        if new_result is not None:
                            result = new_result
                except Exception as e:
                    self.logger.warning(f"Plugin {plugin.name} {hook_name} failed: {e}")
        return result

    def _parse_crs(self, crs_input: Union[str, int]) -> str:
        """
        Parse CRS input to proper format.
        Accepts both "32632" and "EPSG:32632" formats.
        """
        if isinstance(crs_input, int):
            return f"EPSG:{crs_input}"
        if isinstance(crs_input, str):
            if crs_input.isdigit():
                return f"EPSG:{crs_input}"
            if crs_input.upper().startswith('EPSG:'):
                return crs_input.upper()
        raise InputError(f"Invalid CRS format: {crs_input}")

    def _get_crs_input(self) -> str:
        """Get CRS input from user."""
        while True:
            crs_input = input("\nEnter CRS code (default: 3857): ").strip()
            if not crs_input:
                return 'EPSG:3857'
            try:
                return self._parse_crs(crs_input)
            except InputError:
                print("Invalid CRS code. Please enter a valid EPSG number.")
    def process(self, input_folder: str, file_extension: str = 'txt') -> None:
        """
        Main processing pipeline.
        
        Args:
            input_folder: Path to folder containing input files
            file_extension: File extension of input files ('txt' or 'csv')
        """
        try:
            self.logger.info(f"Starting processing of {input_folder}")
            
            # Run pre-process hooks
            self._run_plugin_hooks('pre_process')
            
            with tqdm(total=7, desc="Processing", unit="step") as pbar:
                # Step 1: Data Import
                merged_df = self._merge_files(input_folder, file_extension)
                pbar.update(1)
                pbar.set_description(f"Merged {len(merged_df)} records")

                # Step 2: CRS Selection
                crs = self._get_crs_input()
                self.config['crs'] = crs
                pbar.update(1)

                # Step 3: Create Geometries
                self.data = self._create_point_geometries(merged_df)
                pbar.update(1)
                pbar.set_description(f"Created {len(self.data)} points")

                # Step 4: Create Paths
                self.paths = self._create_paths(self.data)
                pbar.update(1)
                pbar.set_description(f"Created {len(self.paths)} paths")

                # Step 5: Calculate Metrics
                self.metrics = self._calculate_metrics()
                pbar.update(1)

                # Step 6: Generate Reports
                self._generate_reports()
                pbar.update(1)

                # Step 7: Create Visualizations
                if self.config['visualize']:
                    self._create_visualizations()
                pbar.update(1)

            # Run post-process hooks
            self._run_plugin_hooks('post_process')

            self.logger.info("Processing completed successfully")
            print(f"\nProcessing complete. Output in: {self.config['output_folder']}")

        except Exception as e:
            self.logger.error(f"Processing failed: {e}")
            raise ProcessingError(f"Processing failed: {e}")

    def _merge_files(self, input_folder: str, file_extension: str) -> pd.DataFrame:
        """
        Merge multiple data files from input folder.
        
        Args:
            input_folder: Path to folder containing input files
            file_extension: File extension to process ('txt' or 'csv')
            
        Returns:
            pd.DataFrame: Merged data from all files
        """
        self.logger.info(f"Merging files from {input_folder}")
        
        # Validate input folder
        if not os.path.isdir(input_folder):
            raise InputError(f"Input folder does not exist: {input_folder}")
        
        # Get list of files
        pattern = os.path.join(input_folder, f"*.{file_extension}")
        files = glob.glob(pattern)
        
        if not files:
            raise InputError(f"No {file_extension} files found in {input_folder}")
        
        # Read and merge files
        dfs = []
        with tqdm(files, desc="Merging files") as pbar:
            for file in pbar:
                try:
                    # Read file based on extension
                    if file_extension.lower() == 'csv':
                        df = pd.read_csv(file)
                    else:  # txt
                        df = pd.read_csv(file, delimiter='\t')
                    
                    # Validate columns
                    missing_cols = set(self.config['required_columns']) - set(df.columns)
                    if missing_cols:
                        self.logger.warning(
                            f"File {file} missing columns: {missing_cols}. Skipping."
                        )
                        continue
                    
                    # Basic data validation
                    self._validate_data(df, file)
                    
                    df['source_file'] = os.path.basename(file)
                    dfs.append(df)
                    
                except Exception as e:
                    self.logger.error(f"Error reading file {file}: {e}")
                    continue
        
        if not dfs:
            raise InputError("No valid data files could be processed")
        
        # Merge all dataframes
        merged_df = pd.concat(dfs, ignore_index=True)
        
        # Process Time column
        try:
            if not pd.api.types.is_datetime64_any_dtype(merged_df['Time']):
                merged_df['Time'] = pd.to_datetime(
                    merged_df['Time'],
                    format='%Y%m%d%H%M%S.%f'
                )
        except Exception as e:
            self.logger.warning(f"Could not parse Time column: {e}")
        
        # Sort by time
        merged_df = merged_df.sort_values('Time')
        
        self.logger.info(
            f"Successfully merged {len(files)} files. "
            f"Total records: {len(merged_df)}"
        )
        
        # Run post-merge hooks
        merged_df = self._run_plugin_hooks('post_merge', data=merged_df)
        
        return merged_df

    def _validate_data(self, df: pd.DataFrame, file_name: str) -> None:
        """
        Validate input data.
        
        Args:
            df: DataFrame to validate
            file_name: Source file name for logging
        """
        # Check for empty data
        if df.empty:
            raise InputError(f"Empty data in file: {file_name}")
        
        # Check coordinate validity
        if df[['X', 'Y', 'Z']].isna().any().any():
            self.logger.warning(f"Found missing coordinates in {file_name}")
        
        # Check angle ranges
        for col in ['Roll', 'Pitch']:
            if (df[col].abs() > 90).any():
                self.logger.warning(f"Found suspicious {col} values in {file_name}")
        
        if ((df['Heading'] < 0) | (df['Heading'] > 360)).any():
            self.logger.warning(f"Found invalid Heading values in {file_name}")

    def _create_point_geometries(self, df: pd.DataFrame) -> gpd.GeoDataFrame:
        """
        Create point geometries from coordinate data.
        
        Args:
            df: DataFrame containing X, Y coordinates
            
        Returns:
            gpd.GeoDataFrame: GeoDataFrame with point geometries
        """
        self.logger.info("Creating point geometries")
        
        try:
            # Create Point geometries
            geometry = [Point(xy) for xy in zip(df['X'], df['Y'])]
            
            # Create GeoDataFrame
            gdf = gpd.GeoDataFrame(
                df,
                geometry=geometry,
                crs=self.config['crs']
            )
            
            # Validate geometries
            invalid_geoms = ~gdf.is_valid
            if invalid_geoms.any():
                invalid_count = invalid_geoms.sum()
                self.logger.warning(f"Found {invalid_count} invalid geometries")
                # Drop invalid geometries
                gdf = gdf[~invalid_geoms].copy()
            
            self.logger.info(f"Created {len(gdf)} valid point geometries")
            return gdf
            
        except Exception as e:
            raise ProcessingError(f"Failed to create geometries: {e}")

    def _create_paths(self, gdf: gpd.GeoDataFrame) -> gpd.GeoDataFrame:
        """
        Create paths from point geometries based on time gaps.
        
        Args:
            gdf: GeoDataFrame with point geometries
            
        Returns:
            gpd.GeoDataFrame: GeoDataFrame with path geometries
        """
        self.logger.info("Creating paths")
        
        try:
            # Sort by time
            gdf_sorted = gdf.sort_values('Time').copy()
            
            # Identify path breaks
            time_diff = gdf_sorted['Time'].diff().dt.total_seconds()
            path_breaks = (time_diff > self.config['gap_threshold']) | (time_diff.isna())
            
            # Assign path IDs
            gdf_sorted['PathID'] = path_breaks.cumsum()
            
            # Create paths
            paths_list = []
            path_properties = []
            
            for path_id, path_points in gdf_sorted.groupby('PathID'):
                if len(path_points) < 2:
                    self.logger.debug(f"Skipping PathID {path_id} - insufficient points")
                    continue
                
                try:
                    # Create LineString
                    line = LineString(path_points.geometry.tolist())
                    if not line.is_valid:
                        self.logger.warning(f"Invalid path geometry for PathID {path_id}")
                        continue
                    
                    # Calculate path properties
                    duration = (
                        path_points['Time'].max() - 
                        path_points['Time'].min()
                    ).total_seconds()
                    
                    length_km = line.length / 1000
                    avg_speed = length_km / (duration / 3600) if duration > 0 else 0
                    
                    path_properties.append({
                        'PathID': path_id,
                        'length_km': length_km,
                        'duration_seconds': duration,
                        'avg_speed_kmh': avg_speed,
                        'point_count': len(path_points),
                        'start_time': path_points['Time'].min(),
                        'end_time': path_points['Time'].max(),
                        'photos': path_points['Image'].nunique(),
                        'min_z': path_points['Z'].min(),
                        'max_z': path_points['Z'].max(),
                    })
                    
                    paths_list.append(line)
                    
                except Exception as e:
                    self.logger.warning(f"Error processing PathID {path_id}: {e}")
                    continue
            
            if not paths_list:
                raise ProcessingError("No valid paths could be created")
            
            # Create paths GeoDataFrame
            paths_gdf = gpd.GeoDataFrame(
                path_properties,
                geometry=paths_list,
                crs=gdf.crs
            )
            
            # Store original points with path IDs
            self.data = gdf_sorted
            
            self.logger.info(
                f"Successfully created {len(paths_gdf)} paths from "
                f"{len(gdf)} points"
            )
            
            # Run post-path-creation hooks
            paths_gdf = self._run_plugin_hooks('post_path_creation', data=paths_gdf)
            
            return paths_gdf
            
        except Exception as e:
            raise ProcessingError(f"Failed to create paths: {e}")            
    def _calculate_metrics(self) -> Dict[str, pd.DataFrame]:
        """
        Calculate comprehensive set of metrics for paths and points.
        """
        self.logger.info("Calculating metrics")
        
        try:
            metrics = {
                'trajectory': self._analyze_trajectory(),
                'camera': self._analyze_camera_stability(),
                'coverage': self._analyze_coverage(),
                'quality': self._calculate_quality_metrics()
            }
            
            return metrics
            
        except Exception as e:
            self.logger.error(f"Error calculating metrics: {e}")
            raise ProcessingError(f"Failed to calculate metrics: {e}")

    def _analyze_trajectory(self) -> Dict[str, pd.DataFrame]:
        """
        Analyze trajectory characteristics.
        """
        try:
            trajectory_metrics = []
            
            for path_id, path_data in self.data.groupby('PathID'):
                if len(path_data) < 2:
                    continue
                
                # Calculate basic metrics
                points = path_data.geometry.tolist()
                distances = [p1.distance(p2) for p1, p2 in zip(points[:-1], points[1:])]
                times = np.diff(path_data['Time'].values).astype('timedelta64[s]').astype(float)
                speeds = np.array(distances) / times
                
                # Movement patterns
                heading_changes = np.abs(np.diff(path_data['Heading']))
                straight_sections = np.sum(heading_changes < 1.0) / len(heading_changes)
                sharp_turns = np.sum(heading_changes > 45.0)
                
                # Path characteristics
                start_point = points[0]
                end_point = points[-1]
                direct_distance = start_point.distance(end_point)
                actual_distance = sum(distances)
                sinuosity = actual_distance / max(direct_distance, 0.1)
                
                # Speed patterns
                speed_consistency = 1 - (np.std(speeds) / np.mean(speeds))
                accelerations = np.diff(speeds)
                smooth_movement = 1 - (np.std(accelerations) / max(np.mean(speeds), 0.1))
                
                trajectory_metrics.append({
                    'PathID': path_id,
                    'point_spacing_mean': np.mean(distances),
                    'point_spacing_std': np.std(distances),
                    'speed_mean': np.mean(speeds),
                    'speed_std': np.std(speeds),
                    'speed_consistency': speed_consistency,
                    'movement_smoothness': smooth_movement,
                    'straight_sections_ratio': straight_sections,
                    'sharp_turns_count': sharp_turns,
                    'path_sinuosity': sinuosity,
                    'acceleration_std': np.std(accelerations)
                })
            
            return {'characteristics': pd.DataFrame(trajectory_metrics)}
            
        except Exception as e:
            self.logger.error(f"Error in trajectory analysis: {e}")
            return {'characteristics': pd.DataFrame()}

    def _analyze_camera_stability(self) -> Dict[str, pd.DataFrame]:
        """
        Analyze camera stability and orientation patterns.
        """
        try:
            stability_metrics = []
            
            for path_id, path_data in self.data.groupby('PathID'):
                if len(path_data) < 2:
                    continue
                
                # Basic orientation metrics
                roll_changes = np.diff(path_data['Roll'])
                pitch_changes = np.diff(path_data['Pitch'])
                heading_changes = np.diff(path_data['Heading'])
                
                # Combined orientation changes
                orientation_changes = np.sqrt(
                    roll_changes**2 + 
                    pitch_changes**2 + 
                    heading_changes**2
                )
                
                # Stability periods
                stable_threshold = 0.1  # degrees
                stable_periods = np.sum(orientation_changes < stable_threshold) / len(orientation_changes)
                
                # Oscillation detection
                roll_oscillations = np.sum(np.diff(np.signbit(roll_changes)))
                pitch_oscillations = np.sum(np.diff(np.signbit(pitch_changes)))
                
                stability_metrics.append({
                    'PathID': path_id,
                    'roll_stability': 1 - (np.std(roll_changes) / max(np.mean(np.abs(roll_changes)), 0.001)),
                    'pitch_stability': 1 - (np.std(pitch_changes) / max(np.mean(np.abs(pitch_changes)), 0.001)),
                    'heading_stability': 1 - (np.std(heading_changes) / max(np.mean(np.abs(heading_changes)), 0.001)),
                    'orientation_change_mean': np.mean(orientation_changes),
                    'orientation_change_std': np.std(orientation_changes),
                    'stable_ratio': stable_periods,
                    'roll_oscillations': roll_oscillations,
                    'pitch_oscillations': pitch_oscillations,
                    'overall_stability': 1 - (np.std(orientation_changes) / max(np.mean(orientation_changes), 0.001))
                })
            
            return {'stability': pd.DataFrame(stability_metrics)}
            
        except Exception as e:
            self.logger.error(f"Error in camera stability analysis: {e}")
            return {'stability': pd.DataFrame()}

    def _analyze_coverage(self) -> Dict[str, pd.DataFrame]:
        """
        Analyze coverage completeness and quality.
        """
        try:
            coverage_metrics = []
            
            for path_id, path_data in self.data.groupby('PathID'):
                if len(path_data) < 2:
                    continue
                
                # Photo spacing
                points = path_data.geometry.tolist()
                distances = [p1.distance(p2) for p1, p2 in zip(points[:-1], points[1:])]
                
                # Time intervals
                time_diffs = np.diff(path_data['Time'].values).astype('timedelta64[s]').astype(float)
                
                # Photo paths analysis
                photo_paths = path_data['Image'].tolist()
                unique_folders = len(set(os.path.dirname(p) for p in photo_paths))
                
                # Calculate coverage metrics
                spacing_consistency = 1 - (np.std(distances) / np.mean(distances))
                timing_consistency = 1 - (np.std(time_diffs) / np.mean(time_diffs))
                
                coverage_metrics.append({
                    'PathID': path_id,
                    'photo_count': len(photo_paths),
                    'photo_spacing_mean': np.mean(distances),
                    'photo_spacing_std': np.std(distances),
                    'spacing_consistency': spacing_consistency,
                    'timing_consistency': timing_consistency,
                    'unique_folders': unique_folders,
                    'photos_per_meter': len(photo_paths) / sum(distances),
                    'max_photo_gap': max(distances),
                    'time_between_photos_mean': np.mean(time_diffs),
                    'time_between_photos_std': np.std(time_diffs)
                })
            
            return {'coverage': pd.DataFrame(coverage_metrics)}
            
        except Exception as e:
            self.logger.error(f"Error in coverage analysis: {e}")
            return {'coverage': pd.DataFrame()}

    def _calculate_quality_metrics(self) -> Dict[str, pd.DataFrame]:
        """
        Calculate comprehensive quality metrics.
        """
        try:
            quality_metrics = []
            
            for path_id in self.paths.index:
                path_data = self.data[self.data['PathID'] == path_id]
                
                # Get metrics from different analyses
                trajectory_quality = self._calculate_trajectory_quality(path_data)
                camera_quality = self._calculate_camera_quality(path_data)
                coverage_quality = self._calculate_coverage_quality(path_data)
                
                # Combine metrics
                quality_metrics.append({
                    'PathID': path_id,
                    **trajectory_quality,
                    **camera_quality,
                    **coverage_quality,
                    'overall_quality': np.mean([
                        trajectory_quality['trajectory_score'],
                        camera_quality['camera_score'],
                        coverage_quality['coverage_score']
                    ])
                })
            
            return {'quality': pd.DataFrame(quality_metrics)}
            
        except Exception as e:
            self.logger.error(f"Error calculating quality metrics: {e}")
            return {'quality': pd.DataFrame()}

    def _calculate_trajectory_quality(self, path_data: pd.DataFrame) -> Dict[str, float]:
        """Calculate trajectory quality score."""
        if len(path_data) < 2:
            return {'trajectory_score': 0.0}
            
        try:
            # Calculate metrics
            points = path_data.geometry.tolist()
            distances = [p1.distance(p2) for p1, p2 in zip(points[:-1], points[1:])]
            
            # Spacing consistency
            spacing_cv = np.std(distances) / np.mean(distances)
            spacing_score = max(0, 1 - spacing_cv)
            
            # Movement smoothness
            time_diffs = np.diff(path_data['Time'].values).astype('timedelta64[s]').astype(float)
            speeds = np.array(distances) / time_diffs
            speed_cv = np.std(speeds) / np.mean(speeds)
            smoothness_score = max(0, 1 - speed_cv)
            
            # Path straightness
            heading_changes = np.abs(np.diff(path_data['Heading']))
            straightness_score = np.sum(heading_changes < 5.0) / len(heading_changes)
            
            # Combine scores
            trajectory_score = np.mean([
                spacing_score,
                smoothness_score,
                straightness_score
            ])
            
            return {
                'trajectory_score': trajectory_score,
                'spacing_score': spacing_score,
                'smoothness_score': smoothness_score,
                'straightness_score': straightness_score
            }
            
        except Exception as e:
            self.logger.error(f"Error in trajectory quality calculation: {e}")
            return {'trajectory_score': 0.0}

    def _calculate_camera_quality(self, path_data: pd.DataFrame) -> Dict[str, float]:
        """Calculate camera quality score."""
        if len(path_data) < 2:
            return {'camera_score': 0.0}
            
        try:
            # Roll stability
            roll_changes = np.abs(np.diff(path_data['Roll']))
            roll_score = max(0, 1 - np.mean(roll_changes) / self.config['quality_thresholds']['orientation']['max_variation'])
            
            # Pitch stability
            pitch_changes = np.abs(np.diff(path_data['Pitch']))
            pitch_score = max(0, 1 - np.mean(pitch_changes) / self.config['quality_thresholds']['orientation']['max_variation'])
            
            # Heading consistency
            heading_changes = np.abs(np.diff(path_data['Heading']))
            heading_score = max(0, 1 - np.mean(heading_changes) / self.config['quality_thresholds']['orientation']['max_variation'])
            
            # Combined score
            camera_score = np.mean([
                roll_score,
                pitch_score,
                heading_score
            ])
            
            return {
                'camera_score': camera_score,
                'roll_score': roll_score,
                'pitch_score': pitch_score,
                'heading_score': heading_score
            }
            
        except Exception as e:
            self.logger.error(f"Error in camera quality calculation: {e}")
            return {'camera_score': 0.0}

    def _calculate_coverage_quality(self, path_data: pd.DataFrame) -> Dict[str, float]:
        """Calculate coverage quality score."""
        if len(path_data) < 2:
            return {'coverage_score': 0.0}
            
        try:
            # Photo spacing
            points = path_data.geometry.tolist()
            distances = [p1.distance(p2) for p1, p2 in zip(points[:-1], points[1:])]
            
            # Spacing consistency
            spacing_cv = np.std(distances) / np.mean(distances)
            spacing_score = max(0, 1 - spacing_cv)
            
            # Photo density
            path_length = sum(distances)
            density = len(path_data) / path_length
            density_score = min(1.0, density / 2.0)  # 2 points per meter as reference
            
            # Timing consistency
            time_diffs = np.diff(path_data['Time'].values).astype('timedelta64[s]').astype(float)
            timing_cv = np.std(time_diffs) / np.mean(time_diffs)
            timing_score = max(0, 1 - timing_cv)
            
            # Combined score
            coverage_score = np.mean([
                spacing_score,
                density_score,
                timing_score
            ])
            
            return {
                'coverage_score': coverage_score,
                'spacing_score': spacing_score,
                'density_score': density_score,
                'timing_score': timing_score
            }
            
        except Exception as e:
            self.logger.error(f"Error in coverage quality calculation: {e}")
            return {'coverage_score': 0.0}                
            class MMSQualityScoring:
    """Quality scoring system for MMS data."""
    
    def __init__(self):
        self.thresholds = {
            'point_spacing': {
                'excellent': 0.2,  # meters
                'good': 0.5,
                'acceptable': 1.0
            },
            'orientation': {
                'roll_variation': 1.0,   # degrees
                'pitch_variation': 1.0,
                'heading_variation': 5.0
            },
            'speed': {
                'min': 1.0,      # km/h
                'max': 50.0,     # km/h
                'variation': 30  # percent
            },
            'photo_spacing': {
                'optimal': 2.0,   # meters
                'acceptable': 5.0
            }
        }
        
        self.weights = {
            'trajectory': {
                'spacing': 0.3,
                'smoothness': 0.4,
                'straightness': 0.3
            },
            'camera': {
                'roll': 0.35,
                'pitch': 0.35,
                'heading': 0.30
            },
            'coverage': {
                'spacing': 0.4,
                'density': 0.3,
                'timing': 0.3
            },
            'overall': {
                'trajectory': 0.35,
                'camera': 0.35,
                'coverage': 0.30
            }
        }

    def calculate_path_quality(self, path_data: pd.DataFrame) -> Dict[str, Any]:
        """Calculate comprehensive quality metrics for a path."""
        try:
            # Calculate individual quality components
            trajectory_quality = self._assess_trajectory_quality(path_data)
            camera_quality = self._assess_camera_quality(path_data)
            coverage_quality = self._assess_coverage_quality(path_data)
            
            # Calculate overall quality
            overall_quality = (
                trajectory_quality['score'] * self.weights['overall']['trajectory'] +
                camera_quality['score'] * self.weights['overall']['camera'] +
                coverage_quality['score'] * self.weights['overall']['coverage']
            )
            
            quality_category = self._get_quality_category(overall_quality)
            
            # Identify quality issues
            quality_issues = self._identify_quality_issues(
                trajectory_quality,
                camera_quality,
                coverage_quality
            )
            
            return {
                'overall_quality': overall_quality,
                'quality_category': quality_category,
                'trajectory_quality': trajectory_quality,
                'camera_quality': camera_quality,
                'coverage_quality': coverage_quality,
                'quality_issues': quality_issues
            }
            
        except Exception as e:
            logging.error(f"Error calculating path quality: {e}")
            return {}

    def _assess_trajectory_quality(self, path_data: pd.DataFrame) -> Dict[str, float]:
        """Assess trajectory quality metrics."""
        try:
            # Point spacing analysis
            points = path_data.geometry.tolist()
            distances = [p1.distance(p2) for p1, p2 in zip(points[:-1], points[1:])]
            
            spacing_score = self._score_point_spacing(
                np.mean(distances),
                np.std(distances)
            )
            
            # Movement smoothness
            time_diffs = np.diff(path_data['Time'].values).astype('timedelta64[s]').astype(float)
            speeds = np.array(distances) / time_diffs
            smoothness_score = self._score_movement_smoothness(speeds)
            
            # Path straightness
            heading_changes = np.abs(np.diff(path_data['Heading']))
            straightness_score = self._score_path_straightness(heading_changes)
            
            # Combined trajectory score
            trajectory_score = (
                spacing_score * self.weights['trajectory']['spacing'] +
                smoothness_score * self.weights['trajectory']['smoothness'] +
                straightness_score * self.weights['trajectory']['straightness']
            )
            
            return {
                'score': trajectory_score,
                'spacing_score': spacing_score,
                'smoothness_score': smoothness_score,
                'straightness_score': straightness_score,
                'metrics': {
                    'mean_spacing': np.mean(distances),
                    'spacing_std': np.std(distances),
                    'mean_speed': np.mean(speeds),
                    'speed_variation': np.std(speeds) / np.mean(speeds),
                    'sharp_turns': np.sum(heading_changes > 45.0)
                }
            }
            
        except Exception as e:
            logging.error(f"Error in trajectory quality assessment: {e}")
            return {'score': 0.0}

    def _assess_camera_quality(self, path_data: pd.DataFrame) -> Dict[str, float]:
        """Assess camera stability quality."""
        try:
            # Orientation stability analysis
            roll_score = self._score_orientation_stability(
                path_data['Roll'],
                self.thresholds['orientation']['roll_variation']
            )
            
            pitch_score = self._score_orientation_stability(
                path_data['Pitch'],
                self.thresholds['orientation']['pitch_variation']
            )
            
            heading_score = self._score_orientation_stability(
                path_data['Heading'],
                self.thresholds['orientation']['heading_variation']
            )
            
            # Combined camera score
            camera_score = (
                roll_score * self.weights['camera']['roll'] +
                pitch_score * self.weights['camera']['pitch'] +
                heading_score * self.weights['camera']['heading']
            )
            
            return {
                'score': camera_score,
                'roll_score': roll_score,
                'pitch_score': pitch_score,
                'heading_score': heading_score,
                'metrics': {
                    'roll_variation': np.std(path_data['Roll']),
                    'pitch_variation': np.std(path_data['Pitch']),
                    'heading_variation': np.std(path_data['Heading'])
                }
            }
            
        except Exception as e:
            logging.error(f"Error in camera quality assessment: {e}")
            return {'score': 0.0}

    def _assess_coverage_quality(self, path_data: pd.DataFrame) -> Dict[str, float]:
        """Assess coverage quality."""
        try:
            # Photo spacing analysis
            points = path_data.geometry.tolist()
            distances = [p1.distance(p2) for p1, p2 in zip(points[:-1], points[1:])]
            
            spacing_score = self._score_photo_spacing(distances)
            
            # Photo density
            path_length = sum(distances)
            density_score = self._score_photo_density(len(path_data), path_length)
            
            # Timing consistency
            time_diffs = np.diff(path_data['Time'].values).astype('timedelta64[s]').astype(float)
            timing_score = self._score_timing_consistency(time_diffs)
            
            # Combined coverage score
            coverage_score = (
                spacing_score * self.weights['coverage']['spacing'] +
                density_score * self.weights['coverage']['density'] +
                timing_score * self.weights['coverage']['timing']
            )
            
            return {
                'score': coverage_score,
                'spacing_score': spacing_score,
                'density_score': density_score,
                'timing_score': timing_score,
                'metrics': {
                    'mean_spacing': np.mean(distances),
                    'spacing_std': np.std(distances),
                    'photos_per_meter': len(path_data) / path_length,
                    'max_gap': max(distances),
                    'timing_variation': np.std(time_diffs) / np.mean(time_diffs)
                }
            }
            
        except Exception as e:
            logging.error(f"Error in coverage quality assessment: {e}")
            return {'score': 0.0}

    def _score_point_spacing(self, mean_spacing: float, std_spacing: float) -> float:
        """Score point spacing quality."""
        # Base score based on mean spacing
        if mean_spacing <= self.thresholds['point_spacing']['excellent']:
            base_score = 1.0
        elif mean_spacing <= self.thresholds['point_spacing']['good']:
            base_score = 0.8
        elif mean_spacing <= self.thresholds['point_spacing']['acceptable']:
            base_score = 0.6
        else:
            base_score = 0.4
        
        # Penalize for inconsistent spacing
        consistency_penalty = min(std_spacing / mean_spacing, 1.0)
        
        return base_score * (1 - consistency_penalty * 0.5)

    def _score_movement_smoothness(self, speeds: np.ndarray) -> float:
        """Score movement smoothness."""
        if len(speeds) < 2:
            return 0.0
        
        # Check speed range
        speed_range_score = self._score_speed_range(speeds)
        
        # Check speed consistency
        speed_variation = np.std(speeds) / np.mean(speeds)
        consistency_score = max(0, 1 - speed_variation / (self.thresholds['speed']['variation'] / 100))
        
        return np.mean([speed_range_score, consistency_score])

    def _score_speed_range(self, speeds: np.ndarray) -> float:
        """Score speed range appropriateness."""
        mean_speed = np.mean(speeds)
        
        if (mean_speed < self.thresholds['speed']['min'] or 
            mean_speed > self.thresholds['speed']['max']):
            return 0.0
        
        # Score based on how close to optimal speed range
        optimal_speed = (self.thresholds['speed']['min'] + self.thresholds['speed']['max']) / 2
        speed_difference = abs(mean_speed - optimal_speed)
        max_difference = (self.thresholds['speed']['max'] - self.thresholds['speed']['min']) / 2
        
        return max(0, 1 - speed_difference / max_difference)

    def _score_path_straightness(self, heading_changes: np.ndarray) -> float:
        """Score path straightness."""
        if len(heading_changes) < 1:
            return 0.0
        
        # Calculate percentage of small heading changes
        small_changes = np.sum(heading_changes < 5.0) / len(heading_changes)
        
        # Penalize sharp turns
        sharp_turns = np.sum(heading_changes > 45.0) / len(heading_changes)
        
        return small_changes * (1 - sharp_turns)

    def _score_orientation_stability(self, values: np.ndarray, threshold: float) -> float:
        """Score orientation stability."""
        changes = np.abs(np.diff(values))
        
        # Score based on variation magnitude
        variation_score = max(0, 1 - np.mean(changes) / threshold)
        
        # Score based on consistency
        consistency_score = max(0, 1 - np.std(changes) / threshold)
        
        return np.mean([variation_score, consistency_score])

    def _score_photo_spacing(self, distances: List[float]) -> float:
        """Score photo spacing quality."""
        if not distances:
            return 0.0
        
        mean_spacing = np.mean(distances)
        
        # Score based on mean spacing
        if mean_spacing <= self.thresholds['photo_spacing']['optimal']:
            spacing_score = 1.0
        elif mean_spacing <= self.thresholds['photo_spacing']['acceptable']:
            spacing_score = 0.7
        else:
            spacing_score = 0.4
        
        # Penalize for inconsistent spacing
        consistency_penalty = min(np.std(distances) / mean_spacing, 1.0)
        
        return spacing_score * (1 - consistency_penalty * 0.5)

    def _score_photo_density(self, num_photos: int, path_length: float) -> float:
        """Score photo density."""
        photos_per_meter = num_photos / max(path_length, 0.1)
        
        if photos_per_meter >= 0.5:  # At least one photo every 2 meters
            return 1.0
        elif photos_per_meter >= 0.2:  # At least one photo every 5 meters
            return 0.7
        elif photos_per_meter >= 0.1:  # At least one photo every 10 meters
            return 0.4
        else:
            return 0.2

    def _score_timing_consistency(self, time_diffs: np.ndarray) -> float:
        """Score timing consistency."""
        if len(time_diffs) < 2:
            return 0.0
        
        # Calculate coefficient of variation
        cv = np.std(time_diffs) / np.mean(time_diffs)
        
        return max(0, 1 - cv)

    def _get_quality_category(self, quality_score: float) -> str:
        """Get quality category based on score."""
        if quality_score >= 0.9:
            return "Excellent"
        elif quality_score >= 0.8:
            return "Very Good"
        elif quality_score >= 0.7:
            return "Good"
        elif quality_score >= 0.6:
            return "Acceptable"
        else:
            return "Poor"

    def _identify_quality_issues(self, trajectory_quality: Dict[str, Any],
                               camera_quality: Dict[str, Any],
                               coverage_quality: Dict[str, Any]) -> List[str]:
        """Identify specific quality issues."""
        issues = []
        
        # Trajectory issues
        if trajectory_quality.get('spacing_score', 0) < 0.6:
            issues.append("Irregular point spacing")
        if trajectory_quality.get('smoothness_score', 0) < 0.6:
            issues.append("Uneven movement")
        if trajectory_quality.get('straightness_score', 0) < 0.6:
            issues.append("Excessive turning")
        
        # Camera issues
        if camera_quality.get('roll_score', 0) < 0.6:
            issues.append("Unstable roll")
        if camera_quality.get('pitch_score', 0) < 0.6:
            issues.append("Unstable pitch")
        if camera_quality.get('heading_score', 0) < 0.6:
            issues.append("Inconsistent heading")
        
        # Coverage issues
        if coverage_quality.get('spacing_score', 0) < 0.6:
            issues.append("Irregular photo spacing")
        if coverage_quality.get('density_score', 0) < 0.6:
            issues.append("Low photo density")
        if coverage_quality.get('timing_score', 0) < 0.6:
            issues.append("Irregular timing")
        
        return issues
        class MMSVisualization:
    """Visualization system for MMS data analysis."""
    
    def __init__(self, output_folder: str):
        self.output_folder = output_folder
        self.plots_folder = os.path.join(output_folder, 'plots')
        os.makedirs(self.plots_folder, exist_ok=True)
        
        # Set up plotting style
        plt.style.use('seaborn')
        plt.rcParams['figure.figsize'] = (12, 8)
        plt.rcParams['figure.dpi'] = 300

    def create_visualizations(self, data: pd.DataFrame, paths: gpd.GeoDataFrame, 
                            metrics: Dict[str, pd.DataFrame]) -> None:
        """Generate all visualizations."""
        try:
            # Create all visualization types
            self._create_trajectory_plots(paths, data)
            self._create_camera_plots(data)
            self._create_coverage_plots(paths, data)
            self._create_quality_plots(metrics)
            self._create_summary_dashboard(data, paths, metrics)
            
        except Exception as e:
            logging.error(f"Error creating visualizations: {e}")

    def _create_trajectory_plots(self, paths: gpd.GeoDataFrame, 
                               data: pd.DataFrame) -> None:
        """Create trajectory-related visualizations."""
        try:
            # 1. Path Overview Map
            fig, ax = plt.subplots(figsize=(15, 15))
            paths.plot(ax=ax, column='length_km', cmap='viridis', 
                      legend=True, legend_kwds={'label': 'Path Length (km)'})
            ax.set_title('Path Overview')
            ax.axis('equal')
            self._save_plot(fig, 'path_overview.png')

            # 2. Path Length Distribution
            fig, ax = plt.subplots()
            sns.histplot(data=paths, x='length_km', bins=30)
            ax.set_title('Path Length Distribution')
            ax.set_xlabel('Length (km)')
            ax.set_ylabel('Count')
            self._save_plot(fig, 'path_length_distribution.png')

            # 3. Speed Profile
            fig, ax = plt.subplots()
            speeds = paths['avg_speed_kmh']
            ax.plot(speeds.index, speeds.values, marker='o', alpha=0.6)
            ax.set_title('Speed Profile by Path')
            ax.set_xlabel('Path ID')
            ax.set_ylabel('Average Speed (km/h)')
            self._save_plot(fig, 'speed_profile.png')

        except Exception as e:
            logging.error(f"Error creating trajectory plots: {e}")

    def _create_camera_plots(self, data: pd.DataFrame) -> None:
        """Create camera orientation visualizations."""
        try:
            # 1. Orientation Distributions
            fig, (ax1, ax2, ax3) = plt.subplots(1, 3, figsize=(15, 5))
            
            sns.histplot(data=data, x='Roll', bins=50, ax=ax1)
            ax1.set_title('Roll Distribution')
            
            sns.histplot(data=data, x='Pitch', bins=50, ax=ax2)
            ax2.set_title('Pitch Distribution')
            
            sns.histplot(data=data, x='Heading', bins=50, ax=ax3)
            ax3.set_title('Heading Distribution')
            
            plt.tight_layout()
            self._save_plot(fig, 'orientation_distributions.png')

            # 2. Camera Stability Over Time
            fig, ax = plt.subplots(figsize=(15, 6))
            ax.plot(data.index, data['Roll'], label='Roll', alpha=0.6)
            ax.plot(data.index, data['Pitch'], label='Pitch', alpha=0.6)
            ax.set_title('Camera Orientation Stability')
            ax.set_xlabel('Point Index')
            ax.set_ylabel('Angle (degrees)')
            ax.legend()
            self._save_plot(fig, 'camera_stability.png')

        except Exception as e:
            logging.error(f"Error creating camera plots: {e}")

    def _create_coverage_plots(self, paths: gpd.GeoDataFrame, 
                             data: pd.DataFrame) -> None:
        """Create coverage analysis visualizations."""
        try:
            # 1. Point Density Heatmap
            fig, ax = plt.subplots(figsize=(15, 15))
            
            points = data.geometry.tolist()
            x = [p.x for p in points]
            y = [p.y for p in points]
            
            plt.hist2d(x, y, bins=50, cmap='viridis')
            plt.colorbar(label='Point Density')
            ax.set_title('Point Density Distribution')
            ax.set_xlabel('X Coordinate')
            ax.set_ylabel('Y Coordinate')
            self._save_plot(fig, 'point_density.png')

            # 2. Photo Spacing Analysis
            fig, ax = plt.subplots()
            for path_id in paths.index:
                path_data = data[data['PathID'] == path_id]
                if len(path_data) > 1:
                    distances = [
                        p1.distance(p2) 
                        for p1, p2 in zip(path_data.geometry[:-1], 
                                        path_data.geometry[1:])
                    ]
                    sns.histplot(data=distances, alpha=0.3, 
                               label=f'Path {path_id}', ax=ax)
            
            ax.set_title('Photo Spacing Distribution')
            ax.set_xlabel('Distance between photos (m)')
            ax.set_ylabel('Count')
            ax.legend()
            self._save_plot(fig, 'photo_spacing.png')

        except Exception as e:
            logging.error(f"Error creating coverage plots: {e}")

    def _create_quality_plots(self, metrics: Dict[str, pd.DataFrame]) -> None:
        """Create quality analysis visualizations."""
        try:
            quality_data = metrics.get('quality', pd.DataFrame())
            if quality_data.empty:
                return

            # 1. Quality Metrics Overview
            fig, ax = plt.subplots(figsize=(12, 6))
            metrics_to_plot = ['trajectory_score', 'camera_score', 
                             'coverage_score', 'overall_quality']
            
            for metric in metrics_to_plot:
                if metric in quality_data.columns:
                    ax.scatter(quality_data.index, quality_data[metric], 
                             label=metric, alpha=0.6)
            
            ax.set_title('Quality Metrics by Path')
            ax.set_xlabel('Path ID')
            ax.set_ylabel('Quality Score')
            ax.legend()
            self._save_plot(fig, 'quality_metrics.png')

            # 2. Quality Score Distribution
            fig, ax = plt.subplots()
            sns.histplot(data=quality_data['overall_quality'], bins=20)
            ax.set_title('Overall Quality Distribution')
            ax.set_xlabel('Quality Score')
            ax.set_ylabel('Count')
            self._save_plot(fig, 'quality_distribution.png')

        except Exception as e:
            logging.error(f"Error creating quality plots: {e}")

    def _create_summary_dashboard(self, data: pd.DataFrame, 
                                paths: gpd.GeoDataFrame,
                                metrics: Dict[str, pd.DataFrame]) -> None:
        """Create comprehensive dashboard."""
        try:
            fig = plt.figure(figsize=(20, 15))
            gs = fig.add_gridspec(3, 3)

            # 1. Path Overview (top left)
            ax1 = fig.add_subplot(gs[0, 0])
            paths.plot(ax=ax1, legend=True)
            ax1.set_title('Path Overview')
            ax1.axis('equal')

            # 2. Camera Orientation (top middle)
            ax2 = fig.add_subplot(gs[0, 1])
            ax2.plot(data.index, data['Roll'], label='Roll', alpha=0.6)
            ax2.plot(data.index, data['Pitch'], label='Pitch', alpha=0.6)
            ax2.set_title('Camera Orientation')
            ax2.legend()

            # 3. Elevation Profile (top right)
            ax3 = fig.add_subplot(gs[0, 2])
            ax3.plot(data.index, data['Z'])
            ax3.set_title('Elevation Profile')

            # 4. Speed Profile (middle row)
            ax4 = fig.add_subplot(gs[1, :])
            ax4.plot(paths.index, paths['avg_speed_kmh'], 
                    marker='o', linestyle='-')
            ax4.set_title('Speed Profile')
            ax4.set_xlabel('Path ID')
            ax4.set_ylabel('Speed (km/h)')

            # 5. Quality Overview (bottom row)
            ax5 = fig.add_subplot(gs[2, :])
            quality_data = metrics.get('quality', pd.DataFrame())
            if not quality_data.empty and 'overall_quality' in quality_data.columns:
                ax5.plot(quality_data.index, quality_data['overall_quality'], 
                        marker='o', linestyle='-')
                ax5.set_title('Quality Overview')
                ax5.set_xlabel('Path ID')
                ax5.set_ylabel('Quality Score')

            plt.tight_layout()
            self._save_plot(fig, 'summary_dashboard.png')

        except Exception as e:
            logging.error(f"Error creating summary dashboard: {e}")

    def _save_plot(self, fig: plt.Figure, filename: str) -> None:
        """Save plot with consistent settings."""
        try:
            output_path = os.path.join(self.plots_folder, filename)
            fig.savefig(output_path, dpi=300, bbox_inches='tight')
            plt.close(fig)
        except Exception as e:
            logging.error(f"Error saving plot {filename}: {e}")
            class MMSExport:
    """Export system for MMS data analysis results."""
    
    def __init__(self, output_folder: str):
        self.output_folder = output_folder
        self.reports_folder = os.path.join(output_folder, 'reports')
        self.data_folder = os.path.join(output_folder, 'data')
        
        # Create folders
        for folder in [self.reports_folder, self.data_folder]:
            os.makedirs(folder, exist_ok=True)

    def export_results(self, data: pd.DataFrame, paths: gpd.GeoDataFrame,
                      metrics: Dict[str, pd.DataFrame]) -> None:
        """Export all analysis results."""
        try:
            # Generate reports
            self._generate_text_report(data, paths, metrics)
            self._generate_excel_report(data, paths, metrics)
            self._generate_html_report(data, paths, metrics)
            
            # Export data files
            self._export_data_files(paths)
            
            # Export metadata
            self._export_metadata(data, paths, metrics)
            
        except Exception as e:
            logging.error(f"Error exporting results: {e}")
            raise OutputError(f"Failed to export results: {e}")

    def _generate_text_report(self, data: pd.DataFrame, paths: gpd.GeoDataFrame,
                            metrics: Dict[str, pd.DataFrame]) -> None:
        """Generate detailed text report."""
        try:
            timestamp = datetime.now().strftime('%Y%m%d_%H%M%S')
            report_path = os.path.join(self.reports_folder, 
                                     f'mms_report_{timestamp}.txt')
            
            report_sections = []
            
            # 1. Basic Survey Information
            report_sections.extend([
                "=== MMS Survey Analysis Report ===",
                f"Analysis Date: {datetime.now().strftime('%Y-%m-%d %H:%M:%S')}",
                f"Total Paths: {len(paths)}",
                f"Total Points: {len(data)}",
                f"Total Photos: {data['Image'].nunique()}",
                "\n"
            ])

            # 2. Survey Quality Overview
            quality_metrics = metrics.get('quality', pd.DataFrame())
            if not quality_metrics.empty:
                overall_quality = quality_metrics['overall_quality'].mean()
                report_sections.extend([
                    "=== Survey Quality Overview ===",
                    f"Overall Survey Quality Score: {overall_quality:.1f}/100",
                    f"Quality Category: {self._get_quality_category(overall_quality)}",
                    f"High-Quality Paths: {len(quality_metrics[quality_metrics['overall_quality'] > 85])}",
                    f"Poor-Quality Paths: {len(quality_metrics[quality_metrics['overall_quality'] < 60])}",
                    "\n"
                ])

            # 3. Path Statistics
            report_sections.extend([
                "=== Path Statistics ===",
                f"Total Distance: {paths['length_km'].sum():.1f} km",
                f"Average Path Length: {paths['length_km'].mean():.1f} km",
                f"Average Speed: {paths['avg_speed_kmh'].mean():.1f} km/h",
                "\n"
            ])

            # 4. Camera Stability
            camera_metrics = metrics.get('camera', pd.DataFrame())
            if not camera_metrics.empty:
                report_sections.extend([
                    "=== Camera Stability Analysis ===",
                    f"Average Roll Stability: {camera_metrics['roll_stability'].mean():.3f}",
                    f"Average Pitch Stability: {camera_metrics['pitch_stability'].mean():.3f}",
                    f"Average Heading Stability: {camera_metrics['heading_stability'].mean():.3f}",
                    "\n"
                ])

            # 5. Coverage Analysis
            coverage_metrics = metrics.get('coverage', pd.DataFrame())
            if not coverage_metrics.empty:
                report_sections.extend([
                    "=== Coverage Analysis ===",
                    f"Average Photo Spacing: {coverage_metrics['photo_spacing_mean'].mean():.2f} m",
                    f"Photos per Meter: {coverage_metrics['photos_per_meter'].mean():.2f}",
                    f"Coverage Consistency: {coverage_metrics['spacing_consistency'].mean():.2f}",
                    "\n"
                ])

            # 6. Detailed Path Analysis
            report_sections.extend(["=== Detailed Path Analysis ==="])
            for idx, path in paths.iterrows():
                path_metrics = quality_metrics.loc[idx] if not quality_metrics.empty else None
                
                report_sections.extend([
                    f"\nPath {idx}:",
                    f"  Length: {path['length_km']*1000:.1f} meters",
                    f"  Duration: {path['duration_seconds']:.1f} seconds",
                    f"  Average Speed: {path['avg_speed_kmh']:.1f} km/h",
                    f"  Photos: {path['photos']}",
                    f"  Quality Score: {path_metrics['overall_quality']:.1f}/100" if path_metrics is not None else "",
                    f"  Quality Category: {self._get_quality_category(path_metrics['overall_quality'])}" if path_metrics is not None else ""
                ])

            # Write report
            with open(report_path, 'w', encoding='utf-8') as f:
                f.write('\n'.join(report_sections))

        except Exception as e:
            logging.error(f"Error generating text report: {e}")
            raise

    def _generate_excel_report(self, data: pd.DataFrame, paths: gpd.GeoDataFrame,
                             metrics: Dict[str, pd.DataFrame]) -> None:
        """Generate detailed Excel report."""
        try:
            timestamp = datetime.now().strftime('%Y%m%d_%H%M%S')
            excel_path = os.path.join(self.reports_folder, 
                                    f'mms_analysis_{timestamp}.xlsx')
            
            with pd.ExcelWriter(excel_path, engine='xlsxwriter') as writer:
                # Path Summary
                path_summary = paths.copy()
                path_summary.drop('geometry', axis=1, inplace=True)
                path_summary.to_excel(writer, sheet_name='Path Summary')
                
                # Quality Metrics
                if 'quality' in metrics:
                    metrics['quality'].to_excel(writer, sheet_name='Quality Metrics')
                
                # Camera Stability
                if 'camera' in metrics:
                    metrics['camera'].to_excel(writer, sheet_name='Camera Stability')
                
                # Coverage Analysis
                if 'coverage' in metrics:
                    metrics['coverage'].to_excel(writer, sheet_name='Coverage Analysis')
                
                # Format worksheets
                workbook = writer.book
                for worksheet in writer.sheets.values():
                    worksheet.set_column('A:Z', 12)
                    worksheet.freeze_panes(1, 0)
                
                # Add conditional formatting for quality scores
                if 'Quality Metrics' in writer.sheets:
                    worksheet = writer.sheets['Quality Metrics']
                    quality_format = workbook.add_format({
                        'bg_color': '#c6efce',
                        'font_color': '#006100'
                    })
                    worksheet.conditional_format('E2:E1000', {
                        'type': 'cell',
                        'criteria': '>=',
                        'value': 85,
                        'format': quality_format
                    })

        except Exception as e:
            logging.error(f"Error generating Excel report: {e}")
            raise

    def _generate_html_report(self, data: pd.DataFrame, paths: gpd.GeoDataFrame,
                            metrics: Dict[str, pd.DataFrame]) -> None:
        """Generate interactive HTML report."""
        try:
            timestamp = datetime.now().strftime('%Y%m%d_%H%M%S')
            html_path = os.path.join(self.reports_folder, 
                                   f'mms_report_{timestamp}.html')
            
            # Create HTML template
            html_content = self._get_html_template()
            
            # Add content sections
            html_content = html_content.replace(
                '{{SUMMARY}}', self._generate_html_summary(data, paths, metrics)
            )
            html_content = html_content.replace(
                '{{QUALITY}}', self._generate_html_quality_section(metrics)
            )
            html_content = html_content.replace(
                '{{PATHS}}', self._generate_html_path_details(paths, metrics)
            )
            html_content = html_content.replace(
                '{{CHARTS}}', self._generate_html_charts(paths, metrics)
            )
            
            with open(html_path, 'w', encoding='utf-8') as f:
                f.write(html_content)

        except Exception as e:
            logging.error(f"Error generating HTML report: {e}")
            raise

    def _export_data_files(self, paths: gpd.GeoDataFrame) -> None:
        """Export data files in different formats."""
        try:
            # Base filename
            timestamp = datetime.now().strftime('%Y%m%d_%H%M%S')
            base_name = os.path.join(self.data_folder, f'mms_paths_{timestamp}')
            
            # Export Shapefile
            paths.to_file(f"{base_name}.shp")
            
            # Export KML
            paths.to_file(f"{base_name}.kml", driver='KML')
            
            # Export GeoJSON
            paths.to_file(f"{base_name}.geojson", driver='GeoJSON')

        except Exception as e:
            logging.error(f"Error exporting data files: {e}")
            raise

    def _export_metadata(self, data: pd.DataFrame, paths: gpd.GeoDataFrame,
                        metrics: Dict[str, pd.DataFrame]) -> None:
        """Export analysis metadata."""
        try:
            timestamp = datetime.now().strftime('%Y%m%d_%H%M%S')
            metadata_path = os.path.join(self.reports_folder, 
                                       f'metadata_{timestamp}.json')
            
            metadata = {
                'analysis_date': datetime.now().isoformat(),
                'summary': {
                    'total_paths': len(paths),
                    'total_points': len(data),
                    'total_photos': data['Image'].nunique(),
                    'total_distance_km': paths['length_km'].sum(),
                    'average_speed_kmh': paths['avg_speed_kmh'].mean()
                },
                'quality_metrics': {
                    'average_quality': metrics['quality']['overall_quality'].mean() 
                        if 'quality' in metrics else None,
                    'high_quality_paths': len(metrics['quality'][
                        metrics['quality']['overall_quality'] > 85
                    ]) if 'quality' in metrics else 0
                },
                'path_statistics': paths.describe().to_dict()
            }
            
            with open(metadata_path, 'w', encoding='utf-8') as f:
                json.dump(metadata, f, indent=2)

        except Exception as e:
            logging.error(f"Error exporting metadata: {e}")
            raise

    def _get_quality_category(self, quality_score: float) -> str:
        """Get quality category based on score."""
        if quality_score >= 0.9:
            return "Excellent"
        elif quality_score >= 0.8:
            return "Very Good"
        elif quality_score >= 0.7:
            return "Good"
        elif quality_score >= 0.6:
            return "Acceptable"
        else:
            return "Poor"

    def _get_html_template(self) -> str:
        """Get HTML report template."""
        return """
        <!DOCTYPE html>
        <html lang="en">
        <head>
            <meta charset="UTF-8">
            <meta name="viewport" content="width=device-width, initial-scale=1.0">
            <title>MMS Analysis Report</title>
            <link href="https://cdn.jsdelivr.net/npm/bootstrap@5.1.3/dist/css/bootstrap.min.css" rel="stylesheet">
            <script src="https://cdn.jsdelivr.net/npm/chart.js"></script>
            <style>
                .metric-card {
                    border-radius: 10px;
                    padding: 15px;
                    margin: 10px;
                    box-shadow: 0 4px 6px rgba(0, 0, 0, 0.1);
                }
                .quality-excellent { background-color: #d4edda; }
                .quality-good { background-color: #fff3cd; }
                .quality-poor { background-color: #f8d7da; }
            </style>
        </head>
        <body>
            <div class="container mt-5">
                <h1 class="mb-4">MMS Analysis Report</h1>
                
                {{SUMMARY}}
                {{QUALITY}}
                {{CHARTS}}
                {{PATHS}}
                
            </div>
            <script src="https://cdn.jsdelivr.net/npm/bootstrap@5.1.3/dist/js/bootstrap.bundle.min.js"></script>
        </body>
        </html>
        """
        def main():
    """Main execution function."""
    parser = argparse.ArgumentParser(description='MMS Data Processing and Analysis')
    
    # Required arguments
    parser.add_argument('--input', '-i', required=True,
                      help='Input folder containing MMS data files')
    
    # Optional arguments
    parser.add_argument('--extension', '-e', default='txt', choices=['txt', 'csv'],
                      help='Input file extension (default: txt)')
    parser.add_argument('--output', '-o', help='Output folder path')
    parser.add_argument('--config', '-c', help='Path to config file')
    parser.add_argument('--no-plots', action='store_true',
                      help='Disable plot generation')
    parser.add_argument('--verbose', '-v', action='store_true',
                      help='Enable verbose output')
    
    args = parser.parse_args()

    try:
        # Load configuration
        config = None
        if args.config:
            try:
                with open(args.config, 'r') as f:
                    config = json.load(f)
            except Exception as e:
                print(f"Error loading config file: {e}")
                sys.exit(1)

        # Update config with command line arguments
        if config is None:
            config = {}
        if args.output:
            config['output_folder'] = args.output
        if args.no_plots:
            config['visualize'] = False
        if args.verbose:
            config['logging_level'] = logging.DEBUG

        # Initialize processor
        processor = MMSProcessor(config)
        
        # Register plugins (if needed)
        # Example:
        # processor.register_plugin(QualityCheckPlugin())
        
        # Initialize visualization and export systems
        visualizer = MMSVisualization(processor.config['output_folder'])
        exporter = MMSExport(processor.config['output_folder'])
        
        # Process data
        processor.process(args.input, args.extension)
        
        # Create visualizations
        if processor.config['visualize']:
            visualizer.create_visualizations(
                processor.data,
                processor.paths,
                processor.metrics
            )
        
        # Export results
        exporter.export_results(
            processor.data,
            processor.paths,
            processor.metrics
        )

    except InputError as e:
        print(f"Input Error: {e}")
        sys.exit(1)
    except ProcessingError as e:
        print(f"Processing Error: {e}")
        sys.exit(1)
    except OutputError as e:
        print(f"Output Error: {e}")
        sys.exit(1)
    except Exception as e:
        print(f"Unexpected error: {e}")
        sys.exit(1)

if __name__ == "__main__":
    # Example usage:
    # python mms_processor.py -i /path/to/input/folder
    # python mms_processor.py -i /path/to/input/folder -o /path/to/output -e csv
    # python mms_processor.py -i /path/to/input/folder -c config.json --no-plots
    
    main()