@fboes/aerofly-patterns
Version:
Landegerät - Create random custom missions for Aerofly FS 4.
319 lines (295 loc) • 11.6 kB
text/typescript
import { AeroflyMission, AeroflyMissionCheckpoint } from "@fboes/aerofly-custom-missions";
import { AeroflyAircraft } from "../../data/AeroflyAircraft.js";
import { AviationWeatherNormalizedMetar } from "../general/AviationWeatherApi.js";
import { Configuration } from "./Configuration.js";
import { AeroflyMissionPosition } from "@fboes/aerofly-custom-missions/types/dto/AeroflyMission.js";
import { AeroflyMissionAutofill } from "../general/AeroflyMissionAutofill.js";
import { Vector } from "@fboes/geojson";
import { Units } from "../../data/Units.js";
import { Degree } from "../general/Degree.js";
import { HoldingPattern, HoldingPatternEntry } from "./HoldingPattern.js";
import { Rand } from "../general/Rand.js";
import { Formatter } from "../general/Formatter.js";
import { HoldingPatternFix } from "./HoldingPatternFix.js";
import { AviationWeatherApiHelper } from "../general/AviationWeatherApiHelper.js";
/**
* Represents a scenario for a holding pattern mission in Aerofly.
* This class encapsulates the properties and methods needed to create a holding pattern mission,
* including the mission title, description, conditions, origin and destination positions, and checkpoints.
*/
export class Scenario {
//date: Date;
//aircraft: AeroflyAircraft;
mission: AeroflyMission;
pattern: HoldingPattern;
patternEntry: HoldingPatternEntry;
static async init(
holdingNavAid: HoldingPatternFix,
configuration: Configuration,
aircraft: AeroflyAircraft,
date: Date,
index: number = 0,
initialBearing: number = -1,
): Promise<Scenario> {
return new Scenario(
configuration,
aircraft,
date,
await AviationWeatherApiHelper.getWeather(configuration.airportCode, date, holdingNavAid.position),
holdingNavAid,
index,
initialBearing,
);
}
constructor(
private readonly configuration: Configuration,
public readonly aircraft: AeroflyAircraft,
private readonly date: Date,
private readonly weather: AviationWeatherNormalizedMetar,
public readonly holdingNavAid: HoldingPatternFix,
public readonly index: number = 0,
public initialBearing: number = -1,
) {
this.pattern = new HoldingPattern(configuration, holdingNavAid, aircraft);
// Building the actual mission
const indexBearing = (360 / configuration.numberOfMissions) * index;
const bearing = Degree(
indexBearing +
this.pattern.inboundHeading +
(initialBearing >= 0 ? initialBearing : Rand.getRandomArbitrary(0, 360)),
); // Distribute entry procedures evenly among missions
this.patternEntry = this.pattern.getEntry(bearing);
const title = this.#getTitle(index);
const description = this.#getDescription(this.pattern);
const conditions = AviationWeatherApiHelper.makeConditions(this.date, this.weather);
const origin = this.#makeOriginPosition(this.pattern, bearing);
const destination = this.#makeDestinationPosition(this.pattern);
const checkpoints = this.#getCheckpoints(this.pattern, this.patternEntry);
this.mission = new AeroflyMission(title, {
description,
aircraft: {
name: aircraft.aeroflyCode,
icao: aircraft.icaoCode,
livery: configuration.livery,
},
callsign: aircraft.callsign,
flightSetting: "cruise",
conditions,
tags: ["holding", "pattern", "practice", "instrument"],
origin,
destination,
checkpoints,
});
const describer = new AeroflyMissionAutofill(this.mission);
this.mission.description = describer.description + "\n" + this.mission.description;
this.mission.tags = this.mission.tags.concat(describer.tags);
this.mission.distance = describer.distance;
this.mission.duration = this.pattern.furtherClearanceInMin * 60;
if (configuration.noGuides) {
describer.removeGuides();
}
}
#getTitle(index: number): string {
return `HOLD #${index + 1}: ${this.holdingNavAid.name}${this.pattern.dmeDistanceNm > 0 ? ` with DME fix` : ""}`;
}
/**
* @see https://www.code7700.com/holding.htm
*/
#getDescription(pattern: HoldingPattern): string {
const direction = Formatter.getDirection(pattern.holdingAreaDirection);
const dmeFix = pattern.dmeDistanceNm > 0 ? `the ${pattern.dmeDistanceNm} DME fix, ` : "";
const radial =
(this.holdingNavAid.type === "NDB" || this.holdingNavAid.type === "FIX" ? "inbound course " : "radial ") +
String(Math.round(pattern.inboundHeading)).padStart(3, "0") +
"°";
const dmeInfo =
pattern.dmeDistanceOutboundNm !== 0
? `${Math.abs(pattern.dmeDistanceNm - pattern.dmeDistanceOutboundNm)}-mile legs, `
: "";
const turnInfo = pattern.isLeftTurn ? "make left-hand turns, " : "make right-hand turns, ";
const altitude = new Intl.NumberFormat("en-US").format(pattern.patternAltitudeFt);
const efcDate = this.pattern.getFurtherClearance(this.date);
const efcString = `${efcDate.getUTCHours().toString().padStart(2, "0")}:${efcDate
.getUTCMinutes()
.toString()
.padStart(2, "0")}Z`;
return `Hold ${direction} of ${dmeFix + this.holdingNavAid.fullName} \
on the ${radial}, \
${dmeInfo}\
${turnInfo}\
maintain ${altitude}'. \
Expect further clearance at ${efcString}.`;
}
#makeOriginPosition(pattern: HoldingPattern, bearing: number): AeroflyMissionPosition {
const origin = pattern.holdingFix.getPointBy(
new Vector(this.configuration.initialDistance * Units.metersPerNauticalMile, bearing),
);
return {
icao: this.weather.icaoId,
latitude: origin.latitude,
longitude: origin.longitude,
alt:
Rand.getRandomInt(this.configuration.minimumSafeAltitude, this.configuration.maximumAltitude) *
100 *
Units.feetPerMeter,
dir: Degree(bearing + 180),
};
}
#makeDestinationPosition(pattern: HoldingPattern): AeroflyMissionPosition {
return {
icao: this.weather.icaoId,
latitude: pattern.holdingFix.latitude,
longitude: pattern.holdingFix.longitude,
alt: pattern.patternAltitudeFt * Units.feetPerMeter,
dir: pattern.inboundHeading,
};
}
#getCheckpoints(pattern: HoldingPattern, patternEntry: HoldingPatternEntry): AeroflyMissionCheckpoint[] {
const turnMultiplier = pattern.isLeftTurn ? -1 : 1;
const pointAfterFix = pattern.holdingFix.getPointBy(
new Vector(pattern.turnRadiusMeters, Degree(pattern.holdingAreaDirectionTrue + 180)),
);
const pointAbeam = pointAfterFix.getPointBy(
new Vector(2 * pattern.turnRadiusMeters, Degree(pattern.holdingAreaDirectionTrue - turnMultiplier * 90)),
);
const pointOutbound = pointAbeam.getPointBy(
new Vector(pattern.legDistanceMeters + 2 * pattern.turnRadiusMeters, Degree(pattern.holdingAreaDirectionTrue)),
);
const pointInbound = pointOutbound.getPointBy(
new Vector(2 * pattern.turnRadiusMeters, Degree(pattern.holdingAreaDirectionTrue + turnMultiplier * 90)),
);
const moreCheckpointProperties = {
altitude: pattern.patternAltitudeFt / Units.feetPerMeter,
altitudeConstraint: true,
};
return [
...this.#getEntryCheckpoints(pattern, patternEntry),
new AeroflyMissionCheckpoint(pattern.id, "waypoint", pattern.holdingFix.longitude, pattern.holdingFix.latitude, {
...moreCheckpointProperties,
frequency: this.holdingNavAid.frequency,
flyOver: true,
}),
new AeroflyMissionCheckpoint(
pattern.id + "-TURN",
"waypoint",
pointAfterFix.longitude,
pointAfterFix.latitude,
moreCheckpointProperties,
),
new AeroflyMissionCheckpoint(
pattern.id + "-ABEAM",
"waypoint",
pointAbeam.longitude,
pointAbeam.latitude,
moreCheckpointProperties,
),
new AeroflyMissionCheckpoint(
pattern.id + "-OUTBND",
"waypoint",
pointOutbound.longitude,
pointOutbound.latitude,
moreCheckpointProperties,
),
new AeroflyMissionCheckpoint(
pattern.id + "-INBND",
"waypoint",
pointInbound.longitude,
pointInbound.latitude,
moreCheckpointProperties,
),
new AeroflyMissionCheckpoint(pattern.id, "waypoint", pattern.holdingFix.longitude, pattern.holdingFix.latitude, {
...moreCheckpointProperties,
frequency: this.holdingNavAid.frequency,
flyOver: true,
}),
];
}
#getEntryCheckpoints(pattern: HoldingPattern, patternEntry: HoldingPatternEntry): AeroflyMissionCheckpoint[] {
if (patternEntry === "direct") {
// Direct entry does not need extra checkpoints
return [];
}
const turnMultiplier = pattern.isLeftTurn ? -1 : 1;
const entryLegDistance = pattern.legDistanceMeters + pattern.turnRadiusMeters;
const moreCheckpointProperties = {
altitude: pattern.patternAltitudeFt / Units.feetPerMeter,
altitudeConstraint: true,
};
if (patternEntry === "offset") {
// Is offset / teardrop entry
const pointAfterFix = pattern.holdingFix.getPointBy(
new Vector(entryLegDistance, Degree(pattern.holdingAreaDirectionTrue + turnMultiplier * -30)),
);
const pointInbound = pattern.holdingFix.getPointBy(
new Vector(entryLegDistance, Degree(pattern.holdingAreaDirectionTrue)),
);
return [
new AeroflyMissionCheckpoint(
pattern.id,
"waypoint",
pattern.holdingFix.longitude,
pattern.holdingFix.latitude,
{
...moreCheckpointProperties,
frequency: this.holdingNavAid.frequency,
flyOver: true,
},
),
new AeroflyMissionCheckpoint(
pattern.id + "-OFF1",
"waypoint",
pointAfterFix.longitude,
pointAfterFix.latitude,
moreCheckpointProperties,
),
new AeroflyMissionCheckpoint(
pattern.id + "-OFF2",
"waypoint",
pointInbound.longitude,
pointInbound.latitude,
moreCheckpointProperties,
),
];
}
// Else it is a parallel entry
const pointAlmostFix = pattern.holdingFix.getPointBy(
new Vector(100, Degree(pattern.holdingAreaDirectionTrue + turnMultiplier * 90)),
);
const pointTurnOutbound = pointAlmostFix.getPointBy(
new Vector(entryLegDistance, Degree(pattern.holdingAreaDirectionTrue)),
);
const pointTurnInbound = pointAlmostFix.getPointBy(
new Vector(entryLegDistance, Degree(pattern.holdingAreaDirectionTrue + turnMultiplier * -30)),
);
const pointInbound = pattern.holdingFix.getPointBy(
new Vector(pattern.turnRadiusMeters, Degree(pattern.holdingAreaDirectionTrue)),
);
return [
new AeroflyMissionCheckpoint(pattern.id + "PRL0", "waypoint", pointAlmostFix.longitude, pointAlmostFix.latitude, {
...moreCheckpointProperties,
frequency: this.holdingNavAid.frequency,
}),
new AeroflyMissionCheckpoint(
pattern.id + "-PRL1",
"waypoint",
pointTurnOutbound.longitude,
pointTurnOutbound.latitude,
moreCheckpointProperties,
),
new AeroflyMissionCheckpoint(
pattern.id + "-PRL2",
"waypoint",
pointTurnInbound.longitude,
pointTurnInbound.latitude,
moreCheckpointProperties,
),
new AeroflyMissionCheckpoint(
pattern.id + "-PRL3",
"waypoint",
pointInbound.longitude,
pointInbound.latitude,
moreCheckpointProperties,
),
];
}
}