Перейти к содержанию

Расширение функциональности CulicidaeLab Mobile

Обзор

Мобильное приложение CulicidaeLab разработано с учетом расширяемости, предоставляя множественные пути для исследователей, разработчиков и учреждений для настройки и расширения его функциональности. Это руководство охватывает разработку плагинов, интеграцию пользовательских моделей, расширения конвейера данных и подходы к настройке пользовательского интерфейса.

Архитектура для расширений

Точки расширения

Приложение предоставляет несколько четко определенных точек расширения:

graph TB
    subgraph "Основное приложение"
        UI[Слой пользовательского интерфейса]
        VM[Слой ViewModel]
        SVC[Слой сервисов]
        REPO[Слой репозиториев]
        MODEL[Слой моделей]
    end

    subgraph "Точки расширения"
        UI_EXT[Расширения UI]
        PLUGIN[Система плагинов]
        MODEL_EXT[Пользовательские модели]
        DATA_EXT[Расширения данных]
        API_EXT[Расширения API]
    end

    subgraph "Внешние системы"
        EXT_API[Внешние API]
        EXT_DB[Внешние базы данных]
        EXT_ML[Внешние МО модели]
        EXT_SVC[Внешние сервисы]
    end

    UI --> UI_EXT
    VM --> PLUGIN
    SVC --> MODEL_EXT
    REPO --> DATA_EXT
    MODEL --> API_EXT

    UI_EXT --> EXT_SVC
    PLUGIN --> EXT_API
    MODEL_EXT --> EXT_ML
    DATA_EXT --> EXT_DB
    API_EXT --> EXT_API

Архитектура плагинов

Основная система плагинов предоставляет стандартизированный способ расширения функциональности:

// Основной интерфейс плагина
abstract class CulicidaeLabPlugin {
  /// Уникальный идентификатор плагина
  String get pluginId;

  /// Человекочитаемое название плагина
  String get name;

  /// Версия плагина
  String get version;

  /// Список функций, которые предоставляет этот плагин
  List<String> get supportedFeatures;

  /// Зависимости плагина
  List<String> get dependencies;

  /// Инициализация плагина
  Future<void> initialize(PluginContext context);

  /// Обработка данных через плагин
  Future<PluginResult> processData(PluginInput input);

  /// Получение UI конфигурации плагина
  Widget? getConfigurationWidget();

  /// Очистка ресурсов плагина
  Future<void> dispose();

  /// Метаданные плагина
  Map<String, dynamic> get metadata;
}

// Контекст плагина предоставляет доступ к основным сервисам
class PluginContext {
  final DatabaseService databaseService;
  final ClassificationService classificationService;
  final UserService userService;
  final Map<String, dynamic> appConfiguration;
  final PluginLogger logger;

  PluginContext({
    required this.databaseService,
    required this.classificationService,
    required this.userService,
    required this.appConfiguration,
    required this.logger,
  });
}

// Стандартизированный ввод/вывод плагина
class PluginInput {
  final String operation;
  final Map<String, dynamic> data;
  final Map<String, dynamic> metadata;

  PluginInput({
    required this.operation,
    required this.data,
    this.metadata = const {},
  });
}

class PluginResult {
  final bool success;
  final Map<String, dynamic> data;
  final String? errorMessage;
  final Map<String, dynamic> metadata;

  PluginResult({
    required this.success,
    this.data = const {},
    this.errorMessage,
    this.metadata = const {},
  });

  factory PluginResult.success(Map<String, dynamic> data) {
    return PluginResult(success: true, data: data);
  }

  factory PluginResult.error(String message) {
    return PluginResult(success: false, errorMessage: message);
  }
}

Пользовательские модели классификации

Интерфейс интеграции моделей

// Интерфейс для пользовательских моделей классификации
abstract class CustomClassificationModel {
  /// Идентификатор модели
  String get modelId;

  /// Версия модели
  String get version;

  /// Список поддерживаемых видов
  List<String> get supportedSpecies;

  /// Метаданные модели
  ModelMetadata get metadata;

  /// Загрузка модели
  Future<void> loadModel();

  /// Классификация изображения
  Future<ClassificationResult> classify(File imageFile);

  /// Пакетная классификация
  Future<List<ClassificationResult>> classifyBatch(List<File> imageFiles);

