UNPKG

@gml/truewind

Version:

Modern ES6+ library for apparent to true wind calculation in sailing applications.

325 lines (270 loc) 9.35 kB
'use strict'; Object.defineProperty(exports, '__esModule', { value: true }); // windHeightCorrection_mps.js /** * Convert wind speed measured at `fromHeight` (m) to `toHeight` (m). * Supports: * - log-law with fixed roughness z0 (matches ORC-style assumption) * - power-law with exponent alpha (marine rule-of-thumb) * * @param {number} speed Wind speed at fromHeight [m/s] * @param {number} fromHeight Sensor height in meters (e.g., 26) * @param {object} [opts] * @param {number} [opts.toHeight=10] Target height in meters (default 10 m) * @param {"log"|"power"} [opts.method="log"] Correction method * @param {number} [opts.z0=0.005] Roughness length for log-law (meters) * @param {number} [opts.alpha=0.11] Exponent for power-law * @returns {number} corrected speed at toHeight [m/s] */ function windToHeight(speed, fromHeight, opts = {}) { const { toHeight = 10, method = 'log', z0 = 0.005, alpha = 0.11 } = opts; if (fromHeight <= 0 || toHeight <= 0) { throw new Error('heights must be > 0 meters'); } if (method === 'log' && (z0 <= 0 || z0 >= Math.min(fromHeight, toHeight))) { throw new Error('For log-law, z0 must be > 0 and < both heights'); } let U_to; if (method === 'log') { // Log-law: U(z) ∝ ln(z/z0) const num = Math.log(toHeight / z0); const den = Math.log(fromHeight / z0); U_to = speed * (num / den); } else if (method === 'power') { // Power-law: U(z) = U_ref * (z/z_ref)^alpha U_to = speed * Math.pow(toHeight / fromHeight, alpha); } else { throw new Error('method must be "log" or "power"'); } return U_to; } /** * A small true wind calculation library put together using slightly modified code * from the links below. Cheers and thank you to the original authors! * * @see http://sailboatinstruments.blogspot.com/2011/05/true-wind-vmg-and-current-calculations.html * @see https://kingtidesailing.blogspot.com/2015/10/correcting-nmea-0183-wind-for-vessel.html */ const DEG_TO_RAD = Math.PI / 180.0; const RAD_TO_DEG = 180.0 / Math.PI; const MS_TO_KT = 1.94384; /** * TrueWind calculation class for converting apparent wind to true wind */ class TrueWind { /** * Calculate true wind from apparent wind and boat parameters * @param {Object} input - Input parameters * @param {number} input.bspd - Boat speed over water as measured (m/s) * @param {number} input.sog - Speed over ground (m/s) * @param {number} input.cog - Course over ground (degrees) * @param {number} input.aws - Apparent wind speed (m/s) * @param {number} input.awa - Apparent wind angle, including any offset (degrees) * @param {number} input.heading - Heading (degrees magnetic) * @param {number} [input.variation=0] - Variation (degrees) * @param {number} [input.roll] - Roll angle of sensor (degrees) * @param {number} [input.pitch] - Pitch angle of sensor (degrees) * @param {number} [input.K] - Leeway coefficient * @param {number} [input.sensorHeight] - Height of wind sensor in meters * @param {"log"|"power"} [input.heightCorrectionMethod] - Height correction method ("log" or "power") * @returns {Object} Calculated wind data */ static getTrue(input) { const s = { ...input }; // To maintain backward compatibility, fill in some missing data using what is given. if (s.variation === undefined) { s.variation = 0; } if (s.bspd === undefined && s.sog !== undefined) { s.bspd = s.sog; } else if (s.sog === undefined && s.bspd !== undefined) { s.sog = s.bspd; } if (s.heading === undefined && s.cog !== undefined) { s.heading = s.cog; } else if (s.cog === undefined && s.heading !== undefined) { s.cog = s.heading; } // In the old version we supplied awd (assued true angle), so here // we will convert that to awa if we don't have it. if (s.awa === undefined && s.awd !== undefined) { s.awa = s.awd - (s.heading + s.variation); while (s.awa > 180) { s.awa -= 360; } while (s.awa < -180) { s.awa += 360; } } // Backward compability stuff ends here. if ( s.awa === undefined || s.aws === undefined || s.heading === undefined || s.bspd === undefined ) { throw new Error('Please supply at least the parameters { awa, aws, heading, bspd }'); } // Adjust into correct half of the circle. if (s.awa > 180) { s.awa -= 360; } else if (s.awa < -180) { s.awa += 360; } // Adjust for pitch and roll Object.assign(s, this.getAttitudeCorrections(s)); // Adjust for leeway let leeway; if (!s.bspd || !s.roll || !s.K || (s.roll > 0 && s.awa > 0) || (s.roll < 0 && s.awa < 0)) { // don't adjust if we are not moving, not heeling, or heeling into the wind leeway = 0; } else { // Calculate leeway in degrees using m/s speeds const bspd_kt = s.bspd * MS_TO_KT; // Convert m/s to knots for leeway calculation leeway = (s.K * s.roll) / (bspd_kt * bspd_kt); if (leeway > 45) { leeway = 45; } else if (leeway < -45) { leeway = -45; } } // Calculate speed through water, accounting for leeway. const stw = s.bspd / Math.cos(leeway * DEG_TO_RAD); // Calculate component of stw perpendicular to boat axis const lateral_speed = stw * Math.sin(leeway * DEG_TO_RAD); // Calculate TWS (true wind speed) const cartesian_awa = (270 - s.awa) * DEG_TO_RAD; const aws_x = s.aws * Math.cos(cartesian_awa); const aws_y = s.aws * Math.sin(cartesian_awa); const tws_x = aws_x + lateral_speed; const tws_y = aws_y + s.bspd; const tws = Math.sqrt(tws_x * tws_x + tws_y * tws_y); // Calculate TWA (true wind angle) const twa_cartesian = Math.atan2(tws_y, tws_x); let twa; if (Number.isNaN(twa_cartesian)) { // singularity if (tws_y < 0.0) { twa = 180.0; } else { twa = 0.0; } } else { twa = 270.0 - twa_cartesian * RAD_TO_DEG; if (s.awa >= 0.0) { twa = twa % 360; } else { twa -= 360.0; } if (twa > 180.0) { twa -= 360.0; } else if (twa < -180) { twa += 360.0; } } const vmg = stw * Math.cos((-twa + leeway) * DEG_TO_RAD); let wdir = s.heading + twa; if (wdir > 360.0) { wdir -= 360.0; } else if (wdir < 0.0) { wdir += 360.0; } const cog_mag = s.cog - s.variation; const alpha = (90.0 - (s.heading + leeway)) * DEG_TO_RAD; const gamma = (90.0 - cog_mag) * DEG_TO_RAD; const curr_x = s.sog * Math.cos(gamma) - stw * Math.cos(alpha); const curr_y = s.sog * Math.sin(gamma) - stw * Math.sin(alpha); const soc = Math.sqrt(curr_x * curr_x + curr_y * curr_y); const doc_cartesian = Math.atan2(curr_y, curr_x); let doc; if (Number.isNaN(doc_cartesian)) { if (curr_y < 0.0) { doc = 180.0; } else { doc = 0.0; } } else { doc = 90.0 - doc_cartesian * RAD_TO_DEG; if (doc > 360.0) { doc -= 360.0; } else if (doc < 0.0) { doc += 360.0; } } let finalTws = tws; // Apply height correction if sensor height and method are provided if (s.sensorHeight && s.heightCorrectionMethod) { const opts = { toHeight: 10, method: s.heightCorrectionMethod }; // Use default roughness values from heightcorrection.js if (s.heightCorrectionMethod === 'log') { opts.z0 = 0.005; // Default roughness for log-law } else if (s.heightCorrectionMethod === 'power') { opts.alpha = 0.11; // Default exponent for power-law } finalTws = windToHeight(tws, s.sensorHeight, opts); } return { awa: s.awa, aws: s.aws, leeway: leeway, stw: stw, vmg: vmg, tws: finalTws, twa: twa, twd: wdir + s.variation, soc: soc, doc: doc + s.variation }; } /** * Correct for pitch and roll. * This code is borrowed mostly from here: * @see https://kingtidesailing.blogspot.com/2015/10/correcting-nmea-0183-wind-for-vessel.html * @param {Object} src - Source data with roll and pitch * @returns {Object} Corrected data */ static getAttitudeCorrections(src) { const { roll, pitch } = src; // Do nothing if we don't have roll and pitch. if (roll === undefined || pitch === undefined) { return src; } let awa = src.awa; if (awa < 0) { awa += 360; } const rwa0 = awa; const ws0 = src.aws; const wx0 = ws0 * Math.sin(rwa0 * DEG_TO_RAD); const wy0 = ws0 * Math.cos(rwa0 * DEG_TO_RAD); // Skipping the rotational velocity adjustments for now const wx1 = wx0; const wy1 = wy0; // Adjust for absolute roll and pitch const wx2 = wx1 / Math.cos(roll * DEG_TO_RAD); const wy2 = wy1 / Math.cos(pitch * DEG_TO_RAD); let ws1 = Math.sqrt(wx2 ** 2 + wy2 ** 2); if (wx2 === 0.0 || wy2 === 0.0) { ws1 = ws0; } let rwa1 = Math.atan2(wx2, wy2) * RAD_TO_DEG; if (rwa1 < 0) { rwa1 += 360; } return { ...src, aws: ws1, awa: rwa1 }; } } exports.TrueWind = TrueWind; exports.default = TrueWind;