import { Injectable } from '@nestjs/common'; import { InjectModel } from '@nestjs/mongoose'; import { Model } from 'mongoose'; import { WeatherDocument } from './models/weather.model'; const DistanceSampling = 5; const AltitudeSampling = 1000; const DensityModel: { altitude: number; density: number }[] = [ { altitude: 50000, density: 0.18648 }, { altitude: 45000, density: 0.23714 }, { altitude: 40000, density: 0.24617 }, { altitude: 38000, density: 0.33199 }, { altitude: 36000, density: 0.36518 }, { altitude: 34000, density: 0.39444 }, { altitude: 32000, density: 0.42546 }, { altitude: 30000, density: 0.45831 }, { altitude: 28000, density: 0.402506 }, { altitude: 26000, density: 0.432497 }, { altitude: 24000, density: 0.464169 }, { altitude: 22000, density: 0.60954 }, { altitude: 20000, density: 0.65269 }, { altitude: 18000, density: 0.69815 }, { altitude: 16000, density: 0.74598 }, { altitude: 15000, density: 0.77082 }, { altitude: 14000, density: 0.79628 }, { altitude: 13000, density: 0.82238 }, { altitude: 12000, density: 0.84914 }, { altitude: 11000, density: 0.87655 }, { altitude: 10000, density: 0.90464 }, { altitude: 9000, density: 0.93341 }, { altitude: 8000, density: 0.96287 }, { altitude: 7000, density: 0.99304 }, { altitude: 6000, density: 1.02393 }, { altitude: 5000, density: 1.05555 }, { altitude: 4000, density: 1.08791 }, { altitude: 3000, density: 1.12102 }, { altitude: 2000, density: 1.1549 }, { altitude: 1000, density: 1.18955 }, { altitude: 0, density: 1.225 }, ]; @Injectable() export class WeatherService { constructor( @InjectModel('weather') private readonly weatherModel: Model, ) {} /// @brief Calculates the air density based on a linear model /// @param altitude The requested altitude private static calculateDensity(altitude: number): number { let upperBorder: { altitude: number; density: number } = null; let lowerBorder: { altitude: number; density: number } = null; DensityModel.forEach((level) => { if (level.altitude > altitude) { upperBorder = level; } else { lowerBorder = level; return; } }); // above maximum altitude if (!upperBorder) return lowerBorder.density; // below minimum altitude if (!lowerBorder) return upperBorder.density; // calculate the ratio to get the correct density const altitudeStepSize = upperBorder.altitude - lowerBorder.altitude; const offset = altitude - lowerBorder.altitude; const ratio = offset / altitudeStepSize; // calculate the delta to get the correct density const densityStepSize = lowerBorder.density - upperBorder.density; const delta = densityStepSize * ratio; return upperBorder.density + delta; } /// @brief Calculates the true airspeed /// Based on the model: https://aerotoolbox.com/airspeed-conversions/ /// @param indicatedAirspeed The requested indicated airspeed /// @param altitude The requested altitude private static calculateTrueAirspeed( indicatedAirspeed: number, altitude: number, ): number { const density = WeatherService.calculateDensity(altitude); return Math.round(indicatedAirspeed * Math.sqrt(1.225 / density)); } /// @brief Calculates the distance based on the sampling constant /// @param distance The actual distance /// @return The sampled distance private static sampleDistanceToStandard(distance: number): number { return Math.floor(distance / DistanceSampling); } /// @brief Calculates the altitude based on the sampling constant /// @param distance The actual altitude /// @return The sampled altitude private static sampleAltitudeToStandard(altitude: number): number { return Math.floor(altitude / AltitudeSampling); } /// @brief Updates the internal weather model that is learned based on reports /// @param airportIcao The destination airport /// @param waypoint The report of a waypoint /// @param distance The current distance to the waypoint /// @param groundspeed The reported groundspeed /// @param heading The reported heading /// @param track The reported ground track /// @param altitude The current altitude async update( airportIcao: string, waypoint: string, distance: number, groundspeed: number, heading: number, track: number, altitude: number, ): Promise { // calculate the delta for mathematical positive rotations and scale to [0,360[ degrees let windIndicatedDelta = heading + track; windIndicatedDelta = ((windIndicatedDelta % 360) + 360) % 360; // convert to [-180, 180[ if (windIndicatedDelta > 180) windIndicatedDelta -= 360; // convert to radians windIndicatedDelta = windIndicatedDelta * (Math.PI / 180.0); // calculate the true airspeed and the correction const trueAirspeed = groundspeed / Math.cos(windIndicatedDelta); const groundspeedCorrection = groundspeed - trueAirspeed; // calculate the standard sampling values const standardDistance = WeatherService.sampleDistanceToStandard(distance); const standardAltitude = WeatherService.sampleAltitudeToStandard(altitude); this.weatherModel .findOne({ icao: airportIcao, waypoint, altitude: standardAltitude, distanceToWaypoint: standardDistance, }) .then((sample) => { if (sample) { const meanCorrection = Math.round( (sample.groundSpeedCorrection + groundspeedCorrection) / 2, ); this.weatherModel.findOneAndUpdate( { icao: airportIcao, waypoint, altitude: standardAltitude, distanceToWaypoint: standardDistance, }, { groundSpeedCorrection: meanCorrection, }, ); } else { this.weatherModel.create({ icao: airportIcao, waypoint, altitude: standardAltitude, distanceToWaypoint: standardDistance, groundSpeedCorrection: groundspeedCorrection, }); } }); } /// @brief Cleanup the weather samples async clean(): Promise { this.weatherModel.deleteMany({}); } /// @brief Calculates the corrected ground speed at a position /// @param airportIcao The destination airport /// @param waypoint The requested waypoint /// @param distance The distance to the waypoint /// @param altitude The requested altitude /// @param indicatedAirspeed The requested indicated airspeed /// @return The corrected ground speed async groundSpeed( airportIcao: string, waypoint: string, distance: number, altitude: number, indicatedAirspeed: number, ): Promise { // calculate the standard sampling values const standardDistance = WeatherService.sampleDistanceToStandard(distance); const standardAltitude = WeatherService.sampleAltitudeToStandard(altitude); const trueAirspeed = WeatherService.calculateTrueAirspeed( indicatedAirspeed, altitude, ); return this.weatherModel .findOne({ icao: airportIcao, waypoint, altitude: standardAltitude, distanceToWaypoint: standardDistance, }) .then((sample) => { if (!sample) return trueAirspeed; return trueAirspeed + sample.groundSpeedCorrection; }); } }