UNPKG

@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
"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 = `<?xml version="1.0" encoding="UTF-8"?> <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 = `<?xml version="1.0" encoding="UTF-8"?> <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 = `<?xml version="1.0" encoding="UTF-8"?> <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 = `<?xml version="1.0" encoding="UTF-8"?> <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 = `<?xml version="1.0" encoding="UTF-8"?> <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 = `<?xml version="1.0" encoding="UTF-8"?> <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 = `<?xml version="1.0" encoding="UTF-8"?> <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)); })); });