premier app version beta

This commit is contained in:
2026-01-18 13:38:09 +01:00
commit 031d4a4e17
164 changed files with 13698 additions and 0 deletions

View 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,
});
}