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
text/typescript
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";
}
}
}