UNPKG

@sailboat-computer/data-storage

Version:

Shared data storage library for sailboat computer v3

262 lines (220 loc) 7.91 kB
/** * Maintenance-based downsampling strategy implementation */ import { Downsampler, StoredData, MaintenanceBasedStrategy } from '../../types'; /** * Maintenance-based downsampler implementation */ export class MaintenanceBasedDownsampler implements Downsampler { readonly type = 'maintenance-based'; /** * Downsample data using maintenance-based strategy * * @param data - Data to downsample * @param strategy - Maintenance-based strategy * @returns Downsampled data */ async downsample(data: StoredData[], strategy: MaintenanceBasedStrategy): Promise<StoredData[]> { 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; }); // Find maintenance events const maintenanceEvents = this.findMaintenanceEvents(sortedData, strategy.maintenanceEventType); if (maintenanceEvents.length === 0) { // No maintenance events found, apply normal downsampling return this.applyNormalDownsampling(sortedData, strategy.normalSamplingInterval); } // Process data around maintenance events const result: StoredData[] = []; let lastProcessedIndex = -1; for (const eventIndex of maintenanceEvents) { // Process data before the event (if not already processed) if (eventIndex > lastProcessedIndex + 1) { const beforeData = sortedData.slice(lastProcessedIndex + 1, eventIndex - strategy.samplesBeforeEvent); const downsampledBeforeData = this.applyNormalDownsampling(beforeData, strategy.normalSamplingInterval); result.push(...downsampledBeforeData); } // Add detailed data around the event const startIndex = Math.max(0, eventIndex - strategy.samplesBeforeEvent); const endIndex = Math.min(sortedData.length - 1, eventIndex + strategy.samplesAfterEvent); const detailedData = sortedData.slice(startIndex, endIndex + 1); // Apply detailed downsampling if there are too many points if (detailedData.length > strategy.samplesBeforeEvent + strategy.samplesAfterEvent + 1) { const downsampledDetailedData = this.applyDetailedDownsampling( detailedData, strategy.detailedSamplingInterval ); result.push(...downsampledDetailedData); } else { result.push(...detailedData); } lastProcessedIndex = endIndex; } // Process remaining data after the last event if (lastProcessedIndex < sortedData.length - 1) { const afterData = sortedData.slice(lastProcessedIndex + 1); const downsampledAfterData = this.applyNormalDownsampling(afterData, strategy.normalSamplingInterval); result.push(...downsampledAfterData); } // Add downsampling metadata for (const item of result) { item.metadata.tags = { ...item.metadata.tags, downsampled: 'true', downsampledFrom: data.length.toString(), downsamplingStrategy: 'maintenance-based' }; } return result; } catch (error) { console.error('Failed to apply maintenance-based downsampling:', error); // Return original data on error return data; } } /** * Find maintenance events in data * * @param data - Data to find maintenance events in * @param eventType - Maintenance event type * @returns Indices of maintenance events */ private findMaintenanceEvents(data: StoredData[], eventType: string): number[] { const events: number[] = []; for (let i = 0; i < data.length; i++) { const item = data[i]; // Check if item is a maintenance event if (this.isMaintenanceEvent(item, eventType)) { events.push(i); } } return events; } /** * Check if data is a maintenance event * * @param data - Data to check * @param eventType - Maintenance event type * @returns Whether data is a maintenance event */ private isMaintenanceEvent(data: StoredData, eventType: string): boolean { // Check if data has a maintenance event tag if (data.metadata.tags && data.metadata.tags.eventType === eventType) { return true; } // Check if data has a maintenance event type if (data.data.eventType === eventType) { return true; } // Check if data has a maintenance event flag if (data.data.maintenanceEvent === true && data.data.type === eventType) { return true; } // Check if data has a maintenance event in a nested object if (data.data.event && typeof data.data.event === 'object') { const event = data.data.event; if (event.type === eventType || event.eventType === eventType) { return true; } } return false; } /** * Apply normal downsampling to data * * @param data - Data to downsample * @param interval - Sampling interval * @returns Downsampled data */ private applyNormalDownsampling(data: StoredData[], interval: string): StoredData[] { if (data.length === 0) { return []; } // Parse interval const intervalMs = this.parseInterval(interval); if (intervalMs <= 0) { return data; } // Always include first and last points const result: StoredData[] = []; if (data.length > 0) { result.push(data[0]); } // Apply interval-based sampling if (data.length > 2) { const firstTime = new Date(data[0].metadata.timestamp).getTime(); const lastTime = new Date(data[data.length - 1].metadata.timestamp).getTime(); // Calculate number of intervals const duration = lastTime - firstTime; const intervals = Math.floor(duration / intervalMs); if (intervals > 0) { // Calculate step size const step = data.length / (intervals + 1); // Add samples at interval boundaries for (let i = 1; i <= intervals; i++) { const index = Math.round(i * step); if (index > 0 && index < data.length - 1) { result.push(data[index]); } } } } if (data.length > 1) { result.push(data[data.length - 1]); } return result; } /** * Apply detailed downsampling to data * * @param data - Data to downsample * @param interval - Sampling interval * @returns Downsampled data */ private applyDetailedDownsampling(data: StoredData[], interval: string): StoredData[] { // Use a smaller interval for detailed downsampling return this.applyNormalDownsampling(data, interval); } /** * Parse interval string to milliseconds * * @param interval - Interval string (e.g., '1m', '5m', '1h') * @returns Interval in milliseconds */ private parseInterval(interval: string): number { const match = interval.match(/^(\d+)([smhd])$/); if (!match) { console.warn(`Invalid interval format: ${interval}`); return 0; } const value = parseInt(match[1]); const unit = match[2]; switch (unit) { case 's': return value * 1000; case 'm': return value * 60 * 1000; case 'h': return value * 60 * 60 * 1000; case 'd': return value * 24 * 60 * 60 * 1000; default: return 0; } } } /** * Create a new maintenance-based downsampler * * @returns Maintenance-based downsampler */ export function createMaintenanceBasedDownsampler(): Downsampler { return new MaintenanceBasedDownsampler(); }