@sailboat-computer/data-storage
Version:
Shared data storage library for sailboat computer v3
230 lines • 8.16 kB
JavaScript
;
/**
* 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