@fboes/aerofly-patterns
Version:
Landegerät - Create random custom missions for Aerofly FS 4.
293 lines (251 loc) • 9.62 kB
text/typescript
import { Airport } from "./Airport.js";
import { AviationWeatherApi } from "../general/AviationWeatherApi.js";
import { Configuration } from "./Configuration.js";
import { FeatureCollection, Feature, LineString, Point } from "@fboes/geojson";
import { Scenario } from "./Scenario.js";
import { DateYielder } from "../general/DateYielder.js";
import { Formatter } from "../general/Formatter.js";
import { LocalTime } from "../general/LocalTime.js";
import {
AeroflyMission,
AeroflyMissionConditions,
AeroflyMissionConditionsCloud,
AeroflyMissionsList,
AeroflyMissionTargetPlane,
} from "@fboes/aerofly-custom-missions";
import { Vector } from "@fboes/geojson";
import { Markdown } from "../general/Markdown.js";
export interface AeroflyPatternsWaypointable {
id: string;
position: Point;
}
export class AeroflyPatterns {
configuration: Configuration;
airport: Airport | null;
scenarios: Scenario[];
constructor(configuration: Configuration) {
this.configuration = configuration;
/**
* @type {Airport?} the airport to build scenarios for
*/
this.airport = null;
/**
* @type {Scenario[]} the scenarios to
*/
this.scenarios = [];
}
static async init(configuration: Configuration): Promise<AeroflyPatterns> {
const self = new AeroflyPatterns(configuration);
const airport = await AviationWeatherApi.fetchAirports([self.configuration.icaoCode]);
if (!airport.length) {
throw new Error("No airport information from API");
}
self.airport = new Airport(airport[0], self.configuration);
const navaids = await AviationWeatherApi.fetchNavaid(self.airport.position, 10000);
self.airport.setNavaids(navaids);
const dateYielder = new DateYielder(self.configuration.numberOfMissions, self.airport.nauticalTimezone);
const dates = dateYielder.entries();
for (const date of dates) {
try {
const scenario = await Scenario.init(self.airport, self.configuration, date);
self.scenarios.push(scenario);
} catch (error) {
console.error(error);
}
}
if (self.scenarios.length === 0) {
throw Error("No scenarios generated, possibly because of missing weather data");
}
return self;
}
buildGeoJson(): FeatureCollection | null {
if (!this.airport) {
return null;
}
const scenario = this.scenarios[0];
const geoJson = new FeatureCollection([
new Feature(this.airport.position, {
title: this.airport.name,
"marker-symbol": "airport",
frequency: this.airport.localFrequency,
}),
]);
this.airport.runways.forEach((r) => {
geoJson.addFeature(
new Feature(r.position, {
title: r.id,
"marker-symbol":
r === scenario.activeRunway ? "triangle" : r.runwayType === "H" ? "heliport" : "triangle-stroked",
alignment: r.alignment,
dimension: r.dimension,
frequency: r.ilsFrequency,
isRightPattern: r.isRightPattern,
}),
);
});
this.airport.navaids.forEach((n) => {
geoJson.addFeature(
new Feature(n.position, {
title: n.id,
"marker-symbol": "communications-tower",
frequency: n.frequency,
type: n.type,
}),
);
});
geoJson.addFeature(
new Feature(scenario.aircraft.position, {
title: scenario.aircraft.data.icaoCode,
"marker-symbol": "airfield",
}),
);
const waypoints = scenario.patternWaypoints.map((p) => {
return p.position;
});
waypoints.push(scenario.patternWaypoints[0].position);
geoJson.addFeature(
new Feature(new LineString(waypoints), {
title: "Traffic pattern",
"stroke-opacity": 0.2,
}),
);
if (scenario.activeRunway && scenario.entryWaypoint) {
geoJson.addFeature(
new Feature(
new LineString([
scenario.aircraft.position,
scenario.entryWaypoint.position,
scenario.patternWaypoints[2].position,
scenario.patternWaypoints[3].position,
scenario.patternWaypoints[4].position,
scenario.activeRunway?.position,
]),
{
title: "Flight plan",
stroke: "#ff1493",
},
),
);
}
scenario.patternWaypoints.forEach((p) => {
geoJson.addFeature(
new Feature(p.position, {
title: p.id,
"marker-symbol": "racetrack",
}),
);
});
return geoJson;
}
buildCustomMissionTmc(): string {
/**
* @type {AeroflyMission[]}
*/
const missions: AeroflyMission[] = this.scenarios.map((s, index) => {
const conditions = new AeroflyMissionConditions({
time: s.date,
wind: {
direction: s.weather?.windDirection ?? 0,
speed: s.weather?.windSpeed ?? 0,
gusts: s.weather?.windGusts ?? 0,
},
turbulenceStrength: s.weather?.turbulenceStrength ?? 0,
visibility_sm: s.weather?.visibility ?? 15,
clouds:
s.weather?.clouds.map((c) => {
return AeroflyMissionConditionsCloud.createInFeet(c.cloudCover, c.cloudBase);
}) ?? [],
});
conditions.temperature = s.weather?.temperature ?? 0;
const mission = new AeroflyMission(`${s.airport.id} #${index + 1}: ${s.airport.name}`, {
checkpoints: s.waypoints,
description: s.description ?? "",
tags: s.tags,
flightSetting: "cruise",
difficulty: 0.5 + index * 0.01,
aircraft: {
name: s.aircraft.aeroflyCode,
livery: s.aircraft.aeroflyLiveryCode,
icao: s.aircraft.data.icaoCode,
},
callsign: s.aircraft.data.callsign,
origin: {
icao: s.airport.id,
longitude: s.aircraft.position.longitude,
latitude: s.aircraft.position.latitude,
dir: s.aircraft.heading,
alt: s.aircraft.position.elevation ?? 0,
},
destination: {
icao: s.airport.id,
longitude: s.airport.position.longitude,
latitude: s.airport.position.latitude,
dir: s.activeRunway?.alignment ?? 0,
alt: s.airport.position.elevation ?? 0,
},
conditions,
});
if (this.configuration.noGuides) {
const targetPosition = s.aircraft.position.getPointBy(new Vector(1, s.aircraft.heading));
mission.finish = new AeroflyMissionTargetPlane(
targetPosition.longitude,
targetPosition.latitude,
s.aircraft.heading,
);
}
return mission;
});
return new AeroflyMissionsList(missions).toString();
}
buildReadmeMarkdown(): string {
if (!this.airport) {
return "";
}
const firstMission = this.scenarios[0];
const markdownTable = Markdown.table([
[`No `, `Local date¹`, `Local time¹`, `Wind`, `Clouds`, `Visibility`, `Runway`, `Aircraft position`],
[`:-:`, `-----------`, `----------:`, `----`, `------`, `---------:`, `------`, `-----------------`],
...this.scenarios.map((s, index) => {
const localNauticalTime = new LocalTime(s.date, s.airport.nauticalTimezone);
const clouds =
s.weather?.clouds[0]?.cloudCoverCode !== "CLR"
? `${s.weather?.clouds[0]?.cloudCoverCode} @ ${s.weather?.clouds[0]?.cloudBase.toLocaleString("en")} ft`
: s.weather?.clouds[0]?.cloudCoverCode;
return [
`#${String(index + 1).padStart(2, "0")}`,
localNauticalTime.toDateString(),
localNauticalTime.toTimeString(),
!s.weather?.windSpeed ? "Calm" : `${s.weather?.windDirection}° @ ${s.weather?.windSpeed} kts`,
clouds,
Math.round(s.weather?.visibility ?? 0) + " SM",
s.activeRunway?.id + (s.activeRunway?.isRightPattern ? " (RP)" : ""),
Formatter.getDirectionArrow(s.aircraft.vectorFromAirport.bearing) +
" To the " +
Formatter.getDirection(s.aircraft.vectorFromAirport.bearing),
];
}),
]);
const airportDescription = this.airport.getDescription(firstMission.aircraft.data.hasNoRadioNav !== true);
return `\
# Landing Challenges: ${this.airport.name} (${this.airport.id})
This [\`custom_missions_user.tmc\`](missions/custom_missions_user.tmc) file contains random landing scenarios for Aerofly FS 4.
Your ${firstMission.aircraft.data.nameFull} is ${this.configuration.initialDistance} NM away from ${this.airport.name} Airport, and you have to make a correct landing pattern entry and land safely.
## Airport details
${airportDescription.trim()}
Get [more information about ${this.airport.name} Airport on SkyVector](https://skyvector.com/airport/${encodeURIComponent(this.airport.id)}):
- What is the tower / CTAF frequency?
- What is the Traffic Pattern Altitude (TPA) for this airport?
- Has the runway standard left turns, or right turns?
- Are there additional navigational aids like ILS for your assigned runways?
- Are there special noise abatement procedures in effect?
## Included missions
${markdownTable}
¹) Local [nautical time](https://en.wikipedia.org/wiki/Nautical_time)
## Installation instructions
1. Download the [\`custom_missions_user.tmc\`](missions/custom_missions_user.tmc)
2. See [the installation instructions](https://fboes.github.io/aerofly-missions/docs/generic-installation.html) on how to import the missions into Aerofly FS 4.
---
Created with [Aerofly Landegerät](https://github.com/fboes/aerofly-patterns)
`;
}
}