introduce a weather module
This commit is contained in:
		| @@ -10,6 +10,7 @@ import { LoggingController } from './logging/logging.controller'; | |||||||
| import { AirportController } from './airport/airport.controller'; | import { AirportController } from './airport/airport.controller'; | ||||||
| import { InboundModule } from './inbound/inbound.module'; | import { InboundModule } from './inbound/inbound.module'; | ||||||
| import { InboundController } from './inbound/inbound.controller'; | import { InboundController } from './inbound/inbound.controller'; | ||||||
|  | import { WeatherModule } from './weather/weather.module'; | ||||||
|  |  | ||||||
| @Module({ | @Module({ | ||||||
|   imports: [ |   imports: [ | ||||||
| @@ -33,6 +34,7 @@ import { InboundController } from './inbound/inbound.controller'; | |||||||
|     AirportModule, |     AirportModule, | ||||||
|     LoggingModule, |     LoggingModule, | ||||||
|     InboundModule, |     InboundModule, | ||||||
|  |     WeatherModule, | ||||||
|   ], |   ], | ||||||
|   controllers: [LoggingController, AirportController, InboundController], |   controllers: [LoggingController, AirportController, InboundController], | ||||||
| }) | }) | ||||||
|   | |||||||
							
								
								
									
										41
									
								
								src/weather/models/weather.model.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										41
									
								
								src/weather/models/weather.model.ts
									
									
									
									
									
										Normal file
									
								
							| @@ -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
									
								
								src/weather/weather.module.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										13
									
								
								src/weather/weather.module.ts
									
									
									
									
									
										Normal file
									
								
							| @@ -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
									
								
								src/weather/weather.service.spec.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										18
									
								
								src/weather/weather.service.spec.ts
									
									
									
									
									
										Normal file
									
								
							| @@ -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
									
								
								src/weather/weather.service.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										216
									
								
								src/weather/weather.service.ts
									
									
									
									
									
										Normal file
									
								
							| @@ -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; | ||||||
|  |       }); | ||||||
|  |   } | ||||||
|  | } | ||||||
		Reference in New Issue
	
	Block a user