@spacexr/geodesy 0.1.0
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 +18 -0
- package/LICENSE +201 -0
- package/README.md +199 -0
- package/dist/chunk-RJFTT4KI.js +227 -0
- package/dist/chunk-RJFTT4KI.js.map +1 -0
- package/dist/chunk-WYCQOUE7.js +488 -0
- package/dist/chunk-WYCQOUE7.js.map +1 -0
- package/dist/ellipsoid.d.ts +150 -0
- package/dist/ellipsoid.js +3 -0
- package/dist/ellipsoid.js.map +1 -0
- package/dist/geodetic-system-DRpWY8qG.d.ts +257 -0
- package/dist/index.d.ts +39 -0
- package/dist/index.js +4 -0
- package/dist/index.js.map +1 -0
- package/dist/system.d.ts +2 -0
- package/dist/system.js +4 -0
- package/dist/system.js.map +1 -0
- package/package.json +89 -0
- package/src/angles.ts +48 -0
- package/src/ellipsoid.ts +261 -0
- package/src/geodetic-system.ts +548 -0
- package/src/index.ts +4 -0
- package/src/types.ts +46 -0
|
@@ -0,0 +1,548 @@
|
|
|
1
|
+
import { normalizeLongitudeRadians, toDegrees, toRadians } from "./angles";
|
|
2
|
+
import { Ellipsoid } from "./ellipsoid";
|
|
3
|
+
import type { ICartesian3, IGeodeticCoordinates, ILocalTangentBasis } from "./types";
|
|
4
|
+
|
|
5
|
+
const POLE_EPSILON = 1e-12;
|
|
6
|
+
const CONVERGENCE_EPSILON = 1e-14;
|
|
7
|
+
const MAX_GEODETIC_ITERATIONS = 10;
|
|
8
|
+
|
|
9
|
+
function cartesianTarget(target?: ICartesian3): ICartesian3 {
|
|
10
|
+
return target ?? { x: 0, y: 0, z: 0 };
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
function geodeticTarget(target?: IGeodeticCoordinates): IGeodeticCoordinates {
|
|
14
|
+
return target ?? { latitude: 0, longitude: 0, height: 0 };
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
function validateGeodetic(latitudeRadians: number, longitudeRadians: number, height: number): void {
|
|
18
|
+
if (!Number.isFinite(latitudeRadians) || latitudeRadians < -Math.PI / 2 || latitudeRadians > Math.PI / 2) {
|
|
19
|
+
throw new RangeError("Geodetic latitude must be a finite value in the interval [-PI/2, PI/2].");
|
|
20
|
+
}
|
|
21
|
+
if (!Number.isFinite(longitudeRadians)) throw new RangeError("Geodetic longitude must be finite.");
|
|
22
|
+
if (!Number.isFinite(height)) throw new RangeError("Ellipsoidal height must be finite.");
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
function validateCartesian(position: ICartesian3): void {
|
|
26
|
+
if (!Number.isFinite(position.x) || !Number.isFinite(position.y) || !Number.isFinite(position.z)) {
|
|
27
|
+
throw new RangeError("Cartesian coordinates must be finite.");
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
function dot(left: ICartesian3, right: ICartesian3): number {
|
|
32
|
+
return left.x * right.x + left.y * right.y + left.z * right.z;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
/**
|
|
36
|
+
* Immutable geodetic reference system backed by one reference ellipsoid.
|
|
37
|
+
*
|
|
38
|
+
* ECEF follows EPSG:4978 axis conventions: X crosses latitude 0 and longitude
|
|
39
|
+
* 0, Y crosses latitude 0 and longitude 90 degrees east, and Z crosses the
|
|
40
|
+
* north pole. Cartesian distances and ellipsoidal heights are expressed in
|
|
41
|
+
* metres.
|
|
42
|
+
*/
|
|
43
|
+
export class GeodeticSystem {
|
|
44
|
+
/** Shared WGS84 geodetic system. The instance is immutable. */
|
|
45
|
+
public static readonly WGS84 = new GeodeticSystem(Ellipsoid.WGS84);
|
|
46
|
+
|
|
47
|
+
/**
|
|
48
|
+
* Creates a geodetic system.
|
|
49
|
+
*
|
|
50
|
+
* @param ellipsoid - Reference ellipsoid. Defaults to WGS84.
|
|
51
|
+
*/
|
|
52
|
+
public constructor(public readonly ellipsoid: Ellipsoid = Ellipsoid.WGS84) {}
|
|
53
|
+
|
|
54
|
+
/**
|
|
55
|
+
* Converts geodetic coordinates in radians to ECEF.
|
|
56
|
+
*
|
|
57
|
+
* @param latitudeRadians - Geodetic latitude in radians.
|
|
58
|
+
* @param longitudeRadians - Longitude in radians east of the reference meridian.
|
|
59
|
+
* @param height - Ellipsoidal height in metres.
|
|
60
|
+
* @param target - Optional mutable output object, used to avoid an allocation.
|
|
61
|
+
* @returns The target populated with ECEF metres.
|
|
62
|
+
*/
|
|
63
|
+
public geodeticRadiansToEcef(latitudeRadians: number, longitudeRadians: number, height = 0, target?: ICartesian3): ICartesian3 {
|
|
64
|
+
validateGeodetic(latitudeRadians, longitudeRadians, height);
|
|
65
|
+
const output = cartesianTarget(target);
|
|
66
|
+
const sineLatitude = Math.sin(latitudeRadians);
|
|
67
|
+
const cosineLatitude = Math.cos(latitudeRadians);
|
|
68
|
+
const cosineLongitude = Math.cos(longitudeRadians);
|
|
69
|
+
const sineLongitude = Math.sin(longitudeRadians);
|
|
70
|
+
const primeVerticalRadius = this.ellipsoid.primeVerticalRadius(latitudeRadians);
|
|
71
|
+
const horizontalRadius = (primeVerticalRadius + height) * cosineLatitude;
|
|
72
|
+
output.x = horizontalRadius * cosineLongitude;
|
|
73
|
+
output.y = horizontalRadius * sineLongitude;
|
|
74
|
+
output.z = (primeVerticalRadius * this.ellipsoid.oneMinusSquaredEccentricity + height) * sineLatitude;
|
|
75
|
+
return output;
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
/**
|
|
79
|
+
* Converts geodetic coordinates in degrees to ECEF.
|
|
80
|
+
*
|
|
81
|
+
* @param latitudeDegrees - Geodetic latitude in degrees.
|
|
82
|
+
* @param longitudeDegrees - Longitude in degrees east of the reference meridian.
|
|
83
|
+
* @param height - Ellipsoidal height in metres.
|
|
84
|
+
* @param target - Optional mutable output object, used to avoid an allocation.
|
|
85
|
+
* @returns The target populated with ECEF metres.
|
|
86
|
+
*/
|
|
87
|
+
public geodeticDegreesToEcef(latitudeDegrees: number, longitudeDegrees: number, height = 0, target?: ICartesian3): ICartesian3 {
|
|
88
|
+
return this.geodeticRadiansToEcef(toRadians(latitudeDegrees), toRadians(longitudeDegrees), height, target);
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
/**
|
|
92
|
+
* Converts ECEF coordinates to geodetic coordinates in radians.
|
|
93
|
+
*
|
|
94
|
+
* Bowring initialization followed by a bounded fixed-point refinement gives
|
|
95
|
+
* stable results at the equator, at the poles, below the ellipsoid and at
|
|
96
|
+
* orbital altitudes. The ellipsoid centre is rejected because it has no
|
|
97
|
+
* unique latitude or longitude.
|
|
98
|
+
*
|
|
99
|
+
* @param position - ECEF coordinate in metres.
|
|
100
|
+
* @param target - Optional mutable output object, used to avoid an allocation.
|
|
101
|
+
* @returns Geodetic latitude and longitude in radians, with height in metres.
|
|
102
|
+
*/
|
|
103
|
+
public ecefToGeodeticRadians(position: ICartesian3, target?: IGeodeticCoordinates): IGeodeticCoordinates {
|
|
104
|
+
validateCartesian(position);
|
|
105
|
+
const output = geodeticTarget(target);
|
|
106
|
+
const { x, y, z } = position;
|
|
107
|
+
const distanceFromAxis = Math.hypot(x, y);
|
|
108
|
+
if (distanceFromAxis < POLE_EPSILON) {
|
|
109
|
+
if (Math.abs(z) < POLE_EPSILON) throw new RangeError("The ellipsoid centre has no unique geodetic coordinate.");
|
|
110
|
+
output.latitude = z > 0 ? Math.PI / 2 : -Math.PI / 2;
|
|
111
|
+
output.longitude = 0;
|
|
112
|
+
output.height = Math.abs(z) - this.ellipsoid.semiMinorAxis;
|
|
113
|
+
return output;
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
const major = this.ellipsoid.semiMajorAxis;
|
|
117
|
+
const minor = this.ellipsoid.semiMinorAxis;
|
|
118
|
+
const eccentricitySquared = this.ellipsoid.squaredEccentricity;
|
|
119
|
+
const secondEccentricitySquared = this.ellipsoid.secondSquaredEccentricity;
|
|
120
|
+
const longitude = Math.atan2(y, x);
|
|
121
|
+
const bowringAngle = Math.atan2(z * major, distanceFromAxis * minor);
|
|
122
|
+
const sineBowring = Math.sin(bowringAngle);
|
|
123
|
+
const cosineBowring = Math.cos(bowringAngle);
|
|
124
|
+
let latitude = Math.atan2(
|
|
125
|
+
z + secondEccentricitySquared * minor * sineBowring ** 3,
|
|
126
|
+
distanceFromAxis - eccentricitySquared * major * cosineBowring ** 3,
|
|
127
|
+
);
|
|
128
|
+
|
|
129
|
+
let height = 0;
|
|
130
|
+
for (let iteration = 0; iteration < MAX_GEODETIC_ITERATIONS; iteration++) {
|
|
131
|
+
const sineLatitude = Math.sin(latitude);
|
|
132
|
+
const cosineLatitude = Math.cos(latitude);
|
|
133
|
+
const primeVerticalRadius = this.ellipsoid.primeVerticalRadius(latitude);
|
|
134
|
+
height =
|
|
135
|
+
Math.abs(cosineLatitude) > 1e-10
|
|
136
|
+
? distanceFromAxis / cosineLatitude - primeVerticalRadius
|
|
137
|
+
: z / sineLatitude - primeVerticalRadius * this.ellipsoid.oneMinusSquaredEccentricity;
|
|
138
|
+
const denominator = 1 - (eccentricitySquared * primeVerticalRadius) / (primeVerticalRadius + height);
|
|
139
|
+
const nextLatitude = Math.atan2(z, distanceFromAxis * denominator);
|
|
140
|
+
if (Math.abs(nextLatitude - latitude) <= CONVERGENCE_EPSILON) {
|
|
141
|
+
latitude = nextLatitude;
|
|
142
|
+
break;
|
|
143
|
+
}
|
|
144
|
+
latitude = nextLatitude;
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
const sineLatitude = Math.sin(latitude);
|
|
148
|
+
const cosineLatitude = Math.cos(latitude);
|
|
149
|
+
const primeVerticalRadius = this.ellipsoid.primeVerticalRadius(latitude);
|
|
150
|
+
height =
|
|
151
|
+
Math.abs(cosineLatitude) > 1e-10
|
|
152
|
+
? distanceFromAxis / cosineLatitude - primeVerticalRadius
|
|
153
|
+
: z / sineLatitude - primeVerticalRadius * this.ellipsoid.oneMinusSquaredEccentricity;
|
|
154
|
+
|
|
155
|
+
output.latitude = latitude;
|
|
156
|
+
output.longitude = normalizeLongitudeRadians(longitude);
|
|
157
|
+
output.height = height;
|
|
158
|
+
return output;
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
/**
|
|
162
|
+
* Converts ECEF coordinates to geodetic coordinates in degrees.
|
|
163
|
+
*
|
|
164
|
+
* @param position - ECEF coordinate in metres.
|
|
165
|
+
* @param target - Optional mutable output object, used to avoid an allocation.
|
|
166
|
+
* @returns Geodetic latitude and longitude in degrees, with height in metres.
|
|
167
|
+
*/
|
|
168
|
+
public ecefToGeodeticDegrees(position: ICartesian3, target?: IGeodeticCoordinates): IGeodeticCoordinates {
|
|
169
|
+
const output = this.ecefToGeodeticRadians(position, target);
|
|
170
|
+
output.latitude = toDegrees(output.latitude);
|
|
171
|
+
output.longitude = toDegrees(output.longitude);
|
|
172
|
+
return output;
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
/**
|
|
176
|
+
* Returns the outward ellipsoid-normal unit vector at geodetic coordinates.
|
|
177
|
+
*
|
|
178
|
+
* @param latitudeRadians - Geodetic latitude in radians.
|
|
179
|
+
* @param longitudeRadians - Longitude in radians.
|
|
180
|
+
* @param target - Optional mutable output object.
|
|
181
|
+
* @returns Unit vector expressed in ECEF axes.
|
|
182
|
+
*/
|
|
183
|
+
public geodeticSurfaceNormalRadians(latitudeRadians: number, longitudeRadians: number, target?: ICartesian3): ICartesian3 {
|
|
184
|
+
validateGeodetic(latitudeRadians, longitudeRadians, 0);
|
|
185
|
+
const output = cartesianTarget(target);
|
|
186
|
+
const cosineLatitude = Math.cos(latitudeRadians);
|
|
187
|
+
output.x = cosineLatitude * Math.cos(longitudeRadians);
|
|
188
|
+
output.y = cosineLatitude * Math.sin(longitudeRadians);
|
|
189
|
+
output.z = Math.sin(latitudeRadians);
|
|
190
|
+
return output;
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
/**
|
|
194
|
+
* Returns the outward ellipsoid-normal unit vector at geodetic coordinates.
|
|
195
|
+
*
|
|
196
|
+
* @param latitudeDegrees - Geodetic latitude in degrees.
|
|
197
|
+
* @param longitudeDegrees - Longitude in degrees.
|
|
198
|
+
* @param target - Optional mutable output object.
|
|
199
|
+
* @returns Unit vector expressed in ECEF axes.
|
|
200
|
+
*/
|
|
201
|
+
public geodeticSurfaceNormalDegrees(latitudeDegrees: number, longitudeDegrees: number, target?: ICartesian3): ICartesian3 {
|
|
202
|
+
return this.geodeticSurfaceNormalRadians(toRadians(latitudeDegrees), toRadians(longitudeDegrees), target);
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
/**
|
|
206
|
+
* Creates an immutable local east, north, up tangent plane.
|
|
207
|
+
*
|
|
208
|
+
* @param latitudeRadians - Origin geodetic latitude in radians.
|
|
209
|
+
* @param longitudeRadians - Origin longitude in radians.
|
|
210
|
+
* @param height - Origin ellipsoidal height in metres.
|
|
211
|
+
* @returns A local tangent plane bound to this geodetic system.
|
|
212
|
+
*/
|
|
213
|
+
public createLocalTangentPlaneRadians(latitudeRadians: number, longitudeRadians: number, height = 0): LocalTangentPlane {
|
|
214
|
+
return new LocalTangentPlane(this, { latitude: latitudeRadians, longitude: longitudeRadians, height });
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
/**
|
|
218
|
+
* Creates an immutable local east, north, up tangent plane.
|
|
219
|
+
*
|
|
220
|
+
* @param latitudeDegrees - Origin geodetic latitude in degrees.
|
|
221
|
+
* @param longitudeDegrees - Origin longitude in degrees.
|
|
222
|
+
* @param height - Origin ellipsoidal height in metres.
|
|
223
|
+
* @returns A local tangent plane bound to this geodetic system.
|
|
224
|
+
*/
|
|
225
|
+
public createLocalTangentPlaneDegrees(latitudeDegrees: number, longitudeDegrees: number, height = 0): LocalTangentPlane {
|
|
226
|
+
return this.createLocalTangentPlaneRadians(toRadians(latitudeDegrees), toRadians(longitudeDegrees), height);
|
|
227
|
+
}
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
/**
|
|
231
|
+
* Immutable local tangent plane anchored to a geodetic coordinate.
|
|
232
|
+
*
|
|
233
|
+
* ENU axes are X east, Y north and Z up. NED axes are X north, Y east and Z
|
|
234
|
+
* down. All local values are metres. The stored matrices use column-major
|
|
235
|
+
* order and multiply column vectors, matching glTF and 3D Tiles transforms.
|
|
236
|
+
*/
|
|
237
|
+
export class LocalTangentPlane {
|
|
238
|
+
private readonly origin: IGeodeticCoordinates;
|
|
239
|
+
private readonly originCartesian: ICartesian3;
|
|
240
|
+
private readonly tangentBasis: ILocalTangentBasis;
|
|
241
|
+
private readonly toEnu: Float64Array;
|
|
242
|
+
private readonly fromEnu: Float64Array;
|
|
243
|
+
private readonly toNed: Float64Array;
|
|
244
|
+
private readonly fromNed: Float64Array;
|
|
245
|
+
|
|
246
|
+
/**
|
|
247
|
+
* Creates a local tangent plane.
|
|
248
|
+
*
|
|
249
|
+
* Prefer {@link GeodeticSystem.createLocalTangentPlaneRadians} or
|
|
250
|
+
* {@link GeodeticSystem.createLocalTangentPlaneDegrees}, which make the
|
|
251
|
+
* angular unit explicit at the call site.
|
|
252
|
+
*
|
|
253
|
+
* @param system - Geodetic reference system.
|
|
254
|
+
* @param originRadians - Origin with angular components in radians.
|
|
255
|
+
*/
|
|
256
|
+
public constructor(
|
|
257
|
+
public readonly system: GeodeticSystem,
|
|
258
|
+
originRadians: IGeodeticCoordinates,
|
|
259
|
+
) {
|
|
260
|
+
validateGeodetic(originRadians.latitude, originRadians.longitude, originRadians.height);
|
|
261
|
+
this.origin = { ...originRadians, longitude: normalizeLongitudeRadians(originRadians.longitude) };
|
|
262
|
+
this.originCartesian = system.geodeticRadiansToEcef(this.origin.latitude, this.origin.longitude, this.origin.height);
|
|
263
|
+
const sineLatitude = Math.sin(this.origin.latitude);
|
|
264
|
+
const cosineLatitude = Math.cos(this.origin.latitude);
|
|
265
|
+
const sineLongitude = Math.sin(this.origin.longitude);
|
|
266
|
+
const cosineLongitude = Math.cos(this.origin.longitude);
|
|
267
|
+
this.tangentBasis = {
|
|
268
|
+
east: { x: -sineLongitude, y: cosineLongitude, z: 0 },
|
|
269
|
+
north: {
|
|
270
|
+
x: -sineLatitude * cosineLongitude,
|
|
271
|
+
y: -sineLatitude * sineLongitude,
|
|
272
|
+
z: cosineLatitude,
|
|
273
|
+
},
|
|
274
|
+
up: {
|
|
275
|
+
x: cosineLatitude * cosineLongitude,
|
|
276
|
+
y: cosineLatitude * sineLongitude,
|
|
277
|
+
z: sineLatitude,
|
|
278
|
+
},
|
|
279
|
+
};
|
|
280
|
+
const { east, north, up } = this.tangentBasis;
|
|
281
|
+
this.toEnu = new Float64Array([
|
|
282
|
+
east.x,
|
|
283
|
+
north.x,
|
|
284
|
+
up.x,
|
|
285
|
+
0,
|
|
286
|
+
east.y,
|
|
287
|
+
north.y,
|
|
288
|
+
up.y,
|
|
289
|
+
0,
|
|
290
|
+
east.z,
|
|
291
|
+
north.z,
|
|
292
|
+
up.z,
|
|
293
|
+
0,
|
|
294
|
+
-dot(east, this.originCartesian),
|
|
295
|
+
-dot(north, this.originCartesian),
|
|
296
|
+
-dot(up, this.originCartesian),
|
|
297
|
+
1,
|
|
298
|
+
]);
|
|
299
|
+
this.fromEnu = new Float64Array([
|
|
300
|
+
east.x,
|
|
301
|
+
east.y,
|
|
302
|
+
east.z,
|
|
303
|
+
0,
|
|
304
|
+
north.x,
|
|
305
|
+
north.y,
|
|
306
|
+
north.z,
|
|
307
|
+
0,
|
|
308
|
+
up.x,
|
|
309
|
+
up.y,
|
|
310
|
+
up.z,
|
|
311
|
+
0,
|
|
312
|
+
this.originCartesian.x,
|
|
313
|
+
this.originCartesian.y,
|
|
314
|
+
this.originCartesian.z,
|
|
315
|
+
1,
|
|
316
|
+
]);
|
|
317
|
+
this.toNed = new Float64Array([
|
|
318
|
+
north.x,
|
|
319
|
+
east.x,
|
|
320
|
+
-up.x,
|
|
321
|
+
0,
|
|
322
|
+
north.y,
|
|
323
|
+
east.y,
|
|
324
|
+
-up.y,
|
|
325
|
+
0,
|
|
326
|
+
north.z,
|
|
327
|
+
east.z,
|
|
328
|
+
-up.z,
|
|
329
|
+
0,
|
|
330
|
+
-dot(north, this.originCartesian),
|
|
331
|
+
-dot(east, this.originCartesian),
|
|
332
|
+
dot(up, this.originCartesian),
|
|
333
|
+
1,
|
|
334
|
+
]);
|
|
335
|
+
this.fromNed = new Float64Array([
|
|
336
|
+
north.x,
|
|
337
|
+
north.y,
|
|
338
|
+
north.z,
|
|
339
|
+
0,
|
|
340
|
+
east.x,
|
|
341
|
+
east.y,
|
|
342
|
+
east.z,
|
|
343
|
+
0,
|
|
344
|
+
-up.x,
|
|
345
|
+
-up.y,
|
|
346
|
+
-up.z,
|
|
347
|
+
0,
|
|
348
|
+
this.originCartesian.x,
|
|
349
|
+
this.originCartesian.y,
|
|
350
|
+
this.originCartesian.z,
|
|
351
|
+
1,
|
|
352
|
+
]);
|
|
353
|
+
}
|
|
354
|
+
|
|
355
|
+
/** Origin geodetic coordinate with latitude and longitude in radians. */
|
|
356
|
+
public get originRadians(): IGeodeticCoordinates {
|
|
357
|
+
return { ...this.origin };
|
|
358
|
+
}
|
|
359
|
+
|
|
360
|
+
/** Origin geodetic coordinate with latitude and longitude in degrees. */
|
|
361
|
+
public get originDegrees(): IGeodeticCoordinates {
|
|
362
|
+
return {
|
|
363
|
+
latitude: toDegrees(this.origin.latitude),
|
|
364
|
+
longitude: toDegrees(this.origin.longitude),
|
|
365
|
+
height: this.origin.height,
|
|
366
|
+
};
|
|
367
|
+
}
|
|
368
|
+
|
|
369
|
+
/** Origin expressed as ECEF metres. */
|
|
370
|
+
public get originEcef(): ICartesian3 {
|
|
371
|
+
return { ...this.originCartesian };
|
|
372
|
+
}
|
|
373
|
+
|
|
374
|
+
/** Canonical east, north and up tangent basis expressed as ECEF unit vectors. */
|
|
375
|
+
public get basis(): ILocalTangentBasis {
|
|
376
|
+
return {
|
|
377
|
+
east: { ...this.tangentBasis.east },
|
|
378
|
+
north: { ...this.tangentBasis.north },
|
|
379
|
+
up: { ...this.tangentBasis.up },
|
|
380
|
+
};
|
|
381
|
+
}
|
|
382
|
+
|
|
383
|
+
/** Column-major matrix transforming ECEF positions to ENU positions. */
|
|
384
|
+
public get ecefToEnuMatrix(): Float64Array {
|
|
385
|
+
return new Float64Array(this.toEnu);
|
|
386
|
+
}
|
|
387
|
+
|
|
388
|
+
/** Column-major matrix transforming ENU positions to ECEF positions. */
|
|
389
|
+
public get enuToEcefMatrix(): Float64Array {
|
|
390
|
+
return new Float64Array(this.fromEnu);
|
|
391
|
+
}
|
|
392
|
+
|
|
393
|
+
/** Column-major matrix transforming ECEF positions to NED positions. */
|
|
394
|
+
public get ecefToNedMatrix(): Float64Array {
|
|
395
|
+
return new Float64Array(this.toNed);
|
|
396
|
+
}
|
|
397
|
+
|
|
398
|
+
/** Column-major matrix transforming NED positions to ECEF positions. */
|
|
399
|
+
public get nedToEcefMatrix(): Float64Array {
|
|
400
|
+
return new Float64Array(this.fromNed);
|
|
401
|
+
}
|
|
402
|
+
|
|
403
|
+
/**
|
|
404
|
+
* Converts an ECEF position to local ENU metres.
|
|
405
|
+
*
|
|
406
|
+
* @param position - ECEF position in metres.
|
|
407
|
+
* @param target - Optional mutable output object.
|
|
408
|
+
* @returns Local coordinate with X east, Y north and Z up.
|
|
409
|
+
*/
|
|
410
|
+
public ecefToEnu(position: ICartesian3, target?: ICartesian3): ICartesian3 {
|
|
411
|
+
validateCartesian(position);
|
|
412
|
+
const output = cartesianTarget(target);
|
|
413
|
+
const delta = {
|
|
414
|
+
x: position.x - this.originCartesian.x,
|
|
415
|
+
y: position.y - this.originCartesian.y,
|
|
416
|
+
z: position.z - this.originCartesian.z,
|
|
417
|
+
};
|
|
418
|
+
output.x = dot(this.tangentBasis.east, delta);
|
|
419
|
+
output.y = dot(this.tangentBasis.north, delta);
|
|
420
|
+
output.z = dot(this.tangentBasis.up, delta);
|
|
421
|
+
return output;
|
|
422
|
+
}
|
|
423
|
+
|
|
424
|
+
/**
|
|
425
|
+
* Converts a local ENU position to ECEF metres.
|
|
426
|
+
*
|
|
427
|
+
* @param position - Local coordinate with X east, Y north and Z up.
|
|
428
|
+
* @param target - Optional mutable output object.
|
|
429
|
+
* @returns ECEF coordinate in metres.
|
|
430
|
+
*/
|
|
431
|
+
public enuToEcef(position: ICartesian3, target?: ICartesian3): ICartesian3 {
|
|
432
|
+
validateCartesian(position);
|
|
433
|
+
const output = cartesianTarget(target);
|
|
434
|
+
output.x =
|
|
435
|
+
this.originCartesian.x +
|
|
436
|
+
this.tangentBasis.east.x * position.x +
|
|
437
|
+
this.tangentBasis.north.x * position.y +
|
|
438
|
+
this.tangentBasis.up.x * position.z;
|
|
439
|
+
output.y =
|
|
440
|
+
this.originCartesian.y +
|
|
441
|
+
this.tangentBasis.east.y * position.x +
|
|
442
|
+
this.tangentBasis.north.y * position.y +
|
|
443
|
+
this.tangentBasis.up.y * position.z;
|
|
444
|
+
output.z =
|
|
445
|
+
this.originCartesian.z +
|
|
446
|
+
this.tangentBasis.east.z * position.x +
|
|
447
|
+
this.tangentBasis.north.z * position.y +
|
|
448
|
+
this.tangentBasis.up.z * position.z;
|
|
449
|
+
return output;
|
|
450
|
+
}
|
|
451
|
+
|
|
452
|
+
/**
|
|
453
|
+
* Converts an ECEF position to local NED metres.
|
|
454
|
+
*
|
|
455
|
+
* @param position - ECEF position in metres.
|
|
456
|
+
* @param target - Optional mutable output object.
|
|
457
|
+
* @returns Local coordinate with X north, Y east and Z down.
|
|
458
|
+
*/
|
|
459
|
+
public ecefToNed(position: ICartesian3, target?: ICartesian3): ICartesian3 {
|
|
460
|
+
validateCartesian(position);
|
|
461
|
+
const output = cartesianTarget(target);
|
|
462
|
+
const delta = {
|
|
463
|
+
x: position.x - this.originCartesian.x,
|
|
464
|
+
y: position.y - this.originCartesian.y,
|
|
465
|
+
z: position.z - this.originCartesian.z,
|
|
466
|
+
};
|
|
467
|
+
output.x = dot(this.tangentBasis.north, delta);
|
|
468
|
+
output.y = dot(this.tangentBasis.east, delta);
|
|
469
|
+
output.z = -dot(this.tangentBasis.up, delta);
|
|
470
|
+
return output;
|
|
471
|
+
}
|
|
472
|
+
|
|
473
|
+
/**
|
|
474
|
+
* Converts a local NED position to ECEF metres.
|
|
475
|
+
*
|
|
476
|
+
* @param position - Local coordinate with X north, Y east and Z down.
|
|
477
|
+
* @param target - Optional mutable output object.
|
|
478
|
+
* @returns ECEF coordinate in metres.
|
|
479
|
+
*/
|
|
480
|
+
public nedToEcef(position: ICartesian3, target?: ICartesian3): ICartesian3 {
|
|
481
|
+
validateCartesian(position);
|
|
482
|
+
const output = cartesianTarget(target);
|
|
483
|
+
output.x =
|
|
484
|
+
this.originCartesian.x +
|
|
485
|
+
this.tangentBasis.north.x * position.x +
|
|
486
|
+
this.tangentBasis.east.x * position.y -
|
|
487
|
+
this.tangentBasis.up.x * position.z;
|
|
488
|
+
output.y =
|
|
489
|
+
this.originCartesian.y +
|
|
490
|
+
this.tangentBasis.north.y * position.x +
|
|
491
|
+
this.tangentBasis.east.y * position.y -
|
|
492
|
+
this.tangentBasis.up.y * position.z;
|
|
493
|
+
output.z =
|
|
494
|
+
this.originCartesian.z +
|
|
495
|
+
this.tangentBasis.north.z * position.x +
|
|
496
|
+
this.tangentBasis.east.z * position.y -
|
|
497
|
+
this.tangentBasis.up.z * position.z;
|
|
498
|
+
return output;
|
|
499
|
+
}
|
|
500
|
+
|
|
501
|
+
/**
|
|
502
|
+
* Converts geodetic radians directly to local ENU metres.
|
|
503
|
+
*
|
|
504
|
+
* @param latitudeRadians - Geodetic latitude in radians.
|
|
505
|
+
* @param longitudeRadians - Longitude in radians.
|
|
506
|
+
* @param height - Ellipsoidal height in metres.
|
|
507
|
+
* @param target - Optional mutable output object.
|
|
508
|
+
* @returns Local coordinate with X east, Y north and Z up.
|
|
509
|
+
*/
|
|
510
|
+
public geodeticRadiansToEnu(latitudeRadians: number, longitudeRadians: number, height = 0, target?: ICartesian3): ICartesian3 {
|
|
511
|
+
return this.ecefToEnu(this.system.geodeticRadiansToEcef(latitudeRadians, longitudeRadians, height), target);
|
|
512
|
+
}
|
|
513
|
+
|
|
514
|
+
/**
|
|
515
|
+
* Converts geodetic degrees directly to local ENU metres.
|
|
516
|
+
*
|
|
517
|
+
* @param latitudeDegrees - Geodetic latitude in degrees.
|
|
518
|
+
* @param longitudeDegrees - Longitude in degrees.
|
|
519
|
+
* @param height - Ellipsoidal height in metres.
|
|
520
|
+
* @param target - Optional mutable output object.
|
|
521
|
+
* @returns Local coordinate with X east, Y north and Z up.
|
|
522
|
+
*/
|
|
523
|
+
public geodeticDegreesToEnu(latitudeDegrees: number, longitudeDegrees: number, height = 0, target?: ICartesian3): ICartesian3 {
|
|
524
|
+
return this.ecefToEnu(this.system.geodeticDegreesToEcef(latitudeDegrees, longitudeDegrees, height), target);
|
|
525
|
+
}
|
|
526
|
+
|
|
527
|
+
/**
|
|
528
|
+
* Converts local ENU metres directly to geodetic radians.
|
|
529
|
+
*
|
|
530
|
+
* @param position - Local coordinate with X east, Y north and Z up.
|
|
531
|
+
* @param target - Optional mutable output object.
|
|
532
|
+
* @returns Geodetic latitude and longitude in radians, with height in metres.
|
|
533
|
+
*/
|
|
534
|
+
public enuToGeodeticRadians(position: ICartesian3, target?: IGeodeticCoordinates): IGeodeticCoordinates {
|
|
535
|
+
return this.system.ecefToGeodeticRadians(this.enuToEcef(position), target);
|
|
536
|
+
}
|
|
537
|
+
|
|
538
|
+
/**
|
|
539
|
+
* Converts local ENU metres directly to geodetic degrees.
|
|
540
|
+
*
|
|
541
|
+
* @param position - Local coordinate with X east, Y north and Z up.
|
|
542
|
+
* @param target - Optional mutable output object.
|
|
543
|
+
* @returns Geodetic latitude and longitude in degrees, with height in metres.
|
|
544
|
+
*/
|
|
545
|
+
public enuToGeodeticDegrees(position: ICartesian3, target?: IGeodeticCoordinates): IGeodeticCoordinates {
|
|
546
|
+
return this.system.ecefToGeodeticDegrees(this.enuToEcef(position), target);
|
|
547
|
+
}
|
|
548
|
+
}
|
package/src/index.ts
ADDED
package/src/types.ts
ADDED
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Mutable three-dimensional Cartesian coordinate.
|
|
3
|
+
*
|
|
4
|
+
* Axis meaning depends on the coordinate system that produces the value. ECEF
|
|
5
|
+
* uses metres along the conventional Earth-fixed X, Y and Z axes. ENU uses
|
|
6
|
+
* metres along east, north and up. NED uses metres along north, east and down.
|
|
7
|
+
*/
|
|
8
|
+
export interface ICartesian3 {
|
|
9
|
+
/** First Cartesian component in metres. */
|
|
10
|
+
x: number;
|
|
11
|
+
/** Second Cartesian component in metres. */
|
|
12
|
+
y: number;
|
|
13
|
+
/** Third Cartesian component in metres. */
|
|
14
|
+
z: number;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
/**
|
|
18
|
+
* Mutable geodetic coordinate on a reference ellipsoid.
|
|
19
|
+
*
|
|
20
|
+
* The angular unit is determined by the method returning or accepting the
|
|
21
|
+
* object. Methods are explicitly suffixed with `Degrees` or `Radians`.
|
|
22
|
+
*/
|
|
23
|
+
export interface IGeodeticCoordinates {
|
|
24
|
+
/** Geodetic latitude, not geocentric latitude. */
|
|
25
|
+
latitude: number;
|
|
26
|
+
/** Longitude east of the reference meridian. */
|
|
27
|
+
longitude: number;
|
|
28
|
+
/** Ellipsoidal height in metres. */
|
|
29
|
+
height: number;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
/**
|
|
33
|
+
* Canonical local tangent basis expressed as ECEF unit vectors.
|
|
34
|
+
*
|
|
35
|
+
* The basis describes physical directions independently from a coordinate
|
|
36
|
+
* convention. ENU orders it as east, north, up. NED orders it as north, east,
|
|
37
|
+
* down, where down is the opposite of up.
|
|
38
|
+
*/
|
|
39
|
+
export interface ILocalTangentBasis {
|
|
40
|
+
/** Unit vector pointing east. */
|
|
41
|
+
east: ICartesian3;
|
|
42
|
+
/** Unit vector pointing north. */
|
|
43
|
+
north: ICartesian3;
|
|
44
|
+
/** Unit vector normal to the ellipsoid and pointing up. */
|
|
45
|
+
up: ICartesian3;
|
|
46
|
+
}
|