UNPKG

bowling-analysis-system

Version:

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

308 lines (260 loc) 8.72 kB
/** * @fileoverview Utility functions for extracting and analyzing bowling metrics * @module utils/metricsExtractor */ const { getNestedProperty, getTimeSeriesFromPath } = require('./timeSeriesConverter'); /** * Extracts key metrics from a complete metrics dataset * @param {Object} metricsData - Complete metrics data object * @param {Array<string>} metricPaths - Array of dot-notation paths to extract * @returns {Object} Extracted metrics */ function extractKeyMetrics(metricsData, metricPaths) { if (!metricsData || !Array.isArray(metricPaths)) { return {}; } const result = {}; metricPaths.forEach(path => { const value = getNestedProperty(metricsData, path); if (value !== undefined) { result[path] = value; } }); return result; } /** * Extracts all metrics for a specific event * @param {Object} metricsData - Complete metrics data object * @param {string} eventName - Name of the event (e.g., 'releasePoint', 'frontFootLanding') * @returns {Object} All metrics related to the event */ function extractEventMetrics(metricsData, eventName) { if (!metricsData || !eventName) { return {}; } const result = {}; // Find all metrics related to the event const eventMetricPaths = findMetricPathsByPrefix(metricsData, eventName.toLowerCase()); eventMetricPaths.forEach(path => { const value = getNestedProperty(metricsData, path); if (value !== undefined) { result[path] = value; } }); return result; } /** * Finds all metric paths with a given prefix using recursive search * @param {Object} data - The data object to search * @param {string} prefix - The prefix to search for * @param {string} [currentPath=''] - Current path in the recursion * @param {Array} [results=[]] - Accumulated results * @returns {Array} Array of matching paths */ function findMetricPathsByPrefix(data, prefix, currentPath = '', results = []) { if (!data || typeof data !== 'object' || data === null) { return results; } Object.keys(data).forEach(key => { const newPath = currentPath ? `${currentPath}.${key}` : key; if (key.toLowerCase().includes(prefix)) { results.push(newPath); } if (typeof data[key] === 'object' && data[key] !== null && !Array.isArray(data[key])) { findMetricPathsByPrefix(data[key], prefix, newPath, results); } }); return results; } /** * Extracts event data from metrics * @param {Object} metricsData - Complete metrics data object * @returns {Object} Extracted event data with frame indices */ function extractEvents(metricsData) { if (!metricsData || !metricsData.events) { return {}; } const events = metricsData.events; const result = {}; // Standard bowling events to look for const standardEvents = [ 'releasePoint', 'frontFootLanding', 'backFootLanding', 'loadPosition', 'followThrough' ]; // Extract standard events standardEvents.forEach(eventName => { if (events[eventName]) { result[eventName] = events[eventName]; } }); // Extract any other events Object.keys(events).forEach(eventName => { if (!standardEvents.includes(eventName)) { result[eventName] = events[eventName]; } }); return result; } /** * Extracts time series data for a specific metric * @param {Object} metricsData - Complete metrics data object * @param {string} metricPath - Dot-notation path to the metric * @returns {Array|null} Time series data or null if not found */ function extractTimeSeriesData(metricsData, metricPath) { if (!metricsData || !metricPath) { return null; } // Check if path points to timeSeries data if (!metricPath.includes('timeSeries')) { // Try to find in timeSeries const parts = metricPath.split('.'); const category = parts[0]; // First part is usually the category (angles, position, etc.) if (parts.length > 1) { const name = parts[parts.length - 1]; // Last part is the metric name const timeSeriesPath = `timeSeries.${category}.${name}`; return getTimeSeriesFromPath(metricsData, timeSeriesPath); } } // Path already includes timeSeries, use it directly return getTimeSeriesFromPath(metricsData, metricPath); } /** * Extracts metrics for analysis between two events * @param {Object} metricsData - Complete metrics data object * @param {string} metricPath - Dot-notation path to the time series metric * @param {string} startEvent - Name of the start event * @param {string} endEvent - Name of the end event * @returns {Object} Metrics between the events or null if events not found */ function extractMetricsBetweenEvents(metricsData, metricPath, startEvent, endEvent) { if (!metricsData || !metricPath || !startEvent || !endEvent) { return null; } // Get the events const events = extractEvents(metricsData); if (!events[startEvent] || !events[endEvent]) { return null; } const startFrame = events[startEvent].frameIndex; const endFrame = events[endEvent].frameIndex; if (startFrame === undefined || endFrame === undefined) { return null; } // Get the time series data const timeSeriesData = extractTimeSeriesData(metricsData, metricPath); if (!timeSeriesData || !Array.isArray(timeSeriesData)) { return null; } // Extract data between the events const slicedData = timeSeriesData.slice(startFrame, endFrame + 1); // Calculate statistics return { metric: metricPath, startEvent, endEvent, startFrame, endFrame, values: slicedData, statistics: calculateStatistics(slicedData) }; } /** * Calculates statistics for a time series * @param {Array} data - Time series data * @returns {Object} Statistics including min, max, avg, etc. */ function calculateStatistics(data) { if (!Array.isArray(data) || data.length === 0) { return { min: null, max: null, avg: null, median: null, start: null, end: null, change: null, stdDev: null }; } // Check if data contains left/right format const isLeftRightFormat = data.some(item => item !== null && typeof item === 'object' && ('left' in item || 'right' in item) ); if (isLeftRightFormat) { // Calculate stats for left and right only return { left: calculateSimpleStatistics(data.map(item => item ? item.left : null)), right: calculateSimpleStatistics(data.map(item => item ? item.right : null)) }; } // Regular data return calculateSimpleStatistics(data); } /** * Calculates simple statistics for an array of values * @param {Array} values - Array of numeric values * @returns {Object} Simple statistics */ function calculateSimpleStatistics(values) { // Filter out null/undefined values const validValues = values.filter(val => val !== null && val !== undefined); if (validValues.length === 0) { return { min: null, max: null, avg: null, median: null, start: null, end: null, change: null, stdDev: null }; } // Sort values for min/max/median const sortedValues = [...validValues].sort((a, b) => a - b); // Calculate basic statistics const min = sortedValues[0]; const max = sortedValues[sortedValues.length - 1]; const sum = validValues.reduce((acc, val) => acc + val, 0); const avg = sum / validValues.length; // Calculate median let median; const middleIndex = Math.floor(sortedValues.length / 2); if (sortedValues.length % 2 === 0) { median = (sortedValues[middleIndex - 1] + sortedValues[middleIndex]) / 2; } else { median = sortedValues[middleIndex]; } // Start and end values const start = values[0]; const end = values[values.length - 1]; const change = end !== null && start !== null ? end - start : null; // Standard deviation const squaredDiffs = validValues.map(val => (val - avg) ** 2); const avgSquaredDiff = squaredDiffs.reduce((acc, val) => acc + val, 0) / validValues.length; const stdDev = Math.sqrt(avgSquaredDiff); return { min, max, avg, median, start, end, change, stdDev }; } module.exports = { extractKeyMetrics, extractEventMetrics, extractEvents, extractTimeSeriesData, extractMetricsBetweenEvents, calculateStatistics, findMetricPathsByPrefix };