  /// Получение метрик производительности модели
  Future<ModelPerformanceMetrics> getPerformanceMetrics();

  /// Выгрузка модели
  Future<void> unloadModel();

  /// Проверка загружена ли модель
  bool get isLoaded;
}

class ModelMetadata {
  final String name;
  final String description;
  final String author;
  final DateTime createdAt;
  final String license;
  final List<String> trainingDatasets;
  final Map<String, dynamic> hyperparameters;
  final ModelPerformanceMetrics performanceMetrics;

  ModelMetadata({
    required this.name,
    required this.description,
    required this.author,
    required this.createdAt,
    required this.license,
    required this.trainingDatasets,
    required this.hyperparameters,
    required this.performanceMetrics,
  });
}

class ModelPerformanceMetrics {
  final double accuracy;
  final double precision;
  final double recall;
  final double f1Score;
  final Map<String, double> perClassAccuracy;
  final int totalParameters;
  final double modelSizeMB;
  final double averageInferenceTimeMs;

  ModelPerformanceMetrics({
    required this.accuracy,
    required this.precision,
    required this.recall,
    required this.f1Score,
    required this.perClassAccuracy,
    required this.totalParameters,
    required this.modelSizeMB,
    required this.averageInferenceTimeMs,
  });
}
```#
## Пример модели TensorFlow Lite

```dart
class CustomTensorFlowLiteModel implements CustomClassificationModel {
  late Interpreter _interpreter;
  late List<String> _labels;
  bool _isLoaded = false;

  @override
  String get modelId => 'custom_tflite_mosquito_v2';

  @override
  String get version => '2.1.0';

  @override
  List<String> get supportedSpecies => _labels;

  @override
  ModelMetadata get metadata => ModelMetadata(
    name: 'Пользовательский классификатор комаров TensorFlow Lite',
    description: 'Улучшенная модель классификации видов комаров с повышенной точностью',
    author: 'Исследовательская команда XYZ',
    createdAt: DateTime(2024, 1, 15),
    license: 'Apache 2.0',
    trainingDatasets: ['mosquito_dataset_46_3139', 'custom_field_data_2024'],
    hyperparameters: {
      'learning_rate': 0.001,
      'batch_size': 32,
      'epochs': 100,
      'optimizer': 'Adam',
      'input_size': [224, 224, 3],
    },
    performanceMetrics: ModelPerformanceMetrics(
      accuracy: 0.94,
      precision: 0.93,
      recall: 0.92,
      f1Score: 0.925,
      perClassAccuracy: {
        'Aedes aegypti': 0.96,
        'Aedes albopictus': 0.94,
        'Culex pipiens': 0.91,
        // ... другие виды
      },
      totalParameters: 2500000,
      modelSizeMB: 9.8,
      averageInferenceTimeMs: 180.0,
    ),
  );

  @override
  bool get isLoaded => _isLoaded;

  @override
  Future<void> loadModel() async {
    try {
      // Загрузка модели TensorFlow Lite
      final modelFile = await _loadModelAsset('assets/models/custom_model.tflite');
      _interpreter = Interpreter.fromFile(modelFile);

      // Загрузка меток
      final labelsString = await rootBundle.loadString('assets/models/custom_labels.txt');
      _labels = labelsString.split('\n').where((label) => label.isNotEmpty).toList();

      _isLoaded = true;
      print('Пользовательская модель TensorFlow Lite успешно загружена');
    } catch (e) {
      throw ModelLoadException('Не удалось загрузить пользовательскую модель TensorFlow Lite: $e');
    }
  }

  @override
  Future<ClassificationResult> classify(File imageFile) async {
    if (!_isLoaded) {
      throw ModelNotLoadedException('Модель должна быть загружена перед классификацией');
    }

    final stopwatch = Stopwatch()..start();

    try {
      // Предобработка изображения
      final input = await _preprocessImage(imageFile);

      // Выполнение вывода
      final output = List.filled(_labels.length, 0.0).reshape([1, _labels.length]);
      _interpreter.run(input, output);

      // Постобработка результатов
      final probabilities = output[0] as List<double>;
      final maxIndex = probabilities.indexOf(probabilities.reduce(math.max));
      final confidence = probabilities[maxIndex];
      final speciesName = _labels[maxIndex];

      stopwatch.stop();

      // Создание объекта вида
      final species = await _createSpeciesFromName(speciesName);
      final relatedDiseases = await _getRelatedDiseases(species);

      return ClassificationResult(
        species: species,
        confidence: confidence,
        inferenceTime: stopwatch.elapsedMilliseconds,
        relatedDiseases: relatedDiseases,
        imageFile: imageFile,
      );
    } catch (e) {
      throw ClassificationException('Классификация не удалась: $e');
    }
  }

