@wemap/geo 0.1.4 → 0.2.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/index.js +13 -4
- package/package.json +5 -2
- package/src/coordinates/Level.js +108 -0
- package/src/coordinates/WGS84.js +54 -6
- package/src/coordinates/WGS84UserPosition.js +3 -0
- package/src/graph/Edge.js +30 -25
- package/src/graph/GraphRouter.js +242 -0
- package/src/graph/Itinerary.js +116 -19
- package/src/graph/MapMatching.js +28 -13
- package/src/graph/MapMatching.spec.js +37 -12
- package/src/graph/Network.js +15 -22
- package/src/graph/Node.js +26 -0
package/index.js
CHANGED
|
@@ -1,19 +1,28 @@
|
|
|
1
|
-
import Attitude from './src/rotations/Attitude';
|
|
2
1
|
import Constants from './src/Constants';
|
|
2
|
+
|
|
3
|
+
import Attitude from './src/rotations/Attitude';
|
|
4
|
+
|
|
5
|
+
import Level from './src/coordinates/Level';
|
|
6
|
+
import WGS84 from './src/coordinates/WGS84';
|
|
7
|
+
import WGS84UserPosition from './src/coordinates/WGS84UserPosition';
|
|
8
|
+
|
|
3
9
|
import Edge from './src/graph/Edge';
|
|
10
|
+
import GraphRouter from './src/graph/GraphRouter';
|
|
4
11
|
import Itinerary from './src/graph/Itinerary';
|
|
5
|
-
import MapMatching from './src/graph/MapMatching';
|
|
6
12
|
import Network from './src/graph/Network';
|
|
7
|
-
import
|
|
8
|
-
import
|
|
13
|
+
import Node from './src/graph/Node';
|
|
14
|
+
import MapMatching from './src/graph/MapMatching';
|
|
9
15
|
|
|
10
16
|
export {
|
|
11
17
|
Attitude,
|
|
12
18
|
Constants,
|
|
13
19
|
Edge,
|
|
20
|
+
GraphRouter,
|
|
14
21
|
Itinerary,
|
|
22
|
+
Level,
|
|
15
23
|
MapMatching,
|
|
16
24
|
Network,
|
|
25
|
+
Node,
|
|
17
26
|
WGS84,
|
|
18
27
|
WGS84UserPosition
|
|
19
28
|
};
|
package/package.json
CHANGED
|
@@ -12,7 +12,7 @@
|
|
|
12
12
|
"directory": "packages/geo"
|
|
13
13
|
},
|
|
14
14
|
"name": "@wemap/geo",
|
|
15
|
-
"version": "0.
|
|
15
|
+
"version": "0.2.0",
|
|
16
16
|
"bugs": {
|
|
17
17
|
"url": "https://github.com/wemap/wemap-utils-js/issues"
|
|
18
18
|
},
|
|
@@ -26,5 +26,8 @@
|
|
|
26
26
|
"wemap"
|
|
27
27
|
],
|
|
28
28
|
"license": "ISC",
|
|
29
|
-
"
|
|
29
|
+
"dependencies": {
|
|
30
|
+
"lodash.isnumber": "^3.0.3"
|
|
31
|
+
},
|
|
32
|
+
"gitHead": "6673bc4d0f4b6ff404534e0642122c340fe58483"
|
|
30
33
|
}
|
|
@@ -0,0 +1,108 @@
|
|
|
1
|
+
import Logger from '@wemap/logger';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* A Level is the representation of a building floor number
|
|
5
|
+
* A level can be a simple number (val) or a range (low, up)
|
|
6
|
+
* To know if a level is a range or a number, isRange can be used
|
|
7
|
+
*/
|
|
8
|
+
class Level {
|
|
9
|
+
|
|
10
|
+
constructor() {
|
|
11
|
+
this.val = null;
|
|
12
|
+
this.isRange = false;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
clone() {
|
|
16
|
+
const output = new Level();
|
|
17
|
+
output.isRange = this.isRange;
|
|
18
|
+
output.val = this.val;
|
|
19
|
+
output.up = this.up;
|
|
20
|
+
output.low = this.low;
|
|
21
|
+
return output;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
/**
|
|
25
|
+
* Create a level from a string
|
|
26
|
+
* @param {*} str level in str format (eg. 1, -2, 1;2, -2;3, 2;-1, 0.5;1 ...)
|
|
27
|
+
*/
|
|
28
|
+
static fromString(str) {
|
|
29
|
+
const level = new Level();
|
|
30
|
+
const num = Number(str);
|
|
31
|
+
if (!isNaN(num)) {
|
|
32
|
+
level.val = num;
|
|
33
|
+
level.isRange = false;
|
|
34
|
+
return level;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
const splited = str.split(';');
|
|
38
|
+
if (splited.length === 2) {
|
|
39
|
+
const num1 = Number(splited[0]);
|
|
40
|
+
const num2 = Number(splited[1]);
|
|
41
|
+
if (!isNaN(num1) && !isNaN(num2)) {
|
|
42
|
+
level.up = Math.max(num1, num2);
|
|
43
|
+
level.low = Math.min(num1, num2);
|
|
44
|
+
level.isRange = true;
|
|
45
|
+
return level;
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
Logger.warn('Cannot parse following level: ' + str);
|
|
50
|
+
return null;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
/**
|
|
54
|
+
* Is the given number inside the current level
|
|
55
|
+
* @param {Number} val the given number
|
|
56
|
+
*/
|
|
57
|
+
isInside(val) {
|
|
58
|
+
return this.isRange ? val >= this.low && val <= this.up : val === this.val;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
/**
|
|
62
|
+
* Retrieve the intersection of two levels
|
|
63
|
+
* @param {Level} other The other level
|
|
64
|
+
*/
|
|
65
|
+
intersect(other) {
|
|
66
|
+
|
|
67
|
+
if (!other) {
|
|
68
|
+
return null;
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
if (this.isRange && !other.isRange) {
|
|
72
|
+
if (this.isInside(other.val)) {
|
|
73
|
+
return other;
|
|
74
|
+
}
|
|
75
|
+
return null;
|
|
76
|
+
}
|
|
77
|
+
if (!this.isRange && other.isRange) {
|
|
78
|
+
if (other.isInside(this.val)) {
|
|
79
|
+
return this;
|
|
80
|
+
}
|
|
81
|
+
return null;
|
|
82
|
+
}
|
|
83
|
+
if (this.isRange && other.isRange) {
|
|
84
|
+
const level = new Level();
|
|
85
|
+
level.low = Math.max(this.low, other.low);
|
|
86
|
+
level.up = Math.min(this.up, other.up);
|
|
87
|
+
level.isRange = true;
|
|
88
|
+
return level;
|
|
89
|
+
}
|
|
90
|
+
return this.val === other.val ? this : null;
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
multiplyBy(factor) {
|
|
94
|
+
if (this.isRange) {
|
|
95
|
+
this.low *= factor;
|
|
96
|
+
this.up *= factor;
|
|
97
|
+
} else {
|
|
98
|
+
this.val *= factor;
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
toString() {
|
|
103
|
+
return this.isRange ? this.low + ';' + this.up : this.val;
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
export default Level;
|
package/src/coordinates/WGS84.js
CHANGED
|
@@ -2,14 +2,30 @@ import {
|
|
|
2
2
|
Utils, Vector3, Quaternion
|
|
3
3
|
} from '@wemap/maths';
|
|
4
4
|
import Constants from '../Constants';
|
|
5
|
-
|
|
6
|
-
|
|
5
|
+
import isNumber from 'lodash/isnumber';
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* JSON output formats:
|
|
9
|
+
* - FORMAT_LAT_LNG_ELE (default)
|
|
10
|
+
* - FORMAT_LNG_LAT_STRLEVEL
|
|
11
|
+
*/
|
|
12
|
+
const FORMAT_LAT_LNG_ELE = 0;
|
|
13
|
+
const FORMAT_LNG_LAT_STRLEVEL = 1;
|
|
14
|
+
|
|
15
|
+
/**
|
|
16
|
+
* A WGS84 position using at least latitude (lat) and longitude (lng).
|
|
17
|
+
* Optionnal fields are: altitude (alt) and level.
|
|
18
|
+
*
|
|
19
|
+
* Basic geo methods are directly accessibles from here:
|
|
20
|
+
* distanceTo, bearingTo, toEcef...
|
|
21
|
+
*/
|
|
7
22
|
class WGS84 {
|
|
8
23
|
|
|
9
24
|
constructor(lat, lng, alt) {
|
|
10
25
|
this._lat = lat;
|
|
11
26
|
this._lng = lng;
|
|
12
27
|
this._alt = alt;
|
|
28
|
+
this._level = null;
|
|
13
29
|
this._ecef = null;
|
|
14
30
|
}
|
|
15
31
|
|
|
@@ -25,6 +41,10 @@ class WGS84 {
|
|
|
25
41
|
return this._alt;
|
|
26
42
|
}
|
|
27
43
|
|
|
44
|
+
get level() {
|
|
45
|
+
return this._level;
|
|
46
|
+
}
|
|
47
|
+
|
|
28
48
|
set lat(lat) {
|
|
29
49
|
this._ecef = null;
|
|
30
50
|
this._lat = lat;
|
|
@@ -40,8 +60,16 @@ class WGS84 {
|
|
|
40
60
|
this._alt = alt;
|
|
41
61
|
}
|
|
42
62
|
|
|
63
|
+
set level(level) {
|
|
64
|
+
this._level = level;
|
|
65
|
+
}
|
|
66
|
+
|
|
43
67
|
clone() {
|
|
44
|
-
|
|
68
|
+
const output = new WGS84(this.lat, this.lng, this.alt);
|
|
69
|
+
if (this.level) {
|
|
70
|
+
output.level = this.level.clone();
|
|
71
|
+
}
|
|
72
|
+
return output;
|
|
45
73
|
}
|
|
46
74
|
|
|
47
75
|
equalsTo(other) {
|
|
@@ -62,6 +90,17 @@ class WGS84 {
|
|
|
62
90
|
return str;
|
|
63
91
|
}
|
|
64
92
|
|
|
93
|
+
toJson(format = FORMAT_LAT_LNG_ELE) {
|
|
94
|
+
switch (format) {
|
|
95
|
+
case FORMAT_LNG_LAT_STRLEVEL:
|
|
96
|
+
return [this.lng, this.lat, this.level ? this.level.toString() : null];
|
|
97
|
+
|
|
98
|
+
case FORMAT_LAT_LNG_ELE:
|
|
99
|
+
default:
|
|
100
|
+
return [this.lat, this.lng, this.ele];
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
|
|
65
104
|
destinationPoint(distance, bearing, elevation = 0) {
|
|
66
105
|
const newPoint = this.clone();
|
|
67
106
|
newPoint.move(distance, bearing, elevation);
|
|
@@ -201,8 +240,8 @@ class WGS84 {
|
|
|
201
240
|
// Adapted to ECEF
|
|
202
241
|
getProjection(edge) {
|
|
203
242
|
|
|
204
|
-
const p1 = edge.node1;
|
|
205
|
-
const p2 = edge.node2;
|
|
243
|
+
const p1 = edge.node1.coords;
|
|
244
|
+
const p2 = edge.node2.coords;
|
|
206
245
|
|
|
207
246
|
const a = Vector3.normalize(p1.ecef);
|
|
208
247
|
const b = Vector3.normalize(p2.ecef);
|
|
@@ -219,7 +258,7 @@ class WGS84 {
|
|
|
219
258
|
// So if p1.alt and p2.alt are defined we take the middle elevation between p1 and p2.
|
|
220
259
|
// Otherwise we remove alt from projection because the residual has no sense.
|
|
221
260
|
let alt;
|
|
222
|
-
if (
|
|
261
|
+
if (isNumber(p1.alt) && isNumber(p2.alt)) {
|
|
223
262
|
alt = (p1.alt + p2.alt) / 2;
|
|
224
263
|
}
|
|
225
264
|
const projection = new WGS84(poseWGS84.lat, poseWGS84.lng, alt);
|
|
@@ -290,6 +329,15 @@ class WGS84 {
|
|
|
290
329
|
|
|
291
330
|
return sampledRoute;
|
|
292
331
|
}
|
|
332
|
+
|
|
333
|
+
|
|
334
|
+
static get FORMAT_LNG_LAT_STRLEVEL() {
|
|
335
|
+
return FORMAT_LNG_LAT_STRLEVEL;
|
|
336
|
+
}
|
|
337
|
+
|
|
338
|
+
static get FORMAT_LAT_LNG_ELE() {
|
|
339
|
+
return FORMAT_LAT_LNG_ELE;
|
|
340
|
+
}
|
|
293
341
|
}
|
|
294
342
|
|
|
295
343
|
export default WGS84;
|
|
@@ -2,6 +2,9 @@ import WGS84 from './WGS84';
|
|
|
2
2
|
|
|
3
3
|
const MESSAGE_TYPE = 'WGS84UserPosition';
|
|
4
4
|
|
|
5
|
+
/**
|
|
6
|
+
* A WGS84 User Position is a WGS84 position with specific data related to user (bearing, provider, time, accuracy)
|
|
7
|
+
*/
|
|
5
8
|
class WGS84UserPosition extends WGS84 {
|
|
6
9
|
|
|
7
10
|
constructor(lat, lng, alt = null, time = null,
|
package/src/graph/Edge.js
CHANGED
|
@@ -1,55 +1,56 @@
|
|
|
1
|
-
import
|
|
1
|
+
import Node from './Node';
|
|
2
2
|
|
|
3
|
+
/**
|
|
4
|
+
* An Edge is a segment composed of two Node
|
|
5
|
+
* An edge is mostly issued from an OsmWay (data attribute), but this is not
|
|
6
|
+
* always the case. For example, edges created by mapmatching.
|
|
7
|
+
*/
|
|
3
8
|
class Edge {
|
|
4
9
|
|
|
5
|
-
constructor(node1, node2) {
|
|
10
|
+
constructor(node1, node2, data) {
|
|
6
11
|
|
|
7
|
-
if (!(node1 instanceof
|
|
8
|
-
throw new TypeError('node1 is not an instance of
|
|
12
|
+
if (!(node1 instanceof Node)) {
|
|
13
|
+
throw new TypeError('node1 is not an instance of Node');
|
|
9
14
|
}
|
|
10
15
|
|
|
11
|
-
if (!(node2 instanceof
|
|
12
|
-
throw new TypeError('node2 is not an instance of
|
|
16
|
+
if (!(node2 instanceof Node)) {
|
|
17
|
+
throw new TypeError('node2 is not an instance of Node');
|
|
13
18
|
}
|
|
14
19
|
|
|
15
20
|
this.node1 = node1;
|
|
16
21
|
this.node2 = node2;
|
|
22
|
+
this.data = data;
|
|
23
|
+
this.level = null;
|
|
24
|
+
|
|
17
25
|
this.computedSizeAndBearing = false;
|
|
18
26
|
}
|
|
19
27
|
|
|
20
|
-
|
|
28
|
+
/**
|
|
29
|
+
* Get edge bearing from node1 to node2
|
|
30
|
+
*/
|
|
31
|
+
get bearing() {
|
|
21
32
|
if (!this.computedSizeAndBearing) {
|
|
22
33
|
this.computeSizeAndBearing();
|
|
23
34
|
}
|
|
24
|
-
return this.
|
|
35
|
+
return this._bearing;
|
|
25
36
|
}
|
|
26
37
|
|
|
27
|
-
|
|
38
|
+
/**
|
|
39
|
+
* get edge length
|
|
40
|
+
*/
|
|
41
|
+
get length() {
|
|
28
42
|
if (!this.computedSizeAndBearing) {
|
|
29
43
|
this.computeSizeAndBearing();
|
|
30
44
|
}
|
|
31
|
-
return this.
|
|
45
|
+
return this._length;
|
|
32
46
|
}
|
|
33
47
|
|
|
34
48
|
computeSizeAndBearing() {
|
|
35
|
-
this.
|
|
36
|
-
this.
|
|
49
|
+
this._length = this.node1.distanceTo(this.node2);
|
|
50
|
+
this._bearing = this.node1.bearingTo(this.node2);
|
|
37
51
|
this.computedSizedAndBearing = true;
|
|
38
52
|
}
|
|
39
53
|
|
|
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
54
|
equalsTo(other) {
|
|
54
55
|
|
|
55
56
|
if (!(other instanceof Edge)) {
|
|
@@ -58,6 +59,10 @@ class Edge {
|
|
|
58
59
|
|
|
59
60
|
return other.node1.equalsTo(this.node1) && other.node2.equalsTo(this.node2);
|
|
60
61
|
}
|
|
62
|
+
|
|
63
|
+
clone(node1, node2) {
|
|
64
|
+
return new Edge(node1, node2, this.data);
|
|
65
|
+
}
|
|
61
66
|
}
|
|
62
67
|
|
|
63
68
|
export default Edge;
|
|
@@ -0,0 +1,242 @@
|
|
|
1
|
+
/* eslint-disable max-statements */
|
|
2
|
+
/* eslint-disable complexity */
|
|
3
|
+
|
|
4
|
+
import { WGS84 } from '@wemap/geo';
|
|
5
|
+
|
|
6
|
+
import Edge from './Edge';
|
|
7
|
+
import Itinerary from './Itinerary';
|
|
8
|
+
import Node from './Node';
|
|
9
|
+
import MapMatching from './MapMatching';
|
|
10
|
+
|
|
11
|
+
class GraphRouter {
|
|
12
|
+
|
|
13
|
+
constructor(network) {
|
|
14
|
+
this.network = network;
|
|
15
|
+
this.mapMatching = new MapMatching(network);
|
|
16
|
+
this.mapMatching.maxDistance = 50;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
getShortestPath(_start, _end, acceptEdgeFn = () => true) {
|
|
20
|
+
let start;
|
|
21
|
+
let startNodeCreated;
|
|
22
|
+
|
|
23
|
+
if (_start instanceof Node) {
|
|
24
|
+
start = _start;
|
|
25
|
+
} else if (_start instanceof WGS84) {
|
|
26
|
+
const node = this.network.getNodeByCoords(_start);
|
|
27
|
+
if (node) {
|
|
28
|
+
start = node;
|
|
29
|
+
} else {
|
|
30
|
+
const proj = this.mapMatching.getProjection(_start, true, false, false, acceptEdgeFn);
|
|
31
|
+
if (proj && proj.nearestElement instanceof Node) {
|
|
32
|
+
start = proj.nearestElement;
|
|
33
|
+
} else if (proj && proj.nearestElement instanceof Edge) {
|
|
34
|
+
startNodeCreated = this.createNodeInsideEdge(
|
|
35
|
+
proj.nearestElement,
|
|
36
|
+
proj.projection
|
|
37
|
+
);
|
|
38
|
+
start = startNodeCreated;
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
if (!start) {
|
|
44
|
+
throw new Error('Cannot provide path because no starting point has been found');
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
let end;
|
|
48
|
+
let endNodeCreated;
|
|
49
|
+
|
|
50
|
+
if (_end instanceof Node) {
|
|
51
|
+
end = _end;
|
|
52
|
+
} else if (_end instanceof WGS84) {
|
|
53
|
+
const node = this.network.getNodeByCoords(_end);
|
|
54
|
+
if (node) {
|
|
55
|
+
end = node;
|
|
56
|
+
} else {
|
|
57
|
+
const proj = this.mapMatching.getProjection(_end, true, false, false, acceptEdgeFn);
|
|
58
|
+
if (proj && proj.nearestElement instanceof Node) {
|
|
59
|
+
end = proj.nearestElement;
|
|
60
|
+
} else if (proj && proj.nearestElement instanceof Edge) {
|
|
61
|
+
endNodeCreated = this.createNodeInsideEdge(
|
|
62
|
+
proj.nearestElement,
|
|
63
|
+
proj.projection
|
|
64
|
+
);
|
|
65
|
+
end = endNodeCreated;
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
if (!end) {
|
|
71
|
+
if (startNodeCreated) {
|
|
72
|
+
this.removeNodeFromPreviouslyCreatedEdge(startNodeCreated);
|
|
73
|
+
}
|
|
74
|
+
throw new Error('Cannot provide path because no end point has been found');
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
const path = this.getShortestPathInternal(start, end, acceptEdgeFn);
|
|
78
|
+
|
|
79
|
+
if (endNodeCreated) {
|
|
80
|
+
this.removeNodeFromPreviouslyCreatedEdge(endNodeCreated);
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
if (startNodeCreated) {
|
|
84
|
+
this.removeNodeFromPreviouslyCreatedEdge(startNodeCreated);
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
let route;
|
|
88
|
+
if (path && path.length !== 0) {
|
|
89
|
+
route = Itinerary.fromNetworkNodes(path, _start, _end);
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
return route;
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
createNodeInsideEdge(edge, point) {
|
|
96
|
+
const a = edge.node1;
|
|
97
|
+
const b = edge.node2;
|
|
98
|
+
|
|
99
|
+
const m = new Node(point);
|
|
100
|
+
m.coords.level = edge.level;
|
|
101
|
+
|
|
102
|
+
const u = new Edge(a, m);
|
|
103
|
+
u.level = edge.level;
|
|
104
|
+
u.data = edge.data;
|
|
105
|
+
|
|
106
|
+
const v = new Edge(b, m);
|
|
107
|
+
v.level = edge.level;
|
|
108
|
+
v.data = edge.data;
|
|
109
|
+
|
|
110
|
+
m.edges.push(u, v);
|
|
111
|
+
a.edges.push(u);
|
|
112
|
+
b.edges.push(v);
|
|
113
|
+
a.edges = a.edges.filter(_edge => _edge !== edge);
|
|
114
|
+
b.edges = b.edges.filter(_edge => _edge !== edge);
|
|
115
|
+
|
|
116
|
+
this.network.nodes.push(m);
|
|
117
|
+
this.network.edges.push(u, v);
|
|
118
|
+
|
|
119
|
+
this.network.edges = this.network.edges.filter(
|
|
120
|
+
_edge => _edge !== edge
|
|
121
|
+
);
|
|
122
|
+
|
|
123
|
+
return m;
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
removeNodeFromPreviouslyCreatedEdge(_node) {
|
|
127
|
+
const u = _node.edges[0];
|
|
128
|
+
const v = _node.edges[1];
|
|
129
|
+
|
|
130
|
+
u.node1.edges = u.node1.edges.filter(edge => edge !== u);
|
|
131
|
+
v.node1.edges = v.node1.edges.filter(edge => edge !== v);
|
|
132
|
+
|
|
133
|
+
const oldEdge = new Edge(u.node1, v.node1);
|
|
134
|
+
oldEdge.data = u.data;
|
|
135
|
+
this.network.edges.push(oldEdge);
|
|
136
|
+
u.node1.edges.push(oldEdge);
|
|
137
|
+
v.node1.edges.push(oldEdge);
|
|
138
|
+
|
|
139
|
+
this.network.nodes = this.network.nodes.filter(node => node !== _node);
|
|
140
|
+
this.network.edges = this.network.edges.filter(
|
|
141
|
+
edge => edge !== u && edge !== v
|
|
142
|
+
);
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
getShortestPathInternal(start, end, acceptEdgeFn) {
|
|
146
|
+
const distanceMap = {},
|
|
147
|
+
checking = {},
|
|
148
|
+
vertexList = {},
|
|
149
|
+
vertexNodes = {},
|
|
150
|
+
parentVertices = {},
|
|
151
|
+
path = [];
|
|
152
|
+
let vertexId = 0;
|
|
153
|
+
|
|
154
|
+
// Initially, we assume each vertex is unreachable
|
|
155
|
+
this.network.nodes.forEach(vertex => {
|
|
156
|
+
|
|
157
|
+
// Generate Unique Router Id
|
|
158
|
+
vertex.uniqueRouterId = vertexId;
|
|
159
|
+
vertexNodes[vertexId] = vertex;
|
|
160
|
+
|
|
161
|
+
distanceMap[vertexId] = Infinity;
|
|
162
|
+
checking[vertexId] = null;
|
|
163
|
+
vertexList[vertexId] = true;
|
|
164
|
+
|
|
165
|
+
vertexId++;
|
|
166
|
+
});
|
|
167
|
+
|
|
168
|
+
// The cost from the starting vertex to the starting vertex is 0
|
|
169
|
+
distanceMap[start.uniqueRouterId] = 0;
|
|
170
|
+
|
|
171
|
+
// check each vertex
|
|
172
|
+
while (Object.keys(vertexList).length > 0) {
|
|
173
|
+
const current = Number(
|
|
174
|
+
Object.keys(vertexList).reduce((_checking, vertex) => {
|
|
175
|
+
return distanceMap[_checking] > distanceMap[vertex]
|
|
176
|
+
? vertex
|
|
177
|
+
: _checking;
|
|
178
|
+
}, Object.keys(vertexList)[0])
|
|
179
|
+
);
|
|
180
|
+
|
|
181
|
+
// all the vertices accessible from current vertex
|
|
182
|
+
this.network.edges
|
|
183
|
+
.filter(edge => {
|
|
184
|
+
|
|
185
|
+
if (!acceptEdgeFn(edge)) {
|
|
186
|
+
return false;
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
const from = edge.node1.uniqueRouterId,
|
|
190
|
+
to = edge.node2.uniqueRouterId;
|
|
191
|
+
// are these vertices joined?
|
|
192
|
+
return from === current || to === current;
|
|
193
|
+
})
|
|
194
|
+
// for each vertex we can reach
|
|
195
|
+
.forEach(edge => {
|
|
196
|
+
let to, from;
|
|
197
|
+
// determine the direction of travel
|
|
198
|
+
if (edge.node1.uniqueRouterId === current) {
|
|
199
|
+
to = edge.node2.uniqueRouterId;
|
|
200
|
+
from = edge.node1.uniqueRouterId;
|
|
201
|
+
} else {
|
|
202
|
+
to = edge.node1.uniqueRouterId;
|
|
203
|
+
from = edge.node2.uniqueRouterId;
|
|
204
|
+
}
|
|
205
|
+
// distance is how far we travelled to reach the
|
|
206
|
+
// current vertex, plus cost of travel the next(to)
|
|
207
|
+
const distance = distanceMap[current] + edge.length;
|
|
208
|
+
|
|
209
|
+
// if we have found a cheaper path
|
|
210
|
+
// update the hash of costs
|
|
211
|
+
// and record which vertex we came from
|
|
212
|
+
if (distanceMap[to] > distance) {
|
|
213
|
+
distanceMap[to] = distance;
|
|
214
|
+
checking[to] = current;
|
|
215
|
+
parentVertices[to] = from;
|
|
216
|
+
}
|
|
217
|
+
});
|
|
218
|
+
|
|
219
|
+
// remove vertex so we don't revisit it
|
|
220
|
+
delete vertexList[current];
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
// now we have the most efficient paths for all vertices
|
|
224
|
+
// build the path for the user specified vertex(end)
|
|
225
|
+
let endId = end.uniqueRouterId;
|
|
226
|
+
while (parentVertices[endId]) {
|
|
227
|
+
path.unshift(vertexNodes[endId]);
|
|
228
|
+
endId = parentVertices[endId];
|
|
229
|
+
}
|
|
230
|
+
if (path.length !== 0) {
|
|
231
|
+
path.unshift(start);
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
// Remove Unique Router Id
|
|
235
|
+
this.network.nodes.forEach(vertex => {
|
|
236
|
+
delete vertex.uniqueRouterId;
|
|
237
|
+
});
|
|
238
|
+
|
|
239
|
+
return path;
|
|
240
|
+
}
|
|
241
|
+
}
|
|
242
|
+
export default GraphRouter;
|
package/src/graph/Itinerary.js
CHANGED
|
@@ -1,36 +1,133 @@
|
|
|
1
|
-
|
|
1
|
+
|
|
2
|
+
import { WGS84 } from '@wemap/geo';
|
|
3
|
+
|
|
2
4
|
import Network from './Network';
|
|
5
|
+
import Edge from './Edge';
|
|
6
|
+
import Node from './Node';
|
|
3
7
|
|
|
4
|
-
|
|
8
|
+
/**
|
|
9
|
+
* Main attributes are:
|
|
10
|
+
* nodes: the ordered list of Node
|
|
11
|
+
* edges: the ordered list of Edge
|
|
12
|
+
* start: the start point (WGS84)
|
|
13
|
+
* end: the end point (WGS84)
|
|
14
|
+
* length: the route length
|
|
15
|
+
*/
|
|
5
16
|
class Itinerary extends Network {
|
|
6
17
|
|
|
7
|
-
constructor(
|
|
8
|
-
super(
|
|
18
|
+
constructor() {
|
|
19
|
+
super();
|
|
20
|
+
this.start = null;
|
|
21
|
+
this.end = null;
|
|
22
|
+
this.length = -1;
|
|
9
23
|
}
|
|
10
24
|
|
|
11
|
-
|
|
12
|
-
|
|
25
|
+
/**
|
|
26
|
+
* Get route duration
|
|
27
|
+
* @param {Number} speed in km/h
|
|
28
|
+
*/
|
|
29
|
+
getDuration(speed = 4) {
|
|
30
|
+
return (speed * 1000 / 3600) * this.length;
|
|
13
31
|
}
|
|
14
32
|
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
33
|
+
/**
|
|
34
|
+
* Get edge at given distance
|
|
35
|
+
* @param {Number} distance
|
|
36
|
+
* @returns {Edge} the edge or null if not exists
|
|
37
|
+
*/
|
|
38
|
+
getEdgeAt(distance) {
|
|
39
|
+
|
|
40
|
+
if (distance === 0 && this.edges.length > 0) {
|
|
41
|
+
return this.edges[0];
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
let sum = 0;
|
|
45
|
+
for (let i = 0; i <= this.edges.length; i++) {
|
|
46
|
+
sum += this.edges[i].length;
|
|
47
|
+
if (distance <= sum) {
|
|
48
|
+
return this.edges[i];
|
|
49
|
+
}
|
|
18
50
|
}
|
|
19
|
-
return
|
|
51
|
+
return null;
|
|
20
52
|
}
|
|
21
53
|
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
54
|
+
|
|
55
|
+
static fromNetworkNodes(nodes, start, end) {
|
|
56
|
+
|
|
57
|
+
/**
|
|
58
|
+
* Itinerary part of network is cloned and ordered
|
|
59
|
+
*/
|
|
60
|
+
|
|
61
|
+
const itinerary = new Itinerary();
|
|
62
|
+
itinerary.length = 0;
|
|
63
|
+
itinerary.start = start;
|
|
64
|
+
itinerary.end = end;
|
|
65
|
+
|
|
66
|
+
let nextItineraryNode;
|
|
67
|
+
|
|
68
|
+
for (let i = 0; i < nodes.length; i++) {
|
|
69
|
+
|
|
70
|
+
const networkNode = nodes[i];
|
|
71
|
+
const itineraryNode = nextItineraryNode ? nextItineraryNode : networkNode.clone();
|
|
72
|
+
|
|
73
|
+
itinerary.nodes.push(itineraryNode);
|
|
74
|
+
|
|
75
|
+
if (i === nodes.length - 1) {
|
|
76
|
+
break;
|
|
77
|
+
}
|
|
78
|
+
const nextNetworkNode = nodes[i + 1];
|
|
79
|
+
nextItineraryNode = nextNetworkNode.clone();
|
|
80
|
+
|
|
81
|
+
const networkEdge = Network.getEdgeByNodes(networkNode.edges, networkNode, nextNetworkNode);
|
|
82
|
+
const itineraryEdge = networkEdge.clone(itineraryNode, nextItineraryNode);
|
|
83
|
+
itineraryNode.edges.push(itineraryEdge);
|
|
84
|
+
nextItineraryNode.edges.push(itineraryEdge);
|
|
85
|
+
|
|
86
|
+
itinerary.edges.push(itineraryEdge);
|
|
87
|
+
itinerary.length += itineraryEdge.length;
|
|
25
88
|
}
|
|
26
|
-
|
|
89
|
+
|
|
90
|
+
return itinerary;
|
|
27
91
|
}
|
|
28
92
|
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
93
|
+
/**
|
|
94
|
+
* Convert lng/lat points to Itinerary
|
|
95
|
+
* @param {2DArray} points 2D points array of lng/lat (or lat/lng see lngLatFormat)
|
|
96
|
+
* @param {Boolean} lngLatFormat true: lng/lat (default), false: lat/lng
|
|
97
|
+
*/
|
|
98
|
+
static fromOrderedPointsArray(points, start, end, lngLatFormat) {
|
|
99
|
+
const route = new Itinerary();
|
|
100
|
+
route.length = 0;
|
|
101
|
+
route.start = new WGS84(start[lngLatFormat ? 1 : 0],
|
|
102
|
+
start[lngLatFormat ? 0 : 1]);
|
|
103
|
+
route.end = new WGS84(end[lngLatFormat ? 1 : 0],
|
|
104
|
+
end[lngLatFormat ? 0 : 1]);
|
|
105
|
+
|
|
106
|
+
let lastPoint = null;
|
|
107
|
+
|
|
108
|
+
for (let i = 0; i < points.length; i++) {
|
|
109
|
+
|
|
110
|
+
const currentPoint = new Node(
|
|
111
|
+
new WGS84(points[i][lngLatFormat ? 1 : 0],
|
|
112
|
+
points[i][lngLatFormat ? 0 : 1])
|
|
113
|
+
);
|
|
114
|
+
|
|
115
|
+
if (lastPoint) {
|
|
116
|
+
const edge = new Edge(lastPoint, currentPoint);
|
|
117
|
+
|
|
118
|
+
route.edges.push(edge);
|
|
119
|
+
route.length += edge.length;
|
|
120
|
+
|
|
121
|
+
currentPoint.edges.push(edge);
|
|
122
|
+
lastPoint.edges.push(edge);
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
route.nodes.push(currentPoint);
|
|
126
|
+
lastPoint = currentPoint;
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
return route;
|
|
33
130
|
}
|
|
34
|
-
}
|
|
35
131
|
|
|
132
|
+
}
|
|
36
133
|
export default Itinerary;
|
package/src/graph/MapMatching.js
CHANGED
|
@@ -1,10 +1,10 @@
|
|
|
1
|
-
/* eslint
|
|
2
|
-
/* eslint
|
|
1
|
+
/* eslint-disable max-statements */
|
|
2
|
+
/* eslint-disable complexity */
|
|
3
|
+
import isNumber from 'lodash/isnumber';
|
|
3
4
|
|
|
4
|
-
import {
|
|
5
|
-
import Network from './Network';
|
|
5
|
+
import { rad2deg } from '@wemap/maths';
|
|
6
6
|
|
|
7
|
-
|
|
7
|
+
import Network from './Network';
|
|
8
8
|
|
|
9
9
|
class MapMatching {
|
|
10
10
|
|
|
@@ -12,7 +12,7 @@ class MapMatching {
|
|
|
12
12
|
|
|
13
13
|
/**
|
|
14
14
|
* Constructor of Map-matching
|
|
15
|
-
* @param {Network} network
|
|
15
|
+
* @param {Network} network
|
|
16
16
|
*/
|
|
17
17
|
constructor(network) {
|
|
18
18
|
if (network) {
|
|
@@ -35,7 +35,7 @@ class MapMatching {
|
|
|
35
35
|
this.network = network;
|
|
36
36
|
}
|
|
37
37
|
|
|
38
|
-
getProjection(location, useDistance = true, useBearing = true) {
|
|
38
|
+
getProjection(location, useDistance = true, useBearing = true, useMultiLevelSegments = true, acceptEdgeFn = () => true) {
|
|
39
39
|
|
|
40
40
|
if (!this.network) {
|
|
41
41
|
return null;
|
|
@@ -56,29 +56,42 @@ class MapMatching {
|
|
|
56
56
|
for (let i = 0; i < this.network.edges.length; i++) {
|
|
57
57
|
const edge = this.network.edges[i];
|
|
58
58
|
|
|
59
|
+
if (!acceptEdgeFn(edge)) {
|
|
60
|
+
continue;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
if (!useMultiLevelSegments && edge.level && edge.level.isRange) {
|
|
64
|
+
continue;
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
if (location.level && !location.level.intersect(edge.level)
|
|
68
|
+
|| !location.level && edge.level) {
|
|
69
|
+
continue;
|
|
70
|
+
}
|
|
71
|
+
|
|
59
72
|
if (useBearing) {
|
|
60
|
-
const edgeBearing = rad2deg(edge.
|
|
73
|
+
const edgeBearing = rad2deg(edge.bearing);
|
|
61
74
|
const diffAngle = MapMatching.diffAngle90(edgeBearing, location.bearing);
|
|
62
75
|
if (diffAngle > this._maxAngleBearing) {
|
|
63
76
|
continue;
|
|
64
77
|
}
|
|
65
78
|
} else {
|
|
66
79
|
// Check node 1
|
|
67
|
-
const distNode1 = location.distanceTo(edge.node1);
|
|
80
|
+
const distNode1 = location.distanceTo(edge.node1.coords);
|
|
68
81
|
if (distNode1 < projection.distanceFromNearestElement
|
|
69
82
|
&& (!useDistance || distNode1 <= this._maxDistance)) {
|
|
70
83
|
projection.distanceFromNearestElement = distNode1;
|
|
71
84
|
projection.nearestElement = edge.node1;
|
|
72
|
-
projection.projection = edge.node1.clone();
|
|
85
|
+
projection.projection = edge.node1.coords.clone();
|
|
73
86
|
}
|
|
74
87
|
|
|
75
88
|
// Check node 2
|
|
76
|
-
const distNode2 = location.distanceTo(edge.node2);
|
|
89
|
+
const distNode2 = location.distanceTo(edge.node2.coords);
|
|
77
90
|
if (distNode2 < projection.distanceFromNearestElement
|
|
78
91
|
&& (!useDistance || distNode2 <= this._maxDistance)) {
|
|
79
92
|
projection.distanceFromNearestElement = distNode2;
|
|
80
93
|
projection.nearestElement = edge.node2;
|
|
81
|
-
projection.projection = edge.node2.clone();
|
|
94
|
+
projection.projection = edge.node2.coords.clone();
|
|
82
95
|
}
|
|
83
96
|
}
|
|
84
97
|
|
|
@@ -97,7 +110,9 @@ class MapMatching {
|
|
|
97
110
|
|
|
98
111
|
if (!projection.projection) {
|
|
99
112
|
return null;
|
|
100
|
-
}
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
if (!isNumber(projection.projection.alt)) {
|
|
101
116
|
projection.projection.alt = location.alt;
|
|
102
117
|
}
|
|
103
118
|
|
|
@@ -1,27 +1,52 @@
|
|
|
1
1
|
import chai from 'chai';
|
|
2
2
|
import chaiAlmost from 'chai-almost';
|
|
3
3
|
|
|
4
|
+
import {
|
|
5
|
+
WGS84, WGS84UserPosition
|
|
6
|
+
} from '@wemap/geo';
|
|
7
|
+
|
|
4
8
|
import Edge from './Edge';
|
|
5
|
-
import
|
|
9
|
+
import Node from './Node';
|
|
6
10
|
import Network from './Network';
|
|
7
|
-
import
|
|
8
|
-
import WGS84UserPosition from '../coordinates/WGS84UserPosition';
|
|
11
|
+
import MapMatching from './MapMatching';
|
|
9
12
|
|
|
10
13
|
const expect = chai.expect;
|
|
11
14
|
chai.use(chaiAlmost());
|
|
12
15
|
|
|
16
|
+
|
|
17
|
+
function edgeFromTwoNodes(node1, node2) {
|
|
18
|
+
if (!(node1 instanceof Node)) {
|
|
19
|
+
throw new TypeError('node1 is not an instance of Node');
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
if (!(node2 instanceof Node)) {
|
|
23
|
+
throw new TypeError('node2 is not an instance of Node');
|
|
24
|
+
}
|
|
25
|
+
const edge = new Edge(node1, node2);
|
|
26
|
+
node1.edges.push(edge);
|
|
27
|
+
node2.edges.push(edge);
|
|
28
|
+
return edge;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
function createNetworkFromEdgesAndNodes(edges, nodes) {
|
|
32
|
+
const network = new Network();
|
|
33
|
+
network.edges = edges;
|
|
34
|
+
network.nodes = nodes;
|
|
35
|
+
return network;
|
|
36
|
+
}
|
|
37
|
+
|
|
13
38
|
describe('matching', () => {
|
|
14
39
|
it('Should return the good value', () => {
|
|
15
40
|
|
|
16
|
-
const
|
|
17
|
-
const
|
|
18
|
-
const
|
|
19
|
-
const
|
|
20
|
-
const e1 =
|
|
21
|
-
const e2 =
|
|
22
|
-
const e3 =
|
|
23
|
-
const e4 =
|
|
24
|
-
const network =
|
|
41
|
+
const n1 = new Node(new WGS84(43.6091883, 3.8841242));
|
|
42
|
+
const n2 = new Node(new WGS84(43.6091709, 3.8842382));
|
|
43
|
+
const n3 = new Node(new WGS84(43.6091288, 3.884226));
|
|
44
|
+
const n4 = new Node(new WGS84(43.6091461, 3.884112));
|
|
45
|
+
const e1 = edgeFromTwoNodes(n1, n2);
|
|
46
|
+
const e2 = edgeFromTwoNodes(n2, n3);
|
|
47
|
+
const e3 = edgeFromTwoNodes(n3, n4);
|
|
48
|
+
const e4 = edgeFromTwoNodes(n4, n1);
|
|
49
|
+
const network = createNetworkFromEdgesAndNodes([e1, e2, e3, e4], [n1, n2, n3, n4]);
|
|
25
50
|
|
|
26
51
|
const mapMatching = new MapMatching(network);
|
|
27
52
|
mapMatching.maxAngleBearing = 30;
|
package/src/graph/Network.js
CHANGED
|
@@ -1,33 +1,26 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
1
|
+
/**
|
|
2
|
+
* A typical network with nodes (Node) and edges (Edge)
|
|
3
|
+
*/
|
|
3
4
|
class Network {
|
|
4
5
|
|
|
5
|
-
constructor(
|
|
6
|
-
this.
|
|
6
|
+
constructor() {
|
|
7
|
+
this.nodes = [];
|
|
8
|
+
this.edges = [];
|
|
7
9
|
}
|
|
8
10
|
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
if (!this.edges) {
|
|
12
|
-
this.edges = [];
|
|
13
|
-
}
|
|
14
|
-
this.edges = this.edges.concat(edges);
|
|
11
|
+
getNodeByCoords(coords) {
|
|
12
|
+
return this.nodes.find(node => node.coords.equalsTo(coords));
|
|
15
13
|
}
|
|
16
14
|
|
|
17
|
-
|
|
18
|
-
return this.edges
|
|
15
|
+
getEdgeByNodes(node1, node2) {
|
|
16
|
+
return Network.getEdgeByNodes(this.edges, node1, node2);
|
|
19
17
|
}
|
|
20
18
|
|
|
21
|
-
static
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
edges.forEach(edge => {
|
|
27
|
-
if (!(edge instanceof Edge)) {
|
|
28
|
-
throw new TypeError('Network creation: ' + edge + ' is not an Edge');
|
|
29
|
-
}
|
|
30
|
-
});
|
|
19
|
+
static getEdgeByNodes(edges, node1, node2) {
|
|
20
|
+
return edges.find(edge =>
|
|
21
|
+
node1 === edge.node1 && node2 === edge.node2
|
|
22
|
+
|| node2 === edge.node1 && node1 === edge.node2
|
|
23
|
+
);
|
|
31
24
|
}
|
|
32
25
|
}
|
|
33
26
|
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
class Node {
|
|
2
|
+
|
|
3
|
+
constructor(coords, data) {
|
|
4
|
+
this.coords = coords;
|
|
5
|
+
this.data = data;
|
|
6
|
+
this.edges = [];
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
distanceTo(other) {
|
|
10
|
+
return this.coords.distanceTo(other.coords);
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
bearingTo(other) {
|
|
14
|
+
return this.coords.bearingTo(other.coords);
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
equalsTo(other) {
|
|
18
|
+
return this.coords.equalsTo(other.coords);
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
clone() {
|
|
22
|
+
return new Node(this.coords, this.data);
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
export default Node;
|