@wemap/geo 1.0.5 → 2.7.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/index.js +2 -0
- package/package.json +4 -3
- package/src/Constants.js +11 -1
- package/src/Utils.js +46 -0
- package/src/Utils.spec.js +39 -0
- package/src/coordinates/Level.js +1 -0
- package/src/coordinates/Level.spec.js +39 -30
- package/src/coordinates/WGS84.js +150 -134
- package/src/coordinates/WGS84.spec.js +268 -0
- package/src/coordinates/WGS84UserPosition.js +136 -23
- package/src/coordinates/WGS84UserPosition.spec.js +247 -0
- package/src/rotations/Attitude.js +56 -30
- package/src/rotations/Attitude.spec.js +82 -0
package/src/coordinates/WGS84.js
CHANGED
|
@@ -1,8 +1,9 @@
|
|
|
1
1
|
import {
|
|
2
|
-
Utils, Vector3, Quaternion
|
|
2
|
+
Utils, Vector3, Quaternion, rad2deg
|
|
3
3
|
} from '@wemap/maths';
|
|
4
4
|
import Constants from '../Constants';
|
|
5
5
|
import isNumber from 'lodash.isnumber';
|
|
6
|
+
import isFinite from 'lodash.isfinite';
|
|
6
7
|
import Level from './Level';
|
|
7
8
|
|
|
8
9
|
/**
|
|
@@ -11,14 +12,18 @@ import Level from './Level';
|
|
|
11
12
|
*
|
|
12
13
|
* Basic geo methods are directly accessibles from here:
|
|
13
14
|
* distanceTo, bearingTo, toEcef...
|
|
15
|
+
*
|
|
16
|
+
* /!\ This class has been adapted to use earth as a sphere and not as an ellipsoid
|
|
17
|
+
* /!\ So, this class does not stricly represent WGS84 coordinates anymore
|
|
18
|
+
* /!\ This modifications have been made for computational improvements.
|
|
14
19
|
*/
|
|
15
20
|
class WGS84 {
|
|
16
21
|
|
|
17
22
|
constructor(lat, lng, alt, level) {
|
|
18
|
-
this.
|
|
19
|
-
this.
|
|
20
|
-
this.
|
|
21
|
-
this.
|
|
23
|
+
this.lat = lat;
|
|
24
|
+
this.lng = lng;
|
|
25
|
+
this.alt = alt;
|
|
26
|
+
this.level = level;
|
|
22
27
|
this._ecef = null;
|
|
23
28
|
}
|
|
24
29
|
|
|
@@ -39,22 +44,44 @@ class WGS84 {
|
|
|
39
44
|
}
|
|
40
45
|
|
|
41
46
|
set lat(lat) {
|
|
47
|
+
if (isNumber(lat) && Math.abs(lat) <= 90) {
|
|
48
|
+
this._lat = lat;
|
|
49
|
+
} else {
|
|
50
|
+
throw new Error('lat argument is not in [-90; 90]');
|
|
51
|
+
}
|
|
42
52
|
this._ecef = null;
|
|
43
|
-
this._lat = lat;
|
|
44
53
|
}
|
|
45
54
|
|
|
46
55
|
set lng(lng) {
|
|
56
|
+
if (isNumber(lng) && Math.abs(lng) <= 180) {
|
|
57
|
+
this._lng = lng;
|
|
58
|
+
} else {
|
|
59
|
+
throw new Error('lng argument is not in [-180; 180]');
|
|
60
|
+
}
|
|
47
61
|
this._ecef = null;
|
|
48
|
-
this._lng = lng;
|
|
49
62
|
}
|
|
50
63
|
|
|
51
64
|
set alt(alt) {
|
|
65
|
+
if (isNumber(alt) && isFinite(alt)) {
|
|
66
|
+
this._alt = alt;
|
|
67
|
+
} else {
|
|
68
|
+
if (typeof alt !== 'undefined' && alt !== null) {
|
|
69
|
+
throw new Error('alt argument is not a finite number');
|
|
70
|
+
}
|
|
71
|
+
this._alt = null;
|
|
72
|
+
}
|
|
52
73
|
this._ecef = null;
|
|
53
|
-
this._alt = alt;
|
|
54
74
|
}
|
|
55
75
|
|
|
56
76
|
set level(level) {
|
|
57
|
-
|
|
77
|
+
if (level instanceof Level) {
|
|
78
|
+
this._level = level;
|
|
79
|
+
} else {
|
|
80
|
+
if (typeof level !== 'undefined' && level !== null) {
|
|
81
|
+
throw new Error('level argument is not a Level object');
|
|
82
|
+
}
|
|
83
|
+
this._level = null;
|
|
84
|
+
}
|
|
58
85
|
}
|
|
59
86
|
|
|
60
87
|
clone() {
|
|
@@ -65,92 +92,104 @@ class WGS84 {
|
|
|
65
92
|
return output;
|
|
66
93
|
}
|
|
67
94
|
|
|
68
|
-
|
|
95
|
+
/**
|
|
96
|
+
* Compares two WGS84
|
|
97
|
+
* @param {WGS84} pos1 position 1
|
|
98
|
+
* @param {WGS84} pos2 position 2
|
|
99
|
+
* @param {Number} eps latitude and longitude epsilon in degrees (default: 1e-8 [~1mm at lat=0])
|
|
100
|
+
* @param {Number} epsAlt altitude epsilon in meters (default: 1e-3 [= 1mm])
|
|
101
|
+
*/
|
|
102
|
+
static equalsTo(pos1, pos2, eps = Constants.EPS_DEG_MM, epsAlt = Constants.EPS_MM) {
|
|
103
|
+
|
|
104
|
+
// Handle null comparison
|
|
105
|
+
if (pos1 === null && pos1 === pos2) {
|
|
106
|
+
return true;
|
|
107
|
+
}
|
|
108
|
+
|
|
69
109
|
if (!(pos1 instanceof WGS84) || !(pos2 instanceof WGS84)) {
|
|
70
110
|
return false;
|
|
71
111
|
}
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
&&
|
|
112
|
+
|
|
113
|
+
return Math.abs(pos2.lat - pos1.lat) < eps
|
|
114
|
+
&& Math.abs(pos2.lng - pos1.lng) < eps
|
|
115
|
+
&& (pos1.alt === pos2.alt
|
|
116
|
+
|| pos1.alt !== null && pos2.alt !== null
|
|
117
|
+
&& Math.abs(pos2.alt - pos1.alt) < epsAlt)
|
|
118
|
+
&& Level.equalsTo(pos1.level, pos2.level);
|
|
75
119
|
}
|
|
76
120
|
|
|
77
121
|
equalsTo(other) {
|
|
78
122
|
return WGS84.equalsTo(this, other);
|
|
79
123
|
}
|
|
80
124
|
|
|
81
|
-
|
|
82
|
-
let str = '[' + this._lat.toFixed(7) + ', ' + this._lng.toFixed(7);
|
|
83
|
-
if (this._alt) {
|
|
84
|
-
str += ', ' + this._alt.toFixed(2);
|
|
85
|
-
}
|
|
86
|
-
if (this._level) {
|
|
87
|
-
str += ', [' + this._level.toString() + ']';
|
|
88
|
-
}
|
|
89
|
-
str += ']';
|
|
90
|
-
return str;
|
|
91
|
-
}
|
|
92
|
-
|
|
93
|
-
destinationPoint(distance, bearing, elevation = 0) {
|
|
125
|
+
destinationPoint(distance, bearing, elevation) {
|
|
94
126
|
const newPoint = this.clone();
|
|
95
127
|
newPoint.move(distance, bearing, elevation);
|
|
96
128
|
return newPoint;
|
|
97
129
|
}
|
|
98
130
|
|
|
99
131
|
// Source: http://www.movable-type.co.uk/scripts/latlong.html#destPoint
|
|
100
|
-
move(distance, bearing, elevation
|
|
101
|
-
|
|
102
|
-
const
|
|
103
|
-
const
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
const
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
const
|
|
110
|
-
|
|
111
|
-
|
|
132
|
+
move(distance, bearing, elevation) {
|
|
133
|
+
|
|
134
|
+
const dR = distance / Constants.R_MAJOR;
|
|
135
|
+
const cosDr = Math.cos(dR);
|
|
136
|
+
const sinDr = Math.sin(dR);
|
|
137
|
+
|
|
138
|
+
const phi1 = Utils.deg2rad(this.lat);
|
|
139
|
+
const lambda1 = Utils.deg2rad(this.lng);
|
|
140
|
+
|
|
141
|
+
const phi2 = Math.asin(
|
|
142
|
+
Math.sin(phi1) * cosDr
|
|
143
|
+
+ Math.cos(phi1) * sinDr * Math.cos(bearing)
|
|
144
|
+
);
|
|
145
|
+
const lambda2 = lambda1 + Math.atan2(
|
|
146
|
+
Math.sin(bearing) * sinDr * Math.cos(phi1),
|
|
147
|
+
cosDr - Math.sin(phi1) * Math.sin(phi2)
|
|
148
|
+
);
|
|
149
|
+
|
|
150
|
+
this.lat = Utils.rad2deg(phi2);
|
|
151
|
+
this.lng = Utils.rad2deg(lambda2);
|
|
152
|
+
|
|
153
|
+
if (isNumber(elevation) && isFinite(elevation)) {
|
|
154
|
+
if (this.alt === null) {
|
|
155
|
+
throw new Error('Point altitude is not defined');
|
|
156
|
+
}
|
|
157
|
+
this.alt += elevation;
|
|
158
|
+
}
|
|
112
159
|
|
|
113
|
-
this.lat = Utils.rad2deg(φ2);
|
|
114
|
-
this.lng = Utils.rad2deg(λ2);
|
|
115
|
-
this.alt = altitude;
|
|
116
160
|
|
|
117
161
|
return this;
|
|
118
162
|
}
|
|
119
163
|
|
|
120
164
|
/**
|
|
121
165
|
* Returns a distance between two points in meters
|
|
122
|
-
* @param {
|
|
166
|
+
* @param {WGS84} location2 latitude / longitude point
|
|
123
167
|
* @return {Number} distance in meters
|
|
124
168
|
*/
|
|
125
169
|
distanceTo(location2) {
|
|
126
|
-
var cosn, dist, dlat, dlng, angle, tangy, tangx,
|
|
127
|
-
dlatsin, dlngsin, lat1cos, lat2cos, lat2rad, lat1rad;
|
|
128
|
-
|
|
129
170
|
const lat1 = this.lat;
|
|
130
171
|
const lng1 = this.lng;
|
|
131
172
|
|
|
132
173
|
const lat2 = location2.lat;
|
|
133
174
|
const lng2 = location2.lng;
|
|
134
175
|
|
|
135
|
-
dlat = Utils.deg2rad(lat2 - lat1);
|
|
136
|
-
dlng = Utils.deg2rad(lng2 - lng1);
|
|
176
|
+
const dlat = Utils.deg2rad(lat2 - lat1);
|
|
177
|
+
const dlng = Utils.deg2rad(lng2 - lng1);
|
|
137
178
|
|
|
138
|
-
dlngsin = Math.sin(dlng / 2);
|
|
139
|
-
dlatsin = Math.sin(dlat / 2);
|
|
140
|
-
lat1rad = Utils.deg2rad(lat1);
|
|
141
|
-
lat1cos = Math.cos(lat1rad);
|
|
142
|
-
lat2rad = Utils.deg2rad(lat2);
|
|
143
|
-
lat2cos = Math.cos(lat2rad);
|
|
144
|
-
angle =
|
|
179
|
+
const dlngsin = Math.sin(dlng / 2);
|
|
180
|
+
const dlatsin = Math.sin(dlat / 2);
|
|
181
|
+
const lat1rad = Utils.deg2rad(lat1);
|
|
182
|
+
const lat1cos = Math.cos(lat1rad);
|
|
183
|
+
const lat2rad = Utils.deg2rad(lat2);
|
|
184
|
+
const lat2cos = Math.cos(lat2rad);
|
|
185
|
+
const angle = dlatsin * dlatsin + lat1cos * lat2cos * dlngsin * dlngsin;
|
|
145
186
|
|
|
146
187
|
// arctangent
|
|
147
|
-
tangy = Math.sqrt(angle);
|
|
148
|
-
tangx = Math.sqrt(1 - angle);
|
|
149
|
-
cosn =
|
|
188
|
+
const tangy = Math.sqrt(angle);
|
|
189
|
+
const tangx = Math.sqrt(1 - angle);
|
|
190
|
+
const cosn = 2 * Math.atan2(tangy, tangx);
|
|
150
191
|
|
|
151
|
-
|
|
152
|
-
dist = (Constants.EARTH_RADIUS_KM * 1000 * cosn);
|
|
153
|
-
return dist;
|
|
192
|
+
return Constants.R_MAJOR * cosn;
|
|
154
193
|
}
|
|
155
194
|
|
|
156
195
|
static distanceBetween(point1, point2) {
|
|
@@ -166,24 +205,31 @@ class WGS84 {
|
|
|
166
205
|
Math.cos(lat1) * Math.sin(lat2) - Math.sin(lat1) * Math.cos(lat2) * Math.cos(diffLng));
|
|
167
206
|
}
|
|
168
207
|
|
|
208
|
+
static bearingTo(point1, point2) {
|
|
209
|
+
return point1.bearingTo(point2);
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
|
|
213
|
+
/**
|
|
214
|
+
* ECEF Transformations
|
|
215
|
+
* Here we used a light version of ECEF considering earth
|
|
216
|
+
* as a sphere instead of an ellipse
|
|
217
|
+
*/
|
|
169
218
|
|
|
170
|
-
|
|
219
|
+
get enuToEcefRotation() {
|
|
171
220
|
const rot1 = Quaternion.fromAxisAngle([0, 0, 1], Math.PI / 2 + Utils.deg2rad(this.lng));
|
|
172
221
|
const rot2 = Quaternion.fromAxisAngle([1, 0, 0], Math.PI / 2 - Utils.deg2rad(this.lat));
|
|
173
222
|
return Quaternion.multiply(rot1, rot2);
|
|
174
223
|
}
|
|
175
224
|
|
|
176
|
-
|
|
225
|
+
get ecefToEnuRotation() {
|
|
177
226
|
const rot1 = Quaternion.fromAxisAngle([1, 0, 0], Utils.deg2rad(this.lat) - Math.PI / 2);
|
|
178
227
|
const rot2 = Quaternion.fromAxisAngle([0, 0, 1], -Utils.deg2rad(this.lng) - Math.PI / 2);
|
|
179
228
|
return Quaternion.multiply(rot1, rot2);
|
|
180
229
|
}
|
|
181
230
|
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
}
|
|
185
|
-
|
|
186
|
-
|
|
231
|
+
// https://gist.github.com/klucar/1536194
|
|
232
|
+
// Adapted for spherical formula
|
|
187
233
|
get ecef() {
|
|
188
234
|
|
|
189
235
|
if (!this._ecef) {
|
|
@@ -191,11 +237,9 @@ class WGS84 {
|
|
|
191
237
|
const lng = Utils.deg2rad(this.lng);
|
|
192
238
|
const alt = this.alt || 0;
|
|
193
239
|
|
|
194
|
-
const
|
|
195
|
-
|
|
196
|
-
const
|
|
197
|
-
const y = (N + alt) * Math.cos(lat) * Math.sin(lng);
|
|
198
|
-
const z = ((1 - Constants.ECCENTRICITY_2) * N + alt) * Math.sin(lat);
|
|
240
|
+
const x = (Constants.R_MAJOR + alt) * Math.cos(lat) * Math.cos(lng);
|
|
241
|
+
const y = (Constants.R_MAJOR + alt) * Math.cos(lat) * Math.sin(lng);
|
|
242
|
+
const z = (Constants.R_MAJOR + alt) * Math.sin(lat);
|
|
199
243
|
|
|
200
244
|
this._ecef = [x, y, z];
|
|
201
245
|
}
|
|
@@ -209,50 +253,49 @@ class WGS84 {
|
|
|
209
253
|
const y = ecef[1];
|
|
210
254
|
const z = ecef[2];
|
|
211
255
|
|
|
212
|
-
const
|
|
213
|
-
const bsq = Math.pow(b, 2);
|
|
214
|
-
const ep = Math.sqrt((Constants.R_MAJOR_2 - bsq) / bsq);
|
|
215
|
-
const p = Math.sqrt(Math.pow(x, 2) + Math.pow(y, 2));
|
|
216
|
-
const th = Math.atan2(Constants.R_MAJOR * z, b * p);
|
|
256
|
+
const p = Math.sqrt(x ** 2 + y ** 2);
|
|
217
257
|
|
|
218
258
|
let lng = Math.atan2(y, x);
|
|
219
|
-
const lat = Math.atan2(
|
|
220
|
-
const
|
|
221
|
-
const alt = p / Math.cos(lat) - N;
|
|
259
|
+
const lat = Math.atan2(z, p);
|
|
260
|
+
const alt = p / Math.cos(lat) - Constants.R_MAJOR;
|
|
222
261
|
|
|
223
262
|
lng = lng % (2 * Math.PI);
|
|
224
263
|
|
|
225
|
-
const newPoint = new WGS84(
|
|
264
|
+
const newPoint = new WGS84(rad2deg(lat), rad2deg(lng), alt);
|
|
226
265
|
newPoint._ecef = ecef;
|
|
227
266
|
return newPoint;
|
|
228
267
|
}
|
|
229
268
|
|
|
269
|
+
|
|
230
270
|
// https://stackoverflow.com/questions/1299567/how-to-calculate-distance-from-a-point-to-a-line-segment-on-a-sphere
|
|
231
271
|
// Adapted to ECEF
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
const p1 = edge.node1.coords;
|
|
235
|
-
const p2 = edge.node2.coords;
|
|
272
|
+
getSegmentProjection(p1, p2) {
|
|
236
273
|
|
|
237
274
|
const a = Vector3.normalize(p1.ecef);
|
|
238
275
|
const b = Vector3.normalize(p2.ecef);
|
|
239
276
|
const c = Vector3.normalize(this.ecef);
|
|
240
277
|
|
|
241
278
|
const G = Vector3.cross(a, b);
|
|
279
|
+
if (Vector3.norm(G) === 0) {
|
|
280
|
+
return null;
|
|
281
|
+
}
|
|
282
|
+
|
|
242
283
|
const F = Vector3.cross(c, G);
|
|
243
284
|
const t = Vector3.normalize(Vector3.cross(G, F));
|
|
244
285
|
|
|
245
|
-
const posECEF = Vector3.multiplyScalar(t,
|
|
286
|
+
const posECEF = Vector3.multiplyScalar(t, Constants.R_MAJOR);
|
|
246
287
|
const poseWGS84 = WGS84.fromECEF(posECEF);
|
|
247
288
|
|
|
248
289
|
// poseWGS84.alt is not 0 here due to the ECEF transformation residual.
|
|
249
290
|
// So if p1.alt and p2.alt are defined we take the middle elevation between p1 and p2.
|
|
250
291
|
// Otherwise we remove alt from projection because the residual has no sense.
|
|
251
292
|
let alt;
|
|
252
|
-
if (
|
|
293
|
+
if (p1.alt !== null && p2.alt !== null) {
|
|
294
|
+
// This formula is maybe not the best one.
|
|
253
295
|
alt = (p1.alt + p2.alt) / 2;
|
|
254
296
|
}
|
|
255
|
-
const projection = new WGS84(poseWGS84.lat, poseWGS84.lng,
|
|
297
|
+
const projection = new WGS84(poseWGS84.lat, poseWGS84.lng,
|
|
298
|
+
alt, Level.intersect(p1.level, p2.level));
|
|
256
299
|
|
|
257
300
|
if (Math.abs((p1.distanceTo(p2) - p1.distanceTo(projection) - p2.distanceTo(projection))) > 1e-6) {
|
|
258
301
|
return null;
|
|
@@ -261,67 +304,40 @@ class WGS84 {
|
|
|
261
304
|
return projection;
|
|
262
305
|
}
|
|
263
306
|
|
|
307
|
+
/**
|
|
308
|
+
* Input / Output
|
|
309
|
+
*/
|
|
264
310
|
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
311
|
+
toString() {
|
|
312
|
+
let str = '[' + this._lat.toFixed(7) + ', ' + this._lng.toFixed(7);
|
|
313
|
+
if (this._alt !== null) {
|
|
314
|
+
str += ', ' + this._alt.toFixed(2);
|
|
315
|
+
}
|
|
316
|
+
if (this._level !== null) {
|
|
317
|
+
str += ', [' + this._level.toString() + ']';
|
|
318
|
+
}
|
|
319
|
+
str += ']';
|
|
320
|
+
return str;
|
|
270
321
|
}
|
|
271
322
|
|
|
272
|
-
|
|
323
|
+
toJson() {
|
|
273
324
|
const output = {
|
|
274
325
|
lat: this.lat,
|
|
275
326
|
lng: this.lng
|
|
276
327
|
};
|
|
277
|
-
if (
|
|
328
|
+
if (this.alt !== null) {
|
|
278
329
|
output.alt = this.alt;
|
|
279
330
|
}
|
|
280
|
-
if (this.level) {
|
|
281
|
-
output.level = this.level;
|
|
331
|
+
if (this.level !== null) {
|
|
332
|
+
output.level = this.level.toString();
|
|
282
333
|
}
|
|
283
334
|
return output;
|
|
284
335
|
}
|
|
285
336
|
|
|
286
|
-
static
|
|
287
|
-
return new WGS84(
|
|
337
|
+
static fromJson(json) {
|
|
338
|
+
return new WGS84(json.lat, json.lng, json.alt, Level.fromString(json.level));
|
|
288
339
|
}
|
|
289
340
|
|
|
290
|
-
static sampleRoute(route, stepSize = 0.7, maxLength = Number.MAX_VALUE) {
|
|
291
|
-
|
|
292
|
-
let p1, p2 = null;
|
|
293
|
-
let bearing, distance;
|
|
294
|
-
|
|
295
|
-
const sampledRoute = [];
|
|
296
|
-
let reportedDistance = 0;
|
|
297
|
-
let totalDistance = 0;
|
|
298
|
-
|
|
299
|
-
for (let i = 0; i < route.length - 1; i++) {
|
|
300
|
-
|
|
301
|
-
p1 = route[i];
|
|
302
|
-
p2 = route[i + 1];
|
|
303
|
-
|
|
304
|
-
bearing = p1.bearingTo(p2);
|
|
305
|
-
distance = p1.distanceTo(p2);
|
|
306
|
-
|
|
307
|
-
if (distance > 0) {
|
|
308
|
-
let ratio = reportedDistance / distance;
|
|
309
|
-
while (ratio < 1 && totalDistance <= maxLength) {
|
|
310
|
-
const newPoint = p1.destinationPoint(ratio * distance, bearing);
|
|
311
|
-
newPoint.bearing = bearing;
|
|
312
|
-
sampledRoute.push(newPoint);
|
|
313
|
-
ratio += stepSize / distance;
|
|
314
|
-
totalDistance += stepSize;
|
|
315
|
-
}
|
|
316
|
-
reportedDistance = (ratio - 1) * distance;
|
|
317
|
-
}
|
|
318
|
-
}
|
|
319
|
-
if (p2) {
|
|
320
|
-
sampledRoute.push(p2);
|
|
321
|
-
}
|
|
322
|
-
|
|
323
|
-
return sampledRoute;
|
|
324
|
-
}
|
|
325
341
|
}
|
|
326
342
|
|
|
327
343
|
export default WGS84;
|