my script: 

#!/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': 3857,  # Web Mercator as default (numeric)
    '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}")

        # Validate CRS
        if 'crs' in config:
            if not isinstance(config['crs'], int):
                raise InputError(f"CRS must be an integer representing the EPSG code, got: {config['crs']}")

    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: int) -> str:
        """
        Parse CRS input to proper EPSG format.
        Accepts only numeric CRS codes and prefixes them with 'EPSG:'.

        Args:
            crs_input (int): Numeric CRS code (e.g., 3857)

        Returns:
            str: CRS code in 'EPSG:XXXX' format
        """
        if isinstance(crs_input, int):
            return f"EPSG:{crs_input}"
        else:
            raise InputError(f"CRS input must be an integer, got: {crs_input}")

    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._parse_crs(self.config['crs'])
                self.config['crs'] = crs
                self.logger.info(f"Using 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.get('visualize', True):
                    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']):
                # Attempt to convert Time as seconds since epoch
                merged_df['Time'] = pd.to_numeric(merged_df['Time'], errors='coerce')
                merged_df['Time'] = pd.to_datetime(
                    merged_df['Time'], unit='s', errors='coerce', origin='unix'
                )
                num_parsed = merged_df['Time'].notna().sum()
                self.logger.info(f"Parsed {num_parsed} Time entries as epoch time")
        except Exception as e:
            self.logger.warning(f"Could not parse Time column as epoch time: {e}")

        # Optional: Drop records with unparsed Time values
        initial_count = len(merged_df)
        merged_df = merged_df.dropna(subset=['Time']).copy()
        dropped_count = initial_count - len(merged_df)
        if dropped_count > 0:
            self.logger.warning(f"Dropped {dropped_count} records due to invalid Time values.")

        # 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.
        """
        self.logger.info("Calculating quality metrics")

        try:
            quality_scoring = MMSQualityScoring()
            quality_metrics = []

            for path_id in self.paths['PathID']:
                path_data = self.data[self.data['PathID'] == path_id]
                quality = quality_scoring.calculate_path_quality(path_data)
                quality_metrics.append({
                    'PathID': path_id,
                    'overall_quality': quality.get('overall_quality', 0.0),
                    'quality_category': quality.get('quality_category', 'Undefined'),
                    'quality_issues': "; ".join(quality.get('quality_issues', []))
                })

            return {'quality': pd.DataFrame(quality_metrics)}

        except Exception as e:
            self.logger.error(f"Error calculating quality metrics: {e}")
            return {'quality': pd.DataFrame()}

    def _generate_reports(self):
        """Placeholder for report generation method."""
        # Implement report generation logic here
        pass

    def _create_visualizations(self):
        """Placeholder for visualization creation method."""
        # Implement visualization creation logic here
        pass


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."""
        if len(path_data) < 2:
            return {'score': 0.0}

        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
            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) if len(
                heading_changes) > 0 else 0.0

            # Combine scores
            trajectory_score = np.mean([
                spacing_score,
                smoothness_score,
                straightness_score
            ])

            return {
                'score': trajectory_score,
                'spacing_score': spacing_score,
                'smoothness_score': smoothness_score,
                'straightness_score': straightness_score
            }

        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."""
        if len(path_data) < 2:
            return {'score': 0.0}

        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
            }

        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."""
        if len(path_data) < 2:
            return {'score': 0.0}

        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
            }

        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_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 using Seaborn
        sns.set_theme(style='darkgrid', palette='deep', font='sans-serif', font_scale=1.2)
        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['PathID']:
                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[
                    quality_metrics['PathID'] == path['PathID']] if not quality_metrics.empty else None

                report_sections.extend([
                    f"\nPath {path['PathID']}:",
                    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'].values[0]:.1f}/100" if path_metrics is not None and not path_metrics.empty else "",
                    f"  Quality Category: {path_metrics['quality_category'].values[0]}" if path_metrics is not None and not path_metrics.empty 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', index=False)

                # Quality Metrics
                if 'quality' in metrics:
                    metrics['quality'].to_excel(writer, sheet_name='Quality Metrics', index=False)

                # Camera Stability
                if 'camera' in metrics:
                    metrics['camera'].to_excel(writer, sheet_name='Camera Stability', index=False)

                # Coverage Analysis
                if 'coverage' in metrics:
                    metrics['coverage'].to_excel(writer, sheet_name='Coverage Analysis', index=False)

                # Format worksheets
                workbook = writer.book
                for sheet_name in writer.sheets:
                    worksheet = writer.sheets[sheet_name]
                    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 _generate_html_summary(self, data: pd.DataFrame, paths: gpd.GeoDataFrame,
                               metrics: Dict[str, pd.DataFrame]) -> str:
        """Generate HTML summary section."""
        try:
            summary = f"""
            <div class="metric-card">
                <h2>Basic Survey Information</h2>
                <p>Total Paths: {len(paths)}</p>
                <p>Total Points: {len(data)}</p>
                <p>Total Photos: {data['Image'].nunique()}</p>
            </div>
            """
            return summary
        except Exception as e:
            logging.error(f"Error generating HTML summary: {e}")
            return ""

    def _generate_html_quality_section(self, metrics: Dict[str, pd.DataFrame]) -> str:
        """Generate HTML quality section."""
        try:
            quality_metrics = metrics.get('quality', pd.DataFrame())
            if quality_metrics.empty:
                return ""

            average_quality = quality_metrics['overall_quality'].mean()
            quality_category = self._get_quality_category(average_quality)
            high_quality = len(quality_metrics[quality_metrics['overall_quality'] > 85])
            poor_quality = len(quality_metrics[quality_metrics['overall_quality'] < 60])

            quality_html = f"""
            <div class="metric-card">
                <h2>Survey Quality Overview</h2>
                <p>Overall Survey Quality Score: {average_quality:.1f}/100</p>
                <p>Quality Category: {quality_category}</p>
                <p>High-Quality Paths: {high_quality}</p>
                <p>Poor-Quality Paths: {poor_quality}</p>
            </div>
            """
            return quality_html
        except Exception as e:
            logging.error(f"Error generating HTML quality section: {e}")
            return ""

    def _generate_html_path_details(self, paths: gpd.GeoDataFrame,
                                    metrics: Dict[str, pd.DataFrame]) -> str:
        """Generate HTML path details section."""
        try:
            quality_metrics = metrics.get('quality', pd.DataFrame())
            path_details = "<div class='metric-card'><h2>Detailed Path Analysis</h2>"
            for idx, path in paths.iterrows():
                path_metric = quality_metrics[quality_metrics['PathID'] == path['PathID']]
                if not path_metric.empty:
                    q_cat = path_metric['quality_category'].values[0]
                    path_details += f"""
                    <h4>Path {path['PathID']}</h4>
                    <p>Length: {path['length_km'] * 1000:.1f} meters</p>
                    <p>Duration: {path['duration_seconds']:.1f} seconds</p>
                    <p>Average Speed: {path['avg_speed_kmh']:.1f} km/h</p>
                    <p>Photos: {path['photos']}</p>
                    <p>Quality Score: {path_metric['overall_quality'].values[0]:.1f}/100</p>
                    <p>Quality Category: {q_cat}</p>
                    """
            path_details += "</div>"
            return path_details
        except Exception as e:
            logging.error(f"Error generating HTML path details: {e}")
            return ""

    def _generate_html_charts(self, paths: gpd.GeoDataFrame,
                              metrics: Dict[str, pd.DataFrame]) -> str:
        """Generate HTML charts section."""
        # Implement chart generation and embed in HTML
        # This is a placeholder implementation
        return ""


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('--crs', '-c', type=int, help='CRS code number (e.g., 3857)')
    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 = {}
        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 args.output:
            config['output_folder'] = args.output
        if args.crs:
            config['crs'] = args.crs
        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.get('visualize', True):
            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()


gives me this error:

PS C:\Users\SPL\Music\mega_3> python .\mega_3.py -i "C:\Users\SPL\Pictures" -o "C:\Users\SPL\Pictures\test_mega3" -e txt -c 3857
Traceback (most recent call last):
  File "C:\Users\SPL\Music\mega_3\mega_3.py", line 1767, in <module>
    main()
    ~~~~^^
  File "C:\Users\SPL\Music\mega_3\mega_3.py", line 1689, in main
    parser.add_argument('--crs', '-c', type=int, help='CRS code number (e.g., 3857)')
    ~~~~~~~~~~~~~~~~~~~^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
  File "C:\Users\SPL\scoop\apps\python\current\Lib\argparse.py", line 1477, in add_argument
    return self._add_action(action)
           ~~~~~~~~~~~~~~~~^^^^^^^^
  File "C:\Users\SPL\scoop\apps\python\current\Lib\argparse.py", line 1861, in _add_action
    self._optionals._add_action(action)
    ~~~~~~~~~~~~~~~~~~~~~~~~~~~^^^^^^^^
  File "C:\Users\SPL\scoop\apps\python\current\Lib\argparse.py", line 1681, in _add_action
    action = super(_ArgumentGroup, self)._add_action(action)
  File "C:\Users\SPL\scoop\apps\python\current\Lib\argparse.py", line 1491, in _add_action
    self._check_conflict(action)
    ~~~~~~~~~~~~~~~~~~~~^^^^^^^^
  File "C:\Users\SPL\scoop\apps\python\current\Lib\argparse.py", line 1630, in _check_conflict
    conflict_handler(action, confl_optionals)
    ~~~~~~~~~~~~~~~~^^^^^^^^^^^^^^^^^^^^^^^^^
  File "C:\Users\SPL\scoop\apps\python\current\Lib\argparse.py", line 1639, in _handle_conflict_error
    raise ArgumentError(action, message % conflict_string)
argparse.ArgumentError: argument --crs/-c: conflicting option string: -c