  @override
  Future<List<ClassificationResult>> classifyBatch(List<File> imageFiles) async {
    final results = <ClassificationResult>[];

    for (final imageFile in imageFiles) {
      try {
        final result = await classify(imageFile);
        results.add(result);
      } catch (e) {
        print('Не удалось классифицировать ${imageFile.path}: $e');
        // Продолжить с другими изображениями
      }
    }

    return results;
  }

  Future<List<List<List<List<double>>>>> _preprocessImage(File imageFile) async {
    // Загрузка и декодирование изображения
    final bytes = await imageFile.readAsBytes();
    final image = img.decodeImage(bytes);

    if (image == null) {
      throw ImageProcessingException('Не удалось декодировать изображение');
    }

    // Изменение размера до размера входа модели
    final resized = img.copyResize(image, width: 224, height: 224);

    // Преобразование в нормализованный массив float
    final input = List.generate(
      1,
      (batch) => List.generate(
        224,
        (y) => List.generate(
          224,
          (x) => List.generate(3, (c) {
            final pixel = resized.getPixel(x, y);
            switch (c) {
              case 0: return (pixel.r / 255.0 - 0.485) / 0.229; // Красный канал
              case 1: return (pixel.g / 255.0 - 0.456) / 0.224; // Зеленый канал
              case 2: return (pixel.b / 255.0 - 0.406) / 0.225; // Синий канал
              default: return 0.0;
            }
          }),
        ),
      ),
    );

    return input;
  }

  @override
  Future<ModelPerformanceMetrics> getPerformanceMetrics() async {
    return metadata.performanceMetrics;
  }

  @override
  Future<void> unloadModel() async {
    if (_isLoaded) {
      _interpreter.close();
      _isLoaded = false;
      print('Пользовательская модель TensorFlow Lite выгружена');
    }
  }

  Future<File> _loadModelAsset(String assetPath) async {
    final byteData = await rootBundle.load(assetPath);
    final tempDir = await getTemporaryDirectory();
    final file = File('${tempDir.path}/custom_model.tflite');
    await file.writeAsBytes(byteData.buffer.asUint8List());
    return file;
  }
}

// Пользовательские исключения
class ModelLoadException implements Exception {
  final String message;
  ModelLoadException(this.message);
  @override
  String toString() => 'ModelLoadException: $message';
}

class ModelNotLoadedException implements Exception {
  final String message;
  ModelNotLoadedException(this.message);
  @override
  String toString() => 'ModelNotLoadedException: $message';
}

class ClassificationException implements Exception {
  final String message;
  ClassificationException(this.message);
  @override
  String toString() => 'ClassificationException: $message';
}

class ImageProcessingException implements Exception {
  final String message;
  ImageProcessingException(this.message);
  @override
  String toString() => 'ImageProcessingException: $message';
}

Интеграция модели ONNX

class ONNXClassificationModel implements CustomClassificationModel {
  late OnnxDart _session;
  late List<String> _labels;
  bool _isLoaded = false;

  @override
  String get modelId => 'onnx_mosquito_classifier_v1';

  @override
  String get version => '1.0.0';

  @override
  List<String> get supportedSpecies => _labels;

  @override
  bool get isLoaded => _isLoaded;

  @override
  Future<void> loadModel() async {
    try {
      // Загрузка модели ONNX
      final modelBytes = await rootBundle.load('assets/models/mosquito_classifier.onnx');
      _session = OnnxDart.createSession(modelBytes.buffer.asUint8List());

      // Загрузка меток
      final labelsString = await rootBundle.loadString('assets/models/onnx_labels.txt');
      _labels = labelsString.split('\n').where((label) => label.isNotEmpty).toList();

      _isLoaded = true;
      print('Модель ONNX успешно загружена');
    } catch (e) {
      throw ModelLoadException('Не удалось загрузить модель ONNX: $e');
    }
  }

