@wemap/geo 0.1.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 +17 -0
- package/package.json +26 -0
- package/src/Constants.js +18 -0
- package/src/coordinates/WGS84.js +285 -0
- package/src/coordinates/WGS84UserPosition.js +52 -0
- package/src/graph/Edge.js +63 -0
- package/src/graph/Itinerary.js +36 -0
- package/src/graph/MapMatching.js +118 -0
- package/src/graph/MapMatching.spec.js +47 -0
- package/src/graph/Network.js +34 -0
package/index.js
ADDED
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import Constants from './src/Constants';
|
|
2
|
+
import Edge from './src/graph/Edge';
|
|
3
|
+
import Itinerary from './src/graph/Itinerary';
|
|
4
|
+
import MapMatching from './src/graph/MapMatching';
|
|
5
|
+
import Network from './src/graph/Network';
|
|
6
|
+
import WGS84 from './src/coordinates/WGS84';
|
|
7
|
+
import WGS84UserPosition from './src/coordinates/WGS84UserPosition';
|
|
8
|
+
|
|
9
|
+
export {
|
|
10
|
+
Constants,
|
|
11
|
+
Edge,
|
|
12
|
+
Itinerary,
|
|
13
|
+
MapMatching,
|
|
14
|
+
Network,
|
|
15
|
+
WGS84,
|
|
16
|
+
WGS84UserPosition
|
|
17
|
+
};
|
package/package.json
ADDED
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
{
|
|
2
|
+
"author": "Wemap",
|
|
3
|
+
"contributors": [
|
|
4
|
+
{
|
|
5
|
+
"name": "Thibaud Michel",
|
|
6
|
+
"email": "thibaud@getwemap.com"
|
|
7
|
+
},
|
|
8
|
+
{
|
|
9
|
+
"name": "Guillaume Pannetier",
|
|
10
|
+
"email": "guillaume.pannetier@getwemap.com"
|
|
11
|
+
}
|
|
12
|
+
],
|
|
13
|
+
"description": "A package using different geoloc systems",
|
|
14
|
+
"dependencies": {
|
|
15
|
+
"@wemap/maths": "latest"
|
|
16
|
+
},
|
|
17
|
+
"repository": {
|
|
18
|
+
"type": "git",
|
|
19
|
+
"url": "https://github.com/wemap/wemap-utils-js.git",
|
|
20
|
+
"directory": "packages/geo"
|
|
21
|
+
},
|
|
22
|
+
"main": "index.js",
|
|
23
|
+
"name": "@wemap/geo",
|
|
24
|
+
"version": "0.1.1",
|
|
25
|
+
"gitHead": "00ed0bae7da0086bb47d6e958343fd2020bac2b1"
|
|
26
|
+
}
|
package/src/Constants.js
ADDED
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
const Constants = {
|
|
2
|
+
R_MAJOR: 6378137.0,
|
|
3
|
+
R_MINOR: 6356752.3142,
|
|
4
|
+
EARTH_GRAVITY: 9.80665,
|
|
5
|
+
EARTH_RADIUS_KM: 6371
|
|
6
|
+
};
|
|
7
|
+
|
|
8
|
+
Constants.ELLIPSOID_FLATNESS = (Constants.R_MAJOR - Constants.R_MINOR) / Constants.R_MAJOR;
|
|
9
|
+
|
|
10
|
+
Constants.ECCENTRICITY = Math.sqrt(Constants.ELLIPSOID_FLATNESS * (2 - Constants.ELLIPSOID_FLATNESS));
|
|
11
|
+
|
|
12
|
+
Constants.R_MAJOR_2 = Constants.R_MAJOR * Constants.R_MAJOR;
|
|
13
|
+
Constants.R_MAJOR_4 = Constants.R_MAJOR_2 * Constants.R_MAJOR_2;
|
|
14
|
+
Constants.R_MINOR_2 = Constants.R_MINOR * Constants.R_MINOR;
|
|
15
|
+
Constants.R_MINOR_4 = Constants.R_MINOR_2 * Constants.R_MINOR_2;
|
|
16
|
+
Constants.ECCENTRICITY_2 = Constants.ECCENTRICITY * Constants.ECCENTRICITY;
|
|
17
|
+
|
|
18
|
+
export default Constants;
|
|
@@ -0,0 +1,285 @@
|
|
|
1
|
+
import { Utils, Vector3, Quaternion } from '@wemap/maths';
|
|
2
|
+
import Constants from '../Constants';
|
|
3
|
+
|
|
4
|
+
|
|
5
|
+
class WGS84 {
|
|
6
|
+
|
|
7
|
+
constructor(lat, lng, alt = 0) {
|
|
8
|
+
this._lat = lat;
|
|
9
|
+
this._lng = lng;
|
|
10
|
+
this._alt = alt;
|
|
11
|
+
this._ecef = null;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
get lat() {
|
|
15
|
+
return this._lat;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
get lng() {
|
|
19
|
+
return this._lng;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
get alt() {
|
|
23
|
+
return this._alt;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
set lat(lat) {
|
|
27
|
+
this._ecef = null;
|
|
28
|
+
this._lat = lat;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
set lng(lng) {
|
|
32
|
+
this._ecef = null;
|
|
33
|
+
this._lng = lng;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
set alt(alt) {
|
|
37
|
+
this._ecef = null;
|
|
38
|
+
this._alt = alt;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
clone() {
|
|
42
|
+
return new WGS84(this.lat, this.lng, this.alt);
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
equalsTo(other) {
|
|
46
|
+
|
|
47
|
+
if (!(other instanceof WGS84)) {
|
|
48
|
+
return false;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
return other.lat === this.lat && other.lng === this.lng;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
toString() {
|
|
55
|
+
let str = '[' + this._lat.toFixed(7) + ', ' + this._lng.toFixed(7);
|
|
56
|
+
if (this._alt) {
|
|
57
|
+
str += ', ' + this._alt.toFixed(2);
|
|
58
|
+
}
|
|
59
|
+
str += ']';
|
|
60
|
+
return str;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
destinationPoint(distance, bearing, elevation = 0) {
|
|
64
|
+
const newPoint = this.clone();
|
|
65
|
+
newPoint.move(distance, bearing, elevation);
|
|
66
|
+
return newPoint;
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
// Source: http://www.movable-type.co.uk/scripts/latlong.html#destPoint
|
|
70
|
+
move(distance, bearing, elevation = 0) {
|
|
71
|
+
|
|
72
|
+
const d = distance;
|
|
73
|
+
const R = Constants.R_MAJOR;
|
|
74
|
+
|
|
75
|
+
const φ1 = Utils.deg2rad(this.lat);
|
|
76
|
+
const λ1 = Utils.deg2rad(this.lng);
|
|
77
|
+
|
|
78
|
+
const φ2 = Math.asin(Math.sin(φ1) * Math.cos(d / R) + Math.cos(φ1) * Math.sin(d / R) * Math.cos(bearing));
|
|
79
|
+
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));
|
|
80
|
+
|
|
81
|
+
const altitude = (this.alt || 0) + elevation;
|
|
82
|
+
|
|
83
|
+
this.lat = Utils.rad2deg(φ2);
|
|
84
|
+
this.lng = Utils.rad2deg(λ2);
|
|
85
|
+
this.alt = altitude;
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
/**
|
|
89
|
+
* Returns a distance between two points in meters
|
|
90
|
+
* @param {Number} location2 latitude / longitude point2
|
|
91
|
+
* @return {Number} distance in meters
|
|
92
|
+
*/
|
|
93
|
+
distanceTo(location2) {
|
|
94
|
+
var cosn, dist, dlat, dlng, angle, tangy, tangx,
|
|
95
|
+
dlatsin, dlngsin, lat1cos, lat2cos, lat2rad, lat1rad;
|
|
96
|
+
|
|
97
|
+
const lat1 = this.lat;
|
|
98
|
+
const lng1 = this.lng;
|
|
99
|
+
|
|
100
|
+
const lat2 = location2.lat;
|
|
101
|
+
const lng2 = location2.lng;
|
|
102
|
+
|
|
103
|
+
dlat = Utils.deg2rad(lat2 - lat1);
|
|
104
|
+
dlng = Utils.deg2rad(lng2 - lng1);
|
|
105
|
+
|
|
106
|
+
dlngsin = Math.sin(dlng / 2);
|
|
107
|
+
dlatsin = Math.sin(dlat / 2);
|
|
108
|
+
lat1rad = Utils.deg2rad(lat1);
|
|
109
|
+
lat1cos = Math.cos(lat1rad);
|
|
110
|
+
lat2rad = Utils.deg2rad(lat2);
|
|
111
|
+
lat2cos = Math.cos(lat2rad);
|
|
112
|
+
angle = (dlatsin * dlatsin + lat1cos * lat2cos * dlngsin * dlngsin);
|
|
113
|
+
|
|
114
|
+
// arctangent
|
|
115
|
+
tangy = Math.sqrt(angle);
|
|
116
|
+
tangx = Math.sqrt(1 - angle);
|
|
117
|
+
cosn = (2 * Math.atan2(tangy, tangx));
|
|
118
|
+
|
|
119
|
+
// distance in km
|
|
120
|
+
dist = (Constants.EARTH_RADIUS_KM * 1000 * cosn);
|
|
121
|
+
return dist;
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
static distanceBetween(point1, point2) {
|
|
125
|
+
return point1.distanceTo(point2);
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
bearingTo(location2) {
|
|
129
|
+
const lat1 = Utils.deg2rad(this.lat);
|
|
130
|
+
const lat2 = Utils.deg2rad(location2.lat);
|
|
131
|
+
const diffLng = Utils.deg2rad(location2.lng - this.lng);
|
|
132
|
+
|
|
133
|
+
return Math.atan2(Math.sin(diffLng) * Math.cos(lat2),
|
|
134
|
+
Math.cos(lat1) * Math.sin(lat2) - Math.sin(lat1) * Math.cos(lat2) * Math.cos(diffLng));
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
|
|
138
|
+
getEnuToEcefRotation() {
|
|
139
|
+
const rot1 = Quaternion.fromAxisAngle([0, 0, 1], Math.PI / 2 + Utils.deg2rad(this.lng));
|
|
140
|
+
const rot2 = Quaternion.fromAxisAngle([1, 0, 0], Math.PI / 2 - Utils.deg2rad(this.lat));
|
|
141
|
+
return Quaternion.multiply(rot1, rot2);
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
getEcefToEnuRotation() {
|
|
145
|
+
const rot1 = Quaternion.fromAxisAngle([1, 0, 0], Utils.deg2rad(this.lat) - Math.PI / 2);
|
|
146
|
+
const rot2 = Quaternion.fromAxisAngle([0, 0, 1], -Utils.deg2rad(this.lng) - Math.PI / 2);
|
|
147
|
+
return Quaternion.multiply(rot1, rot2);
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
getUpVector() {
|
|
151
|
+
return Vector3.normalize(this.ecef);
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
|
|
155
|
+
get ecef() {
|
|
156
|
+
|
|
157
|
+
if (!this._ecef) {
|
|
158
|
+
const lat = Utils.deg2rad(this.lat);
|
|
159
|
+
const lng = Utils.deg2rad(this.lng);
|
|
160
|
+
const alt = this.alt || 0;
|
|
161
|
+
|
|
162
|
+
const N = Constants.R_MAJOR / Math.sqrt(1 - Constants.ECCENTRICITY_2 * Math.pow(Math.sin(lat), 2));
|
|
163
|
+
|
|
164
|
+
const x = (N + alt) * Math.cos(lat) * Math.cos(lng);
|
|
165
|
+
const y = (N + alt) * Math.cos(lat) * Math.sin(lng);
|
|
166
|
+
const z = ((1 - Constants.ECCENTRICITY_2) * N + alt) * Math.sin(lat);
|
|
167
|
+
|
|
168
|
+
this._ecef = [x, y, z];
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
return this._ecef;
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
static fromECEF(ecef) {
|
|
175
|
+
|
|
176
|
+
const x = ecef[0];
|
|
177
|
+
const y = ecef[1];
|
|
178
|
+
const z = ecef[2];
|
|
179
|
+
|
|
180
|
+
const b = Math.sqrt(Constants.R_MAJOR_2 * (1 - Constants.ECCENTRICITY_2));
|
|
181
|
+
const bsq = Math.pow(b, 2);
|
|
182
|
+
const ep = Math.sqrt((Constants.R_MAJOR_2 - bsq) / bsq);
|
|
183
|
+
const p = Math.sqrt(Math.pow(x, 2) + Math.pow(y, 2));
|
|
184
|
+
const th = Math.atan2(Constants.R_MAJOR * z, b * p);
|
|
185
|
+
|
|
186
|
+
let lng = Math.atan2(y, x);
|
|
187
|
+
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)));
|
|
188
|
+
const N = Constants.R_MAJOR / (Math.sqrt(1 - Constants.ECCENTRICITY_2 * Math.pow(Math.sin(lat), 2)));
|
|
189
|
+
const alt = p / Math.cos(lat) - N;
|
|
190
|
+
|
|
191
|
+
lng = lng % (2 * Math.PI);
|
|
192
|
+
|
|
193
|
+
const newPoint = new WGS84(Utils.rad2deg(lat), Utils.rad2deg(lng), Utils.rad2deg(alt));
|
|
194
|
+
newPoint._ecef = ecef;
|
|
195
|
+
return newPoint;
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
// https://stackoverflow.com/questions/1299567/how-to-calculate-distance-from-a-point-to-a-line-segment-on-a-sphere
|
|
199
|
+
// Adapted to ECEF
|
|
200
|
+
getProjection(edge) {
|
|
201
|
+
|
|
202
|
+
const p1 = edge.node1;
|
|
203
|
+
const p2 = edge.node2;
|
|
204
|
+
|
|
205
|
+
const a = Vector3.normalize(p1.ecef);
|
|
206
|
+
const b = Vector3.normalize(p2.ecef);
|
|
207
|
+
const c = Vector3.normalize(this.ecef);
|
|
208
|
+
|
|
209
|
+
const G = Vector3.cross(a, b);
|
|
210
|
+
const F = Vector3.cross(c, G);
|
|
211
|
+
let t = Vector3.normalize(Vector3.cross(G, F));
|
|
212
|
+
|
|
213
|
+
t = Vector3.multiplyScalar(t, this.getEarthRadiusAtPosition());
|
|
214
|
+
|
|
215
|
+
const projection = WGS84.fromECEF(t);
|
|
216
|
+
projection.alt = p1.alt && p2.alt ? (p1.alt + p2.alt) / 2 : 0;
|
|
217
|
+
if (Math.abs((p1.distanceTo(p2) - p1.distanceTo(projection) - p2.distanceTo(projection))) > 1e-6) {
|
|
218
|
+
return null;
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
return projection;
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
|
|
225
|
+
getEarthRadiusAtPosition() {
|
|
226
|
+
const latR = Utils.deg2rad(this.lat);
|
|
227
|
+
const cos2 = Math.cos(latR) * Math.cos(latR);
|
|
228
|
+
const sin2 = Math.sin(latR) * Math.sin(latR);
|
|
229
|
+
return Math.sqrt((Constants.R_MAJOR_4 * cos2 + Constants.R_MINOR_4 * sin2) / (Constants.R_MAJOR_2 * cos2 + Constants.R_MINOR_2 * sin2));
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
toMessage() {
|
|
233
|
+
return {
|
|
234
|
+
lat: this.lat,
|
|
235
|
+
lng: this.lng,
|
|
236
|
+
alt: this.alt
|
|
237
|
+
};
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
static fromMessage(message) {
|
|
241
|
+
return new WGS84(message.lat, message.lng, message.alt);
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
static fromPinpoint(pinpoint) {
|
|
245
|
+
return new WGS84(pinpoint.latitude, pinpoint.longitude, pinpoint.altitude);
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
static sampleRoute(route, stepSize = 0.7, maxLength = Number.MAX_VALUE) {
|
|
249
|
+
|
|
250
|
+
let p1, p2 = null;
|
|
251
|
+
let bearing, distance;
|
|
252
|
+
|
|
253
|
+
const sampledRoute = [];
|
|
254
|
+
let reportedDistance = 0;
|
|
255
|
+
let totalDistance = 0;
|
|
256
|
+
|
|
257
|
+
for (let i = 0; i < route.length - 1; i++) {
|
|
258
|
+
|
|
259
|
+
p1 = route[i];
|
|
260
|
+
p2 = route[i + 1];
|
|
261
|
+
|
|
262
|
+
bearing = p1.bearingTo(p2);
|
|
263
|
+
distance = p1.distanceTo(p2);
|
|
264
|
+
|
|
265
|
+
if (distance > 0) {
|
|
266
|
+
let ratio = reportedDistance / distance;
|
|
267
|
+
while (ratio < 1 && totalDistance <= maxLength) {
|
|
268
|
+
const newPoint = p1.destinationPoint(ratio * distance, bearing);
|
|
269
|
+
newPoint.bearing = bearing;
|
|
270
|
+
sampledRoute.push(newPoint);
|
|
271
|
+
ratio += stepSize / distance;
|
|
272
|
+
totalDistance += stepSize;
|
|
273
|
+
}
|
|
274
|
+
reportedDistance = (ratio - 1) * distance;
|
|
275
|
+
}
|
|
276
|
+
}
|
|
277
|
+
if (p2) {
|
|
278
|
+
sampledRoute.push(p2);
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
return sampledRoute;
|
|
282
|
+
}
|
|
283
|
+
}
|
|
284
|
+
|
|
285
|
+
export default WGS84;
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
import WGS84 from './WGS84';
|
|
2
|
+
|
|
3
|
+
const MESSAGE_TYPE = 'WGS84UserPosition';
|
|
4
|
+
|
|
5
|
+
class WGS84UserPosition extends WGS84 {
|
|
6
|
+
|
|
7
|
+
constructor(lat, lng, alt = null, time = null,
|
|
8
|
+
accuracy = null, bearing = null, provider = null) {
|
|
9
|
+
super(lat, lng, alt);
|
|
10
|
+
this.bearing = bearing;
|
|
11
|
+
this.provider = provider;
|
|
12
|
+
this.time = time;
|
|
13
|
+
this.accuracy = accuracy;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
// Create a WGS84UserPosition with lat, lng, alt from WGS84 coordinates and
|
|
17
|
+
// other fields from another WGS84UserPosition
|
|
18
|
+
static fromWGS84(wgs84Coordinates, userPosition) {
|
|
19
|
+
|
|
20
|
+
if (userPosition) {
|
|
21
|
+
return new WGS84UserPosition(wgs84Coordinates.lat, wgs84Coordinates.lng, wgs84Coordinates.alt,
|
|
22
|
+
userPosition.time, userPosition.accuracy, userPosition.bearing, userPosition.provider);
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
return new WGS84UserPosition(wgs84Coordinates.lat, wgs84Coordinates.lng, wgs84Coordinates.alt);
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
clone() {
|
|
29
|
+
return new WGS84UserPosition(this.lat, this.lng, this.alt, this.time, this.accuracy, this.bearing, this.provider);
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
static get MESSAGE_TYPE() {
|
|
33
|
+
return MESSAGE_TYPE;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
toMessage() {
|
|
37
|
+
const message = super.toMessage();
|
|
38
|
+
message.type = MESSAGE_TYPE;
|
|
39
|
+
message.time = this.time;
|
|
40
|
+
message.bearing = this.bearing;
|
|
41
|
+
message.provider = this.provider;
|
|
42
|
+
message.accuracy = this.accuracy;
|
|
43
|
+
return message;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
static fromMessage(message) {
|
|
47
|
+
return new WGS84UserPosition(message.lat, message.lng, message.alt,
|
|
48
|
+
message.time, message.accuracy, message.bearing, message.provider);
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
export default WGS84UserPosition;
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
import WGS84 from '../coordinates/WGS84';
|
|
2
|
+
|
|
3
|
+
class Edge {
|
|
4
|
+
|
|
5
|
+
constructor(node1, node2) {
|
|
6
|
+
|
|
7
|
+
if (!(node1 instanceof WGS84)) {
|
|
8
|
+
throw new TypeError('node1 is not an instance of WGS84');
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
if (!(node2 instanceof WGS84)) {
|
|
12
|
+
throw new TypeError('node2 is not an instance of WGS84');
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
this.node1 = node1;
|
|
16
|
+
this.node2 = node2;
|
|
17
|
+
this.computedSizeAndBearing = false;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
getBearing() {
|
|
21
|
+
if (!this.computedSizeAndBearing) {
|
|
22
|
+
this.computeSizeAndBearing();
|
|
23
|
+
}
|
|
24
|
+
return this.bearing;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
getLength() {
|
|
28
|
+
if (!this.computedSizeAndBearing) {
|
|
29
|
+
this.computeSizeAndBearing();
|
|
30
|
+
}
|
|
31
|
+
return this.length;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
computeSizeAndBearing() {
|
|
35
|
+
this.length = this.node1.distanceTo(this.node2);
|
|
36
|
+
this.bearing = this.node1.bearingTo(this.node2);
|
|
37
|
+
this.computedSizedAndBearing = true;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
static pointsToEdges(points, lngLatFormat = true) {
|
|
41
|
+
let lastPoint = null;
|
|
42
|
+
const edgesNetwork = [];
|
|
43
|
+
for (let i = 0; i < points.length; i++) {
|
|
44
|
+
const currentPoint = new WGS84(points[i][lngLatFormat ? 1 : 0], points[i][lngLatFormat ? 0 : 1]);
|
|
45
|
+
if (lastPoint) {
|
|
46
|
+
edgesNetwork.push(new Edge(lastPoint, currentPoint));
|
|
47
|
+
}
|
|
48
|
+
lastPoint = currentPoint;
|
|
49
|
+
}
|
|
50
|
+
return edgesNetwork;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
equalsTo(other) {
|
|
54
|
+
|
|
55
|
+
if (!(other instanceof Edge)) {
|
|
56
|
+
return false;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
return other.node1.equalsTo(this.node1) && other.node2.equalsTo(this.node2);
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
export default Edge;
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
import Edge from './Edge';
|
|
2
|
+
import Network from './Network';
|
|
3
|
+
|
|
4
|
+
// An itinerary is an ordered list of edges where edge(n-1).p2 = edge.(n).p1
|
|
5
|
+
class Itinerary extends Network {
|
|
6
|
+
|
|
7
|
+
constructor(edges = []) {
|
|
8
|
+
super(edges);
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
static fromPoints(points, lngLatFormat = true) {
|
|
12
|
+
return new Itinerary(Edge.pointsToEdges(points, lngLatFormat));
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
get firstEdge() {
|
|
16
|
+
if (this.edges.length < 1) {
|
|
17
|
+
throw new TypeError('first edge of itinerary does not exist');
|
|
18
|
+
}
|
|
19
|
+
return this.edges[0];
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
get secondEdge() {
|
|
23
|
+
if (this.edges.length < 2) {
|
|
24
|
+
throw new TypeError('second edge of itinerary does not exist');
|
|
25
|
+
}
|
|
26
|
+
return this.edges[1];
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
get distance() {
|
|
30
|
+
let dist = 0;
|
|
31
|
+
this.edges.forEach(edge => (dist += edge.getLength()));
|
|
32
|
+
return dist;
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
export default Itinerary;
|
|
@@ -0,0 +1,118 @@
|
|
|
1
|
+
/* eslint complexity: ["error", 21]*/
|
|
2
|
+
/* eslint max-statements: ["error", 45]*/
|
|
3
|
+
|
|
4
|
+
import { Utils as MathUtils} from '@wemap/maths';
|
|
5
|
+
import Network from './Network';
|
|
6
|
+
|
|
7
|
+
const rad2deg = MathUtils.rad2deg;
|
|
8
|
+
|
|
9
|
+
class MapMatching {
|
|
10
|
+
|
|
11
|
+
_maxDistance = Number.MAX_VALUE;
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* Constructor of Map-matching
|
|
15
|
+
* @param {Network} network List of Edges
|
|
16
|
+
*/
|
|
17
|
+
constructor(network) {
|
|
18
|
+
if (network) {
|
|
19
|
+
this.setNetwork(network);
|
|
20
|
+
}
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
set maxAngleBearing(maxAngleBearing) {
|
|
24
|
+
this._maxAngleBearing = maxAngleBearing;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
set maxDistance(maxDistance) {
|
|
28
|
+
this._maxDistance = maxDistance;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
setNetwork(network) {
|
|
32
|
+
if (!(network instanceof Network)) {
|
|
33
|
+
throw new Error(network + ' is not an instance of Network');
|
|
34
|
+
}
|
|
35
|
+
this.network = network;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
getProjection(location, useDistance = true, useBearing = true) {
|
|
39
|
+
|
|
40
|
+
if (!this.network) {
|
|
41
|
+
return null;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
if (useBearing && (!location.bearing || !this._maxAngleBearing)) {
|
|
45
|
+
return null;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
if (useDistance && !this._maxDistance) {
|
|
49
|
+
return null;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
const projection = {};
|
|
53
|
+
projection.origin = location;
|
|
54
|
+
projection.distanceFromNearestElement = Number.MAX_VALUE;
|
|
55
|
+
|
|
56
|
+
for (let i = 0; i < this.network.edges.length; i++) {
|
|
57
|
+
const edge = this.network.edges[i];
|
|
58
|
+
|
|
59
|
+
if (useBearing) {
|
|
60
|
+
const edgeBearing = rad2deg(edge.getBearing());
|
|
61
|
+
const diffAngle = MapMatching.diffAngle90(edgeBearing, location.bearing);
|
|
62
|
+
if (diffAngle > this._maxAngleBearing) {
|
|
63
|
+
continue;
|
|
64
|
+
}
|
|
65
|
+
} else {
|
|
66
|
+
// Check node 1
|
|
67
|
+
const distNode1 = location.distanceTo(edge.node1);
|
|
68
|
+
if (distNode1 < projection.distanceFromNearestElement
|
|
69
|
+
&& (!useDistance || distNode1 <= this._maxDistance)) {
|
|
70
|
+
projection.distanceFromNearestElement = distNode1;
|
|
71
|
+
projection.nearestElement = edge.node1;
|
|
72
|
+
projection.projection = edge.node1.clone();
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
// Check node 2
|
|
76
|
+
const distNode2 = location.distanceTo(edge.node2);
|
|
77
|
+
if (distNode2 < projection.distanceFromNearestElement
|
|
78
|
+
&& (!useDistance || distNode2 <= this._maxDistance)) {
|
|
79
|
+
projection.distanceFromNearestElement = distNode2;
|
|
80
|
+
projection.nearestElement = edge.node2;
|
|
81
|
+
projection.projection = edge.node2.clone();
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
// Check edge
|
|
86
|
+
const segmentProjection = location.getProjection(edge);
|
|
87
|
+
if (segmentProjection) {
|
|
88
|
+
const distEdge = location.distanceTo(segmentProjection);
|
|
89
|
+
if (distEdge < projection.distanceFromNearestElement
|
|
90
|
+
&& (!useDistance || distEdge <= this._maxDistance)) {
|
|
91
|
+
projection.distanceFromNearestElement = distEdge;
|
|
92
|
+
projection.nearestElement = edge;
|
|
93
|
+
projection.projection = segmentProjection.clone();
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
if (!projection.projection) {
|
|
99
|
+
return null;
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
return projection;
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
static diffAngle90(angle1, angle2) {
|
|
106
|
+
let diffAngle = Math.abs(angle1 - angle2);
|
|
107
|
+
if (diffAngle > 180) {
|
|
108
|
+
diffAngle = 360 - diffAngle;
|
|
109
|
+
}
|
|
110
|
+
if (diffAngle > 90) {
|
|
111
|
+
diffAngle = 180 - diffAngle;
|
|
112
|
+
}
|
|
113
|
+
return diffAngle;
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
export default MapMatching;
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
import chai from 'chai';
|
|
2
|
+
import chaiAlmost from 'chai-almost';
|
|
3
|
+
|
|
4
|
+
import Edge from './Edge';
|
|
5
|
+
import MapMatching from './MapMatching';
|
|
6
|
+
import Network from './Network';
|
|
7
|
+
import WGS84 from '../coordinates/WGS84';
|
|
8
|
+
import WGS84UserPosition from '../coordinates/WGS84UserPosition';
|
|
9
|
+
|
|
10
|
+
const expect = chai.expect;
|
|
11
|
+
chai.use(chaiAlmost());
|
|
12
|
+
|
|
13
|
+
describe('matching', () => {
|
|
14
|
+
it('Should return the good value', () => {
|
|
15
|
+
|
|
16
|
+
const p1 = new WGS84(43.6091883, 3.8841242);
|
|
17
|
+
const p2 = new WGS84(43.6091709, 3.8842382);
|
|
18
|
+
const p3 = new WGS84(43.6091288, 3.884226);
|
|
19
|
+
const p4 = new WGS84(43.6091461, 3.884112);
|
|
20
|
+
const e1 = new Edge(p1, p2);
|
|
21
|
+
const e2 = new Edge(p2, p3);
|
|
22
|
+
const e3 = new Edge(p3, p4);
|
|
23
|
+
const e4 = new Edge(p4, p1);
|
|
24
|
+
const network = new Network([e1, e2, e3, e4]);
|
|
25
|
+
|
|
26
|
+
const mapMatching = new MapMatching(network);
|
|
27
|
+
mapMatching.maxAngleBearing = 30;
|
|
28
|
+
|
|
29
|
+
const location = new WGS84UserPosition(43.6091762, 3.8841239);
|
|
30
|
+
|
|
31
|
+
location.bearing = 101.9;
|
|
32
|
+
const projection1 = mapMatching.getProjection(location);
|
|
33
|
+
const result1 = projection1.projection;
|
|
34
|
+
const expected1 = new WGS84(43.609187832342556, 3.884127262759803);
|
|
35
|
+
const distance1 = result1.distanceTo(expected1);
|
|
36
|
+
expect(distance1).to.below(0.01);
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
location.bearing = 11.9;
|
|
40
|
+
const projection2 = mapMatching.getProjection(location);
|
|
41
|
+
const result2 = projection2.projection;
|
|
42
|
+
const expected2 = new WGS84(43.609176667533255, 3.8841208369745965);
|
|
43
|
+
const distance2 = result2.distanceTo(expected2);
|
|
44
|
+
expect(distance2).to.below(0.01);
|
|
45
|
+
|
|
46
|
+
});
|
|
47
|
+
});
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
import Edge from './Edge';
|
|
2
|
+
|
|
3
|
+
class Network {
|
|
4
|
+
|
|
5
|
+
constructor(edges = []) {
|
|
6
|
+
this.addEdges(edges);
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
addEdges(edges) {
|
|
10
|
+
Network.checkEdgesArray(edges);
|
|
11
|
+
if (!this.edges) {
|
|
12
|
+
this.edges = [];
|
|
13
|
+
}
|
|
14
|
+
this.edges = this.edges.concat(edges);
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
get length() {
|
|
18
|
+
return this.edges.length;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
static checkEdgesArray(edges) {
|
|
22
|
+
if (!Array.isArray(edges)) {
|
|
23
|
+
throw new TypeError('Network creation: edges param is not an Array');
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
edges.forEach(edge => {
|
|
27
|
+
if (!(edge instanceof Edge)) {
|
|
28
|
+
throw new TypeError('Network creation: ' + edge + ' is not an Edge');
|
|
29
|
+
}
|
|
30
|
+
});
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
export default Network;
|