@gml/truewind
Version:
Modern ES6+ library for apparent to true wind calculation in sailing applications.
251 lines (208 loc) • 6.89 kB
JavaScript
/**
* 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
* @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.0) {
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;
}
}
return {
awa: s.awa,
aws: s.aws,
leeway: leeway,
stw: stw,
vmg: vmg,
tws: tws,
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
};
}
}
export { TrueWind };
export default TrueWind;