  @override
  Future<ClassificationResult> classify(File imageFile) async {
    if (!_isLoaded) {
      throw ModelNotLoadedException('Модель ONNX должна быть загружена перед классификацией');
    }

    final stopwatch = Stopwatch()..start();

    try {
      // Предобработка изображения для ONNX
      final inputTensor = await _preprocessImageForONNX(imageFile);

      // Выполнение вывода ONNX
      final outputs = _session.run({'input': inputTensor});
      final probabilities = outputs['output'] as List<double>;

      // Поиск лучшего предсказания
      final maxIndex = probabilities.indexOf(probabilities.reduce(math.max));
      final confidence = probabilities[maxIndex];
      final speciesName = _labels[maxIndex];

      stopwatch.stop();

      // Создание результата
      final species = await _createSpeciesFromName(speciesName);
      final relatedDiseases = await _getRelatedDiseases(species);

      return ClassificationResult(
        species: species,
        confidence: confidence,
        inferenceTime: stopwatch.elapsedMilliseconds,
        relatedDiseases: relatedDiseases,
        imageFile: imageFile,
      );
    } catch (e) {
      throw ClassificationException('Классификация ONNX не удалась: $e');
    }
  }

  Future<List<List<List<double>>>> _preprocessImageForONNX(File imageFile) async {
    // Предобработка специфичная для ONNX
    final bytes = await imageFile.readAsBytes();
    final image = img.decodeImage(bytes);

    if (image == null) {
      throw ImageProcessingException('Не удалось декодировать изображение для ONNX');
    }

    final resized = img.copyResize(image, width: 224, height: 224);

    // ONNX ожидает формат CHW (Channel, Height, Width)
    final input = List.generate(3, (c) =>
      List.generate(224, (h) =>
        List.generate(224, (w) {
          final pixel = resized.getPixel(w, h);
          switch (c) {
            case 0: return (pixel.r / 255.0 - 0.485) / 0.229;
            case 1: return (pixel.g / 255.0 - 0.456) / 0.224;
            case 2: return (pixel.b / 255.0 - 0.406) / 0.225;
            default: return 0.0;
          }
        })
      )
    );

    return input;
  }

  @override
  Future<void> unloadModel() async {
    if (_isLoaded) {
      _session.release();
      _isLoaded = false;
      print('Модель ONNX выгружена');
    }
  }
}

Расширения конвейера данных

Пользовательские процессоры данных

// Интерфейс для плагинов обработки данных
abstract class DataProcessor {
  String get processorId;
  String get name;
  List<String> get supportedDataTypes;

  Future<ProcessingResult> processData(ProcessingInput input);
  Future<void> initialize();
  Future<void> dispose();
}

// Пример процессора экологических данных
class EnvironmentalDataProcessor implements DataProcessor {
  late WeatherAPI _weatherAPI;
  late SoilAPI _soilAPI;

  @override
  String get processorId => 'environmental_data_processor';

  @override
  String get name => 'Процессор экологических данных';

  @override
  List<String> get supportedDataTypes => [
    'weather_data',
    'soil_data',
    'vegetation_data',
    'water_quality_data'
  ];

  @override
  Future<void> initialize() async {
    _weatherAPI = WeatherAPI(apiKey: await _getWeatherAPIKey());
    _soilAPI = SoilAPI(apiKey: await _getSoilAPIKey());
  }

  @override
  Future<ProcessingResult> processData(ProcessingInput input) async {
    final location = Location.fromJson(input.data['location']);
    final timestamp = DateTime.parse(input.data['timestamp']);

    try {
      final results = <String, dynamic>{};

      // Сбор данных о погоде
      if (input.requestedTypes.contains('weather_data')) {
        results['weather_data'] = await _collectWeatherData(location, timestamp);
      }

      // Сбор данных о почве
      if (input.requestedTypes.contains('soil_data')) {
        results['soil_data'] = await _collectSoilData(location);
      }

      // Сбор данных о растительности
      if (input.requestedTypes.contains('vegetation_data')) {
        results['vegetation_data'] = await _collectVegetationData(location);
      }

      return ProcessingResult.success(results);
    } catch (e) {
      return ProcessingResult.error('Сбор экологических данных не удался: $e');
    }
  }

