|
@@ -0,0 +1,216 @@
|
|
|
+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<WeatherDocument>,
|
|
|
+ ) {}
|
|
|
+
|
|
|
+ /// @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<void> {
|
|
|
+ // 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<void> {
|
|
|
+ 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<number> {
|
|
|
+ // 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;
|
|
|
+ });
|
|
|
+ }
|
|
|
+}
|