New Case Study:See how Anthropic automated 95% of dependency reviews with Socket.Learn More
Socket
Sign inDemoInstall
Socket

@fboes/aerofly-patterns

Package Overview
Dependencies
Maintainers
0
Versions
30
Alerts
File Explorer

Advanced tools

Socket logo

Install Socket

Detect and block malicious and high-risk dependencies

Install

@fboes/aerofly-patterns - npm Package Compare versions

Comparing version 2.5.3 to 2.5.4

dist/lib/general/ConfigurationAbstract.js

10

CHANGELOG.md
# Changelog
# 2.5.4
- Added option to add approaches to HEMS missions
- Improved Markdown table output
- Refactored location methods
- Changed to `init` pattern for `async` class instantiation
- Extended San Francisco POIs
- Improved CLI output
- Added improved `GeoJsonLocation` to incorporate `Point` geo positions
# 2.5.3

@@ -4,0 +14,0 @@

4

dist/data/hems/MissionTypes.js

@@ -115,7 +115,7 @@ // @ts-check

*
* @param {import("../../lib/hems/GeoJsonLocations").GeoJsonFeature} location
* @param {import("../../lib/hems/GeoJsonLocations").GeoJsonLocation} location
* @returns {MissionType}
*/
get(location) {
switch (location.properties["marker-symbol"]) {
switch (location.markerSymbol) {
case `hospital`:

@@ -122,0 +122,0 @@ return MissionTypes.patientTransfer;

@@ -12,13 +12,5 @@ #!/usr/bin/env node

process.stdout
.write(`\x1b[94mUsage: npx -p @fboes/aerofly-patterns@latest aerofly-hems GEOJSON_FILE [AFS_AIRCRAFT_CODE] [AFS_LIVERY_CODE] [...options]\x1b[0m
Create landing pattern lessons for Aerofly FS 4.
.write(`\x1b[94mUsage: npx -p ${configuration.name}@latest aerofly-hems GEOJSON_FILE [AFS_AIRCRAFT_CODE] [AFS_LIVERY_CODE] [...options]\x1b[0m
${configuration.helpText}
Arguments:
\x1b[94m GEOJSON_FILE \x1b[0mGeoJSON file containing possible mission locations.
\x1b[94m AFS_AIRCRAFT_CODE \x1b[0mInternal aircraft code in Aerofly FS 4. Defaults to "ec135".
\x1b[94m AFS_LIVERY_CODE \x1b[0mInternal aircraft code in Aerofly FS 4. Defaults to "adac".
Options:
${Configuration.argumentList()}
`);

@@ -28,6 +20,5 @@ process.exit(0);

const app = new AeroflyHems(configuration);
await app.build();
const app = await AeroflyHems.init(configuration);
await FileWriter.writeFile(app, process.cwd());
console.log(`✅ Done`);

@@ -12,13 +12,5 @@ #!/usr/bin/env node

process.stdout
.write(`\x1b[94mUsage: npx @fboes/aerofly-patterns@latest ICAO_AIRPORT_CODE [AFS_AIRCRAFT_CODE] [AFS_LIVERY_CODE] [...options]\x1b[0m
Create landing pattern lessons for Aerofly FS 4.
.write(`\x1b[94mUsage: npx ${configuration.name}@latest ICAO_AIRPORT_CODE [AFS_AIRCRAFT_CODE] [AFS_LIVERY_CODE] [...options]\x1b[0m
${configuration.helpText}
Arguments:
\x1b[94m ICAO_AIRPORT_CODE \x1b[0mICAO airport code which needs to be available in Aerofly FS 4.
\x1b[94m AFS_AIRCRAFT_CODE \x1b[0mInternal aircraft code in Aerofly FS 4.
\x1b[94m AFS_LIVERY_CODE \x1b[0mInternal aircraft code in Aerofly FS 4.
Options:
${Configuration.argumentList()}
`);

@@ -28,6 +20,5 @@ process.exit(0);

const app = new AeroflyPatterns(configuration);
await app.build();
const app = await AeroflyPatterns.init(configuration);
await FileWriter.writeFile(app, process.cwd());
console.log(`✅ Done with ${app.airport?.name} (${app.airport?.id})`);

@@ -7,4 +7,5 @@ // @ts-check

import { FormatterTest } from "./lib/general/Formatter.test.js";
import AeroflyMissionAutofillTest from "./lib/general/AeroflyMissionAutofill.test.js";
import GeoJsonLocationsTest from "./lib/hems/GeoJsonLocations.test.js";
import { AeroflyMissionAutofillTest } from "./lib/general/AeroflyMissionAutofill.test.js";
import { GeoJsonLocationsTest, GeoJsonLocationTest } from "./lib/hems/GeoJsonLocations.test.js";
import { MarkdownTest } from "./lib/general/Markdown.test.js";

@@ -18,2 +19,5 @@ new AirportTest();

new GeoJsonLocationsTest();
new GeoJsonLocationTest();
new MarkdownTest();
process.exit();

@@ -6,3 +6,3 @@ // @ts-check

export default class AeroflyMissionAutofill {
export class AeroflyMissionAutofill {
/**

@@ -239,3 +239,3 @@ * @type {AeroflyMission}

case "takeoff":
return "read for take-off";
return "ready for take-off";
case "cruise":

@@ -242,0 +242,0 @@ return "cruising";

@@ -5,5 +5,5 @@ // @ts-check

import { strict as assert } from "node:assert";
import AeroflyMissionAutofill from "./AeroflyMissionAutofill.js";
import { AeroflyMissionAutofill } from "./AeroflyMissionAutofill.js";
export default class AeroflyMissionAutofillTest {
export class AeroflyMissionAutofillTest {
constructor() {

@@ -10,0 +10,0 @@ this.checkConversion();

@@ -11,2 +11,3 @@ // @ts-check

import { AeroflyTslGenerator } from "./AeroflyTslGenerator.js";
import { Markdown } from "../general/Markdown.js";

@@ -48,14 +49,19 @@ export class AeroflyHems {

async build() {
this.locations = new GeoJsonLocations(this.configuration.geoJsonFile);
this.nauticalTimezone = Math.round((this.locations.heliports[0]?.geometry?.coordinates[0] ?? 0) / 15);
/**
*
* @param {Configuration} configuration
* @returns {Promise<AeroflyHems>}
*/
static async init(configuration) {
const self = new AeroflyHems(configuration);
self.locations = new GeoJsonLocations(self.configuration.geoJsonFile);
self.nauticalTimezone = Math.round((self.locations.heliports[0]?.coordinates.longitude ?? 0) / 15);
const dateYielder = new DateYielder(this.configuration.numberOfMissions, this.nauticalTimezone);
const dateYielder = new DateYielder(self.configuration.numberOfMissions, self.nauticalTimezone);
const dates = dateYielder.entries();
let index = 0;
for (const date of dates) {
const scenario = new Scenario(this.locations, this.configuration, this.aircraft, date, index++);
try {
await scenario.build();
this.scenarios.push(scenario);
const scenario = await Scenario.init(self.locations, self.configuration, self.aircraft, date, index++);
self.scenarios.push(scenario);
} catch (error) {

@@ -66,5 +72,7 @@ console.error(error);

if (this.scenarios.length === 0) {
if (self.scenarios.length === 0) {
throw Error("No scenarios generated, possibly because of missing weather data");
}
return self;
}

@@ -100,17 +108,24 @@

.map((s) => {
const markdownTable = Markdown.table([
["Departure", "Duration", "Flight distance"],
["---------", "--------", "---------------"],
[
s.mission.origin.icao,
`${Math.ceil((s.mission.duration ?? 0) / 60)} min`,
`${Math.ceil((s.mission.distance ?? 0) / 1000)} km`,
],
]);
return `\
### ${s.mission.title}
| Departure | Duration | Flight distance |
| --------- | -------- | --------------- |
| ${s.mission.origin.icao} | ${Math.ceil((s.mission.duration ?? 0) / 60)} min | ${Math.ceil((s.mission.distance ?? 0) / 1000)} km |
${markdownTable}
${s.mission.description.replace(/\n/g, " \n")}
`;
${s.mission.description.replace(/\n/g, " \n")}`;
})
.join("\n");
.join("\n\n");
const featuredSitesMarkdown = this.locations?.heliportsAndHospitals
.map((l) => {
const title = l.properties.url ? `[${l.properties.title}](${l.properties.url})` : l.properties.title;
const title = l.url ? `[${l.title}](${l.url})` : l.title;
return "- " + title;

@@ -123,3 +138,3 @@ })

This file contains ${this.configuration.numberOfMissions} Helicopter Emergency Medical Service (HEMS) missions for the ${this.aircraft.name} starting at ${this.locations?.heliports[0]?.properties?.title ?? "a random heliport"}.
This file contains ${this.configuration.numberOfMissions} Helicopter Emergency Medical Service (HEMS) missions for the ${this.aircraft.name} starting at ${this.locations?.heliports[0]?.title ?? "a random heliport"}.

@@ -150,3 +165,3 @@ - See [the installation instructions](https://fboes.github.io/aerofly-missions/docs/generic-installation.html) on how to import [the missions into Aerofly FS 4](missions/custom_missions_user.tmc) and all other files.

getEmergencySitesFolderSuffix() {
const coordinates = this.locations?.heliports[0].geometry.coordinates;
const coordinates = this.locations?.heliports[0].coordinates;
if (!coordinates) {

@@ -157,6 +172,6 @@ return "";

return (
(coordinates[0] > 0 ? "e" : "w") +
String(Math.abs(Math.round(coordinates[0] * 100))).padStart(5, "0") +
(coordinates[1] > 0 ? "n" : "s") +
String(Math.abs(Math.round(coordinates[1] * 100))).padStart(4, "0") +
(coordinates.longitude > 0 ? "e" : "w") +
String(Math.abs(Math.round(coordinates.longitude * 100))).padStart(5, "0") +
(coordinates.latitude > 0 ? "n" : "s") +
String(Math.abs(Math.round(coordinates.latitude * 100))).padStart(4, "0") +
"_"

@@ -163,0 +178,0 @@ );

@@ -8,3 +8,3 @@ // @ts-check

*
* @param {import("./GeoJsonLocations").GeoJsonFeature[]} locations
* @param {import("./GeoJsonLocations").GeoJsonLocation[]} locations
*/

@@ -32,3 +32,3 @@ constructor(locations) {

<[vector3_float64][position][${coordinates.longitude} ${coordinates.latitude} 0]>
<[float64][direction][${location.properties?.direction ?? 0}]>
<[float64][direction][${location.direction}]>
<[string8u][name][${object.xref}]>

@@ -58,3 +58,3 @@ >`);

*
* @param {import("./GeoJsonLocations").GeoJsonFeature} location
* @param {import("./GeoJsonLocations").GeoJsonLocation} location
* @param {number} index

@@ -68,4 +68,4 @@ * @returns {{

return {
longitude: location.geometry.coordinates[0] + 0.00035 * index,
latitude: location.geometry.coordinates[1] + 0.000064 * index,
longitude: location.coordinates.longitude + 0.00035 * index,
latitude: location.coordinates.latitude + 0.000064 * index,
};

@@ -72,0 +72,0 @@ }

@@ -6,3 +6,3 @@ // @ts-check

*
* @param {import("./GeoJsonLocations").GeoJsonFeature[]} locations
* @param {import("./GeoJsonLocations").GeoJsonLocation[]} locations
* @param {string} environmentId

@@ -22,3 +22,3 @@ */

<[tmsimulator_scenery_object][element][0]
<[vector3_float64][position][${location.geometry.coordinates[0]} ${location.geometry.coordinates[1]} -10]>
<[vector3_float64][position][${location.coordinates.longitude} ${location.coordinates.latitude} -10]>
<[int32][autoheight_override][-1]>

@@ -25,0 +25,0 @@ <[string8][geometry][fallback/fallback]>

// @ts-check
import { parseArgs } from "node:util";
import * as path from "node:path";
import { fileURLToPath } from "node:url";
import { ConfigurationAbstract } from "../general/ConfigurationAbstract.js";
/**
* @typedef ParseArgsParameters
* @type {object}
* @property {"string"|"boolean"} type
* @property {string} [short]
* @property {boolean|string} [default]
* @property {string} [description]
* @property {string} [example]
*/
export class Configuration {
export class Configuration extends ConfigurationAbstract {
/**
* @type {{[key:string]: ParseArgsParameters}}
*/
static options = {
"metar-icao": {
type: "string",
short: "m",
default: "",
description: "Use this ICAO station code to find weather reports",
example: "EHAM",
},
missions: {
type: "string",
default: "10",
description: "Number of missions in file.",
},
callsign: {
type: "string",
default: "MEDEVAC",
description: "Optional callsign, else default callsign will be used.",
},
"no-guides": {
type: "boolean",
default: false,
description: "Try to remove virtual guides from missions.",
},
"cold-dark": {
type: "boolean",
default: false,
description: "Start cold & dark.",
},
transfer: {
type: "boolean",
short: "t",
default: false,
description: "Mission types can also be transfers.",
},
"no-poi": {
type: "boolean",
short: "p",
default: false,
description: "Do not generate POI files.",
},
directory: {
type: "boolean",
short: "d",
default: false,
description: "Create files in another directory instead of current directory.",
},
help: {
type: "boolean",
short: "h",
default: false,
description: "Will output the help.",
},
};
/**
*

@@ -80,9 +13,83 @@ * @param {string[]} args

constructor(args) {
const { values, positionals } = parseArgs({
args: args.slice(2),
options: Configuration.options,
allowPositionals: true,
});
super();
/**
* @type {import("../general/ConfigurationAbstract").ConfigurationPositional[]}
*/
this._arguments = [
{
name: "GEOJSON_FILE",
description: "GeoJSON file containing possible mission locations.",
},
{ name: "AFS_AIRCRAFT_CODE", description: "Internal aircraft code in Aerofly FS 4.", default: "ec135" },
{ name: "AFS_LIVERY_CODE", description: "Internal livery code in Aerofly FS 4", default: "adac" },
];
/**
* @type {{[key:string]: import("../general/ConfigurationAbstract").ParseArgsParameters}}
*/
this._options = {
"metar-icao": {
type: "string",
short: "m",
default: "",
description: "Use this ICAO station code to find weather reports",
example: "EHAM",
},
missions: {
type: "string",
default: "10",
description: "Number of missions in file.",
},
callsign: {
type: "string",
default: "MEDEVAC",
description: "Optional callsign, else default callsign will be used.",
},
"no-guides": {
type: "boolean",
default: false,
description: "Try to remove virtual guides from missions.",
},
"cold-dark": {
type: "boolean",
short: "c",
default: false,
description: "Start cold & dark.",
},
transfer: {
type: "boolean",
short: "t",
default: false,
description: "Mission types can also be transfers.",
},
approach: {
type: "boolean",
short: "a",
default: false,
description: "Add approach guides to flight plan.",
},
"no-poi": {
type: "boolean",
short: "p",
default: false,
description: "Do not generate POI files.",
},
directory: {
type: "boolean",
short: "d",
default: false,
description: "Create files in another directory instead of current directory.",
},
help: {
type: "boolean",
short: "h",
default: false,
description: "Will output the help.",
},
};
const { values, positionals } = this.parseArgs(args);
/**
* @type {string}

@@ -155,4 +162,9 @@ */

/**
* @type {boolean} missions types can also be "transfer"
* @type {boolean} Add approach guides to flight plan.
*/
this.withApproaches = Boolean(values["approach"]);
/**
* @type {boolean} Do not generate POI files.
*/
this.doNotGeneratePois = Boolean(values["no-poi"]);

@@ -165,38 +177,2 @@

}
/**
* @returns {string}
*/
static argumentList() {
/**
* @type {string[]}
*/
let parameters = [];
for (let parameterName in Configuration.options) {
const option = Configuration.options[parameterName];
let parameter = `--${parameterName}`;
if (option.type === "string") {
parameter += "=..";
}
if (option.short) {
parameter += `, -${option.short}`;
if (option.type === "string") {
parameter += "=..";
}
}
parameters.push(`\x1b[94m ${parameter.padEnd(24, " ")} \x1b[0m ${option.description}`);
if (option.default) {
parameters.push(` Default value: \x1b[4m${option.default}\x1b[0m`);
}
if (option.example) {
parameters.push(` Example value: \x1b[4m${option.example}\x1b[0m`);
}
}
return parameters.join("\n");
}
}
// @ts-check
import { Point, Vector } from "@fboes/geojson";
import * as fs from "node:fs";

@@ -49,19 +50,19 @@

const pointFeatures = featureCollection.features.filter((f) => {
return f.type === "Feature" && f.geometry.type === "Point";
});
const pointFeatures = featureCollection.features
.filter((f) => {
return f.type === "Feature" && f.geometry.type === "Point";
})
.map((f) => {
return new GeoJsonLocation(f);
});
if (pointFeatures.length === 0) {
throw Error("Missing Features in GeoJson file");
}
this.#validateGeoJsonFeatures(pointFeatures);
/**
* @type {GeoJsonFeature[]}
* @type {GeoJsonLocation[]}
*/
this.heliports = pointFeatures.filter((f) => {
return (
f.properties &&
(f.properties["marker-symbol"] === GeoJsonLocations.MARKER_HELIPORT ||
f.properties["marker-symbol"] === GeoJsonLocations.MARKER_HELIPORT_HOSPITAL)
);
return f.isHeliport;
});

@@ -73,10 +74,6 @@ if (this.heliports.length === 0) {

/**
* @type {GeoJsonFeature[]}
* @type {GeoJsonLocation[]}
*/
this.hospitals = pointFeatures.filter((f) => {
return (
f.properties &&
(f.properties["marker-symbol"] === GeoJsonLocations.MARKER_HOSPITAL ||
f.properties["marker-symbol"] === GeoJsonLocations.MARKER_HELIPORT_HOSPITAL)
);
return f.isHospital;
});

@@ -88,11 +85,6 @@ if (this.hospitals.length === 0) {

/**
* @type {GeoJsonFeature[]}
* @type {GeoJsonLocation[]}
*/
this.other = pointFeatures.filter((f) => {
return (
f.properties &&
f.properties["marker-symbol"] !== GeoJsonLocations.MARKER_HELIPORT &&
f.properties["marker-symbol"] !== GeoJsonLocations.MARKER_HOSPITAL &&
f.properties["marker-symbol"] !== GeoJsonLocations.MARKER_HELIPORT_HOSPITAL
);
return !f.isHeliport && !f.isHospital;
});

@@ -104,3 +96,3 @@ if (this.other.length === 0) {

/**
* @type {Generator<GeoJsonFeature, void, unknown>}
* @type {Generator<GeoJsonLocation, void, unknown>}
*/

@@ -112,3 +104,3 @@

/**
* @returns {GeoJsonFeature[]}
* @returns {GeoJsonLocation[]}
*/

@@ -118,3 +110,3 @@ get heliportsAndHospitals() {

this.hospitals?.filter((l) => {
return l.properties["marker-symbol"] !== GeoJsonLocations.MARKER_HELIPORT_HOSPITAL;
return l.markerSymbol !== GeoJsonLocations.MARKER_HELIPORT_HOSPITAL;
}) ?? [],

@@ -125,13 +117,4 @@ );

/**
*
* @param {GeoJsonFeature} location
* @returns {boolean}
*/
static isHeliportHospital(location) {
return location.properties["marker-symbol"] === GeoJsonLocations.MARKER_HELIPORT_HOSPITAL;
}
/**
* Infinite generator of randomized `this.other`. On end of list will return to beginning, but keeping the random order.
* @yields {GeoJsonFeature}
* @yields {GeoJsonLocation}
* @generator

@@ -143,3 +126,7 @@ */

let temp;
const emergencySites = structuredClone(this.other);
//const emergencySites = structuredClone(this.other);
/**
* @type {number[]}
*/
const emergencySiteIndexes = [...Array(i).keys()];

@@ -150,10 +137,10 @@ while (i--) {

// swap randomly chosen element with current element
temp = emergencySites[i];
emergencySites[i] = emergencySites[j];
emergencySites[j] = temp;
temp = emergencySiteIndexes[i];
emergencySiteIndexes[i] = emergencySiteIndexes[j];
emergencySiteIndexes[j] = temp;
}
while (emergencySites.length) {
for (const location of emergencySites) {
yield location;
while (emergencySiteIndexes.length) {
for (const locationIndex of emergencySiteIndexes) {
yield this.other[locationIndex];
}

@@ -164,14 +151,178 @@ }

/**
* @param {GeoJsonFeature[]} geoJsonFeatures
*
* @param {GeoJsonLocation} location
* @returns {GeoJsonLocation}
*/
#validateGeoJsonFeatures(geoJsonFeatures) {
for (const geoJsonFeature of geoJsonFeatures) {
if (!geoJsonFeature.properties.title) {
throw Error(`Missing properties.title in GeoJSONFeature ${geoJsonFeature.id}`);
getNearesHospital(location) {
/** @type {number?} */
let distance = null;
let nearestLocation = this.hospitals[0];
for (const testLocation of this.hospitals) {
const vector = location.coordinates.getVectorTo(testLocation.coordinates);
if (distance === null || vector.meters < distance) {
nearestLocation = testLocation;
distance = vector.meters;
}
if (!geoJsonFeature.geometry.coordinates) {
throw Error(`Missing properties.geometry.coordinates in GeoJSONFeature ${geoJsonFeature.id}`);
}
}
return nearestLocation;
}
/**
* @param {GeoJsonLocation?} butNot
* @returns {GeoJsonLocation}
*/
getRandHospital(butNot = null) {
return this.getRandLocation(this.hospitals, butNot);
}
/**
* @returns {GeoJsonLocation} heliports or hospitals with heliport
*/
getRandHeliport() {
return this.getRandLocation(this.heliports);
}
/**
* @param {GeoJsonLocation[]} locations
* @param {GeoJsonLocation?} butNot
* @returns {GeoJsonLocation}
*/
getRandLocation(locations, butNot = null) {
if (butNot && locations.length < 2) {
throw Error("Not enough locations to search for an alternate");
}
let location = null;
do {
location = locations[Math.floor(Math.random() * locations.length)];
} while (butNot && location.title === butNot?.title);
return location;
}
}
export class GeoJsonLocation {
/**
* @param {object} json
*/
constructor(json) {
if (!json.properties.title) {
throw Error(`Missing properties.title in GeoJSONFeature ${json.id}`);
}
if (!json.geometry.coordinates) {
throw Error(`Missing properties.geometry.coordinates in GeoJSONFeature ${json.id}`);
}
/**
* @type {string}
*/
this.type = json.type;
/**
* @type {string?}
*/
this.id = json.id ?? null;
this.coordinates = new Point(
json.geometry.coordinates[0],
json.geometry.coordinates[1],
json.geometry.coordinates[2] ?? null,
);
/**
* @type {string}
*/
this.markerSymbol = json.properties["marker-symbol"] ?? "";
/**
* @type {string}
*/
this.title = json.properties.title;
/**
* @type {string?}
*/
this.icaoCode = json.properties.icaoCode?.replace(/[-]+/g, "") || null;
if (this.icaoCode !== null && !this.icaoCode.match(/^[a-zA-Z0-9-+]+$/)) {
throw new Error("Invalid icaoCode: " + this.icaoCode);
}
/**
* @type {number}
*/
this.direction = json.properties.direction ?? 0;
/**
* @type {number[]}
*/
this.approaches = json.properties.approaches ?? [];
if (
json.properties.approaches == undefined &&
json.properties.direction !== undefined &&
json.properties.icaoCode !== undefined
) {
this.approaches = [json.properties.direction, (json.properties.direction + 180) % 360];
}
/**
* @type {string?}
*/
this.url = json.properties.url ?? null;
}
/**
* @returns {boolean}
*/
get isHeliport() {
return (
this.markerSymbol === GeoJsonLocations.MARKER_HELIPORT ||
this.markerSymbol === GeoJsonLocations.MARKER_HELIPORT_HOSPITAL
);
}
/**
* @returns {boolean}
*/
get isHospital() {
return (
this.markerSymbol === GeoJsonLocations.MARKER_HOSPITAL ||
this.markerSymbol === GeoJsonLocations.MARKER_HELIPORT_HOSPITAL
);
}
/**
* @returns {string}
*/
get checkPointName() {
if (this.icaoCode) {
return this.icaoCode.toUpperCase();
}
let name = this.isHospital ? "HOSPITAL" : "EVAC";
return ("W-" + name).toUpperCase().replace(/[^A-Z0-9-+]/, "");
}
/**
*
* @param {string} title
* @param {Vector?} vector
* @param {number} [altitudeChange] in feet
* @returns {GeoJsonLocation}
*/
clone(title = "", vector = null, altitudeChange = 0) {
const coordinates = vector ? this.coordinates.getPointBy(vector) : this.coordinates;
let altitude = (coordinates.elevation ?? 0) * 3.28084; // in feet
altitude += altitudeChange; // plus feet
altitude = Math.ceil(altitude / 100) * 100; // rounded to the next 100ft
altitude /= 3.28084; // in meters
return new GeoJsonLocation({
properties: {
title: title,
icaoCode: title,
},
geometry: {
coordinates: [coordinates.longitude, coordinates.latitude, altitude],
},
});
}
}

@@ -5,6 +5,6 @@ //@ts-check

import { fileURLToPath } from "node:url";
import { GeoJsonLocations } from "./GeoJsonLocations.js";
import { GeoJsonLocation, GeoJsonLocations } from "./GeoJsonLocations.js";
import { strict as assert } from "node:assert";
export default class GeoJsonLocationsTest {
export class GeoJsonLocationsTest {
constructor() {

@@ -24,3 +24,3 @@ /**

while (i--) {
assert.ok(g.randomEmergencySite.next().value?.properties?.title);
assert.ok(g.randomEmergencySite.next().value?.title);
}

@@ -30,1 +30,155 @@ console.log(`✅ ${this.constructor.name}.testRandomEmergencySites successful`);

}
export class GeoJsonLocationTest {
constructor() {
this.testApproaches();
this.tesIcaoCode();
}
testApproaches() {
{
const json = {
properties: {
title: "Test",
icaoCode: "TEST",
approaches: [1, 2],
},
geometry: {
coordinates: [1, 2, 3],
},
};
const location = new GeoJsonLocation(json);
assert.deepStrictEqual(location.title, "Test");
assert.deepStrictEqual(location.icaoCode, "TEST");
assert.deepStrictEqual(location.approaches, [1, 2]);
}
{
const json = {
properties: {
title: "Test",
icaoCode: "TEST",
approaches: [0],
},
geometry: {
coordinates: [1, 2, 3],
},
};
const location = new GeoJsonLocation(json);
assert.deepStrictEqual(location.approaches, [0]);
}
{
const json = {
properties: {
title: "Test",
icaoCode: "TEST",
direction: 0,
},
geometry: {
coordinates: [1, 2, 3],
},
};
const location = new GeoJsonLocation(json);
assert.deepStrictEqual(location.approaches, [0, 180]);
}
{
const json = {
properties: {
title: "Test",
icaoCode: "TEST",
},
geometry: {
coordinates: [1, 2, 3],
},
};
const location = new GeoJsonLocation(json);
assert.deepStrictEqual(location.approaches, []);
}
{
const json = {
properties: {
title: "Test",
direction: 0,
},
geometry: {
coordinates: [1, 2, 3],
},
};
const location = new GeoJsonLocation(json);
assert.deepStrictEqual(location.approaches, []);
}
console.log(`✅ ${this.constructor.name}.testApproaches successful`);
}
tesIcaoCode() {
{
const json = {
properties: {
title: "Test",
icaoCode: "",
},
geometry: {
coordinates: [1, 2, 3],
},
};
const location = new GeoJsonLocation(json);
assert.strictEqual(location.icaoCode, null);
}
{
const json = {
properties: {
title: "Test",
icaoCode: "TEST",
},
geometry: {
coordinates: [1, 2, 3],
},
};
const location = new GeoJsonLocation(json);
assert.strictEqual(location.icaoCode, "TEST");
}
{
const json = {
properties: {
title: "Test",
icaoCode: "TE-ST-123",
},
geometry: {
coordinates: [1, 2, 3],
},
};
const location = new GeoJsonLocation(json);
assert.strictEqual(location.icaoCode, "TEST123");
}
assert.throws(() => {
const json = {
properties: {
title: "Test",
icaoCode: "ü-()",
},
geometry: {
coordinates: [1, 2, 3],
},
};
const location = new GeoJsonLocation(json);
assert.ok(location.icaoCode);
});
console.log(`✅ ${this.constructor.name}.tesIcaoCode successful`);
}
}

@@ -9,6 +9,7 @@ import {

import { AviationWeatherApi, AviationWeatherNormalizedMetar } from "../general/AviationWeatherApi.js";
import AeroflyMissionAutofill from "../general/AeroflyMissionAutofill.js";
import { Point } from "@fboes/geojson";
import { AeroflyMissionAutofill } from "../general/AeroflyMissionAutofill.js";
import { MissionTypeFinder } from "../../data/hems/MissionTypes.js";
import { GeoJsonLocations } from "./GeoJsonLocations.js";
import { GeoJsonLocation } from "./GeoJsonLocations.js";
import { Vector } from "@fboes/geojson";
import { degreeDifference } from "../general/Degree.js";

@@ -22,15 +23,33 @@ export class Scenario {

* @param {number} index
* @returns {Promise<Scenario>}
*/
constructor(locations, configuration, aircraft, time, index = 0) {
/**
* @type {Configuration}
*/
this.configuration = configuration;
static async init(locations, configuration, aircraft, time, index = 0) {
const missionLocations = Scenario.getMissionLocations(
locations,
configuration.canTransfer && locations.hospitals.length > 1 && Math.random() <= 0.1,
);
/**
* @type {import('./GeoJsonLocations.js').GeoJsonFeature}
*/
this.origin = this.#getRandLocation(locations.heliports);
this.#checkIcao(this.origin);
const metarIcaoCode = configuration.icaoCode ?? missionLocations[0].icao;
if (metarIcaoCode === null) {
throw new Error("No ICAO code for METAR informaton found");
}
const weathers = await AviationWeatherApi.fetchMetar([metarIcaoCode], time);
if (!weathers.length) {
throw new Error("No METAR information from API for " + metarIcaoCode);
}
const weather = new AviationWeatherNormalizedMetar(weathers[0]);
return new Scenario(missionLocations, configuration, aircraft, time, weather, index);
}
/**
* @param {import('./GeoJsonLocations.js').GeoJsonLocations} missionLocations
* @param {Configuration} configuration
* @param {import('../../data/AeroflyAircraft.js').AeroflyAircraft} aircraft
* @param {Date} time
* @param {AviationWeatherNormalizedMetar} weather
* @param {number} index
*/
constructor(missionLocations, configuration, aircraft, time, weather, index = 0) {
/**

@@ -46,70 +65,12 @@ * @type {Date}

const isTransfer = this.configuration.canTransfer && locations.hospitals.length > 1 && Math.random() <= 0.1;
/**
* @type {import("./GeoJsonLocations.js").GeoJsonFeature}
*/
const waypoint1 = isTransfer
? this.#getRandLocation(locations.hospitals)
: locations.randomEmergencySite.next().value;
/**
* @type {import("./GeoJsonLocations.js").GeoJsonFeature}
*/
let waypoint2 = isTransfer
? this.#getRandLocation(locations.hospitals, waypoint1)
: this.#getNearestLocation(locations.hospitals, waypoint1);
const mission = MissionTypeFinder.get(missionLocations[1]);
const bringPatientToOrigin =
GeoJsonLocations.isHeliportHospital(this.origin) && !GeoJsonLocations.isHeliportHospital(waypoint2);
const destination = bringPatientToOrigin ? this.origin : this.#getRandLocation(locations.heliports);
this.#checkIcao(destination);
if (bringPatientToOrigin) {
waypoint2 = destination;
}
const checkpoints = bringPatientToOrigin
? [
this.#makeCheckpoint(this.origin, "origin"),
this.#makeCheckpoint(waypoint1),
this.#makeCheckpoint(destination, "destination"),
]
: [
this.#makeCheckpoint(this.origin, "origin"),
this.#makeCheckpoint(waypoint1),
this.#makeCheckpoint(waypoint2),
this.#makeCheckpoint(destination, "destination"),
];
// Building the actual mission
const title = this.#getTitle(index, mission, missionLocations);
const description = this.#getDescription(mission, missionLocations);
const conditions = this.#makeConditions(time, weather);
const origin = this.#makeMissionPosition(missionLocations[0]);
const destination = this.#makeMissionPosition(missionLocations[missionLocations.length - 1]);
const checkpoints = this.#getCheckpoints(missionLocations, configuration.withApproaches ? weather : null);
const conditions = new AeroflyMissionConditions({
time,
});
const mission = MissionTypeFinder.get(waypoint1);
const title =
`HEMS #${index + 1}: ` +
mission.title.replace(/\$\{(.+?)\}/g, (matches, variableName) => {
const location = variableName === "origin" ? waypoint1 : waypoint2;
return location.properties.title;
});
const description = mission.description.replace(/\$\{(.+?)\}/g, (matches, variableName) => {
const location = variableName === "origin" ? waypoint1 : waypoint2;
let description = location.properties.title;
if (location.properties.icaoCode) {
description += ` (${location.properties.icaoCode})`;
}
if (location.properties.approaches || location.properties.direction) {
let approaches = location.properties.approaches ?? [
location.properties.direction,
(location.properties.direction + 180) % 360,
];
description += ` with possible approaches ${approaches
.map((a) => {
return `${String(Math.round(a)).padStart(3, "0")}°`;
})
.join(" / ")}`;
}
return description;
});
this.mission = new AeroflyMission(title, {

@@ -120,49 +81,13 @@ description,

icao: aircraft.icaoCode,
livery: this.configuration.livery,
livery: configuration.livery,
},
callsign: aircraft.callsign,
flightSetting: this.configuration.isColdAndDark ? "cold_and_dark" : "takeoff",
flightSetting: configuration.isColdAndDark ? "cold_and_dark" : "takeoff",
conditions,
tags: ["medical", "dropoff"],
origin: {
icao: this.origin.properties.icaoCode ?? this.origin.properties.title,
longitude: this.origin.geometry.coordinates[0],
latitude: this.origin.geometry.coordinates[1],
alt: this.origin.geometry.coordinates[2] ?? 0,
dir: this.origin.properties?.direction ?? 0,
},
destination: {
icao: destination.properties.icaoCode ?? destination.properties.title,
longitude: destination.geometry.coordinates[0],
latitude: destination.geometry.coordinates[1],
alt: destination.geometry.coordinates[2] ?? 0,
dir: destination.properties?.direction ?? 0,
},
origin,
destination,
checkpoints,
});
}
async build() {
const id = this.configuration.icaoCode ?? this.origin.properties.title;
if (id === null) {
return;
}
const weathers = await AviationWeatherApi.fetchMetar([id], this.date);
if (!weathers.length) {
throw new Error("No METAR information from API for " + id);
}
const weather = new AviationWeatherNormalizedMetar(weathers[0]);
this.mission.conditions.wind = {
direction: weather.wdir ?? 0,
speed: weather.wspd,
gusts: weather.wgst ?? 0,
};
this.mission.conditions.temperature = weather.temp;
this.mission.conditions.visibility_sm = Math.min(15, weather.visib);
this.mission.conditions.clouds = weather.clouds.map((c) => {
return AeroflyMissionConditionsCloud.createInFeet(c.coverOctas / 8, c.base);
});
const describer = new AeroflyMissionAutofill(this.mission);

@@ -173,3 +98,3 @@ this.mission.description = describer.description + "\n" + this.mission.description;

this.mission.duration = describer.calculateDuration(this.aircraft.cruiseSpeed);
if (this.configuration.noGuides) {
if (configuration.noGuides) {
describer.removeGuides();

@@ -179,35 +104,111 @@ }

#makeConditions(time, weather) {
return new AeroflyMissionConditions({
time,
wind: {
direction: weather.wdir ?? 0,
speed: weather.wspd,
gusts: weather.wgst ?? 0,
},
temperature: weather.temp,
visibility_sm: Math.min(15, weather.visib),
clouds: weather.clouds.map((c) => {
return AeroflyMissionConditionsCloud.createInFeet(c.coverOctas / 8, c.base);
}),
});
}
/**
* @param {import('./GeoJsonLocations.js').GeoJsonFeature[]} locations
* @param {import('./GeoJsonLocations.js').GeoJsonFeature?} butNot
* @returns {import('./GeoJsonLocations.js').GeoJsonFeature}
* @param {GeoJsonLocation[]} locations
* @param {boolean} isTransfer
* @returns {GeoJsonLocation[]}
*/
#getRandLocation(locations, butNot = null) {
if (butNot && locations.length < 2) {
throw Error("Not enough locations to search for an alternate");
static getMissionLocations(locations, isTransfer) {
/**
* @type {GeoJsonLocation[]}
*/
const missionLocations = [
locations.getRandHeliport(),
isTransfer ? locations.getRandHospital() : locations.randomEmergencySite.next().value,
];
missionLocations.push(
isTransfer ? locations.getRandHospital(missionLocations[1]) : locations.getNearesHospital(missionLocations[1]),
);
const broughtPatientToOrigin = missionLocations[0] === missionLocations[2];
if (!broughtPatientToOrigin) {
missionLocations.push(locations.getRandHeliport());
}
let location = null;
do {
location = locations[Math.floor(Math.random() * locations.length)];
} while (butNot && location.properties.title === butNot?.properties.title);
return location;
return missionLocations;
}
/**
* @param {import('./GeoJsonLocations.js').GeoJsonFeature[]} locations
* @param {import('./GeoJsonLocations.js').GeoJsonFeature} location
* @returns {import('./GeoJsonLocations.js').GeoJsonFeature}
* @param {number} index
* @param {import("../../data/hems/MissionTypes.js").MissionType} mission
* @param {GeoJsonLocation[]} missionLocations
* @returns {string}
*/
#getNearestLocation(locations, location) {
let distance = null;
let nearestLocation = locations[0];
for (const testLocation of locations) {
const testDistance = this.#getDistanceBetweenLocations(testLocation, location);
if (distance === null || testDistance < distance) {
nearestLocation = testLocation;
distance = testDistance;
#getTitle(index, mission, missionLocations) {
return (
`HEMS #${index + 1}: ` +
mission.title.replace(/\$\{(.+?)\}/g, (matches, variableName) => {
const location = variableName === "origin" ? missionLocations[1] : missionLocations[2];
return location.title;
})
);
}
/**
* @param {import("../../data/hems/MissionTypes.js").MissionType} mission
* @param {GeoJsonLocation[]} missionLocations
* @returns {string}
*/
#getDescription(mission, missionLocations) {
return mission.description.replace(/\$\{(.+?)\}/g, (matches, variableName) => {
const location = variableName === "origin" ? missionLocations[1] : missionLocations[2];
let description = location.title;
if (location.icaoCode) {
description += ` (${location.icaoCode})`;
}
if (location.approaches.length > 0) {
description += ` with possible approaches ${location.approaches
.map((a) => {
return `${String(Math.round(a)).padStart(3, "0")}°`;
})
.join(" / ")}`;
}
return description;
});
}
/**
* @param {GeoJsonLocation[]} missionLocations
* @param {AviationWeatherNormalizedMetar} [weather]
* @returns {AeroflyMissionCheckpoint[]}
*/
#getCheckpoints(missionLocations, weather = null) {
if (weather) {
const missionLocationsPlus = [];
missionLocations.forEach((missionLocation, index) => {
if (index > 0 && missionLocation.approaches.length) {
missionLocationsPlus.push(this.#getApproachLocation(missionLocation, weather));
}
missionLocationsPlus.push(missionLocation);
if (index < missionLocations.length - 1 && missionLocation.approaches.length) {
missionLocationsPlus.push(this.#getApproachLocation(missionLocation, weather, true));
}
});
missionLocations = missionLocationsPlus;
}
return nearestLocation;
return missionLocations.map((location, index) => {
let type = "waypoint";
if (index === 0) {
type = "origin";
} else if (index === missionLocations.length - 1) {
type = "destination";
}
return this.#makeCheckpoint(location, type);
});
}

@@ -217,15 +218,24 @@

*
* @param {import('./GeoJsonLocations.js').GeoJsonFeature} lastCp
* @param {import('./GeoJsonLocations.js').GeoJsonFeature} cp
* @returns {number} distance in meters
* @param {GeoJsonLocation} missionLocation
* @param {AviationWeatherNormalizedMetar} weather
* @param {boolean} asDeparture
* @returns {GeoJsonLocation}
*/
#getDistanceBetweenLocations(lastCp, cp) {
const vector = new Point(
cp.geometry.coordinates[0],
cp.geometry.coordinates[1],
cp.geometry.coordinates[2] ?? null,
).getVectorTo(
new Point(lastCp.geometry.coordinates[0], lastCp.geometry.coordinates[1], lastCp.geometry.coordinates[2] ?? null),
);
return vector.meters;
#getApproachLocation(missionLocation, weather, asDeparture = false) {
/**
* @param {number} alignment
* @returns {number}
*/
const difference = (alignment) => {
return Math.abs(degreeDifference((alignment + (asDeparture ? 180 : 0)) % 360, weather.wdir));
};
let approach = missionLocation.approaches.reduce((a, b) => {
return difference(a) < difference(b) ? a : b;
});
const course = (approach + (asDeparture ? 180 : 0)) % 360;
const vector = new Vector(1852 * (asDeparture ? 0.75 : 1.5), (approach + 180) % 360);
return missionLocation.clone(`${String(Math.round(course / 10) % 36).padStart(2, "0")}H`, vector, 500);
}

@@ -235,3 +245,18 @@

*
* @param {import('./GeoJsonLocations.js').GeoJsonFeature} location
* @param {GeoJsonLocation} location
* @returns {import("@fboes/aerofly-custom-missions").AeroflyMissionPosition}
*/
#makeMissionPosition(location) {
return {
icao: location.icaoCode ?? location.title,
longitude: location.coordinates.longitude,
latitude: location.coordinates.latitude,
alt: location.coordinates.elevation ?? 0,
dir: location.direction ?? 0,
};
}
/**
*
* @param {import('./GeoJsonLocations.js').GeoJsonLocation} location
* @param {import("@fboes/aerofly-custom-missions").AeroflyMissionCheckpointType} type

@@ -242,38 +267,12 @@ * @returns {AeroflyMissionCheckpoint}

return new AeroflyMissionCheckpoint(
this.#makeCheckpointName(location),
location.checkPointName,
type,
location.geometry.coordinates[0],
location.geometry.coordinates[1],
location.coordinates.longitude,
location.coordinates.latitude,
{
altitude: location.geometry.coordinates[2] ?? 243.83,
flyOver: true,
altitude: location.coordinates.elevation ?? 0,
flyOver: !location.checkPointName.match(/\d+H$/),
},
);
}
/**
* @param {import('./GeoJsonLocations.js').GeoJsonFeature} location
* @returns {boolean}
*/
#checkIcao(location) {
if (!location.properties.icaoCode?.match(/^[a-zA-Z0-9]+$/)) {
throw Error(`No property "icaCode" found on location ${location.properties.title}`);
}
return true;
}
/**
*
* @param {import('./GeoJsonLocations.js').GeoJsonFeature} location
* @returns {string}
*/
#makeCheckpointName(location) {
if (location.properties.icaoCode) {
return location.properties.icaoCode.toUpperCase();
}
let name = location.properties["marker-symbol"] === "hospital" ? "HOSPITAL" : "EVAC";
return ("W-" + name).toUpperCase().replace(/[^A-Z0-9-]/, "");
}
}

@@ -18,2 +18,3 @@ // @ts-check

import { Vector } from "@fboes/geojson";
import { Markdown } from "../general/Markdown.js";

@@ -49,19 +50,25 @@ /**

async build() {
const airport = await AviationWeatherApi.fetchAirports([this.configuration.icaoCode]);
/**
*
* @param {Configuration} configuration
* @returns {Promise<AeroflyPatterns>}
*/
static async init(configuration) {
const self = new AeroflyPatterns(configuration);
const airport = await AviationWeatherApi.fetchAirports([self.configuration.icaoCode]);
if (!airport.length) {
throw new Error("No airport information from API");
}
this.airport = new Airport(airport[0], this.configuration);
self.airport = new Airport(airport[0], self.configuration);
const navaids = await AviationWeatherApi.fetchNavaid(this.airport.position, 10000);
this.airport.setNavaids(navaids);
const navaids = await AviationWeatherApi.fetchNavaid(self.airport.position, 10000);
self.airport.setNavaids(navaids);
const dateYielder = new DateYielder(this.configuration.numberOfMissions, this.airport.nauticalTimezone);
const dateYielder = new DateYielder(self.configuration.numberOfMissions, self.airport.nauticalTimezone);
const dates = dateYielder.entries();
for (const date of dates) {
const scenario = new Scenario(this.airport, this.configuration, date);
try {
await scenario.build();
this.scenarios.push(scenario);
const scenario = await Scenario.init(self.airport, self.configuration, date);
self.scenarios.push(scenario);
} catch (error) {

@@ -72,5 +79,7 @@ console.error(error);

if (this.scenarios.length === 0) {
if (self.scenarios.length === 0) {
throw Error("No scenarios generated, possibly because of missing weather data");
}
return self;
}

@@ -247,15 +256,2 @@

/**
* @param {number|string|undefined} value
* @param {number} targetLength
* @param {boolean} start
* @returns {string}
*/
const pad = (value, targetLength = 2, start = false) => {
if (value === undefined) {
return "";
}
return start ? String(value).padStart(targetLength, " ") : String(value).padEnd(targetLength, " ");
};
/**
* @param {number|string} value

@@ -270,86 +266,67 @@ * @param {number} targetLength

const firstMission = this.scenarios[0];
const markdownTable = Markdown.table([
[`No `, `Local date¹`, `Local time¹`, `Wind`, `Clouds`, `Visibility`, `Runway`, `Aircraft position`],
[`:-:`, `-----------`, `----------:`, `----`, `------`, `---------:`, `------`, `-----------------`],
...this.scenarios.map((s, index) => {
const localNauticalTime = LocalTime(s.date, s.airport.nauticalTimezone);
const clouds =
s.weather?.clouds[0]?.cloudCoverCode !== "CLR"
? `${s.weather?.clouds[0]?.cloudCoverCode} @ ${s.weather?.clouds[0]?.cloudBase.toLocaleString("en")} ft`
: s.weather?.clouds[0]?.cloudCoverCode;
let output = [`# Landing Challenges: ${this.airport.name} (${this.airport.id})`];
return [
"#" + String(index + 1),
localNauticalTime.fullYear +
"-" +
padNumber(localNauticalTime.month + 1) +
"-" +
padNumber(localNauticalTime.date),
padNumber(localNauticalTime.hours) + ":" + padNumber(localNauticalTime.minutes),
!s.weather?.windSpeed ? "Calm" : `${s.weather?.windDirection}° @ ${s.weather?.windSpeed} kts`,
clouds,
Math.round(s.weather?.visibility ?? 0) + " SM",
s.activeRunway?.id + (s.activeRunway?.isRightPattern ? " (RP)" : ""),
Formatter.getDirectionArrow(s.aircraft.vectorFromAirport.bearing) +
" To the " +
Formatter.getDirection(s.aircraft.vectorFromAirport.bearing),
];
}),
]);
const airportDescription = this.airport.getDescription(firstMission.aircraft.data.hasNoRadioNav !== true);
output.push(
"",
"This [`custom_missions_user.tmc`](missions/custom_missions_user.tmc) file contains random landing scenarios for Aerofly FS 4.",
"",
`Your ${firstMission.aircraft.data.name} is ${this.configuration.initialDistance} NM away from ${this.airport.name} Airport, and you have to make a correct landing pattern entry and land safely.`,
"",
"## Airport details",
);
return `\
# Landing Challenges: ${this.airport.name} (${this.airport.id})
const airportDescription = this.airport.getDescription(firstMission.aircraft.data.hasNoRadioNav !== true);
if (airportDescription) {
output.push(airportDescription);
}
This [\`custom_missions_user.tmc\`](missions/custom_missions_user.tmc) file contains random landing scenarios for Aerofly FS 4.
output.push(
"",
`Get [more information about ${this.airport.name} Airport on SkyVector](https://skyvector.com/airport/${encodeURIComponent(this.airport.id)}):`,
"",
`- What is the tower / CTAF frequency?
Your ${firstMission.aircraft.data.name} is ${this.configuration.initialDistance} NM away from ${this.airport.name} Airport, and you have to make a correct landing pattern entry and land safely.
## Airport details
${airportDescription.trim()}
Get [more information about ${this.airport.name} Airport on SkyVector](https://skyvector.com/airport/${encodeURIComponent(this.airport.id)}):
- What is the tower / CTAF frequency?
- What is the Traffic Pattern Altitude (TPA) for this airport?
- Has the runway standard left turns, or right turns?
- Are there additional navigational aids like ILS for your assigned runways?
- Are there special noises abatement procedures in effect?`,
);
- Are there special noise abatement procedures in effect?
output.push(
"",
"## Included missions",
"",
`| No | Local date¹ | Local time¹ | Wind | Clouds | Visibility | Runway | Aircraft position |`,
`| :-: | ----------- | ----------: | ------------- | --------------- | ---------: | -------- | ------------------- |`,
);
this.scenarios.forEach((s, index) => {
const localNauticalTime = LocalTime(s.date, s.airport.nauticalTimezone);
const clouds =
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);
## Included missions
output.push(
"| " +
[
"#" + pad(index + 1),
pad(
localNauticalTime.fullYear +
"-" +
padNumber(localNauticalTime.month + 1) +
"-" +
padNumber(localNauticalTime.date),
11,
true,
),
pad(padNumber(localNauticalTime.hours) + ":" + padNumber(localNauticalTime.minutes), 11, true),
!s.weather?.windSpeed
? pad("Calm", 13)
: `${pad(s.weather?.windDirection, 3, true)}° @ ${pad(s.weather?.windSpeed, 2, true)} kts`,
clouds,
pad(Math.round(s.weather?.visibility ?? 0), 7, true) + " SM",
pad(s.activeRunway?.id + (s.activeRunway?.isRightPattern ? " (RP)" : ""), 8),
Formatter.getDirectionArrow(s.aircraft.vectorFromAirport.bearing) +
" To the " +
pad(Formatter.getDirection(s.aircraft.vectorFromAirport.bearing), 10),
].join(" | ") +
" |",
);
});
${markdownTable}
output.push(
"",
"¹) Local [nautical time](https://en.wikipedia.org/wiki/Nautical_time)",
"",
"## Installation instructions",
"",
"1. Download the [`custom_missions_user.tmc`](missions/custom_missions_user.tmc)",
`2. See [the installation instructions](https://fboes.github.io/aerofly-missions/docs/generic-installation.html) on how to import the missions into Aerofly FS 4.`,
);
¹) Local [nautical time](https://en.wikipedia.org/wiki/Nautical_time)
output.push("", `---`, ``, `Created with [Aerofly Landegerät](https://github.com/fboes/aerofly-patterns)`, ``);
## Installation instructions
return output.join("\n");
1. Download the [\`custom_missions_user.tmc\`](missions/custom_missions_user.tmc)
2. See [the installation instructions](https://fboes.github.io/aerofly-missions/docs/generic-installation.html) on how to import the missions into Aerofly FS 4.
---
Created with [Aerofly Landegerät](https://github.com/fboes/aerofly-patterns)
`;
}
}
// @ts-check
import { parseArgs } from "node:util";
import { ConfigurationAbstract } from "../general/ConfigurationAbstract.js";
/**
* @typedef ParseArgsParameters
* @type {object}
* @property {"string"|"boolean"} type
* @property {string} [short]
* @property {boolean|string} [default]
* @property {string} [description]
* @property {string} [example]
*/
export class Configuration {
export class Configuration extends ConfigurationAbstract {
/**
* @type {{[key:string]: ParseArgsParameters}}
*/
static options = {
"right-pattern": {
type: "string",
default: "",
description: "Comma-separated list of runway names with right-turn pattern.",
example: "24,33",
},
"min-altitude": {
type: "string",
default: "0",
description: "Minimum safe altitude of aircraft, in 100ft MSL. At least airport elevation.",
example: "145",
},
missions: {
type: "string",
default: "10",
description: "Number of missions in file.",
},
distance: {
type: "string",
default: "8",
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": {
type: "string",
default: "1",
description: "Pattern distance from airport runway in Nautical Miles.",
},
"pattern-final-distance": {
type: "string",
default: "1",
description: "Pattern final distance from airport runway edge in Nautical Miles.",
},
"rnd-heading": {
type: "string",
default: "0",
description: "Randomized aircraft heading deviation from direct heading to airport in degree.",
},
"prefer-rwy": {
type: "string",
default: "",
description: "Comma-separated list of runway names which are preferred if wind is indecisive.",
example: "24,33",
},
"pattern-altitude-msl": {
type: "boolean",
short: "m",
default: false,
description: "Pattern altitude is in MSL instead of AGL",
},
"no-guides": {
type: "boolean",
default: false,
description: "Try to remove virtual guides from missions.",
},
directory: {
type: "boolean",
short: "d",
default: false,
description: "Create files in a subdirectory instead of current directory.",
},
help: {
type: "boolean",
short: "h",
default: false,
description: "Will output the help.",
},
};
/**
*

@@ -98,9 +11,105 @@ * @param {string[]} args

constructor(args) {
const { values, positionals } = parseArgs({
args: args.slice(2),
options: Configuration.options,
allowPositionals: true,
});
super();
/**
* @type {import("../general/ConfigurationAbstract.js").ConfigurationPositional[]}
*/
this._arguments = [
{
name: "ICAO_AIRPORT_CODE",
description: "ICAO airport code which needs to be available in Aerofly FS 4.",
default: "KEYW",
},
{
name: "AFS_AIRCRAFT_CODE",
description: "Internal aircraft code in Aerofly FS 4.",
default: "c172",
},
{
name: "AFS_LIVERY_CODE",
description: "Internal livery code in Aerofly FS 4.",
default: "",
},
];
/**
* @type {{[key:string]: import("../general/ConfigurationAbstract").ParseArgsParameters}}
*/
this._options = {
"right-pattern": {
type: "string",
default: "",
description: "Comma-separated list of runway names with right-turn pattern.",
example: "24,33",
},
"min-altitude": {
type: "string",
default: "0",
description: "Minimum safe altitude of aircraft, in 100ft MSL. At least airport elevation.",
example: "145",
},
missions: {
type: "string",
default: "10",
description: "Number of missions in file.",
},
distance: {
type: "string",
default: "8",
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": {
type: "string",
default: "1",
description: "Pattern distance from airport runway in Nautical Miles.",
},
"pattern-final-distance": {
type: "string",
default: "1",
description: "Pattern final distance from airport runway edge in Nautical Miles.",
},
"rnd-heading": {
type: "string",
default: "0",
description: "Randomized aircraft heading deviation from direct heading to airport in degree.",
},
"prefer-rwy": {
type: "string",
default: "",
description: "Comma-separated list of runway names which are preferred if wind is indecisive.",
example: "24,33",
},
"pattern-altitude-msl": {
type: "boolean",
short: "m",
default: false,
description: "Pattern altitude is in MSL instead of AGL",
},
"no-guides": {
type: "boolean",
default: false,
description: "Try to remove virtual guides from missions.",
},
directory: {
type: "boolean",
short: "d",
default: false,
description: "Create files in a subdirectory instead of current directory.",
},
help: {
type: "boolean",
short: "h",
default: false,
description: "Will output the help.",
},
};
const { values, positionals } = this.parseArgs(args);
/**
* @type {string}

@@ -193,38 +202,2 @@ */

}
/**
* @returns {string}
*/
static argumentList() {
/**
* @type {string[]}
*/
let parameters = [];
for (let parameterName in Configuration.options) {
const option = Configuration.options[parameterName];
let parameter = `--${parameterName}`;
if (option.type === "string") {
parameter += "=..";
}
if (option.short) {
parameter += `, -${option.short}`;
if (option.type === "string") {
parameter += "=..";
}
}
parameters.push(`\x1b[94m ${parameter.padEnd(24, " ")} \x1b[0m ${option.description}`);
if (option.default) {
parameters.push(` Default value: \x1b[4m${option.default}\x1b[0m`);
}
if (option.example) {
parameters.push(` Example value: \x1b[4m${option.example}\x1b[0m`);
}
}
return parameters.join("\n");
}
}

@@ -85,9 +85,17 @@ // @ts-check

async build() {
const weather = await AviationWeatherApi.fetchMetar([this.airport.id], this.date);
/**
* @param {import('./Airport.js').Airport} airport
* @param {Configuration} configuration
* @param {Date?} date
* @returns {Promise<Scenario>}
*/
static async init(airport, configuration, date) {
const self = new Scenario(airport, configuration, date);
const weather = await AviationWeatherApi.fetchMetar([self.airport.id], self.date);
if (!weather.length) {
throw new Error("No METAR information from API for " + this.airport.id);
throw new Error("No METAR information from API for " + self.airport.id);
}
this.weather = new ScenarioWeather(weather[0]);
this.getActiveRunway();
self.weather = new ScenarioWeather(weather[0]);
self.getActiveRunway();
return self;
}

@@ -94,0 +102,0 @@

@@ -29,9 +29,11 @@ # Aerofly Landegerät: Helicopter Emergency Medical Services

```
Usage: npx -p @fboes/aerofly-patterns@latest aerofly-hems GEOJSON_FILE [AFS_AIRCRAFT_CODE] [AFS_LIVERY_CODE] [...options]
Create landing pattern lessons for Aerofly FS 4.
Usage: npx -p @fboes/aerofly-patterns@latest aerofly-hems GEOJSON_FILE [AFS_AIRCRAFT_CODE] [AFS_LIVERY_CODE] [...options]
v2.5.4: Landegerät - Create random custom missions for Aerofly FS 4.
Arguments:
GEOJSON_FILE GeoJSON file containing possible mission locations.
AFS_AIRCRAFT_CODE Internal aircraft code in Aerofly FS 4. Defaults to "ec135".
AFS_LIVERY_CODE Internal aircraft code in Aerofly FS 4. Defaults to "adac".
AFS_AIRCRAFT_CODE Internal aircraft code in Aerofly FS 4.
Default value: ec135
AFS_LIVERY_CODE Internal livery code in Aerofly FS 4
Default value: adac

@@ -44,5 +46,7 @@ Options:

--callsign=.. Optional callsign, else default callsign will be used.
Default value: MEDEVAC
--no-guides Try to remove virtual guides from missions.
--cold-dark Start cold & dark.
--cold-dark, -c Start cold & dark.
--transfer, -t Mission types can also be transfers.
--approach, -a Add approach guides to flight plan.
--no-poi, -p Do not generate POI files.

@@ -49,0 +53,0 @@ --directory, -d Create files in another directory instead of current directory.

{
"name": "@fboes/aerofly-patterns",
"version": "2.5.3",
"version": "2.5.4",
"description": "Landegerät - Create random custom missions for Aerofly FS 4.",

@@ -5,0 +5,0 @@ "main": "dist/index.js",

@@ -31,3 +31,3 @@ # Aerofly Landegerät

Usage: npx @fboes/aerofly-patterns@latest ICAO_AIRPORT_CODE [AEROFLY_AIRCRAFT_CODE] [...options]
Create landing pattern lessons for Aerofly FS 4.
Create random custom missions for Aerofly FS 4.

@@ -34,0 +34,0 @@ Arguments:

Sorry, the diff of this file is not supported yet

Sorry, the diff of this file is not supported yet

Sorry, the diff of this file is not supported yet

SocketSocket SOC 2 Logo

Product

  • Package Alerts
  • Integrations
  • Docs
  • Pricing
  • FAQ
  • Roadmap
  • Changelog

Packages

npm

Stay in touch

Get open source security insights delivered straight into your inbox.


  • Terms
  • Privacy
  • Security

Made with ⚡️ by Socket Inc