@sports-alliance/sports-lib
Version:
A Library to for importing / exporting and processing GPX, TCX, FIT and JSON files from services such as Strava, Movescount, Garmin, Polar etc
356 lines (355 loc) • 22.1 kB
JavaScript
"use strict";
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
if (k2 === undefined) k2 = k;
var desc = Object.getOwnPropertyDescriptor(m, k);
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
desc = { enumerable: true, get: function() { return m[k]; } };
}
Object.defineProperty(o, k2, desc);
}) : (function(o, m, k, k2) {
if (k2 === undefined) k2 = k;
o[k2] = m[k];
}));
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
Object.defineProperty(o, "default", { enumerable: true, value: v });
}) : function(o, v) {
o["default"] = v;
});
var __importStar = (this && this.__importStar) || (function () {
var ownKeys = function(o) {
ownKeys = Object.getOwnPropertyNames || function (o) {
var ar = [];
for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
return ar;
};
return ownKeys(o);
};
return function (mod) {
if (mod && mod.__esModule) return mod;
var result = {};
if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
__setModuleDefault(result, mod);
return result;
};
})();
var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) {
function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); }
return new (P || (P = Promise))(function (resolve, reject) {
function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } }
function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } }
function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); }
step((generator = generator.apply(thisArg, _arguments || [])).next());
});
};
Object.defineProperty(exports, "__esModule", { value: true });
const fs = __importStar(require("fs"));
const path = __importStar(require("path"));
const xmldom_1 = require("@xmldom/xmldom");
const activity_types_1 = require("../../../../activities/activity.types");
const data_altitude_1 = require("../../../../data/data.altitude");
const data_ascent_1 = require("../../../../data/data.ascent");
const data_descent_1 = require("../../../../data/data.descent");
const data_distance_1 = require("../../../../data/data.distance");
const data_gnss_distance_miles_1 = require("../../../../data/data.gnss-distance-miles");
const data_grade_1 = require("../../../../data/data.grade");
const data_grade_avg_1 = require("../../../../data/data.grade-avg");
const data_grade_max_1 = require("../../../../data/data.grade-max");
const data_grade_min_1 = require("../../../../data/data.grade-min");
const data_latitude_degrees_1 = require("../../../../data/data.latitude-degrees");
const data_pace_1 = require("../../../../data/data.pace");
const data_speed_1 = require("../../../../data/data.speed");
const index_1 = require("../../../../index");
const route_parsing_options_1 = require("../../../route-parsing-options");
const exporter_route_gpx_1 = require("../../exporters/exporter.route.gpx");
const exporter_route_json_1 = require("../../exporters/exporter.route.json");
const importer_route_json_1 = require("../json/importer.route.json");
const importer_route_gpx_1 = require("./importer.route.gpx");
describe('RouteImporterGPX', () => {
const samplesDir = path.resolve(__dirname, '../../../../../samples/gpx');
const routeSamplePath = path.join(samplesDir, 'route.gpx');
function readRouteSample() {
return fs.readFileSync(routeSamplePath, 'utf-8');
}
it('parses route.gpx as a first-class route file', () => __awaiter(void 0, void 0, void 0, function* () {
const result = yield importer_route_gpx_1.RouteImporterGPX.getFromString(readRouteSample(), xmldom_1.DOMParser);
expect(result.name).toEqual('TEST');
expect(result.getRoutes()).toHaveLength(1);
expect(result.getWaypoints()).toHaveLength(8);
const route = result.getFirstRoute();
expect(route.activityType).toBeNull();
expect(route.getPointCount()).toBe(2987);
expect(route.hasStreamData(data_latitude_degrees_1.DataLatitudeDegrees.type)).toBe(true);
expect(route.hasStreamData(data_altitude_1.DataAltitude.type)).toBe(true);
expect(route.hasStreamData(data_grade_1.DataGrade.type)).toBe(true);
expect(route.hasStreamData(data_speed_1.DataSpeed.type)).toBe(false);
expect(route.hasStreamData(data_pace_1.DataPace.type)).toBe(false);
expect(route.getStat(data_distance_1.DataDistance.type).getValue()).toBeGreaterThan(0);
expect(route.getStat(data_ascent_1.DataAscent.type).getValue()).toBeGreaterThan(0);
expect(route.getStat(data_grade_avg_1.DataGradeAvg.type)).toBeDefined();
expect(result.getStat(data_distance_1.DataDistance.type).getValue()).toEqual(route.getStat(data_distance_1.DataDistance.type).getValue());
expect(result.getStat(data_ascent_1.DataAscent.type).getValue()).toEqual(route.getStat(data_ascent_1.DataAscent.type).getValue());
expect(result.getStat(data_descent_1.DataDescent.type).getValue()).toEqual(route.getStat(data_descent_1.DataDescent.type).getValue());
expect(result.getStat(data_grade_min_1.DataGradeMin.type).getValue()).toEqual(route.getStat(data_grade_min_1.DataGradeMin.type).getValue());
expect(result.getStat(data_grade_max_1.DataGradeMax.type).getValue()).toEqual(route.getStat(data_grade_max_1.DataGradeMax.type).getValue());
expect(result.getStat(data_grade_avg_1.DataGradeAvg.type).getValue()).toEqual(route.getStat(data_grade_avg_1.DataGradeAvg.type).getValue());
}));
it('keeps metadata time on the route file rather than fabricating activity dates', () => __awaiter(void 0, void 0, void 0, function* () {
const gpxString = `
<gpx creator="Garmin Connect" version="1.1" xmlns="http://www.topografix.com/GPX/1/1">
<metadata>
<name>Route Collection</name>
<time>2019-09-29T13:58:25.000Z</time>
</metadata>
<rte>
<name>Route A</name>
<type>Hiking</type>
<rtept lat="1" lon="2"><ele>10</ele></rtept>
<rtept lat="1.1" lon="2.1"><ele>20</ele></rtept>
</rte>
</gpx>`;
const result = yield importer_route_gpx_1.RouteImporterGPX.getFromString(gpxString, xmldom_1.DOMParser);
expect(result.createdAt.toISOString()).toEqual('2019-09-29T13:58:25.000Z');
expect(result.getFirstRoute().name).toEqual('Route A');
expect(result.getFirstRoute().activityType).toEqual(activity_types_1.ActivityTypes.Hiking);
}));
it('parses untimed GPX tracks as first-class routes', () => __awaiter(void 0, void 0, void 0, function* () {
const gpxString = `
<gpx creator="Garmin Connect" version="1.1" xmlns="http://www.topografix.com/GPX/1/1">
<trk>
<name>Untimed Track Route</name>
<type>Running</type>
<trkseg>
<trkpt lat="1" lon="2"><ele>10</ele></trkpt>
<trkpt lat="1.1" lon="2.1"><ele>20</ele></trkpt>
</trkseg>
</trk>
</gpx>`;
const result = yield importer_route_gpx_1.RouteImporterGPX.getFromString(gpxString, xmldom_1.DOMParser);
expect(result.getRoutes()).toHaveLength(1);
expect(result.getFirstRoute().name).toEqual('Untimed Track Route');
expect(result.getFirstRoute().activityType).toEqual(activity_types_1.ActivityTypes.Running);
expect(result.getFirstRoute().getPointCount()).toEqual(2);
}));
it('rejects timed GPX activity tracks by default', () => __awaiter(void 0, void 0, void 0, function* () {
const gpxString = `
<gpx creator="Garmin Connect" version="1.1" xmlns="http://www.topografix.com/GPX/1/1">
<metadata>
<name>Activity Track File</name>
<time>2019-07-11T05:36:07.000Z</time>
</metadata>
<trk>
<name>Timed Activity Track</name>
<type>cycling</type>
<trkseg>
<trkpt lat="39.80649" lon="21.00458">
<ele>908.6</ele>
<time>2019-07-11T05:36:07.000Z</time>
</trkpt>
<trkpt lat="39.80650" lon="21.00459">
<ele>909.1</ele>
<time>2019-07-11T05:36:08.000Z</time>
</trkpt>
</trkseg>
</trk>
</gpx>`;
yield expect(importer_route_gpx_1.RouteImporterGPX.getFromString(gpxString, xmldom_1.DOMParser)).rejects.toThrow('No routes found in GPX');
}));
it('parses timed GPX tracks as route geometry when explicitly enabled', () => __awaiter(void 0, void 0, void 0, function* () {
const gpxString = `
<gpx creator="Garmin Connect" version="1.1" xmlns="http://www.topografix.com/GPX/1/1">
<metadata>
<name>Activity Track File</name>
<time>2019-07-11T05:36:07.000Z</time>
</metadata>
<trk>
<name>Timed Activity Track</name>
<type>cycling</type>
<trkseg>
<trkpt lat="39.80649" lon="21.00458">
<ele>908.6</ele>
<time>2019-07-11T05:36:07.000Z</time>
</trkpt>
<trkpt lat="39.80650" lon="21.00459">
<ele>909.1</ele>
<time>2019-07-11T05:36:08.000Z</time>
</trkpt>
</trkseg>
</trk>
</gpx>`;
const result = yield index_1.SportsLib.importRoutesFromGPX(gpxString, xmldom_1.DOMParser, new route_parsing_options_1.RouteParsingOptions({ gpx: { importTimedTracksAsRoutes: true } }));
expect(result.name).toEqual('Activity Track File');
expect(result.createdAt.toISOString()).toEqual('2019-07-11T05:36:07.000Z');
expect(result.getRoutes()).toHaveLength(1);
expect(result.getFirstRoute().name).toEqual('Timed Activity Track');
expect(result.getFirstRoute().activityType).toEqual(activity_types_1.ActivityTypes.Cycling);
expect(result.getFirstRoute().getPointCount()).toEqual(2);
expect(result.getFirstRoute().getPointData()[0]).toEqual(expect.objectContaining({
latitudeDegrees: 39.80649,
longitudeDegrees: 21.00458,
altitude: 908.6
}));
expect(result.getFirstRoute().hasStreamData(data_latitude_degrees_1.DataLatitudeDegrees.type)).toBe(true);
expect(result.getFirstRoute().hasStreamData(data_altitude_1.DataAltitude.type)).toBe(true);
expect(result.getFirstRoute().hasStreamData(data_speed_1.DataSpeed.type)).toBe(false);
expect(result.getFirstRoute().getStat(data_distance_1.DataDistance.type).getValue()).toBeGreaterThan(0);
}));
it('does not add timed activity tracks when explicit GPX routes exist', () => __awaiter(void 0, void 0, void 0, function* () {
const gpxString = `
<gpx creator="Garmin Connect" version="1.1" xmlns="http://www.topografix.com/GPX/1/1">
<rte>
<name>Explicit Route</name>
<rtept lat="1" lon="2"><ele>10</ele></rtept>
<rtept lat="1.1" lon="2.1"><ele>20</ele></rtept>
</rte>
<trk>
<name>Timed Activity Track</name>
<trkseg>
<trkpt lat="3" lon="4">
<time>2019-07-11T05:36:07.000Z</time>
</trkpt>
<trkpt lat="3.1" lon="4.1">
<time>2019-07-11T05:36:08.000Z</time>
</trkpt>
</trkseg>
</trk>
</gpx>`;
const result = yield importer_route_gpx_1.RouteImporterGPX.getFromString(gpxString, xmldom_1.DOMParser, new route_parsing_options_1.RouteParsingOptions({ gpx: { importTimedTracksAsRoutes: true } }));
expect(result.getRoutes()).toHaveLength(1);
expect(result.getFirstRoute().name).toEqual('Explicit Route');
}));
it('dedupes compatibility GPX files that encode the same route as rte and untimed trk', () => __awaiter(void 0, void 0, void 0, function* () {
const gpxString = `
<gpx creator="Garmin Connect" version="1.1" xmlns="http://www.topografix.com/GPX/1/1">
<rte>
<name>Compatibility Route</name>
<rtept lat="1" lon="2"><ele>10</ele></rtept>
<rtept lat="1.1" lon="2.1"><ele>20</ele></rtept>
</rte>
<trk>
<name>Compatibility Track</name>
<trkseg>
<trkpt lat="1" lon="2"><ele>10</ele></trkpt>
<trkpt lat="1.1" lon="2.1"><ele>20</ele></trkpt>
</trkseg>
</trk>
</gpx>`;
const result = yield importer_route_gpx_1.RouteImporterGPX.getFromString(gpxString, xmldom_1.DOMParser);
expect(result.getRoutes()).toHaveLength(1);
expect(result.getFirstRoute().name).toEqual('Compatibility Route');
}));
it('round-trips GPX route and point metadata that the route model supports', () => __awaiter(void 0, void 0, void 0, function* () {
const gpxString = `
<gpx creator="Garmin Connect" version="1.1" xmlns="http://www.topografix.com/GPX/1/1">
<rte>
<name>Metadata Route</name>
<cmt>route comment</cmt>
<desc>route description</desc>
<number>7</number>
<link href="https://example.test/route"><text>Route link</text><type>text/html</type></link>
<type>Hiking</type>
<extensions><cue><value>left</value></cue></extensions>
<rtept lat="1" lon="2">
<ele>10</ele>
<name>Point 1</name>
<cmt>point comment</cmt>
<desc>point description</desc>
<sym>Flag</sym>
<type>Waypoint</type>
<link href="https://example.test/point"><text>Point link</text></link>
</rtept>
<rtept lat="1.1" lon="2.1"><ele>20</ele></rtept>
</rte>
</gpx>`;
const routeFile = yield importer_route_gpx_1.RouteImporterGPX.getFromString(gpxString, xmldom_1.DOMParser);
const route = routeFile.getFirstRoute();
const firstPoint = route.getPointData()[0];
expect(route.comment).toEqual('route comment');
expect(route.description).toEqual('route description');
expect(route.number).toEqual(7);
expect(route.links).toEqual([{ href: 'https://example.test/route', text: 'Route link', type: 'text/html' }]);
expect(firstPoint.comment).toEqual('point comment');
expect(firstPoint.symbol).toEqual('Flag');
expect(firstPoint.links).toEqual([{ href: 'https://example.test/point', text: 'Point link', type: null }]);
const exportedGPX = yield exporter_route_gpx_1.RouteExporterGPX.getAsString(routeFile);
expect(exportedGPX).toContain('<cmt>route comment</cmt>');
expect(exportedGPX).toContain('<number>7</number>');
expect(exportedGPX).toContain('<sym>Flag</sym>');
expect(exportedGPX).toContain('<extensions><cue><value>left</value></cue></extensions>');
const reparsed = yield importer_route_gpx_1.RouteImporterGPX.getFromString(exportedGPX, xmldom_1.DOMParser);
expect(reparsed.getFirstRoute().comment).toEqual('route comment');
expect(reparsed.getFirstRoute().getPointData()[0].symbol).toEqual('Flag');
}));
it('supports route stream includeTypes for route-safe derived streams', () => __awaiter(void 0, void 0, void 0, function* () {
const result = yield importer_route_gpx_1.RouteImporterGPX.getFromString(readRouteSample(), xmldom_1.DOMParser, new route_parsing_options_1.RouteParsingOptions({
streams: { includeTypes: [data_distance_1.DataDistance.type, data_gnss_distance_miles_1.DataGNSSDistanceMiles.type] }
}));
const streamTypes = result
.getFirstRoute()
.getAllStreams()
.map(stream => stream.type);
expect(new Set(streamTypes)).toEqual(new Set([data_distance_1.DataDistance.type, data_gnss_distance_miles_1.DataGNSSDistanceMiles.type]));
}));
it('rejects unsupported activity-only includeTypes for routes', () => __awaiter(void 0, void 0, void 0, function* () {
yield expect(importer_route_gpx_1.RouteImporterGPX.getFromString(readRouteSample(), xmldom_1.DOMParser, new route_parsing_options_1.RouteParsingOptions({ streams: { includeTypes: [data_speed_1.DataSpeed.type] } }))).rejects.toThrow('Unsupported route stream includeTypes');
}));
it('rejects unknown includeTypes for routes', () => __awaiter(void 0, void 0, void 0, function* () {
yield expect(importer_route_gpx_1.RouteImporterGPX.getFromString(readRouteSample(), xmldom_1.DOMParser, new route_parsing_options_1.RouteParsingOptions({ streams: { includeTypes: ['Not A Stream Type'] } }))).rejects.toThrow('Unknown route stream includeTypes');
}));
it('round-trips route files through native JSON', () => __awaiter(void 0, void 0, void 0, function* () {
const routeFile = yield index_1.SportsLib.importRoutesFromGPX(readRouteSample(), xmldom_1.DOMParser);
routeFile.setID('route-file-id');
routeFile.getFirstRoute().setID('route-id');
const json = exporter_route_json_1.RouteExporterJSON.export(routeFile);
const importedRouteFile = importer_route_json_1.RouteImporterJSON.getRouteFileFromJSON(json);
expect(json.id).toEqual('route-file-id');
expect(json.routes[0].id).toEqual('route-id');
expect(json.stats[data_distance_1.DataDistance.type]).toEqual(routeFile.getStat(data_distance_1.DataDistance.type).getValue());
expect(importedRouteFile.toJSON()).toEqual(json);
}));
it('preserves route file stats from native JSON when present', () => __awaiter(void 0, void 0, void 0, function* () {
const routeFile = yield index_1.SportsLib.importRoutesFromGPX(readRouteSample(), xmldom_1.DOMParser);
const json = exporter_route_json_1.RouteExporterJSON.export(routeFile);
json.stats[data_distance_1.DataDistance.type] = 12345;
const importedRouteFile = importer_route_json_1.RouteImporterJSON.getRouteFileFromJSON(json);
expect(importedRouteFile.getStat(data_distance_1.DataDistance.type).getValue()).toBe(12345);
expect(importedRouteFile.toJSON().stats[data_distance_1.DataDistance.type]).toBe(12345);
}));
it('regenerates route file stats from routes when native JSON has an empty stats object', () => __awaiter(void 0, void 0, void 0, function* () {
const routeFile = yield index_1.SportsLib.importRoutesFromGPX(readRouteSample(), xmldom_1.DOMParser);
const json = exporter_route_json_1.RouteExporterJSON.export(routeFile);
json.stats = {};
const importedRouteFile = importer_route_json_1.RouteImporterJSON.getRouteFileFromJSON(json);
expect(importedRouteFile.getStat(data_distance_1.DataDistance.type).getValue()).toEqual(importedRouteFile.getFirstRoute().getStat(data_distance_1.DataDistance.type).getValue());
expect(importedRouteFile.toJSON().stats[data_distance_1.DataDistance.type]).toBeDefined();
}));
it('regenerates route file stats from routes for older native JSON payloads', () => __awaiter(void 0, void 0, void 0, function* () {
const routeFile = yield index_1.SportsLib.importRoutesFromGPX(readRouteSample(), xmldom_1.DOMParser);
const json = exporter_route_json_1.RouteExporterJSON.export(routeFile);
delete json.stats;
const importedRouteFile = importer_route_json_1.RouteImporterJSON.getRouteFileFromJSON(json);
expect(importedRouteFile.getStat(data_distance_1.DataDistance.type).getValue()).toEqual(importedRouteFile.getFirstRoute().getStat(data_distance_1.DataDistance.type).getValue());
expect(importedRouteFile.toJSON().stats[data_distance_1.DataDistance.type]).toBeDefined();
}));
it('rejects route JSON when point geometry conflicts with geometry streams', () => __awaiter(void 0, void 0, void 0, function* () {
const routeFile = yield index_1.SportsLib.importRoutesFromGPX(readRouteSample(), xmldom_1.DOMParser);
const json = exporter_route_json_1.RouteExporterJSON.export(routeFile);
const latitudeStream = Array.isArray(json.routes[0].streams)
? json.routes[0].streams.find(stream => stream.type === data_latitude_degrees_1.DataLatitudeDegrees.type)
: null;
latitudeStream.data[0] = latitudeStream.data[0] + 1;
expect(() => importer_route_json_1.RouteImporterJSON.getRouteFileFromJSON(json)).toThrow('Route JSON geometry conflict');
}));
it('exports routes to GPX rte/rtept elements without activity tracks', () => __awaiter(void 0, void 0, void 0, function* () {
const routeFile = yield importer_route_gpx_1.RouteImporterGPX.getFromString(readRouteSample(), xmldom_1.DOMParser);
const gpxString = yield exporter_route_gpx_1.RouteExporterGPX.getAsString(routeFile);
expect(gpxString).toContain('<rte>');
expect(gpxString).toContain('<rtept');
expect(gpxString).not.toContain('<trk>');
const parsedExport = yield importer_route_gpx_1.RouteImporterGPX.getFromString(gpxString, xmldom_1.DOMParser);
expect(parsedExport.getFirstRoute().getPointCount()).toEqual(routeFile.getFirstRoute().getPointCount());
}));
it('exports routes to GPX through the public SportsLib API', () => __awaiter(void 0, void 0, void 0, function* () {
const routeFile = yield index_1.SportsLib.importRoutesFromGPX(readRouteSample(), xmldom_1.DOMParser);
yield expect(index_1.SportsLib.exportRoutesToGPX(routeFile)).resolves.toEqual(yield exporter_route_gpx_1.RouteExporterGPX.getAsString(routeFile));
}));
});