  Future<Map<String, dynamic>> _collectWeatherData(Location location, DateTime timestamp) async {
    final weatherData = await _weatherAPI.getWeatherData(
      latitude: location.lat,
      longitude: location.lng,
      timestamp: timestamp,
    );

    return {
      'temperature_celsius': weatherData.temperature,
      'humidity_percent': weatherData.humidity,
      'pressure_hpa': weatherData.pressure,
      'wind_speed_ms': weatherData.windSpeed,
      'wind_direction_degrees': weatherData.windDirection,
      'precipitation_mm': weatherData.precipitation,
      'cloud_cover_percent': weatherData.cloudCover,
      'uv_index': weatherData.uvIndex,
      'visibility_km': weatherData.visibility,
    };
  }

  Future<Map<String, dynamic>> _collectSoilData(Location location) async {
    final soilData = await _soilAPI.getSoilData(
      latitude: location.lat,
      longitude: location.lng,
    );

    return {
      'soil_type': soilData.type,
      'ph_level': soilData.phLevel,
      'organic_matter_percent': soilData.organicMatter,
      'nitrogen_ppm': soilData.nitrogen,
      'phosphorus_ppm': soilData.phosphorus,
      'potassium_ppm': soilData.potassium,
      'moisture_percent': soilData.moisture,
      'temperature_celsius': soilData.temperature,
    };
  }

  Future<Map<String, dynamic>> _collectVegetationData(Location location) async {
    // Использование спутниковых снимков или локальных баз данных для данных о растительности
    return {
      'vegetation_type': 'mixed_forest',
      'canopy_cover_percent': 75.0,
      'dominant_species': ['Дуб', 'Сосна', 'Береза'],
      'ndvi_index': 0.65,
      'leaf_area_index': 4.2,
    };
  }

  @override
  Future<void> dispose() async {
    await _weatherAPI.dispose();
    await _soilAPI.dispose();
  }
}

class ProcessingInput {
  final Map<String, dynamic> data;
  final List<String> requestedTypes;
  final Map<String, dynamic> options;

  ProcessingInput({
    required this.data,
    required this.requestedTypes,
    this.options = const {},
  });
}

class ProcessingResult {
  final bool success;
  final Map<String, dynamic> data;
  final String? errorMessage;
  final Map<String, dynamic> metadata;

  ProcessingResult({
    required this.success,
    this.data = const {},
    this.errorMessage,
    this.metadata = const {},
  });

  factory ProcessingResult.success(Map<String, dynamic> data) {
    return ProcessingResult(success: true, data: data);
  }

  factory ProcessingResult.error(String message) {
    return ProcessingResult(success: false, errorMessage: message);
  }
}
```##
# Пользовательские форматы экспорта

```dart
// Интерфейс для пользовательских форматов экспорта
abstract class DataExporter {
  String get exporterId;
  String get name;
  String get fileExtension;
  List<String> get supportedDataTypes;

  Future<ExportResult> exportData(ExportRequest request);
  Future<bool> validateData(List<dynamic> data);
}

// Экспортер для исследований
class ResearchDataExporter implements DataExporter {
  @override
  String get exporterId => 'research_data_exporter';

  @override
  String get name => 'Экспортер исследовательских данных';

  @override
  String get fileExtension => '.rdf'; // Формат исследовательских данных

  @override
  List<String> get supportedDataTypes => [
    'observations',
    'classifications',
    'environmental_data',
    'species_data'
  ];

  @override
  Future<ExportResult> exportData(ExportRequest request) async {
    try {
      final exportData = <String, dynamic>{};

      // Добавление метаданных
      exportData['metadata'] = {
        'export_timestamp': DateTime.now().toIso8601String(),
        'exporter_version': '1.0.0',
        'data_format_version': '2.1',
        'total_records': request.data.length,
        'export_parameters': request.parameters,
      };

      // Обработка различных типов данных
      if (request.dataType == 'observations') {
        exportData['observations'] = await _exportObservations(request.data as List<Observation>);
      } else if (request.dataType == 'classifications') {
        exportData['classifications'] = await _exportClassifications(request.data as List<ClassificationResult>);
      }

      // Добавление анализа при запросе
      if (request.parameters['include_analysis'] == true) {
        exportData['analysis'] = await _generateAnalysis(request.data);
      }

      // Преобразование в указанный формат
      final content = await _formatData(exportData, request.format);

      return ExportResult.success(content);
    } catch (e) {
      return ExportResult.error('Экспорт не удался: $e');
    }
  }

