@fboes/aerofly-custom-missions 1.12.0 → 1.13.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (51) hide show
  1. package/CHANGELOG.md +12 -1
  2. package/dist/dto-flight/AeroflyNavRouteAirports.js +3 -3
  3. package/dist/dto-flight/AeroflyNavRouteBase.js +63 -11
  4. package/dist/dto-flight/AeroflyNavRouteBase.test.js +56 -0
  5. package/dist/dto-flight/AeroflyNavRouteRunway.js +9 -9
  6. package/dist/dto-flight/AeroflyNavRouteTransition.js +8 -7
  7. package/dist/dto-flight/AeroflyNavRouteWaypoint.js +17 -9
  8. package/dist/dto-flight/AeroflyNavigationConfig.js +4 -4
  9. package/dist/dto-flight/AeroflySettingsFlight.js +16 -15
  10. package/dist/dto-flight/AeroflySettingsFlight.test.js +0 -22
  11. package/dist/dto-flight/AeroflySettingsFuelLoad.js +6 -6
  12. package/dist/node/AeroflyTypes.js +63 -0
  13. package/dist/node/Convert.js +137 -71
  14. package/dist/node/Convert.test.js +58 -28
  15. package/docs/flight.json +13 -10
  16. package/docs/flight.mcf +14 -10
  17. package/docs/flight.xml +14 -10
  18. package/package.json +1 -1
  19. package/src/dto-flight/AeroflyFlight.ts +0 -0
  20. package/src/dto-flight/AeroflyNavRouteAirports.ts +3 -3
  21. package/src/dto-flight/AeroflyNavRouteBase.test.ts +65 -0
  22. package/src/dto-flight/AeroflyNavRouteBase.ts +75 -13
  23. package/src/dto-flight/AeroflyNavRouteRunway.ts +9 -9
  24. package/src/dto-flight/AeroflyNavRouteTransition.ts +8 -7
  25. package/src/dto-flight/AeroflyNavRouteWaypoint.ts +24 -9
  26. package/src/dto-flight/AeroflyNavigationConfig.ts +4 -4
  27. package/src/dto-flight/AeroflySettingsFlight.test.ts +0 -30
  28. package/src/dto-flight/AeroflySettingsFlight.ts +31 -18
  29. package/src/dto-flight/AeroflySettingsFuelLoad.ts +11 -7
  30. package/src/node/AeroflyTypes.ts +74 -0
  31. package/src/node/Convert.test.ts +129 -42
  32. package/src/node/Convert.ts +172 -81
  33. package/types/dto-flight/AeroflyNavRouteBase.d.ts +26 -4
  34. package/types/dto-flight/AeroflyNavRouteBase.d.ts.map +1 -1
  35. package/types/dto-flight/AeroflyNavRouteBase.test.d.ts +2 -0
  36. package/types/dto-flight/AeroflyNavRouteBase.test.d.ts.map +1 -0
  37. package/types/dto-flight/AeroflyNavRouteRunway.d.ts +1 -1
  38. package/types/dto-flight/AeroflyNavRouteRunway.d.ts.map +1 -1
  39. package/types/dto-flight/AeroflyNavRouteTransition.d.ts +1 -1
  40. package/types/dto-flight/AeroflyNavRouteTransition.d.ts.map +1 -1
  41. package/types/dto-flight/AeroflyNavRouteWaypoint.d.ts +9 -1
  42. package/types/dto-flight/AeroflyNavRouteWaypoint.d.ts.map +1 -1
  43. package/types/dto-flight/AeroflySettingsFlight.d.ts +9 -4
  44. package/types/dto-flight/AeroflySettingsFlight.d.ts.map +1 -1
  45. package/types/dto-flight/AeroflySettingsFuelLoad.d.ts +1 -1
  46. package/types/dto-flight/AeroflySettingsFuelLoad.d.ts.map +1 -1
  47. package/types/node/AeroflyTypes.d.ts +40 -0
  48. package/types/node/AeroflyTypes.d.ts.map +1 -0
  49. package/types/node/Convert.d.ts +78 -16
  50. package/types/node/Convert.d.ts.map +1 -1
  51. package/AGENTS.md +0 -43
package/CHANGELOG.md CHANGED
@@ -2,7 +2,18 @@
2
2
 
3
3
  This changelog documents all notable changes to the Aerofly Custom Missions project. Each version entry includes a list of changes, with the most recent version at the top.
4
4
 
5
- ## [1.12.0] - 2026-07-08
5
+ ## [Unreleased]
6
+
7
+ ## [1.13.1] - 2026-07-24
8
+
9
+ - Fixed orientation / heading parsing
10
+
11
+ ## [1.13.0] - 2026-07-23
12
+
13
+ - Made unit conversions more precise
14
+ - Added correct UID generator
15
+
16
+ ## [1.12.0] - 2026-07-01
6
17
 
7
18
  - Added unofficial `_cruiseSpeed` property to `AeroflyNavigationConfig`
8
19
 
@@ -1,5 +1,5 @@
1
1
  import { AeroflyConfigurationNode } from "../node/AeroflyConfigurationNode.js";
2
- import { Convert } from "../node/Convert.js";
2
+ import { convertFeetToMeter, convertMeterToFeet } from "../node/Convert.js";
3
3
  import { AeroflyNavRouteBase } from "./AeroflyNavRouteBase.js";
