signalk-noaa-weather-report
Version:
Sends Voluntary Ship Observations to NOAA (and Windy) Requires an official ships radio callsign
132 lines (116 loc) • 5.28 kB
JavaScript
// Weather Report Module for SignalK Plugin
function getBbxxQuadrant(lat, lon) {
/**
* Returns the BBXX quadrant code for the given latitude and longitude.
* Quadrants:
* 1: N/E (lat >= 0, lon >= 0)
* 3: S/E (lat < 0, lon >= 0)
* 5: S/W (lat < 0, lon < 0)
* 7: N/W (lat >= 0, lon < 0)
*/
if (lat >= 0 && lon >= 0) {
return 1; // N/E
} else if (lat < 0 && lon >= 0) {
return 3; // S/E
} else if (lat < 0 && lon < 0) {
return 5; // S/W
} else if (lat >= 0 && lon < 0) {
return 7; // N/W
} else {
return 9; // Unknown
}
}
function generateBbxxReport(trueWindDirection, trueWindSpeed, lat, lon, utcTime, stationId = "9RM2K7C", waterTemp = null) {
/**
* Generate a BBXX weather report from true wind direction, speed, position, and UTC time.
* Args:
* trueWindDirection (number): Wind direction in degrees true
* trueWindSpeed (number): Wind speed in knots
* lat (number): Latitude in decimal degrees
* lon (number): Longitude in decimal degrees
* utcTime (Date): UTC datetime for the report
* stationId (string): Station identifier (default: 9RM2K7C)
* waterTemp (number): Water temperature in Celsius (default: null)
* Returns:
* string: BBXX weather report
*/
// Day of month and hour (UTC), wind indicator 4 (anemometer, knots)
const dayHour = `${utcTime.getUTCDate().toString().padStart(2, '0')}${utcTime.getUTCHours().toString().padStart(2, '0')}4`;
// Latitude: 99 + lat in tenths, 3 digits
const latCode = `99${Math.round(Math.abs(lat) * 10).toString().padStart(3, '0')}`;
// Longitude: quadrant + lon in tenths, 4 digits
const quadrant = getBbxxQuadrant(lat, lon);
const lonCode = `${quadrant}${Math.round(Math.abs(lon) * 10).toString().padStart(4, '0')}`;
// Precipitation group is always omitted: 43///
const precipitationGroup = "43///";
// Cloud and wind group: /ddff (cloud omitted, wind direction in tens, wind speed in knots)
let cloudWindGroup;
if (trueWindDirection !== null && trueWindSpeed !== null) {
const windDirCode = Math.round(trueWindDirection / 10.0).toString().padStart(2, '0');
const windSpeedCode = trueWindSpeed < 100 ? Math.round(trueWindSpeed).toString().padStart(2, '0') : "//";
cloudWindGroup = `/${windDirCode}${windSpeedCode}`;
} else {
cloudWindGroup = "/////";
}
// Water temperature group: 0 + 4/5 for sign + 3 digits for temperature in tenths
let tempCode;
if (waterTemp !== null) {
const tempAbs = Math.abs(waterTemp);
const sign = waterTemp >= 0 ? "4" : "5";
const tempTenths = Math.round(tempAbs * 10.0); // Convert degrees to tenths
tempCode = `0${sign}${tempTenths.toString().padStart(3, '0')}`;
} else {
tempCode = "0////";
}
// Compose BBXX
const bbxx = `BBXX ${stationId} ${dayHour} ${latCode} ${lonCode} ${precipitationGroup} ${cloudWindGroup} 1//// 2//// 4//// 5//// 7//// 8//// 222// ${tempCode} 0//// 2//// 3//// 4//// 5//// 6//// 8//// ICE /////=`;
return bbxx;
}
function decimalToDm(decimalDegrees) {
/**
* Convert decimal degrees to degrees/minutes.decimal format.
* Returns an object with degrees, minutes, and hemisphere where hemisphere is N/S for latitude or E/W for longitude.
*/
if (typeof decimalDegrees === 'number') {
const hemisphere = decimalDegrees >= 0 ? 'N' : 'S';
const absDegrees = Math.abs(decimalDegrees);
const degrees = Math.floor(absDegrees);
const minutes = (absDegrees - degrees) * 60;
return { degrees, minutes, hemisphere };
}
return { degrees: null, minutes: null, hemisphere: null };
}
function decimalToDmStr(decimalDegrees, isLatitude = true) {
/**
* Convert decimal degrees to a formatted degrees/minutes.decimal string.
* For latitude: DD°MM.mmm'N/S
* For longitude: DDD°MM.mmm'E/W
*/
if (decimalDegrees === null || decimalDegrees === undefined) {
return "N/A";
}
const { degrees, minutes, hemisphere } = decimalToDm(decimalDegrees);
if (degrees === null) {
return "N/A";
}
// For longitude, use E/W instead of N/S
const finalHemisphere = isLatitude ? hemisphere : (decimalDegrees >= 0 ? 'E' : 'W');
// Format with appropriate width (2 digits for lat, 3 for lon)
const degFormat = isLatitude ? degrees.toString().padStart(2, '0') : degrees.toString().padStart(3, '0');
return `${degFormat}°${minutes.toFixed(3).padStart(6, '0')}'${finalHemisphere}`;
}
function humanReadableReport(avgLat, avgLon, avgTwDir, avgTwSpeed, utcTime) {
const latDm = decimalToDmStr(avgLat, true);
const lonDm = decimalToDmStr(avgLon, false);
return `UTC: ${utcTime.toISOString().slice(0, 16).replace('T', ' ')}\n` +
` Position: ${avgLat.toFixed(6)}, ${avgLon.toFixed(6)}\n` +
` ${latDm}, ${lonDm}\n` +
` True Wind: ${avgTwDir.toFixed(1)}° ${avgTwSpeed.toFixed(2)} knots`;
}
module.exports = {
getBbxxQuadrant,
generateBbxxReport,
decimalToDm,
decimalToDmStr,
humanReadableReport
};