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

316 lines (315 loc) 16.9 kB
"use strict"; Object.defineProperty(exports, "__esModule", { value: true }); exports.RouteUtilities = void 0; 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_altitude_1 = require("../data/data.altitude"); const data_altitude_avg_1 = require("../data/data.altitude-avg"); const data_altitude_max_1 = require("../data/data.altitude-max"); const data_altitude_min_1 = require("../data/data.altitude-min"); const data_altitude_smooth_1 = require("../data/data.altitude-smooth"); const data_end_altitude_1 = require("../data/data.end-altitude"); const data_gnss_distance_1 = require("../data/data.gnss-distance"); 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_grade_smooth_1 = require("../data/data.grade-smooth"); const data_latitude_degrees_1 = require("../data/data.latitude-degrees"); const data_longitude_degrees_1 = require("../data/data.longitude-degrees"); const data_start_altitude_1 = require("../data/data.start-altitude"); const data_store_1 = require("../data/data.store"); const parsing_event_lib_error_1 = require("../errors/parsing-event-lib.error"); const geolib_adapter_1 = require("../geodesy/adapters/geolib.adapter"); const grade_calculator_1 = require("../events/utilities/grade-calculator/grade-calculator"); const helpers_1 = require("../events/utilities/helpers"); const low_pass_filter_1 = require("../events/utilities/grade-calculator/low-pass-filter"); const route_stream_1 = require("./route-stream"); const ALTITUDE_SPIKES_FILTER_WIN = 3; class RouteUtilities { static generateMissingStreamsAndStatsForRoute(route) { this.createDerivedStreams(route); this.createDistanceStreams(route); this.createGradeStreams(route); if (route.parseOptions.generateUnitStreams) { route.addStreams(this.createUnitStreams(route)); } this.generateStats(route); if (route.parseOptions.generateUnitStreams) { this.generateUnitStats(route); } this.pruneStreamsByIncludeTypes(route); return route; } static getSupportedRouteStreamTypes() { const unitTypes = Array.from(this.routeUnitBaseStreamTypes).reduce((accu, baseStreamType) => { return accu.concat(Object.keys(data_store_1.DynamicDataLoader.dataTypeUnitGroups[baseStreamType] || {})); }, []); return Array.from(new Set([...Array.from(this.baseRouteStreamTypes), ...unitTypes])); } static createDerivedStreams(route) { var _a, _b, _c; if (((_c = (_b = (_a = route.parseOptions) === null || _a === void 0 ? void 0 : _a.streams) === null || _b === void 0 ? void 0 : _b.smooth) === null || _c === void 0 ? void 0 : _c.altitudeSmooth) && route.hasStreamData(data_altitude_1.DataAltitude.type) && !route.hasStreamData(data_altitude_smooth_1.DataAltitudeSmooth.type)) { route.addStream(new route_stream_1.RouteStream(data_altitude_smooth_1.DataAltitudeSmooth.type, Array.from(route.getStreamData(data_altitude_1.DataAltitude.type)))); this.shapeStream(route, data_altitude_smooth_1.DataAltitudeSmooth.type, squashedAltData => { squashedAltData = (0, helpers_1.medianFilter)(squashedAltData, ALTITUDE_SPIKES_FILTER_WIN); return low_pass_filter_1.LowPassFilter.smooth(squashedAltData); }); } } static createDistanceStreams(route) { if (route.hasStreamData(data_latitude_degrees_1.DataLatitudeDegrees.type) && route.hasStreamData(data_longitude_degrees_1.DataLongitudeDegrees.type) && (!route.hasStreamData(data_distance_1.DataDistance.type) || !route.hasStreamData(data_gnss_distance_1.DataGNSSDistance.type))) { const streamData = Array(route.getPointCount()).fill(null); let distance = 0; let previousPosition = null; route.getPositionData().forEach((position, index) => { if (!position) { return; } if (previousPosition) { distance += this.geoLibAdapter.getDistance([previousPosition, position]); } streamData[index] = this.round(distance, 2); previousPosition = position; }); if (!route.hasStreamData(data_distance_1.DataDistance.type)) { route.addStream(new route_stream_1.RouteStream(data_distance_1.DataDistance.type, streamData)); } if (!route.hasStreamData(data_gnss_distance_1.DataGNSSDistance.type)) { route.addStream(new route_stream_1.RouteStream(data_gnss_distance_1.DataGNSSDistance.type, streamData)); } } } static createGradeStreams(route) { var _a, _b, _c, _d, _f, _g; if (((_c = (_b = (_a = route.parseOptions) === null || _a === void 0 ? void 0 : _a.streams) === null || _b === void 0 ? void 0 : _b.smooth) === null || _c === void 0 ? void 0 : _c.grade) && route.hasStreamData(data_distance_1.DataDistance.type) && (route.hasStreamData(data_altitude_smooth_1.DataAltitudeSmooth.type) || route.hasStreamData(data_altitude_1.DataAltitude.type)) && !route.hasStreamData(data_grade_1.DataGrade.type)) { const altitudeStreamType = route.hasStreamData(data_altitude_smooth_1.DataAltitudeSmooth.type) ? data_altitude_smooth_1.DataAltitudeSmooth.type : data_altitude_1.DataAltitude.type; const gradeStreamData = grade_calculator_1.GradeCalculator.computeGradeStreamByDistance(route.getStreamData(data_distance_1.DataDistance.type), route.getStreamData(altitudeStreamType)); route.addStream(new route_stream_1.RouteStream(data_grade_1.DataGrade.type, gradeStreamData)); if ((_g = (_f = (_d = route.parseOptions) === null || _d === void 0 ? void 0 : _d.streams) === null || _f === void 0 ? void 0 : _f.smooth) === null || _g === void 0 ? void 0 : _g.gradeSmooth) { route.addStream(new route_stream_1.RouteStream(data_grade_smooth_1.DataGradeSmooth.type, Array.from(gradeStreamData))); this.shapeStream(route, data_grade_smooth_1.DataGradeSmooth.type, squashedGradeData => { return low_pass_filter_1.LowPassFilter.smooth(squashedGradeData); }); } } } static createUnitStreams(route) { const existingStreamTypes = new Set(route.getAllStreams().map(stream => stream.type)); return Array.from(this.routeUnitBaseStreamTypes).reduce((streams, baseDataType) => { if (!route.hasStreamData(baseDataType)) { return streams; } Object.keys(data_store_1.DynamicDataLoader.dataTypeUnitGroups[baseDataType] || {}).forEach(unitBasedDataType => { if (existingStreamTypes.has(unitBasedDataType)) { return; } streams.push(new route_stream_1.RouteStream(unitBasedDataType, route.getStreamData(baseDataType).map(dataValue => { if (!(0, helpers_1.isNumber)(dataValue)) { return null; } return data_store_1.DynamicDataLoader.dataTypeUnitGroups[baseDataType][unitBasedDataType](dataValue); }))); }); return streams; }, []); } static generateStats(route) { if (!route.getStat(data_distance_1.DataDistance.type) && route.hasStreamData(data_distance_1.DataDistance.type)) { const lastDistance = this.getDataTypeLast(route, data_distance_1.DataDistance.type); if (lastDistance !== null) { route.addStat(new data_distance_1.DataDistance(lastDistance)); } } if (!route.getStat(data_gnss_distance_1.DataGNSSDistance.type) && route.hasStreamData(data_gnss_distance_1.DataGNSSDistance.type)) { const lastDistance = this.getDataTypeLast(route, data_gnss_distance_1.DataGNSSDistance.type); if (lastDistance !== null) { route.addStat(new data_gnss_distance_1.DataGNSSDistance(lastDistance)); } } const altitudeStreamType = route.hasStreamData(data_altitude_smooth_1.DataAltitudeSmooth.type) ? data_altitude_smooth_1.DataAltitudeSmooth.type : route.hasStreamData(data_altitude_1.DataAltitude.type) ? data_altitude_1.DataAltitude.type : null; if (altitudeStreamType && this.hasNumericData(route, altitudeStreamType)) { if (!route.getStat(data_ascent_1.DataAscent.type)) { route.addStat(new data_ascent_1.DataAscent(this.getDataTypeGain(route, altitudeStreamType))); } if (!route.getStat(data_descent_1.DataDescent.type)) { route.addStat(new data_descent_1.DataDescent(this.getDataTypeLoss(route, altitudeStreamType))); } if (!route.getStat(data_altitude_max_1.DataAltitudeMax.type)) { route.addStat(new data_altitude_max_1.DataAltitudeMax(this.getDataTypeMax(route, altitudeStreamType))); } if (!route.getStat(data_altitude_min_1.DataAltitudeMin.type)) { route.addStat(new data_altitude_min_1.DataAltitudeMin(this.getDataTypeMin(route, altitudeStreamType))); } if (!route.getStat(data_altitude_avg_1.DataAltitudeAvg.type)) { route.addStat(new data_altitude_avg_1.DataAltitudeAvg(this.getDataTypeAvg(route, altitudeStreamType))); } if (!route.getStat(data_start_altitude_1.DataStartAltitude.type)) { const startAltitude = this.getDataTypeFirst(route, altitudeStreamType); if (startAltitude !== null) { route.addStat(new data_start_altitude_1.DataStartAltitude(startAltitude)); } } if (!route.getStat(data_end_altitude_1.DataEndAltitude.type)) { const endAltitude = this.getDataTypeLast(route, altitudeStreamType); if (endAltitude !== null) { route.addStat(new data_end_altitude_1.DataEndAltitude(endAltitude)); } } } const gradeStreamType = route.hasStreamData(data_grade_smooth_1.DataGradeSmooth.type) ? data_grade_smooth_1.DataGradeSmooth.type : route.hasStreamData(data_grade_1.DataGrade.type) ? data_grade_1.DataGrade.type : null; if (gradeStreamType && this.hasNumericData(route, gradeStreamType)) { if (!route.getStat(data_grade_max_1.DataGradeMax.type)) { route.addStat(new data_grade_max_1.DataGradeMax(this.getDataTypeMax(route, gradeStreamType))); } if (!route.getStat(data_grade_min_1.DataGradeMin.type)) { route.addStat(new data_grade_min_1.DataGradeMin(this.getDataTypeMin(route, gradeStreamType))); } if (!route.getStat(data_grade_avg_1.DataGradeAvg.type)) { route.addStat(new data_grade_avg_1.DataGradeAvg(this.getDataTypeAvg(route, gradeStreamType))); } } } static generateUnitStats(route) { Array.from(this.routeUnitBaseStreamTypes).forEach(baseDataType => { const stat = route.getStat(baseDataType); if (!stat) { return; } Object.keys(data_store_1.DynamicDataLoader.dataTypeUnitGroups[baseDataType] || {}).forEach(unitBasedDataType => { if (route.getStat(unitBasedDataType)) { return; } route.addStat(data_store_1.DynamicDataLoader.getDataInstanceFromDataType(unitBasedDataType, data_store_1.DynamicDataLoader.dataTypeUnitGroups[baseDataType][unitBasedDataType](stat.getValue()))); }); }); } static pruneStreamsByIncludeTypes(route) { var _a, _b; const includeTypes = ((_b = (_a = route.parseOptions) === null || _a === void 0 ? void 0 : _a.streams) === null || _b === void 0 ? void 0 : _b.includeTypes) || []; if (!includeTypes.length) { return; } this.assertSupportedIncludeTypes(includeTypes); route .getAllStreams() .slice() .forEach(stream => { if (includeTypes.indexOf(stream.type) === -1) { route.removeStream(stream); } }); } static assertSupportedIncludeTypes(includeTypes) { const unknownTypes = includeTypes.filter(streamType => !this.isKnownDataType(streamType)); if (unknownTypes.length) { throw new parsing_event_lib_error_1.ParsingEventLibError(`Unknown route stream includeTypes: ${unknownTypes.join(', ')}`); } const supportedTypes = new Set(this.getSupportedRouteStreamTypes()); const unsupportedTypes = includeTypes.filter(streamType => !supportedTypes.has(streamType)); if (unsupportedTypes.length) { throw new parsing_event_lib_error_1.ParsingEventLibError(`Unsupported route stream includeTypes: ${unsupportedTypes.join(', ')}`); } } static isKnownDataType(streamType) { try { data_store_1.DynamicDataLoader.getDataClassFromDataType(streamType); return true; } catch (_e) { return false; } } static shapeStream(route, streamType, shape) { const data = route.getStreamData(streamType); const numericIndexes = []; const squashedData = data.reduce((accu, value, index) => { if (!(0, helpers_1.isNumber)(value)) { return accu; } numericIndexes.push(index); accu.push(value); return accu; }, []); const shapedData = shape(squashedData); numericIndexes.forEach((sourceIndex, shapedIndex) => { data[sourceIndex] = this.round(shapedData[shapedIndex], 2); }); } static getNumericData(route, dataType) { return route.getSquashedStreamData(dataType).filter(Number.isFinite); } static hasNumericData(route, dataType) { return this.getNumericData(route, dataType).length > 0; } static getDataTypeMax(route, dataType) { return Math.max(...this.getNumericData(route, dataType)); } static getDataTypeMin(route, dataType) { return Math.min(...this.getNumericData(route, dataType)); } static getDataTypeAvg(route, dataType) { const data = this.getNumericData(route, dataType); return this.round(data.reduce((sum, value) => sum + value, 0) / data.length, 2); } static getDataTypeFirst(route, dataType) { var _a; return (_a = this.getNumericData(route, dataType)[0]) !== null && _a !== void 0 ? _a : null; } static getDataTypeLast(route, dataType) { var _a; const data = this.getNumericData(route, dataType); return (_a = data[data.length - 1]) !== null && _a !== void 0 ? _a : null; } static getDataTypeGain(route, dataType) { return this.getDataTypeDeltaSum(route, dataType, delta => delta > 0); } static getDataTypeLoss(route, dataType) { return this.getDataTypeDeltaSum(route, dataType, delta => delta < 0); } static getDataTypeDeltaSum(route, dataType, predicate) { const data = this.getNumericData(route, dataType); return this.round(data.reduce((sum, value, index) => { if (index === 0) { return sum; } const delta = value - data[index - 1]; return predicate(delta) ? sum + Math.abs(delta) : sum; }, 0), 2); } static round(value, precision = 2) { const factor = Math.pow(10, precision); return Math.round(value * factor) / factor; } } exports.RouteUtilities = RouteUtilities; RouteUtilities.geoLibAdapter = new geolib_adapter_1.GeoLibAdapter(); RouteUtilities.baseRouteStreamTypes = new Set([ data_latitude_degrees_1.DataLatitudeDegrees.type, data_longitude_degrees_1.DataLongitudeDegrees.type, data_altitude_1.DataAltitude.type, data_altitude_smooth_1.DataAltitudeSmooth.type, data_distance_1.DataDistance.type, data_gnss_distance_1.DataGNSSDistance.type, data_grade_1.DataGrade.type, data_grade_smooth_1.DataGradeSmooth.type ]); RouteUtilities.routeUnitBaseStreamTypes = new Set([data_distance_1.DataDistance.type, data_gnss_distance_1.DataGNSSDistance.type]);