@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
338 lines (337 loc) • 14.9 kB
JavaScript
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
exports.Activity = void 0;
const activity_types_1 = require("./activity.types");
const duration_class_abstract_1 = require("../duration/duration.class.abstract");
const data_latitude_degrees_1 = require("../data/data.latitude-degrees");
const data_longitude_degrees_1 = require("../data/data.longitude-degrees");
const stream_1 = require("../streams/stream");
const helpers_1 = require("../events/utilities/helpers");
const data_power_1 = require("../data/data.power");
const data_start_event_1 = require("../data/data.start-event");
const data_stop_event_1 = require("../data/data.stop-event");
const data_stop_all_event_1 = require("../data/data.stop-all-event");
const data_time_1 = require("../data/data.time");
const data_ibi_1 = require("../data/data.ibi");
const activity_utilities_1 = require("../events/utilities/activity.utilities");
const data_distance_1 = require("../data/data.distance");
const data_duration_1 = require("../data/data.duration");
const data_rider_position_change_event_1 = require("../data/data.rider-position-change-event");
const activity_parsing_options_1 = require("./activity-parsing-options");
const parsing_event_lib_error_1 = require("../errors/parsing-event-lib.error");
const duration_exceeded_event_lib_error_1 = require("../errors/duration-exceeded-event-lib.error");
class Activity extends duration_class_abstract_1.DurationClassAbstract {
constructor(startDate, endDate, type, creator, options = activity_parsing_options_1.ActivityParsingOptions.DEFAULT, name = '') {
super(startDate, endDate);
this.intensityZones = []; // maybe rename
this.laps = [];
this.swimLengths = [];
this.streams = [];
this.events = [];
if (!startDate || !endDate) {
throw new parsing_event_lib_error_1.ParsingEventLibError('Start and end dates are required');
}
if (endDate < startDate) {
throw new parsing_event_lib_error_1.ParsingEventLibError('Activity end date is before the start date and that is not acceptable');
}
if (+endDate - +startDate > options.maxActivityDurationDays * 24 * 60 * 60 * 1000) {
throw new duration_exceeded_event_lib_error_1.DurationExceededEventLibError(`Activity duration exceeds ${options.maxActivityDurationDays} days. That is not supported`);
}
this.type = type;
this.creator = creator;
this.parseOptions = options;
this.name = name;
}
createStream(type) {
const stream = new stream_1.Stream(type, Array(activity_utilities_1.ActivityUtilities.getDataLength(this.startDate, this.endDate)).fill(null));
return stream;
}
addDataToStream(type, date, value) {
this.getStreamData(type)[this.getDateIndex(date)] = value;
return this;
}
addStream(stream) {
if (this.streams.find(activityStream => activityStream.type === stream.type)) {
throw new Error(`Duplicate type of stream when adding ${stream.type} to activity ${this.getID()}`);
}
this.streams.push(stream);
return this;
}
clearStreams() {
this.streams = [];
return this;
}
removeStream(streamType) {
const stream = streamType instanceof stream_1.Stream ? streamType : this.getStream(streamType);
this.streams = this.streams.filter(activityStream => stream !== activityStream);
return this;
}
replaceStreamData(streamType, data) {
this.removeStream(streamType);
this.addStream(this.createStream(streamType).setData(data));
return this;
}
addStreams(streams) {
const existingTypes = new Set(this.streams.map(stream => stream.type));
streams.forEach(stream => {
if (existingTypes.has(stream.type)) {
return;
}
this.streams.push(stream);
existingTypes.add(stream.type);
});
return this;
}
getAllStreams() {
return this.streams;
}
// @todo needs more logic improvement and transparency
getAllExportableStreams() {
return this.getAllStreams().filter(stream => stream.isExportable());
}
hasStreamData(streamType, startDate, endDate) {
try {
this.getStreamData(streamType, startDate, endDate);
}
catch (_e) {
return false;
}
return true;
}
hasPositionData(startDate, endDate) {
return (this.hasStreamData(data_latitude_degrees_1.DataLatitudeDegrees.type, startDate, endDate) &&
this.hasStreamData(data_longitude_degrees_1.DataLongitudeDegrees.type, startDate, endDate));
}
isTrainer() {
return Activity.TRAINER_TYPES.indexOf(this.type) !== -1;
}
hasPowerMeter() {
return this.hasStreamData(data_power_1.DataPower.type);
}
getStream(streamType) {
const find = this.streams.find(stream => stream.type === streamType);
if (!find) {
throw Error(`No stream found with type ${streamType}`);
}
return find;
}
getStreamData(streamType, startDate, endDate) {
const stream = streamType instanceof stream_1.Stream ? streamType : this.getStream(streamType);
if (!startDate && !endDate) {
return stream.getData();
}
if (startDate && endDate) {
return stream
.getData()
.filter((_value, index) => new Date(this.startDate.getTime() + index * 1000) <= endDate)
.filter((_value, index) => new Date(this.startDate.getTime() + index * 1000) >= startDate);
}
if (startDate) {
return stream.getData().filter((_value, index) => new Date(this.startDate.getTime() + index * 1000) > startDate);
}
if (endDate) {
return stream.getData().filter((_value, index) => new Date(this.startDate.getTime() + index * 1000) < endDate);
}
return [];
}
// @todo see how this fits with the filtering on the stream class
/**
* Gets the data array of an activity stream excluding the non numeric ones
* @todo include strings and all data abstract types
* @param streamType
* @param startDate
* @param endDate
*/
getSquashedStreamData(streamType, startDate, endDate) {
return this.getStreamData(streamType, startDate, endDate).filter(data => (0, helpers_1.isNumber)(data));
}
getPositionData(startDate, endDate) {
const latitudeStreamData = this.getStreamData(data_latitude_degrees_1.DataLatitudeDegrees.type, startDate, endDate);
const longitudeStreamData = this.getStreamData(data_longitude_degrees_1.DataLongitudeDegrees.type, startDate, endDate);
return latitudeStreamData.reduce((positionArray, _value, index, _array) => {
const currentLatitude = latitudeStreamData[index];
const currentLongitude = longitudeStreamData[index];
if (!(0, helpers_1.isNumber)(currentLatitude) || !(0, helpers_1.isNumber)(currentLongitude)) {
positionArray.push(null);
return positionArray;
}
positionArray.push({
latitudeDegrees: currentLatitude,
longitudeDegrees: currentLongitude
});
return positionArray;
}, []);
}
getSquashedPositionData(startDate, endDate) {
return this.getPositionData(startDate, endDate).filter(data => data !== null);
}
getStreamDataTypesBasedOnDataType(streamTypeToBaseOn, streamTypes) {
return activity_utilities_1.ActivityUtilities.getStreamDataTypesBasedOnDataType(this.getStream(streamTypeToBaseOn), this.getAllStreams()
.filter(stream => stream.type !== streamTypeToBaseOn)
.filter(stream => streamTypes.indexOf(stream.type) !== -1));
}
getStreamDataTypesBasedOnTime(streamTypes) {
return activity_utilities_1.ActivityUtilities.getStreamDataTypesBasedOnTime(this.startDate, this.endDate, !streamTypes
? this.getAllStreams()
: this.getAllStreams().filter(stream => streamTypes.indexOf(stream.type) !== -1));
}
getStreamDataByTime(streamType, filterNull = false, filterInfinity = false) {
return this.getStream(streamType).getStreamDataByTime(this.startDate, filterNull, filterInfinity);
}
getStreamDataByDuration(streamType, filterNull = false, filterInfinity = false) {
return this.getStream(streamType).getStreamDataByDuration(0, filterNull, filterInfinity);
}
addLap(lap) {
this.laps.push(lap);
return this;
}
getLaps(_activity) {
return this.laps;
}
hasLaps() {
return this.laps.length > 0;
}
getSwimLengths() {
return this.swimLengths;
}
addSwimLength(swimLength) {
this.swimLengths.push(swimLength);
return this;
}
setSwimLengths(swimLengths) {
this.swimLengths = swimLengths;
return this;
}
hasSwimLengths() {
return this.swimLengths.length > 0;
}
getAllEvents() {
return this.events;
}
getStartEvents() {
return this.events.filter(event => event instanceof data_start_event_1.DataStartEvent);
}
getStopEvents() {
return this.events.filter(event => event instanceof data_stop_event_1.DataStopEvent);
}
getStopAllEvents() {
return this.events.filter(event => event instanceof data_stop_all_event_1.DataStopAllEvent);
}
getAllRiderPositionChangeEvents() {
return this.events.filter(event => event instanceof data_rider_position_change_event_1.DataRiderPositionChangeEvent);
}
addEvent(event) {
this.events.push(event);
return this;
}
setAllEvents(events) {
this.events = events;
return this;
}
generateTimeStream(streamTypes = []) {
let streams = this.getAllStreams();
if (streamTypes.length) {
streams = streams.filter(stream => streamTypes.indexOf(stream.type) !== -1);
}
else {
// Irregular event streams should not define the default 1 Hz occupancy grid.
streams = streams.filter(stream => stream.type !== data_ibi_1.DataIBI.type);
}
const timeStream = new stream_1.Stream(data_time_1.DataTime.type, Array(this.getTimeGridLength(streams)).fill(null));
const timeStreamData = timeStream.getData();
streams.forEach(stream => {
this.getStreamDataByDuration(stream.type, true, false).forEach((data) => {
const timeIndex = stream.type === data_ibi_1.DataIBI.type ? Activity.projectMillisecondsToSecondGrid(data.time) : data.time / 1000;
if (Number.isInteger(timeIndex) && timeIndex >= 0 && timeIndex < timeStreamData.length) {
timeStreamData[timeIndex] = timeIndex;
}
});
});
return timeStream;
}
generateDurationStream(streamTypes = []) {
const timeStream = this.generateTimeStream(streamTypes);
const durationStream = this.createStream(data_duration_1.DataDuration.type);
durationStream.setData(timeStream.getData());
return durationStream;
}
getDateIndex(date) {
return Activity.projectMillisecondsToSecondGrid(+date - +this.startDate);
}
static projectMillisecondsToSecondGrid(milliseconds) {
// The library's generic stream model is 1 Hz, so irregular timestamps must be projected onto whole-second slots.
return Math.round(milliseconds / 1000);
}
getTimeGridLength(streams) {
const activityLength = activity_utilities_1.ActivityUtilities.getDataLength(this.startDate, this.endDate);
return streams.reduce((maxLength, stream) => {
if (stream.type === data_ibi_1.DataIBI.type) {
const ibiLength = stream
.getStreamDataByDuration(0, true, false)
.reduce((maxTimeIndex, data) => Math.max(maxTimeIndex, Activity.projectMillisecondsToSecondGrid(data.time) + 1), 0);
return Math.max(maxLength, ibiLength);
}
return Math.max(maxLength, stream.getData().length);
}, activityLength);
}
toJSON() {
const intensityZones = [];
this.intensityZones.forEach((value) => {
intensityZones.push(value.toJSON());
});
const stats = {};
this.stats.forEach((value, _key) => {
Object.assign(stats, value.toJSON());
});
// Fetch streams from activity
const streams = this.getAllStreams().reduce((streams, stream) => {
streams.push(stream.toJSON());
return streams;
}, []);
// Now append missing time stream to JSON export
if (streams === null || streams === void 0 ? void 0 : streams.length) {
const stream = streams.find(s => s.type === data_distance_1.DataDistance.type) || streams[0];
const timeStream = this.generateTimeStream([stream.type]).toJSON();
streams.push(timeStream);
}
const activityJSON = {
name: this.name || null,
startDate: this.startDate.getTime(),
endDate: this.endDate.getTime(),
type: this.type,
creator: this.creator.toJSON(),
intensityZones: intensityZones,
powerMeter: this.hasPowerMeter(),
trainer: this.isTrainer(),
powerCurve: this.powerCurve ? this.powerCurve.toJSON() : null,
stats: stats,
streams: streams,
events: this.getAllEvents().reduce((eventsArray, event) => {
eventsArray.push(event.toJSON());
return eventsArray;
}, []),
laps: this.getLaps().reduce((jsonLapsArray, lap) => {
jsonLapsArray.push(lap.toJSON(this));
return jsonLapsArray;
}, []),
swimLengths: this.getSwimLengths().reduce((jsonSwimLengthsArray, swimLength) => {
jsonSwimLengthsArray.push(swimLength.toJSON());
return jsonSwimLengthsArray;
}, [])
};
return activityJSON;
}
}
exports.Activity = Activity;
Activity.TRAINER_TYPES = [
activity_types_1.ActivityTypes.VirtualRun,
activity_types_1.ActivityTypes.VirtualCycling,
activity_types_1.ActivityTypes.Treadmill,
activity_types_1.ActivityTypes.IndoorCycling,
activity_types_1.ActivityTypes.IndoorRunning,
activity_types_1.ActivityTypes.IndoorRowing,
activity_types_1.ActivityTypes.Crosstrainer,
activity_types_1.ActivityTypes.EllipticalTrainer,
activity_types_1.ActivityTypes.FitnessEquipment,
activity_types_1.ActivityTypes.StairStepper
];