@fboes/aerofly-custom-missions 1.13.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 (40) hide show
  1. package/CHANGELOG.md +6 -0
  2. package/dist/dto-flight/AeroflyNavRouteBase.js +3 -2
  3. package/dist/dto-flight/AeroflyNavRouteBase.test.js +45 -15
  4. package/dist/dto-flight/AeroflyNavRouteRunway.js +5 -5
  5. package/dist/dto-flight/AeroflyNavRouteTransition.js +5 -4
  6. package/dist/dto-flight/AeroflySettingsFlight.js +11 -10
  7. package/dist/dto-flight/AeroflySettingsFlight.test.js +0 -22
  8. package/dist/node/AeroflyTypes.js +63 -0
  9. package/dist/node/Convert.js +54 -23
  10. package/dist/node/Convert.test.js +51 -21
  11. package/docs/flight.json +1 -1
  12. package/docs/flight.mcf +1 -1
  13. package/docs/flight.xml +1 -1
  14. package/package.json +1 -1
  15. package/src/dto-flight/AeroflyFlight.ts +0 -0
  16. package/src/dto-flight/AeroflyNavRouteBase.test.ts +50 -15
  17. package/src/dto-flight/AeroflyNavRouteBase.ts +5 -4
  18. package/src/dto-flight/AeroflyNavRouteRunway.ts +5 -5
  19. package/src/dto-flight/AeroflyNavRouteTransition.ts +6 -5
  20. package/src/dto-flight/AeroflyNavRouteWaypoint.ts +1 -1
  21. package/src/dto-flight/AeroflySettingsFlight.test.ts +0 -30
  22. package/src/dto-flight/AeroflySettingsFlight.ts +20 -16
  23. package/src/node/AeroflyTypes.ts +74 -0
  24. package/src/node/Convert.test.ts +115 -36
  25. package/src/node/Convert.ts +79 -27
  26. package/types/dto-flight/AeroflyNavRouteBase.d.ts +2 -2
  27. package/types/dto-flight/AeroflyNavRouteBase.d.ts.map +1 -1
  28. package/types/dto-flight/AeroflyNavRouteRunway.d.ts +1 -1
  29. package/types/dto-flight/AeroflyNavRouteRunway.d.ts.map +1 -1
  30. package/types/dto-flight/AeroflyNavRouteTransition.d.ts +1 -1
  31. package/types/dto-flight/AeroflyNavRouteTransition.d.ts.map +1 -1
  32. package/types/dto-flight/AeroflyNavRouteWaypoint.d.ts +1 -1
  33. package/types/dto-flight/AeroflyNavRouteWaypoint.d.ts.map +1 -1
  34. package/types/dto-flight/AeroflySettingsFlight.d.ts +9 -4
  35. package/types/dto-flight/AeroflySettingsFlight.d.ts.map +1 -1
  36. package/types/dto-flight/AeroflySettingsFuelLoad.d.ts.map +1 -1
  37. package/types/node/AeroflyTypes.d.ts +40 -0
  38. package/types/node/AeroflyTypes.d.ts.map +1 -0
  39. package/types/node/Convert.d.ts +29 -5
  40. package/types/node/Convert.d.ts.map +1 -1
package/CHANGELOG.md CHANGED
@@ -2,6 +2,12 @@
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
+ ## [Unreleased]
6
+
7
+ ## [1.13.1] - 2026-07-24
8
+
9
+ - Fixed orientation / heading parsing
10
+
5
11
  ## [1.13.0] - 2026-07-23
6
12
 
7
13
  - Made unit conversions more precise
@@ -1,5 +1,6 @@
1
1
  import { convertLonLatToVector, convertVectorToLonLat } from "../node/Convert.js";
2
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"
@@ -53,13 +54,13 @@ 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
+ .appendChild("vector3_float64", "Position", this.position.toArray(), `Lon ${this.longitude.toFixed(6)}, Lat ${this.latitude.toFixed(6)}`)
63
64
  .appendChild("uint64", "Uid", this.uid ?? this.getUidFallback(), this.uid ? "" : "Fallback UID, not matching Aerofly FS internal UID");
64
65
  return element;
65
66
  }
@@ -1,26 +1,56 @@
1
1
  import { describe, it } from "node:test";
