@fboes/aerofly-patterns
Version:
Landegerät - Create random custom missions for Aerofly FS 4.
272 lines (231 loc) • 9.09 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, AeroflyMissionsList, AeroflyMissionTargetPlane } from "@fboes/aerofly-custom-missions";
import { Vector } from "@fboes/geojson";
import { Markdown } from "../general/Markdown.js";
import { AviationWeatherApiHelper } from "../general/AviationWeatherApiHelper.js";
export interface AeroflyPatternsWaypointable {
id: string;
position: Point;
}
export class AeroflyPatterns {
configuration: Configuration;
airport: Airport;
scenarios: Scenario[];
constructor(configuration: Configuration, airport: Airport) {
this.configuration = configuration;
/**
* @type {Airport} the airport to build scenarios for
*/
this.airport = airport;
/**
* @type {Scenario[]} the scenarios to
*/
this.scenarios = [];
}
static async init(configuration: Configuration): Promise<AeroflyPatterns> {
const airport = await AviationWeatherApi.fetchAirports([configuration.icaoCode]);
if (!airport.length) {
throw new Error("No airport information from API");
}
const self = new AeroflyPatterns(configuration, new Airport(airport[0], configuration));
const navaids = await AviationWeatherApi.fetchNavaidsByPosition(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 = AviationWeatherApiHelper.makeConditions(s.date, s.weather);
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]?.cover !== "CLR"
? `${s.weather.clouds[0]?.cover} @ ${s.weather.clouds[0]?.base?.toLocaleString("en")} ft`
: s.weather.clouds[0]?.cover;
return [
`#${String(index + 1).padStart(2, "0")}`,
localNauticalTime.toDateString(),
localNauticalTime.toTimeString(),
!s.weather.wspd ? "Calm" : `${s.weather.wdir}° @ ${s.weather.wspd} kts`,
clouds,
Math.round(Math.min(s.weather.visib, 10)) + " 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 or instrument approach procedure 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)
`;
}
}