import 'dart:math' as math; import '../data/models/shot.dart'; class GroupingResult { final double centerX; // Center of the group (relative 0-1) final double centerY; final double diameter; // Maximum spread diameter (relative 0-1) final double standardDeviation; // Dispersion metric final double meanRadius; // Average distance from center final int shotCount; GroupingResult({ required this.centerX, required this.centerY, required this.diameter, required this.standardDeviation, required this.meanRadius, required this.shotCount, }); /// Grouping quality rating (1-5 stars) int get qualityRating { // Based on standard deviation relative to typical target size if (standardDeviation < 0.02) return 5; if (standardDeviation < 0.04) return 4; if (standardDeviation < 0.06) return 3; if (standardDeviation < 0.10) return 2; return 1; } String get qualityDescription { switch (qualityRating) { case 5: return 'Excellent'; case 4: return 'Tres bien'; case 3: return 'Bien'; case 2: return 'Moyen'; default: return 'A ameliorer'; } } factory GroupingResult.empty() { return GroupingResult( centerX: 0.5, centerY: 0.5, diameter: 0, standardDeviation: 0, meanRadius: 0, shotCount: 0, ); } } class GroupingAnalyzerService { /// Analyze the grouping of a list of shots GroupingResult analyzeGrouping(List shots) { if (shots.isEmpty) { return GroupingResult.empty(); } if (shots.length == 1) { return GroupingResult( centerX: shots.first.x, centerY: shots.first.y, diameter: 0, standardDeviation: 0, meanRadius: 0, shotCount: 1, ); } // Calculate center of group (centroid) double sumX = 0; double sumY = 0; for (final shot in shots) { sumX += shot.x; sumY += shot.y; } final centerX = sumX / shots.length; final centerY = sumY / shots.length; // Calculate distances from center final distances = []; for (final shot in shots) { final dx = shot.x - centerX; final dy = shot.y - centerY; distances.add(math.sqrt(dx * dx + dy * dy)); } // Calculate mean radius final meanRadius = distances.reduce((a, b) => a + b) / distances.length; // Calculate standard deviation double sumSquaredDiff = 0; for (final distance in distances) { sumSquaredDiff += math.pow(distance - meanRadius, 2); } final standardDeviation = math.sqrt(sumSquaredDiff / distances.length); // Calculate maximum spread (diameter) // Find the two points that are farthest apart double maxDistance = 0; for (int i = 0; i < shots.length; i++) { for (int j = i + 1; j < shots.length; j++) { final dx = shots[i].x - shots[j].x; final dy = shots[i].y - shots[j].y; final distance = math.sqrt(dx * dx + dy * dy); if (distance > maxDistance) { maxDistance = distance; } } } return GroupingResult( centerX: centerX, centerY: centerY, diameter: maxDistance, standardDeviation: standardDeviation, meanRadius: meanRadius, shotCount: shots.length, ); } /// Calculate offset from target center (double, double) calculateOffset({ required double groupCenterX, required double groupCenterY, required double targetCenterX, required double targetCenterY, }) { return ( groupCenterX - targetCenterX, groupCenterY - targetCenterY, ); } /// Get directional offset description (e.g., "haut-gauche") String getOffsetDescription(double offsetX, double offsetY) { if (offsetX.abs() < 0.02 && offsetY.abs() < 0.02) { return 'Centre'; } String vertical = ''; String horizontal = ''; if (offsetY < -0.02) { vertical = 'Haut'; } else if (offsetY > 0.02) { vertical = 'Bas'; } if (offsetX < -0.02) { horizontal = 'Gauche'; } else if (offsetX > 0.02) { horizontal = 'Droite'; } if (vertical.isNotEmpty && horizontal.isNotEmpty) { return '$vertical-$horizontal'; } return vertical.isNotEmpty ? vertical : horizontal; } /// Analyze trend across multiple sessions GroupingTrend analyzeTrend(List results) { if (results.length < 2) { return GroupingTrend( improving: false, averageDiameter: results.isEmpty ? 0 : results.first.diameter, recentDiameter: results.isEmpty ? 0 : results.first.diameter, improvementPercentage: 0, ); } // Calculate averages for first half vs second half final midpoint = results.length ~/ 2; final firstHalf = results.sublist(0, midpoint); final secondHalf = results.sublist(midpoint); double firstHalfAvg = 0; for (final r in firstHalf) { firstHalfAvg += r.diameter; } firstHalfAvg /= firstHalf.length; double secondHalfAvg = 0; for (final r in secondHalf) { secondHalfAvg += r.diameter; } secondHalfAvg /= secondHalf.length; // Overall average double totalAvg = 0; for (final r in results) { totalAvg += r.diameter; } totalAvg /= results.length; final improvement = ((firstHalfAvg - secondHalfAvg) / firstHalfAvg) * 100; return GroupingTrend( improving: secondHalfAvg < firstHalfAvg, averageDiameter: totalAvg, recentDiameter: secondHalfAvg, improvementPercentage: improvement, ); } } class GroupingTrend { final bool improving; final double averageDiameter; final double recentDiameter; final double improvementPercentage; GroupingTrend({ required this.improving, required this.averageDiameter, required this.recentDiameter, required this.improvementPercentage, }); }