introduce the database and controller connections for all inbounds

This commit is contained in:
Sven Czarnian
2022-10-23 16:58:00 +02:00
parent 7a41dd0fb2
commit f69e365623
19 changed files with 849 additions and 4 deletions

View File

@@ -25,9 +25,9 @@ export class AirportService {
}
async airport(icao: string): Promise<Airport> {
return this.airportModel.find({ icao }).then((response) => {
if (!response || response.length !== 1) return null;
return response[0];
return this.airportModel.findOne({ icao }).then((response) => {
if (!response) return null;
return response;
});
}

View File

@@ -8,6 +8,8 @@ import { AirportModule } from './airport/airport.module';
import { LoggingModule } from './logging/logging.module';
import { LoggingController } from './logging/logging.controller';
import { AirportController } from './airport/airport.controller';
import { InboundModule } from './inbound/inbound.module';
import { InboundController } from './inbound/inbound.controller';
@Module({
imports: [
@@ -30,7 +32,8 @@ import { AirportController } from './airport/airport.controller';
PerformanceModule,
AirportModule,
LoggingModule,
InboundModule,
],
controllers: [LoggingController, AirportController],
controllers: [LoggingController, AirportController, InboundController],
})
export class AppModule {}

View File

@@ -0,0 +1,44 @@
import { IsNotEmpty, Length, IsInt, Min, Max } from 'class-validator';
import { ApiProperty } from '@nestjs/swagger';
export class AircraftDto {
@IsNotEmpty()
@ApiProperty({
description: 'The ICAO code of the aircraft',
example: 'A359',
})
type: string;
@IsNotEmpty()
@ApiProperty({
description: 'The Wake Turbulence Category (possible: L, M, H, S)',
example: 'M',
})
@Length(1)
wtc: string;
@IsNotEmpty()
@ApiProperty({
description: 'The Wake Turbulence Category (possible: A, B, C, D, E, F)',
example: 'C',
})
@Length(1)
wakeRecat: string;
@IsNotEmpty()
@ApiProperty({
description: 'The number of engines',
example: 2,
})
@IsInt()
@Min(1)
@Max(6)
engineCount: number;
@IsNotEmpty()
@ApiProperty({
description: 'The engine type (possible: ELECTRIC, TURBOPROP, JET)',
example: 'JET',
})
engineType: string;
}

View File

@@ -0,0 +1,32 @@
import { IsNotEmpty, IsOptional, IsDateString } from 'class-validator';
import { ApiProperty } from '@nestjs/swagger';
import { WaypointDto } from '../../generic/dto/waypoint.dto';
export class ControllerInputDto {
@IsOptional()
@ApiProperty({
description: 'The timestamp of the last report',
})
@IsDateString()
reportedTime: string;
@IsNotEmpty()
@ApiProperty({
description: 'The remaining route of the inbound',
})
remainingRoute: WaypointDto[];
@IsOptional()
@ApiProperty({
description: 'The requested runway by the pilot',
example: '25L',
})
requestedRunway: string;
@IsOptional()
@ApiProperty({
description: 'The planned stand after landing',
example: 'A05',
})
plannedStand: string;
}

View File

@@ -0,0 +1,49 @@
import { IsNotEmpty, IsInt, Min, Max } from 'class-validator';
import { ApiProperty } from '@nestjs/swagger';
import { CoordinateDto } from '../../generic/dto/coordinate.dto';
export class FlightStateDto {
@IsNotEmpty()
@ApiProperty({
description: 'The current position',
})
position: CoordinateDto;
@IsNotEmpty()
@ApiProperty({
description: 'The reported ground speed',
example: 400,
})
@IsInt()
@Min(0)
@Max(700)
groundSpeed: number;
@IsNotEmpty()
@ApiProperty({
description: 'The reported ground track',
example: 400,
})
@IsInt()
@Min(0)
@Max(360)
groundTrack: number;
@IsNotEmpty()
@ApiProperty({
description: 'The reported altitude',
example: 30000,
})
@IsInt()
@Min(0)
@Max(60000)
altitude: number;
@IsNotEmpty()
@ApiProperty({
description: 'The reported vertical speed',
example: 400,
})
@IsInt()
verticalSpeed: number;
}

View File

@@ -0,0 +1,56 @@
import { IsNotEmpty, IsOptional, Length } from 'class-validator';
import { ApiProperty } from '@nestjs/swagger';
import { AircraftDto } from './aircraft.dto';
import { ControllerInputDto } from './controllerinput.dto';
import { FlightStateDto } from './flightstate.dto';
import { PlanningDto } from './planning.dto';
export class InboundDto {
@IsNotEmpty()
@ApiProperty({
description: 'The used callsign',
example: 'DLH3PM',
})
callsign: string;
@IsNotEmpty()
@ApiProperty({
description: 'The filed destination',
example: 'EDDB',
})
@Length(4)
destination: string;
@IsNotEmpty()
@ApiProperty({
description:
'The reported controller level (possible: UNK, DEL, GRD, TWR, APP, DEP, CTR)',
example: 'EDDB',
})
@Length(3)
reporter: string;
@IsNotEmpty()
@ApiProperty({
description: 'The used aircraft',
})
aircraft: AircraftDto;
@IsNotEmpty()
@ApiProperty({
description: 'The current flight state',
})
flightState: FlightStateDto;
@IsOptional()
@ApiProperty({
description: 'The data given by the controller',
})
controllerData: ControllerInputDto;
@IsOptional()
@ApiProperty({
description: 'The planned arrival',
})
plan: PlanningDto;
}

View File

@@ -0,0 +1,31 @@
import { IsNotEmpty } from 'class-validator';
import { ApiProperty } from '@nestjs/swagger';
import { PredictedWaypointDto } from './predictedwaypoint.dto';
export class PlanningDto {
@IsNotEmpty()
@ApiProperty({
description: 'The planned arrival route',
example: 'KETAP25L',
})
arrivalRoute: string;
@IsNotEmpty()
@ApiProperty({
description: 'The planned arrival runway',
example: '25L',
})
arrivalRunway: string;
@IsNotEmpty()
@ApiProperty({
description: 'The planned route',
})
plannedRoute: PredictedWaypointDto[];
@IsNotEmpty()
@ApiProperty({
description: 'Indicates if the plan is fixed',
})
fixedPlan: boolean;
}

View File

@@ -0,0 +1,49 @@
import { IsNotEmpty, IsInt, Min, Max } from 'class-validator';
import { ApiProperty } from '@nestjs/swagger';
import { WaypointDto } from 'src/generic/dto/waypoint.dto';
export class PredictedWaypointDto {
@IsNotEmpty()
@ApiProperty({
description: 'The waypoint',
})
waypoint: WaypointDto;
@IsNotEmpty()
@ApiProperty({
description: 'The planned altitude',
example: 23000,
})
@IsInt()
@Min(0)
@Max(60000)
altitude: number;
@IsNotEmpty()
@ApiProperty({
description: 'The planned indicated airspeed',
example: 200,
})
@IsInt()
@Min(0)
@Max(600)
indicatedAirspeed: number;
@IsNotEmpty()
@ApiProperty({
description: 'The planned groundspeed',
example: 500,
})
@IsInt()
@Min(0)
@Max(800)
groundspeed: number;
@IsNotEmpty()
@ApiProperty({
description: 'The planned time overhead the waypoint',
example: 'Wed, 14 Jun 2017 07:00:00z',
})
@IsInt()
plannedOverheadTime: string;
}

View File

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

View File

@@ -0,0 +1,213 @@
import {
Body,
Controller,
Get,
Delete,
Post,
Query,
HttpException,
HttpStatus,
} from '@nestjs/common';
import { ApiBody, ApiQuery, ApiResponse } from '@nestjs/swagger';
import { AirportService } from 'src/airport/airport.service';
import { CoordinateConverter, WaypointConverter } from 'src/generic/converters';
import { CoordinateDto } from 'src/generic/dto/coordinate.dto';
import { WaypointDto } from 'src/generic/dto/waypoint.dto';
import { AircraftDto } from './dto/aircraft.dto';
import { ControllerInputDto } from './dto/controllerinput.dto';
import { FlightStateDto } from './dto/flightstate.dto';
import { InboundDto } from './dto/inbound.dto';
import { PlanningDto } from './dto/planning.dto';
import { PredictedWaypointDto } from './dto/predictedwaypoint.dto';
import { InboundService } from './inbound.service';
import { Aircraft } from './models/aircraft.model';
import { ControllerInput } from './models/controllerinput.model';
import { FlightState } from './models/flightstate.model';
import { Inbound } from './models/inbound.model';
import { Planning } from './models/planning.model';
import { PredictedWaypoint } from './models/predictedwaypoint.model';
@Controller('inbound')
export class InboundController {
constructor(
private readonly inboundService: InboundService,
private readonly airportService: AirportService,
) {}
private static convertAircraft<T>(aircraft: Aircraft | AircraftDto): T {
return {
type: aircraft.type,
wtc: aircraft.wtc,
wakeRecat: aircraft.wakeRecat,
engineCount: aircraft.engineCount,
engineType: aircraft.engineType,
} as T;
}
private static convertControllerInput<T>(
input: ControllerInput | ControllerInputDto,
): T {
return {
reportedTime: input.reportedTime,
remainingRoute: WaypointConverter.convertList(input.remainingRoute),
requestedRunway: input.requestedRunway,
plannedStand: input.plannedStand,
} as T;
}
private static convertFlightState<T>(input: FlightState | FlightStateDto): T {
return {
position: CoordinateConverter.convert(input.position),
groundSpeed: input.groundSpeed,
groundTrack: input.groundTrack,
altitude: input.altitude,
verticalSpeed: input.verticalSpeed,
} as T;
}
private static convertPredictedWaypoints(
waypoints: PredictedWaypoint[],
): PredictedWaypointDto[] {
const retval: PredictedWaypointDto[] = [];
waypoints.forEach((input) =>
retval.push({
waypoint: WaypointConverter.convert<WaypointDto, CoordinateDto>(
input.waypoint,
),
altitude: input.altitude,
indicatedAirspeed: input.indicatedAirspeed,
groundspeed: input.groundspeed,
plannedOverheadTime: input.plannedOverheadTime,
}),
);
return retval;
}
private static convertPlanning(plan: Planning): PlanningDto {
if (plan === undefined) return undefined;
return {
arrivalRoute: plan.arrivalRoute,
arrivalRunway: plan.arrivalRunway,
plannedRoute: InboundController.convertPredictedWaypoints(
plan.plannedRoute,
),
fixedPlan: plan.fixedPlan,
};
}
private static convertInboundToInboundDto(inbound: Inbound): InboundDto {
return {
callsign: inbound.callsign,
destination: inbound.destination,
reporter: inbound.reporter,
aircraft: InboundController.convertAircraft(inbound.aircraft),
flightState: InboundController.convertFlightState(inbound.flightState),
controllerData: InboundController.convertControllerInput(
inbound.controllerData,
),
plan: InboundController.convertPlanning(inbound.plan),
};
}
private static convertInboundDtoInbound(inbound: InboundDto): Inbound {
return {
callsign: inbound.callsign,
destination: inbound.destination,
reporter: inbound.reporter,
aircraft: InboundController.convertAircraft(inbound.aircraft),
flightState: InboundController.convertFlightState(inbound.flightState),
controllerData: InboundController.convertControllerInput(
inbound.controllerData,
),
plan: undefined,
};
}
@Get('/inbounds')
@ApiQuery({
name: 'icao',
description: 'The ICAO code of the airport',
type: String,
})
@ApiResponse({
status: 200,
description: 'All known inbounds',
type: [InboundDto],
})
async inbounds(@Query('icao') icao: string): Promise<InboundDto[]> {
return this.inboundService.airportInbounds(icao).then((inbounds) => {
if (!inbounds) {
throw new HttpException(
'No inbounds for the airport found',
HttpStatus.NOT_FOUND,
);
}
const retval: InboundDto[] = [];
inbounds.forEach((inbound) =>
retval.push(InboundController.convertInboundToInboundDto(inbound)),
);
return retval;
});
}
@Post('/insert')
@ApiBody({
description: 'The new or updated inbound',
type: InboundDto,
})
@ApiResponse({
status: 201,
description: 'Inserted or updated the inbound',
})
@ApiResponse({
status: 404,
description: 'The destination airport is unknown',
})
async insert(inbound: InboundDto): Promise<boolean> {
return this.airportService.airport(inbound.destination).then((airport) => {
if (!airport) {
throw new HttpException('Airport not found', HttpStatus.NOT_FOUND);
}
return this.inboundService
.callsignKnown(inbound.callsign)
.then((known) => {
const internalInbound =
InboundController.convertInboundDtoInbound(inbound);
if (known) {
return this.inboundService.update(internalInbound);
} else {
return this.inboundService.add(internalInbound);
}
});
});
}
@Delete('/remove')
@ApiQuery({
name: 'callsign',
description: 'The callsign of the inbound',
type: String,
})
@ApiResponse({
status: 200,
description: 'The inbound is deleted',
})
@ApiResponse({
status: 404,
description: 'The inbound is unknown',
})
async remove(@Query('callsign') callsign: string): Promise<void> {
await this.inboundService.callsignKnown(callsign).then(async (known) => {
if (!known) {
throw new HttpException('Inbound not found', HttpStatus.NOT_FOUND);
}
await this.inboundService.remove(callsign);
});
}
}

View File

@@ -0,0 +1,17 @@
import { Module } from '@nestjs/common';
import { MongooseModule } from '@nestjs/mongoose';
import { InboundSchema } from './models/inbound.model';
import { InboundService } from './inbound.service';
import { InboundController } from './inbound.controller';
import { AirportModule } from 'src/airport/airport.module';
@Module({
imports: [
MongooseModule.forFeature([{ name: 'inbound', schema: InboundSchema }]),
AirportModule,
],
providers: [InboundService],
controllers: [InboundController],
exports: [MongooseModule, InboundService],
})
export class InboundModule {}

View File

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

View File

@@ -0,0 +1,61 @@
import { Injectable } from '@nestjs/common';
import { InjectModel } from '@nestjs/mongoose';
import { Model } from 'mongoose';
import { Inbound, InboundDocument } from './models/inbound.model';
@Injectable()
export class InboundService {
constructor(
@InjectModel('inbound')
private readonly inboundModel: Model<InboundDocument>,
) {}
async callsignKnown(callsign: string): Promise<boolean> {
return this.inboundModel
.findOne({ callsign })
.then((inbound) => inbound !== null);
}
async add(inbound: Inbound): Promise<boolean> {
return this.callsignKnown(inbound.callsign).then(async (known) => {
if (known) return false;
// modify the inbound data
inbound.plan = undefined;
inbound.controllerData.reportedTime = new Date().toUTCString();
await this.inboundModel.create(inbound);
return true;
});
}
async remove(callsign: string): Promise<boolean> {
return this.callsignKnown(callsign).then(async (known) => {
if (!known) return false;
await this.inboundModel.deleteOne({ callsign });
return true;
});
}
async update(inbound: Inbound): Promise<boolean> {
return this.callsignKnown(inbound.callsign).then(async (known) => {
if (!known) return false;
// modify the inbound data
inbound.plan = undefined;
inbound.controllerData.reportedTime = new Date().toUTCString();
await this.inboundModel.findOneAndUpdate(
{ callsign: inbound.callsign },
inbound,
);
return true;
});
}
async airportInbounds(icao: string): Promise<Inbound[]> {
return this.inboundModel.find({ destination: icao });
}
}

View File

@@ -0,0 +1,42 @@
import { Prop, Schema, SchemaFactory } from '@nestjs/mongoose';
import { Document } from 'mongoose';
export type AircraftDocument = Aircraft & Document;
@Schema()
export class Aircraft {
@Prop({
required: true,
type: String,
})
type: string;
@Prop({
required: true,
type: String,
enum: ['L', 'M', 'H', 'S'],
})
wtc: string;
@Prop({
required: true,
type: String,
enum: ['A', 'B', 'C', 'D', 'E', 'F'],
})
wakeRecat: string;
@Prop({
required: true,
type: Number,
})
engineCount: number;
@Prop({
required: true,
type: String,
enum: ['ELECTRIC', 'TURBOPROP', 'JET'],
})
engineType: string;
}
export const AircraftSchema = SchemaFactory.createForClass(Aircraft);

View File

@@ -0,0 +1,32 @@
import { Prop, Schema, SchemaFactory } from '@nestjs/mongoose';
import { Document } from 'mongoose';
import { Waypoint, WaypointSchema } from 'src/generic/models/waypoint.model';
export type ControllerInputDocument = ControllerInput & Document;
@Schema()
export class ControllerInput {
@Prop({
type: String,
})
reportedTime: string;
@Prop({
required: true,
type: [WaypointSchema],
})
remainingRoute: Waypoint[];
@Prop({
type: String,
})
requestedRunway: string;
@Prop({
type: String,
})
plannedStand: string;
}
export const ControllerInputSchema =
SchemaFactory.createForClass(ControllerInput);

View File

@@ -0,0 +1,43 @@
import { Prop, Schema, SchemaFactory } from '@nestjs/mongoose';
import { Document } from 'mongoose';
import {
Coordinate,
CoordinateSchema,
} from '../../generic/models/coordinate.model';
export type FlightStateDocument = FlightState & Document;
@Schema()
export class FlightState {
@Prop({
required: true,
type: CoordinateSchema,
})
position: Coordinate;
@Prop({
required: true,
type: Number,
})
groundSpeed: number;
@Prop({
required: true,
type: Number,
})
groundTrack: number;
@Prop({
required: true,
type: Number,
})
altitude: number;
@Prop({
required: true,
type: Number,
})
verticalSpeed: number;
}
export const FlightStateSchema = SchemaFactory.createForClass(FlightState);

View File

@@ -0,0 +1,59 @@
import { Prop, Schema, SchemaFactory } from '@nestjs/mongoose';
import { Document } from 'mongoose';
import { Aircraft, AircraftSchema } from './aircraft.model';
import {
ControllerInput,
ControllerInputSchema,
} from './controllerinput.model';
import { FlightState, FlightStateSchema } from './flightstate.model';
import { Planning, PlanningSchema } from './planning.model';
export type InboundDocument = Inbound & Document;
@Schema()
export class Inbound {
@Prop({
required: true,
index: true,
type: String,
})
callsign: string;
@Prop({
required: true,
index: true,
type: String,
})
destination: string;
@Prop({
required: true,
type: String,
enum: ['UNK', 'DEL', 'GRD', 'TWR', 'APP', 'DEP', 'CTR'],
})
reporter: string;
@Prop({
required: true,
type: AircraftSchema,
})
aircraft: Aircraft;
@Prop({
required: true,
type: FlightStateSchema,
})
flightState: FlightState;
@Prop({
type: ControllerInputSchema,
})
controllerData: ControllerInput;
@Prop({
type: PlanningSchema,
})
plan: Planning;
}
export const InboundSchema = SchemaFactory.createForClass(Inbound);

View File

@@ -0,0 +1,37 @@
import { Prop, Schema, SchemaFactory } from '@nestjs/mongoose';
import { Document } from 'mongoose';
import {
PredictedWaypoint,
PredictedWaypointSchema,
} from './predictedwaypoint.model';
export type PlanningDocument = Planning & Document;
@Schema()
export class Planning {
@Prop({
required: true,
type: String,
})
arrivalRoute: string;
@Prop({
required: true,
type: String,
})
arrivalRunway: string;
@Prop({
required: true,
type: [PredictedWaypointSchema],
})
plannedRoute: PredictedWaypoint[];
@Prop({
required: true,
type: Boolean,
})
fixedPlan: boolean;
}
export const PlanningSchema = SchemaFactory.createForClass(Planning);

View File

@@ -0,0 +1,41 @@
import { Prop, Schema, SchemaFactory } from '@nestjs/mongoose';
import { Document } from 'mongoose';
import { Waypoint, WaypointSchema } from '../../generic/models/waypoint.model';
export type PredictedWaypointDocument = PredictedWaypoint & Document;
@Schema()
export class PredictedWaypoint {
@Prop({
required: true,
type: WaypointSchema,
})
waypoint: Waypoint;
@Prop({
required: true,
type: Number,
})
altitude: number;
@Prop({
required: true,
type: Number,
})
indicatedAirspeed: number;
@Prop({
required: true,
type: Number,
})
groundspeed: number;
@Prop({
required: true,
type: String,
})
plannedOverheadTime: string;
}
export const PredictedWaypointSchema =
SchemaFactory.createForClass(PredictedWaypoint);