premier app version beta
This commit is contained in:
230
lib/services/grouping_analyzer_service.dart
Normal file
230
lib/services/grouping_analyzer_service.dart
Normal file
@@ -0,0 +1,230 @@
|
||||
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<Shot> 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 = <double>[];
|
||||
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<GroupingResult> 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,
|
||||
});
|
||||
}
|
||||
Reference in New Issue
Block a user