UNPKG

@signalk/course-provider

Version:
308 lines (307 loc) 12.7 kB
"use strict"; 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 a value within the range of a compass * @param angle: angle (in radians) * @returns value between 0 - 2*PI */ function compassAngle(angle) { const maxAngle = Math.PI * 2; 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; const vmgValue = vmg(src); // 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 = vmgValue; const gcVmc = vmc(src, bearingTrue, 'true'); // for ETA, TTG - prefer 'true' values const gcTime = timeCalcs(src, gcDistance, gcVmc, false); res.gc = { calcMethod: 'GreatCircle', bearingTrackTrue: bearingTrackTrue, bearingTrackMagnetic: bearingTrackMagnetic, crossTrackError: xte, distance: gcDistance, bearingTrue: bearingTrue, bearingMagnetic: bearingMagnetic, velocityMadeGood: gcVmg, velocityMadeGoodToCourse: gcVmc, timeToGo: gcTime.nextPoint.ttg, estimatedTimeOfArrival: gcTime.nextPoint.eta, previousPoint: { distance: vesselPosition === null || vesselPosition === void 0 ? void 0 : vesselPosition.distanceTo(startPoint) }, route: { timeToGo: gcTime.route.ttg, estimatedTimeOfArrival: gcTime.route.eta, distance: gcTime.route.dtg }, 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 = vmgValue; const rlVmc = vmc(src, rlBearingTrue, 'true'); // for ETA, TTG - prefer 'true' values const rlTime = timeCalcs(src, rlDistance, rlVmc, true); res.rl = { calcMethod: 'Rhumbline', bearingTrackTrue: rlBearingTrackTrue, bearingTrackMagnetic: rlBearingTrackMagnetic, crossTrackError: xte, distance: rlDistance, bearingTrue: rlBearingTrue, bearingMagnetic: rlBearingMagnetic, velocityMadeGood: rlVmg, velocityMadeGoodToCourse: rlVmc, timeToGo: rlTime.nextPoint.ttg, estimatedTimeOfArrival: rlTime.nextPoint.eta, previousPoint: { distance: vesselPosition === null || vesselPosition === void 0 ? void 0 : vesselPosition.rhumbDistanceTo(startPoint) }, route: { timeToGo: rlTime.route.ttg, estimatedTimeOfArrival: rlTime.route.eta, distance: rlTime.route.dtg }, targetSpeed: targetSpeed(src, rlDistance, true) }; // passed destination perpendicular res.passedPerpendicular = passedPerpendicular(vesselPosition, destination, startPoint); return res; } // Velocity Made Good to wind function vmg(src) { if (typeof src['environment.wind.angleTrueGround'] !== 'number' || typeof src['navigation.speedOverGround'] !== 'number') { return null; } return (Math.cos(src['environment.wind.angleTrueGround']) * src['navigation.speedOverGround']); } // Velocity Made Good to Course (used for ETA / TTG calcs) function vmc(src, bearing, bearingType = 'true') { const cog = bearingType === 'true' ? src['navigation.courseOverGroundTrue'] : src['navigation.courseOverGroundMagnetic']; if (typeof cog !== 'number' || typeof src['navigation.speedOverGround'] !== 'number') { return null; } return Math.cos(Math.abs(Angle.difference(bearing, cog))) * src['navigation.speedOverGround']; } // Time to Go & Estimated time of arrival at the nextPoint / route destination function timeCalcs(src, distance, vmc, rhumbLine) { var _a, _b; const isRoute = Array.isArray((_a = src['activeRoute']) === null || _a === void 0 ? void 0 : _a.waypoints) && ((_b = src['activeRoute']) === null || _b === void 0 ? void 0 : _b.waypoints.length) !== 0; const result = { nextPoint: { ttg: null, eta: null }, route: { ttg: null, eta: null, dtg: null } }; if (typeof distance !== 'number' || !vmc) { return result; } const date = src['navigation.datetime'] ? new Date(src['navigation.datetime']) : new Date(); const dateMsec = date.getTime(); const nextTtgMsec = Math.floor((distance / vmc) * 1000); const nextEtaMsec = dateMsec + nextTtgMsec; result.nextPoint.ttg = nextTtgMsec / 1000; result.nextPoint.eta = new Date(nextEtaMsec).toISOString(); if (isRoute) { const rteDistance = distance + routeRemaining(src, rhumbLine); const routeTtgMsec = Math.floor((rteDistance / vmc) * 1000); const routeEtaMsec = dateMsec + routeTtgMsec; result.route.ttg = routeTtgMsec / 1000; result.route.eta = new Date(routeEtaMsec).toISOString(); result.route.dtg = rteDistance; } return result; } // Avg speed required to arrive at destination at targetArrivalTime function targetSpeed(src, distance, rhumbLine) { var _a; if (typeof distance !== 'number' || !src['navigation.course.targetArrivalTime']) { return null; } // if route totalDistance = distance plus + length of remaining route segments if ((_a = src['activeRoute']) === null || _a === void 0 ? void 0 : _a.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) { var _a, _b, _c, _d, _e, _f; if (((_a = src['activeRoute']) === null || _a === void 0 ? void 0 : _a.pointIndex) === null || !Array.isArray((_b = src['activeRoute']) === null || _b === void 0 ? void 0 : _b.waypoints)) { return 0; } if (((_c = src['activeRoute']) === null || _c === void 0 ? void 0 : _c.waypoints.length) < 2) { return 0; } let reverse = (_d = src['activeRoute']) === null || _d === void 0 ? void 0 : _d.reverse; let ptIndex = (_e = src['activeRoute']) === null || _e === void 0 ? void 0 : _e.pointIndex; let lastIndex = ((_f = src['activeRoute']) === null || _f === void 0 ? void 0 : _f.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['activeRoute'].waypoints; let rteLen = 0; for (let idx = fromIndex; idx < lastIndex; idx++) { let pt = new latlon_spherical_js_1.LatLonSpherical(wpts[idx][1], wpts[idx][0]); if (rhumbLine) { rteLen += pt.rhumbDistanceTo(new latlon_spherical_js_1.LatLonSpherical(wpts[idx + 1][1], wpts[idx + 1][0])); } else { rteLen += pt.distanceTo(new latlon_spherical_js_1.LatLonSpherical(wpts[idx + 1][1], wpts[idx + 1][0])); } } 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; } class Angle { /** difference between two angles (in radians) * @param h: angle 1 * @param b: angle 2 * @returns angle (-ive = port) */ static difference(h, b) { const d = (Math.PI * 2) - b; const hd = h + d; const a = Angle.normalise(hd); return a < Math.PI ? 0 - a : (Math.PI * 2) - a; } /** Add two angles (in radians) * @param h: angle 1 * @param b: angle 2 * @returns sum angle */ static add(h, b) { return Angle.normalise(h + b); } /** Normalises angle to a value between 0 & 2Pi radians * @param a: angle * @returns value between 0-2Pi */ static normalise(a) { const pi2 = (Math.PI * 2); return a < 0 ? a + pi2 : a >= pi2 ? a - pi2 : a; } }