From 3b4a25030f77a9702d0f82a81fe0f126eb8d3bb2 Mon Sep 17 00:00:00 2001 From: Sven Czarnian Date: Mon, 24 Oct 2022 21:23:58 +0200 Subject: [PATCH] introduce a weather module --- src/app.module.ts | 2 + src/weather/models/weather.model.ts | 41 ++++++ src/weather/weather.module.ts | 13 ++ src/weather/weather.service.spec.ts | 18 +++ src/weather/weather.service.ts | 216 ++++++++++++++++++++++++++++ 5 files changed, 290 insertions(+) create mode 100644 src/weather/models/weather.model.ts create mode 100644 src/weather/weather.module.ts create mode 100644 src/weather/weather.service.spec.ts create mode 100644 src/weather/weather.service.ts diff --git a/src/app.module.ts b/src/app.module.ts index b1f91c5..8371dae 100644 --- a/src/app.module.ts +++ b/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], }) diff --git a/src/weather/models/weather.model.ts b/src/weather/models/weather.model.ts new file mode 100644 index 0000000..798067e --- /dev/null +++ b/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); diff --git a/src/weather/weather.module.ts b/src/weather/weather.module.ts new file mode 100644 index 0000000..0c9119e --- /dev/null +++ b/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 {} diff --git a/src/weather/weather.service.spec.ts b/src/weather/weather.service.spec.ts new file mode 100644 index 0000000..9614b2d --- /dev/null +++ b/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); + }); + + it('should be defined', () => { + expect(service).toBeDefined(); + }); +}); diff --git a/src/weather/weather.service.ts b/src/weather/weather.service.ts new file mode 100644 index 0000000..d27f3bc --- /dev/null +++ b/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, + ) {} + + /// @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; + }); + } +}