2
+ import { strict as assert } from "node:assert";
2
3
  import { AeroflyNavRouteDestination } from "../index.js";
3
4
  describe("AeroflyNavRouteBase", () => {
4
- it("should create a valid AeroflyNavRouteBase structure", () => {
5
- const waypoints = [
6
- { wp: new AeroflyNavRouteDestination("MAX-NE", 179.999, 89.999), uid: 0n },
7
- { wp: new AeroflyNavRouteDestination("MAX-SW", -179.999, -89.999), uid: 0n },
8
- { wp: new AeroflyNavRouteDestination("KEYW", -81.7599558, 24.5561197), uid: 5033914504046249216n },
9
- { wp: new AeroflyNavRouteDestination("EGLL", -0.45277777, 51.47138888), uid: 9199731073154483456n },
10
- { wp: new AeroflyNavRouteDestination("YSSY", 151.177, -33.4961), uid: 16969837613165511936n },
11
- { wp: new AeroflyNavRouteDestination("RJTT", 139.779, 35.709), uid: 16385890514078663936n },
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
+ },
12
37
  ];
13
- for (const waypoint of waypoints) {
14
- console.log(waypoint.wp.getUidFallback(), waypoint.uid);
38
+ for (const testCase of testCases) {
39
+ assert.strictEqual(testCase.wp.getUidFallback(), testCase.assertUid, `${testCase.wp.identifier} was ${testCase.ingameUid}`);
15
40
  }
16
41
  });
17
- it("should test some unpacking of UIDs", () => {
42
+ it("should unpack UIDs for airports to code 1280", () => {
18
43
  const unpackPayload = (packed) => {
19
44
  return Number(packed & 0xffffn);
20
45
  };
21
- console.log("KEYW", unpackPayload(5033914504046314752n));
22
- console.log("EGLL", unpackPayload(9200173076798702848n));
23
- console.log("YSSY", unpackPayload(16969826619723154688n));
24
- console.log("RJTT", unpackPayload(16385782762532177152n));
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
+ }
25
55
  });
26
56
  });
@@ -1,6 +1,7 @@
1
- import { convertFeetToMeter, convertMeterToFeet } 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
@@ -45,16 +46,15 @@ class AeroflyNavRouteRunway extends AeroflyNavRouteBase {
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
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"
@@ -38,19 +39,19 @@ class AeroflyNavRouteTransition extends AeroflyNavRouteBase {
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,5 +1,6 @@
1
1
  import { AeroflyConfigurationNode } from "../node/AeroflyConfigurationNode.js";
2
- import { convertFeetToMeter, convertLonLatToVector, convertVectorToLonLat, convertDegreeToMatrix, convertMatrixToDegree, convertMeterToFeet, } from "../node/Convert.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;
@@ -56,9 +57,9 @@ export class AeroflySettingsFlight {
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
  /**
@@ -109,17 +110,17 @@ 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 convertDegreeToMatrix(this.heading_degree);
120
+ return convertHeadingToOrientation(this.heading_degree, this.position);
120
121
  }
121
122
  set orientation(orientation) {
122
- this.heading_degree = convertMatrixToDegree(orientation);
123
+ this.heading_degree = convertOrientationToHeading(orientation, this.position);
123
124
  }
124
125
  /**
125
126
  * @returns {number} altitude in feet AGL
@@ -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",
@@ -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
+ }
@@ -1,3 +1,4 @@
1
+ import { AeroflyVector3Float, AeroflyMatrix3Float } from "./AeroflyTypes.js";
1
2
  /**
2
3
  * @param {number} longitude in degrees
3
4
  * @param {number} latitude in degrees
@@ -19,37 +20,35 @@ export function convertLonLatToVector(longitude, latitude, altitude_meter) {
19
20
  const x = (N + h) * cosLat * cosLon;
20
21
  const y = (N + h) * cosLat * sinLon;
21
22
  const z = (N * (1 - e2) + h) * sinLat;
22
- return [x, y, z];
23
+ return new AeroflyVector3Float(x, y, z);
23
24
  }
24
25
  /**
25
26
  * @param {AeroflyVector3Float} coordinates to convert
26
27
  * @returns {object} as with longitude, latitude, altitude_meter
27
28
  */
28
29
  export function convertVectorToLonLat(coordinates) {
29
- // TODO: This implementation is not correct
30
30
  const f = 1.0 / 298.257223563; // WGS84
31
31
  const e2 = 2 * f - f * f;
32
- //const lambda = VectorToAngle( coordinates[0], coordinates[1] );
33
32
  let lambda = 0;
34
- if (coordinates[0] > 0) {
35
- if (coordinates[1] < 0) {
36
- lambda = 2 * Math.PI + Math.atan(coordinates[1] / coordinates[0]);
33
+ if (coordinates.x > 0) {
34
+ if (coordinates.y < 0) {
35
+ lambda = 2 * Math.PI + Math.atan(coordinates.y / coordinates.x);
37
36
  }
38
37
  else {
39
- lambda = Math.atan(coordinates[1] / coordinates[0]);
38
+ lambda = Math.atan(coordinates.y / coordinates.x);
40
39
  }
41
40
  }
42
- else if (coordinates[0] < 0) {
43
- lambda = Math.PI + Math.atan(coordinates[1] / coordinates[0]);
41
+ else if (coordinates.x < 0) {
42
+ lambda = Math.PI + Math.atan(coordinates.y / coordinates.x);
44
43
  }
45
- else if (coordinates[1] > 0) {
44
+ else if (coordinates.y > 0) {
46
45
  lambda = 0.5 * Math.PI;
47
46
  }
48
47
  else {
49
48
  lambda = 1.5 * Math.PI;
50
49
  }
51
- const rho = Math.sqrt(coordinates[0] * coordinates[0] + coordinates[1] * coordinates[1]);
52
- const phi = Math.atan(coordinates[2] / ((1.0 - e2) * rho));
50
+ const rho = Math.sqrt(coordinates.x * coordinates.x + coordinates.y * coordinates.y);
51
+ const phi = Math.atan(coordinates.z / ((1.0 - e2) * rho));
53
52
  const longitude = (lambda * 180) / Math.PI;
54
53
  const latitude = (phi * 180) / Math.PI;
55
54
  const altitude_meter = rho / Math.cos(phi) - 6378137.0 / Math.sqrt(1 - e2 * Math.sin(phi) * Math.sin(phi));
@@ -60,29 +59,61 @@ export function convertVectorToLonLat(coordinates) {
60
59
  };
61
60
  }
62
61
  /**
62
+ * This method receives a heading in degrees and a position vector, and calculates the orientation matrix.
63
+ * The orientation matrix is calculated based on the heading and the position of the aircraft.
63
64
  * @param {number} heading_degree in deg
65
+ * @param {AeroflyVector3Float} position as vector
64
66
  * @returns {AeroflyMatrix3Float} for Aerofly
65
67
  */
66
- export function convertDegreeToMatrix(heading_degree) {
67
- // TODO: This implementation is not correct
68
+ export function convertHeadingToOrientation(heading_degree, position) {
68
69
  const theta = heading_degree * (Math.PI / 180); // heading in radians
69
- const cosTheta = Math.cos(theta);
70
70
  const sinTheta = Math.sin(theta);
71
- return [cosTheta, -sinTheta, 0, sinTheta, cosTheta, 0, 0, 0, 1];
71
+ const cosTheta = Math.cos(theta);
72
+ // Local ENU frame at this position — mirrors convertOrientationToHeading
73
+ const east = new AeroflyVector3Float(-position.y, position.x, 0).normalize();
74
+ const upRaw = new AeroflyVector3Float(position.x, position.y, position.z / (1.0 - 0.00669437999014));
75
+ const up = upRaw.normalize();
76
+ const north = up.cross(east).normalize();
77
+ // Heading -> forward vector in ENU space (sinθ, cosθ, 0), then rotated into world space
78
+ const forward = new AeroflyVector3Float(east.x * sinTheta + north.x * cosTheta, east.y * sinTheta + north.y * cosTheta, east.z * sinTheta + north.z * cosTheta).normalize();
79
+ // Complete the right-handed orthonormal basis (x = forward, z = up, y = up × forward)
80
+ const right = up.cross(forward).normalize();
81
+ return new AeroflyMatrix3Float(...forward.toArray(), ...right.toArray(), ...up.toArray());
72
82
  }
73
83
  /**
74
- * @param {AeroflyMatrix3Float} orientation as Matrxi
84
+ * This method receives an orientation matrix and a position vector, and calculates the heading in degrees.
85
+ * The heading is calculated based on the direction of the aircraft in relation to the north direction.
86
+ * @param {AeroflyMatrix3Float} orientation as matrix
87
+ * @param {AeroflyVector3Float} position as vector
75
88
  * @returns {number} heading in degrees
76
89
  * @see https://www.aerofly.com/community/forum/index.php?thread/28025-custom-missions-file-livery-and-parking-position-property/&postID=184313#post184313
77
90
  * @see
78
91
  */
79
- export function convertMatrixToDegree(orientation) {
80
- // TODO: https://www.aerofly.com/community/forum/index.php?thread/28025-custom-missions-file-livery-and-parking-position-property/&postID=184313#post184313
81
- const headingRad = Math.atan2(orientation[3], orientation[0]);
82
- let headingDeg = headingRad * (180 / Math.PI);
83
- // Normalize to [0, 360)
84
- if (headingDeg < 0)
92
+ export function convertOrientationToHeading(orientation, position) {
93
+ const direction = orientation.multiplyVector(new AeroflyVector3Float(1, 0, 0));
94
+ return convertDirectionToHeading(direction, position);
95
+ }
96
+ /**
97
+ * This method receives an orientation matrix and a position vector, and calculates the heading in degrees.
98
+ * The heading is calculated based on the direction of the aircraft in relation to the north direction.
99
+ * @param {AeroflyVector3Float} direction as vector
100
+ * @param {AeroflyVector3Float} position as vector
101
+ * @returns {number} heading in degrees
102
+ * @see https://www.aerofly.com/community/forum/index.php?thread/28025-custom-missions-file-livery-and-parking-position-property/&postID=184313#post184313
103
+ * @see
104
+ */
105
+ export function convertDirectionToHeading(direction, position) {
106
+ const east = new AeroflyVector3Float(-position.y, position.x, 0).normalize();
107
+ let up = position;
108
+ up.z *= 1.0 / (1.0 - 0.00669437999014); // WGS84
109
+ up = up.normalize();
110
+ const north = up.cross(east).normalize();
111
+ const m = new AeroflyMatrix3Float(...east.toArray(), ...north.toArray(), ...up.toArray()).transpose();
112
+ const local_direction = m.multiplyVector(direction);
113
+ let headingDeg = Math.atan2(local_direction.x, local_direction.y) * (180 / Math.PI);
114
+ if (headingDeg < 0) {
85
115
  headingDeg += 360;
116
+ }
86
117
  return headingDeg;
87
118
  }
88
119
  /**
@@ -1,6 +1,7 @@
1
1
  import { describe, it } from "node:test";
2
2
  import { strict as assert } from "node:assert";
3
- import { convertDegreeToMatrix, convertFeetToMeter, convertLonLatToVector, convertMatrixToDegree, convertMeterToFeet, convertVectorToLonLat, } from "./Convert.js";
3
+ import { convertHeadingToOrientation, convertFeetToMeter, convertLonLatToVector, convertOrientationToHeading, convertMeterToFeet, convertVectorToLonLat, } from "./Convert.js";
4
+ import { AeroflyMatrix3Float, AeroflyVector3Float } from "./AeroflyTypes.js";
4
5
  describe("Convert", () => {
5
6
  it("should convert feet to meters correctly", () => {
6
7
  const feet = 1000;
@@ -24,27 +25,56 @@ describe("Convert", () => {
24
25
  assert.strictEqual(lonLat.longitude.toFixed(4), expectedLongitude.toFixed(4), `Expected longitude to be ${expectedLongitude.toFixed(4)}, got ${lonLat.longitude.toFixed(4)}`);
25
26
  assert.strictEqual(lonLat.latitude.toFixed(4), expectedLatitude.toFixed(4), `Expected latitude to be ${expectedLatitude.toFixed(4)}, got ${lonLat.latitude.toFixed(4)}`);
26
27
  });
27
- it("should convert heading degree to orientation matrix and back correctly", () => {
28
- const orientationMatrix = [0, -1, 0, 1, 0, 0, 0, 0, 1];
29
- const expectedHeadingDegree = 90;
30
- const actualHeadingDegree = convertMatrixToDegree(orientationMatrix);
31
- assert.strictEqual(actualHeadingDegree.toFixed(4), expectedHeadingDegree.toFixed(4), `Expected heading degree to be ${expectedHeadingDegree.toFixed(4)}, got ${actualHeadingDegree.toFixed(4)}`);
32
- });
33
- it("should convert heading degree to orientation matrix and back correctly", () => {
34
- const heading_degree = 90;
35
- const expectedHeadingDegree = 90;
36
- const orientationMatrix = convertDegreeToMatrix(heading_degree);
37
- console.log("Orientation Matrix:", orientationMatrix);
38
- const actualHeadingDegree = convertMatrixToDegree(orientationMatrix);
39
- assert.strictEqual(actualHeadingDegree.toFixed(4), expectedHeadingDegree.toFixed(4), `Expected heading degree to be ${expectedHeadingDegree.toFixed(4)}, got ${actualHeadingDegree.toFixed(4)}`);
28
+ it("should convert orientation matrix to heading degree", () => {
29
+ const positionKEYW = new AeroflyVector3Float(831922.0452075421, -5744695.679418418, 2634432.6068597846);
30
+ const positionYSSY = new AeroflyVector3Float(-4640429.92586909, 2553500.8064244, -3541489.45541516);
31
+ const testCases = [
32
+ {
33
+ orientation: new AeroflyMatrix3Float(-0.0668066264395785, 0.431792064197595, 0.899495685348041, -0.98941528906399, -0.145060537051883, -0.00385050082983023, 0.128818711490931, -0.890232022500988, 0.43691267512355),
34
+ position: positionKEYW,
35
+ heading: 0,
36
+ },
37
+ {
38
+ orientation: new AeroflyMatrix3Float(0.985978516472605, 0.166832368887515, -0.00365044461148948, -0.066064640816506, 0.41034317377044, 0.90953501470465, 0.153237816116869, -0.896540819166301, 0.41561127424596),
39
+ position: positionKEYW,
40
+ heading: 90,
41
+ },
42
+ {
43
+ orientation: new AeroflyMatrix3Float(-0.465135059130883, -0.885151853036744, 0.0124729239927332, -0.489418463305539, 0.268872261996669, 0.829564508946978, -0.737644185595572, 0.379755057628484, -0.558271575187768),
44
+ position: positionYSSY,
45
+ heading: 90,
46
+ },
47
+ ];
48
+ for (const testCase of testCases) {
49
+ const calculatedHeading = convertOrientationToHeading(testCase.orientation, testCase.position);
50
+ const message = `Expected heading to be ${testCase.heading.toFixed(4)}, got ${calculatedHeading.toFixed(4)}`;
51
+ assert.strictEqual(Math.round(calculatedHeading) % 360, Math.round(testCase.heading) % 360, message);
52
+ }
40
53
  });
41
- it("should show the orientation at Tellruide correctly", () => {
42
- const orientationMatrix = [
43
- -0.449510470601295, -0.541223404385256, -0.710645877610949, 0.868733701939534, -0.450067862031944,
44
- -0.206738179058507, -0.207947329711721, -0.710293000184874, 0.672489228132418,
54
+ it("should convert heading degree to orientation matrix (ignoring pitch, roll, yaw, etc.)", () => {
55
+ const positionKEYW = new AeroflyVector3Float(831922.0452075421, -5744695.679418418, 2634432.6068597846);
56
+ const positionYSSY = new AeroflyVector3Float(-4640429.92586909, 2553500.8064244, -3541489.45541516);
57
+ const testCases = [
58
+ {
59
+ orientation: new AeroflyMatrix3Float(-0.0668066264395785, 0.431792064197595, 0.899495685348041, -0.98941528906399, -0.145060537051883, -0.00385050082983023, 0.128818711490931, -0.890232022500988, 0.43691267512355),
60
+ position: positionKEYW,
61
+ heading: 0,
62
+ },
63
+ {
64
+ orientation: new AeroflyMatrix3Float(0.985978516472605, 0.166832368887515, -0.00365044461148948, -0.066064640816506, 0.41034317377044, 0.90953501470465, 0.153237816116869, -0.896540819166301, 0.41561127424596),
65
+ position: positionKEYW,
66
+ heading: 90,
67
+ },
68
+ {
69
+ orientation: new AeroflyMatrix3Float(-0.465135059130883, -0.885151853036744, 0.0124729239927332, -0.489418463305539, 0.268872261996669, 0.829564508946978, -0.737644185595572, 0.379755057628484, -0.558271575187768),
70
+ position: positionYSSY,
71
+ heading: 90,
72
+ },
45
73
  ];
46
- const expectedHeadingDegree = 117.3585;
47
- const actualHeadingDegree = convertMatrixToDegree(orientationMatrix);
48
- assert.strictEqual(actualHeadingDegree.toFixed(4), expectedHeadingDegree.toFixed(4), `Expected heading degree to be ${expectedHeadingDegree.toFixed(4)}, got ${actualHeadingDegree.toFixed(4)}`);
74
+ for (const testCase of testCases) {
75
+ const calculatedOrientation = convertHeadingToOrientation(testCase.heading, testCase.position);
76
+ const calculatedHeading = convertOrientationToHeading(calculatedOrientation, testCase.position);
77
+ assert.strictEqual(calculatedHeading, testCase.heading);
78
+ }
49
79
  });
50
80
  });
package/docs/flight.json CHANGED
@@ -8,7 +8,7 @@
8
8
  "flightSetting": {
9
9
  "longitude": -107.90107635928504,
10
10
  "latitude": 37.95381167941058,
11
- "heading_degree": 117.3584840119476,
11
+ "heading_degree": 195.1999966628333,
12
12
  "speed_kts": 0,
13
13
  "gear": 1,
14
14
  "throttle": 0,
package/docs/flight.mcf CHANGED
@@ -9,7 +9,7 @@
9
9
  <[tmsettings_flight][flight_setting][]
10
10
  <[vector3_float64][position][-1548480.2914129882 -4793879.340758891 3903103.47375716]> // Lon -107.901076, Lat 37.953812, 9073 ft
11
11
  <[vector3_float64][velocity][0 0 0]> // 0 kts
12
- <[matrix3_float64][orientation][-0.4595563612734764 -0.8881486085183504 0 0.8881486085183504 -0.4595563612734764 0 0 0 1]> // 117° heading
12
+ <[matrix3_float64][orientation][-0.4319264385609424 -0.48418840234394955 -0.7609212460614266 0.8687335474152472 -0.45006838984503683 -0.20673767933692305 -0.24236661335130477 -0.7503332829469246 0.6150271451202048]> // 195° heading
13
13
  <[float64][gear][1]>
14
14
  <[float64][throttle][0]>
15
15
  <[float64][flaps][0]>
package/docs/flight.xml CHANGED
@@ -9,7 +9,7 @@
9
9
  <flight_setting type="tmsettings_flight">
10
10
  <position type="vector3_float64">-1548480.2914129882 -4793879.340758891 3903103.47375716</position> <!-- Lon -107.901076, Lat 37.953812, 9073 ft -->
11
11
  <velocity type="vector3_float64">0 0 0</velocity> <!-- 0 kts -->
12
- <orientation type="matrix3_float64">-0.4595563612734764 -0.8881486085183504 0 0.8881486085183504 -0.4595563612734764 0 0 0 1</orientation> <!-- 117° heading -->
12
+ <orientation type="matrix3_float64">-0.4319264385609424 -0.48418840234394955 -0.7609212460614266 0.8687335474152472 -0.45006838984503683 -0.20673767933692305 -0.24236661335130477 -0.7503332829469246 0.6150271451202048</orientation> <!-- 195° heading -->
13
13
  <gear type="float64">1</gear>
14
14
  <throttle type="float64">0</throttle>
15
15
  <flaps type="float64">0</flaps>
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@fboes/aerofly-custom-missions",
3
- "version": "1.13.0",
3
+ "version": "1.13.1",
4
4
  "description": "Builder for Aerofly FS4 Custom Missions Files",
5
5
  "main": "dist/index.js",
6
6
  "scripts": {
File without changes