@fboes/aerofly-patterns
Advanced tools
Comparing version 2.2.0 to 2.3.0
# Changelog | ||
## 2.3.0 | ||
- Added API changes from AviationWeather | ||
- Added approximate alignment for water runways | ||
- Added extra cloud layers | ||
- Extracted FileWriter to have NodeJs dependency separate | ||
- Improved `DateYielder` | ||
## 2.2.0 | ||
@@ -4,0 +12,0 @@ |
@@ -38,2 +38,3 @@ // @ts-check | ||
KHAF: { runways: [{ id: "30", isRightPattern: true }] }, | ||
KJAC: { minimumSafeAltitude: 14_900, runways: [{ id: "19", ilsFrequency: 109.1, isPreferred: true }] }, | ||
KMVY: { | ||
@@ -52,2 +53,3 @@ runways: [ | ||
}, | ||
KWYS: { minimumSafeAltitude: 12_600, runways: [{ id: "01", ilsFrequency: 110.7, isPreferred: true }] }, | ||
}; |
@@ -6,2 +6,3 @@ #!/usr/bin/env node | ||
import { Configuration } from "./lib/Configuration.js"; | ||
import { FileWriter } from "./lib/FileWriter.js"; | ||
@@ -12,3 +13,3 @@ const configuration = new Configuration(process.argv); | ||
process.stdout | ||
.write(`\x1b[94mUsage: npx @fboes/aerofly-patterns ICAO_AIRPORT_CODE [AEROFLY_AIRCRAFT_CODE] [...options]\x1b[0m | ||
.write(`\x1b[94mUsage: npx @fboes/aerofly-patterns@latest ICAO_AIRPORT_CODE [AEROFLY_AIRCRAFT_CODE] [...options]\x1b[0m | ||
Create landing pattern lessons for Aerofly FS 4. | ||
@@ -28,3 +29,5 @@ | ||
const app = new AeroflyPatterns(configuration); | ||
await app.build(process.cwd()); | ||
await app.build(); | ||
await FileWriter.writeFile(app, process.cwd()); | ||
console.log(`✅ Done with ${app.airport?.name} (${app.airport?.id})`); |
// @ts-check | ||
import { AirportTest } from "./lib/Airport.test.js"; | ||
import { AviationWeatherApiTest, AviationWeatherApiHelpersTest } from "./lib/AviationWeatherApi.test.js"; | ||
import { DateYielderTest } from "./lib/DateYielder.test.js"; | ||
@@ -8,4 +9,6 @@ import { DegreeTest } from "./lib/Degree.test.js"; | ||
new AirportTest(); | ||
new AviationWeatherApiTest(); | ||
new AviationWeatherApiHelpersTest(); | ||
new DateYielderTest(); | ||
new DegreeTest(); | ||
new FormatterTest(); |
// @ts-check | ||
import * as fs from "node:fs/promises"; | ||
import { Airport } from "./Airport.js"; | ||
@@ -52,7 +51,3 @@ import { AviationWeatherApi } from "./AviationWeatherApi.js"; | ||
/** | ||
* | ||
* @param {string} saveDirectory | ||
*/ | ||
async build(saveDirectory) { | ||
async build() { | ||
const airport = await AviationWeatherApi.fetchAirports([this.configuration.icaoCode]); | ||
@@ -82,4 +77,2 @@ if (!airport.length) { | ||
} | ||
await this.writeCustomMissionFiles(saveDirectory); | ||
} | ||
@@ -111,2 +104,3 @@ | ||
alignment: r.alignment, | ||
dimension: r.dimension, | ||
frequency: r.ilsFrequency, | ||
@@ -229,2 +223,3 @@ isRightPattern: r.isRightPattern, | ||
<[string8u] [aircraft_name] [${s.aircraft.aeroflyCode}]> | ||
//<[string8u][aircraft_livery] []> | ||
<[stringt8c] [aircraft_icao] [${s.aircraft.data.icaoCode}]> | ||
@@ -252,4 +247,8 @@ <[stringt8c] [callsign] [${s.aircraft.data.callsign}]> | ||
<[float64][visibility][${(s.weather?.visibility ?? 15) * Units.meterPerStatuteMile}]> | ||
<[float64][cloud_cover][${s.weather?.cloudCover ?? 0}]> | ||
<[float64][cloud_base][${(s.weather?.cloudBase ?? 0) / Units.feetPerMeter}]> | ||
<[float64][cloud_cover][${s.weather?.clouds[0]?.cloudCover ?? 0}]> // ${s.weather?.clouds[0]?.cloudCoverCode ?? "CLR"} | ||
<[float64][cloud_base][${(s.weather?.clouds[0]?.cloudBase ?? 0) / Units.feetPerMeter}]> // ${s.weather?.clouds[0]?.cloudBase ?? 0} ft | ||
//<[float64][cloud2_cover][${s.weather?.clouds[1]?.cloudCover ?? 0}]> // ${s.weather?.clouds[1]?.cloudCoverCode ?? "CLR"} | ||
//<[float64][cloud2_base][${(s.weather?.clouds[1]?.cloudBase ?? 0) / Units.feetPerMeter}]> // ${s.weather?.clouds[1]?.cloudBase ?? 0} ft | ||
//<[float64][cloud3_cover][${s.weather?.clouds[2]?.cloudCover ?? 0}]> // ${s.weather?.clouds[2]?.cloudCoverCode ?? "CLR"} | ||
//<[float64][cloud3_base][${(s.weather?.clouds[2]?.cloudBase ?? 0) / Units.feetPerMeter}]> // ${s.weather?.clouds[2]?.cloudBase ?? 0} ft | ||
> | ||
@@ -352,5 +351,5 @@ <[list_tmmission_checkpoint][checkpoints][] | ||
const clouds = | ||
s.weather?.cloudCoverCode !== "CLR" | ||
? `${pad(s.weather?.cloudCoverCode, 3, true)} @ ${pad(s.weather?.cloudBase.toLocaleString("en"), 6, true)} ft` | ||
: pad(s.weather?.cloudCoverCode, 15); | ||
s.weather?.clouds[0]?.cloudCoverCode !== "CLR" | ||
? `${pad(s.weather?.clouds[0]?.cloudCoverCode, 3, true)} @ ${pad(s.weather?.clouds[0]?.cloudBase.toLocaleString("en"), 6, true)} ft` | ||
: pad(s.weather?.clouds[0]?.cloudCoverCode, 15); | ||
@@ -362,3 +361,3 @@ output.push( | ||
Formatter.getUtcCompleteDate(s.date), | ||
pad(padNumber(lst) + ":00", 10, true), | ||
pad(padNumber(lst) + ":" + padNumber(s.date.getUTCMinutes()), 10, true), | ||
s.weather?.windSpeed === 0 | ||
@@ -390,25 +389,2 @@ ? pad("Calm", 12) | ||
} | ||
/** | ||
* | ||
* @param {string} saveDirectory | ||
*/ | ||
async writeCustomMissionFiles(saveDirectory) { | ||
if (this.configuration.directoryMode) { | ||
saveDirectory = `${saveDirectory}/data/Landing_Challenges-${this.configuration.icaoCode}-${this.configuration.aircraft}`; | ||
await fs.mkdir(saveDirectory, { recursive: true }); | ||
} | ||
await Promise.all([ | ||
fs.writeFile(`${saveDirectory}/custom_missions_user.tmc`, this.buildCustomMissionTmc()), | ||
!this.configuration.readme || fs.writeFile(`${saveDirectory}/README.md`, this.buildReadmeMarkdown()), | ||
!this.configuration.geojson || | ||
fs.writeFile( | ||
`${saveDirectory}/${this.configuration.icaoCode}-${this.configuration.aircraft}.geojson`, | ||
JSON.stringify(this.buildGeoJson(), null, 2), | ||
), | ||
// fs.writeFile(`${saveDirectory}/debug.json`, JSON.stringify(this, null, 2)), | ||
]); | ||
} | ||
} |
@@ -8,2 +8,3 @@ // @ts-check | ||
import { Formatter } from "./Formatter.js"; | ||
import { AviationWeatherApiHelpers } from "./AviationWeatherApi.js"; | ||
@@ -20,3 +21,3 @@ /** | ||
constructor(airportJson, configuration = null) { | ||
this.id = airportJson.id; | ||
this.id = airportJson.icaoId; | ||
this.position = new Point(airportJson.lon, airportJson.lat, airportJson.elev); | ||
@@ -50,3 +51,3 @@ | ||
this.buildRunways(r, this.position, configuration).forEach((runway) => { | ||
this.runways.push(runway); | ||
runway && this.runways.push(runway); | ||
}); | ||
@@ -79,6 +80,6 @@ }); | ||
this.magneticDeclination = 0; | ||
const mag_dec_match = airportJson.mag_dec.match(/^(\d+)(E|W)$/); | ||
if (mag_dec_match) { | ||
this.magneticDeclination = Number(mag_dec_match[1]); | ||
if (mag_dec_match[2] === "W") { | ||
const magdecMatch = airportJson.magdec.match(/^(\d+)(E|W)$/); | ||
if (magdecMatch) { | ||
this.magneticDeclination = Number(magdecMatch[1]); | ||
if (magdecMatch[2] === "W") { | ||
this.magneticDeclination *= -1; | ||
@@ -96,3 +97,3 @@ } | ||
*/ | ||
this.hasTower = airportJson.tower !== "-"; | ||
this.hasTower = airportJson.tower === "T"; | ||
@@ -102,5 +103,5 @@ /** | ||
*/ | ||
this.hasBeacon = airportJson.beacon !== "-"; | ||
this.hasBeacon = airportJson.beacon === "B"; | ||
const lclP = airportJson.freqs.find((f) => { | ||
const lclP = AviationWeatherApiHelpers.fixFrequencies(airportJson.freqs).find((f) => { | ||
return f.type === "LCL/P"; | ||
@@ -112,3 +113,3 @@ }); | ||
*/ | ||
this.localFrequency = lclP ? lclP.freq : null; | ||
this.localFrequency = lclP?.freq ?? null; | ||
@@ -210,5 +211,12 @@ /** | ||
// Helipads | ||
const alignmentBase = runwayJson.alignment !== "-" ? Number(runwayJson.alignment) : 0; | ||
// Helipads & Water runways get an approximate alignment | ||
if (runwayJson.alignment === "-") { | ||
runwayJson.alignment = id[0].replace(/\D/g, "") + "0"; | ||
} | ||
const alignmentBase = Number(runwayJson.alignment); | ||
if (isNaN(alignmentBase)) { | ||
return []; | ||
} | ||
/** | ||
@@ -215,0 +223,0 @@ * @type {[number, number]} both directions |
@@ -16,9 +16,10 @@ //@ts-check | ||
const airportJson = { | ||
id: "KMCI", | ||
icaoId: "KMCI", | ||
name: "KANSAS CITY/KANSAS_CITY_INTL", | ||
type: "ARP", | ||
lat: 39.2976, | ||
lon: -94.7139, | ||
elev: 313.1, | ||
mag_dec: "02E", | ||
rwy_num: 3, | ||
magdec: "02E", | ||
rwyNum: 3, | ||
tower: "T", | ||
@@ -76,9 +77,10 @@ beacon: "B", | ||
const airportJson = { | ||
id: "KMVY", | ||
icaoId: "KMVY", | ||
name: "VINEYARD HAVEN/MARTHA'S_VINEYARD", | ||
type: "ARP", | ||
lat: 41.3934, | ||
lon: -70.6139, | ||
elev: 20.4, | ||
mag_dec: "15W", | ||
rwy_num: 2, | ||
magdec: "15W", | ||
rwyNum: 2, | ||
tower: "T", | ||
@@ -121,9 +123,10 @@ beacon: "B", | ||
const airportJson = { | ||
id: "KSCK", | ||
icaoId: "KSCK", | ||
name: "STOCKTON/STOCKTON_METRO", | ||
type: "ARP", | ||
lat: 37.8944, | ||
lon: -121.2387, | ||
elev: 10.1, | ||
mag_dec: "14E", | ||
rwy_num: 3, | ||
magdec: "14E", | ||
rwyNum: 3, | ||
tower: "T", | ||
@@ -130,0 +133,0 @@ beacon: "B", |
@@ -32,6 +32,10 @@ // @ts-check | ||
/** | ||
* @typedef {"A"|"C"|"G"|"W"|"T"} AviationWeatherApiRunwaySurface "A" Asphalt, "C" Concrete, "G" Grass, "W" Water, "T" Turf Dirt | ||
*/ | ||
/** | ||
* @typedef {object} AviationWeatherApiRunway | ||
* @property {string} id "01L/19R" | ||
* @property {string} dimension "10801x150" in feet | ||
* @property {"A"|"C"|"G"} surface "A" Asphalt, "C" Concrete, "G" Grass | ||
* @property {AviationWeatherApiRunwaySurface} surface | ||
* @property {string} alignment "013" or "-" | ||
@@ -43,4 +47,4 @@ * @see https://aviationweather.gov/data/api/#/Data/dataAirport | ||
* @typedef {object} AviationWeatherApiFrequencies | ||
* @property {string} type "LCL/P", | ||
* @property {number} freq 121.4 | ||
* @property {string} type "LCL/P" or "-" | ||
* @property {number} [freq] 121.4 | ||
*/ | ||
@@ -50,13 +54,14 @@ | ||
* @typedef {object} AviationWeatherApiAirport | ||
* @property {string} id "KMCI" | ||
* @property {string} icaoId "KMCI" | ||
* @property {string} name "KANSAS CITY/KANSAS_CITY_INTL" | ||
* @property {"ARP"|"HEL"} type Airport, Heliport | ||
* @property {number} lat | ||
* @property {number} lon | ||
* @property {number} elev 313.1 meters MSL | ||
* @property {string} mag_dec "02E" for East | ||
* @property {number} rwy_num | ||
* @property {"T"|"-"} tower | ||
* @property {"B"|"-"} beacon | ||
* @property {string} magdec "02E" for East | ||
* @property {number} rwyNum | ||
* @property {"T"|"-"|null} tower | ||
* @property {"B"|"-"|null} beacon | ||
* @property {AviationWeatherApiRunway[]} runways | ||
* @property {AviationWeatherApiFrequencies[]} freqs | ||
* @property {AviationWeatherApiFrequencies[]|string} freqs or "LCL/P,123.9;ATIS,124.7" | ||
* @see https://aviationweather.gov/data/api/#/Data/dataAirport | ||
@@ -145,1 +150,29 @@ */ | ||
} | ||
export class AviationWeatherApiHelpers { | ||
/** | ||
* | ||
* @param {AviationWeatherApiFrequencies[]|string} freq | ||
* @returns {AviationWeatherApiFrequencies[]} | ||
*/ | ||
static fixFrequencies(freq) { | ||
if (typeof freq !== "string") { | ||
return freq; | ||
} | ||
return freq.split(";").map( | ||
/** | ||
* | ||
* @param {string} f | ||
* @returns {AviationWeatherApiFrequencies} | ||
*/ | ||
(f) => { | ||
const parts = f.split(","); | ||
return { | ||
type: parts[0], | ||
freq: parts[1] ? Number(parts[1]) : undefined, | ||
}; | ||
}, | ||
); | ||
} | ||
} |
@@ -190,3 +190,3 @@ // @ts-check | ||
/** | ||
* @type {boolean} | ||
* @type {boolean} if generate | ||
*/ | ||
@@ -193,0 +193,0 @@ this.readme = Boolean(values["readme"]); |
@@ -24,3 +24,3 @@ // @ts-check | ||
*/ | ||
this.startDate = startDate; | ||
this.startDate = this.getLocalTime(startDate, 12 - (count - 1) / 2); | ||
} | ||
@@ -36,8 +36,6 @@ | ||
while (index < this.count) { | ||
const currenDate = new Date(this.startDate.getTime()); | ||
if (this.count > 1) { | ||
const percentage = index / (this.count - 1); | ||
currenDate.setDate(currenDate.getDate() - Math.round(percentage * 12) - 1); | ||
currenDate.setUTCHours(8 - this.offsetHours + percentage * 12); | ||
} | ||
const currenDate = new Date(this.startDate); | ||
currenDate.setDate(currenDate.getDate() - index); | ||
currenDate.setUTCHours(currenDate.getUTCHours() + index); | ||
yield currenDate; | ||
@@ -48,2 +46,20 @@ | ||
} | ||
/** | ||
* Gets the next local 8 o'clock in the past | ||
* @param {Date} startDate | ||
* @param {number} hours local time | ||
* @returns {Date} | ||
*/ | ||
getLocalTime(startDate, hours = 6) { | ||
const eightOClock = new Date(startDate); | ||
eightOClock.setUTCMinutes(60 * (hours % 1)); | ||
eightOClock.setUTCHours(Math.floor(hours - this.offsetHours)); | ||
while (eightOClock.valueOf() > startDate.valueOf()) { | ||
eightOClock.setDate(eightOClock.getDate() - 1); | ||
} | ||
return eightOClock; | ||
} | ||
} |
@@ -8,19 +8,8 @@ //@ts-check | ||
constructor() { | ||
this.checkSingleEntry(); | ||
this.checkMultipleEntries(5); | ||
this.checkMultipleEntries(12); | ||
this.checkMultipleEntries(24); | ||
} | ||
checkSingleEntry() { | ||
const startDate = new Date(Date.UTC(2024, 4, 15, 12, 0, 0)); | ||
const dateYielder = new DateYielder(1, 0, startDate); | ||
const dates = dateYielder.entries(); | ||
for (const currentDate of dates) { | ||
//console.log(currentDate); | ||
assert.ok(currentDate); | ||
assert.equal(currentDate.toISOString(), startDate.toISOString()); | ||
// All time zones | ||
for (let i = 12; i >= -12; i--) { | ||
this.checkEntries(1, i); | ||
this.checkEntries(5, i); | ||
this.checkEntries(12, i); | ||
} | ||
console.log(`✅ ${this.constructor.name}.checkSingleEntry successful`); | ||
} | ||
@@ -31,15 +20,20 @@ | ||
* @param {number} entries | ||
* @param {number} offsetHours | ||
*/ | ||
checkMultipleEntries(entries) { | ||
const startDate = new Date(Date.UTC(2024, 4, 15, 12, 0, 0)); | ||
const dateYielder = new DateYielder(entries, 2, startDate); | ||
const dates = dateYielder.entries(); | ||
for (const currentDate of dates) { | ||
//console.log(currentDate); | ||
assert.ok(currentDate); | ||
assert.notEqual(currentDate.toISOString(), startDate.toISOString()); | ||
} | ||
checkEntries(entries, offsetHours) { | ||
const startDate = new Date(Date.UTC(2024, 4, 15, 12, 32, 0)); | ||
const dateYielder = new DateYielder(entries, offsetHours, startDate); | ||
const dates = Array.from(dateYielder.entries()); | ||
console.log(`✅ ${this.constructor.name}.checkMultipleEntries(${entries}) successful`); | ||
assert.strictEqual(dates.length, entries); | ||
dates.forEach((d) => { | ||
assert.strictEqual(d.getUTCFullYear(), 2024); | ||
assert.strictEqual(d.getUTCMonth(), 4); | ||
assert.ok(d.getUTCDate() <= 15); | ||
assert.ok(d.valueOf() <= startDate.valueOf()); | ||
}); | ||
//console.log(dateYielder.startDate, dates); | ||
console.log(`✅ ${this.constructor.name}.checkEntries(${entries}, ${offsetHours}) successful`); | ||
} | ||
} |
@@ -134,3 +134,3 @@ // @ts-check | ||
} else { | ||
switch (weather.cloudCoverCode) { | ||
switch (weather.clouds[0]?.cloudCoverCode) { | ||
case "OVC": | ||
@@ -137,0 +137,0 @@ adjectives.push("overcast"); |
@@ -5,3 +5,3 @@ //@ts-check | ||
import { Formatter } from "./Formatter.js"; | ||
import { ScenarioWeather } from "./Scenario.js"; | ||
import { ScenarioWeather, ScenarioWeatherCloud } from "./Scenario.js"; | ||
@@ -78,3 +78,3 @@ export class FormatterTest { | ||
w.cloudCoverCode = "OVC"; | ||
w.clouds[0] = new ScenarioWeatherCloud("OVC", 1000); | ||
assert.strictEqual("foggy", Formatter.getWeatherAdjectives(w)); | ||
@@ -81,0 +81,0 @@ |
@@ -85,3 +85,3 @@ // @ts-check | ||
if (!weather.length) { | ||
throw new Error("No METAR information from API"); | ||
throw new Error("No METAR information from API for " + this.airport.id); | ||
} | ||
@@ -376,12 +376,2 @@ this.weather = new ScenarioWeather(weather[0]); | ||
/** | ||
* @type {number} 0..1 | ||
*/ | ||
#cloudCover = 0; | ||
/** | ||
* @type {"CLR"|"FEW"|"SCT"|"BKN"|"OVC"} | ||
*/ | ||
#cloudCoverCode = "CLR"; | ||
/** | ||
* @param {import('./AviationWeatherApi.js').AviationWeatherApiMetar} weatherJson | ||
@@ -410,10 +400,7 @@ */ | ||
this.cloudCoverCode = weatherJson.clouds[0]?.cover; | ||
this.clouds = weatherJson.clouds.map((c) => { | ||
return new ScenarioWeatherCloud(c.cover, c.base); | ||
}); | ||
/** | ||
* @type {number} in ft | ||
*/ | ||
this.cloudBase = weatherJson.clouds[0]?.base ?? 0; | ||
/** | ||
* @type {number} 0..1 | ||
@@ -430,4 +417,35 @@ */ | ||
} | ||
} | ||
export class ScenarioWeatherCloud { | ||
/** | ||
* @type {number} 0..1 | ||
*/ | ||
#cloudCover = 0; | ||
/** | ||
* @type {"CLR"|"FEW"|"SCT"|"BKN"|"OVC"} | ||
*/ | ||
#cloudCoverCode = "CLR"; | ||
/** | ||
* @param {"CAVOK"|"CLR"|"FEW"|"SCT"|"BKN"|"OVC"} cover | ||
* @param {number?} base | ||
*/ | ||
constructor(cover, base) { | ||
this.cloudCoverCode = cover; | ||
/** | ||
* @type {number} in ft | ||
*/ | ||
this.cloudBase = base ?? 0; | ||
} | ||
/** | ||
* @returns {number} 0..1 | ||
*/ | ||
get cloudCover() { | ||
return this.#cloudCover; | ||
} | ||
/** | ||
* | ||
@@ -462,9 +480,2 @@ * @param {"CAVOK"|"CLR"|"FEW"|"SCT"|"BKN"|"OVC"} cloudCoverCode | ||
} | ||
/** | ||
* @returns {number} 0..1 | ||
*/ | ||
get cloudCover() { | ||
return this.#cloudCover; | ||
} | ||
} |
{ | ||
"name": "@fboes/aerofly-patterns", | ||
"version": "2.2.0", | ||
"version": "2.3.0", | ||
"description": "Landegerät - Create landing pattern lessons for Aerofly FS 4.", | ||
@@ -5,0 +5,0 @@ "main": "dist/index.js", |
@@ -25,3 +25,3 @@ # Aerofly Landegerät | ||
``` | ||
Usage: npx @fboes/aerofly-patterns ICAO_AIRPORT_CODE [AEROFLY_AIRCRAFT_CODE] [...options] | ||
Usage: npx @fboes/aerofly-patterns@latest ICAO_AIRPORT_CODE [AEROFLY_AIRCRAFT_CODE] [...options] | ||
Create landing pattern lessons for Aerofly FS 4. | ||
@@ -28,0 +28,0 @@ |
90410
26
2523