P2 wave 1: extract concerns from pumpingStation specificClass
Splits pumpingStation/src/ into focused concern modules. specificClass.js
will be slimmed to an orchestrator in P2.9 (integration); for now both
the inlined logic AND the new modules coexist so tests stay green
throughout.
src/basin/ BasinGeometry + thresholdValidator (pure)
src/measurement/ flowAggregator + measurementRouter + calibration
src/control/ levelBased + flowBased(stub) + manual + index dispatcher
src/safety/ safetyController split into dryRun + overfill rules
src/commands/ registry array + handlers (canonical names from start)
src/editor.js 260 lines of SVG basin-diagram redraw, was inline in .html
examples/standalone-demo.js was if(require.main===module) at bottom of specificClass.js
CONTRACT.md canonical inputs + outputs + emitted events
Modified:
src/specificClass.js removed the 170-line standalone demo block
pumpingStation.html oneditprepare/oneditsave delegate to editor.{init,save}
pumpingStation.js added admin endpoint serving src/editor.js
102 basic tests pass (60 new + 42 existing).
specificClass.js itself is unchanged in behaviour — integration is P2.9.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
91
src/basin/BasinGeometry.js
Normal file
91
src/basin/BasinGeometry.js
Normal file
@@ -0,0 +1,91 @@
|
||||
// Basin geometry for a wet-well pumping station.
|
||||
//
|
||||
// Models the basin as a rectangular prism (constant cross-section), so
|
||||
// volume = level × surfaceArea. Owns the level↔volume conversions and the
|
||||
// derived threshold volumes used by control + safety. Pure domain — no
|
||||
// Node-RED, no logger, no side effects beyond construction.
|
||||
|
||||
class BasinGeometry {
|
||||
/**
|
||||
* @param {object} basinConfig - { volume, height, inflowLevel, outflowLevel, overflowLevel }
|
||||
* @param {object} hydraulicsConfig - { minHeightBasedOn: 'inlet' | 'outlet' }
|
||||
*/
|
||||
constructor(basinConfig, hydraulicsConfig) {
|
||||
const volEmptyBasin = basinConfig.volume;
|
||||
const heightBasin = basinConfig.height;
|
||||
const inflowLevel = basinConfig.inflowLevel;
|
||||
const outflowLevel = basinConfig.outflowLevel;
|
||||
const overflowLevel = basinConfig.overflowLevel;
|
||||
const minHeightBasedOn = hydraulicsConfig?.minHeightBasedOn;
|
||||
|
||||
const surfaceArea = volEmptyBasin / heightBasin;
|
||||
|
||||
// maxVol ≡ volEmptyBasin under the constant cross-section assumption;
|
||||
// kept as a separate field for naming symmetry with the trigger volumes.
|
||||
const maxVol = heightBasin * surfaceArea;
|
||||
const maxVolAtOverflow = overflowLevel * surfaceArea;
|
||||
const minVolAtOutflow = outflowLevel * surfaceArea;
|
||||
const minVolAtInflow = inflowLevel * surfaceArea;
|
||||
const minVol = minHeightBasedOn === 'inlet' ? minVolAtInflow : minVolAtOutflow;
|
||||
|
||||
this._volEmptyBasin = volEmptyBasin;
|
||||
this._heightBasin = heightBasin;
|
||||
this._inflowLevel = inflowLevel;
|
||||
this._outflowLevel = outflowLevel;
|
||||
this._overflowLevel = overflowLevel;
|
||||
this._surfaceArea = surfaceArea;
|
||||
this._maxVol = maxVol;
|
||||
this._maxVolAtOverflow = maxVolAtOverflow;
|
||||
this._minVolAtInflow = minVolAtInflow;
|
||||
this._minVolAtOutflow = minVolAtOutflow;
|
||||
this._minVol = minVol;
|
||||
this._minHeightBasedOn = minHeightBasedOn;
|
||||
}
|
||||
|
||||
get volEmptyBasin() { return this._volEmptyBasin; }
|
||||
get heightBasin() { return this._heightBasin; }
|
||||
get inflowLevel() { return this._inflowLevel; }
|
||||
get outflowLevel() { return this._outflowLevel; }
|
||||
get overflowLevel() { return this._overflowLevel; }
|
||||
get surfaceArea() { return this._surfaceArea; }
|
||||
get maxVol() { return this._maxVol; }
|
||||
get maxVolAtOverflow() { return this._maxVolAtOverflow; }
|
||||
get minVolAtInflow() { return this._minVolAtInflow; }
|
||||
get minVolAtOutflow() { return this._minVolAtOutflow; }
|
||||
get minVol() { return this._minVol; }
|
||||
get minHeightBasedOn() { return this._minHeightBasedOn; }
|
||||
|
||||
/** Convert level (m from floor) → volume (m3). Negative levels clamp to 0. */
|
||||
volumeFromLevel(level) {
|
||||
return Math.max(level, 0) * this._surfaceArea;
|
||||
}
|
||||
|
||||
/** Convert volume (m3) → level (m from floor). Negative volumes clamp to 0. */
|
||||
levelFromVolume(volume) {
|
||||
return Math.max(volume, 0) / this._surfaceArea;
|
||||
}
|
||||
|
||||
/**
|
||||
* Plain-object snapshot mirroring the legacy `this.basin` shape so
|
||||
* getOutput / status code can keep using the same field names without
|
||||
* caring whether it's holding a class instance or a plain object.
|
||||
*/
|
||||
snapshot() {
|
||||
return {
|
||||
volEmptyBasin: this._volEmptyBasin,
|
||||
heightBasin: this._heightBasin,
|
||||
inflowLevel: this._inflowLevel,
|
||||
outflowLevel: this._outflowLevel,
|
||||
overflowLevel: this._overflowLevel,
|
||||
surfaceArea: this._surfaceArea,
|
||||
maxVol: this._maxVol,
|
||||
maxVolAtOverflow: this._maxVolAtOverflow,
|
||||
minVolAtInflow: this._minVolAtInflow,
|
||||
minVolAtOutflow: this._minVolAtOutflow,
|
||||
minVol: this._minVol,
|
||||
minHeightBasedOn: this._minHeightBasedOn,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = BasinGeometry;
|
||||
Reference in New Issue
Block a user