@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
150 lines (149 loc) • 7.6 kB
JavaScript
;
Object.defineProperty(exports, "__esModule", { value: true });
exports.EventUtilities = void 0;
const event_1 = require("../event");
const data_activity_types_1 = require("../../data/data.activity-types");
const data_device_names_1 = require("../../data/data.device-names");
const privacy_class_interface_1 = require("../../privacy/privacy.class.interface");
const data_description_1 = require("../../data/data.description");
const activity_utilities_1 = require("./activity.utilities");
const data_power_curve_1 = require("../../data/data.power-curve");
const data_duration_1 = require("../../data/data.duration");
const data_power_1 = require("../../data/data.power");
const data_power_watts_per_kg_1 = require("../../data/data.power-watts-per-kg");
class EventUtilities {
static mergeEvents(events) {
events.sort((eventA, eventB) => {
return +eventA.getFirstActivity().startDate - +eventB.getFirstActivity().startDate;
});
const activities = events
.reduce((activitiesArray, event) => {
activitiesArray.push(...event.getActivities());
return activitiesArray;
}, [])
.map(activity => {
return activity.setID(null);
});
const event = new event_1.Event(`Merged at ${new Date().toISOString()}`, activities[0].startDate, activities[activities.length - 1].endDate, events[0].srcFileType, privacy_class_interface_1.Privacy.Private, `A merge of 2 or more activities `, true);
event.addActivities(activities);
this.generateStatsForAll(event);
return event;
}
static generateStatsForAll(event) {
// First generate that stats on the activity it self
event.getActivities().forEach((activity) => {
activity_utilities_1.ActivityUtilities.generateMissingStreamsAndStatsForActivity(activity);
});
this.reGenerateStatsForEvent(event);
}
/**
* Regenerates event-level stats from the current in-memory event state.
*
* Important behavior:
* - Clears ONLY event stats (`event.clearStats()`).
* - Does NOT clear activity stats.
* - Does NOT read the original imported file (FIT/TCX/GPX/etc.); raw source payload is not retained.
*
* Regeneration model:
* - Single-activity event: copies current activity stats into the event.
* - Multi-activity event: recomputes event summary stats from activity stats and aggregates power curves.
*
* Interaction with `generateStatsForAll(event)`:
* - `generateStatsForAll` calls activity generation first (`generateMissingStreamsAndStatsForActivity`),
* then calls this method.
* - Activity generation is "fill missing", not "replace existing": existing activity stats are preserved.
* - If activity stats were cleared or removed by caller code before regeneration, missing stats are recomputed
* from streams (for example, Average Speed from speed stream mean, Ascent from altitude gain).
*
* Distance behavior:
* - Event distance is rebuilt from activities during event summarization.
* - Activity distance is recomputed only when missing or equal to zero (in activity generation path).
*/
static reGenerateStatsForEvent(event) {
event.clearStats();
event.startDate = event.getFirstActivity().startDate;
event.endDate = event.getLastActivity().endDate;
event.addStat(new data_activity_types_1.DataActivityTypes(event.getActivities().map(activity => activity.type)));
event.addStat(new data_device_names_1.DataDeviceNames(event.getActivities().map(activity => activity.creator.name)));
// If only one
if (event.getActivities().length === 1) {
const firstActivity = event.getFirstActivity();
firstActivity.getStats().forEach(stat => {
event.addStat(stat);
});
if (firstActivity.powerCurve) {
event.powerCurve = firstActivity.powerCurve;
}
// Add the description
const description = event.getStat(data_description_1.DataDescription.type);
if (description && description.getValue()) {
event.description = description.getValue();
}
return;
}
event.startDate = event.getFirstActivity().startDate;
event.endDate = event.getLastActivity().endDate;
// Standard stat summarization
activity_utilities_1.ActivityUtilities.getSummaryStatsForActivities(event.getActivities()).forEach(stat => event.addStat(stat));
// Special handling for Power Curve Aggregation
this.aggregatePowerCurves(event);
// Calculate Critical Power & W' for the aggregated curve
const cpWPrime = activity_utilities_1.ActivityUtilities.calculateCriticalPowerAndWPrime(event);
if (cpWPrime) {
event.addStat(cpWPrime.cp);
event.addStat(cpWPrime.wPrime);
}
}
static aggregatePowerCurves(event) {
const activities = event.getActivities();
if (activities.length <= 1)
return; // Already handled by single activity logic or no need to merge
// Map duration (seconds) -> { maxPower: number, maxWKg: number }
const mergedCurve = new Map();
activities.forEach(activity => {
const curveStat = activity.getStat(data_power_curve_1.DataPowerCurve.type);
if (curveStat && curveStat.getValue()) {
const points = curveStat.getValue();
points.forEach(point => {
const duration = point.duration.getValue();
const power = point.power.getValue();
const wKg = point.wattsPerKg ? point.wattsPerKg.getValue() : 0;
if (!mergedCurve.has(duration)) {
mergedCurve.set(duration, { maxPower: power, maxWKg: wKg });
}
else {
const current = mergedCurve.get(duration);
if (power > current.maxPower) {
current.maxPower = power;
// Usually max power implies max W/kg, but if weight changed mid-event, we take the W/kg associated with this power or just max W/kg?
// Let's take max W/kg independently to be safe/best-effort.
}
if (wKg > current.maxWKg) {
current.maxWKg = wKg;
}
}
});
}
});
if (mergedCurve.size > 0) {
const curvePoints = [];
// Sort by duration
const sortedDurations = Array.from(mergedCurve.keys()).sort((a, b) => a - b);
for (const duration of sortedDurations) {
const data = mergedCurve.get(duration);
const point = {
duration: new data_duration_1.DataDuration(duration),
power: new data_power_1.DataPower(data.maxPower)
};
if (data.maxWKg > 0) {
point.wattsPerKg = new data_power_watts_per_kg_1.DataPowerWattsPerKg(data.maxWKg);
}
curvePoints.push(point);
}
const powerCurve = new data_power_curve_1.DataPowerCurve(curvePoints);
event.addStat(powerCurve);
event.powerCurve = powerCurve;
}
}
}
exports.EventUtilities = EventUtilities;