@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.
- package/CHANGELOG.md +6 -0
- package/dist/dto-flight/AeroflyNavRouteBase.js +3 -2
- package/dist/dto-flight/AeroflyNavRouteBase.test.js +45 -15
- package/dist/dto-flight/AeroflyNavRouteRunway.js +5 -5
- package/dist/dto-flight/AeroflyNavRouteTransition.js +5 -4
- package/dist/dto-flight/AeroflySettingsFlight.js +11 -10
- package/dist/dto-flight/AeroflySettingsFlight.test.js +0 -22
- package/dist/node/AeroflyTypes.js +63 -0
- package/dist/node/Convert.js +54 -23
- package/dist/node/Convert.test.js +51 -21
- package/docs/flight.json +1 -1
- package/docs/flight.mcf +1 -1
- package/docs/flight.xml +1 -1
- package/package.json +1 -1
- package/src/dto-flight/AeroflyFlight.ts +0 -0
- package/src/dto-flight/AeroflyNavRouteBase.test.ts +50 -15
- package/src/dto-flight/AeroflyNavRouteBase.ts +5 -4
- package/src/dto-flight/AeroflyNavRouteRunway.ts +5 -5
- package/src/dto-flight/AeroflyNavRouteTransition.ts +6 -5
- package/src/dto-flight/AeroflyNavRouteWaypoint.ts +1 -1
- package/src/dto-flight/AeroflySettingsFlight.test.ts +0 -30
- package/src/dto-flight/AeroflySettingsFlight.ts +20 -16
- package/src/node/AeroflyTypes.ts +74 -0
- package/src/node/Convert.test.ts +115 -36
- package/src/node/Convert.ts +79 -27
- package/types/dto-flight/AeroflyNavRouteBase.d.ts +2 -2
- package/types/dto-flight/AeroflyNavRouteBase.d.ts.map +1 -1
- package/types/dto-flight/AeroflyNavRouteRunway.d.ts +1 -1
- package/types/dto-flight/AeroflyNavRouteRunway.d.ts.map +1 -1
- package/types/dto-flight/AeroflyNavRouteTransition.d.ts +1 -1
- package/types/dto-flight/AeroflyNavRouteTransition.d.ts.map +1 -1
- package/types/dto-flight/AeroflyNavRouteWaypoint.d.ts +1 -1
- package/types/dto-flight/AeroflyNavRouteWaypoint.d.ts.map +1 -1
- package/types/dto-flight/AeroflySettingsFlight.d.ts +9 -4
- package/types/dto-flight/AeroflySettingsFlight.d.ts.map +1 -1
- package/types/dto-flight/AeroflySettingsFuelLoad.d.ts.map +1 -1
- package/types/node/AeroflyTypes.d.ts +40 -0
- package/types/node/AeroflyTypes.d.ts.map +1 -0
- package/types/node/Convert.d.ts +29 -5
- 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
|
|
5
|
-
const
|
|
6
|
-
{
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
{
|
|
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
|
|
14
|
-
|
|
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
|
|
42
|
+
it("should unpack UIDs for airports to code 1280", () => {
|
|
18
43
|
const unpackPayload = (packed) => {
|
|
19
44
|
return Number(packed & 0xffffn);
|
|
20
45
|
};
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
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:
|
|
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 =
|
|
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
|
|
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
|
|
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 {
|
|
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
|
|
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
|
|
120
|
+
return convertHeadingToOrientation(this.heading_degree, this.position);
|
|
120
121
|
}
|
|
121
122
|
set orientation(orientation) {
|
|
122
|
-
this.heading_degree =
|
|
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
|
+
}
|
package/dist/node/Convert.js
CHANGED
|
@@ -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
|
|
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
|
|
35
|
-
if (coordinates
|
|
36
|
-
lambda = 2 * Math.PI + Math.atan(coordinates
|
|
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
|
|
38
|
+
lambda = Math.atan(coordinates.y / coordinates.x);
|
|
40
39
|
}
|
|
41
40
|
}
|
|
42
|
-
else if (coordinates
|
|
43
|
-
lambda = Math.PI + Math.atan(coordinates
|
|
41
|
+
else if (coordinates.x < 0) {
|
|
42
|
+
lambda = Math.PI + Math.atan(coordinates.y / coordinates.x);
|
|
44
43
|
}
|
|
45
|
-
else if (coordinates
|
|
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
|
|
52
|
-
const phi = Math.atan(coordinates
|
|
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
|
|
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
|
-
|
|
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
|
-
*
|
|
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
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
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 {
|
|
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
|
|
28
|
-
const
|
|
29
|
-
const
|
|
30
|
-
const
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
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
|
|
42
|
-
const
|
|
43
|
-
|
|
44
|
-
|
|
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
|
|
47
|
-
|
|
48
|
-
|
|
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
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.
|
|
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.
|
|
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
|
File without changes
|