UNPKG

@fboes/aerofly-patterns

Version:

Landegerät - Create random custom missions for Aerofly FS 4.

253 lines (220 loc) 9.01 kB
import { Feature, FeatureCollection, LineString, Point } from "@fboes/geojson"; import { AeroflyAircraft, AeroflyAircraftFinder } from "../../data/AeroflyAircraft.js"; import { AviationWeatherApi } from "../general/AviationWeatherApi.js"; import { DateYielder } from "../general/DateYielder.js"; import { Configuration } from "./Configuration.js"; import { Scenario } from "./Scenario.js"; import { LocalTime } from "../general/LocalTime.js"; import { AeroflyMissionsList } from "@fboes/aerofly-custom-missions"; import { Markdown } from "../general/Markdown.js"; import { Formatter } from "../general/Formatter.js"; import { HoldingPatternFix } from "./HoldingPatternFix.js"; import { Rand } from "../general/Rand.js"; export class AeroflyHolding { scenarios: Scenario[]; nauticalTimezone: number; aircraft: AeroflyAircraft; holdingFix: HoldingPatternFix | null = null; static async init(configuration: Configuration): Promise<AeroflyHolding> { const self = new AeroflyHolding(configuration); self.holdingFix = await self.getHoldingFix(self.configuration.navaidCode); self.nauticalTimezone = Math.round((self.holdingFix.position.longitude ?? 0) / 15); const initialBearing = Rand.getRandomArbitrary(0, 360); 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.holdingFix, self.configuration, self.aircraft, date, index++, initialBearing, ); 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; } private constructor(public readonly configuration: Configuration) { this.scenarios = []; /** * @type {number} a time zone which only considers the longitude, rounded to the full hour, in hours difference to UTC * @see https://en.wikipedia.org/wiki/Nautical_time */ this.nauticalTimezone = 0; /** * @type {AeroflyAircraft} additional aircraft information like name and technical properties */ this.aircraft = AeroflyAircraftFinder.get(this.configuration.aircraft); } buildCustomMissionTmc(): string { return new AeroflyMissionsList( this.scenarios.map((s) => { return s.mission; }), ).toString(); } buildReadmeMarkdown(): string { if (!this.holdingFix || !this.aircraft) { return ""; } const localTime = new LocalTime(new Date(), this.nauticalTimezone); const markdownTable = Markdown.table([ [ `No `, `Local date¹`, `Local time¹`, ` Wind`, `Clouds`, `Visibility`, `Radial`, `Area`, `DME`, `Turn`, `Altitude`, ], [ `:-:`, `-----------`, `----------:`, `-----------:`, `------`, `---------:`, `-----:`, `----`, `---:`, `:--:`, `-------:`, ], ...this.scenarios.map((s, index): string[] => { const conditions = s.mission.conditions; const localNauticalTime = new LocalTime(conditions.time, this.nauticalTimezone); const wind = Formatter.getWindDescription(conditions); const clouds = Formatter.getCloudsDescription(conditions); return [ `#${String(index + 1).padStart(2, "0")}`, localNauticalTime.toDateString(), localNauticalTime.toTimeString(), wind, clouds, Math.round(Math.min(conditions.visibility_sm, 10)) + " SM", s.pattern.inboundHeading.toFixed(0).padStart(3, "0") + "°", Formatter.getDirection(s.pattern.holdingAreaDirection) + (s.pattern.dmeHoldingAwayFromNavaid ? "²" : ""), s.pattern.dmeDistanceNm > 0 ? `${s.pattern.dmeDistanceNm} NM` : "—", s.pattern.isLeftTurn ? "L" : "R", s.pattern.patternAltitudeFt.toLocaleString("en") + " ft", ]; }), ]); return `\ # Landegerät: Holding at ${this.holdingFix.name} This file contains ${this.configuration.numberOfMissions} holding procedure lessons for the ${this.aircraft.nameFull} starting in the vicinity of [${this.holdingFix.fullName})](https://skyvector.com/?ll=${encodeURIComponent(this.holdingFix.position.latitude.toString() + "," + this.holdingFix.position.longitude.toString())}5&chart=301&zoom=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](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}) - ²) DME procedure is holding _away from_ the ${this.holdingFix.name}, instead of _towards_ it. --- 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) { throw new Error("No scenario available to build GeoJSON"); } if (this.holdingFix == null) { throw new Error("No holding fix available to build GeoJSON"); } geoJson.addFeature( new Feature(this.holdingFix.position, { id: 0, title: this.holdingFix.id, desription: this.holdingFix.name, "marker-symbol": "communications-tower", }), ); const lastCp = scenario.mission.checkpoints.at(-1); scenario.mission.checkpoints.forEach((cp, index) => { geoJson.addFeature( new Feature(new Point(cp.longitude, cp.latitude, cp.altitude), { id: index + 1, title: cp.name, "marker-symbol": cp === lastCp ? "racetrack" : "triangle", }), ); }); const colors = ["#FF1493", "#C2E812", "#91F5AD", "#F96900", "#3B429F"]; this.scenarios.forEach((scenario, index) => { geoJson.addFeature( new Feature( new LineString([ new Point(scenario.mission.origin.longitude, scenario.mission.origin.latitude, scenario.mission.origin.alt), ...scenario.mission.checkpoints.map((cp): Point => { return new Point(cp.longitude, cp.latitude, cp.altitude); }), ]), { id: index * 10, title: scenario.mission.title, description: scenario.mission.description, stroke: colors[index % colors.length], "stroke-opacity": index === 0 ? 1 : 0.3, isLeftTurn: scenario.pattern.isLeftTurn, holdingAreaDirection: scenario.pattern.holdingAreaDirection, inboundHeading: scenario.pattern.inboundHeading, magDec: scenario.holdingNavAid.mag_dec, dmeDistanceNm: scenario.pattern.dmeDistanceNm, dmeDistanceOutboundNm: scenario.pattern.dmeDistanceOutboundNm, patternAltitudeFt: scenario.pattern.patternAltitudeFt, patternSpeedKts: scenario.pattern.patternSpeedKts, legTimeMin: scenario.pattern.legTimeMin, patternEntry: scenario.patternEntry, }, ), ); geoJson.addFeature( new Feature( new Point(scenario.mission.origin.longitude, scenario.mission.origin.latitude, scenario.mission.origin.alt), { title: scenario.aircraft.icaoCode, description: scenario.aircraft.nameFull, id: index * 10 + 1, "marker-symbol": (index + 1).toString(), }, ), ); }); return JSON.stringify(geoJson, null, 2); } async getHoldingFix(navaidCode: string): Promise<HoldingPatternFix> { const holdingFixId = HoldingPatternFix.fromId(navaidCode); if (holdingFixId) { return holdingFixId; } if (navaidCode.length <= 3) { const holdingFix = await AviationWeatherApi.fetchNavaids([navaidCode]); if (!holdingFix.length) { throw new Error("No holding fix information from API"); } return HoldingPatternFix.fromNavaid(holdingFix[0]); } else { const holdingFix = await AviationWeatherApi.fetchFix([navaidCode]); if (!holdingFix.length) { throw new Error("No holding fix information from API"); } return HoldingPatternFix.fromFix(holdingFix[0]); } } }