@fboes/aerofly-patterns
Version:
Landegerät - Create random custom missions for Aerofly FS 4.
174 lines (143 loc) • 6.49 kB
text/typescript
import { AeroflyMissionsList } from "@fboes/aerofly-custom-missions";
import { AeroflyAircraft, AeroflyAircraftFinder } from "../../data/AeroflyAircraft.js";
import { DateYielder } from "../general/DateYielder.js";
import { Configuration } from "./Configuration.js";
import { Scenario } from "./Scenario.js";
import { Feature, FeatureCollection, LineString, Point } from "@fboes/geojson";
import { OpenStreetMapApi, OpenStreetMapApiAirport } from "../general/OpenStreetMapApi.js";
import { Markdown } from "../general/Markdown.js";
import { LocalTime } from "../general/LocalTime.js";
import { Formatter } from "../general/Formatter.js";
export class AeroflyAirRace {
configuration: Configuration;
airport: OpenStreetMapApiAirport | null = null;
aircraft: AeroflyAircraft | null = null;
scenarios: Scenario[] = [];
nauticalTimezone: number = 0;
constructor(configuration: Configuration) {
this.configuration = configuration;
}
static async init(configuration: Configuration): Promise<AeroflyAirRace> {
const airport = await OpenStreetMapApi.search(configuration.startingLocation);
if (!airport.length) {
throw new Error("No location information from API");
}
const self = new AeroflyAirRace(configuration);
self.airport = new OpenStreetMapApiAirport(airport[0]);
self.nauticalTimezone = Math.round((self.airport.lon ?? 0) / 15);
self.aircraft = AeroflyAircraftFinder.get(configuration.aircraft);
const dateYielder = new DateYielder(self.configuration.numberOfMissions, self.nauticalTimezone);
const dates = dateYielder.entries();
let index = 0;
for (const date of dates) {
try {
const scenario = await Scenario.init(self.configuration, self.aircraft, self.airport, date, index++);
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;
}
buildCustomMissionTmc(): string {
return new AeroflyMissionsList(
this.scenarios.map((s) => {
return s.mission;
}),
).toString();
}
buildReadmeMarkdown(): string {
if (!this.airport || !this.aircraft) {
return "";
}
const markdownTable = Markdown.table([
[`No `, `Local date¹`, `Local time¹`, ` Wind`, `Clouds`, `Thermal`, `Duration`, `Flight distance`],
[`:-:`, `-----------`, `----------:`, `-----------:`, `------`, `-------`, `-------:`, `--------------:`],
...this.scenarios.map((s, index): string[] => {
const localNauticalTime = new LocalTime(s.date, this.nauticalTimezone);
const conditions = s.mission.conditions;
const wind = Formatter.getWindDescription(conditions);
const clouds = Formatter.getCloudsDescription(conditions);
const thermalStrength = Formatter.getThermalStrengthDescription(conditions);
return [
`#${String(index + 1).padStart(2, "0")}`,
localNauticalTime.toDateString(),
localNauticalTime.toTimeString(),
wind,
clouds,
thermalStrength,
`${Math.ceil((s.mission.duration ?? 0) / 60)} min`,
`${Math.ceil((s.mission.distance ?? 0) / 1000)} km`,
];
}),
]);
const localTime = new LocalTime(new Date(), this.nauticalTimezone);
return `\
# Landegerät: Air Racing at ${this.airport.name}
This file contains ${this.configuration.numberOfMissions} air racing missions for the ${this.aircraft.nameFull} starting at [${this.airport.name}](https://skyvector.com/airport/${encodeURIComponent(this.airport.icaoId ?? "")}).
- See [the installation instructions](https://fboes.github.io/aerofly-missions/docs/generic-installation.html) on how to import [the missions into Aerofly FS 4](missions/custom_missions_user.tmc) and all other files.
- See [the Aerofly FS 4 manual on challenges / missions](https://www.aerofly.com/tutorials/missions/) on how to access these missions in Aerofly FS 4.
## Included missions
There are ${this.configuration.numberOfMissions} missions included in this [custom missions file](missions/custom_missions_user.tmc).
${markdownTable}
¹) Local [nautical time](https://en.wikipedia.org/wiki/Nautical_time) with UTC${localTime.timeZone} (${localTime.nauticalZoneId})
---
Created with [Aerofly Landegerät](https://github.com/fboes/aerofly-patterns)
`;
}
buildGeoJson(): string {
const geoJson = new FeatureCollection();
const scenario = this.scenarios.at(0);
if (scenario == undefined) {
return "";
}
const colors = ["#FF1493", "#C2E812", "#91F5AD", "#F96900", "#3B429F"];
this.scenarios.forEach((scenario, scenarioIndex) => {
const opacity = scenarioIndex === 0 ? 1 : 0.33;
const lastCp = scenario.mission.checkpoints.at(-1);
if (scenarioIndex === 0) {
scenario.mission.checkpoints.forEach((cp, index) => {
geoJson.addFeature(
new Feature(new Point(cp.longitude, cp.latitude, cp.altitude), {
id: scenarioIndex * 100 + index,
title: cp.name,
desription: scenario.mission.title,
"marker-symbol": index === 0 ? (scenarioIndex + 1).toString() : cp === lastCp ? "racetrack" : "triangle",
"fill-opacity": opacity,
}),
);
});
} else if (lastCp) {
geoJson.addFeature(
new Feature(new Point(lastCp.longitude, lastCp.latitude, lastCp.altitude), {
id: scenarioIndex * 100 + 1,
title: lastCp.name,
desription: scenario.mission.title,
"marker-symbol": "racetrack",
"fill-opacity": opacity,
}),
);
}
geoJson.addFeature(
new Feature(
new LineString(
scenario.mission.checkpoints.map((cp): Point => {
return new Point(cp.longitude, cp.latitude, cp.altitude);
}),
),
{
id: scenarioIndex * 100,
title: scenario.mission.title,
description: `${scenario.mission.description.replace(/\n/g, " \n")} The flight distance is ${Math.ceil((scenario.mission.distance ?? 0) / 1000)} km.`,
stroke: colors[scenarioIndex % colors.length],
"stroke-opacity": opacity,
},
),
);
});
return JSON.stringify(geoJson, null, 2);
}
}