448 lines
12 KiB
JavaScript
448 lines
12 KiB
JavaScript
const MeasurementBuilder = require('./MeasurementBuilder');
|
|
const EventEmitter = require('events');
|
|
const convertModule = require('../convert/index');
|
|
|
|
class MeasurementContainer {
|
|
constructor(options = {},logger) {
|
|
this.emitter = new EventEmitter();
|
|
this.measurements = {};
|
|
this.windowSize = options.windowSize || 10; // Default window size
|
|
|
|
// For chaining context
|
|
this._currentType = null;
|
|
this._currentVariant = null;
|
|
this._currentPosition = null;
|
|
this._currentDistance = null;
|
|
this._unit = null;
|
|
|
|
// Default units for each measurement type
|
|
this.defaultUnits = {
|
|
pressure: 'mbar',
|
|
flow: 'm3/h',
|
|
power: 'kW',
|
|
temperature: 'C',
|
|
volume: 'm3',
|
|
length: 'm',
|
|
...options.defaultUnits // Allow override
|
|
};
|
|
|
|
// Auto-conversion settings
|
|
this.autoConvert = options.autoConvert !== false; // Default to true
|
|
this.preferredUnits = options.preferredUnits || {}; // Per-measurement overrides
|
|
|
|
// For chaining context
|
|
this._currentType = null;
|
|
this._currentVariant = null;
|
|
this._currentPosition = null;
|
|
this._unit = null;
|
|
|
|
// NEW: Enhanced child identification
|
|
this.childId = null;
|
|
this.childName = null;
|
|
this.parentRef = null;
|
|
|
|
}
|
|
|
|
// NEW: Methods to set child context
|
|
setChildId(childId) {
|
|
this.childId = childId;
|
|
return this;
|
|
}
|
|
|
|
setChildName(childName) {
|
|
this.childName = childName;
|
|
return this;
|
|
}
|
|
|
|
setParentRef(parent) {
|
|
this.parentRef = parent;
|
|
return this;
|
|
}
|
|
|
|
// New method to set preferred units
|
|
setPreferredUnit(measurementType, unit) {
|
|
this.preferredUnits[measurementType] = unit;
|
|
return this;
|
|
}
|
|
|
|
// Get the target unit for a measurement type
|
|
_getTargetUnit(measurementType) {
|
|
return this.preferredUnits[measurementType] ||
|
|
this.defaultUnits[measurementType] ||
|
|
null;
|
|
}
|
|
|
|
// Chainable methods
|
|
type(typeName) {
|
|
this._currentType = typeName;
|
|
this._currentVariant = null;
|
|
this._currentPosition = null;
|
|
return this;
|
|
}
|
|
|
|
variant(variantName) {
|
|
if (!this._currentType) {
|
|
throw new Error('Type must be specified before variant');
|
|
}
|
|
this._currentVariant = variantName;
|
|
this._currentPosition = null;
|
|
return this;
|
|
}
|
|
|
|
position(positionValue) {
|
|
if (!this._currentVariant) {
|
|
throw new Error('Variant must be specified before position');
|
|
}
|
|
|
|
this._currentPosition = positionValue;
|
|
|
|
return this;
|
|
}
|
|
|
|
distance(distance) {
|
|
// If distance is not provided, derive from positionVsParent
|
|
if(distance === null) {
|
|
distance = this._convertPositionStr2Num(this._currentPosition);
|
|
}
|
|
|
|
this._currentDistance = distance;
|
|
|
|
return this;
|
|
}
|
|
|
|
// ENHANCED: Update your existing value method
|
|
value(val, timestamp = Date.now(), sourceUnit = null) {
|
|
if (!this._ensureChainIsValid()) return this;
|
|
|
|
const measurement = this._getOrCreateMeasurement();
|
|
const targetUnit = this._getTargetUnit(this._currentType);
|
|
|
|
let convertedValue = val;
|
|
let finalUnit = sourceUnit || targetUnit;
|
|
|
|
// Auto-convert if enabled and units are specified
|
|
if (this.autoConvert && sourceUnit && targetUnit && sourceUnit !== targetUnit) {
|
|
try {
|
|
convertedValue = convertModule(val).from(sourceUnit).to(targetUnit);
|
|
finalUnit = targetUnit;
|
|
|
|
if (this.logger) {
|
|
this.logger.debug(`Auto-converted ${val} ${sourceUnit} to ${convertedValue} ${targetUnit}`);
|
|
}
|
|
} catch (error) {
|
|
if (this.logger) {
|
|
this.logger.warn(`Auto-conversion failed from ${sourceUnit} to ${targetUnit}: ${error.message}`);
|
|
}
|
|
convertedValue = val;
|
|
finalUnit = sourceUnit;
|
|
}
|
|
}
|
|
|
|
measurement.setValue(convertedValue, timestamp);
|
|
|
|
if (finalUnit && !measurement.unit) {
|
|
measurement.setUnit(finalUnit);
|
|
}
|
|
|
|
// ENHANCED: Emit event with rich context
|
|
const eventData = {
|
|
value: convertedValue,
|
|
originalValue: val,
|
|
unit: finalUnit,
|
|
sourceUnit: sourceUnit,
|
|
timestamp,
|
|
position: this._currentPosition,
|
|
distance: this._currentDistance,
|
|
variant: this._currentVariant,
|
|
type: this._currentType,
|
|
// NEW: Enhanced context
|
|
childId: this.childId,
|
|
childName: this.childName,
|
|
parentRef: this.parentRef,
|
|
};
|
|
|
|
// Emit the exact event your parent expects
|
|
this.emitter.emit(`${this._currentType}.${this._currentVariant}.${this._currentPosition}`, eventData);
|
|
console.log(`Emitted event: ${this._currentType}.${this._currentVariant}.${this._currentPosition}`, eventData);
|
|
|
|
return this;
|
|
}
|
|
|
|
|
|
unit(unitName) {
|
|
if (!this._ensureChainIsValid()) return this;
|
|
|
|
const measurement = this._getOrCreateMeasurement();
|
|
measurement.setUnit(unitName);
|
|
this._unit = unitName;
|
|
return this;
|
|
}
|
|
|
|
// Terminal operations - get data out
|
|
get() {
|
|
if (!this._ensureChainIsValid()) return null;
|
|
return this._getOrCreateMeasurement();
|
|
}
|
|
|
|
getCurrentValue(requestedUnit = null) {
|
|
const measurement = this.get();
|
|
if (!measurement) return null;
|
|
|
|
const value = measurement.getCurrentValue();
|
|
if (value === null) return null;
|
|
|
|
// Return as-is if no unit conversion requested
|
|
if (!requestedUnit) {
|
|
return value;
|
|
}
|
|
|
|
// Convert if needed
|
|
if (measurement.unit && requestedUnit !== measurement.unit) {
|
|
try {
|
|
return convertModule(value).from(measurement.unit).to(requestedUnit);
|
|
} catch (error) {
|
|
if (this.logger) {
|
|
this.logger.error(`Unit conversion failed: ${error.message}`);
|
|
}
|
|
return value; // Return original value if conversion fails
|
|
}
|
|
}
|
|
|
|
return value;
|
|
}
|
|
|
|
getAverage(requestedUnit = null) {
|
|
const measurement = this.get();
|
|
if (!measurement) return null;
|
|
|
|
const avgValue = measurement.getAverage();
|
|
if (avgValue === null) return null;
|
|
|
|
if (!requestedUnit || !measurement.unit || requestedUnit === measurement.unit) {
|
|
return avgValue;
|
|
}
|
|
|
|
try {
|
|
return convertModule(avgValue).from(measurement.unit).to(requestedUnit);
|
|
} catch (error) {
|
|
if (this.logger) {
|
|
this.logger.error(`Unit conversion failed: ${error.message}`);
|
|
}
|
|
return avgValue;
|
|
}
|
|
}
|
|
|
|
getMin() {
|
|
const measurement = this.get();
|
|
return measurement ? measurement.getMin() : null;
|
|
}
|
|
|
|
getMax() {
|
|
const measurement = this.get();
|
|
return measurement ? measurement.getMax() : null;
|
|
}
|
|
|
|
getAllValues() {
|
|
const measurement = this.get();
|
|
return measurement ? measurement.getAllValues() : null;
|
|
}
|
|
|
|
|
|
|
|
// Difference calculations between positions
|
|
difference(requestedUnit = null) {
|
|
|
|
if (!this._currentType || !this._currentVariant) {
|
|
throw new Error('Type and variant must be specified for difference calculation');
|
|
}
|
|
const upstream = this.measurements?.[this._currentType]?.[this._currentVariant]?.['upstream'] || null;
|
|
const downstream = this.measurements?.[this._currentType]?.[this._currentVariant]?.['downstream'] || null;
|
|
|
|
if (!upstream || !downstream || upstream.values.length === 0 || downstream.values.length === 0) {
|
|
return null;
|
|
}
|
|
|
|
// Get target unit for conversion
|
|
const targetUnit = requestedUnit || upstream.unit || downstream.unit;
|
|
|
|
// Get values in the same unit
|
|
const upstreamValue = this._convertValueToUnit(upstream.getCurrentValue(), upstream.unit, targetUnit);
|
|
const downstreamValue = this._convertValueToUnit(downstream.getCurrentValue(), downstream.unit, targetUnit);
|
|
|
|
const upstreamAvg = this._convertValueToUnit(upstream.getAverage(), upstream.unit, targetUnit);
|
|
const downstreamAvg = this._convertValueToUnit(downstream.getAverage(), downstream.unit, targetUnit);
|
|
|
|
return {
|
|
value: downstreamValue - upstreamValue,
|
|
avgDiff: downstreamAvg - upstreamAvg,
|
|
unit: targetUnit
|
|
};
|
|
}
|
|
|
|
// Helper methods
|
|
_ensureChainIsValid() {
|
|
if (!this._currentType || !this._currentVariant || !this._currentPosition) {
|
|
if (this.logger) {
|
|
this.logger.error('Incomplete measurement chain, required: type, variant, and position');
|
|
}
|
|
return false;
|
|
}
|
|
return true;
|
|
}
|
|
|
|
_getOrCreateMeasurement() {
|
|
// Initialize nested structure if needed
|
|
if (!this.measurements[this._currentType]) {
|
|
this.measurements[this._currentType] = {};
|
|
}
|
|
|
|
if (!this.measurements[this._currentType][this._currentVariant]) {
|
|
this.measurements[this._currentType][this._currentVariant] = {};
|
|
}
|
|
|
|
if (!this.measurements[this._currentType][this._currentVariant][this._currentPosition]) {
|
|
this.measurements[this._currentType][this._currentVariant][this._currentPosition] =
|
|
new MeasurementBuilder()
|
|
.setType(this._currentType)
|
|
.setVariant(this._currentVariant)
|
|
.setPosition(this._currentPosition)
|
|
.setWindowSize(this.windowSize)
|
|
.setDistance(this._currentDistance)
|
|
.build();
|
|
}
|
|
|
|
return this.measurements[this._currentType][this._currentVariant][this._currentPosition];
|
|
}
|
|
|
|
// Additional utility methods
|
|
getTypes() {
|
|
return Object.keys(this.measurements);
|
|
}
|
|
|
|
getVariants() {
|
|
if (!this._currentType) {
|
|
throw new Error('Type must be specified before listing variants');
|
|
}
|
|
return this.measurements[this._currentType] ?
|
|
Object.keys(this.measurements[this._currentType]) : [];
|
|
}
|
|
|
|
getPositions() {
|
|
if (!this._currentType || !this._currentVariant) {
|
|
throw new Error('Type and variant must be specified before listing positions');
|
|
}
|
|
|
|
if (!this.measurements[this._currentType] ||
|
|
!this.measurements[this._currentType][this._currentVariant]) {
|
|
return [];
|
|
}
|
|
|
|
return Object.keys(this.measurements[this._currentType][this._currentVariant]);
|
|
}
|
|
|
|
clear() {
|
|
this.measurements = {};
|
|
this._currentType = null;
|
|
this._currentVariant = null;
|
|
this._currentPosition = null;
|
|
}
|
|
|
|
// Helper method for value conversion
|
|
_convertValueToUnit(value, fromUnit, toUnit) {
|
|
if (!value || !fromUnit || !toUnit || fromUnit === toUnit) {
|
|
return value;
|
|
}
|
|
|
|
try {
|
|
return convertModule(value).from(fromUnit).to(toUnit);
|
|
} catch (error) {
|
|
if (this.logger) {
|
|
this.logger.warn(`Conversion failed from ${fromUnit} to ${toUnit}: ${error.message}`);
|
|
}
|
|
return value;
|
|
}
|
|
}
|
|
|
|
// Get available units for a measurement type
|
|
getAvailableUnits(measurementType = null) {
|
|
const type = measurementType || this._currentType;
|
|
if (!type) return [];
|
|
|
|
// Map measurement types to convert module measures
|
|
const measureMap = {
|
|
pressure: 'pressure',
|
|
flow: 'volumeFlowRate',
|
|
power: 'power',
|
|
temperature: 'temperature',
|
|
volume: 'volume',
|
|
length: 'length',
|
|
mass: 'mass',
|
|
energy: 'energy'
|
|
};
|
|
|
|
const convertMeasure = measureMap[type];
|
|
if (!convertMeasure) return [];
|
|
|
|
try {
|
|
return convertModule().possibilities(convertMeasure);
|
|
} catch (error) {
|
|
return [];
|
|
}
|
|
}
|
|
|
|
// Get best unit for current value
|
|
getBestUnit(excludeUnits = []) {
|
|
const measurement = this.get();
|
|
if (!measurement || !measurement.unit) return null;
|
|
|
|
const currentValue = measurement.getCurrentValue();
|
|
if (currentValue === null) return null;
|
|
|
|
try {
|
|
const best = convertModule(currentValue)
|
|
.from(measurement.unit)
|
|
.toBest({ exclude: excludeUnits });
|
|
|
|
return best;
|
|
} catch (error) {
|
|
if (this.logger) {
|
|
this.logger.error(`getBestUnit failed: ${error.message}`);
|
|
}
|
|
return null;
|
|
}
|
|
}
|
|
|
|
_convertPositionStr2Num(positionString) {
|
|
switch(positionString) {
|
|
case "atEquipment":
|
|
return 0;
|
|
case "upstream":
|
|
return Number.POSITIVE_INFINITY;
|
|
case "downstream":
|
|
return Number.NEGATIVE_INFINITY;
|
|
|
|
default:
|
|
if (this.logger) {
|
|
this.logger.error(`Invalid positionVsParent provided: ${positionString}`);
|
|
}
|
|
return;
|
|
}
|
|
}
|
|
|
|
_convertPositionNum2Str(positionValue) {
|
|
switch (positionValue) {
|
|
case 0:
|
|
return "atEquipment";
|
|
case (positionValue < 0):
|
|
return "upstream";
|
|
case (positionValue > 0):
|
|
return "downstream";
|
|
default:
|
|
console.log(`Invalid position provided: ${positionValue}`);
|
|
}
|
|
}
|
|
|
|
}
|
|
|
|
module.exports = MeasurementContainer;
|