@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.
@@ -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._lat = lat;
19
- this._lng = lng;
20
- this._alt = alt;
21
- this._level = level;
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
- this._level = level;
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
- static equalsTo(pos1, pos2) {
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
- return pos2.lat === pos1.lat
73
- && pos2.lng === pos1.lng
74
- && Level.equalsTo(pos2.level, pos1.level);
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
- toString() {
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 = 0) {
101
-
102
- const d = distance;
103
- const R = Constants.R_MAJOR;
104
-
105
- const φ1 = Utils.deg2rad(this.lat);
106
- const λ1 = Utils.deg2rad(this.lng);
107
-
108
- const φ2 = Math.asin(Math.sin(φ1) * Math.cos(d / R) + Math.cos(φ1) * Math.sin(d / R) * Math.cos(bearing));
109
- const λ2 = λ1 + Math.atan2(Math.sin(bearing) * Math.sin(d / R) * Math.cos(φ1), Math.cos(d / R) - Math.sin(φ1) * Math.sin(φ2));
110
-
111
- const altitude = (this.alt || 0) + elevation;
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 {Number} location2 latitude / longitude point2
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 = (dlatsin * dlatsin + lat1cos * lat2cos * dlngsin * dlngsin);
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 = (2 * Math.atan2(tangy, tangx));
188
+ const tangy = Math.sqrt(angle);
189
+ const tangx = Math.sqrt(1 - angle);
190
+ const cosn = 2 * Math.atan2(tangy, tangx);
150
191
 
151
- // distance in km
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
- getEnuToEcefRotation() {
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
- getEcefToEnuRotation() {
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
- getUpVector() {
183
- return Vector3.normalize(this.ecef);
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 N = Constants.R_MAJOR / Math.sqrt(1 - Constants.ECCENTRICITY_2 * Math.pow(Math.sin(lat), 2));
195
-
196
- const x = (N + alt) * Math.cos(lat) * Math.cos(lng);
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 b = Math.sqrt(Constants.R_MAJOR_2 * (1 - Constants.ECCENTRICITY_2));
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((z + Math.pow(ep, 2) * b * Math.pow(Math.sin(th), 3)), (p - Constants.ECCENTRICITY_2 * Constants.R_MAJOR * Math.pow(Math.cos(th), 3)));
220
- const N = Constants.R_MAJOR / (Math.sqrt(1 - Constants.ECCENTRICITY_2 * Math.pow(Math.sin(lat), 2)));
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(Utils.rad2deg(lat), Utils.rad2deg(lng), Utils.rad2deg(alt));
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
- getProjection(edge) {
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, this.getEarthRadiusAtPosition());
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 (isNumber(p1.alt) && isNumber(p2.alt)) {
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, alt);
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
- getEarthRadiusAtPosition() {
266
- const latR = Utils.deg2rad(this.lat);
267
- const cos2 = Math.cos(latR) * Math.cos(latR);
268
- const sin2 = Math.sin(latR) * Math.sin(latR);
269
- return Math.sqrt((Constants.R_MAJOR_4 * cos2 + Constants.R_MINOR_4 * sin2) / (Constants.R_MAJOR_2 * cos2 + Constants.R_MINOR_2 * sin2));
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
- toMessage() {
323
+ toJson() {
273
324
  const output = {
274
325
  lat: this.lat,
275
326
  lng: this.lng
276
327
  };
277
- if (isNumber(this.alt)) {
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 fromMessage(message) {
287
- return new WGS84(message.lat, message.lng, message.alt, Level.fromString(message.level));
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;