signalk-parquet
Version:
Vessel data Parquet file archive with automated value and geospatial triggers. History API compliant with cloud backups and queries.
117 lines • 4.49 kB
JavaScript
;
/**
* Minimal GPX parser
*
* Extracts track points (<trkpt>) from a GPX file. Waypoints and routes are
* ignored — only trkpt elements carry the <time> tag needed for partitioning
* time-series data into the parquet store.
*
* Supports GPX 1.0 and 1.1. Uses regex-based extraction rather than a full
* XML parser to avoid adding a dependency; this is safe for GPX because the
* schema is shallow and well-defined.
*
* Example input fragment:
* <trkpt lat="47.5" lon="8.7">
* <ele>412.5</ele>
* <time>2024-06-01T10:15:30Z</time>
* <speed>5.14</speed>
* <course>180.0</course>
* </trkpt>
* Produces: { latitude: 47.5, longitude: 8.7, time: Date(...), elevation: 412.5, speedMs: 5.14, courseDeg: 180 }
*/
Object.defineProperty(exports, "__esModule", { value: true });
exports.parseGpx = parseGpx;
// Real-world GPX exporters add namespace prefixes liberally (Garmin
// Connect uses "gpxx:", some OpenCPN configs use "ns3:", certain Suunto
// and Polar exporters use other ones). The (?:[\w-]+:)? group is
// non-capturing so existing capture-group indices stay the same.
// Match a single <trkpt ...>...</trkpt> block (possibly namespace-prefixed),
// capturing attributes and inner content. Multiline because trkpt contents
// span several lines.
const TRKPT_RE = /<(?:[\w-]+:)?trkpt\b([^>]*)>([\s\S]*?)<\/(?:[\w-]+:)?trkpt>|<(?:[\w-]+:)?trkpt\b([^>]*)\/>/g;
// Match <trk>...</trk>; within each we parse name and trkpt elements.
const TRK_RE = /<(?:[\w-]+:)?trk\b[^>]*>([\s\S]*?)<\/(?:[\w-]+:)?trk>/g;
const NAME_RE = /<(?:[\w-]+:)?name\b[^>]*>([\s\S]*?)<\/(?:[\w-]+:)?name>/;
function extractAttr(attrs, name) {
// e.g. extract lat="47.5" from ' lat="47.5" lon="8.7"'
const re = new RegExp(`\\b${name}\\s*=\\s*"([^"]*)"`);
const m = attrs.match(re);
return m ? m[1] : undefined;
}
function extractTag(inner, tag) {
// Allow an optional namespace prefix on either the open or close tag,
// matching the broader tolerance in TRKPT_RE / TRK_RE / NAME_RE.
const re = new RegExp(`<(?:[\\w-]+:)?${tag}\\b[^>]*>([\\s\\S]*?)<\\/(?:[\\w-]+:)?${tag}>`, 'i');
const m = inner.match(re);
return m ? m[1].trim() : undefined;
}
function parseFloatOrUndef(s) {
if (s === undefined)
return undefined;
const n = parseFloat(s);
return Number.isFinite(n) ? n : undefined;
}
function parsePoint(attrs, inner) {
const lat = parseFloatOrUndef(extractAttr(attrs, 'lat'));
const lon = parseFloatOrUndef(extractAttr(attrs, 'lon'));
if (lat === undefined || lon === undefined) {
return null;
}
const timeStr = extractTag(inner, 'time');
let time;
if (timeStr) {
const d = new Date(timeStr);
if (!isNaN(d.getTime())) {
time = d;
}
}
return {
latitude: lat,
longitude: lon,
time,
elevation: parseFloatOrUndef(extractTag(inner, 'ele')),
speedMs: parseFloatOrUndef(extractTag(inner, 'speed')),
courseDeg: parseFloatOrUndef(extractTag(inner, 'course')),
};
}
/**
* Parse a GPX XML string into structured tracks.
*
* Points without a valid lat/lon are skipped. Points without <time> are
* still returned (caller may drop them) — the time field is undefined.
*/
function parseGpx(xml) {
const tracks = [];
let totalPoints = 0;
let firstTime;
let lastTime;
let trkMatch;
TRK_RE.lastIndex = 0;
while ((trkMatch = TRK_RE.exec(xml)) !== null) {
const trkInner = trkMatch[1];
const nameMatch = trkInner.match(NAME_RE);
const name = nameMatch ? nameMatch[1].trim() : undefined;
const points = [];
let ptMatch;
TRKPT_RE.lastIndex = 0;
while ((ptMatch = TRKPT_RE.exec(trkInner)) !== null) {
// Self-closing form uses group 3; otherwise group 1 + 2
const attrs = ptMatch[1] ?? ptMatch[3] ?? '';
const inner = ptMatch[2] ?? '';
const pt = parsePoint(attrs, inner);
if (!pt)
continue;
points.push(pt);
totalPoints++;
if (pt.time) {
if (!firstTime || pt.time < firstTime)
firstTime = pt.time;
if (!lastTime || pt.time > lastTime)
lastTime = pt.time;
}
}
tracks.push({ name, points });
}
return { tracks, totalPoints, firstTime, lastTime };
}
//# sourceMappingURL=gpx-parser.js.map