1- // Other imports and functions remain unchanged...
1+ import { calculateVolume , convertVolume } from './index' ;
2+ import type { VesselConfig , StrappingOptions , StrappingEntry , FillRateInput , FillRateResult , AlarmConfig , AlarmResult , TankReading , InventoryEntry , InventoryResult , Unit } from './types' ;
23
4+ /**
5+ * Generates a strapping table (also known as a calibration chart) for a given vessel.
6+ * This table maps liquid height to volume and percentage fill.
7+ *
8+ * @param vessel The configuration of the vessel.
9+ * @param options Options for generating the table, including number of steps, unit, and precision.
10+ * @returns An array of StrappingEntry objects, each containing height, volume, and percentage.
11+ * @throws {Error } If `steps` is out of the valid range (1-10000).
12+ */
313export function strappingTable ( vessel : VesselConfig , options ?: StrappingOptions ) : readonly StrappingEntry [ ] {
414 const steps = options ?. steps ?? 20 ;
515 const unit = options ?. unit ?? 'cubicMeters' ;
616 const precision = options ?. precision ?? 4 ;
717
818 if ( steps < 1 || steps > 10000 ) {
9- throw new Error ( ' Steps must be between 1 and 10000' ) ;
19+ throw new Error ( `Invalid steps value: ${ steps } . Steps must be between 1 and 10000.` ) ;
1020 }
1121
12- // Rest of function remains unchanged...
22+ const table : StrappingEntry [ ] = [ ] ;
23+ const totalHeight = vessel . dimensions . height ;
24+
25+ // Calculate total volume once for percentage calculation
26+ let totalVolume : number ;
27+ try {
28+ totalVolume = calculateVolume ( vessel , totalHeight ) . volume ;
29+ } catch ( error : any ) {
30+ throw new Error ( `Failed to calculate total volume for strapping table: ${ error . message } ` ) ;
31+ }
32+
33+ for ( let i = 0 ; i <= steps ; i ++ ) {
34+ const height = ( i / steps ) * totalHeight ;
35+ let volumeResult ;
36+ try {
37+ volumeResult = calculateVolume ( vessel , height ) ;
38+ } catch ( error : any ) {
39+ // Log the error but try to continue if possible, or rethrow if critical
40+ console . warn ( `Warning: Failed to calculate volume for height ${ height } in strapping table. Error: ${ error . message } ` ) ;
41+ volumeResult = { volume : 0 , percentage : 0 } ; // Fallback
42+ }
43+
44+ const volumeInTargetUnit = convertVolume ( volumeResult . volume , 'cubicMeters' , unit ) ;
45+ const percentage = totalVolume > 0 ? ( volumeResult . volume / totalVolume ) * 100 : 0 ;
46+
47+ table . push ( {
48+ height : parseFloat ( height . toFixed ( precision ) ) ,
49+ volume : parseFloat ( volumeInTargetUnit . toFixed ( precision ) ) ,
50+ percentage : parseFloat ( percentage . toFixed ( precision ) ) ,
51+ } ) ;
52+ }
53+
54+ return table ;
55+ }
56+
57+ /**
58+ * Calculates the fill or drain rate of a vessel based on two height readings over time.
59+ *
60+ * @param input The FillRateInput object containing vessel config, heights, and time.
61+ * @returns A FillRateResult object with rates, direction, and time to full/empty.
62+ * @throws {Error } If `timeMinutes` is not positive.
63+ * @throws {Error } If vessel total volume calculation fails.
64+ */
65+ export function fillRate ( input : FillRateInput ) : FillRateResult {
66+ const { vessel, heightBefore, heightAfter, timeMinutes } = input ;
67+
68+ if ( timeMinutes <= 0 ) {
69+ throw new Error ( `Invalid timeMinutes: ${ timeMinutes } . Must be a positive number.` ) ;
70+ }
71+
72+ const totalHeight = vessel . dimensions . height ;
73+ let totalVolume : number ;
74+ try {
75+ totalVolume = calculateVolume ( vessel , totalHeight ) . volume ;
76+ } catch ( error : any ) {
77+ throw new Error ( `Failed to calculate total volume for fill rate: ${ error . message } ` ) ;
78+ }
79+
80+ if ( totalVolume === 0 ) {
81+ console . warn ( 'Warning: Vessel has zero total volume, fill rate calculations may be inaccurate.' ) ;
82+ return {
83+ ratePerMinute : 0 ,
84+ ratePerHour : 0 ,
85+ direction : 'stable' ,
86+ volumeChange : 0 ,
87+ minutesToFull : null ,
88+ minutesToEmpty : null ,
89+ percentBefore : 0 ,
90+ percentAfter : 0 ,
91+ } ;
92+ }
93+
94+ let volumeBefore : number , volumeAfter : number ;
95+ try {
96+ volumeBefore = calculateVolume ( vessel , heightBefore ) . volume ;
97+ volumeAfter = calculateVolume ( vessel , heightAfter ) . volume ;
98+ } catch ( error : any ) {
99+ throw new Error ( `Failed to calculate volume for fill rate at height ${ heightBefore } or ${ heightAfter } : ${ error . message } ` ) ;
100+ }
101+
102+ const volumeChange = volumeAfter - volumeBefore ;
103+ const ratePerMinute = volumeChange / timeMinutes ;
104+ const ratePerHour = ratePerMinute * 60 ;
105+
106+ let direction : FillRateResult [ 'direction' ] = 'stable' ;
107+ if ( volumeChange > 0 ) {
108+ direction = 'filling' ;
109+ } else if ( volumeChange < 0 ) {
110+ direction = 'draining' ;
111+ }
112+
113+ const remainingVolume = totalVolume - volumeAfter ;
114+ const minutesToFull = ratePerMinute > 0 ? remainingVolume / ratePerMinute : null ;
115+
116+ const currentVolume = volumeAfter ;
117+ const minutesToEmpty = ratePerMinute < 0 ? currentVolume / Math . abs ( ratePerMinute ) : null ;
118+
119+ const percentBefore = ( volumeBefore / totalVolume ) * 100 ;
120+ const percentAfter = ( volumeAfter / totalVolume ) * 100 ;
121+
122+ return {
123+ ratePerMinute,
124+ ratePerHour,
125+ direction,
126+ volumeChange,
127+ minutesToFull : minutesToFull !== null && minutesToFull >= 0 ? minutesToFull : null ,
128+ minutesToEmpty : minutesToEmpty !== null && minutesToEmpty >= 0 ? minutesToEmpty : null ,
129+ percentBefore,
130+ percentAfter,
131+ } ;
132+ }
133+
134+ /**
135+ * Checks the current liquid level against predefined alarm thresholds.
136+ *
137+ * @param vessel The vessel configuration.
138+ * @param liquidHeight The current liquid height in the vessel.
139+ * @param alarmConfig The alarm thresholds (high-high, high, low, low-low) as percentages.
140+ * @returns An AlarmResult object indicating the current alarm status.
141+ * @throws {Error } If `liquidHeight` is negative or exceeds vessel height.
142+ * @throws {Error } If vessel total volume calculation fails.
143+ */
144+ export function tankAlarms ( vessel : VesselConfig , liquidHeight : number , alarmConfig : AlarmConfig ) : AlarmResult {
145+ const totalHeight = vessel . dimensions . height ;
146+
147+ if ( liquidHeight < 0 ) {
148+ throw new Error ( `Liquid height cannot be negative: ${ liquidHeight } .` ) ;
149+ }
150+ if ( liquidHeight > totalHeight ) {
151+ console . warn ( `Warning: Liquid height (${ liquidHeight } ) exceeds vessel total height (${ totalHeight } ).` ) ;
152+ }
153+
154+ let currentVolumeResult ;
155+ try {
156+ currentVolumeResult = calculateVolume ( vessel , liquidHeight ) ;
157+ } catch ( error : any ) {
158+ throw new Error ( `Failed to calculate volume for alarm check at height ${ liquidHeight } : ${ error . message } ` ) ;
159+ }
160+
161+ const percentage = currentVolumeResult . percentage ;
162+ const activeAlarms : AlarmStatus [ ] = [ ] ;
163+ let status : AlarmStatus = 'normal' ;
164+
165+ if ( alarmConfig . highHigh !== undefined && percentage >= alarmConfig . highHigh ) {
166+ activeAlarms . push ( 'high-high' ) ;
167+ status = 'high-high' ;
168+ } else if ( alarmConfig . high !== undefined && percentage >= alarmConfig . high ) {
169+ activeAlarms . push ( 'high' ) ;
170+ status = 'high' ;
171+ } else if ( alarmConfig . lowLow !== undefined && percentage <= alarmConfig . lowLow ) {
172+ activeAlarms . push ( 'low-low' ) ;
173+ status = 'low-low' ;
174+ } else if ( alarmConfig . low !== undefined && percentage <= alarmConfig . low ) {
175+ activeAlarms . push ( 'low' ) ;
176+ status = 'low' ;
177+ }
178+
179+ return {
180+ status,
181+ percentage : parseFloat ( percentage . toFixed ( 2 ) ) ,
182+ activeAlarms,
183+ isAlarmed : activeAlarms . length > 0 ,
184+ } ;
13185}
14186
15- // Rest of file remains unchanged...
187+ /**
188+ * Calculates the total inventory and average fill percentage for multiple tanks.
189+ *
190+ * @param readings An array of TankReading objects, each representing a tank's current state.
191+ * @param targetUnit The unit to convert all volumes to for the total and individual entries.
192+ * @param precision The number of decimal places for volume and percentage.
193+ * @returns An InventoryResult object with total volume, average percentage, and individual tank entries.
194+ * @throws {Error } If any tank reading fails volume calculation.
195+ */
196+ export function tankInventory ( readings : readonly TankReading [ ] , targetUnit : Unit = 'cubicMeters' , precision : number = 4 ) : InventoryResult {
197+ let totalVolume = 0 ;
198+ let totalPercentage = 0 ;
199+ const tanks : InventoryEntry [ ] = [ ] ;
200+
201+ if ( readings . length === 0 ) {
202+ return {
203+ tanks : [ ] ,
204+ totalVolume : 0 ,
205+ averagePercentage : 0 ,
206+ count : 0 ,
207+ unit : targetUnit ,
208+ } ;
209+ }
210+
211+ for ( const reading of readings ) {
212+ const { name, vessel, liquidHeight, unit : inputUnit = 'cubicMeters' } = reading ;
213+
214+ let volumeResult ;
215+ try {
216+ volumeResult = calculateVolume ( vessel , liquidHeight ) ;
217+ } catch ( error : any ) {
218+ throw new Error ( `Failed to calculate volume for tank '${ name } ' at height ${ liquidHeight } : ${ error . message } ` ) ;
219+ }
220+
221+ const volumeInTargetUnit = convertVolume ( volumeResult . volume , 'cubicMeters' , targetUnit ) ;
222+
223+ tanks . push ( {
224+ name,
225+ volume : parseFloat ( volumeInTargetUnit . toFixed ( precision ) ) ,
226+ percentage : parseFloat ( volumeResult . percentage . toFixed ( precision ) ) ,
227+ unit : targetUnit ,
228+ } ) ;
229+
230+ totalVolume += volumeInTargetUnit ;
231+ totalPercentage += volumeResult . percentage ;
232+ }
233+
234+ const averagePercentage = totalPercentage / readings . length ;
235+
236+ return {
237+ tanks,
238+ totalVolume : parseFloat ( totalVolume . toFixed ( precision ) ) ,
239+ averagePercentage : parseFloat ( averagePercentage . toFixed ( precision ) ) ,
240+ count : readings . length ,
241+ unit : targetUnit ,
242+ } ;
243+ }
0 commit comments