import 'dart:math' as math; import '../data/models/session.dart'; import '../data/models/shot.dart'; /// Time period for filtering statistics enum StatsPeriod { session, // Single session week, // Last 7 days month, // Last 30 days all, // All time } /// Heat zone data for a region of the target class HeatZone { final int row; final int col; final int shotCount; final double intensity; // 0-1, normalized final double avgScore; const HeatZone({ required this.row, required this.col, required this.shotCount, required this.intensity, required this.avgScore, }); } /// Heat map grid for the target class HeatMap { final int gridSize; final List> zones; final int maxShotsInZone; final int totalShots; const HeatMap({ required this.gridSize, required this.zones, required this.maxShotsInZone, required this.totalShots, }); } /// Precision statistics class PrecisionStats { /// Average distance from target center (0-1 normalized) final double avgDistanceFromCenter; /// Grouping diameter (spread of shots) final double groupingDiameter; /// Precision score (0-100, higher = better) final double precisionScore; /// Consistency score based on standard deviation (0-100) final double consistencyScore; const PrecisionStats({ required this.avgDistanceFromCenter, required this.groupingDiameter, required this.precisionScore, required this.consistencyScore, }); } /// Standard deviation statistics class StdDevStats { final double stdDevX; final double stdDevY; final double stdDevRadial; final double stdDevScore; final double meanX; final double meanY; final double meanScore; const StdDevStats({ required this.stdDevX, required this.stdDevY, required this.stdDevRadial, required this.stdDevScore, required this.meanX, required this.meanY, required this.meanScore, }); } /// Regional distribution class RegionalStats { final Map quadrantDistribution; final Map sectorDistribution; final String dominantDirection; final double biasX; final double biasY; const RegionalStats({ required this.quadrantDistribution, required this.sectorDistribution, required this.dominantDirection, required this.biasX, required this.biasY, }); } /// Complete statistics result class SessionStatistics { final int totalShots; final int totalScore; final double avgScore; final int maxScore; final int minScore; final HeatMap heatMap; final PrecisionStats precision; final StdDevStats stdDev; final RegionalStats regional; final List sessions; final StatsPeriod period; const SessionStatistics({ required this.totalShots, required this.totalScore, required this.avgScore, required this.maxScore, required this.minScore, required this.heatMap, required this.precision, required this.stdDev, required this.regional, required this.sessions, required this.period, }); } /// Service for calculating shooting statistics class StatisticsService { /// Calculate statistics for given sessions SessionStatistics calculateStatistics( List sessions, { StatsPeriod period = StatsPeriod.all, }) { // Filter sessions by period final filteredSessions = _filterByPeriod(sessions, period); // Collect all shots from all analyses in all sessions final allShots = []; for (final session in filteredSessions) { for (final analysis in session.analyses) { allShots.addAll(analysis.shots); } } if (allShots.isEmpty) { return _emptyStatistics(period, filteredSessions); } // Calculate basic stats final totalShots = allShots.length; final totalScore = allShots.fold(0, (sum, shot) => sum + shot.score); final avgScore = totalScore / totalShots; final maxScore = allShots.map((s) => s.score).reduce(math.max); final minScore = allShots.map((s) => s.score).reduce(math.min); // Calculate heat map final heatMap = _calculateHeatMap(allShots, gridSize: 5); // Calculate precision (using relative center 0.5, 0.5) final precision = _calculatePrecision(allShots, 0.5, 0.5); // Calculate standard deviation final stdDev = _calculateStdDev(allShots); // Calculate regional distribution final regional = _calculateRegional(allShots, 0.5, 0.5); return SessionStatistics( totalShots: totalShots, totalScore: totalScore, avgScore: avgScore, maxScore: maxScore, minScore: minScore, heatMap: heatMap, precision: precision, stdDev: stdDev, regional: regional, sessions: filteredSessions, period: period, ); } /// Filter sessions by time period List _filterByPeriod(List sessions, StatsPeriod period) { if (period == StatsPeriod.all) return sessions; final now = DateTime.now(); final cutoff = switch (period) { StatsPeriod.session => now.subtract(const Duration(hours: 24)), StatsPeriod.week => now.subtract(const Duration(days: 7)), StatsPeriod.month => now.subtract(const Duration(days: 30)), StatsPeriod.all => DateTime(1970), }; return sessions.where((s) => s.createdAt.isAfter(cutoff)).toList(); } /// Calculate heat map HeatMap _calculateHeatMap(List shots, {int gridSize = 5}) { final grid = List.generate(gridSize, (_) => List.generate(gridSize, (_) => [])); for (final shot in shots) { final col = (shot.x * gridSize).floor().clamp(0, gridSize - 1); final row = (shot.y * gridSize).floor().clamp(0, gridSize - 1); grid[row][col].add(shot); } int maxCount = 0; for (final row in grid) { for (final cell in row) { if (cell.length > maxCount) maxCount = cell.length; } } final zones = >[]; for (int row = 0; row < gridSize; row++) { final rowZones = []; for (int col = 0; col < gridSize; col++) { final cellShots = grid[row][col]; final avgScore = cellShots.isEmpty ? 0.0 : cellShots.fold(0, (sum, s) => sum + s.score) / cellShots.length; rowZones.add(HeatZone( row: row, col: col, shotCount: cellShots.length, intensity: maxCount > 0 ? cellShots.length / maxCount : 0, avgScore: avgScore, )); } zones.add(rowZones); } return HeatMap(gridSize: gridSize, zones: zones, maxShotsInZone: maxCount, totalShots: shots.length); } /// Calculate precision statistics PrecisionStats _calculatePrecision(List shots, double centerX, double centerY) { if (shots.isEmpty) { return const PrecisionStats(avgDistanceFromCenter: 0, groupingDiameter: 0, precisionScore: 0, consistencyScore: 0); } final distances = shots.map((shot) { final dx = shot.x - centerX; final dy = shot.y - centerY; return math.sqrt(dx * dx + dy * dy); }).toList(); final avgDistance = distances.reduce((a, b) => a + b) / distances.length; double maxSpread = 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 dist = math.sqrt(dx * dx + dy * dy); if (dist > maxSpread) maxSpread = dist; } } double variance = 0; for (final d in distances) { variance += math.pow(d - avgDistance, 2); } final stdDevDist = math.sqrt(variance / distances.length); final precisionScore = math.max(0, (1 - avgDistance * 2) * 100); final consistencyScore = math.max(0, (1 - stdDevDist * 5) * 100); return PrecisionStats( avgDistanceFromCenter: avgDistance, groupingDiameter: maxSpread, precisionScore: precisionScore.toDouble().clamp(0.0, 100.0), consistencyScore: consistencyScore.toDouble().clamp(0.0, 100.0), ); } /// Calculate standard deviation statistics StdDevStats _calculateStdDev(List shots) { if (shots.isEmpty) { return const StdDevStats(stdDevX: 0, stdDevY: 0, stdDevRadial: 0, stdDevScore: 0, meanX: 0.5, meanY: 0.5, meanScore: 0); } double sumX = 0, sumY = 0, sumScore = 0; for (final shot in shots) { sumX += shot.x; sumY += shot.y; sumScore += shot.score; } final meanX = sumX / shots.length; final meanY = sumY / shots.length; final meanScore = sumScore / shots.length; double varianceX = 0, varianceY = 0, varianceScore = 0; for (final shot in shots) { varianceX += math.pow(shot.x - meanX, 2); varianceY += math.pow(shot.y - meanY, 2); varianceScore += math.pow(shot.score - meanScore, 2); } varianceX /= shots.length; varianceY /= shots.length; varianceScore /= shots.length; return StdDevStats( stdDevX: math.sqrt(varianceX), stdDevY: math.sqrt(varianceY), stdDevRadial: math.sqrt(varianceX + varianceY), stdDevScore: math.sqrt(varianceScore), meanX: meanX, meanY: meanY, meanScore: meanScore, ); } /// Calculate regional distribution RegionalStats _calculateRegional(List shots, double centerX, double centerY) { if (shots.isEmpty) { return const RegionalStats(quadrantDistribution: {}, sectorDistribution: {}, dominantDirection: 'Centre', biasX: 0, biasY: 0); } final quadrants = {'Haut-Gauche': 0, 'Haut-Droite': 0, 'Bas-Gauche': 0, 'Bas-Droite': 0}; final sectors = {'N': 0, 'NE': 0, 'E': 0, 'SE': 0, 'S': 0, 'SO': 0, 'O': 0, 'NO': 0, 'Centre': 0}; double sumDx = 0, sumDy = 0; for (final shot in shots) { final dx = shot.x - centerX; final dy = shot.y - centerY; sumDx += dx; sumDy += dy; if (dy < 0) { quadrants[dx < 0 ? 'Haut-Gauche' : 'Haut-Droite'] = quadrants[dx < 0 ? 'Haut-Gauche' : 'Haut-Droite']! + 1; } else { quadrants[dx < 0 ? 'Bas-Gauche' : 'Bas-Droite'] = quadrants[dx < 0 ? 'Bas-Gauche' : 'Bas-Droite']! + 1; } final distance = math.sqrt(dx * dx + dy * dy); if (distance < 0.1) { sectors['Centre'] = sectors['Centre']! + 1; } else { final angle = math.atan2(dy, dx) * 180 / math.pi; final sector = _angleToSector(angle); sectors[sector] = sectors[sector]! + 1; } } String dominant = 'Centre'; int maxCount = 0; sectors.forEach((key, value) { if (value > maxCount) { maxCount = value; dominant = key; } }); return RegionalStats( quadrantDistribution: quadrants, sectorDistribution: sectors, dominantDirection: dominant, biasX: sumDx / shots.length, biasY: sumDy / shots.length, ); } String _angleToSector(double angle) { if (angle >= -22.5 && angle < 22.5) return 'E'; if (angle >= 22.5 && angle < 67.5) return 'SE'; if (angle >= 67.5 && angle < 112.5) return 'S'; if (angle >= 112.5 && angle < 157.5) return 'SO'; if (angle >= 157.5 || angle < -157.5) return 'O'; if (angle >= -157.5 && angle < -112.5) return 'NO'; if (angle >= -112.5 && angle < -67.5) return 'N'; if (angle >= -67.5 && angle < -22.5) return 'NE'; return 'Centre'; } SessionStatistics _emptyStatistics(StatsPeriod period, List sessions) { return SessionStatistics( totalShots: 0, totalScore: 0, avgScore: 0, maxScore: 0, minScore: 0, heatMap: const HeatMap(gridSize: 5, zones: [], maxShotsInZone: 0, totalShots: 0), precision: const PrecisionStats(avgDistanceFromCenter: 0, groupingDiameter: 0, precisionScore: 0, consistencyScore: 0), stdDev: const StdDevStats(stdDevX: 0, stdDevY: 0, stdDevRadial: 0, stdDevScore: 0, meanX: 0.5, meanY: 0.5, meanScore: 0), regional: const RegionalStats(quadrantDistribution: {}, sectorDistribution: {}, dominantDirection: 'Centre', biasX: 0, biasY: 0), sessions: sessions, period: period, ); } }