@@ -169,12 +169,20 @@ enum Calculator {
169169
170170 // OVER_MAX_GROSS or NEAR_MAX_GROSS
171171 if !isWithinWeightLimit {
172+ let excess = totalWeight - maxGross
173+ let remediation = buildWeightRemediation (
174+ excess: excess,
175+ stationDetails: stationDetails,
176+ fuelDetails: fuelDetails,
177+ fuelTanks: aircraft. fuelTanks
178+ )
172179 warnings. append ( CalculationWarning (
173180 level: . danger,
174181 code: . overMaxGross,
175182 message: " Aircraft exceeds maximum takeoff weight " ,
176- detail: " \( formatDecimal ( totalWeight, decimals: 1 ) ) lbs exceeds limit of \( formatWithComma ( maxGross) ) lbs by \( formatDecimal ( totalWeight - maxGross, decimals: 1 ) ) lbs " ,
177- regulatoryRef: " FAR 91.103 "
183+ detail: " \( formatDecimal ( totalWeight, decimals: 1 ) ) lbs exceeds limit of \( formatWithComma ( maxGross) ) lbs by \( formatDecimal ( excess, decimals: 1 ) ) lbs " ,
184+ regulatoryRef: " FAR 91.103 " ,
185+ remediation: remediation
178186 ) )
179187 } else if weightMargin < maxGross * 0.05 {
180188 warnings. append ( CalculationWarning (
@@ -264,6 +272,134 @@ enum Calculator {
264272 )
265273 }
266274
275+ // MARK: - Landing Calculation
276+
277+ /// Calculate landing weight and CG after fuel burn.
278+ ///
279+ /// Distributes the fuel burn proportionally across all tanks based on
280+ /// their current load. Returns nil if no fuel burn is specified.
281+ ///
282+ /// - Parameters:
283+ /// - takeoffResult: The takeoff calculation result.
284+ /// - aircraft: The aircraft definition.
285+ /// - fuelLoads: Current fuel loads in each tank.
286+ /// - fuelBurnGallons: Total gallons expected to burn during flight.
287+ /// - Returns: A `LandingResult` with landing weight, CG, and any warnings.
288+ static func calculateLanding(
289+ takeoffResult: CalculationResult ,
290+ aircraft: Aircraft ,
291+ fuelLoads: [ FuelLoad ] ,
292+ fuelBurnGallons: Double
293+ ) -> LandingResult ? {
294+ guard fuelBurnGallons > 0 else { return nil }
295+
296+ let totalFuelGallons = fuelLoads. reduce ( 0.0 ) { $0 + max( 0 , $1. gallons) }
297+ guard totalFuelGallons > 0 else { return nil }
298+
299+ // Distribute burn proportionally across tanks
300+ var burnedWeight = 0.0
301+ var burnedMoment = 0.0
302+ var warnings : [ CalculationWarning ] = [ ]
303+
304+ let actualBurn = min ( fuelBurnGallons, totalFuelGallons)
305+ if fuelBurnGallons > totalFuelGallons {
306+ warnings. append ( CalculationWarning (
307+ level: . danger,
308+ code: . negativeFuel,
309+ message: " Fuel burn exceeds loaded fuel " ,
310+ detail: " Planned burn of \( formatDecimal ( fuelBurnGallons, decimals: 0 ) ) gal exceeds \( formatDecimal ( totalFuelGallons, decimals: 0 ) ) gal loaded " ,
311+ regulatoryRef: nil
312+ ) )
313+ }
314+
315+ for load in fuelLoads {
316+ guard let tank = aircraft. fuelTanks. first ( where: { $0. id == load. tankId } ) else { continue }
317+ let proportion = load. gallons / totalFuelGallons
318+ let tankBurnGallons = actualBurn * proportion
319+ let tankBurnWeight = tankBurnGallons * tank. fuelWeightPerGallon
320+ burnedWeight += tankBurnWeight
321+ burnedMoment += tankBurnWeight * tank. arm. value
322+ }
323+
324+ let landingWeight = takeoffResult. totalWeight - burnedWeight
325+ let landingMoment = takeoffResult. totalMoment - burnedMoment
326+ let landingCG = landingWeight > 0 ? landingMoment / landingWeight : 0
327+
328+ let isWithinEnvelope = Envelope . isPointInEnvelope (
329+ weight: landingWeight,
330+ cg: landingCG,
331+ envelope: aircraft. cgEnvelope
332+ )
333+ let isWithinWeightLimit = landingWeight <= aircraft. maxGrossWeight. value
334+
335+ // Check landing weight limit if aircraft has one
336+ if let maxLanding = aircraft. maxLandingWeight {
337+ if landingWeight > maxLanding. value {
338+ warnings. append ( CalculationWarning (
339+ level: . warning,
340+ code: . overMaxLanding,
341+ message: " Landing weight exceeds max landing weight " ,
342+ detail: " \( formatDecimal ( landingWeight, decimals: 0 ) ) lbs exceeds landing limit of \( formatWithComma ( maxLanding. value) ) lbs " ,
343+ regulatoryRef: nil
344+ ) )
345+ }
346+ }
347+
348+ if !isWithinEnvelope {
349+ warnings. append ( CalculationWarning (
350+ level: . danger,
351+ code: . cgOutOfEnvelope,
352+ message: " Landing CG outside approved envelope " ,
353+ detail: " CG shifts to \( formatDecimal ( landingCG, decimals: 2 ) ) in after \( formatDecimal ( actualBurn, decimals: 0 ) ) gal fuel burn " ,
354+ regulatoryRef: " FAR 91.103 "
355+ ) )
356+ }
357+
358+ return LandingResult (
359+ landingWeight: landingWeight,
360+ landingCG: landingCG,
361+ fuelBurnGallons: actualBurn,
362+ fuelBurnWeight: burnedWeight,
363+ isWithinCGEnvelope: isWithinEnvelope,
364+ isWithinWeightLimit: isWithinWeightLimit,
365+ warnings: warnings
366+ )
367+ }
368+
369+ // MARK: - Remediation Helpers
370+
371+ /// Build actionable remediation text for an overweight condition.
372+ /// Suggests removing fuel or reducing the heaviest loaded station.
373+ private static func buildWeightRemediation(
374+ excess: Double ,
375+ stationDetails: [ StationDetail ] ,
376+ fuelDetails: [ FuelDetail ] ,
377+ fuelTanks: [ FuelTank ]
378+ ) -> String {
379+ var suggestions : [ String ] = [ ]
380+
381+ // Suggest fuel reduction for each tank that has fuel
382+ for detail in fuelDetails where detail. weight > 0 {
383+ if let tank = fuelTanks. first ( where: { $0. id == detail. tankId } ) {
384+ let gallonsToRemove = ceil ( excess / tank. fuelWeightPerGallon)
385+ if gallonsToRemove <= detail. gallons {
386+ suggestions. append ( " Remove \( formatDecimal ( gallonsToRemove, decimals: 0 ) ) gal from \( detail. name) " )
387+ }
388+ }
389+ }
390+
391+ // Suggest reducing the heaviest station
392+ if let heaviest = stationDetails. filter ( { $0. weight > 0 } ) . max ( by: { $0. weight < $1. weight } ) ,
393+ heaviest. weight >= excess {
394+ suggestions. append ( " Reduce \( heaviest. name) by \( formatDecimal ( excess, decimals: 0 ) ) lbs " )
395+ }
396+
397+ if suggestions. isEmpty {
398+ return " Reduce total load by \( formatDecimal ( excess, decimals: 0 ) ) lbs "
399+ }
400+ return suggestions. prefix ( 2 ) . joined ( separator: " OR " )
401+ }
402+
267403 // MARK: - Formatting Helpers
268404
269405 /// Format a number with a fixed number of decimal places.
0 commit comments