UNPKG

aerofly-missions

Version:

The Aerofly Missionsgerät converts simulator flight plan files for Aerofly FS 4, Microsoft Flight Simulator, X-Plane, GeoFS, and Garmin / Infinite Flight flight plan files. It also imports SimBrief flight plans.

331 lines (303 loc) 11.4 kB
import { MissionCheckpoint, MissionCheckpointTypeExtended } from "../Aerofly/MissionCheckpoint.js"; import { Quote } from "../Export/Quote.js"; import { LonLat } from "../World/LonLat.js"; import { Units } from "../World/Units.js"; import { GaminFplWaypoint, GarminExportAbstract, GarminFpl, GarminFplWaypointType } from "./GarminFpl.js"; type MsfsPlnWaypointType = "none" | "Airport" | "Intersection" | "VOR" | "NDB" | "User" | "ATC"; type MsfsPlnRunwayDesignator = "NONE" | "CENTER" | "LEFT" | "RIGHT" | "WATER" | "A" | "B"; /** * @see https://docs.flightsimulator.com/html/Content_Configuration/Flights_And_Missions/Flight_Plan_Definitions.htm * @see https://docs.flightsimulator.com/msfs2024/html/5_Content_Configuration/Mission_XML_Files/EFB_Flight_Plan_XML_Properties.htm */ export class MsfsPln extends GarminFpl { read(configFileContent: string): void { const waypointTableXml = this.getXmlNode(configFileContent, "FlightPlan.FlightPlan"); const versionId = Number(this.getXmlNode(waypointTableXml, "AppVersionMajor")); if (versionId <= 0 || versionId > 12) { throw Error("Unknown flight plan version ID"); } this.cruisingAltFt = Number(this.getXmlNode(waypointTableXml, "CruisingAlt")); const waypointsXml = this.getXmlNodes(waypointTableXml, "ATCWaypoint"); this.waypoints = waypointsXml.map((xml, index): GaminFplWaypoint => { // N52° 45' 7.51",W3° 53' 2.16",+002500.00 const worldPosition = this.getXmlNode(xml, "WorldPosition"); const coords = this.convertCoordinate(worldPosition); if (index === 0 || index === waypointsXml.length - 1) { const runwayNumberFP = this.getXmlNode(xml, "RunwayNumberFP"); const runwayDesignatorFP = this.getXmlNode(xml, "RunwayDesignatorFP") as MsfsPlnRunwayDesignator | ""; const rw = runwayNumberFP + (runwayDesignatorFP === "NONE" ? "" : runwayDesignatorFP.substring(0, 1)); if (runwayNumberFP) { if (index === 0) { this.departureRunway = rw; } else { this.destinationRunway = rw; } } } return { identifier: this.getXmlNode(xml, "ICAOIdent") || this.getXmlAttribute(xml, "id"), type: this.convertWaypointType(this.getXmlNode(xml, "ATCWaypointType") as MsfsPlnWaypointType), lat: coords.lat, lon: coords.lon, elevationMeter: coords.altitude_ft / Units.feetPerMeter, countryCode: this.getXmlNode(xml, "ICAORegion") || undefined, }; }); } convertWaypointType(type: MsfsPlnWaypointType): GarminFplWaypointType { switch (type) { case "Airport": return "AIRPORT"; case "Intersection": return "INT"; case "NDB": return "NDB"; case "VOR": return "VOR"; default: return "USER WAYPOINT"; } } convertCoordinate(coordinate: string): LonLat { const parts = coordinate.split(/,\s*/); if (parts.length < 2) { throw new Error( `Wrong coordinates format "${coordinate}", expexted something like N52° 45' 7.51",W3° 53' 2.16",+002500.00` ); } const numbers = parts.map((p): number => { const m = p.match(/([NSEW])(\d+)\D+(\d+)\D+([0-9.]+)/); if (m) { let b = Number(m[2]); // degree b += Number(m[3]) / 60; // minutes b += Number(m[4]) / 3600; // seconds return m[1] === "S" || m[1] === "W" ? -b : b; } return 0; }); return new LonLat(numbers[1], numbers[0], Number(parts[2] || 0) / Units.feetPerMeter); } } /** * @see https://docs.flightsimulator.com/html/Content_Configuration/Flights_And_Missions/Flight_Plan_Definitions.htm * @see https://docs.flightsimulator.com/msfs2024/html/5_Content_Configuration/Mission_XML_Files/Flight_Plan_XML_Properties.htm */ export class MsfsPlnExport extends GarminExportAbstract { getLla(lon_lat: LonLat): string { // N53° 14' 48.24",W4° 32' 7.71",+000025.00 // N53° 14' 48.24",W4° 32' 7.71",+000025.00 const lat = lon_lat.latMinute; const lon = lon_lat.lonMinute; return ( lon_lat.latHemisphere + Math.abs(lat.degree).toFixed() + "° " + lat.minutes.toFixed() + "' " + lat.secondsDecimal.toFixed(2) + '"' + "," + lon_lat.lonHemisphere + Math.abs(lon.degree).toFixed() + "° " + lon.minutes.toFixed() + "' " + lon.secondsDecimal.toFixed(2) + '"' + "," + (lon_lat.altitude_m >= 0 ? "+" : "-") + Math.abs(lon_lat.altitude_ft).toFixed(2).padStart(9, "0") ); } toString(): string { const m = this.mission; const departureRunwayCp = m.findCheckPointByType(MissionCheckpoint.TYPE_DEPARTURE_RUNWAY); const departureRunway = departureRunwayCp ? departureRunwayCp.name : ""; const destinationRunwayCp = m.findCheckPointByType(MissionCheckpoint.TYPE_DESTINATION_RUNWAY); const destinationRunway = destinationRunwayCp ? destinationRunwayCp.name : ""; let pln = `\ <?xml version="1.0" encoding="UTF-8"?> <SimBase.Document Type="AceXML" version="1,0"> <!-- Exported by Aerofly Missionsgerät --> <Descr>AceXML Document</Descr> <FlightPlan.FlightPlan> <Title>${Quote.xml(m.origin_icao + " to " + m.destination_icao)}</Title> <Descr>${Quote.xml(m.title)}</Descr> <FPType>${Quote.xml(m.conditions.getFlightCategory(true))}</FPType> <RouteType>Direct</RouteType> <CruisingAlt>${Quote.xml(m.cruise_altitude_ft.toFixed())}</CruisingAlt> <DepartureID>${Quote.xml(m.origin_icao)}</DepartureID> <DepartureName>${Quote.xml(m.origin_icao)}</DepartureName> <DepartureLLA>${this.getLla(m.origin_lon_lat)}</DepartureLLA> <!--DeparturePosition></DeparturePosition--> <DestinationID>${Quote.xml(m.destination_icao)}</DestinationID> <DestinationName>${Quote.xml(m.destination_icao)}</DestinationName> <DestinationLLA>${this.getLla(m.destination_lon_lat)}</DestinationLLA> <AppVersion> <AppVersionMajor>10</AppVersionMajor> <AppVersionBuild>61472</AppVersionBuild> </AppVersion> `; m.checkpoints.forEach((cp) => { const type = this.convertWaypointType(cp.type_extended); let name = cp.name; if ( (cp.type === MissionCheckpoint.TYPE_DEPARTURE_RUNWAY || cp.type === MissionCheckpoint.TYPE_DESTINATION_RUNWAY) && !name.match(/^RW/) ) { name = "RW" + name; } pln += `\ <ATCWaypoint id="${Quote.xml(name)}"> <ATCWaypointType>${Quote.xml(type)}</ATCWaypointType> <WorldPosition>${this.getLla(cp.lon_lat)}</WorldPosition> <!--SpeedMaxFP>${Quote.xml((cp.speed ? cp.speed : -1).toFixed())}</SpeedMaxFP--> `; if (cp.type === MissionCheckpoint.TYPE_ORIGIN && departureRunway) { pln += this.runwayXml(departureRunway); } else if (cp.type === MissionCheckpoint.TYPE_DESTINATION && destinationRunway) { pln += this.runwayXml(destinationRunway); } if (type !== "User") { pln += `\ <ICAO> <ICAOIdent>${Quote.xml(cp.name)}</ICAOIdent> <ICAORegion>${Quote.xml(cp.icao_region || "")}</ICAORegion> </ICAO> `; } pln += `\ </ATCWaypoint> `; }); pln += `\ </FlightPlan.FlightPlan> </SimBase.Document> `; return pln; } convertWaypointType(type: MissionCheckpointTypeExtended): MsfsPlnWaypointType { switch (type) { case MissionCheckpoint.TYPE_AIRPORT: return "Airport"; case MissionCheckpoint.TYPE_DESTINATION: return "Airport"; case MissionCheckpoint.TYPE_INTERSECTION: return "Intersection"; case MissionCheckpoint.TYPE_NDB: return "NDB"; case MissionCheckpoint.TYPE_ORIGIN: return "Airport"; case MissionCheckpoint.TYPE_VOR: return "VOR"; default: return "User"; } } runwayXml(runway: string, hideNoneDesignator = false): string { const runwayParts = runway.match(/(\d+)([LRCSGHUW])?/); if (runwayParts) { let RunwayDesignatorFP: MsfsPlnRunwayDesignator = "NONE"; switch (runwayParts[2]) { case "L": RunwayDesignatorFP = "LEFT"; break; case "R": RunwayDesignatorFP = "RIGHT"; break; case "C": RunwayDesignatorFP = "CENTER"; break; case "W": RunwayDesignatorFP = "WATER"; break; } let rw = `\ <RunwayNumberFP>${Number(runwayParts[1])}</RunwayNumberFP> `; if (!hideNoneDesignator || RunwayDesignatorFP !== "NONE") { rw += ` <RunwayDesignatorFP>${Quote.xml(RunwayDesignatorFP)}</RunwayDesignatorFP> `; } return rw; } return ""; } } /** * @see https://docs.flightsimulator.com/msfs2024/html/5_Content_Configuration/Mission_XML_Files/EFB_Flight_Plan_XML_Properties.htm */ export class Msfs2024Export extends MsfsPlnExport { toString(): string { const m = this.mission; const departureRunwayCp = m.findCheckPointByType(MissionCheckpoint.TYPE_DEPARTURE_RUNWAY); const departureRunway = departureRunwayCp ? departureRunwayCp.name : ""; const destinationRunwayCp = m.findCheckPointByType(MissionCheckpoint.TYPE_DESTINATION_RUNWAY); const destinationRunway = destinationRunwayCp ? destinationRunwayCp.name : ""; let pln = `\ <?xml version="1.0" encoding="UTF-8"?> <SimBase.Document> <!-- Exported by Aerofly Missionsgerät --> <FlightPlan.FlightPlan> <Title>${Quote.xml(m.origin_icao + " to " + m.destination_icao)}</Title> <Descr>${Quote.xml(m.title)}</Descr> <FPType>${Quote.xml(m.conditions.getFlightCategory(true))}</FPType> <DepartureID>${Quote.xml(m.origin_icao)}</DepartureID> <DestinationID>${Quote.xml(m.destination_icao)}</DestinationID> <CruisingAlt>${Quote.xml(m.cruise_altitude_ft.toFixed())}</CruisingAlt> <AppVersion> <AppVersionMajor>12</AppVersionMajor> </AppVersion> `; if (departureRunway) { pln += `\ <DepartureDetails> ${this.runwayXml(departureRunway, true)}\ </DepartureDetails> `; } m.checkpoints .filter((cp) => { return ![ MissionCheckpoint.TYPE_ORIGIN, MissionCheckpoint.TYPE_DEPARTURE_RUNWAY, MissionCheckpoint.TYPE_DESTINATION_RUNWAY, MissionCheckpoint.TYPE_DESTINATION, ].includes(cp.type); }) .forEach((cp) => { // ARINC-424 ICAO region code const type = this.convertWaypointType(cp.type_extended); pln += `\ <ATCWaypoint id="${Quote.xml(cp.name)}"> <ATCWaypointType>${Quote.xml(type)}</ATCWaypointType> <ICAO> `; if (type !== "User") { pln += `\ <ICAORegion>${Quote.xml(cp.icao_region || "")}</ICAORegion> <ICAOIdent>${Quote.xml(cp.name)}</ICAOIdent> `; } pln += `\ <WorldLocation>${this.getLla(cp.lon_lat)}</WorldLocation> </ICAO> </ATCWaypoint> `; }); if (destinationRunway) { pln += `\ <ArrivalDetails> ${this.runwayXml(destinationRunway, true)}\ </ArrivalDetails> `; } pln += `\ </FlightPlan.FlightPlan> </SimBase.Document> `; return pln; } }