bowling-analysis-system
Version:
A comprehensive system for analyzing bowling techniques using video processing and metrics calculation
308 lines (260 loc) • 8.72 kB
JavaScript
/**
* @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
};