@fboes/aerofly-patterns
Advanced tools
Comparing version 2.1.2 to 2.2.0
# Changelog | ||
## 2.2.0 | ||
- Added navigational aids to GeoJSON | ||
- Improved GeoJSON output | ||
- Added TPA configuration | ||
- Added simple mechanism to determine right pattern | ||
- Improved support for helipads | ||
- Improved support for "L"/"R" runways | ||
## 2.1.2 | ||
@@ -4,0 +13,0 @@ |
@@ -27,2 +27,2 @@ #!/usr/bin/env node | ||
await app.build(process.cwd()); | ||
console.log("✅ Done"); | ||
console.log(`✅ Done with ${app.airport?.name} (${app.airport?.id})`); |
@@ -6,3 +6,3 @@ // @ts-check | ||
import { Configuration } from "./Configuration.js"; | ||
import { FeatureCollection, Feature } from "@fboes/geojson"; | ||
import { FeatureCollection, Feature, LineString } from "@fboes/geojson"; | ||
import { Scenario } from "./Scenario.js"; | ||
@@ -100,2 +100,3 @@ import { DateYielder } from "./DateYielder.js"; | ||
"marker-symbol": "airport", | ||
frequency: this.airport.localFrequency, | ||
}), | ||
@@ -107,3 +108,7 @@ ]); | ||
title: r.id, | ||
"marker-symbol": r === scenario.activeRunway ? "triangle" : "triangle-stroked", | ||
"marker-symbol": | ||
r === scenario.activeRunway ? "triangle" : r.runwayType === "H" ? "heliport" : "triangle-stroked", | ||
alignment: r.alignment, | ||
frequency: r.ilsFrequency, | ||
isRightPattern: r.isRightPattern, | ||
}), | ||
@@ -113,2 +118,13 @@ ); | ||
this.airport.navaids.forEach((n) => { | ||
geoJson.addFeature( | ||
new Feature(n.position, { | ||
title: n.id, | ||
"marker-symbol": "communications-tower", | ||
frequency: n.frequency, | ||
type: n.type, | ||
}), | ||
); | ||
}); | ||
geoJson.addFeature( | ||
@@ -121,2 +137,32 @@ new Feature(scenario.aircraft.position, { | ||
const waypoints = scenario.patternWaypoints.map((p) => { | ||
return p.position; | ||
}); | ||
waypoints.push(scenario.patternWaypoints[0].position); | ||
geoJson.addFeature( | ||
new Feature(new LineString(waypoints), { | ||
title: "Traffic pattern", | ||
"stroke-opacity": 0.2, | ||
}), | ||
); | ||
if (scenario.activeRunway) { | ||
geoJson.addFeature( | ||
new Feature( | ||
new LineString([ | ||
scenario.aircraft.position, | ||
scenario.entryWaypoint?.position, | ||
scenario.patternWaypoints[2].position, | ||
scenario.patternWaypoints[3].position, | ||
scenario.patternWaypoints[4].position, | ||
scenario.activeRunway?.position, | ||
]), | ||
{ | ||
title: "Flight plan", | ||
stroke: "#ff1493", | ||
}, | ||
), | ||
); | ||
} | ||
scenario.patternWaypoints.forEach((p) => { | ||
@@ -300,4 +346,4 @@ geoJson.addFeature( | ||
"", | ||
`| No | Local date | Local time | Wind | Clouds | Visibility | Runway | Aircraft position |`, | ||
`| :-: | ---------- | ---------: | ------------ | --------------- | ---------: | ------- | ------------------- |`, | ||
`| No | Local date | Local time | Wind | Clouds | Visibility | Runway | Aircraft position |`, | ||
`| :-: | ---------- | ---------: | ------------ | --------------- | ---------: | -------- | ------------------- |`, | ||
); | ||
@@ -322,3 +368,3 @@ this.scenarios.forEach((s, index) => { | ||
pad(Math.round(s.weather?.visibility ?? 0), 7, true) + " SM", | ||
pad(s.activeRunway?.id + (s.activeRunway?.isRightPattern ? " (RP)" : ""), 7), | ||
pad(s.activeRunway?.id + (s.activeRunway?.isRightPattern ? " (RP)" : ""), 8), | ||
Formatter.getDirectionArrow(s.aircraft.vectorFromAirport.bearing) + | ||
@@ -325,0 +371,0 @@ " To the " + |
@@ -18,3 +18,3 @@ // @ts-check | ||
*/ | ||
constructor(airportJson, configuration) { | ||
constructor(airportJson, configuration = null) { | ||
this.id = airportJson.id; | ||
@@ -30,2 +30,3 @@ this.position = new Point(airportJson.lon, airportJson.lat, airportJson.elev); | ||
.replace(/\bRGNL\b/g, "REGIONAL") | ||
.replace(/\bFLD\b/g, "FIELD") | ||
.replace(/(\/)/g, " $1 ") | ||
@@ -182,3 +183,3 @@ .toLowerCase() | ||
* @param {import('./Configuration.js').Configuration?} configuration | ||
* @returns {[AirportRunway, AirportRunway]} | ||
* @returns {AirportRunway[]} both directions, or in case of helipads on single helipad | ||
*/ | ||
@@ -205,6 +206,9 @@ buildRunways(runwayJson, airportPosition, configuration) { | ||
// Helipads | ||
const alignmentBase = runwayJson.alignment !== "-" ? Number(runwayJson.alignment) : 0; | ||
/** | ||
* @type {[number, number]} both directions | ||
*/ | ||
const alignment = [Number(runwayJson.alignment), Degree(Number(runwayJson.alignment) + 180)]; | ||
const alignment = [alignmentBase, Degree(alignmentBase + 180)]; | ||
@@ -219,3 +223,12 @@ /** | ||
return [ | ||
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( | ||
@@ -226,14 +239,21 @@ id[0], | ||
positions[0], | ||
configuration?.rightPatternRunways.indexOf(id[0]) !== -1, | ||
rightPatternRunways[0], | ||
configuration?.preferredRunways.indexOf(id[0]) !== -1, | ||
), | ||
new AirportRunway( | ||
id[1], | ||
dimension, | ||
alignment[1], | ||
positions[1], | ||
configuration?.rightPatternRunways.indexOf(id[1]) !== -1, | ||
configuration?.preferredRunways.indexOf(id[1]) !== -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; | ||
} | ||
@@ -265,6 +285,8 @@ } | ||
const alignmentAdjustment = id.endsWith("R") ? 0.1 : 0; | ||
/** | ||
* @type {number} | ||
*/ | ||
this.alignment = Degree(alignment); | ||
this.alignment = Degree(alignment + alignmentAdjustment); | ||
@@ -286,3 +308,3 @@ /** | ||
const endMatch = id.match(/[SGHUW]$/); | ||
const endMatch = id.match(/([SGUW]$)|H/); | ||
@@ -289,0 +311,0 @@ /** |
@@ -10,2 +10,3 @@ //@ts-check | ||
this.checkMarthasVineyard(); | ||
this.checkStockton(); | ||
} | ||
@@ -115,2 +116,61 @@ | ||
} | ||
checkStockton() { | ||
/** @type {import('./AviationWeatherApi.js').AviationWeatherApiAirport} */ | ||
const airportJson = { | ||
id: "KSCK", | ||
name: "STOCKTON/STOCKTON_METRO", | ||
lat: 37.8944, | ||
lon: -121.2387, | ||
elev: 10.1, | ||
mag_dec: "14E", | ||
rwy_num: 3, | ||
tower: "T", | ||
beacon: "B", | ||
runways: [ | ||
{ | ||
id: "11L/29R", | ||
dimension: "10249x150", | ||
surface: "A", | ||
alignment: "128", | ||
}, | ||
{ | ||
id: "11R/29L", | ||
dimension: "4448x75", | ||
surface: "A", | ||
alignment: "128", | ||
}, | ||
{ | ||
id: "H1", | ||
dimension: "70x70", | ||
surface: "C", | ||
alignment: "-", | ||
}, | ||
], | ||
freqs: [ | ||
{ | ||
type: "ATIS", | ||
freq: 118.25, | ||
}, | ||
{ | ||
type: "LCL/P", | ||
freq: 120.3, | ||
}, | ||
], | ||
}; | ||
const airport = new Airport(airportJson); | ||
assert.strictEqual(airport.id, "KSCK"); | ||
assert.strictEqual(airport.hasTower, true); | ||
assert.strictEqual(airport.hasBeacon, true); | ||
assert.strictEqual(airport.runways.length, 5); | ||
assert.strictEqual(airport.runways[0].id, "11L"); | ||
assert.strictEqual(airport.runways[0].isRightPattern, false); | ||
assert.strictEqual(airport.runways[2].id, "11R"); | ||
assert.strictEqual(airport.runways[2].isRightPattern, true); | ||
assert.strictEqual(airport.runways[4].runwayType, "H"); | ||
console.log(`✅ ${this.constructor.name}.checkStockton successful`); | ||
} | ||
} |
@@ -36,3 +36,3 @@ // @ts-check | ||
* @property {"A"|"C"|"G"} surface "A" Asphalt, "C" Concrete, "G" Grass | ||
* @property {string} alignment "013" | ||
* @property {string} alignment "013" or "-" | ||
* @see https://aviationweather.gov/data/api/#/Data/dataAirport | ||
@@ -39,0 +39,0 @@ */ |
@@ -29,3 +29,3 @@ // @ts-check | ||
default: "0", | ||
description: "Minimum safe altitude of aircraft, in 100ft. At least airport elevation.", | ||
description: "Minimum safe altitude of aircraft, in 100ft MSL. At least airport elevation.", | ||
example: "145", | ||
@@ -41,4 +41,9 @@ }, | ||
default: "8", | ||
description: "Initial distance from airport in Nautical Miles.", | ||
description: "Initial aircraft distance from airport in Nautical Miles.", | ||
}, | ||
"pattern-altitude": { | ||
type: "string", | ||
default: "1000", | ||
description: "Pattern altitude in ft AGL. For MSL see `--pattern-altitude-msl`", | ||
}, | ||
"pattern-distance": { | ||
@@ -49,2 +54,7 @@ type: "string", | ||
}, | ||
"pattern-final-distance": { | ||
type: "string", | ||
default: "1", | ||
description: "Pattern final distance from airport runway edge in Nautical Miles.", | ||
}, | ||
"rnd-heading": { | ||
@@ -61,2 +71,8 @@ type: "string", | ||
}, | ||
"pattern-altitude-msl": { | ||
type: "boolean", | ||
short: "m", | ||
default: false, | ||
description: "Pattern altitude is in MSL instead of AGL", | ||
}, | ||
directory: { | ||
@@ -112,5 +128,7 @@ type: "boolean", | ||
*/ | ||
this.rightPatternRunways = String(values["right-pattern"]) | ||
.toUpperCase() | ||
.split(/[,\s]+/); | ||
this.rightPatternRunways = values["right-pattern"] | ||
? String(values["right-pattern"]) | ||
.toUpperCase() | ||
.split(/[,\s]+/) | ||
: []; | ||
@@ -133,2 +151,7 @@ /** | ||
/** | ||
* @type {number} in ft AGL (or MSL if isPatternAltitudeMsl) | ||
*/ | ||
this.patternAltitude = Number(values["pattern-altitude"]); | ||
/** | ||
* @type {number} in Nautical Miles | ||
@@ -139,2 +162,7 @@ */ | ||
/** | ||
* @type {number} in Nautical miles | ||
*/ | ||
this.patternFinalDistance = Number(values["pattern-final-distance"]); | ||
/** | ||
* @type {number} Randomized aircraft heading deviation from direct heading to airport in degree. | ||
@@ -147,7 +175,14 @@ */ | ||
*/ | ||
this.preferredRunways = String(values["prefer-rwy"]) | ||
.toUpperCase() | ||
.split(/[,\s]+/); | ||
this.preferredRunways = values["prefer-rwy"] | ||
? String(values["prefer-rwy"]) | ||
.toUpperCase() | ||
.split(/[,\s]+/) | ||
: []; | ||
/** | ||
* @type {boolean} if this.patternAltitude is in MSL instead of AGL | ||
*/ | ||
this.isPatternAltitudeMsl = Boolean(values["pattern-altitude-msl"]); | ||
/** | ||
* @type {boolean} if files should be created in subfolder | ||
@@ -154,0 +189,0 @@ */ |
@@ -75,2 +75,7 @@ // @ts-check | ||
this.patternWaypoints = []; | ||
/** | ||
* @type {import("./AeroflyPatterns.js").AeroflyPatternsWaypointable?} | ||
*/ | ||
this.entryWaypoint = null; | ||
} | ||
@@ -124,13 +129,35 @@ | ||
/** | ||
* @type {number} in meters | ||
*/ | ||
const exitDistance = this.configuration.patternDistance * Units.meterPerNauticalMile; | ||
/** | ||
* @type {number} in meters | ||
*/ | ||
const downwindDistance = this.configuration.patternDistance * Units.meterPerNauticalMile; | ||
const finalDistance = this.configuration.patternDistance * Units.meterPerNauticalMile; | ||
const patternOrientation = this.activeRunway.alignment + Degree(this.activeRunway.isRightPattern ? 90 : 270); | ||
const patternAltitude = (this.airport.position.elevation ?? 0) + 1000 / Units.feetPerMeter; | ||
/** | ||
* @type {number} meters to sink per 1 NM to have 3° glide slope | ||
* @type {number} in meters | ||
*/ | ||
const glideSlope = 319.8 / Units.feetPerMeter; | ||
const finalDistance = this.configuration.patternFinalDistance * Units.meterPerNauticalMile; | ||
/** | ||
* @type {number} in degree | ||
*/ | ||
const patternOrientation = Degree(this.activeRunway.alignment + (this.activeRunway.isRightPattern ? 90 : 270)); | ||
/** | ||
* @type {number} in meters MSL | ||
*/ | ||
let patternAltitude = this.configuration.patternAltitude / Units.feetPerMeter; | ||
if (!this.configuration.isPatternAltitudeMsl && this.airport.position.elevation) { | ||
patternAltitude += this.airport.position.elevation; | ||
} | ||
/** | ||
* @type {number} meters to sink per meter distance to have 3° glide slope | ||
*/ | ||
const glideSlope = 319.8 / Units.feetPerMeter / Units.meterPerNauticalMile; | ||
if (this.weather?.windDirection) { | ||
@@ -145,3 +172,3 @@ const crosswindAngle = degreeDifference(this.activeRunway.alignment, this.weather.windDirection); | ||
); | ||
const finalAltitude = (this.airport.position.elevation ?? 0) + this.configuration.patternDistance * glideSlope; | ||
const finalAltitude = (this.airport.position.elevation ?? 0) + finalDistance * glideSlope; | ||
activeRunwayFinal.elevation = Math.min(finalAltitude, patternAltitude); | ||
@@ -151,3 +178,3 @@ | ||
const activeRunwayBase = activeRunwayFinal.getPointBy(new Vector(downwindDistance, patternOrientation)); | ||
const baseAltitude = finalAltitude + this.configuration.patternDistance * glideSlope; | ||
const baseAltitude = finalAltitude + downwindDistance * glideSlope; | ||
activeRunwayBase.elevation = Math.min(baseAltitude, patternAltitude); | ||
@@ -187,2 +214,12 @@ | ||
]; | ||
this.entryWaypoint = { | ||
id: this.activeRunway.id + "-VENTRY", | ||
position: activeRunwayEntry.getPointBy( | ||
new Vector( | ||
0.5 * Units.meterPerNauticalMile, | ||
Degree(patternOrientation + (this.activeRunway.isRightPattern ? -45 : 45)), | ||
), | ||
), | ||
}; | ||
} | ||
@@ -189,0 +226,0 @@ |
{ | ||
"name": "@fboes/aerofly-patterns", | ||
"version": "2.1.2", | ||
"version": "2.2.0", | ||
"description": "Landegerät - Create landing pattern lessons for Aerofly FS 4.", | ||
@@ -5,0 +5,0 @@ "main": "dist/index.js", |
@@ -35,3 +35,3 @@ # Aerofly Landegerät | ||
Example value: 24,33 | ||
--min-altitude=.. Minimum safe altitude of aircraft, in 100ft. At least airport elevation. | ||
--min-altitude=.. Minimum safe altitude of aircraft, in 100ft MSL. At least airport elevation. | ||
Default value: 0 | ||
@@ -41,6 +41,10 @@ Example value: 145 | ||
Default value: 10 | ||
--distance=.. Initial distance from airport in Nautical Miles. | ||
--distance=.. Initial aircraft distance from airport in Nautical Miles. | ||
Default value: 8 | ||
--pattern-altitude=.. Pattern altitude in ft AGL. For MSL see `--pattern-altitude-msl` | ||
Default value: 1000 | ||
--pattern-distance=.. Pattern distance from airport runway in Nautical Miles. | ||
Default value: 1 | ||
--pattern-final-distance=.. Pattern final distance from airport runway edge in Nautical Miles. | ||
Default value: 1 | ||
--rnd-heading=.. Randomized aircraft heading deviation from direct heading to airport in degree. | ||
@@ -50,2 +54,3 @@ Default value: 0 | ||
Example value: 24,33 | ||
--pattern-altitude-msl, -m Pattern altitude is in MSL instead of AGL | ||
--directory, -d Create files in a subdirectory instead of current directory. | ||
@@ -52,0 +57,0 @@ --geojson, -g Create a GeoJSON file. |
84543
2383
110