@sailboat-computer/data-storage
Version:
Shared data storage library for sailboat computer v3
262 lines (220 loc) • 7.91 kB
text/typescript
/**
* 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();
}