@fboes/aerofly-patterns
Version:
Landegerät - Create random custom missions for Aerofly FS 4.
228 lines (227 loc) • 8.66 kB
JavaScript
import { Vector, Point } from "@fboes/geojson";
import { Units } from "../../data/Units.js";
import { Degree } from "../general/Degree.js";
import { Airports } from "../../data/Airports.js";
import { Formatter } from "../general/Formatter.js";
import { AviationWeatherNormalizedAirport, } from "../general/AviationWeatherApi.js";
/**
* @type {AeroflyPatternsWaypointable}
*/
export class Airport {
constructor(airportApiData, configuration = null) {
const airportNormalized = new AviationWeatherNormalizedAirport(airportApiData);
this.id = airportNormalized.icaoId;
this.position = new Point(airportNormalized.lon, airportNormalized.lat, airportNormalized.elev);
this.name = airportNormalized.name;
// Remove municipality name if already present in airport name
const duplicateMatch = this.name.match(/^(.+?) \/ (.+)$/);
if (duplicateMatch && duplicateMatch[2].includes(duplicateMatch[1])) {
this.name = duplicateMatch[2];
}
this.runways = [];
airportNormalized.runways.map((r) => {
this.buildRunways(r, this.position, configuration).forEach((runway) => {
if (runway) {
this.runways.push(runway);
}
});
});
const airportDatabase = Airports[this.id] ?? null;
if (airportDatabase) {
airportDatabase.runways.forEach((r) => {
const matchingRunway = this.runways.find((rr) => {
return rr.id === r.id;
});
if (matchingRunway) {
if (!matchingRunway.isRightPattern && r.isRightPattern) {
matchingRunway.isRightPattern = r.isRightPattern;
}
if (!matchingRunway.ilsFrequency && r.ilsFrequency) {
matchingRunway.ilsFrequency = r.ilsFrequency;
}
if (!matchingRunway.isPreferred && r.isPreferred) {
matchingRunway.isPreferred = r.isPreferred;
}
}
});
}
/**
* @type {number} with "+" to the east and "-" to the west. Substracted to a true heading this will give the magnetic heading.
*/
this.magneticDeclination = airportNormalized.magdec;
/**
* @type {number} a tome zone which only considers the longitude, rounded to the full hour, in hours difference to UTC
* @see https://en.wikipedia.org/wiki/Nautical_time
*/
this.nauticalTimezone = Math.round(this.position.longitude / 15);
this.hasTower = airportNormalized.tower;
this.hasBeacon = airportNormalized.beacon;
const lclP = airportNormalized.freqs.find((f) => {
return f.type === "LCL/P";
});
this.localFrequency = lclP?.freq ?? null;
this.navaids = [];
/**
* @type {string} Local description
*/
this.radioDescription = this.localFrequency
? "- Local tower / CTAF frequency: " + this.localFrequency.toFixed(2)
: "";
/**
* @type {string} Local description
*/
this.navAidDescription = "";
}
/**
*
* @param {AviationWeatherApiNavaid[]} navaids
*/
setNavaids(navaids) {
this.navaids = navaids.map((n) => {
return new AirportNavaid(n);
});
if (this.navaids.length) {
this.navAidDescription =
"- Local navigational aids: " +
this.navaids
.map((n) => {
return `${n.type} ${n.id} (${n.frequency.toFixed(n.type !== "NDB" ? 2 : 0)}) ${Formatter.getVector(this.position.getVectorTo(n.position))}`;
})
.join(", ");
}
}
get description() {
return this.getDescription();
}
/**
* @param {boolean} withNavAid if to include navigational aids in description
* @returns {string}
*/
getDescription(withNavAid = true) {
let description = "";
const anyDescription = this.radioDescription || (this.navAidDescription && withNavAid);
if (!anyDescription) {
return description;
}
if (this.radioDescription) {
description += "\n" + this.radioDescription;
}
if (this.navAidDescription && withNavAid) {
description += "\n" + this.navAidDescription;
}
return description;
}
/**
*
* @param {AviationWeatherNormalizedRunway} runwayApiData
* @param {Point} airportPosition
* @param {Configuration?} configuration
* @returns {AirportRunway[]} both directions, or in case of helipads on single helipad
*/
buildRunways(runwayApiData, airportPosition, configuration) {
/**
* @type {[string,string]} both directions
*/
const id = runwayApiData.id;
/**
* @type {[number,number]} length, width in ft
*/
const dimension = runwayApiData.dimension;
// Helipads & Water runways get an approximate alignment
if (runwayApiData.alignment === null) {
runwayApiData.alignment = Number(id[0].replace(/\D/g, "") + "0");
}
const alignmentBase = runwayApiData.alignment;
if (isNaN(alignmentBase)) {
return [];
}
/**
* @type {[number, number]} both directions
*/
const alignment = [alignmentBase, Degree(alignmentBase + 180)];
/**
* @type {[Point,Point]} both directions
*/
const positions = [
airportPosition.getPointBy(new Vector(dimension[0] / 2 / Units.feetPerMeter, alignment[0] + 180)),
airportPosition.getPointBy(new Vector(dimension[0] / 2 / Units.feetPerMeter, alignment[1] + 180)),
];
const rightPatternRunways = [
configuration && configuration.rightPatternRunways.length
? configuration.rightPatternRunways.indexOf(id[0]) !== -1
: id[0].endsWith("R"),
configuration && configuration.rightPatternRunways.length
? configuration.rightPatternRunways.indexOf(id[1]) !== -1
: id[1].endsWith("R"),
];
const runways = [
new AirportRunway(id[0], dimension, alignment[0], positions[0], rightPatternRunways[0], configuration?.preferredRunways.indexOf(id[0]) !== -1),
];
if (id[1] !== "") {
runways.push(new AirportRunway(id[1], dimension, alignment[1], positions[1], rightPatternRunways[1], configuration?.preferredRunways.indexOf(id[1]) !== -1));
}
return runways;
}
}
/**
* @type {AeroflyPatternsWaypointable}
*/
export class AirportRunway {
/**
*
* @param {string} id
* @param {[number,number?]} dimension
* @param {number} alignment
* @param {Point} position
* @param {boolean} isRightPattern
* @param {boolean} isPreferred
* @param {number} ilsFrequency
*/
constructor(id, dimension, alignment, position, isRightPattern = false, isPreferred = false, ilsFrequency = 0) {
this.id = id;
this.position = position;
/**
* @type {[number,number?]} length, width in ft
*/
this.dimension = dimension;
const alignmentAdjustment = id.endsWith("R") ? 0.1 : 0;
/**
* @type {number}
*/
this.alignment = Degree(alignment + alignmentAdjustment);
/**
* @type {boolean}
*/
this.isRightPattern = isRightPattern;
/**
* @type {boolean} isPreferred most active runways, will be used in case wind is indecisive
*/
this.isPreferred = isPreferred;
/**
* @type {number} in MHz
*/
this.ilsFrequency = ilsFrequency;
const endMatch = id.match(/([SGUW]$)|H/);
/**
* @property {"S"|"G"|"H"|"U"|"W"?} type STOL, Glider, Helicopter, Ultralight, Water, all
*/
this.runwayType = endMatch ? endMatch[0] : null;
}
}
/**
* @type {AeroflyPatternsWaypointable}
*/
export class AirportNavaid {
constructor(navaidJson) {
this.id = navaidJson.id;
this.position = new Point(navaidJson.lon, navaidJson.lat, navaidJson.elev);
/**
* @type {"VORTAC"|"VOR/DME"|"TACAN"|"NDB"|"VOR"}
*/
this.type = navaidJson.type;
/**
* @type {number}
*/
this.frequency = navaidJson.freq;
}
}