introduce a weather module

This commit is contained in:
Sven Czarnian
2022-10-24 21:23:58 +02:00
parent b65f1d7561
commit 3b4a25030f
5 changed files with 290 additions and 0 deletions

View File

@@ -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],
}) })

View 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);

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

View 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();
});
});

View 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;
});
}
}