@jbroll/nmea-simple
Version:
NMEA 0183 sentence parser and encoder
406 lines (304 loc) • 10.2 kB
text/typescript
// Copied from from https://github.com/nherment/node-nmea/blob/master/lib/Helper.js
const m_hex = ["0", "1", "2", "3", "4", "5", "6", "7", "8", "9", "A", "B", "C", "D", "E", "F"];
export function toHexString(v: number): string {
const msn = (v >> 4) & 0x0f;
const lsn = (v >> 0) & 0x0f;
return m_hex[msn] + m_hex[lsn];
}
export function padLeft(value: string | number, length: number, paddingCharacter: string): string {
let result = typeof value === "string" ? value : value.toFixed(0);
while (result.length < length) {
result = paddingCharacter + result;
}
return result;
}
// =========================================
// checksum related functions
// =========================================
/**
* Checks that the given NMEA sentence has a valid checksum.
*/
export function validNmeaChecksum(nmeaSentence: string): boolean {
const [sentenceWithoutChecksum, checksumString] = nmeaSentence.split("*");
const correctChecksum = computeNmeaChecksum(sentenceWithoutChecksum);
// checksum is a 2 digit hex value
const actualChecksum = parseInt(checksumString, 16);
return correctChecksum === actualChecksum;
}
/**
* Generate a checksum for an NMEA sentence without the trailing "*xx".
*/
export function computeNmeaChecksum(sentenceWithoutChecksum: string): number {
// init to first character value after the $
let checksum = sentenceWithoutChecksum.charCodeAt(1);
// process rest of characters, zero delimited
for (let i = 2; i < sentenceWithoutChecksum.length; i += 1) {
checksum = checksum ^ sentenceWithoutChecksum.charCodeAt(i);
}
// checksum is between 0x00 and 0xff
checksum = checksum & 0xff;
return checksum;
}
/**
* Generate the correct trailing "*xx" footer for an NMEA sentence.
*/
export function createNmeaChecksumFooter(sentenceWithoutChecksum: string): string {
return "*" + toHexString(computeNmeaChecksum(sentenceWithoutChecksum));
}
/**
* Append the correct trailing "*xx" footer for an NMEA string and return the result.
*/
export function appendChecksumFooter(sentenceWithoutChecksum: string): string {
return sentenceWithoutChecksum + createNmeaChecksumFooter(sentenceWithoutChecksum);
}
// =========================================
// field encoders
// =========================================
export function encodeFixed(value: number | undefined, decimalPlaces: number): string {
if (value === undefined) {
return "";
}
return value.toFixed(decimalPlaces);
}
/**
* Encodes the latitude in the standard NMEA format "ddmm.mmmmmm".
*
* @param latitude Latitude in decimal degrees.
*/
export function encodeLatitude(latitude?: number): string {
if (latitude === undefined) {
return ",";
}
let hemisphere: string;
if (latitude < 0) {
hemisphere = "S";
latitude = -latitude;
} else {
hemisphere = "N";
}
// get integer degrees
const d = Math.floor(latitude);
// latitude degrees are always 2 digits
let s = padLeft(d, 2, "0");
// get fractional degrees
const f = latitude - d;
// convert to fractional minutes
const m = (f * 60.0);
// format the fixed point fractional minutes "mm.mmmmmm"
const t = padLeft(m.toFixed(6), 9, "0");
s = s + t + "," + hemisphere;
return s;
}
/**
* Encodes the longitude in the standard NMEA format "dddmm.mmmmmm".
*
* @param longitude Longitude in decimal degrees.
*/
export function encodeLongitude(longitude?: number): string {
if (longitude === undefined) {
return ",";
}
let hemisphere: string;
if (longitude < 0) {
hemisphere = "W";
longitude = -longitude;
} else {
hemisphere = "E";
}
// get integer degrees
const d = Math.floor(longitude);
// longitude degrees are always 3 digits
let s = padLeft(d, 3, "0");
// get fractional degrees
const f = longitude - d;
// convert to fractional minutes and round up to the specified precision
const m = (f * 60.0);
// format the fixed point fractional minutes "mm.mmmmmm"
const t = padLeft(m.toFixed(6), 9, "0");
s = s + t + "," + hemisphere;
return s;
}
// 1 decimal, always meters
export function encodeAltitude(alt: number): string {
if (alt === undefined) {
return ",";
}
return alt.toFixed(1) + ",M";
}
// Some encodings don't want the unit
export function encodeAltitudeNoUnits(alt: number): string {
if (alt === undefined) {
return "";
}
return alt.toFixed(1);
}
// 1 decimal, always meters
export function encodeGeoidalSeperation(geoidalSep: number): string {
if (geoidalSep === undefined) {
return ",";
}
return geoidalSep.toFixed(1) + ",M";
}
// Some encodings don't want the unit
export function encodeGeoidalSeperationNoUnits(geoidalSep: number): string {
if (geoidalSep === undefined) {
return "";
}
return geoidalSep.toFixed(1);
}
// degrees
export function encodeDegrees(degrees?: number): string {
if (degrees === undefined) {
return "";
}
return padLeft(degrees.toFixed(2), 6, "0");
}
export function encodeDate(date?: Date): string {
if (date === undefined) {
return "";
}
const year = date.getUTCFullYear();
const month = date.getUTCMonth() + 1;
const day = date.getUTCDate();
return padLeft(day, 2, "0") + padLeft(month, 2, "0") + year.toFixed(0).substr(2);
}
export function encodeTime(date?: Date): string {
if (date === undefined) {
return "";
}
const hours = date.getUTCHours();
const minutes = date.getUTCMinutes();
const seconds = date.getUTCSeconds();
return padLeft(hours, 2, "0") + padLeft(minutes, 2, "0") + padLeft(seconds, 2, "0");
}
export function encodeValue(value?: any): string {
if (value === undefined) {
return "";
}
return value.toString();
}
// =========================================
// field traditionalDecoders
// =========================================
/**
* Parse the given string to a float, returning 0 for an empty string.
*/
export function parseFloatSafe(str: string): number {
if (str === "") {
return 0.0;
}
return parseFloat(str);
}
/**
* Parse the given string to a integer, returning 0 for an empty string.
*/
export function parseIntSafe(i: string): number {
if (i === "") {
return 0;
}
return parseInt(i, 10);
}
/**
* Parse the given string to a float if possible, returning 0 for an undefined
* value and a string the the given string cannot be parsed.
*/
export function parseNumberOrString(str?: string): number | string {
if (str === undefined) {
return "";
}
const num = parseFloat(str);
return isNaN(num) ? str : num;
}
/**
* Parses coordinate given as "dddmm.mm", "ddmm.mm", "dmm.mm" or "mm.mm"
*/
export function parseDmCoordinate(coordinate: string): number {
const dotIdx = coordinate.indexOf(".");
if (dotIdx < 0) {
return 0;
}
let degrees: string;
let minutes: string;
if (dotIdx >= 3) {
degrees = coordinate.substring(0, dotIdx - 2);
minutes = coordinate.substring(dotIdx - 2);
} else {
// no degrees, just minutes (nonstandard but a buggy unit might do this)
degrees = "0";
minutes = coordinate;
}
return (parseFloat(degrees) + (parseFloat(minutes) / 60.0));
}
/**
* Parses latitude given as "ddmm.mm", "dmm.mm" or "mm.mm" (assuming zero
* degrees) along with a given hemisphere of "N" or "S" into decimal degrees,
* where north is positive and south is negative.
*/
export function parseLatitude(lat: string, hemi: string): number {
const hemisphere = (hemi === "N") ? 1.0 : -1.0;
return parseDmCoordinate(lat) * hemisphere;
}
/**
* Parses latitude given as "dddmm.mm", "ddmm.mm", "dmm.mm" or "mm.mm" (assuming
* zero degrees) along with a given hemisphere of "E" or "W" into decimal
* degrees, where east is positive and west is negative.
*/
export function parseLongitude(lon: string, hemi: string): number {
const hemisphere = (hemi === "E") ? 1.0 : -1.0;
return parseDmCoordinate(lon) * hemisphere;
}
function getYearFromString(yearString: string, rmcCompatible: boolean): number {
if (yearString.length === 4) {
return Number(yearString);
} else if (yearString.length === 2) {
if (rmcCompatible) {
// GPRMC date doesn't specify century. GPS came out in 1973 so if the year
// is less than 73, assume it's 20xx, otherwise assume it is 19xx.
let year = Number(yearString);
if (year < 73) {
year = 2000 + year;
}
else {
year = 1900 + year;
}
return year;
}
else {
return Number("20" + yearString);
}
}
else {
throw Error(`Unexpected year string: ${yearString}`);
}
}
/**
* Parses a UTC time and optionally a date and returns a Date
* object.
* @param {String} time Time the format "hhmmss" or "hhmmss.ss"
* @param {String=} date Optional date in format the ddmmyyyy or ddmmyy
* @returns {Date}
*/
export function parseTime(time: string, date?: string, rmcCompatible = false): Date {
if (time === "") {
return new Date(0);
}
const ret = new Date();
if (date) {
const year = date.slice(4);
const month = parseInt(date.slice(2, 4), 10) - 1;
const day = date.slice(0, 2);
ret.setUTCFullYear(getYearFromString(year, rmcCompatible), Number(month), Number(day));
}
ret.setUTCHours(Number(time.slice(0, 2)));
ret.setUTCMinutes(Number(time.slice(2, 4)));
ret.setUTCSeconds(Number(time.slice(4, 6)));
// Extract the milliseconds, since they can be not present, be 3 decimal place, or 2 decimal places, or other?
const msStr = time.slice(7);
const msExp = msStr.length;
let ms = 0;
if (msExp !== 0) {
ms = parseFloat(msStr) * Math.pow(10, 3 - msExp);
}
ret.setUTCMilliseconds(Number(ms));
return ret;
}