Browse Source

introduce a weather module

Sven Czarnian 2 years ago
parent
commit
3b4a25030f

+ 2 - 0
src/app.module.ts

@@ -10,6 +10,7 @@ import { LoggingController } from './logging/logging.controller';
 import { AirportController } from './airport/airport.controller';
 import { InboundModule } from './inbound/inbound.module';
 import { InboundController } from './inbound/inbound.controller';
+import { WeatherModule } from './weather/weather.module';
 
 @Module({
   imports: [
@@ -33,6 +34,7 @@ import { InboundController } from './inbound/inbound.controller';
     AirportModule,
     LoggingModule,
     InboundModule,
+    WeatherModule,
   ],
   controllers: [LoggingController, AirportController, InboundController],
 })

+ 41 - 0
src/weather/models/weather.model.ts

@@ -0,0 +1,41 @@
+import { Prop, Schema, SchemaFactory } from '@nestjs/mongoose';
+import { Document } from 'mongoose';
+
+export type WeatherDocument = Weather & Document;
+
+@Schema()
+export class Weather {
+  @Prop({
+    required: true,
+    index: true,
+    type: String,
+  })
+  icao: string;
+
+  @Prop({
+    required: true,
+    index: true,
+    type: String,
+  })
+  waypoint: string;
+
+  @Prop({
+    required: true,
+    type: Number,
+  })
+  altitude: number;
+
+  @Prop({
+    required: true,
+    type: Number,
+  })
+  distanceToWaypoint: number;
+
+  @Prop({
+    required: true,
+    type: Number,
+  })
+  groundSpeedCorrection: number;
+}
+
+export const WeatherSchema = SchemaFactory.createForClass(Weather);

+ 13 - 0
src/weather/weather.module.ts

@@ -0,0 +1,13 @@
+import { Module } from '@nestjs/common';
+import { MongooseModule } from '@nestjs/mongoose';
+import { WeatherSchema } from './models/weather.model';
+import { WeatherService } from './weather.service';
+
+@Module({
+  imports: [
+    MongooseModule.forFeature([{ name: 'weather', schema: WeatherSchema }]),
+  ],
+  providers: [WeatherService],
+  exports: [MongooseModule, WeatherService],
+})
+export class WeatherModule {}

+ 18 - 0
src/weather/weather.service.spec.ts

@@ -0,0 +1,18 @@
+import { Test, TestingModule } from '@nestjs/testing';
+import { WeatherService } from './weather.service';
+
+describe('WeatherService', () => {
+  let service: WeatherService;
+
+  beforeEach(async () => {
+    const module: TestingModule = await Test.createTestingModule({
+      providers: [WeatherService],
+    }).compile();
+
+    service = module.get<WeatherService>(WeatherService);
+  });
+
+  it('should be defined', () => {
+    expect(service).toBeDefined();
+  });
+});

+ 216 - 0
src/weather/weather.service.ts

@@ -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;
+      });
+  }
+}