signalk-racer 1.1.0 → 1.1.2
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/.claude/settings.local.json +11 -0
- package/README.md +38 -1
- package/ReleaseNotes.md +10 -0
- package/index.js +50 -144
- package/package.json +7 -1
- package/public/index.html +1 -0
- package/racer.js +183 -0
- package/tests/racer-maths.test.js +36 -0
- package/tests/racer-ttl.test.js +103 -0
- package/tests/racer-vmg.test.js +168 -0
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
{
|
|
2
|
+
"permissions": {
|
|
3
|
+
"allow": [
|
|
4
|
+
"Bash(npm test:*)",
|
|
5
|
+
"Bash(git -C /home/gregw/src/signalk-racer tag -l)",
|
|
6
|
+
"Bash(git -C /home/gregw/src/signalk-racer log --oneline -5)",
|
|
7
|
+
"Bash(git -C /home/gregw/src/signalk-racer status -sb)",
|
|
8
|
+
"Bash(git -C /home/gregw/src/signalk-racer remote -v)"
|
|
9
|
+
]
|
|
10
|
+
}
|
|
11
|
+
}
|
package/README.md
CHANGED
|
@@ -189,4 +189,41 @@ npm install
|
|
|
189
189
|
npm link
|
|
190
190
|
cd ~/.signalk
|
|
191
191
|
npm link signalk-racer
|
|
192
|
-
```
|
|
192
|
+
```
|
|
193
|
+
|
|
194
|
+
---
|
|
195
|
+
## 🚀 Releasing
|
|
196
|
+
|
|
197
|
+
Releases are cut manually from a clean `main` checkout and published to npm.
|
|
198
|
+
|
|
199
|
+
### Pre-flight
|
|
200
|
+
|
|
201
|
+
1. Confirm `main` is up to date and clean: `git checkout main && git pull && git status`.
|
|
202
|
+
2. Run the test suite: `npm test`.
|
|
203
|
+
3. Review commits since the last release: `git log $(git describe --tags --abbrev=0)..HEAD --oneline`.
|
|
204
|
+
4. Decide the version bump (semver): `patch` for bug fixes, `minor` for features, `major` for breaking changes.
|
|
205
|
+
|
|
206
|
+
### Cut the release
|
|
207
|
+
|
|
208
|
+
1. Update `ReleaseNotes.md` — add a new entry at the top with the new version and a short bullet list of changes, then `git add ReleaseNotes.md`.
|
|
209
|
+
2. Bump the version and create the git tag in one step (note the non-default tag prefix so it matches the existing `signalk-racer-X.Y.Z` convention):
|
|
210
|
+
```
|
|
211
|
+
npm version <patch|minor|major> --tag-version-prefix=signalk-racer-
|
|
212
|
+
```
|
|
213
|
+
This rewrites `package.json`, includes any staged files (e.g. `ReleaseNotes.md`) in the bump commit, and creates tag `signalk-racer-X.Y.Z`.
|
|
214
|
+
3. Push the commit and the tag:
|
|
215
|
+
```
|
|
216
|
+
git push origin main
|
|
217
|
+
git push origin signalk-racer-X.Y.Z
|
|
218
|
+
```
|
|
219
|
+
|
|
220
|
+
### Publish to npm
|
|
221
|
+
|
|
222
|
+
1. Confirm npm auth: `npm whoami` (login with `npm login` if needed).
|
|
223
|
+
2. Dry-run the publish to inspect the tarball contents: `npm publish --dry-run`.
|
|
224
|
+
3. Publish: `npm publish` (the package is public and unscoped; no `--access` flag needed).
|
|
225
|
+
|
|
226
|
+
### After release
|
|
227
|
+
|
|
228
|
+
1. Verify on npm: `npm view signalk-racer version` should report the new version.
|
|
229
|
+
2. (Optional) Create a GitHub release from the new tag with notes copied from `ReleaseNotes.md`.
|
package/ReleaseNotes.md
CHANGED
|
@@ -1,3 +1,13 @@
|
|
|
1
|
+
## 1.1.2 TTL fixes
|
|
2
|
+
+ use a FIFO queue for VMGs towards the line.
|
|
3
|
+
+ avoid undefined values in deltas
|
|
4
|
+
|
|
5
|
+
## 1.1.1 Unit tests
|
|
6
|
+
+ split into index.js and racing.js for unit testing
|
|
7
|
+
+ unit tested TTL
|
|
8
|
+
+ allows negative TTB
|
|
9
|
+
+ added configurations for VMG calculations
|
|
10
|
+
|
|
1
11
|
## 1.1.0 Named lines
|
|
2
12
|
+ Added Named lines support
|
|
3
13
|
+ Refactored distance to line calculations
|
package/index.js
CHANGED
|
@@ -2,11 +2,6 @@ module.exports = (app) => {
|
|
|
2
2
|
const state = {
|
|
3
3
|
startLineName: null,
|
|
4
4
|
startLine: null,
|
|
5
|
-
|
|
6
|
-
vmgToLinePos: [], // VMG towards the line (not OCS to line)
|
|
7
|
-
vmgToLineNeg: [], // VMG away from the line (OCS to line)
|
|
8
|
-
vmgToZonePort: [], // Motion along the line towards the PORT end (pin)
|
|
9
|
-
vmgToZoneStb: [], // Motion along the line towards the STARBOARD end (boat)
|
|
10
5
|
};
|
|
11
6
|
const geolib = require('geolib')
|
|
12
7
|
const racerSchema = {
|
|
@@ -43,7 +38,26 @@ module.exports = (app) => {
|
|
|
43
38
|
title: 'Start Timer default initial period in seconds',
|
|
44
39
|
default: 300
|
|
45
40
|
},
|
|
46
|
-
|
|
41
|
+
maxSamples : {
|
|
42
|
+
type: 'number',
|
|
43
|
+
title: 'Number of samples to collect for VMG calculations',
|
|
44
|
+
default: 600
|
|
45
|
+
},
|
|
46
|
+
percentile: {
|
|
47
|
+
type: 'number',
|
|
48
|
+
title: 'Percentile used to select sample for VMG calculations (0-1)',
|
|
49
|
+
default: 0.9
|
|
50
|
+
},
|
|
51
|
+
minSog: {
|
|
52
|
+
type: 'number',
|
|
53
|
+
title: 'Minimum speed over ground (SOG) in knots to consider for VMG calculations',
|
|
54
|
+
default: 1.0
|
|
55
|
+
},
|
|
56
|
+
maxDistance: {
|
|
57
|
+
type: 'number',
|
|
58
|
+
title: 'Maximum distance to line and/or line zone in meters to consider for VMG calculations',
|
|
59
|
+
default: 2000
|
|
60
|
+
},
|
|
47
61
|
lines: {
|
|
48
62
|
type: 'array',
|
|
49
63
|
title: 'Named lines',
|
|
@@ -84,7 +98,16 @@ module.exports = (app) => {
|
|
|
84
98
|
}
|
|
85
99
|
}
|
|
86
100
|
}
|
|
87
|
-
const { v4: uuidv4 } = require('uuid')
|
|
101
|
+
const { v4: uuidv4 } = require('uuid');
|
|
102
|
+
const {
|
|
103
|
+
initRacer,
|
|
104
|
+
toDegrees,
|
|
105
|
+
toRadians,
|
|
106
|
+
collectVmgSamples,
|
|
107
|
+
resetVmgSamples,
|
|
108
|
+
computeTimeToLine
|
|
109
|
+
} = require('./racer');
|
|
110
|
+
|
|
88
111
|
const unsubscribes = [];
|
|
89
112
|
|
|
90
113
|
// Update meta data for non standard paths
|
|
@@ -173,19 +196,22 @@ module.exports = (app) => {
|
|
|
173
196
|
});
|
|
174
197
|
|
|
175
198
|
// send multiple deltas, each in the form of { path, value }
|
|
199
|
+
// null values are preserved (meaningful in SignalK for clearing a value);
|
|
200
|
+
// undefined values are dropped to avoid "Delta is missing value" warnings.
|
|
176
201
|
function sendDeltas(updatesArray) {
|
|
177
|
-
const
|
|
202
|
+
const values = updatesArray
|
|
203
|
+
.filter(({value}) => value !== undefined)
|
|
204
|
+
.map(({path, value}) => ({path, value}));
|
|
205
|
+
|
|
206
|
+
if (values.length === 0) return;
|
|
178
207
|
|
|
179
208
|
const delta = {
|
|
180
209
|
context: 'vessels.self',
|
|
181
210
|
updates: [
|
|
182
211
|
{
|
|
183
212
|
source: {label: 'signalk-racer'},
|
|
184
|
-
timestamp:
|
|
185
|
-
values
|
|
186
|
-
path,
|
|
187
|
-
value
|
|
188
|
-
}))
|
|
213
|
+
timestamp: new Date().toISOString(),
|
|
214
|
+
values
|
|
189
215
|
}
|
|
190
216
|
]
|
|
191
217
|
};
|
|
@@ -210,16 +236,6 @@ module.exports = (app) => {
|
|
|
210
236
|
}
|
|
211
237
|
}
|
|
212
238
|
|
|
213
|
-
function toDegrees(rad) {
|
|
214
|
-
if (rad === null || rad === undefined) return null;
|
|
215
|
-
return rad * (180 / Math.PI);
|
|
216
|
-
}
|
|
217
|
-
|
|
218
|
-
function toRadians(degrees) {
|
|
219
|
-
if (degrees === null || degrees === undefined) return null;
|
|
220
|
-
return degrees * (Math.PI / 180);
|
|
221
|
-
}
|
|
222
|
-
|
|
223
239
|
function toOtherEnd(end) {
|
|
224
240
|
switch (end) {
|
|
225
241
|
case 'port' :
|
|
@@ -672,10 +688,7 @@ module.exports = (app) => {
|
|
|
672
688
|
|
|
673
689
|
app.debug(`STARTLINE: startLinePort: ${JSON.stringify(startLine)}`);
|
|
674
690
|
state.startLine = startLine;
|
|
675
|
-
|
|
676
|
-
state.vmgToLinePos = [];
|
|
677
|
-
state.vmgToZoneStb = [];
|
|
678
|
-
state.vmgToZonePort = [];
|
|
691
|
+
resetVmgSamples();
|
|
679
692
|
deltas = [
|
|
680
693
|
{path: 'navigation.racing.startLinePort', value: startLine.port},
|
|
681
694
|
{path: 'navigation.racing.startLineStb', value: startLine.stb},
|
|
@@ -802,8 +815,8 @@ module.exports = (app) => {
|
|
|
802
815
|
if (cog != null && sog != null) {
|
|
803
816
|
collectVmgSamples(cog, sog, startLine.bearing, toZoneVz, perpToLineVx);
|
|
804
817
|
}
|
|
805
|
-
const ttl = computeTimeToLine(cog, sog, startLine.bearing, toZoneVz, perpToLineVx, ocs, closestEnd);
|
|
806
|
-
const ttb = !ocs && state.timerRunning && state.timeToStart > 0 ?
|
|
818
|
+
const ttl = computeTimeToLine(cog, sog, startLine.bearing, toZoneVz, perpToLineVx, ocs, closestEnd, state.timeToStart ?? 0);
|
|
819
|
+
const ttb = !ocs && state.timerRunning && state.timeToStart > 0 && ttl != null ? (state.timeToStart - ttl) : null;
|
|
807
820
|
|
|
808
821
|
sendDeltas([
|
|
809
822
|
{path: 'navigation.racing.distanceStartline', value: distanceToLine},
|
|
@@ -822,121 +835,6 @@ module.exports = (app) => {
|
|
|
822
835
|
}
|
|
823
836
|
}
|
|
824
837
|
|
|
825
|
-
function collectVmgSamples(cog, sog, lineBearing, toZoneVz, perpToLineVx) {
|
|
826
|
-
try {
|
|
827
|
-
// TODO configure these rejection values
|
|
828
|
-
if (sog < 1.0) return; // ignore drifting / tacking stalls
|
|
829
|
-
if (toZoneVz > 1000 || perpToLineVx > 1000) return; // to far away from the line
|
|
830
|
-
|
|
831
|
-
// Compute angle boatDir-relative to line bearing
|
|
832
|
-
let angleRad = cog - toRadians(lineBearing);
|
|
833
|
-
|
|
834
|
-
// VMG components
|
|
835
|
-
const vmgNormal = sog * Math.sin(angleRad); // +ve = towards line, -ve = away
|
|
836
|
-
const vmgTangent = sog * Math.cos(angleRad); // +ve = towards pin, -ve = towards stb
|
|
837
|
-
|
|
838
|
-
// Insert vmg arrays
|
|
839
|
-
insertSample(state.vmgToLinePos, vmgNormal);
|
|
840
|
-
insertSample(state.vmgToLineNeg, -vmgNormal);
|
|
841
|
-
insertSample(state.vmgToZonePort, vmgTangent);
|
|
842
|
-
insertSample(state.vmgToZoneStb, -vmgTangent);
|
|
843
|
-
} catch (err) {
|
|
844
|
-
app.error(err);
|
|
845
|
-
}
|
|
846
|
-
}
|
|
847
|
-
|
|
848
|
-
function insertSample(arr, value) {
|
|
849
|
-
if (value <= 1.0) return; // reject low VMGs
|
|
850
|
-
if (arr.length >= 600) arr.shift(); // Keep up to 10 minutes at 1Hz
|
|
851
|
-
|
|
852
|
-
let lo = 0, hi = arr.length;
|
|
853
|
-
while (lo < hi) {
|
|
854
|
-
const mid = (lo + hi) >> 1;
|
|
855
|
-
if (arr[mid] < value) lo = mid + 1;
|
|
856
|
-
else hi = mid;
|
|
857
|
-
}
|
|
858
|
-
arr.splice(lo, 0, value);
|
|
859
|
-
}
|
|
860
|
-
|
|
861
|
-
function computeTimeToLine(cog, sog, lineBearing, toZoneVz, perpToLineVx, ocs, closestEnd) {
|
|
862
|
-
|
|
863
|
-
let vmgNormalSigned = 0;
|
|
864
|
-
let vmgTangentSigned = 0;
|
|
865
|
-
|
|
866
|
-
if (cog != null && sog != null) {
|
|
867
|
-
const lineBearingRad = toRadians(lineBearing);
|
|
868
|
-
if (lineBearingRad === null) {
|
|
869
|
-
return 0;
|
|
870
|
-
}
|
|
871
|
-
|
|
872
|
-
// Angle between boat COG and line bearing
|
|
873
|
-
const angleRad = cog - lineBearingRad;
|
|
874
|
-
|
|
875
|
-
// Signed VMG components from *current* COG/SOG
|
|
876
|
-
vmgNormalSigned = sog * Math.sin(angleRad); // normal to line
|
|
877
|
-
vmgTangentSigned = sog * Math.cos(angleRad); // along line (stb->port is +)
|
|
878
|
-
}
|
|
879
|
-
|
|
880
|
-
// 1. Effective VMG normal (perpendicular to line)
|
|
881
|
-
//
|
|
882
|
-
// History:
|
|
883
|
-
// - if OCS, use vmgToLineNeg (away from line => back towards line in this case)
|
|
884
|
-
// - otherwise, use vmgToLinePos
|
|
885
|
-
const histNormal = percentileFromSorted(ocs ? state.vmgToLineNeg : state.vmgToLinePos, 0.9);
|
|
886
|
-
|
|
887
|
-
// Instantaneous VMG in the *required* direction
|
|
888
|
-
// if we are OCS, then the required direction is the opposite of the effective direction
|
|
889
|
-
let vmgInstNormal = ocs ? -vmgNormalSigned : vmgNormalSigned;
|
|
890
|
-
const vmgEffNormal = Math.max(histNormal || 0, vmgInstNormal || 0);
|
|
891
|
-
|
|
892
|
-
// 2. Effective VMG along the line (towards the chosen zone entry)
|
|
893
|
-
let vmgHistParallel = 0;
|
|
894
|
-
let vmgInstParallel = 0;
|
|
895
|
-
|
|
896
|
-
if (toZoneVz > 0) {
|
|
897
|
-
if (closestEnd === 'port') {
|
|
898
|
-
// Coming from the pin end, we will sail from PORT towards STB
|
|
899
|
-
// => use STB-direction samples.
|
|
900
|
-
vmgHistParallel = percentileFromSorted(state.vmgToZoneStb, 0.9);
|
|
901
|
-
if (vmgTangentSigned < 0) {
|
|
902
|
-
vmgInstParallel = -vmgTangentSigned; // towards stb
|
|
903
|
-
}
|
|
904
|
-
} else {
|
|
905
|
-
// Coming from the boat end, we will sail from STB towards PORT
|
|
906
|
-
// => use PORT-direction samples.
|
|
907
|
-
vmgHistParallel = percentileFromSorted(state.vmgToZonePort, 0.9);
|
|
908
|
-
if (vmgTangentSigned > 0) {
|
|
909
|
-
vmgInstParallel = vmgTangentSigned; // towards port
|
|
910
|
-
}
|
|
911
|
-
}
|
|
912
|
-
}
|
|
913
|
-
|
|
914
|
-
const vmgEffectParallel = Math.max(vmgHistParallel || 0, vmgInstParallel || 0);
|
|
915
|
-
|
|
916
|
-
// 3. Combine legs: along to zone, then perpendicular to line
|
|
917
|
-
let ttl = 0;
|
|
918
|
-
|
|
919
|
-
// Outside the start zone: first go along the line (or zone boundary)
|
|
920
|
-
if (toZoneVz > 0 && vmgEffectParallel > 0) {
|
|
921
|
-
ttl += toZoneVz / vmgEffectParallel;
|
|
922
|
-
}
|
|
923
|
-
|
|
924
|
-
// Then go perpendicular to the line to actually hit it
|
|
925
|
-
if (perpToLineVx > 0 && vmgEffNormal > 0) {
|
|
926
|
-
ttl += perpToLineVx / vmgEffNormal;
|
|
927
|
-
}
|
|
928
|
-
|
|
929
|
-
app.debug(`computeTimeToLine: ${closestEnd} ${toZoneVz} ${perpToLineVx} ${vmgEffectParallel} ${vmgEffNormal} = ${ttl}`);
|
|
930
|
-
// If we still somehow have zero (no speed or distances), just return time to start so TTB logic doesn't explode.
|
|
931
|
-
return ttl <= 0 ? state.timeToStart : ttl;
|
|
932
|
-
}
|
|
933
|
-
|
|
934
|
-
function percentileFromSorted(arr, p) {
|
|
935
|
-
if (!arr || !arr.length) return 0;
|
|
936
|
-
const idx = Math.floor(p * (arr.length - 1));
|
|
937
|
-
return arr[idx];
|
|
938
|
-
}
|
|
939
|
-
|
|
940
838
|
function processWind(twd) {
|
|
941
839
|
if (!twd) {
|
|
942
840
|
twd = app.getSelfPath('environment.wind.directionTrue');
|
|
@@ -1073,6 +971,14 @@ module.exports = (app) => {
|
|
|
1073
971
|
app.error('Missing waypoint names: startLinePort and/or startLineStb not configured.');
|
|
1074
972
|
return;
|
|
1075
973
|
}
|
|
974
|
+
|
|
975
|
+
initRacer({
|
|
976
|
+
minSog: options.minSog ?? 1.0,
|
|
977
|
+
maxDistance: options.maxDistance ?? 2000,
|
|
978
|
+
maxSamples: options.maxSamples ?? 600,
|
|
979
|
+
percentile: options.percentile ?? 0.9
|
|
980
|
+
}, app);
|
|
981
|
+
|
|
1076
982
|
app.debug('startLinePort:' + options.startLinePort);
|
|
1077
983
|
app.debug('startLineStb:' + options.startLineStb);
|
|
1078
984
|
app.debug('app.selfId:' + app.selfId);
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "signalk-racer",
|
|
3
|
-
"version": "1.1.
|
|
3
|
+
"version": "1.1.2",
|
|
4
4
|
"description": "Signalk plugin to calculate values of interest to sail racers, such as: Time to Start; Time of Start, Time to Burn; Distance to Line; Next leg TWA.",
|
|
5
5
|
"main": "index.js",
|
|
6
6
|
"repository": {
|
|
@@ -26,5 +26,11 @@
|
|
|
26
26
|
"dependencies": {
|
|
27
27
|
"geolib": "^3.3.4",
|
|
28
28
|
"uuid": "^8.1.0"
|
|
29
|
+
},
|
|
30
|
+
"devDependencies": {
|
|
31
|
+
"jest": "^30.0.0"
|
|
32
|
+
},
|
|
33
|
+
"scripts": {
|
|
34
|
+
"test": "jest"
|
|
29
35
|
}
|
|
30
36
|
}
|
package/public/index.html
CHANGED
package/racer.js
ADDED
|
@@ -0,0 +1,183 @@
|
|
|
1
|
+
// lib/racerMath.js
|
|
2
|
+
'use strict';
|
|
3
|
+
|
|
4
|
+
let cfg = {
|
|
5
|
+
minSog: 1.0,
|
|
6
|
+
maxDistance: 2000,
|
|
7
|
+
maxSamples: 600,
|
|
8
|
+
percentile: 0.9
|
|
9
|
+
};
|
|
10
|
+
|
|
11
|
+
let log = {
|
|
12
|
+
debug: () => {
|
|
13
|
+
}
|
|
14
|
+
};
|
|
15
|
+
|
|
16
|
+
function initRacer(config = {}, logger = null) {
|
|
17
|
+
cfg = {...cfg, ...config};
|
|
18
|
+
if (logger?.debug) log = logger;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
// VMG sample state is kept private to this module.
|
|
22
|
+
// Each entry has a `sorted` array (ascending, for O(1) percentile) and a
|
|
23
|
+
// `queue` array (insertion-order FIFO, for correct oldest-first eviction).
|
|
24
|
+
const vmgState = {
|
|
25
|
+
vmgToLinePos: {sorted: [], queue: []}, // VMG towards the line (not OCS to line)
|
|
26
|
+
vmgToLineNeg: {sorted: [], queue: []}, // VMG away from the line (OCS to line)
|
|
27
|
+
vmgToZonePort: {sorted: [], queue: []}, // Motion along the line towards the PORT end (pin)
|
|
28
|
+
vmgToZoneStb: {sorted: [], queue: []}, // Motion along the line towards the STARBOARD end (boat)
|
|
29
|
+
};
|
|
30
|
+
|
|
31
|
+
function resetVmgSamples() {
|
|
32
|
+
for (const vmg of Object.values(vmgState)) {
|
|
33
|
+
vmg.sorted.length = 0;
|
|
34
|
+
vmg.queue.length = 0;
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
function toDegrees(rad) {
|
|
39
|
+
if (rad === null || rad === undefined) return null;
|
|
40
|
+
return rad * (180 / Math.PI);
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
function toRadians(deg) {
|
|
44
|
+
if (deg === null || deg === undefined) return null;
|
|
45
|
+
return deg * (Math.PI / 180);
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
function percentile(vmg, p) {
|
|
49
|
+
if (!vmg.sorted.length) return 0;
|
|
50
|
+
const idx = Math.floor(p * (vmg.sorted.length - 1));
|
|
51
|
+
return vmg.sorted[idx];
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
function collectVmgSamples(cog, sog, lineBearing, toZoneVz, perpToLineVx) {
|
|
55
|
+
if (sog < cfg.minSog) return; // ignore drifting / tacking stalls
|
|
56
|
+
if (toZoneVz > cfg.maxDistance || perpToLineVx > cfg.maxDistance) return; // to far away from the line
|
|
57
|
+
|
|
58
|
+
// Compute angle boatDir-relative to line bearing
|
|
59
|
+
let angleRad = cog - toRadians(lineBearing);
|
|
60
|
+
|
|
61
|
+
// VMG components
|
|
62
|
+
const vmgNormal = sog * Math.sin(angleRad); // +ve = towards line, -ve = away
|
|
63
|
+
const vmgTangent = sog * Math.cos(angleRad); // +ve = towards pin, -ve = towards stb
|
|
64
|
+
|
|
65
|
+
// Insert vmg arrays
|
|
66
|
+
insertSample(vmgState.vmgToLinePos, vmgNormal);
|
|
67
|
+
insertSample(vmgState.vmgToLineNeg, -vmgNormal);
|
|
68
|
+
insertSample(vmgState.vmgToZonePort, vmgTangent);
|
|
69
|
+
insertSample(vmgState.vmgToZoneStb, -vmgTangent);
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
function insertSample(vmg, value) {
|
|
73
|
+
if (value <= 1.0)
|
|
74
|
+
return; // reject low VMGs
|
|
75
|
+
if (vmg.queue.length >= cfg.maxSamples) {
|
|
76
|
+
const oldest = vmg.queue.shift(); // evict oldest from FIFO
|
|
77
|
+
// remove oldest from sorted (binary search)
|
|
78
|
+
let lo = 0, hi = vmg.sorted.length;
|
|
79
|
+
while (lo < hi) {
|
|
80
|
+
const mid = (lo + hi) >> 1;
|
|
81
|
+
if (vmg.sorted[mid] < oldest)
|
|
82
|
+
lo = mid + 1;
|
|
83
|
+
else
|
|
84
|
+
hi = mid;
|
|
85
|
+
}
|
|
86
|
+
vmg.sorted.splice(lo, 1);
|
|
87
|
+
}
|
|
88
|
+
// insert value into sorted (binary search)
|
|
89
|
+
let lo = 0, hi = vmg.sorted.length;
|
|
90
|
+
while (lo < hi) {
|
|
91
|
+
const mid = (lo + hi) >> 1;
|
|
92
|
+
if (vmg.sorted[mid] < value)
|
|
93
|
+
lo = mid + 1;
|
|
94
|
+
else
|
|
95
|
+
hi = mid;
|
|
96
|
+
}
|
|
97
|
+
vmg.sorted.splice(lo, 0, value);
|
|
98
|
+
vmg.queue.push(value);
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
function computeTimeToLine(cog, sog, lineBearing, toZoneVz, perpToLineVx, ocs, closestEnd, timeToStart = 0) {
|
|
102
|
+
let vmgNormalSigned = 0;
|
|
103
|
+
let vmgTangentSigned = 0;
|
|
104
|
+
|
|
105
|
+
if (cog != null && sog != null) {
|
|
106
|
+
const lineBearingRad = toRadians(lineBearing);
|
|
107
|
+
if (lineBearingRad === null) {
|
|
108
|
+
return 0;
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
// Angle between boat COG and line bearing
|
|
112
|
+
const angleRad = cog - lineBearingRad;
|
|
113
|
+
|
|
114
|
+
// Signed VMG components from *current* COG/SOG
|
|
115
|
+
vmgNormalSigned = sog * Math.sin(angleRad); // normal to line
|
|
116
|
+
vmgTangentSigned = sog * Math.cos(angleRad); // along line (stb->port is +)
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
// 1. Effective VMG normal (perpendicular to line)
|
|
120
|
+
//
|
|
121
|
+
// History:
|
|
122
|
+
// - if OCS, use vmgToLineNeg (away from line => back towards line in this case)
|
|
123
|
+
// - otherwise, use vmgToLinePos
|
|
124
|
+
const histNormal = percentile(ocs ? vmgState.vmgToLineNeg : vmgState.vmgToLinePos, cfg.percentile);
|
|
125
|
+
|
|
126
|
+
// Instantaneous VMG in the *required* direction
|
|
127
|
+
// if we are OCS, then the required direction is the opposite of the effective direction
|
|
128
|
+
let vmgInstNormal = ocs ? -vmgNormalSigned : vmgNormalSigned;
|
|
129
|
+
const vmgEffNormal = Math.max(histNormal || 0, vmgInstNormal || 0);
|
|
130
|
+
|
|
131
|
+
// 2. Effective VMG along the line (towards the chosen zone entry)
|
|
132
|
+
let vmgHistParallel = 0;
|
|
133
|
+
let vmgInstParallel = 0;
|
|
134
|
+
|
|
135
|
+
if (toZoneVz > 0) {
|
|
136
|
+
if (closestEnd === 'port') {
|
|
137
|
+
// Coming from the pin end, we will sail from PORT towards STB
|
|
138
|
+
// => use STB-direction samples.
|
|
139
|
+
vmgHistParallel = percentile(vmgState.vmgToZoneStb, cfg.percentile);
|
|
140
|
+
if (vmgTangentSigned < 0) {
|
|
141
|
+
vmgInstParallel = -vmgTangentSigned; // towards stb
|
|
142
|
+
}
|
|
143
|
+
} else {
|
|
144
|
+
// Coming from the boat end, we will sail from STB towards PORT
|
|
145
|
+
// => use PORT-direction samples.
|
|
146
|
+
vmgHistParallel = percentile(vmgState.vmgToZonePort, cfg.percentile);
|
|
147
|
+
if (vmgTangentSigned > 0) {
|
|
148
|
+
vmgInstParallel = vmgTangentSigned; // towards port
|
|
149
|
+
}
|
|
150
|
+
}
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
const vmgEffectParallel = Math.max(vmgHistParallel || 0, vmgInstParallel || 0);
|
|
154
|
+
|
|
155
|
+
// 3. Combine legs: along to zone, then perpendicular to line
|
|
156
|
+
let ttl = 0;
|
|
157
|
+
|
|
158
|
+
// Outside the start zone: first go along the line (or zone boundary)
|
|
159
|
+
if (toZoneVz > 0 && vmgEffectParallel > 0) {
|
|
160
|
+
ttl += toZoneVz / vmgEffectParallel;
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
// Then go perpendicular to the line to actually hit it
|
|
164
|
+
if (perpToLineVx > 0 && vmgEffNormal > 0) {
|
|
165
|
+
ttl += perpToLineVx / vmgEffNormal;
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
// If we still somehow have zero (no speed or distances), just return time to start so TTB logic doesn't explode.
|
|
169
|
+
return ttl <= 0 ? timeToStart : ttl;
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
module.exports = {
|
|
173
|
+
initRacer,
|
|
174
|
+
toDegrees,
|
|
175
|
+
toRadians,
|
|
176
|
+
resetVmgSamples,
|
|
177
|
+
collectVmgSamples,
|
|
178
|
+
computeTimeToLine,
|
|
179
|
+
|
|
180
|
+
_percentile: percentile, // for testing purposes only
|
|
181
|
+
_vmgState: vmgState // for testing purposes only
|
|
182
|
+
|
|
183
|
+
};
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
// tests/racerMath.test.js
|
|
2
|
+
/* global describe, test, expect */
|
|
3
|
+
'use strict';
|
|
4
|
+
|
|
5
|
+
const {
|
|
6
|
+
toDegrees,
|
|
7
|
+
toRadians,
|
|
8
|
+
_percentile,
|
|
9
|
+
} = require('../racer');
|
|
10
|
+
|
|
11
|
+
describe('racer math', () => {
|
|
12
|
+
test('toDegrees: test maths', () => {
|
|
13
|
+
expect(toDegrees(Math.PI)).toBeCloseTo(180);
|
|
14
|
+
expect(toDegrees(Math.PI / 2)).toBeCloseTo(90);
|
|
15
|
+
expect(toDegrees(0)).toBeCloseTo(0);
|
|
16
|
+
expect(toDegrees(null)).toBeNull();
|
|
17
|
+
expect(toDegrees(undefined)).toBeNull();
|
|
18
|
+
});
|
|
19
|
+
test('toRadians: test maths', () => {
|
|
20
|
+
expect(toRadians(180)).toBeCloseTo(Math.PI);
|
|
21
|
+
expect(toRadians(90)).toBeCloseTo(Math.PI / 2);
|
|
22
|
+
expect(toRadians(0)).toBeCloseTo(0);
|
|
23
|
+
expect(toRadians(null)).toBeNull();
|
|
24
|
+
expect(toRadians(undefined)).toBeNull();
|
|
25
|
+
});
|
|
26
|
+
test('percentile: test maths', () => {
|
|
27
|
+
const vmg = (arr) => ({ sorted: arr, queue: [] });
|
|
28
|
+
expect(_percentile(vmg([]), 0.5)).toBe(0);
|
|
29
|
+
expect(_percentile(vmg([10]), 0.5)).toBe(10);
|
|
30
|
+
expect(_percentile(vmg([10, 20, 30]), 0)).toBe(10);
|
|
31
|
+
expect(_percentile(vmg([10, 20, 30]), 0.5)).toBe(20);
|
|
32
|
+
expect(_percentile(vmg([10, 20, 30]), 0.9)).toBe(20);
|
|
33
|
+
expect(_percentile(vmg([10, 20, 30]), 1)).toBe(30);
|
|
34
|
+
expect(_percentile(vmg([1, 2, 3, 4, 5, 6, 7, 8, 9, 10]), 0.75)).toBe(7);
|
|
35
|
+
})
|
|
36
|
+
});
|
|
@@ -0,0 +1,103 @@
|
|
|
1
|
+
// tests/racerMath.test.js
|
|
2
|
+
/* global describe, test, expect */
|
|
3
|
+
'use strict';
|
|
4
|
+
|
|
5
|
+
const {
|
|
6
|
+
toRadians,
|
|
7
|
+
computeTimeToLine,
|
|
8
|
+
resetVmgSamples,
|
|
9
|
+
_vmgState
|
|
10
|
+
} = require('../racer');
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
describe('racer ttl', () => {
|
|
14
|
+
test('timeToLineNoVMG: no VMG', () => {
|
|
15
|
+
resetVmgSamples();
|
|
16
|
+
expect(computeTimeToLine(toRadians(0), 10, 270, 0, 100, false, 'stb')).toBeCloseTo(10);
|
|
17
|
+
// Fallback: ttl computed as 0, so returns the supplied timeToStart.
|
|
18
|
+
expect(computeTimeToLine(toRadians(0), 10, 270, 100, 0, false, 'stb', 300)).toBe(300);
|
|
19
|
+
// Default timeToStart parameter is 0.
|
|
20
|
+
expect(computeTimeToLine(toRadians(0), 10, 270, 100, 0, false, 'stb')).toBe(0);
|
|
21
|
+
});
|
|
22
|
+
|
|
23
|
+
test('timeToLineSmallVmg: small VMG', () => {
|
|
24
|
+
resetVmgSamples();
|
|
25
|
+
_vmgState.vmgToLinePos.sorted.push(1, 1, 1);
|
|
26
|
+
_vmgState.vmgToLineNeg.sorted.push(2, 2, 2);
|
|
27
|
+
_vmgState.vmgToZonePort.sorted.push(1, 1, 1);
|
|
28
|
+
_vmgState.vmgToZoneStb.sorted.push(2, 2, 2);
|
|
29
|
+
|
|
30
|
+
expect(computeTimeToLine(toRadians(-1), 10, 270, 0, 100, false, 'stb')).toBeCloseTo(10);
|
|
31
|
+
expect(computeTimeToLine(toRadians(0), 10, 270, 100, 0, false, 'stb')).toBeCloseTo(100);
|
|
32
|
+
expect(computeTimeToLine(toRadians(1), 10, 270, 100, 100, false, 'stb')).toBeCloseTo(110);
|
|
33
|
+
|
|
34
|
+
expect(computeTimeToLine(toRadians(89), 10, 270, 0, 100, false, 'stb')).toBeCloseTo(100);
|
|
35
|
+
expect(computeTimeToLine(toRadians(90), 10, 270, 100, 0, false, 'stb')).toBeCloseTo(100);
|
|
36
|
+
expect(computeTimeToLine(toRadians(91), 10, 270, 100, 100, false, 'stb')).toBeCloseTo(200);
|
|
37
|
+
|
|
38
|
+
expect(computeTimeToLine(toRadians(179), 10, 270, 0, 100, false, 'stb')).toBeCloseTo(100);
|
|
39
|
+
expect(computeTimeToLine(toRadians(180), 10, 270, 100, 0, false, 'stb')).toBeCloseTo(100);
|
|
40
|
+
expect(computeTimeToLine(toRadians(181), 10, 270, 100, 100, false, 'stb')).toBeCloseTo(200);
|
|
41
|
+
|
|
42
|
+
expect(computeTimeToLine(toRadians(271), 10, 270, 0, 100, false, 'stb')).toBeCloseTo(100);
|
|
43
|
+
expect(computeTimeToLine(toRadians(269), 10, 270, 100, 0, false, 'stb')).toBeCloseTo(10);
|
|
44
|
+
expect(computeTimeToLine(toRadians(270), 10, 270, 100, 100, false, 'stb')).toBeCloseTo(110);
|
|
45
|
+
|
|
46
|
+
expect(computeTimeToLine(toRadians(359), 10, 270, 0, 100, true, 'port')).toBeCloseTo(50);
|
|
47
|
+
expect(computeTimeToLine(toRadians(0), 10, 270, 100, 0, true, 'port')).toBeCloseTo(50);
|
|
48
|
+
expect(computeTimeToLine(toRadians(1), 10, 270, 100, 100, true, 'port')).toBeCloseTo(100);
|
|
49
|
+
|
|
50
|
+
expect(computeTimeToLine(toRadians(89), 10, 270, 0, 100, true, 'port')).toBeCloseTo(50);
|
|
51
|
+
expect(computeTimeToLine(toRadians(90), 10, 270, 100, 0, true, 'port')).toBeCloseTo(10);
|
|
52
|
+
expect(computeTimeToLine(toRadians(91), 10, 270, 100, 100, true, 'port')).toBeCloseTo(60);
|
|
53
|
+
|
|
54
|
+
expect(computeTimeToLine(toRadians(179), 10, 270, 0, 100, true, 'port')).toBeCloseTo(10);
|
|
55
|
+
expect(computeTimeToLine(toRadians(180), 10, 270, 100, 0, true, 'port')).toBeCloseTo(50);
|
|
56
|
+
expect(computeTimeToLine(toRadians(181), 10, 270, 100, 100, true, 'port')).toBeCloseTo(60);
|
|
57
|
+
|
|
58
|
+
expect(computeTimeToLine(toRadians(271), 10, 270, 0, 100, true, 'port')).toBeCloseTo(50);
|
|
59
|
+
expect(computeTimeToLine(toRadians(269), 10, 270, 100, 0, true, 'port')).toBeCloseTo(50);
|
|
60
|
+
expect(computeTimeToLine(toRadians(270), 10, 270, 100, 100, true, 'port')).toBeCloseTo(100);
|
|
61
|
+
});
|
|
62
|
+
|
|
63
|
+
test('timeToLineSmallInstant: small instant', () => {
|
|
64
|
+
resetVmgSamples();
|
|
65
|
+
_vmgState.vmgToLinePos.sorted.push(10, 10, 10, 10, 10, 10, 10, 10, 10, 15);
|
|
66
|
+
_vmgState.vmgToLineNeg.sorted.push(20, 20, 20, 20, 20, 20, 20, 20, 20, 25);
|
|
67
|
+
_vmgState.vmgToZonePort.sorted.push(10, 10, 10, 10, 10, 10, 10, 10, 10, 15);
|
|
68
|
+
_vmgState.vmgToZoneStb.sorted.push(20, 20, 20, 20, 20, 20, 20, 20, 20, 25);
|
|
69
|
+
|
|
70
|
+
expect(computeTimeToLine(toRadians(-1), 1, 270, 0, 100, false, 'stb')).toBeCloseTo(10);
|
|
71
|
+
expect(computeTimeToLine(toRadians(0), 1, 270, 100, 0, false, 'stb')).toBeCloseTo(10);
|
|
72
|
+
expect(computeTimeToLine(toRadians(1), 1, 270, 100, 100, false, 'stb')).toBeCloseTo(20);
|
|
73
|
+
|
|
74
|
+
expect(computeTimeToLine(toRadians(89), 1, 270, 0, 100, false, 'stb')).toBeCloseTo(10);
|
|
75
|
+
expect(computeTimeToLine(toRadians(90), 1, 270, 100, 0, false, 'stb')).toBeCloseTo(10);
|
|
76
|
+
expect(computeTimeToLine(toRadians(91), 1, 270, 100, 100, false, 'stb')).toBeCloseTo(20);
|
|
77
|
+
|
|
78
|
+
expect(computeTimeToLine(toRadians(179), 1, 270, 0, 100, false, 'stb')).toBeCloseTo(10);
|
|
79
|
+
expect(computeTimeToLine(toRadians(180), 1, 270, 100, 0, false, 'stb')).toBeCloseTo(10);
|
|
80
|
+
expect(computeTimeToLine(toRadians(181), 1, 270, 100, 100, false, 'stb')).toBeCloseTo(20);
|
|
81
|
+
|
|
82
|
+
expect(computeTimeToLine(toRadians(271), 1, 270, 0, 100, false, 'stb')).toBeCloseTo(10);
|
|
83
|
+
expect(computeTimeToLine(toRadians(269), 1, 270, 100, 0, false, 'stb')).toBeCloseTo(10);
|
|
84
|
+
expect(computeTimeToLine(toRadians(270), 1, 270, 100, 100, false, 'stb')).toBeCloseTo(20);
|
|
85
|
+
|
|
86
|
+
expect(computeTimeToLine(toRadians(359), 1, 270, 0, 100, true, 'port')).toBeCloseTo(5);
|
|
87
|
+
expect(computeTimeToLine(toRadians(0), 1, 270, 100, 0, true, 'port')).toBeCloseTo(5);
|
|
88
|
+
expect(computeTimeToLine(toRadians(1), 1, 270, 100, 100, true, 'port')).toBeCloseTo(10);
|
|
89
|
+
|
|
90
|
+
expect(computeTimeToLine(toRadians(89), 1, 270, 0, 100, true, 'port')).toBeCloseTo(5);
|
|
91
|
+
expect(computeTimeToLine(toRadians(90), 1, 270, 100, 0, true, 'port')).toBeCloseTo(5);
|
|
92
|
+
expect(computeTimeToLine(toRadians(91), 1, 270, 100, 100, true, 'port')).toBeCloseTo(10);
|
|
93
|
+
|
|
94
|
+
expect(computeTimeToLine(toRadians(179), 1, 270, 0, 100, true, 'port')).toBeCloseTo(5);
|
|
95
|
+
expect(computeTimeToLine(toRadians(180), 1, 270, 100, 0, true, 'port')).toBeCloseTo(5);
|
|
96
|
+
expect(computeTimeToLine(toRadians(181), 1, 270, 100, 100, true, 'port')).toBeCloseTo(10);
|
|
97
|
+
|
|
98
|
+
expect(computeTimeToLine(toRadians(271), 1, 270, 0, 100, true, 'port')).toBeCloseTo(5);
|
|
99
|
+
expect(computeTimeToLine(toRadians(269), 1, 270, 100, 0, true, 'port')).toBeCloseTo(5);
|
|
100
|
+
expect(computeTimeToLine(toRadians(270), 1, 270, 100, 100, true, 'port')).toBeCloseTo(10);
|
|
101
|
+
});
|
|
102
|
+
|
|
103
|
+
});
|
|
@@ -0,0 +1,168 @@
|
|
|
1
|
+
// tests/racerMath.test.js
|
|
2
|
+
/* global describe, test, expect */
|
|
3
|
+
'use strict';
|
|
4
|
+
|
|
5
|
+
const {
|
|
6
|
+
toRadians,
|
|
7
|
+
collectVmgSamples,
|
|
8
|
+
resetVmgSamples,
|
|
9
|
+
_vmgState
|
|
10
|
+
} = require('../racer');
|
|
11
|
+
|
|
12
|
+
describe('racer vmg', () => {
|
|
13
|
+
test('resetVmgSamples: test arrays are cleared', () => {
|
|
14
|
+
_vmgState.vmgToLinePos.sorted.push(1, 2, 3);
|
|
15
|
+
_vmgState.vmgToLinePos.queue.push(1, 2, 3);
|
|
16
|
+
_vmgState.vmgToLineNeg.sorted.push(4, 5, 6);
|
|
17
|
+
_vmgState.vmgToLineNeg.queue.push(4, 5, 6);
|
|
18
|
+
_vmgState.vmgToZonePort.sorted.push(7, 8, 9);
|
|
19
|
+
_vmgState.vmgToZonePort.queue.push(7, 8, 9);
|
|
20
|
+
_vmgState.vmgToZoneStb.sorted.push(10, 11, 12);
|
|
21
|
+
_vmgState.vmgToZoneStb.queue.push(10, 11, 12);
|
|
22
|
+
expect(_vmgState.vmgToLinePos.sorted.length).toBe(3);
|
|
23
|
+
expect(_vmgState.vmgToLineNeg.sorted.length).toBe(3);
|
|
24
|
+
expect(_vmgState.vmgToZonePort.sorted.length).toBe(3);
|
|
25
|
+
expect(_vmgState.vmgToZoneStb.sorted.length).toBe(3);
|
|
26
|
+
resetVmgSamples();
|
|
27
|
+
expect(_vmgState.vmgToLinePos.sorted.length).toBe(0);
|
|
28
|
+
expect(_vmgState.vmgToLineNeg.sorted.length).toBe(0);
|
|
29
|
+
expect(_vmgState.vmgToZonePort.sorted.length).toBe(0);
|
|
30
|
+
expect(_vmgState.vmgToZoneStb.sorted.length).toBe(0);
|
|
31
|
+
});
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
test('collectVmgSamplesEastWestNarrow: test east west line, with perpendicular and narrow approaches', () => {
|
|
35
|
+
resetVmgSamples();
|
|
36
|
+
collectVmgSamples(toRadians(0), 19.7, 270, 0, 0);
|
|
37
|
+
expect(_vmgState.vmgToLinePos.sorted.length).toBe(1);
|
|
38
|
+
expect(_vmgState.vmgToLinePos.sorted[0]).toBeCloseTo(19.7);
|
|
39
|
+
expect(_vmgState.vmgToLineNeg.sorted.length).toBe(0);
|
|
40
|
+
expect(_vmgState.vmgToZonePort.sorted.length).toBe(0);
|
|
41
|
+
expect(_vmgState.vmgToZoneStb.sorted.length).toBe(0);
|
|
42
|
+
|
|
43
|
+
collectVmgSamples(toRadians(8.21), 19.7, 270, 0, 0);
|
|
44
|
+
expect(_vmgState.vmgToLinePos.sorted.length).toBe(2);
|
|
45
|
+
expect(_vmgState.vmgToLinePos.sorted[0]).toBeCloseTo(19.5);
|
|
46
|
+
expect(_vmgState.vmgToLinePos.sorted[1]).toBeCloseTo(19.7);
|
|
47
|
+
expect(_vmgState.vmgToLineNeg.sorted.length).toBe(0);
|
|
48
|
+
expect(_vmgState.vmgToZonePort.sorted.length).toBe(0);
|
|
49
|
+
expect(_vmgState.vmgToZoneStb.sorted.length).toBe(1);
|
|
50
|
+
expect(_vmgState.vmgToZoneStb.sorted[0]).toBeCloseTo(2.81);
|
|
51
|
+
});
|
|
52
|
+
|
|
53
|
+
|
|
54
|
+
test('collectVmgSamplesEastWest: test east west line with circle', () => {
|
|
55
|
+
resetVmgSamples();
|
|
56
|
+
collectVmgSamples(toRadians(0), 5, 270, 0, 0);
|
|
57
|
+
expect(_vmgState.vmgToLinePos.sorted.length).toBe(1);
|
|
58
|
+
expect(_vmgState.vmgToLinePos.sorted[0]).toBe(5);
|
|
59
|
+
expect(_vmgState.vmgToLineNeg.sorted.length).toBe(0);
|
|
60
|
+
expect(_vmgState.vmgToZonePort.sorted.length).toBe(0);
|
|
61
|
+
expect(_vmgState.vmgToZoneStb.sorted.length).toBe(0);
|
|
62
|
+
|
|
63
|
+
collectVmgSamples(toRadians(36.87), 5, 270, 0, 0);
|
|
64
|
+
expect(_vmgState.vmgToLinePos.sorted.length).toBe(2);
|
|
65
|
+
expect(_vmgState.vmgToLinePos.sorted[0]).toBeCloseTo(4);
|
|
66
|
+
expect(_vmgState.vmgToLinePos.sorted[1]).toBe(5);
|
|
67
|
+
expect(_vmgState.vmgToLineNeg.sorted.length).toBe(0);
|
|
68
|
+
expect(_vmgState.vmgToZonePort.sorted.length).toBe(0);
|
|
69
|
+
expect(_vmgState.vmgToZoneStb.sorted.length).toBe(1);
|
|
70
|
+
expect(_vmgState.vmgToZoneStb.sorted[0]).toBeCloseTo(3);
|
|
71
|
+
|
|
72
|
+
collectVmgSamples(toRadians(90), 5, 270, 0, 0);
|
|
73
|
+
expect(_vmgState.vmgToLinePos.sorted.length).toBe(2);
|
|
74
|
+
expect(_vmgState.vmgToLinePos.sorted[0]).toBeCloseTo(4);
|
|
75
|
+
expect(_vmgState.vmgToLinePos.sorted[1]).toBeCloseTo(5);
|
|
76
|
+
expect(_vmgState.vmgToLineNeg.sorted.length).toBe(0);
|
|
77
|
+
expect(_vmgState.vmgToZonePort.sorted.length).toBe(0);
|
|
78
|
+
expect(_vmgState.vmgToZoneStb.sorted.length).toBe(2);
|
|
79
|
+
expect(_vmgState.vmgToZoneStb.sorted[0]).toBeCloseTo(3);
|
|
80
|
+
expect(_vmgState.vmgToZoneStb.sorted[1]).toBeCloseTo(5);
|
|
81
|
+
|
|
82
|
+
collectVmgSamples(toRadians(90 + 36.87), 5, 270, 0, 0);
|
|
83
|
+
expect(_vmgState.vmgToLinePos.sorted.length).toBe(2);
|
|
84
|
+
expect(_vmgState.vmgToLinePos.sorted[0]).toBeCloseTo(4);
|
|
85
|
+
expect(_vmgState.vmgToLinePos.sorted[1]).toBeCloseTo(5);
|
|
86
|
+
expect(_vmgState.vmgToLineNeg.sorted.length).toBe(1);
|
|
87
|
+
expect(_vmgState.vmgToLineNeg.sorted[0]).toBeCloseTo(3);
|
|
88
|
+
expect(_vmgState.vmgToZonePort.sorted.length).toBe(0);
|
|
89
|
+
expect(_vmgState.vmgToZoneStb.sorted.length).toBe(3);
|
|
90
|
+
expect(_vmgState.vmgToZoneStb.sorted[0]).toBeCloseTo(3);
|
|
91
|
+
expect(_vmgState.vmgToZoneStb.sorted[1]).toBeCloseTo(4);
|
|
92
|
+
expect(_vmgState.vmgToZoneStb.sorted[2]).toBeCloseTo(5);
|
|
93
|
+
|
|
94
|
+
collectVmgSamples(toRadians(180), 5, 270, 0, 0);
|
|
95
|
+
expect(_vmgState.vmgToLinePos.sorted.length).toBe(2);
|
|
96
|
+
expect(_vmgState.vmgToLinePos.sorted[0]).toBeCloseTo(4);
|
|
97
|
+
expect(_vmgState.vmgToLinePos.sorted[1]).toBeCloseTo(5);
|
|
98
|
+
expect(_vmgState.vmgToLineNeg.sorted.length).toBe(2);
|
|
99
|
+
expect(_vmgState.vmgToLineNeg.sorted[0]).toBeCloseTo(3);
|
|
100
|
+
expect(_vmgState.vmgToLineNeg.sorted[1]).toBeCloseTo(5);
|
|
101
|
+
expect(_vmgState.vmgToZonePort.sorted.length).toBe(0);
|
|
102
|
+
expect(_vmgState.vmgToZoneStb.sorted.length).toBe(3);
|
|
103
|
+
expect(_vmgState.vmgToZoneStb.sorted[0]).toBeCloseTo(3);
|
|
104
|
+
expect(_vmgState.vmgToZoneStb.sorted[1]).toBeCloseTo(4);
|
|
105
|
+
expect(_vmgState.vmgToZoneStb.sorted[2]).toBeCloseTo(5);
|
|
106
|
+
|
|
107
|
+
collectVmgSamples(toRadians(180 + 53.13), 5, 270, 0, 0);
|
|
108
|
+
expect(_vmgState.vmgToLinePos.sorted.length).toBe(2);
|
|
109
|
+
expect(_vmgState.vmgToLinePos.sorted[0]).toBeCloseTo(4);
|
|
110
|
+
expect(_vmgState.vmgToLinePos.sorted[1]).toBeCloseTo(5);
|
|
111
|
+
expect(_vmgState.vmgToLineNeg.sorted.length).toBe(3);
|
|
112
|
+
expect(_vmgState.vmgToLineNeg.sorted[0]).toBeCloseTo(3);
|
|
113
|
+
expect(_vmgState.vmgToLineNeg.sorted[1]).toBeCloseTo(3);
|
|
114
|
+
expect(_vmgState.vmgToLineNeg.sorted[2]).toBeCloseTo(5);
|
|
115
|
+
expect(_vmgState.vmgToZonePort.sorted.length).toBe(1);
|
|
116
|
+
expect(_vmgState.vmgToZonePort.sorted[0]).toBeCloseTo(4);
|
|
117
|
+
expect(_vmgState.vmgToZoneStb.sorted.length).toBe(3);
|
|
118
|
+
expect(_vmgState.vmgToZoneStb.sorted[0]).toBeCloseTo(3);
|
|
119
|
+
expect(_vmgState.vmgToZoneStb.sorted[1]).toBeCloseTo(4);
|
|
120
|
+
expect(_vmgState.vmgToZoneStb.sorted[2]).toBeCloseTo(5);
|
|
121
|
+
|
|
122
|
+
collectVmgSamples(toRadians(270), 5, 270, 0, 0);
|
|
123
|
+
expect(_vmgState.vmgToLinePos.sorted.length).toBe(2);
|
|
124
|
+
expect(_vmgState.vmgToLinePos.sorted[0]).toBeCloseTo(4);
|
|
125
|
+
expect(_vmgState.vmgToLinePos.sorted[1]).toBeCloseTo(5);
|
|
126
|
+
expect(_vmgState.vmgToLineNeg.sorted.length).toBe(3);
|
|
127
|
+
expect(_vmgState.vmgToLineNeg.sorted[0]).toBeCloseTo(3);
|
|
128
|
+
expect(_vmgState.vmgToLineNeg.sorted[1]).toBeCloseTo(3);
|
|
129
|
+
expect(_vmgState.vmgToLineNeg.sorted[2]).toBeCloseTo(5);
|
|
130
|
+
expect(_vmgState.vmgToZonePort.sorted.length).toBe(2);
|
|
131
|
+
expect(_vmgState.vmgToZonePort.sorted[0]).toBeCloseTo(4);
|
|
132
|
+
expect(_vmgState.vmgToZonePort.sorted[1]).toBeCloseTo(5);
|
|
133
|
+
expect(_vmgState.vmgToZoneStb.sorted.length).toBe(3);
|
|
134
|
+
expect(_vmgState.vmgToZoneStb.sorted[0]).toBeCloseTo(3);
|
|
135
|
+
expect(_vmgState.vmgToZoneStb.sorted[1]).toBeCloseTo(4);
|
|
136
|
+
expect(_vmgState.vmgToZoneStb.sorted[2]).toBeCloseTo(5);
|
|
137
|
+
|
|
138
|
+
collectVmgSamples(-toRadians(53.13), 5, 270, 0, 0);
|
|
139
|
+
expect(_vmgState.vmgToLinePos.sorted.length).toBe(3);
|
|
140
|
+
expect(_vmgState.vmgToLinePos.sorted[0]).toBeCloseTo(3);
|
|
141
|
+
expect(_vmgState.vmgToLinePos.sorted[1]).toBeCloseTo(4);
|
|
142
|
+
expect(_vmgState.vmgToLinePos.sorted[2]).toBeCloseTo(5);
|
|
143
|
+
expect(_vmgState.vmgToLineNeg.sorted.length).toBe(3);
|
|
144
|
+
expect(_vmgState.vmgToLineNeg.sorted[0]).toBeCloseTo(3);
|
|
145
|
+
expect(_vmgState.vmgToLineNeg.sorted[1]).toBeCloseTo(3);
|
|
146
|
+
expect(_vmgState.vmgToLineNeg.sorted[2]).toBeCloseTo(5);
|
|
147
|
+
expect(_vmgState.vmgToZonePort.sorted.length).toBe(3);
|
|
148
|
+
expect(_vmgState.vmgToZonePort.sorted[0]).toBeCloseTo(4);
|
|
149
|
+
expect(_vmgState.vmgToZonePort.sorted[1]).toBeCloseTo(4);
|
|
150
|
+
expect(_vmgState.vmgToZonePort.sorted[2]).toBeCloseTo(5);
|
|
151
|
+
expect(_vmgState.vmgToZoneStb.sorted.length).toBe(3);
|
|
152
|
+
expect(_vmgState.vmgToZoneStb.sorted[0]).toBeCloseTo(3);
|
|
153
|
+
expect(_vmgState.vmgToZoneStb.sorted[1]).toBeCloseTo(4);
|
|
154
|
+
expect(_vmgState.vmgToZoneStb.sorted[2]).toBeCloseTo(5);
|
|
155
|
+
|
|
156
|
+
});
|
|
157
|
+
|
|
158
|
+
test('collectVmgSamplesNW2SE: test NW to SE line approached obliquely', () => {
|
|
159
|
+
resetVmgSamples();
|
|
160
|
+
collectVmgSamples(toRadians(0), 19.7, 8.21, 0, 0);
|
|
161
|
+
expect(_vmgState.vmgToLinePos.sorted.length).toBe(0);
|
|
162
|
+
expect(_vmgState.vmgToLineNeg.sorted.length).toBe(1);
|
|
163
|
+
expect(_vmgState.vmgToLineNeg.sorted[0]).toBeCloseTo(2.81);
|
|
164
|
+
expect(_vmgState.vmgToZonePort.sorted.length).toBe(1);
|
|
165
|
+
expect(_vmgState.vmgToZonePort.sorted[0]).toBeCloseTo(19.5);
|
|
166
|
+
expect(_vmgState.vmgToZoneStb.sorted.length).toBe(0);
|
|
167
|
+
});
|
|
168
|
+
});
|