feat: implement base architecture and core repositories for weapon tracking and target analysis functionality

This commit is contained in:
streaper2
2026-05-08 20:29:18 +02:00
parent 5dd58da51c
commit 774dbfcf40
37 changed files with 3687 additions and 2713 deletions

View File

@@ -66,25 +66,12 @@ class PrecisionStats {
/// Standard deviation statistics
class StdDevStats {
/// Standard deviation of X positions
final double stdDevX;
/// Standard deviation of Y positions
final double stdDevY;
/// Combined standard deviation (radial)
final double stdDevRadial;
/// Standard deviation of scores
final double stdDevScore;
/// Mean X position
final double meanX;
/// Mean Y position
final double meanY;
/// Mean score
final double meanScore;
const StdDevStats({
@@ -98,18 +85,11 @@ class StdDevStats {
});
}
/// Regional distribution (quadrants or sectors)
/// Regional distribution
class RegionalStats {
/// Shot distribution by quadrant (top-left, top-right, bottom-left, bottom-right)
final Map<String, int> quadrantDistribution;
/// Shot distribution by sector (N, NE, E, SE, S, SW, W, NW, Center)
final Map<String, int> sectorDistribution;
/// Dominant direction (where most shots land)
final String dominantDirection;
/// Bias offset from center
final double biasX;
final double biasY;
@@ -157,16 +137,16 @@ class StatisticsService {
SessionStatistics calculateStatistics(
List<Session> sessions, {
StatsPeriod period = StatsPeriod.all,
double targetCenterX = 0.5,
double targetCenterY = 0.5,
}) {
// Filter sessions by period
final filteredSessions = _filterByPeriod(sessions, period);
// Collect all shots
// Collect all shots from all analyses in all sessions
final allShots = <Shot>[];
for (final session in filteredSessions) {
allShots.addAll(session.shots);
for (final analysis in session.analyses) {
allShots.addAll(analysis.shots);
}
}
if (allShots.isEmpty) {
@@ -183,14 +163,14 @@ class StatisticsService {
// Calculate heat map
final heatMap = _calculateHeatMap(allShots, gridSize: 5);
// Calculate precision
final precision = _calculatePrecision(allShots, targetCenterX, targetCenterY);
// 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, targetCenterX, targetCenterY);
final regional = _calculateRegional(allShots, 0.5, 0.5);
return SessionStatistics(
totalShots: totalShots,
@@ -224,20 +204,13 @@ class StatisticsService {
/// Calculate heat map
HeatMap _calculateHeatMap(List<Shot> shots, {int gridSize = 5}) {
// Initialize grid
final grid = List.generate(
gridSize,
(_) => List.generate(gridSize, (_) => <Shot>[]),
);
// Assign shots to grid cells
final grid = List.generate(gridSize, (_) => List.generate(gridSize, (_) => <Shot>[]));
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);
}
// Find max count for normalization
int maxCount = 0;
for (final row in grid) {
for (final cell in row) {
@@ -245,16 +218,12 @@ class StatisticsService {
}
}
// Create heat zones
final zones = <List<HeatZone>>[];
for (int row = 0; row < gridSize; row++) {
final rowZones = <HeatZone>[];
for (int col = 0; col < gridSize; col++) {
final cellShots = grid[row][col];
final avgScore = cellShots.isEmpty
? 0.0
: cellShots.fold<int>(0, (sum, s) => sum + s.score) / cellShots.length;
final avgScore = cellShots.isEmpty ? 0.0 : cellShots.fold<int>(0, (sum, s) => sum + s.score) / cellShots.length;
rowZones.add(HeatZone(
row: row,
col: col,
@@ -266,30 +235,15 @@ class StatisticsService {
zones.add(rowZones);
}
return HeatMap(
gridSize: gridSize,
zones: zones,
maxShotsInZone: maxCount,
totalShots: shots.length,
);
return HeatMap(gridSize: gridSize, zones: zones, maxShotsInZone: maxCount, totalShots: shots.length);
}
/// Calculate precision statistics
PrecisionStats _calculatePrecision(
List<Shot> shots,
double centerX,
double centerY,
) {
PrecisionStats _calculatePrecision(List<Shot> shots, double centerX, double centerY) {
if (shots.isEmpty) {
return const PrecisionStats(
avgDistanceFromCenter: 0,
groupingDiameter: 0,
precisionScore: 0,
consistencyScore: 0,
);
return const PrecisionStats(avgDistanceFromCenter: 0, groupingDiameter: 0, precisionScore: 0, consistencyScore: 0);
}
// Calculate distances from center
final distances = shots.map((shot) {
final dx = shot.x - centerX;
final dy = shot.y - centerY;
@@ -297,8 +251,6 @@ class StatisticsService {
}).toList();
final avgDistance = distances.reduce((a, b) => a + b) / distances.length;
// Calculate grouping (spread between shots)
double maxSpread = 0;
for (int i = 0; i < shots.length; i++) {
for (int j = i + 1; j < shots.length; j++) {
@@ -309,45 +261,29 @@ class StatisticsService {
}
}
// Calculate standard deviation of distances (consistency)
final meanDist = avgDistance;
double variance = 0;
for (final d in distances) {
variance += math.pow(d - meanDist, 2);
variance += math.pow(d - avgDistance, 2);
}
final stdDevDist = math.sqrt(variance / distances.length);
// Precision score: based on average distance from center (0-100)
// 0 distance = 100 score, 0.5 distance = 0 score
final precisionScore = math.max(0, (1 - avgDistance * 2) * 100);
// Consistency score: based on grouping tightness (0-100)
// Lower spread = higher consistency
final consistencyScore = math.max(0, (1 - stdDevDist * 5) * 100);
return PrecisionStats(
avgDistanceFromCenter: avgDistance.toDouble(),
groupingDiameter: maxSpread.toDouble(),
precisionScore: precisionScore.clamp(0.0, 100.0).toDouble(),
consistencyScore: consistencyScore.clamp(0.0, 100.0).toDouble(),
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<Shot> shots) {
if (shots.isEmpty) {
return const StdDevStats(
stdDevX: 0,
stdDevY: 0,
stdDevRadial: 0,
stdDevScore: 0,
meanX: 0.5,
meanY: 0.5,
meanScore: 0,
);
return const StdDevStats(stdDevX: 0, stdDevY: 0, stdDevRadial: 0, stdDevScore: 0, meanX: 0.5, meanY: 0.5, meanScore: 0);
}
// Calculate means
double sumX = 0, sumY = 0, sumScore = 0;
for (final shot in shots) {
sumX += shot.x;
@@ -358,7 +294,6 @@ class StatisticsService {
final meanY = sumY / shots.length;
final meanScore = sumScore / shots.length;
// Calculate variances
double varianceX = 0, varianceY = 0, varianceScore = 0;
for (final shot in shots) {
varianceX += math.pow(shot.x - meanX, 2);
@@ -369,18 +304,11 @@ class StatisticsService {
varianceY /= shots.length;
varianceScore /= shots.length;
final stdDevX = math.sqrt(varianceX);
final stdDevY = math.sqrt(varianceY);
final stdDevScore = math.sqrt(varianceScore);
// Radial standard deviation
final stdDevRadial = math.sqrt(varianceX + varianceY);
return StdDevStats(
stdDevX: stdDevX,
stdDevY: stdDevY,
stdDevRadial: stdDevRadial,
stdDevScore: stdDevScore,
stdDevX: math.sqrt(varianceX),
stdDevY: math.sqrt(varianceY),
stdDevRadial: math.sqrt(varianceX + varianceY),
stdDevScore: math.sqrt(varianceScore),
meanX: meanX,
meanY: meanY,
meanScore: meanScore,
@@ -388,42 +316,13 @@ class StatisticsService {
}
/// Calculate regional distribution
RegionalStats _calculateRegional(
List<Shot> shots,
double centerX,
double centerY,
) {
RegionalStats _calculateRegional(List<Shot> shots, double centerX, double centerY) {
if (shots.isEmpty) {
return const RegionalStats(
quadrantDistribution: {},
sectorDistribution: {},
dominantDirection: 'Centre',
biasX: 0,
biasY: 0,
);
return const RegionalStats(quadrantDistribution: {}, sectorDistribution: {}, dominantDirection: 'Centre', biasX: 0, biasY: 0);
}
// Quadrant distribution
final quadrants = <String, int>{
'Haut-Gauche': 0,
'Haut-Droite': 0,
'Bas-Gauche': 0,
'Bas-Droite': 0,
};
// Sector distribution (8 sectors + center)
final sectors = <String, int>{
'N': 0,
'NE': 0,
'E': 0,
'SE': 0,
'S': 0,
'SO': 0,
'O': 0,
'NO': 0,
'Centre': 0,
};
final quadrants = <String, int>{'Haut-Gauche': 0, 'Haut-Droite': 0, 'Bas-Gauche': 0, 'Bas-Droite': 0};
final sectors = <String, int>{'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) {
@@ -432,16 +331,12 @@ class StatisticsService {
sumDx += dx;
sumDy += dy;
// Quadrant
if (dy < 0) {
quadrants[dx < 0 ? 'Haut-Gauche' : 'Haut-Droite'] =
quadrants[dx < 0 ? 'Haut-Gauche' : 'Haut-Droite']! + 1;
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;
quadrants[dx < 0 ? 'Bas-Gauche' : 'Bas-Droite'] = quadrants[dx < 0 ? 'Bas-Gauche' : 'Bas-Droite']! + 1;
}
// Sector
final distance = math.sqrt(dx * dx + dy * dy);
if (distance < 0.1) {
sectors['Centre'] = sectors['Centre']! + 1;
@@ -452,11 +347,6 @@ class StatisticsService {
}
}
// Calculate bias
final biasX = sumDx / shots.length;
final biasY = sumDy / shots.length;
// Find dominant direction
String dominant = 'Centre';
int maxCount = 0;
sectors.forEach((key, value) {
@@ -470,14 +360,12 @@ class StatisticsService {
quadrantDistribution: quadrants,
sectorDistribution: sectors,
dominantDirection: dominant,
biasX: biasX,
biasY: biasY,
biasX: sumDx / shots.length,
biasY: sumDy / shots.length,
);
}
String _angleToSector(double angle) {
// Angle is in degrees, -180 to 180
// 0 = East, 90 = South, -90 = North, 180/-180 = West
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';
@@ -491,41 +379,12 @@ class StatisticsService {
SessionStatistics _emptyStatistics(StatsPeriod period, List<Session> 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,
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,
);
}
}