UNPKG

@fboes/aerofly-patterns

Version:

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

230 lines (216 loc) 10.1 kB
import { Airport } from "./Airport.js"; import { AviationWeatherApi } from "../general/AviationWeatherApi.js"; import { FeatureCollection, Feature, LineString } 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 class AeroflyPatterns { constructor(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) { 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() { 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() { /** * @type {AeroflyMission[]} */ const missions = 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() { 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) `; } }