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.

192 lines (169 loc) 6.25 kB
import { Mission } from "../Aerofly/Mission.js"; import { MissionCheckpoint, MissionCheckpointTypeExtended } from "../Aerofly/MissionCheckpoint.js"; import { asciify } from "../Cli/Arguments.js"; import { Quote } from "../Export/Quote.js"; export type GarminFplWaypointType = "AIRPORT" | "USER WAYPOINT" | "NDB" | "VOR" | "INT" | "INT-VRP"; export type GaminFplWaypoint = { identifier: string; type: GarminFplWaypointType; lat: number; lon: number; /** * Elevation (in meters) of the waypoint. */ elevationMeter?: number; /** * The country code should be the empty string for user waypoints. */ countryCode?: string; }; export class GarminFpl { waypoints: GaminFplWaypoint[] = []; /** * In feet MSL */ cruisingAltFt?: number; departureRunway?: string; destinationRunway?: string; constructor(configFileContent: string) { this.read(configFileContent); } /** * @see https://www8.garmin.com/xmlschemas/FlightPlanv1.xsd * @param configFileContent */ read(configFileContent: string): void { this.cruisingAltFt = undefined; // Get waypoint definitions const waypointDefinitions: Map<string, GaminFplWaypoint> = new Map(); const waypointTableXml = this.getXmlNode(configFileContent, "waypoint-table") || this.getXmlNode(configFileContent, "waypoints"); this.getXmlNodes(waypointTableXml, "waypoint").forEach((xml) => { const elevation = this.getXmlNode(xml, "elevation"); waypointDefinitions.set(this.getXmlNode(xml, "identifier"), { identifier: this.getXmlNode(xml, "identifier"), type: <GarminFplWaypointType>this.getXmlNode(xml, "type"), lat: Number(this.getXmlNode(xml, "lat")), lon: Number(this.getXmlNode(xml, "lon")), elevationMeter: elevation ? Number(elevation) : undefined, countryCode: this.getXmlNode(xml, "country-code") || undefined, }); }); // Always fetch first route const routeTableXml = this.getXmlNode(configFileContent, "route"); this.waypoints = this.getXmlNodes(routeTableXml, "route-point").map((xml): GaminFplWaypoint => { const waypointDefinition = waypointDefinitions.get(this.getXmlNode(xml, "waypoint-identifier")); if (waypointDefinition === undefined) { throw new Error("Missing waypoint definition for route point"); } return waypointDefinition; }); } protected getXmlNode(xml: string, tag: string): string { const match = xml.match(new RegExp(`<${tag}[^>]*>(.*?)</${tag}>`, "ms")); return match ? Quote.unXml(match[1]) : ""; } protected getXmlNodes(xml: string, tag: string): string[] { const nodes = xml.match(new RegExp(`<${tag}.*?</${tag}>`, "gms")); return nodes ? nodes : []; } protected getXmlAttribute(xml: string, attribute: string): string { const regex = new RegExp(` ${attribute}="(.*?)"`, "ms"); const match = xml.match(regex); return match ? Quote.unXml(match[1]) : ""; } } export abstract class GarminExportAbstract { constructor(protected mission: Mission) {} abstract toString(): string; } /** * @see https://www8.garmin.com/xmlschemas/FlightPlanv1.xsd */ export class GarminExport extends GarminExportAbstract { toString(): string { const routePoints = this.mission.checkpoints.map((cp): GaminFplWaypoint => { return { identifier: cp.name, type: this.convertWaypointType(cp.type_extended), lat: cp.lon_lat.lat, lon: cp.lon_lat.lon, elevationMeter: cp.lon_lat.altitude_m, countryCode: cp.icao_region ?? undefined, }; }); const routeName = asciify(this.mission.title) .toUpperCase() .replace(/_/g, " ") .replace(/[^A-Z0-9 ]+/g, "") .substring(0, 25); const pln = `\ <?xml version="1.0" encoding="utf-8"?> <flight-plan xmlns="http://www8.garmin.com/xmlschemas/FlightPlan/v1" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://www8.garmin.com/xmlschemas/FlightPlan/v1 https://www8.garmin.com/xmlschemas/FlightPlanv1.xsd"> <waypoint-table> ${this.geWaypointXml(routePoints)} </waypoint-table> <route> <route-name>${Quote.xml(routeName)}</route-name> <route-description>${Quote.xml(this.mission.description)}</route-description> <flight-plan-index>1</flight-plan-index> ${this.getRouteXml(routePoints)} </route> </flight-plan> `; return pln; } /** * @param routePoints * @returns An unordered list of unique waypoints referenced by a flight plan. This table may also contain waypoints not referenced by the route of a flight plan. */ protected geWaypointXml(routePoints: GaminFplWaypoint[]): string { const waypoints = routePoints.map((rp): string => { const elevation = rp.elevationMeter ? ` <elevation>${Quote.xml(rp.elevationMeter.toString())}</elevation> ` : ``; return `\ <waypoint> <identifier>${Quote.xml(rp.identifier)}</identifier> <type>${Quote.xml(rp.type)}</type> <country-code>${Quote.xml(rp.countryCode || "")}</country-code> <lat>${Quote.xml(rp.lat.toString())}</lat> <lon>${Quote.xml(rp.lon.toString())}</lon> <comment /> ${elevation}\ </waypoint>`; }); return [...new Set(waypoints)].join("\n"); } protected getRouteXml(routePoints: GaminFplWaypoint[]): string { return routePoints .map((rp): string => { return `\ <route-point> <waypoint-identifier>${Quote.xml(rp.identifier)}</waypoint-identifier> <waypoint-type>${Quote.xml(rp.type)}</waypoint-type> <waypoint-country-code>${Quote.xml(rp.countryCode ?? "")}</waypoint-country-code> </route-point>`; }) .join("\n"); } convertWaypointType(type: MissionCheckpointTypeExtended): GarminFplWaypointType { switch (type) { case MissionCheckpoint.TYPE_AIRPORT: case MissionCheckpoint.TYPE_DESTINATION: case MissionCheckpoint.TYPE_ORIGIN: return "AIRPORT"; case MissionCheckpoint.TYPE_INTERSECTION: return "INT"; case MissionCheckpoint.TYPE_NDB: return "NDB"; case MissionCheckpoint.TYPE_VOR: return "VOR"; default: return "USER WAYPOINT"; } } }