Files
impact/lib/features/analysis/analysis_provider.dart

439 lines
12 KiB
Dart

/// Gestionnaire d'état pour l'analyse des cibles (ChangeNotifier).
///
/// Gère le workflow complet d'analyse : chargement d'image, détection de cible,
/// gestion des impacts (manuels et automatiques), calcul des scores,
/// analyse de groupement et sauvegarde des sessions.
library;
import 'dart:io';
import 'dart:ui' as ui;
import 'package:flutter/foundation.dart';
import 'package:uuid/uuid.dart';
import '../../data/models/session.dart';
import '../../data/models/shot.dart';
import '../../data/models/target_type.dart';
import '../../data/repositories/session_repository.dart';
import '../../services/target_detection_service.dart';
import '../../services/score_calculator_service.dart';
import '../../services/grouping_analyzer_service.dart';
enum AnalysisState { initial, loading, success, error }
class AnalysisProvider extends ChangeNotifier {
final TargetDetectionService _detectionService;
final ScoreCalculatorService _scoreCalculatorService;
final GroupingAnalyzerService _groupingAnalyzerService;
final SessionRepository _sessionRepository;
final Uuid _uuid = const Uuid();
AnalysisProvider({
required TargetDetectionService detectionService,
required ScoreCalculatorService scoreCalculatorService,
required GroupingAnalyzerService groupingAnalyzerService,
required SessionRepository sessionRepository,
}) : _detectionService = detectionService,
_scoreCalculatorService = scoreCalculatorService,
_groupingAnalyzerService = groupingAnalyzerService,
_sessionRepository = sessionRepository;
AnalysisState _state = AnalysisState.initial;
String? _errorMessage;
String? _imagePath;
TargetType? _targetType;
// Target detection results
double _targetCenterX = 0.5;
double _targetCenterY = 0.5;
double _targetRadius = 0.4;
int _ringCount = 10;
List<double>? _ringRadii; // Individual ring radii multipliers
double _imageAspectRatio = 1.0; // width / height
// Shots
List<Shot> _shots = [];
// Score results
ScoreResult? _scoreResult;
// Grouping results
GroupingResult? _groupingResult;
// Reference-based detection
List<Shot> _referenceImpacts = [];
ImpactCharacteristics? _learnedCharacteristics;
// Getters
AnalysisState get state => _state;
String? get errorMessage => _errorMessage;
String? get imagePath => _imagePath;
TargetType? get targetType => _targetType;
double get targetCenterX => _targetCenterX;
double get targetCenterY => _targetCenterY;
double get targetRadius => _targetRadius;
int get ringCount => _ringCount;
List<double>? get ringRadii => _ringRadii != null ? List.unmodifiable(_ringRadii!) : null;
double get imageAspectRatio => _imageAspectRatio;
List<Shot> get shots => List.unmodifiable(_shots);
ScoreResult? get scoreResult => _scoreResult;
GroupingResult? get groupingResult => _groupingResult;
int get totalScore => _scoreResult?.totalScore ?? 0;
int get shotCount => _shots.length;
List<Shot> get referenceImpacts => List.unmodifiable(_referenceImpacts);
ImpactCharacteristics? get learnedCharacteristics => _learnedCharacteristics;
bool get hasLearnedCharacteristics => _learnedCharacteristics != null;
/// Analyze an image
Future<void> analyzeImage(String imagePath, TargetType targetType) async {
_state = AnalysisState.loading;
_imagePath = imagePath;
_targetType = targetType;
_errorMessage = null;
notifyListeners();
try {
// Load image to get dimensions
final file = File(imagePath);
final bytes = await file.readAsBytes();
final codec = await ui.instantiateImageCodec(bytes);
final frame = await codec.getNextFrame();
_imageAspectRatio = frame.image.width / frame.image.height;
frame.image.dispose();
// Detect target and impacts
final result = _detectionService.detectTarget(imagePath, targetType);
if (!result.success) {
_state = AnalysisState.error;
_errorMessage = result.errorMessage;
notifyListeners();
return;
}
_targetCenterX = result.centerX;
_targetCenterY = result.centerY;
_targetRadius = result.radius;
// Create shots from detected impacts
_shots = result.impacts.map((impact) {
return Shot(
id: _uuid.v4(),
x: impact.x,
y: impact.y,
score: impact.suggestedScore,
sessionId: '', // Will be set when saving
);
}).toList();
// Calculate scores
_recalculateScores();
// Calculate grouping
_recalculateGrouping();
_state = AnalysisState.success;
notifyListeners();
} catch (e) {
_state = AnalysisState.error;
_errorMessage = 'Erreur d\'analyse: $e';
notifyListeners();
}
}
/// Add a manual shot
void addShot(double x, double y) {
final score = _calculateShotScore(x, y);
final shot = Shot(
id: _uuid.v4(),
x: x,
y: y,
score: score,
sessionId: '',
);
_shots.add(shot);
_recalculateScores();
_recalculateGrouping();
notifyListeners();
}
/// Remove a shot
void removeShot(String shotId) {
_shots.removeWhere((shot) => shot.id == shotId);
_recalculateScores();
_recalculateGrouping();
notifyListeners();
}
/// Move a shot to a new position
void moveShot(String shotId, double newX, double newY) {
final index = _shots.indexWhere((shot) => shot.id == shotId);
if (index == -1) return;
final newScore = _calculateShotScore(newX, newY);
_shots[index] = _shots[index].copyWith(
x: newX,
y: newY,
score: newScore,
);
_recalculateScores();
_recalculateGrouping();
notifyListeners();
}
/// Auto-detect impacts using image processing
Future<int> autoDetectImpacts({
int darkThreshold = 80,
int minImpactSize = 20,
int maxImpactSize = 500,
double minCircularity = 0.6,
double minFillRatio = 0.5,
bool clearExisting = false,
}) async {
if (_imagePath == null || _targetType == null) return 0;
final settings = ImpactDetectionSettings(
darkThreshold: darkThreshold,
minImpactSize: minImpactSize,
maxImpactSize: maxImpactSize,
minCircularity: minCircularity,
minFillRatio: minFillRatio,
);
final detectedImpacts = _detectionService.detectImpactsOnly(
_imagePath!,
_targetType!,
_targetCenterX,
_targetCenterY,
_targetRadius,
_ringCount,
settings,
);
if (clearExisting) {
_shots.clear();
}
// Add detected impacts as shots
for (final impact in detectedImpacts) {
final score = _calculateShotScore(impact.x, impact.y);
final shot = Shot(
id: _uuid.v4(),
x: impact.x,
y: impact.y,
score: score,
sessionId: '',
);
_shots.add(shot);
}
_recalculateScores();
_recalculateGrouping();
notifyListeners();
return detectedImpacts.length;
}
/// Add a reference impact for calibrated detection
void addReferenceImpact(double x, double y) {
final score = _calculateShotScore(x, y);
final shot = Shot(
id: _uuid.v4(),
x: x,
y: y,
score: score,
sessionId: '',
);
_referenceImpacts.add(shot);
notifyListeners();
}
/// Remove a reference impact
void removeReferenceImpact(String shotId) {
_referenceImpacts.removeWhere((shot) => shot.id == shotId);
_learnedCharacteristics = null;
notifyListeners();
}
/// Clear all reference impacts
void clearReferenceImpacts() {
_referenceImpacts.clear();
_learnedCharacteristics = null;
notifyListeners();
}
/// Learn characteristics from reference impacts
bool learnFromReferences() {
if (_imagePath == null || _referenceImpacts.length < 2) return false;
final references = _referenceImpacts
.map((shot) => ReferenceImpact(x: shot.x, y: shot.y))
.toList();
_learnedCharacteristics = _detectionService.analyzeReferenceImpacts(
_imagePath!,
references,
);
notifyListeners();
return _learnedCharacteristics != null;
}
/// Auto-detect impacts using learned reference characteristics
Future<int> detectFromReferences({
double tolerance = 2.0,
bool clearExisting = false,
}) async {
if (_imagePath == null || _targetType == null || _learnedCharacteristics == null) {
return 0;
}
final detectedImpacts = _detectionService.detectImpactsFromReferences(
_imagePath!,
_targetType!,
_targetCenterX,
_targetCenterY,
_targetRadius,
_ringCount,
_learnedCharacteristics!,
tolerance: tolerance,
);
if (clearExisting) {
_shots.clear();
}
// Add detected impacts as shots
for (final impact in detectedImpacts) {
final score = _calculateShotScore(impact.x, impact.y);
final shot = Shot(
id: _uuid.v4(),
x: impact.x,
y: impact.y,
score: score,
sessionId: '',
);
_shots.add(shot);
}
_recalculateScores();
_recalculateGrouping();
notifyListeners();
return detectedImpacts.length;
}
/// Adjust target position
void adjustTargetPosition(double centerX, double centerY, double radius, {int? ringCount, List<double>? ringRadii}) {
_targetCenterX = centerX;
_targetCenterY = centerY;
_targetRadius = radius;
if (ringCount != null) {
_ringCount = ringCount;
}
if (ringRadii != null) {
_ringRadii = ringRadii;
}
// Recalculate all shot scores based on new target position
_shots = _shots.map((shot) {
final newScore = _calculateShotScore(shot.x, shot.y);
return shot.copyWith(score: newScore);
}).toList();
_recalculateScores();
notifyListeners();
}
int _calculateShotScore(double x, double y) {
if (_targetType == TargetType.concentric) {
return _scoreCalculatorService.calculateConcentricScore(
shotX: x,
shotY: y,
targetCenterX: _targetCenterX,
targetCenterY: _targetCenterY,
targetRadius: _targetRadius,
ringCount: _ringCount,
imageAspectRatio: _imageAspectRatio,
ringRadii: _ringRadii,
);
} else {
return _scoreCalculatorService.calculateSilhouetteScore(
shotX: x,
shotY: y,
targetCenterX: _targetCenterX,
targetCenterY: _targetCenterY,
targetWidth: _targetRadius * 0.8,
targetHeight: _targetRadius * 2,
);
}
}
void _recalculateScores() {
if (_targetType == null) return;
_scoreResult = _scoreCalculatorService.calculateScores(
shots: _shots,
targetType: _targetType!,
targetCenterX: _targetCenterX,
targetCenterY: _targetCenterY,
targetRadius: _targetRadius,
ringCount: _ringCount,
imageAspectRatio: _imageAspectRatio,
ringRadii: _ringRadii,
);
}
void _recalculateGrouping() {
_groupingResult = _groupingAnalyzerService.analyzeGrouping(_shots);
}
/// Save the session
Future<Session> saveSession({String? notes}) async {
if (_imagePath == null || _targetType == null) {
throw Exception('Cannot save: missing image or target type');
}
final session = await _sessionRepository.createSession(
targetType: _targetType!,
imagePath: _imagePath!,
shots: _shots.map((s) => s.copyWith(sessionId: '')).toList(),
totalScore: totalScore,
groupingDiameter: _groupingResult?.diameter,
groupingCenterX: _groupingResult?.centerX,
groupingCenterY: _groupingResult?.centerY,
notes: notes,
targetCenterX: _targetCenterX,
targetCenterY: _targetCenterY,
targetRadius: _targetRadius,
);
// Update shots with session ID
_shots = session.shots;
notifyListeners();
return session;
}
/// Reset the provider
void reset() {
_state = AnalysisState.initial;
_errorMessage = null;
_imagePath = null;
_targetType = null;
_targetCenterX = 0.5;
_targetCenterY = 0.5;
_targetRadius = 0.4;
_ringCount = 10;
_ringRadii = null;
_imageAspectRatio = 1.0;
_shots = [];
_scoreResult = null;
_groupingResult = null;
_referenceImpacts = [];
_learnedCharacteristics = null;
notifyListeners();
}
}