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

150 lines (149 loc) 7.6 kB
"use strict"; 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;