UNPKG

bowling-analysis-system

Version:

A comprehensive system for analyzing bowling techniques using video processing and metrics calculation

359 lines (312 loc) 10.4 kB
/** * @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 };