bowling-analysis-system
Version:
A comprehensive system for analyzing bowling techniques using video processing and metrics calculation
359 lines (312 loc) • 10.4 kB
JavaScript
/**
* @fileoverview Metrics validation for bowling metrics.
* Provides functions for validating metrics structure, required events,
* and key validation checks as specified in the rules.
*/
import { extractByPath, validateTimeSeriesStructure } from './timeSeriesUtils.js';
/**
* Required bowling events that must be present
* @type {string[]}
*/
const REQUIRED_EVENTS = [
'frontFootLanding',
'backFootLanding',
'releasePoint'
];
/**
* Required categories for time series data
* @type {string[]}
*/
const REQUIRED_CATEGORIES = [
'angles',
'position',
'velocity'
];
/**
* Validates the entire metrics object against all validation rules
* @param {Object} metricsData - The complete metrics data object
* @returns {Object} - Validation result with isValid and detailed results
*/
export function validateMetrics(metricsData) {
if (!metricsData) {
return {
isValid: false,
results: [{ rule: 'general', passed: false, message: 'No metrics data provided' }]
};
}
const results = [];
// Check for required events
const eventsResult = validateRequiredEvents(metricsData);
results.push({
rule: 'requiredEvents',
passed: eventsResult.isValid,
message: eventsResult.message,
details: eventsResult.details
});
// Check for required categories
const categoriesResult = validateRequiredCategories(metricsData);
results.push({
rule: 'requiredCategories',
passed: categoriesResult.isValid,
message: categoriesResult.message,
details: categoriesResult.details
});
// Structural validation
const structuralResult = validateStructure(metricsData);
results.push({
rule: 'structure',
passed: structuralResult.isValid,
message: structuralResult.message,
details: structuralResult.details
});
// Left/right validations
const leftRightResult = validateLeftRightPairs(metricsData);
results.push({
rule: 'leftRightPairs',
passed: leftRightResult.isValid,
message: leftRightResult.message,
details: leftRightResult.details
});
// Overall validity
const isValid = results.every(result => result.passed);
return {
isValid,
results
};
}
/**
* Validates that all required events are present
* @param {Object} metricsData - The metrics data to validate
* @returns {Object} - Validation result
*/
export function validateRequiredEvents(metricsData) {
if (!metricsData.events) {
return {
isValid: false,
message: 'No events data found',
details: {
missingEvents: REQUIRED_EVENTS
}
};
}
const missingEvents = REQUIRED_EVENTS.filter(event => !metricsData.events[event]);
return {
isValid: missingEvents.length === 0,
message: missingEvents.length === 0
? 'All required events present'
: `Missing required events: ${missingEvents.join(', ')}`,
details: {
missingEvents,
presentEvents: Object.keys(metricsData.events)
}
};
}
/**
* Validates that all required categories are present
* @param {Object} metricsData - The metrics data to validate
* @returns {Object} - Validation result
*/
export function validateRequiredCategories(metricsData) {
if (!metricsData.timeSeries) {
return {
isValid: false,
message: 'No time series data found',
details: {
missingCategories: REQUIRED_CATEGORIES
}
};
}
const missingCategories = REQUIRED_CATEGORIES.filter(
category => !metricsData.timeSeries[category]
);
return {
isValid: missingCategories.length === 0,
message: missingCategories.length === 0
? 'All required categories present'
: `Missing required categories: ${missingCategories.join(', ')}`,
details: {
missingCategories,
presentCategories: Object.keys(metricsData.timeSeries)
}
};
}
/**
* Validates the overall structure of the metrics
* @param {Object} metricsData - The metrics data to validate
* @returns {Object} - Validation result
*/
export function validateStructure(metricsData) {
if (!metricsData.timeSeries || !metricsData.events || !metricsData.metrics) {
return {
isValid: false,
message: 'Missing one or more required top-level sections',
details: {
hasTimeSeries: !!metricsData.timeSeries,
hasEvents: !!metricsData.events,
hasMetrics: !!metricsData.metrics
}
};
}
// Check metadata
const hasValidMetadata = metricsData.metadata &&
typeof metricsData.metadata.totalFrames === 'number' &&
typeof metricsData.metadata.validFrames === 'number' &&
typeof metricsData.metadata.validFramesPercentage === 'number';
const structureIssues = [];
if (!hasValidMetadata) {
structureIssues.push('Missing or invalid metadata');
}
// Check time series arrays for consistent lengths
const timeSeriesArrays = findTimeSeriesArrays(metricsData.timeSeries);
const arrayLengths = timeSeriesArrays.map(path => {
const array = extractByPath(metricsData.timeSeries, path);
return { path, length: array.length };
});
if (arrayLengths.length > 0) {
const firstLength = arrayLengths[0].length;
const inconsistentArrays = arrayLengths.filter(item => item.length !== firstLength);
if (inconsistentArrays.length > 0) {
structureIssues.push(
`Inconsistent time series array lengths: ${inconsistentArrays.map(a => a.path).join(', ')}`
);
}
}
return {
isValid: structureIssues.length === 0,
message: structureIssues.length === 0
? 'Valid structure'
: `Structure issues: ${structureIssues.join('; ')}`,
details: {
structureIssues,
timeSeriesArrayLengths: arrayLengths
}
};
}
/**
* Validates left/right pairs in metrics
* @param {Object} metricsData - The metrics data to validate
* @returns {Object} - Validation result
*/
export function validateLeftRightPairs(metricsData) {
const issues = [];
// Check metrics section for left/right/average format
if (metricsData.metrics) {
const metricsWithLeftRight = findObjectsWithLeftRightProps(metricsData.metrics);
const invalidMetrics = metricsWithLeftRight.filter(path => {
const metric = extractByPath(metricsData.metrics, path);
return !isValidLeftRightAverage(metric);
});
if (invalidMetrics.length > 0) {
issues.push(
`Metrics with invalid left/right/average format: ${invalidMetrics.join(', ')}`
);
}
}
// Check time series for left/right/average format
if (metricsData.timeSeries) {
const timeSeriesWithLeftRight = findTimeSeriesWithLeftRightFormat(metricsData.timeSeries);
const invalidTimeSeries = timeSeriesWithLeftRight.filter(path => {
const array = extractByPath(metricsData.timeSeries, path);
return array.some(item => !isValidLeftRightAverage(item));
});
if (invalidTimeSeries.length > 0) {
issues.push(
`Time series with invalid left/right/average format: ${invalidTimeSeries.join(', ')}`
);
}
}
return {
isValid: issues.length === 0,
message: issues.length === 0
? 'Valid left/right pairs'
: `Left/right pair issues: ${issues.join('; ')}`,
details: {
issues
}
};
}
/**
* Checks if an object has the valid left/right/average structure
* @param {Object} obj - The object to check
* @returns {boolean} - Whether the object has valid structure
*/
function isValidLeftRightAverage(obj) {
return (
obj &&
typeof obj === 'object' &&
'left' in obj &&
'right' in obj &&
'average' in obj
);
}
/**
* Finds all time series arrays in the data
* @param {Object} timeSeries - The time series object
* @param {string} basePath - Base path for recursion
* @returns {string[]} - Array of paths to time series arrays
*/
function findTimeSeriesArrays(timeSeries, basePath = '') {
const results = [];
for (const key in timeSeries) {
const value = timeSeries[key];
const currentPath = basePath ? `${basePath}.${key}` : key;
if (Array.isArray(value)) {
results.push(currentPath);
} else if (value && typeof value === 'object') {
results.push(...findTimeSeriesArrays(value, currentPath));
}
}
return results;
}
/**
* Finds all objects with left and right properties
* @param {Object} obj - The object to search
* @param {string} basePath - Base path for recursion
* @returns {string[]} - Array of paths to objects with left/right props
*/
function findObjectsWithLeftRightProps(obj, basePath = '') {
const results = [];
for (const key in obj) {
const value = obj[key];
const currentPath = basePath ? `${basePath}.${key}` : key;
if (value && typeof value === 'object' && !Array.isArray(value)) {
if ('left' in value && 'right' in value) {
results.push(currentPath);
}
results.push(...findObjectsWithLeftRightProps(value, currentPath));
}
}
return results;
}
/**
* Finds time series arrays using left/right/average format
* @param {Object} timeSeries - The time series object
* @param {string} basePath - Base path for recursion
* @returns {string[]} - Array of paths to time series with left/right format
*/
function findTimeSeriesWithLeftRightFormat(timeSeries, basePath = '') {
const results = [];
for (const key in timeSeries) {
const value = timeSeries[key];
const currentPath = basePath ? `${basePath}.${key}` : key;
if (Array.isArray(value) && value.length > 0 &&
typeof value[0] === 'object' && 'left' in value[0] && 'right' in value[0]) {
results.push(currentPath);
} else if (value && typeof value === 'object') {
results.push(...findTimeSeriesWithLeftRightFormat(value, currentPath));
}
}
return results;
}
/**
* Module exports
*/
export default {
validateMetrics,
validateRequiredEvents,
validateRequiredCategories,
validateStructure,
validateLeftRightPairs,
REQUIRED_EVENTS,
REQUIRED_CATEGORIES
};