  Future<List<Map<String, dynamic>>> _exportObservations(List<Observation> observations) async {
    return observations.map((obs) => {
      'observation_id': obs.id,
      'species': {
        'scientific_name': obs.speciesScientificName,
        'taxonomic_classification': await _getTaxonomicClassification(obs.speciesScientificName),
      },
      'spatial': {
        'latitude': obs.location.lat,
        'longitude': obs.location.lng,
        'coordinate_system': 'WGS84',
        'accuracy_meters': obs.locationAccuracyM,
        'elevation_meters': null, // Может быть добавлено при наличии
      },
      'temporal': {
        'observed_at': obs.observedAt.toIso8601String(),
        'day_of_year': obs.observedAt.dayOfYear,
        'season': _calculateSeason(obs.observedAt),
        'time_of_day': _calculateTimeOfDay(obs.observedAt),
      },
      'identification': {
        'confidence': obs.confidence,
        'model_id': obs.modelId,
        'identification_method': obs.dataSource,
        'expert_verified': false, // Может быть расширено
      },
      'context': {
        'observer_notes': obs.notes,
        'specimen_count': obs.count,
        'additional_metadata': obs.metadata,
      },
    }).toList();
  }

  Future<String> _formatData(Map<String, dynamic> data, String format) async {
    switch (format.toLowerCase()) {
      case 'json':
        return jsonEncode(data);
      case 'yaml':
        return _convertToYAML(data);
      case 'xml':
        return _convertToXML(data);
      case 'csv':
        return _convertToCSV(data);
      default:
        throw ArgumentError('Неподдерживаемый формат: $format');
    }
  }

  @override
  Future<bool> validateData(List<dynamic> data) async {
    // Реализация логики валидации данных
    for (final item in data) {
      if (item is Observation) {
        if (!_isValidObservation(item)) return false;
      } else if (item is ClassificationResult) {
        if (!_isValidClassificationResult(item)) return false;
      }
    }
    return true;
  }

  bool _isValidObservation(Observation obs) {
    return obs.id.isNotEmpty &&
           obs.speciesScientificName.isNotEmpty &&
           obs.location.lat >= -90 && obs.location.lat <= 90 &&
           obs.location.lng >= -180 && obs.location.lng <= 180;
  }
}

class ExportRequest {
  final String dataType;
  final List<dynamic> data;
  final String format;
  final Map<String, dynamic> parameters;

  ExportRequest({
    required this.dataType,
    required this.data,
    required this.format,
    this.parameters = const {},
  });
}

class ExportResult {
  final bool success;
  final String? content;
  final String? errorMessage;
  final Map<String, dynamic> metadata;

  ExportResult({
    required this.success,
    this.content,
    this.errorMessage,
    this.metadata = const {},
  });

  factory ExportResult.success(String content) {
    return ExportResult(success: true, content: content);
  }

  factory ExportResult.error(String message) {
    return ExportResult(success: false, errorMessage: message);
  }
}

Расширения пользовательского интерфейса

Пользовательские экраны и виджеты

// Интерфейс для расширений UI
abstract class UIExtension {
  String get extensionId;
  String get name;
  List<String> get supportedScreens;

  Widget buildExtensionWidget(BuildContext context, Map<String, dynamic> parameters);
  List<NavigationItem> getNavigationItems();
  List<ActionItem> getActionItems(String screenId);
}

// Расширение UI исследовательских инструментов
class ResearchToolsExtension implements UIExtension {
  @override
  String get extensionId => 'research_tools_extension';

  @override
  String get name => 'Исследовательские инструменты';

  @override
  List<String> get supportedScreens => [
    'data_analysis',
    'field_session',
    'batch_processing',
    'export_manager'
  ];

