@signalk/course-provider
Version:
Course data provider plugin for SignalK Server.
241 lines (240 loc) • 10.2 kB
JavaScript
;
Object.defineProperty(exports, "__esModule", { value: true });
const worker_threads_1 = require("worker_threads");
const latlon_spherical_js_1 = require("../lib/geodesy/latlon-spherical.js");
let activeDest = false;
// process message from main thread
worker_threads_1.parentPort === null || worker_threads_1.parentPort === void 0 ? void 0 : worker_threads_1.parentPort.on('message', (message) => {
if (parseSKPaths(message)) {
worker_threads_1.parentPort === null || worker_threads_1.parentPort === void 0 ? void 0 : worker_threads_1.parentPort.postMessage(calcs(message));
activeDest = true;
}
else {
if (activeDest) {
worker_threads_1.parentPort === null || worker_threads_1.parentPort === void 0 ? void 0 : worker_threads_1.parentPort.postMessage({ gc: {}, rl: {} });
activeDest = false;
}
}
});
function parseSKPaths(src) {
var _a, _b;
return src['navigation.position'] &&
((_a = src['navigation.course.nextPoint']) === null || _a === void 0 ? void 0 : _a.position) &&
((_b = src['navigation.course.previousPoint']) === null || _b === void 0 ? void 0 : _b.position)
? true
: false;
}
function toRadians(value) {
return (value * Math.PI) / 180;
}
/** Normalises angle to return a value between 0 & (2*PI | 360)
* @param angle: angle (in radians or degrees)
* @param units: unit of the angle supplied => 'rad' (radians) or 'deg' (degrees)
* @returns value between 0 - 2*PI
*/
function compassAngle(angle, units = 'rad') {
const maxAngle = units === 'rad' ? Math.PI * 2 : 360;
return angle < 0
? angle + maxAngle
: angle >= maxAngle
? angle - maxAngle
: angle;
}
// course calculations
function calcs(src) {
var _a;
const vesselPosition = src['navigation.position']
? new latlon_spherical_js_1.LatLonSpherical(src['navigation.position'].latitude, src['navigation.position'].longitude)
: null;
const destination = src['navigation.course.nextPoint']
? new latlon_spherical_js_1.LatLonSpherical(src['navigation.course.nextPoint'].position.latitude, src['navigation.course.nextPoint'].position.longitude)
: null;
const startPoint = src['navigation.course.previousPoint'].position
? new latlon_spherical_js_1.LatLonSpherical(src['navigation.course.previousPoint'].position.latitude, src['navigation.course.previousPoint'].position.longitude)
: null;
const res = { gc: {}, rl: {}, passedPerpendicular: false };
if (!vesselPosition || !destination || !startPoint) {
return res;
}
const xte = vesselPosition === null || vesselPosition === void 0 ? void 0 : vesselPosition.crossTrackDistanceTo(startPoint, destination);
const magVar = (_a = src['navigation.magneticVariation']) !== null && _a !== void 0 ? _a : 0.0;
// GreatCircle
const bearingTrackTrue = toRadians(startPoint === null || startPoint === void 0 ? void 0 : startPoint.initialBearingTo(destination));
const bearingTrue = toRadians(vesselPosition === null || vesselPosition === void 0 ? void 0 : vesselPosition.initialBearingTo(destination));
const bearingTrackMagnetic = compassAngle(bearingTrackTrue + magVar);
const bearingMagnetic = compassAngle(bearingTrue + magVar);
const gcDistance = vesselPosition === null || vesselPosition === void 0 ? void 0 : vesselPosition.distanceTo(destination);
const gcVmg = vmg(src, bearingTrue, 'true'); // prefer 'true' values
const gcTime = timeCalcs(src, gcDistance, gcVmg);
res.gc = {
calcMethod: 'GreatCircle',
bearingTrackTrue: bearingTrackTrue,
bearingTrackMagnetic: bearingTrackMagnetic,
crossTrackError: xte,
distance: gcDistance,
bearingTrue: bearingTrue,
bearingMagnetic: bearingMagnetic,
velocityMadeGood: gcVmg,
timeToGo: gcTime.ttg,
estimatedTimeOfArrival: gcTime.eta,
previousPoint: {
distance: vesselPosition === null || vesselPosition === void 0 ? void 0 : vesselPosition.distanceTo(startPoint)
},
targetSpeed: targetSpeed(src, gcDistance)
};
// Rhumbline
const rlBearingTrackTrue = toRadians(startPoint === null || startPoint === void 0 ? void 0 : startPoint.rhumbBearingTo(destination));
const rlBearingTrue = toRadians(vesselPosition === null || vesselPosition === void 0 ? void 0 : vesselPosition.rhumbBearingTo(destination));
const rlBearingTrackMagnetic = compassAngle(rlBearingTrackTrue + magVar);
const rlBearingMagnetic = compassAngle(rlBearingTrue + magVar);
const rlDistance = vesselPosition === null || vesselPosition === void 0 ? void 0 : vesselPosition.rhumbDistanceTo(destination);
const rlVmg = vmg(src, rlBearingTrue, 'true'); // prefer 'true' values
const rlTime = timeCalcs(src, rlDistance, rlVmg);
res.rl = {
calcMethod: 'Rhumbline',
bearingTrackTrue: rlBearingTrackTrue,
bearingTrackMagnetic: rlBearingTrackMagnetic,
crossTrackError: xte,
distance: rlDistance,
bearingTrue: rlBearingTrue,
bearingMagnetic: rlBearingMagnetic,
velocityMadeGood: rlVmg,
timeToGo: rlTime.ttg,
estimatedTimeOfArrival: rlTime.eta,
previousPoint: {
distance: vesselPosition === null || vesselPosition === void 0 ? void 0 : vesselPosition.rhumbDistanceTo(startPoint)
},
targetSpeed: targetSpeed(src, rlDistance, true)
};
// passed destination perpendicular
res.passedPerpendicular = passedPerpendicular(vesselPosition, destination, startPoint);
return res;
}
// Velocity Made Good to Course
function vmg(src, bearing, bearingType = 'true') {
const hdg = bearingType === 'true'
? src['navigation.headingTrue']
: src['navigation.headingMagnetic'];
if (typeof hdg !== 'number' ||
typeof src['navigation.speedOverGround'] !== 'number') {
return null;
}
return Math.cos(bearing - hdg) * src['navigation.speedOverGround'];
}
// Time to Go & Estimated time of arrival at the nextPoint
function timeCalcs(src, distance, vmg) {
if (typeof distance !== 'number' || !vmg) {
return { ttg: null, eta: null };
}
const date = src['navigation.datetime']
? new Date(src['navigation.datetime'])
: new Date();
const dateMsec = date.getTime();
const ttgMsec = Math.floor((distance / vmg) * 1000);
const etaMsec = dateMsec + ttgMsec;
return {
ttg: ttgMsec / 1000,
eta: new Date(etaMsec).toISOString()
};
}
// Avg speed required to arrive at destination at targetArrivalTime
function targetSpeed(src, distance, rhumbLine) {
if (typeof distance !== 'number' ||
!src['navigation.course.targetArrivalTime']) {
return null;
}
// if route totalDistance = distance plus + length of remaining route segments
if (src['navigation.course.activeRoute.waypoints']) {
distance += routeRemaining(src, rhumbLine);
}
const date = src['navigation.datetime']
? new Date(src['navigation.datetime'])
: new Date();
const dateMsec = date.getTime();
const tat = new Date(src['navigation.course.targetArrivalTime']);
const tatMsec = tat.getTime();
if (tatMsec <= dateMsec) {
// current time is after targetArrivalTime
return null;
}
const tDiffSec = (tatMsec - dateMsec) / 1000;
return distance / tDiffSec;
}
// total distance in meters of remaining route segments
function routeRemaining(src, rhumbLine) {
if (src['navigation.course.activeRoute.pointIndex'] === null ||
!src['navigation.course.activeRoute.waypoints'] ||
!Array.isArray(src['navigation.course.activeRoute.waypoints'])) {
return 0;
}
if (src['navigation.course.activeRoute.waypoints'].length < 2) {
return 0;
}
let reverse = src['navigation.course.activeRoute.reverse'];
let ptIndex = src['navigation.course.activeRoute.pointIndex'];
let lastIndex = src['navigation.course.activeRoute.waypoints'].length - 1;
// determine segments to sum
let fromIndex;
let toIndex;
if (reverse) {
fromIndex = 0;
toIndex = lastIndex - ptIndex;
if (toIndex === fromIndex) {
return 0;
}
}
else {
if (ptIndex === lastIndex) {
return 0;
}
fromIndex = ptIndex;
toIndex = lastIndex;
}
// sum segment lengths
let wpts = src['navigation.course.activeRoute.waypoints'];
let rteLen = 0;
for (let idx = fromIndex; idx < lastIndex; idx++) {
let pt = new latlon_spherical_js_1.LatLonSpherical(wpts[idx].position.latitude, wpts[idx].position.longitude);
if (rhumbLine) {
rteLen += pt.rhumbDistanceTo(new latlon_spherical_js_1.LatLonSpherical(wpts[idx + 1].position.latitude, wpts[idx + 1].position.longitude));
}
else {
rteLen += pt.distanceTo(new latlon_spherical_js_1.LatLonSpherical(wpts[idx + 1].position.latitude, wpts[idx + 1].position.longitude));
}
}
return rteLen;
}
// return true if vessel is past perpendicular of destination
function passedPerpendicular(vesselPosition, destination, startPoint) {
const va = toVector(destination, vesselPosition);
const vb = toVector(destination, startPoint);
const rad = Math.acos((va.x * vb.x + va.y * vb.y) / (va.length * vb.length));
const deg = (180 / Math.PI) * rad;
return deg > 90 ? true : false;
}
function toVector(origin, end) {
// calc longitudinal difference (inc dateline transition)
function xDiff(a, b) {
let bx;
if (a > 170 && b < 0) {
// E->W transition
bx = a + (180 - a) + (180 + b);
}
else if (a < -170 && b > 0) {
// W->E transition
bx = a - (180 + a) - (180 - b);
}
else {
bx = b;
}
return bx - a;
}
const x = xDiff(origin.longitude, end.longitude);
const y = end.latitude - origin.latitude;
const v = {
x: x,
y: y,
length: Math.sqrt(Math.pow(x, 2) + Math.pow(y, 2))
};
return v;
}