UNPKG

@sailboat-computer/data-storage

Version:

Shared data storage library for sailboat computer v3

230 lines 8.16 kB
"use strict"; /** * Cycle-based downsampling strategy implementation */ Object.defineProperty(exports, "__esModule", { value: true }); exports.createCycleBasedDownsampler = exports.CycleBasedDownsampler = void 0; /** * Cycle-based downsampler implementation */ class CycleBasedDownsampler { constructor() { this.type = 'cycle-based'; } /** * Downsample data using cycle-based strategy * * @param data - Data to downsample * @param strategy - Cycle-based strategy * @returns Downsampled data */ async downsample(data, strategy) { if (data.length === 0) { return []; } try { // Sort data by timestamp const sortedData = [...data].sort((a, b) => { const aTime = new Date(a.metadata.timestamp).getTime(); const bTime = new Date(b.metadata.timestamp).getTime(); return aTime - bTime; }); // Identify cycles const cycles = this.identifyCycles(sortedData, strategy.cycleDetection); if (cycles.length === 0) { // No cycles found, return original data return sortedData; } // Downsample each cycle const downsampledData = []; for (const cycle of cycles) { const downsampledCycle = this.downsampleCycle(cycle, strategy.samplesPerCycle, strategy.preserveExtrema); downsampledData.push(...downsampledCycle); } // Add downsampling metadata for (const item of downsampledData) { item.metadata.tags = { ...item.metadata.tags, downsampled: 'true', downsampledFrom: data.length.toString(), downsamplingStrategy: 'cycle-based' }; } return downsampledData; } catch (error) { console.error('Failed to apply cycle-based downsampling:', error); // Return original data on error return data; } } /** * Identify cycles in data * * @param data - Data to identify cycles in * @param cycleDetection - Cycle detection configuration * @returns Identified cycles */ identifyCycles(data, cycleDetection) { const cycles = []; let currentCycle = []; let inCycle = false; for (const item of data) { if (!inCycle) { // Check if cycle starts if (this.matchesCondition(item, cycleDetection.startCondition)) { inCycle = true; currentCycle = [item]; } } else { // Add item to current cycle currentCycle.push(item); // Check if cycle ends if (this.matchesCondition(item, cycleDetection.endCondition)) { inCycle = false; cycles.push(currentCycle); currentCycle = []; } } } // Add last cycle if still in progress if (inCycle && currentCycle.length > 0) { cycles.push(currentCycle); } return cycles; } /** * Check if data matches condition * * @param data - Data to check * @param condition - Condition to match * @returns Whether data matches condition */ matchesCondition(data, condition) { const { field, operator, value } = condition; const fieldValue = data.data[field]; switch (operator) { case 'eq': return fieldValue === value; case 'neq': return fieldValue !== value; case 'gt': return fieldValue > value; case 'gte': return fieldValue >= value; case 'lt': return fieldValue < value; case 'lte': return fieldValue <= value; case 'in': return Array.isArray(value) && value.includes(fieldValue); case 'nin': return Array.isArray(value) && !value.includes(fieldValue); case 'contains': return typeof fieldValue === 'string' && fieldValue.includes(value); case 'regex': return typeof fieldValue === 'string' && new RegExp(value).test(fieldValue); default: return false; } } /** * Downsample a cycle * * @param cycle - Cycle to downsample * @param samplesPerCycle - Number of samples to keep per cycle * @param preserveExtrema - Whether to preserve extrema * @returns Downsampled cycle */ downsampleCycle(cycle, samplesPerCycle, preserveExtrema) { if (cycle.length <= samplesPerCycle) { // No downsampling needed return cycle; } // Always include first and last points const result = [cycle[0]]; // Add extrema if requested const extrema = []; if (preserveExtrema) { // Find extrema for each numeric field const numericFields = this.getNumericFields(cycle); for (const field of numericFields) { // Find min and max let minValue = Infinity; let maxValue = -Infinity; let minIndex = -1; let maxIndex = -1; for (let i = 0; i < cycle.length; i++) { const value = cycle[i].data[field]; if (value < minValue) { minValue = value; minIndex = i; } if (value > maxValue) { maxValue = value; maxIndex = i; } } // Add min and max to extrema if (minIndex > 0 && minIndex < cycle.length - 1) { extrema.push(cycle[minIndex]); } if (maxIndex > 0 && maxIndex < cycle.length - 1 && maxIndex !== minIndex) { extrema.push(cycle[maxIndex]); } } } // Calculate number of regular samples to include const remainingSamples = samplesPerCycle - result.length - extrema.length - 1; // -1 for last point if (remainingSamples > 0) { // Calculate step size const step = (cycle.length - 2) / (remainingSamples + 1); // Add regular samples for (let i = 1; i <= remainingSamples; i++) { const index = Math.round(i * step) + 1; if (index > 0 && index < cycle.length - 1) { result.push(cycle[index]); } } } // Add extrema result.push(...extrema); // Add last point result.push(cycle[cycle.length - 1]); // Sort by timestamp result.sort((a, b) => { const aTime = new Date(a.metadata.timestamp).getTime(); const bTime = new Date(b.metadata.timestamp).getTime(); return aTime - bTime; }); return result; } /** * Get numeric fields from data * * @param data - Data to get numeric fields from * @returns Numeric fields */ getNumericFields(data) { if (data.length === 0) { return []; } const fields = Object.keys(data[0].data); return fields.filter(field => { // Check if field is numeric in all data points return data.every(item => typeof item.data[field] === 'number'); }); } } exports.CycleBasedDownsampler = CycleBasedDownsampler; /** * Create a new cycle-based downsampler * * @returns Cycle-based downsampler */ function createCycleBasedDownsampler() { return new CycleBasedDownsampler(); } exports.createCycleBasedDownsampler = createCycleBasedDownsampler; //# sourceMappingURL=cycle-based.js.map