  @override
  Widget buildExtensionWidget(BuildContext context, Map<String, dynamic> parameters) {
    final screenId = parameters['screen_id'] as String;

    switch (screenId) {
      case 'data_analysis':
        return DataAnalysisScreen();
      case 'field_session':
        return FieldSessionScreen();
      case 'batch_processing':
        return BatchProcessingScreen();
      case 'export_manager':
        return ExportManagerScreen();
      default:
        return Container(
          child: Text('Неизвестный экран: $screenId'),
        );
    }
  }

  @override
  List<NavigationItem> getNavigationItems() {
    return [
      NavigationItem(
        id: 'research_tools',
        title: 'Исследовательские инструменты',
        icon: Icons.science,
        route: '/research-tools',
      ),
      NavigationItem(
        id: 'data_analysis',
        title: 'Анализ данных',
        icon: Icons.analytics,
        route: '/research-tools/analysis',
      ),
    ];
  }

  @override
  List<ActionItem> getActionItems(String screenId) {
    switch (screenId) {
      case 'classification_screen':
        return [
          ActionItem(
            id: 'start_field_session',
            title: 'Начать полевую сессию',
            icon: Icons.play_arrow,
            onTap: (context) => _startFieldSession(context),
          ),
        ];
      case 'observation_details_screen':
        return [
          ActionItem(
            id: 'add_environmental_data',
            title: 'Добавить экологические данные',
            icon: Icons.eco,
            onTap: (context) => _addEnvironmentalData(context),
          ),
        ];
      default:
        return [];
    }
  }

  void _startFieldSession(BuildContext context) {
    Navigator.push(
      context,
      MaterialPageRoute(builder: (context) => FieldSessionScreen()),
    );
  }

  void _addEnvironmentalData(BuildContext context) {
    showDialog(
      context: context,
      builder: (context) => EnvironmentalDataDialog(),
    );
  }
}

class NavigationItem {
  final String id;
  final String title;
  final IconData icon;
  final String route;

  NavigationItem({
    required this.id,
    required this.title,
    required this.icon,
    required this.route,
  });
}

class ActionItem {
  final String id;
  final String title;
  final IconData icon;
  final Function(BuildContext) onTap;

  ActionItem({
    required this.id,
    required this.title,
    required this.icon,
    required this.onTap,
  });
}

// Пример пользовательского исследовательского экрана
class DataAnalysisScreen extends StatefulWidget {
  @override
  _DataAnalysisScreenState createState() => _DataAnalysisScreenState();
}

class _DataAnalysisScreenState extends State<DataAnalysisScreen> {
  List<Observation> _observations = [];
  Map<String, dynamic> _analysisResults = {};
  bool _isAnalyzing = false;