4
4
  class AeroflyNavRouteAirport extends AeroflyNavRouteBase {
5
5
  /**
@@ -27,10 +27,10 @@ class AeroflyNavRouteAirport extends AeroflyNavRouteBase {
27
27
  * @returns {number | null} elevation in feet, null if not set
28
28
  */
29
29
  get elevation_ft() {
30
- return this.elevation !== null ? Convert.convertMeterToFeet(this.elevation) : null;
30
+ return this.elevation !== null ? convertMeterToFeet(this.elevation) : null;
31
31
  }
32
32
  set elevation_ft(elevation_ft) {
33
- this.elevation = elevation_ft !== null ? Convert.convertFeetToMeter(elevation_ft) : null;
33
+ this.elevation = elevation_ft !== null ? convertFeetToMeter(elevation_ft) : null;
34
34
  }
35
35
  getElement(index = 0) {
36
36
  const element = super.getElement(index);
@@ -1,5 +1,6 @@
1
- import { Convert } from "../node/Convert.js";
2
- import { AeroflyConfigurationNode, AeroflyConfigurationNodeComment } from "../node/AeroflyConfigurationNode.js";
1
+ import { convertLonLatToVector, convertVectorToLonLat } from "../node/Convert.js";
2
+ import { AeroflyConfigurationNode } from "../node/AeroflyConfigurationNode.js";
3
+ import { AeroflyVector3Float } from "../node/AeroflyTypes.js";
3
4
  export class AeroflyNavRouteBase {
4
5
  /**
5
6
  * @property {AeroflyNavRouteType} type like "origin", "departure_runway", "departure", "waypoint", "arrival", "approach", "destination_runway" or "destination"
@@ -18,7 +19,7 @@ export class AeroflyNavRouteBase {
18
19
  */
19
20
  latitude;
20
21
  /**
21
- * @property {?bigint} uid unique identifier for the waypoint, must match Aerofly FS internal UID if used in an existing mission, can be null for new waypoints
22
+ * @property {?bigint} uid unique identifier for the waypoint, must match Aerofly FS internal UID if used in an existing mission. Obviously the UID encodes the geographic position as well as the name of the waypoint.
22
23
  */
23
24
  uid;
24
25
  /**
@@ -27,7 +28,7 @@ export class AeroflyNavRouteBase {
27
28
  * @param {number} longitude WGS84
28
29
  * @param {number} latitude WGS84
29
30
  * @param {object} [options] additional options for the waypoint
30
- * @param {?bigint} [options.uid] unique identifier for the waypoint, must match Aerofly FS internal UID if used in an existing mission, can be null for new waypoints
31
+ * @param {?bigint} [options.uid] unique identifier for the waypoint, must match Aerofly FS internal UID if used in an existing mission. Obviously the UID encodes the geographic position as well as the name of the waypoint.
31
32
  */
32
33
  constructor(type, identifier, longitude, latitude, { uid = null } = {}) {
33
34
  this.type = type;
@@ -40,10 +41,10 @@ export class AeroflyNavRouteBase {
40
41
  * @returns {AeroflyVector3Float} to use in Aerofly FS4's `main.mcf`
41
42
  */
42
43
  get position() {
43
- return Convert.convertLonLatToVector(this.longitude, this.latitude, 0);
44
+ return convertLonLatToVector(this.longitude, this.latitude, 0);
44
45
  }
45
46
  set position(position) {
46
- const latLonAlt = Convert.convertVectorToLonLat(position);
47
+ const latLonAlt = convertVectorToLonLat(position);
47
48
  this.longitude = latLonAlt.longitude;
48
49
  this.latitude = latLonAlt.latitude;
49
50
  }
@@ -53,18 +54,69 @@ export class AeroflyNavRouteBase {
53
54
  * @returns {this} for chaining
54
55
  */
55
56
  setPosition(position) {
56
- this.position = position;
57
+ this.position = AeroflyVector3Float.fromArray(position);
57
58
  return this;
58
59
  }
59
60
  getElement(index = 0) {
60
61
  const element = new AeroflyConfigurationNode("tmnav_route_" + this.type, this.identifier, String(index))
61
62
  .appendChild("string8u", "Identifier", this.identifier)
62
- .appendChild("vector3_float64", "Position", this.position, `Lon ${this.longitude.toFixed(6)}, Lat ${this.latitude.toFixed(6)}`)
63
- .append(this.uid
64
- ? new AeroflyConfigurationNode("uint64", "Uid", this.uid)
65
- : new AeroflyConfigurationNodeComment("uint64", "Uid", ""));
63
+ .appendChild("vector3_float64", "Position", this.position.toArray(), `Lon ${this.longitude.toFixed(6)}, Lat ${this.latitude.toFixed(6)}`)
64
+ .appendChild("uint64", "Uid", this.uid ?? this.getUidFallback(), this.uid ? "" : "Fallback UID, not matching Aerofly FS internal UID");
66
65
  return element;
67
66
  }
67
+ /**
68
+ * @returns {bigint} 24 bit longitude
69
+ * @see https://www.aerofly.com/community/forum/index.php?thread/29490-navdata-coordinates-to-uid/
70
+ */
71
+ encodeLongitude() {
72
+ let scaled = this.longitude / 180;
73
+ scaled = 65536.0 * (0.5 + 0.5 * scaled); // 16 bit
74
+ return BigInt(Math.round(256.0 * scaled + 0.5)); // 24 bit
75
+ }
76
+ /**
77
+ * @returns {bigint} 24 bit latitude
78
+ * @see https://www.aerofly.com/community/forum/index.php?thread/29490-navdata-coordinates-to-uid/
79
+ */
80
+ encodeLatitude() {
81
+ const worldGridConstantA = 2.3311223704144;
82
+ let scaled = this.latitude / 180;
83
+ scaled = Math.tan(worldGridConstantA * scaled) / worldGridConstantA;
84
+ scaled = 65536.0 * (0.5 + 0.5 * scaled); // 16 bit
85
+ return BigInt(Math.round(256.0 * scaled + 0.5)); // 24 bit
86
+ }
87
+ /**
88
+ * @returns {number} 16 bit type
89
+ */
90
+ encodeWaypointType() {
91
+ // Type code (airport (0500) / runway (0800) / SID (4400) / STAR (4800) / Approach (4C00) / RNAV waypoint (C000) / airways (4000)
92
+ switch (this.type) {
93
+ case "origin":
94
+ case "destination":
95
+ return 0x0500;
96
+ case "departure_runway":
97
+ case "destination_runway":
98
+ return 0x0800;
99
+ case "departure":
100
+ return 0x4400;
101
+ case "arrival":
102
+ return 0x4800;
103
+ case "approach":
104
+ return 0x4c00;
105
+ }
106
+ return 0;
107
+ }
108
+ /**
109
+ * @returns {bigint} Packs both into a single 64-bit value:
110
+ * - Bits 63..40 → longitude (24 bit)
111
+ * - Bits 39..16 → latitude (24 bit)
112
+ * - Bits 15..0 → payload (16 bit)
113
+ * @see https://www.aerofly.com/community/forum/index.php?thread/29490-navdata-coordinates-to-uid/
114
+ */
115
+ getUidFallback() {
116
+ return ((this.encodeLongitude() << 40n) |
117
+ (this.encodeLatitude() << 16n) |
118
+ (BigInt(this.encodeWaypointType()) & 0xffffn));
119
+ }
68
120
  toJSON() {
69
121
  return {
70
122
  ...this,
@@ -0,0 +1,56 @@
1
+ import { describe, it } from "node:test";
2
+ import { strict as assert } from "node:assert";
3
+ import { AeroflyNavRouteDestination } from "../index.js";
4
+ describe("AeroflyNavRouteBase", () => {
5
+ it("should create UIDs almost matching the ingame UIDs", () => {
6
+ const testCases = [
7
+ {
8
+ wp: new AeroflyNavRouteDestination("MAX-NE", 179.999, 89.999),
9
+ assertUid: 18446694595666707712n,
10
+ ingameUid: 0n,
11
+ },
12
+ {
13
+ wp: new AeroflyNavRouteDestination("MAX-SW", -179.999, -89.999),
14
+ assertUid: 51677066167552n,
15
+ ingameUid: 0n,
16
+ },
17
+ {
18
+ wp: new AeroflyNavRouteDestination("KEYW", -81.7599558, 24.5561197),
19
+ assertUid: 5033914504046314752n,
20
+ ingameUid: 5033914504046249216n,
21
+ },
22
+ {
23
+ wp: new AeroflyNavRouteDestination("EGLL", -0.45277777, 51.47138888),
24
+ assertUid: 9200173076798702848n,
25
+ ingameUid: 9199731073154483456n,
26
+ },
27
+ {
28
+ wp: new AeroflyNavRouteDestination("YSSY", 151.177, -33.4961),
29
+ assertUid: 16969826619723154688n,
30
+ ingameUid: 16969837613165511936n,
31
+ },
32
+ {
33
+ wp: new AeroflyNavRouteDestination("RJTT", 139.779, 35.709),
34
+ assertUid: 16385782762532177152n,
35
+ ingameUid: 16385890514078663936n,
36
+ },
37
+ ];
38
+ for (const testCase of testCases) {
39
+ assert.strictEqual(testCase.wp.getUidFallback(), testCase.assertUid, `${testCase.wp.identifier} was ${testCase.ingameUid}`);
40
+ }
41
+ });
42
+ it("should unpack UIDs for airports to code 1280", () => {
43
+ const unpackPayload = (packed) => {
44
+ return Number(packed & 0xffffn);
45
+ };
46
+ const testCases = [
47
+ { uid: 5033914504046314752n, code: 1280, airport: "KEYW" },
48
+ { uid: 9200173076798702848n, code: 1280, airport: "EGLL" },
49
+ { uid: 16969826619723154688n, code: 1280, airport: "YSSY" },
50
+ { uid: 16385782762532177152n, code: 1280, airport: "RJTT" },
51
+ ];
52
+ for (const testCase of testCases) {
53
+ assert.strictEqual(unpackPayload(testCase.uid), testCase.code, testCase.airport);
54
+ }
55
+ });
56
+ });
@@ -1,6 +1,7 @@
1
- import { Convert } from "../node/Convert.js";
1
+ import { convertDirectionToHeading, convertFeetToMeter, convertMeterToFeet } from "../node/Convert.js";
2
2
  import { AeroflyConfigurationNode } from "../node/AeroflyConfigurationNode.js";
3
3
  import { AeroflyNavRouteBase } from "./AeroflyNavRouteBase.js";
4
+ import { AeroflyVector3Float } from "../node/AeroflyTypes.js";
4
5
  class AeroflyNavRouteRunway extends AeroflyNavRouteBase {
5
6
  /**
6
7
  * @property {?number} direction_degree runway direction in degrees, null if not set
@@ -27,34 +28,33 @@ class AeroflyNavRouteRunway extends AeroflyNavRouteBase {
27
28
  * @returns {number | null} elevation in feet, null if not set
28
29
  */
29
30
  get elevation_ft() {
30
- return this.elevation !== null ? Convert.convertMeterToFeet(this.elevation) : null;
31
+ return this.elevation !== null ? convertMeterToFeet(this.elevation) : null;
31
32
  }
32
33
  set elevation_ft(elevation_ft) {
33
- this.elevation = elevation_ft !== null ? Convert.convertFeetToMeter(elevation_ft) : null;
34
+ this.elevation = elevation_ft !== null ? convertFeetToMeter(elevation_ft) : null;
34
35
  }
35
36
  /**
36
37
  * @returns {number | null} runway length in feet, null if not set
37
38
  */
38
39
  get runwayLength_ft() {
39
- return this.runwayLength !== null ? Convert.convertMeterToFeet(this.runwayLength) : null;
40
+ return this.runwayLength !== null ? convertMeterToFeet(this.runwayLength) : null;
40
41
  }
41
42
  set runwayLength_ft(runwayLength_ft) {
42
- this.runwayLength = runwayLength_ft !== null ? Convert.convertFeetToMeter(runwayLength_ft) : null;
43
+ this.runwayLength = runwayLength_ft !== null ? convertFeetToMeter(runwayLength_ft) : null;
43
44
  }
44
45
  /**
45
46
  * @returns {AeroflyVector3Float | null} runway direction, null if not set
46
47
  */
47
48
  get direction() {
48
- // TODO: calculate direction vector from direction_degree
49
- return this.direction_degree !== null ? [0, 0, 0] : null;
49
+ return this.direction_degree !== null ? new AeroflyVector3Float(0, 0, 0) : null; // TODO: Get direction vector
50
50
  }
51
51
  set direction(direction) {
52
- this.direction_degree = 0; // TODO: calculate direction_degree from direction vector
52
+ this.direction_degree = convertDirectionToHeading(direction, this.position);
53
53
  }
54
54
  getElement(index = 0) {
55
55
  const element = super.getElement(index);
56
56
  if (this.direction !== null) {
57
- element.appendChild("vector3_float64", "Direction", this.direction, `Runway direction ${Math.round(this.direction_degree ?? 0)}°`);
57
+ element.appendChild("vector3_float64", "Direction", this.direction.toArray(), `Runway direction ${Math.round(this.direction_degree ?? 0)}°`);
58
58
  }
59
59
  if (this.elevation !== null) {
60
60
  element.appendChild("float64", "Elevation", this.elevation, this.elevation_ft ? `Elevation ${Math.ceil(this.elevation_ft)} ft` : undefined);
@@ -1,6 +1,7 @@
1
- import { Convert } from "../node/Convert.js";
1
+ import { convertFeetToMeter, convertMeterToFeet } from "../node/Convert.js";
2
2
  import { AeroflyConfigurationNode } from "../node/AeroflyConfigurationNode.js";
3
3
  import { AeroflyNavRouteBase } from "./AeroflyNavRouteBase.js";
4
+ import { AeroflyVector3Float } from "../node/AeroflyTypes.js";
4
5
  class AeroflyNavRouteTransition extends AeroflyNavRouteBase {
5
6
  /**
6
7
  * @property {string} airport ICAO code of the airport this transition belongs to, e.g. "SEA", "PDX"
@@ -29,28 +30,28 @@ class AeroflyNavRouteTransition extends AeroflyNavRouteBase {
29
30
  * @returns {number | null} elevation in feet, null if not set
30
31
  */
31
32
  get elevation_ft() {
32
- return this.elevation !== null ? Convert.convertMeterToFeet(this.elevation) : null;
33
+ return this.elevation !== null ? convertMeterToFeet(this.elevation) : null;
33
34
  }
34
35
  set elevation_ft(elevation_ft) {
35
- this.elevation = elevation_ft !== null ? Convert.convertFeetToMeter(elevation_ft) : null;
36
+ this.elevation = elevation_ft !== null ? convertFeetToMeter(elevation_ft) : null;
36
37
  }
37
38
  /**
38
39
  * @returns {AeroflyVector3Float} deliberately empty
39
40
  */
40
41
  get position() {
41
- return [0, 0, 0]; // sic!
42
+ return new AeroflyVector3Float(0, 0, 0); // sic!
42
43
  }
43
44
  /**
44
45
  * @returns {AeroflyVector3Float} deliberately empty
45
46
  */
46
47
  get direction() {
47
- return [0, 0, 0]; // sic!
48
+ return new AeroflyVector3Float(0, 0, 0); // sic!
48
49
  }
49
50
  getElement(index = 0) {
50
51
  const element = super.getElement(index);
51
52
  element
52
53
  .appendChild("string8u", "Airport", this.airport)
53
- .appendChild("vector3_float64", "Direction", this.direction)
54
+ .appendChild("vector3_float64", "Direction", this.direction.toArray())
54
55
  .appendChild("float64", "Elevation", this.elevation ?? 0, `Elevation ${this.elevation_ft !== null ? Math.ceil(this.elevation_ft) + " ft" : "unknown"}`)
55
56
  .appendChild("string8u", "Transition", this.transition)
56
57
  .appendChild("uint64", "TransitionUid", this.transitionUid ?? 0);
@@ -86,7 +87,7 @@ export class AeroflyNavRouteArrival extends AeroflyNavRouteTransition {
86
87
  const element = super.getElement(index);
87
88
  element
88
89
  .appendChild("string8u", "Airport", this.airport)
89
- .appendChild("vector3_float64", "Direction", this.direction)
90
+ .appendChild("vector3_float64", "Direction", this.direction.toArray())
90
91
  .appendChild("float64", "Elevation", this.elevation ?? 0, `Elevation ${this.elevation_ft !== null ? Math.ceil(this.elevation_ft) + " ft" : "unknown"}`);
91
92
  return element;
92
93
  }
@@ -1,4 +1,4 @@
1
- import { Convert } from "../node/Convert.js";
1
+ import { convertFeetToMeter, convertLonLatToVector, convertMeterToFeet, convertVectorToLonLat, } from "../node/Convert.js";
2
2
  import { AeroflyConfigurationNode } from "../node/AeroflyConfigurationNode.js";
3
3
  import { AeroflyNavRouteBase } from "./AeroflyNavRouteBase.js";
4
4
  export class AeroflyNavRouteWaypoint extends AeroflyNavRouteBase {
@@ -9,6 +9,10 @@ export class AeroflyNavRouteWaypoint extends AeroflyNavRouteBase {
9
9
  * @property {?number} navaidFrequency if the waypoint is a navaid, its frequency in Hz
10
10
  */
11
11
  navaidFrequency;
12
+ /**
13
+ * @property {?bigint} navaidUid if the waypoint is a navaid, its unique identifier, must match Aerofly FS internal UID if used in an existing mission
14
+ */
15
+ navaidUid;
12
16
  /**
13
17
  * @property {number | null} altitude in meter, null if not set
14
18
  */
@@ -23,17 +27,21 @@ export class AeroflyNavRouteWaypoint extends AeroflyNavRouteBase {
23
27
  * @param {number} latitude WGS84
24
28
  * @param {object} [options] additional options for the waypoint
25
29
  * @param {?number} [options.navaidFrequency] if the waypoint is a navaid, its frequency in Hz
30
+ * @param {?number} [options.navaidFrequency_khz] if the waypoint is a navaid, its frequency in kHz, will override navaidFrequency in Hz if provided
31
+ * @param {?number} [options.navaidFrequency_mhz] if the waypoint is a navaid, its frequency in MHz, will override navaidFrequency in Hz if provided
32
+ * @param {?bigint} [options.navaidUid] if the waypoint is a navaid, its unique identifier, must match Aerofly FS internal UID if used in an existing mission
26
33
  * @param {number} [options.altitude] in meter
27
34
  * @param {?number} [options.altitude_ft] altitude in feet, will override altitude in meter if provided
28
35
  * @param {boolean} [options.flyOver] if true, the waypoint is meant to be flown over, otherwise it can be used as a fly-by waypoint
29
36
  * @param {?bigint} [options.uid] unique identifier for the waypoint, will be generated automatically if not provided
30
37
  */
31
- constructor(identifier, longitude, latitude, { navaidFrequency = null, navaidFrequency_khz = null, navaidFrequency_mhz = null, altitude = null, altitude_ft = null, flyOver = false, uid = null, } = {}) {
38
+ constructor(identifier, longitude, latitude, { navaidFrequency = null, navaidFrequency_khz = null, navaidFrequency_mhz = null, navaidUid = null, altitude = null, altitude_ft = null, flyOver = false, uid = null, } = {}) {
32
39
  super("waypoint", identifier, longitude, latitude, { uid });
33
40
  this.identifier = identifier;
34
41
  this.longitude = longitude;
35
42
  this.latitude = latitude;
36
43
  this.navaidFrequency = navaidFrequency;
44
+ this.navaidUid = navaidUid;
37
45
  this.altitude = altitude;
38
46
  if (altitude_ft !== null) {
39
47
  this.altitude_ft = altitude_ft;
@@ -50,10 +58,10 @@ export class AeroflyNavRouteWaypoint extends AeroflyNavRouteBase {
50
58
  * @returns {number | null} altitude in feet, null if not set
51
59
  */
52
60
  get altitude_ft() {
53
- return this.altitude !== null ? Convert.convertMeterToFeet(this.altitude) : null;
61
+ return this.altitude !== null ? convertMeterToFeet(this.altitude) : null;
54
62
  }
55
63
  set altitude_ft(altitude_ft) {
56
- this.altitude = altitude_ft !== null ? Convert.convertFeetToMeter(altitude_ft) : null;
64
+ this.altitude = altitude_ft !== null ? convertFeetToMeter(altitude_ft) : null;
57
65
  }
58
66
  get navaidFrequency_khz() {
59
67
  return this.navaidFrequency ? this.navaidFrequency / 1000 : null;
@@ -71,20 +79,19 @@ export class AeroflyNavRouteWaypoint extends AeroflyNavRouteBase {
71
79
  * @returns {AeroflyVector3Float} to use in Aerofly FS4's `main.mcf`
72
80
  */
73
81
  get position() {
74
- return Convert.convertLonLatToVector(this.longitude, this.latitude, this.altitude || 0);
82
+ return convertLonLatToVector(this.longitude, this.latitude, this.altitude || 0);
75
83
  }
76
84
  set position(position) {
77
- const { longitude, latitude, altitude_meter } = Convert.convertVectorToLonLat(position);
85
+ const { longitude, latitude, altitude_meter } = convertVectorToLonLat(position);
78
86
  this.longitude = longitude;
79
87
  this.latitude = latitude;
80
88
  this.altitude = altitude_meter;
81
89
  }
82
90
  getElement(index = 0) {
83
91
  const element = super.getElement(index);
84
- if (this.navaidFrequency) {
85
- element.appendChild("float64", "NavaidFrequency", this.navaidFrequency);
86
- }
87
92
  element
93
+ .appendChild("float64", "NavaidFrequency", this.navaidFrequency ?? 0)
94
+ .appendChild("uint64", "NavaidUid", this.navaidUid ?? BigInt(0))
88
95
  .appendChild("vector2_float64", "Altitude", this.altitude !== null && this.altitude > 0 ? [this.altitude, this.altitude] : [-1001, 100001], `${this.altitude_ft !== null && this.altitude_ft > 0 ? Math.ceil(this.altitude_ft) + " ft" : "unrestricted"}`)
89
96
  .appendChild("bool", "FlyOver", this.flyOver);
90
97
  return element;
@@ -97,6 +104,7 @@ export class AeroflyNavRouteWaypoint extends AeroflyNavRouteBase {
97
104
  altitude_ft: this.altitude_ft,
98
105
  navaidFrequency: undefined,
99
106
  navaidFrequency_khz: this.navaidFrequency_khz,
107
+ navaidUid: this.navaidUid !== null ? this.navaidUid.toString() : null,
100
108
  };
101
109
  }
102
110
  }
@@ -1,5 +1,5 @@
1
1
  import { AeroflyConfigurationNode } from "../node/AeroflyConfigurationNode.js";
2
- import { Convert } from "../node/Convert.js";
2
+ import { convertFeetToMeter, convertMeterToFeet } from "../node/Convert.js";
3
3
  import { AeroflyNavRouteBase } from "./AeroflyNavRouteBase.js";
4
4
  export class AeroflyNavigationConfig {
5
5
  /**
@@ -31,16 +31,16 @@ export class AeroflyNavigationConfig {
31
31
  * @returns {AeroflyNavigationConfig} with cruise altitude converted to meters
32
32
  */
33
33
  static createInFeet(cruiseAltitude_ft, waypoints = [], _cruiseSpeed_kts = undefined) {
34
- return new AeroflyNavigationConfig(Convert.convertFeetToMeter(cruiseAltitude_ft), waypoints, _cruiseSpeed_kts);
34
+ return new AeroflyNavigationConfig(convertFeetToMeter(cruiseAltitude_ft), waypoints, _cruiseSpeed_kts);
35
35
  }
36
36
  /**
37
37
  * @returns {number} cruise altitude in feet
38
38
  */
39
39
  get cruiseAltitude_ft() {
40
- return Convert.convertMeterToFeet(this.cruiseAltitude);
40
+ return convertMeterToFeet(this.cruiseAltitude);
41
41
  }
42
42
  set cruiseAltitude_ft(cruiseAltitude_ft) {
43
- this.cruiseAltitude = Convert.convertFeetToMeter(cruiseAltitude_ft);
43
+ this.cruiseAltitude = convertFeetToMeter(cruiseAltitude_ft);
44
44
  }
45
45
  /**
46
46
  * @returns {AeroflyConfigurationNode[]} indexed checkpoints
@@ -1,5 +1,6 @@
1
- import { Convert } from "../node/Convert.js";
2
1
  import { AeroflyConfigurationNode } from "../node/AeroflyConfigurationNode.js";
2
+ import { AeroflyMatrix3Float, AeroflyVector3Float, } from "../node/AeroflyTypes.js";
3
+ import { convertFeetToMeter, convertLonLatToVector, convertVectorToLonLat, convertHeadingToOrientation, convertOrientationToHeading, convertMeterToFeet, } from "../node/Convert.js";
3
4
  export class AeroflySettingsFlight {
4
5
  longitude;
5
6
  latitude;
@@ -52,13 +53,13 @@ export class AeroflySettingsFlight {
52
53
  this.runway = runway;
53
54
  }
54
55
  static createInFeet(longitude, latitude, altitude_ft, heading_degree, speed_kts = 0, additionalAttributes = {}) {
55
- return new AeroflySettingsFlight(longitude, latitude, Convert.convertFeetToMeter(altitude_ft), heading_degree, speed_kts, additionalAttributes);
56
+ return new AeroflySettingsFlight(longitude, latitude, convertFeetToMeter(altitude_ft), heading_degree, speed_kts, additionalAttributes);
56
57
  }
57
58
  static createInCartesian(position, velocity, orientation, additionalAttributes = {}) {
58
59
  const flight = new AeroflySettingsFlight(0, 0, 0, 0, 0, additionalAttributes);
59
- flight.position = position;
60
- flight.velocity = velocity;
61
- flight.orientation = orientation;
60
+ flight.position = AeroflyVector3Float.fromArray(position);
61
+ flight.velocity = AeroflyVector3Float.fromArray(velocity);
62
+ flight.orientation = AeroflyMatrix3Float.fromArray(orientation);
62
63
  return flight;
63
64
  }
64
65
  /**
@@ -95,10 +96,10 @@ export class AeroflySettingsFlight {
95
96
  * @returns {AeroflyVector3Float} position vector to use in Aerofly FS4's `main.mcf`
96
97
  */
97
98
  get position() {
98
- return Convert.convertLonLatToVector(this.longitude, this.latitude, this.altitude_meter);
99
+ return convertLonLatToVector(this.longitude, this.latitude, this.altitude_meter);
99
100
  }
100
101
  set position(position) {
101
- const { longitude, latitude, altitude_meter } = Convert.convertVectorToLonLat(position);
102
+ const { longitude, latitude, altitude_meter } = convertVectorToLonLat(position);
102
103
  this.longitude = longitude;
103
104
  this.latitude = latitude;
104
105
  this.altitude_meter = altitude_meter;
@@ -109,26 +110,26 @@ export class AeroflySettingsFlight {
109
110
  get velocity() {
110
111
  const speed = this.speed_ms;
111
112
  const heading_rad = this.heading_degree * (Math.PI / 180);
112
- return [Math.cos(heading_rad) * speed, Math.sin(heading_rad) * speed, 0];
113
+ return new AeroflyVector3Float(Math.cos(heading_rad) * speed, Math.sin(heading_rad) * speed, 0);
113
114
  }
114
115
  set velocity(velocity) {
115
116
  // TODO: implement setting velocity vector and updating speed accordingly
116
117
  this.speed_ms = 0;
117
118
  }
118
119
  get orientation() {
119
- return Convert.convertDegreeToMatrix(this.heading_degree);
120
+ return convertHeadingToOrientation(this.heading_degree, this.position);
120
121
  }
121
122
  set orientation(orientation) {
122
- this.heading_degree = Convert.convertMatrixToDegree(orientation);
123
+ this.heading_degree = convertOrientationToHeading(orientation, this.position);
123
124
  }
124
125
  /**
125
126
  * @returns {number} altitude in feet AGL
126
127
  */
127
128
  get altitude_ft() {
128
- return Convert.convertMeterToFeet(this.altitude_meter);
129
+ return convertMeterToFeet(this.altitude_meter);
129
130
  }
130
131
  set altitude_ft(altitude_ft) {
131
- this.altitude_meter = Convert.convertFeetToMeter(altitude_ft);
132
+ this.altitude_meter = convertFeetToMeter(altitude_ft);
132
133
  }
133
134
  /**
134
135
  * @returns {number} speed in meters per seconds
@@ -141,9 +142,9 @@ export class AeroflySettingsFlight {
141
142
  }
142
143
  getElement() {
143
144
  return new AeroflyConfigurationNode("tmsettings_flight", "flight_setting")
144
- .appendChild("vector3_float64", "position", this.position, `Lon ${this.longitude.toFixed(6)}, Lat ${this.latitude.toFixed(6)}, ${Math.ceil(this.altitude_ft)} ft`)
145
- .appendChild("vector3_float64", "velocity", this.velocity, `${Math.round(this.speed_kts)} kts`)
146
- .appendChild("matrix3_float64", "orientation", this.orientation, `${Math.round(this.heading_degree)}° heading`)
145
+ .appendChild("vector3_float64", "position", this.position.toArray(), `Lon ${this.longitude.toFixed(6)}, Lat ${this.latitude.toFixed(6)}, ${Math.ceil(this.altitude_ft)} ft`)
146
+ .appendChild("vector3_float64", "velocity", this.velocity.toArray(), `${Math.round(this.speed_kts)} kts`)
147
+ .appendChild("matrix3_float64", "orientation", this.orientation.toArray(), `${Math.round(this.heading_degree)}° heading`)
147
148
  .appendChild("float64", "gear", this.gear)
148
149
  .appendChild("float64", "throttle", this.throttle)
149
150
  .appendChild("float64", "flaps", this.flaps)
@@ -27,28 +27,6 @@ describe("AeroflySettingsFlight", () => {
27
27
  assert.strictEqual(flight.runway, "16L", `Expected runway to be 16L, got ${flight.runway}`);
28
28
  assertValidAeroflyStructure(flight.toString());
29
29
  });
30
- it("should use valid orientation from main.mcf", () => {
31
- const flight = new AeroflySettingsFlight(-122.3088, 47.4502, 1000, 90, 150);
32
- // Canada
33
- flight.orientation = [
34
- -0.763419555334921, 0.645896043270811, 0.00298057365988389, -0.564914354076689, -0.665451427226094,
35
- -0.487899754622681, -0.313149094027667, -0.374155982565915, 0.872894578754952,
36
- ];
37
- console.log("Orientation:", flight.heading_degree); // should be 270, is 216
38
- // South Africa
39
- flight.orientation = [
40
- 0.324874838801671, -0.945743434574979, 0.0050690306677758, -0.526455468124437, -0.185291526716742,
41
- -0.829766045466347, 0.785685038164447, 0.266901491350839, -0.5580883574482,
42
- ];
43
- console.log("Orientation:", flight.heading_degree); // should be 270, is 301
44
- // Japan
45
- flight.orientation = [
46
- 0.717429258146935, 0.696613268777701, 0.00502128644729529, -0.393795668419003, 0.411487688662976,
47
- -0.821950639401925, -0.574647919242008, 0.587714076606685, 0.569536595021937,
48
- ];
49
- console.log("Orientation:", flight.heading_degree); // should be 270, is 331
50
- assertValidAeroflyStructure(flight.toString());
51
- });
52
30
  it("should create fallbacks for flight configurations", () => {
53
31
  const flight = new AeroflySettingsFlight(-122.3088, 47.4502, 1000, 90, 150, {
54
32
  configuration: "OnGround",
@@ -1,5 +1,5 @@
1
1
  import { AeroflyConfigurationNode } from "../node/AeroflyConfigurationNode.js";
2
- import { Convert } from "../node/Convert.js";
2
+ import { convertKgToLb, convertLbToKg } from "../node/Convert.js";
3
3
  export class AeroflySettingsFuelLoad {
4
4
  /**
5
5
  * @property {string} aircraft aerofly aircraft name (e.g. "c172")
@@ -34,20 +34,20 @@ export class AeroflySettingsFuelLoad {
34
34
  this.configuration = configuration;
35
35
  }
36
36
  get fuelMass_lb() {
37
- return Convert.convertKgToLb(this.fuelMass);
37
+ return convertKgToLb(this.fuelMass);
38
38
  }
39
39
  set fuelMass_lb(fuelMass_lb) {
40
- this.fuelMass = Convert.convertLbToKg(fuelMass_lb);
40
+ this.fuelMass = convertLbToKg(fuelMass_lb);
41
41
  }
42
42
  get payloadMass_lb() {
43
- return Convert.convertKgToLb(this.payloadMass);
43
+ return convertKgToLb(this.payloadMass);
44
44
  }
45
45
  set payloadMass_lb(payloadMass_lb) {
46
- this.payloadMass = Convert.convertLbToKg(payloadMass_lb);
46
+ this.payloadMass = convertLbToKg(payloadMass_lb);
47
47
  }
48
48
  getElement() {
49
49
  return new AeroflyConfigurationNode("tmsettings_fuel_load", "fuel_load_setting")
50
- .appendChild("fuel_load_configuration", "configuration", this.fuelMass > 0 ? "Keep" : this.configuration)
50
+ .appendChild("fuel_load_configuration", "configuration", this.fuelMass > 0 && this.configuration === "Invalid" ? "Custom" : this.configuration)
51
51
  .appendChild("string8u", "aircraft", this.aircraft)
52
52
  .appendChild("float64", "fuel_mass", this.fuelMass)
53
53
  .appendChild("float64", "payload_mass", this.payloadMass);
@@ -0,0 +1,63 @@
1
+ export class AeroflyVector3Float {
2
+ x;
3
+ y;
4
+ z;
5
+ constructor(x, y, z) {
6
+ this.x = x;
7
+ this.y = y;
8
+ this.z = z;
9
+ }
10
+ normalize() {
11
+ const norm = Math.sqrt(this.x * this.x + this.y * this.y + this.z * this.z);
12
+ if (norm === 0) {
13
+ return new AeroflyVector3Float(0, 0, 0);
14
+ }
15
+ return new AeroflyVector3Float(this.x / norm, this.y / norm, this.z / norm);
16
+ }
17
+ cross(b) {
18
+ return new AeroflyVector3Float(this.y * b.z - this.z * b.y, this.z * b.x - this.x * b.z, this.x * b.y - this.y * b.x);
19
+ }
20
+ dot(b) {
21
+ return this.x * b.x + this.y * b.y + this.z * b.z;
22
+ }
23
+ static fromArray(array) {
24
+ return new this(...array);
25
+ }
26
+ toArray() {
27
+ return [this.x, this.y, this.z];
28
+ }
29
+ }
30
+ export class AeroflyMatrix3Float {
31
+ xx;
32
+ yx;
33
+ zx;
34
+ xy;
35
+ yy;
36
+ zy;
37
+ xz;
38
+ yz;
39
+ zz;
40
+ constructor(xx, yx, zx, xy, yy, zy, xz, yz, zz) {
41
+ this.xx = xx;
42
+ this.yx = yx;
43
+ this.zx = zx;
44
+ this.xy = xy;
45
+ this.yy = yy;
46
+ this.zy = zy;
47
+ this.xz = xz;
48
+ this.yz = yz;
49
+ this.zz = zz;
50
+ }
51
+ transpose() {
52
+ return new AeroflyMatrix3Float(this.xx, this.xy, this.xz, this.yx, this.yy, this.yz, this.zx, this.zy, this.zz);
53
+ }
54
+ multiplyVector(v) {
55
+ return new AeroflyVector3Float(this.xx * v.x + this.xy * v.y + this.xz * v.z, this.yx * v.x + this.yy * v.y + this.yz * v.z, this.zx * v.x + this.zy * v.y + this.zz * v.z);
56
+ }
57
+ static fromArray(array) {
58
+ return new this(...array);
59
+ }
60
+ toArray() {
61
+ return [this.xx, this.yx, this.zx, this.xy, this.yy, this.zy, this.xz, this.yz, this.zz];
62
+ }
63
+ }