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.
283 lines (282 loc) • 13.3 kB
JavaScript
import { MissionCheckpoint } from "../Aerofly/MissionCheckpoint.js";
import { Outputtable } from "../Export/Outputtable.js";
import { Quote } from "../Export/Quote.js";
import { SkyVector } from "../Export/SkyVector.js";
import { LonLatArea } from "../World/LonLat.js";
import { LonLatDate } from "../World/LonLatDate.js";
class ComponentsOutputtable extends HTMLElement {
constructor() {
super();
this.elements = {
table: document.createElement("table"),
caption: document.createElement("caption"),
thead: document.createElement("thead"),
tbody: document.createElement("tbody"),
};
this.elements.table.appendChild(this.elements.caption);
this.elements.table.appendChild(this.elements.thead);
this.elements.table.appendChild(this.elements.tbody);
this.appendChild(this.elements.table);
}
draw() {
this.elements.table.classList.toggle("empty", !this.mission);
if (!this.mission) {
this.elements.tbody.innerHTML = "";
}
}
/**
* @param fields Table cell contents
* @param join `td` or `th`; to supress a `th` at the beginnining of a `tr`
* with `td`s set it to `ttd`, which will be converted to `td`
* @returns string
*/
outputLine(fields, join = "td") {
const tag = join === "ttd" ? "td" : join;
return join === "td"
? `<tr><th scope="row">` +
fields[0] +
`</th><${tag}>` +
fields.slice(1).join(`</${tag}><${tag}>`) +
`</${tag}></tr>`
: `<tr><${tag}>` + fields.join(`</${tag}><${tag}>`) + `</${tag}></tr>`;
}
outputDateTime(date) {
return date.toISOString().replace(/:\d+\.\d+/, "");
}
outputSunState(sunState) {
const deg = (sunState.solarElevationAngleDeg / 6) * 5;
const degX = Math.max(0, Math.min(18, 12 - deg));
return `<svg width="20" height="20" version="1.1" viewBox="0 0 20 20" xmlns="http://www.w3.org/2000/svg">
<title>${deg.toFixed()}°</title>
<style>
rect, circle, line { stroke-width: 1px; stroke: currentColor; fill: none; stroke-linecap: round; stroke-linejoin: round; }
</style>
<rect x="1" y="1" width="18" height="18" />
<circle cx="10" cy="10" r="3" />
<line x1="3" y1="10" x2="5" y2="10" />
<line x1="3" y1="10" x2="5" y2="10" transform="rotate(45, 10, 10)" />
<line x1="3" y1="10" x2="5" y2="10" transform="rotate(90, 10, 10)" />
<line x1="3" y1="10" x2="5" y2="10" transform="rotate(135, 10, 10)" />
<line x1="3" y1="10" x2="5" y2="10" transform="rotate(180, 10, 10)" />
<line x1="3" y1="10" x2="5" y2="10" transform="rotate(225, 10, 10)" />
<line x1="3" y1="10" x2="5" y2="10" transform="rotate(270, 10, 10)" />
<line x1="3" y1="10" x2="5" y2="10" transform="rotate(315, 10, 10)" />
<rect x="1" y="${19 - degX}" width="18" height="${degX}" style="fill: currentColor" />
</svg> ${Quote.html(sunState.sunState)}`;
}
outputCover(cloud) {
const octas = Math.round(cloud.cover * 8);
let svg = `<svg width="20" height="20" version="1.1" viewBox="0 0 20 20" xmlns="http://www.w3.org/2000/svg">
<style>
circle, line, path { stroke-width: 1px; stroke: currentColor; fill: none; stroke-linecap: round; stroke-linejoin: round; }
path { fill: currentColor; stroke: none; }
</style>
<title>${octas}/8</title>
<circle cx="10" cy="10" r="9" />`;
const middle = octas === 7 ? 0.5 : 0;
if (octas > 0) {
if (octas === 1 || octas === 3) {
svg += '<line x1="10" y1="1" x2="10" y2="19" />';
}
if (octas === 5) {
svg += '<line x1="1" y1="10" x2="19" y2="10" />';
}
if (octas >= 2) {
svg += `<path d="m ${10 + middle},1 a ${9 - middle},${9 - middle} 0 0 1 9,9 h -9 z" />`;
}
if (octas >= 4) {
svg += `<path d="m ${10 + middle},1 a ${9 - middle},${9 - middle} 0 0 1 9,9 h -9 z" transform="scale(1,-1) translate(0,-20)" />`;
}
if (octas >= 6) {
svg += `<path d="m ${10 + middle},1 a ${9 - middle},${9 - middle} 0 0 1 9,9 h -9 z" transform="scale(-1,-1) translate(-20,-20)" />`;
}
if (octas >= 7) {
svg += `<path d="m ${10 + middle},1 a ${9 - middle},${9 - middle} 0 0 1 9,9 h -9 z" transform="scale(-1,1) translate(-20, 0)" />`;
}
}
svg += "</svg>";
return svg;
}
getWind(conditions) {
let wind_speed = conditions.wind_speed.toFixed();
const gust_type = conditions.wind_gusts_type;
if (gust_type) {
wind_speed += "G" + conditions.wind_gusts.toFixed();
}
return Outputtable.padThree(conditions.wind_direction) + "° @ " + wind_speed;
}
}
export class ComponentsWeather extends ComponentsOutputtable {
constructor() {
super();
this.elements.caption.innerText = "Weather";
this.elements.thead.innerHTML = this.outputLine(["Wind ", "Clouds", "Visibility", "Min flight rules"], "th");
this.draw();
}
draw() {
super.draw();
const m = this.mission;
if (!m) {
return;
}
let html = "";
html += this.outputLine([
Quote.html(this.getWind(m.conditions)) + " kts",
this.outputCover(m.conditions.cloud) +
" " +
Quote.html(m.conditions.cloud.cover_code) +
" @ " +
m.conditions.cloud.height_feet.toLocaleString("en") +
" ft",
m.conditions.visibility.toLocaleString("en") +
" m / " +
Math.round(m.conditions.visibility_sm) +
" SM",
m.conditions.getFlightCategory(m.origin_country !== "US"),
], "ttd");
this.elements.tbody.innerHTML = html;
}
}
export class ComponentsAirports extends ComponentsOutputtable {
constructor() {
super();
this.elements.caption.innerText = "Airports";
this.elements.thead.innerHTML = this.outputLine(["Type", "Location ", "Country", "Date & time ", '<abbr title="Local solar time">LST</abbr>', " Sun"], "th");
this.draw();
}
draw() {
super.draw();
const m = this.mission;
if (!m) {
return;
}
const total_time_enroute = m.time_enroute;
const time = new Date(m.conditions.time.dateTime);
const sunStateOrigin = new LonLatDate(m.origin_lon_lat, time).sunState;
time.setSeconds(time.getSeconds() + total_time_enroute * 3600);
const sunStateDestination = new LonLatDate(m.destination_lon_lat, time).sunState;
let html = "";
html += this.outputLine([
"Departure",
m.origin_icao
? `<a target="airport" href="${SkyVector.airportLink(m.origin_icao)}">${Quote.html(m.origin_icao)}</a>`
: "",
m.origin_country,
this.outputDateTime(m.conditions.time.dateTime),
sunStateOrigin.localSolarTime,
this.outputSunState(sunStateOrigin),
]);
html += this.outputLine([
"Destination",
m.destination_icao
? `<a target="airport" href="${SkyVector.airportLink(m.destination_icao)}">${Quote.html(m.destination_icao)}</a>`
: "",
m.destination_country,
this.outputDateTime(time),
sunStateDestination.localSolarTime,
this.outputSunState(sunStateDestination),
]);
this.elements.tbody.innerHTML = html;
}
}
export class ComponentsCheckpoints extends ComponentsOutputtable {
constructor() {
super();
this.moreElements = {
tfoot: document.createElement("tfoot"),
p: document.createElement("p"),
};
// this.elements.table.appendChild(this.elements.caption);
// this.elements.table.appendChild(this.elements.thead);
// this.elements.table.appendChild(this.elements.tbody);
// this.appendChild(this.elements.table);
this.elements.table.appendChild(this.moreElements.tfoot);
this.appendChild(this.moreElements.p);
this.moreElements.p.className = "no-print";
this.elements.caption.innerText = "Checkpoints";
this.elements.thead.innerHTML = this.outputLine([
"#",
"Waypoint ",
'<abbr title="Frequency">FRQ</abbr>',
"Altitude",
'<abbr title="True Air Speed">TAS</abbr>',
'<abbr title="Desired track magnetic">DTK</abbr>',
'<abbr title="Heading magnetic">HDG</abbr> ',
"Distance",
'<abbr title="Estimated time enroute">ETE</abbr>',
], "th");
this.draw();
}
draw() {
super.draw();
const m = this.mission;
if (!m || !this.mission || m.checkpoints.length === 0) {
this.elements.table.classList.add("empty");
this.moreElements.tfoot.innerHTML = "";
this.moreElements.p.innerHTML = "";
return;
}
const s = new SkyVector(m);
const lonLatArea = new LonLatArea(this.mission.origin_lon_lat);
this.mission.checkpoints.forEach((c) => {
lonLatArea.push(c.lon_lat);
});
const zoomLevel = lonLatArea.getZoomLevel();
const center = lonLatArea.center;
{
let html = "";
m.checkpoints.forEach((c, i) => {
const isAirport = c.type === MissionCheckpoint.TYPE_ORIGIN ||
c.type === MissionCheckpoint.TYPE_DESTINATION ||
i == 0 ||
i == m.checkpoints.length - 1;
const isAirportOrRunway = isAirport ||
c.type === MissionCheckpoint.TYPE_DEPARTURE_RUNWAY ||
c.type === MissionCheckpoint.TYPE_DESTINATION_RUNWAY;
html += this.outputLine([
Outputtable.pad(i + 1, 2, 0, "0") + ".",
!isAirport
? `<input aria-label="Waypoint #${i + 1}" data-cp-id="${i}" data-cp-prop="name" type="text" value="${c.name}" pattern="[A-Z0-9._+\\-]+" maxlength="8" autocapitalize="characters" required="required" />`
: c.name,
`<input aria-label="Frequency #${i + 1}" data-cp-id="${i}" data-cp-prop="frequency_mhz" type="number" min="0.190" step="0.001" max="118" value="${c.frequency ? c.frequency_mhz : ""}" /> MHz`,
`<input aria-label="Altitude #${i + 1}" data-cp-id="${i}" data-cp-prop="altitude_ft" type="number" min="${!isAirportOrRunway ? -1000 : 0}" step="${!isAirportOrRunway ? 100 : 1}" value="${c.lon_lat.altitude_m ? Math.round(c.lon_lat.altitude_ft) : ""}" /> ft`,
i !== 0
? `<input aria-label="True Air Speed #${i + 1}" data-cp-id="${i}" data-cp-prop="speed" type="number" min="0" value="${c.speed >= 0 ? Math.round(c.speed) : ""}" /> kts`
: "",
i !== 0 ? Outputtable.padThree(c.direction_magnetic) + "°" : "",
i !== 0 ? '<span class="heading">' + Outputtable.padThree(c.heading_magnetic) + "</span>°" : "",
i !== 0
? `<span class="distance" title="${c.slope_deg.toFixed(1)}°">${Outputtable.pad(c.distance, 5, 1)}</span> NM`
: "",
i !== 0
? '<span class="time_enroute">' + Outputtable.convertHoursToMinutesString(c.time_enroute) + "</span>"
: "",
]);
});
this.elements.tbody.innerHTML = html;
}
{
let html = "";
html += this.outputLine([
"",
"Total",
"",
"",
"",
"",
"",
Outputtable.pad(m.distance, 4, 1) + " NM",
'<span class="time_enroute">' + Outputtable.convertHoursToMinutesString(m.time_enroute) + "</span>",
], "ttd");
this.moreElements.tfoot.innerHTML = html;
}
{
let html = "";
html += `<p class="no-print">Check your <a href="${s.toString(false)}" target="skyvector" title="${s
.getCheckpoints(false)
.join(" ")}">current flight plan on Sky Vector</a>.
You may also want to take a look at <a href="https://www.google.com/maps/@?api=1&map_action=map&center=${encodeURIComponent(center.lat)},${encodeURIComponent(center.lon)}&zoom=${encodeURIComponent(zoomLevel)}&basemap=terrain" target="gmap">Google Maps</a> / <a href="https://www.openstreetmap.org/#map=${encodeURIComponent(zoomLevel)}/${encodeURIComponent(center.lat)}/${encodeURIComponent(center.lon)}" target="osm">OpenStreetMap</a>.</p>`;
this.moreElements.p.innerHTML = html;
}
}
}