  @override
  void initState() {
    super.initState();
    _loadObservations();
  }

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: Text('Анализ данных'),
        actions: [
          IconButton(
            icon: Icon(Icons.refresh),
            onPressed: _refreshAnalysis,
          ),
          IconButton(
            icon: Icon(Icons.export_notes),
            onPressed: _exportResults,
          ),
        ],
      ),
      body: Column(
        children: [
          _buildAnalysisControls(),
          Expanded(
            child: _isAnalyzing
                ? Center(child: CircularProgressIndicator())
                : _buildAnalysisResults(),
          ),
        ],
      ),
    );
  }

  Widget _buildAnalysisControls() {
    return Card(
      margin: EdgeInsets.all(16),
      child: Padding(
        padding: EdgeInsets.all(16),
        child: Column(
          crossAxisAlignment: CrossAxisAlignment.start,
          children: [
            Text(
              'Параметры анализа',
              style: Theme.of(context).textTheme.headlineSmall,
            ),
            SizedBox(height: 16),
            Row(
              children: [
                Expanded(
                  child: ElevatedButton(
                    onPressed: _performSpeciesAnalysis,
                    child: Text('Анализ видов'),
                  ),
                ),
                SizedBox(width: 8),
                Expanded(
                  child: ElevatedButton(
                    onPressed: _performTemporalAnalysis,
                    child: Text('Временной анализ'),
                  ),
                ),
              ],
            ),
            SizedBox(height: 8),
            Row(
              children: [
                Expanded(
                  child: ElevatedButton(
                    onPressed: _performSpatialAnalysis,
                    child: Text('Пространственный анализ'),
                  ),
                ),
                SizedBox(width: 8),
                Expanded(
                  child: ElevatedButton(
                    onPressed: _performQualityAnalysis,
                    child: Text('Анализ качества'),
                  ),
                ),
              ],
            ),
          ],
        ),
      ),
    );
  }

  Widget _buildAnalysisResults() {
    if (_analysisResults.isEmpty) {
      return Center(
        child: Text(
          'Выберите тип анализа для начала',
          style: Theme.of(context).textTheme.bodyLarge,
        ),
      );
    }

    return ListView(
      padding: EdgeInsets.all(16),
      children: _analysisResults.entries.map((entry) {
        return Card(
          child: ListTile(
            title: Text(entry.key),
            subtitle: Text(entry.value.toString()),
          ),
        );
      }).toList(),
    );
  }

  Future<void> _loadObservations() async {
    // Загрузка наблюдений из базы данных
    final repository = locator<ObservationRepository>();
    final observations = await repository.getAllObservations();
    setState(() {
      _observations = observations;
    });
  }

  Future<void> _performSpeciesAnalysis() async {
    setState(() {
      _isAnalyzing = true;
    });

    try {
      final analyzer = SpeciesAnalyzer();
      final results = await analyzer.analyzeSpeciesDistribution(_observations);

      setState(() {
        _analysisResults = results;
        _isAnalyzing = false;
      });
    } catch (e) {
      setState(() {
        _isAnalyzing = false;
      });
      _showErrorDialog('Ошибка анализа видов: $e');
    }
  }

  Future<void> _performTemporalAnalysis() async {
    setState(() {
      _isAnalyzing = true;
    });

    try {
      final analyzer = TemporalAnalyzer();
      final results = await analyzer.analyzeTemporalPatterns(_observations);

      setState(() {
        _analysisResults = results;
        _isAnalyzing = false;
      });
    } catch (e) {
      setState(() {
        _isAnalyzing = false;
      });
      _showErrorDialog('Ошибка временного анализа: $e');
    }
  }

  Future<void> _performSpatialAnalysis() async {
    setState(() {
      _isAnalyzing = true;
    });

    try {
      final analyzer = SpatialAnalyzer();
      final results = await analyzer.analyzeSpatialPatterns(_observations);

      setState(() {
        _analysisResults = results;
        _isAnalyzing = false;
      });
    } catch (e) {
      setState(() {
        _isAnalyzing = false;
      });
      _showErrorDialog('Ошибка пространственного анализа: $e');
    }
  }

  Future<void> _performQualityAnalysis() async {
    setState(() {
      _isAnalyzing = true;
    });

    try {
      final analyzer = QualityAnalyzer();
      final results = await analyzer.analyzeDataQuality(_observations);

      setState(() {
        _analysisResults = results;
        _isAnalyzing = false;
      });
    } catch (e) {
      setState(() {
        _isAnalyzing = false;
      });
      _showErrorDialog('Ошибка анализа качества: $e');
    }
  }

  void _refreshAnalysis() {
    _loadObservations();
    setState(() {
      _analysisResults.clear();
    });
  }

  void _exportResults() {
    // Реализация экспорта результатов
    final exporter = ResearchDataExporter();
    // ... логика экспорта
  }

  void _showErrorDialog(String message) {
    showDialog(
      context: context,
      builder: (context) => AlertDialog(
        title: Text('Ошибка'),
        content: Text(message),
        actions: [
          TextButton(
            onPressed: () => Navigator.of(context).pop(),
            child: Text('OK'),
          ),
        ],
      ),
    );
  }
}

Заключение

Система расширений CulicidaeLab предоставляет мощные возможности для настройки и расширения функциональности мобильного приложения. Следуя архитектурным паттернам и интерфейсам, описанным в этом руководстве, разработчики могут:

  • Создавать пользовательские модели классификации для специализированных случаев использования
  • Разрабатывать плагины обработки данных для интеграции с внешними системами
  • Реализовывать пользовательские форматы экспорта для исследовательских нужд
  • Расширять пользовательский интерфейс с дополнительными экранами и функциями

Эта расширяемая архитектура обеспечивает, что приложение CulicidaeLab может адаптироваться к разнообразным исследовательским потребностям и развиваться вместе с научным сообществом.