@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
JavaScript
"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]);