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 { 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
									
								
								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