@sports-alliance/sports-lib 6.1.11 → 6.1.13
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/jest.config.js +2 -1
- package/jest.setup.ts +9 -0
- package/lib/cjs/activities/activity.js +4 -3
- package/lib/cjs/activities/activity.types.js +2 -0
- package/lib/cjs/events/adapters/importers/fit/importer.fit.d.ts +1 -0
- package/lib/cjs/events/adapters/importers/fit/importer.fit.js +11 -4
- package/lib/cjs/events/adapters/importers/fit/importer.fit.oom.spec.d.ts +1 -0
- package/lib/cjs/events/adapters/importers/fit/importer.fit.oom.spec.js +60 -0
- package/lib/cjs/events/utilities/activity.utilities.js +23 -22
- package/lib/cjs/events/utilities/activity.utilities.spec.js +51 -2
- package/lib/esm/activities/activity.js +4 -3
- package/lib/esm/activities/activity.types.js +2 -0
- package/lib/esm/events/adapters/importers/fit/importer.fit.d.ts +1 -0
- package/lib/esm/events/adapters/importers/fit/importer.fit.js +11 -4
- package/lib/esm/events/adapters/importers/fit/importer.fit.oom.spec.d.ts +1 -0
- package/lib/esm/events/adapters/importers/fit/importer.fit.oom.spec.js +39 -0
- package/lib/esm/events/utilities/activity.utilities.js +23 -22
- package/lib/esm/events/utilities/activity.utilities.spec.js +52 -3
- package/package.json +2 -2
- package/reproduce_oom.ts +35 -0
package/jest.config.js
CHANGED
package/jest.setup.ts
ADDED
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
|
|
2
|
+
import { ActivityParsingOptions } from './src/activities/activity-parsing-options';
|
|
3
|
+
|
|
4
|
+
// Disable unit stream generation by default for tests to improve performance
|
|
5
|
+
// This creates separate arrays for km/h, mph, etc. which are expensive and rarely needed in tests
|
|
6
|
+
// unless specifically testing the charting logic or unit conversion.
|
|
7
|
+
ActivityParsingOptions.DEFAULT.generateUnitStreams = false;
|
|
8
|
+
|
|
9
|
+
console.log('Jest Setup: Disabled ActivityParsingOptions.generateUnitStreams for faster tests.');
|
|
@@ -41,7 +41,8 @@ class Activity extends duration_class_abstract_1.DurationClassAbstract {
|
|
|
41
41
|
this.name = name;
|
|
42
42
|
}
|
|
43
43
|
createStream(type) {
|
|
44
|
-
|
|
44
|
+
const stream = new stream_1.Stream(type, Array(activity_utilities_1.ActivityUtilities.getDataLength(this.startDate, this.endDate)).fill(null));
|
|
45
|
+
return stream;
|
|
45
46
|
}
|
|
46
47
|
addDataToStream(type, date, value) {
|
|
47
48
|
this.getStreamData(type)[this.getDateIndex(date)] = value;
|
|
@@ -49,7 +50,7 @@ class Activity extends duration_class_abstract_1.DurationClassAbstract {
|
|
|
49
50
|
}
|
|
50
51
|
addStream(stream) {
|
|
51
52
|
if (this.streams.find(activityStream => activityStream.type === stream.type)) {
|
|
52
|
-
throw new Error(`Duplicate type of stream when adding ${stream.type} to activity ${this.getID()}`);
|
|
53
|
+
throw new Error(`Duplicate type of stream when adding ${stream.type} to activity ${this.getID()} `);
|
|
53
54
|
}
|
|
54
55
|
this.streams.push(stream);
|
|
55
56
|
return this;
|
|
@@ -101,7 +102,7 @@ class Activity extends duration_class_abstract_1.DurationClassAbstract {
|
|
|
101
102
|
getStream(streamType) {
|
|
102
103
|
const find = this.streams.find(stream => stream.type === streamType);
|
|
103
104
|
if (!find) {
|
|
104
|
-
throw Error(`No stream found with type ${streamType}`);
|
|
105
|
+
throw Error(`No stream found with type ${streamType} `);
|
|
105
106
|
}
|
|
106
107
|
return find;
|
|
107
108
|
}
|
|
@@ -48,6 +48,7 @@ class ActivityTypesHelper {
|
|
|
48
48
|
case ActivityTypeGroups.TrailRunning:
|
|
49
49
|
return [data_pace_avg_1.DataPaceAvg.type, data_grade_adjusted_pace_avg_1.DataGradeAdjustedPaceAvg.type, data_speed_avg_1.DataSpeedAvg.type, data_grade_adjusted_speed_avg_1.DataGradeAdjustedSpeedAvg.type];
|
|
50
50
|
case ActivityTypeGroups.WaterSports:
|
|
51
|
+
case ActivityTypeGroups.Swimming:
|
|
51
52
|
return [data_speed_avg_1.DataSpeedAvg.type, data_swim_pace_avg_1.DataSwimPaceAvg.type];
|
|
52
53
|
default:
|
|
53
54
|
return [data_speed_avg_1.DataSpeedAvg.type];
|
|
@@ -60,6 +61,7 @@ class ActivityTypesHelper {
|
|
|
60
61
|
case ActivityTypeGroups.TrailRunning:
|
|
61
62
|
return [data_pace_1.DataPace.type, data_speed_1.DataSpeed.type];
|
|
62
63
|
case ActivityTypeGroups.WaterSports:
|
|
64
|
+
case ActivityTypeGroups.Swimming:
|
|
63
65
|
return [data_speed_1.DataSpeed.type, data_swim_pace_1.DataSwimPace.type];
|
|
64
66
|
default:
|
|
65
67
|
return [data_speed_1.DataSpeed.type];
|
|
@@ -13,6 +13,7 @@ export declare class EventImporterFIT {
|
|
|
13
13
|
* Generate streams samples based on lengths on an activity
|
|
14
14
|
* When based on lengths, an activity do not provides sample under records object
|
|
15
15
|
* @param sessionObject
|
|
16
|
+
* @param options
|
|
16
17
|
* @private
|
|
17
18
|
*/
|
|
18
19
|
private static generateSamplesFromLengths;
|
|
@@ -272,7 +272,7 @@ class EventImporterFIT {
|
|
|
272
272
|
// Note: this is how Strava generate streams for this kind of activities
|
|
273
273
|
const isLengthsBased = this.isLengthsBased(sessionObject);
|
|
274
274
|
const samples = isLengthsBased
|
|
275
|
-
? this.generateSamplesFromLengths(sessionObject)
|
|
275
|
+
? this.generateSamplesFromLengths(sessionObject, options)
|
|
276
276
|
: fitDataObject.records.filter((record) => {
|
|
277
277
|
return record.timestamp >= activity.startDate && record.timestamp <= activity.endDate;
|
|
278
278
|
});
|
|
@@ -351,9 +351,10 @@ class EventImporterFIT {
|
|
|
351
351
|
* Generate streams samples based on lengths on an activity
|
|
352
352
|
* When based on lengths, an activity do not provides sample under records object
|
|
353
353
|
* @param sessionObject
|
|
354
|
+
* @param options
|
|
354
355
|
* @private
|
|
355
356
|
*/
|
|
356
|
-
static generateSamplesFromLengths(sessionObject) {
|
|
357
|
+
static generateSamplesFromLengths(sessionObject, options) {
|
|
357
358
|
if (!this.isLengthsBased(sessionObject)) {
|
|
358
359
|
throw new parsing_event_lib_error_1.ParsingEventLibError('Trying to get samples from activities lengths, but no lengths is available');
|
|
359
360
|
}
|
|
@@ -370,12 +371,18 @@ class EventImporterFIT {
|
|
|
370
371
|
lap.lengths.forEach((length) => {
|
|
371
372
|
// Resolve start/end date of current length
|
|
372
373
|
const lengthStartDate = length.start_time;
|
|
373
|
-
const
|
|
374
|
+
const lengthDuration = (length.total_timer_time || length.total_elapsed_time || 0);
|
|
375
|
+
const lengthEndDate = new Date(lengthStartDate.getTime() + lengthDuration * 1000);
|
|
376
|
+
// We check if length is valid comparing to max activity duration
|
|
377
|
+
if (lengthDuration > options.maxActivityDurationDays * 24 * 60 * 60) {
|
|
378
|
+
return;
|
|
379
|
+
}
|
|
374
380
|
if (lengthEndDate.getTime() <= lengthStartDate.getTime()) {
|
|
375
381
|
return;
|
|
376
382
|
}
|
|
377
383
|
// Generate a stream from length start date to end date filled by null values
|
|
378
|
-
|
|
384
|
+
const streamLength = activity_utilities_1.ActivityUtilities.getDataLength(lengthStartDate, lengthEndDate);
|
|
385
|
+
let lengthStream = Array(streamLength).fill(null);
|
|
379
386
|
// Define distance step to be used for distance stream
|
|
380
387
|
const lengthStepSize = lengthMeters / (lengthStream.length - 1);
|
|
381
388
|
// Generate the length stream based on data we have on current length
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
|
|
3
|
+
if (k2 === undefined) k2 = k;
|
|
4
|
+
Object.defineProperty(o, k2, { enumerable: true, get: function() { return m[k]; } });
|
|
5
|
+
}) : (function(o, m, k, k2) {
|
|
6
|
+
if (k2 === undefined) k2 = k;
|
|
7
|
+
o[k2] = m[k];
|
|
8
|
+
}));
|
|
9
|
+
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
|
|
10
|
+
Object.defineProperty(o, "default", { enumerable: true, value: v });
|
|
11
|
+
}) : function(o, v) {
|
|
12
|
+
o["default"] = v;
|
|
13
|
+
});
|
|
14
|
+
var __importStar = (this && this.__importStar) || function (mod) {
|
|
15
|
+
if (mod && mod.__esModule) return mod;
|
|
16
|
+
var result = {};
|
|
17
|
+
if (mod != null) for (var k in mod) if (k !== "default" && Object.prototype.hasOwnProperty.call(mod, k)) __createBinding(result, mod, k);
|
|
18
|
+
__setModuleDefault(result, mod);
|
|
19
|
+
return result;
|
|
20
|
+
};
|
|
21
|
+
var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) {
|
|
22
|
+
function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); }
|
|
23
|
+
return new (P || (P = Promise))(function (resolve, reject) {
|
|
24
|
+
function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } }
|
|
25
|
+
function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } }
|
|
26
|
+
function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); }
|
|
27
|
+
step((generator = generator.apply(thisArg, _arguments || [])).next());
|
|
28
|
+
});
|
|
29
|
+
};
|
|
30
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
31
|
+
const fs = __importStar(require("fs"));
|
|
32
|
+
const path = __importStar(require("path"));
|
|
33
|
+
const importer_fit_1 = require("./importer.fit");
|
|
34
|
+
describe('EventImporterFIT OOM Reproduction', () => {
|
|
35
|
+
const oomSamplesDir = path.resolve(__dirname, '../../../../../samples/fit/oom');
|
|
36
|
+
it('should parse OOM sample fit files', () => __awaiter(void 0, void 0, void 0, function* () {
|
|
37
|
+
if (!fs.existsSync(oomSamplesDir)) {
|
|
38
|
+
console.warn(`OOM Samples directory not found at ${oomSamplesDir}. Skipping reproduction.`);
|
|
39
|
+
return;
|
|
40
|
+
}
|
|
41
|
+
const files = fs.readdirSync(oomSamplesDir).filter(f => f.endsWith('.fit'));
|
|
42
|
+
for (const file of files) {
|
|
43
|
+
console.log(`>>> START Processing ${file}`);
|
|
44
|
+
const filePath = path.join(oomSamplesDir, file);
|
|
45
|
+
const fileBuffer = fs.readFileSync(filePath);
|
|
46
|
+
const arrayBuffer = fileBuffer.buffer.slice(fileBuffer.byteOffset, fileBuffer.byteOffset + fileBuffer.byteLength);
|
|
47
|
+
console.log(`Testing file: ${file} (${fileBuffer.length} bytes)`);
|
|
48
|
+
try {
|
|
49
|
+
const event = yield importer_fit_1.EventImporterFIT.getFromArrayBuffer(arrayBuffer, undefined, file);
|
|
50
|
+
expect(event).toBeDefined();
|
|
51
|
+
console.log(`✅ Successfully parsed ${file}`);
|
|
52
|
+
}
|
|
53
|
+
catch (error) {
|
|
54
|
+
console.error(`❌ Failed to parse ${file}:`, error);
|
|
55
|
+
// We don't necessarily want to throw here if we want to test all files
|
|
56
|
+
}
|
|
57
|
+
console.log(`<<< END Processing ${file}`);
|
|
58
|
+
}
|
|
59
|
+
}), 100000); // 100s timeout as these might take time if they are huge
|
|
60
|
+
});
|
|
@@ -688,32 +688,33 @@ class ActivityUtilities {
|
|
|
688
688
|
...data_store_1.DynamicDataLoader.speedDerivedDataTypes
|
|
689
689
|
];
|
|
690
690
|
let baseUnitStreams = [];
|
|
691
|
-
|
|
692
|
-
|
|
693
|
-
|
|
694
|
-
|
|
691
|
+
// Iterate over all possible base types that can have unit variants
|
|
692
|
+
// This allows us to dynamically include ALL base streams (like Distance, Power, etc.) that need unit conversion
|
|
693
|
+
Object.keys(data_store_1.DynamicDataLoader.dataTypeUnitGroups).forEach(baseDataType => {
|
|
694
|
+
const stream = streams.find(s => s.type === baseDataType);
|
|
695
|
+
if (!stream) {
|
|
696
|
+
return;
|
|
695
697
|
}
|
|
696
|
-
|
|
697
|
-
|
|
698
|
+
// Special handling for derived types (Pace from Speed, etc.)
|
|
699
|
+
if (baseDataType === data_speed_1.DataSpeed.type && options.includeDerivedTypes) {
|
|
700
|
+
baseUnitStreams = baseUnitStreams.concat(this.createByActivityTypeSpeedBasedStreams(stream, activityType));
|
|
701
|
+
return;
|
|
698
702
|
}
|
|
699
|
-
|
|
700
|
-
|
|
701
|
-
|
|
702
|
-
if (options.includeDerivedTypes) {
|
|
703
|
-
baseUnitStreams = baseUnitStreams.concat(this.createByActivityTypeAltiDistanceSpeedBasedStreams(gradeAdjustedSpeedStream, activityType));
|
|
703
|
+
if (baseDataType === data_grade_adjusted_speed_1.DataGradeAdjustedSpeed.type && options.includeDerivedTypes) {
|
|
704
|
+
baseUnitStreams = baseUnitStreams.concat(this.createByActivityTypeAltiDistanceSpeedBasedStreams(stream, activityType));
|
|
705
|
+
return;
|
|
704
706
|
}
|
|
705
|
-
|
|
706
|
-
|
|
707
|
+
if (baseDataType === data_vertical_speed_1.DataVerticalSpeed.type) {
|
|
708
|
+
// Vertical speed handling
|
|
709
|
+
if (activity_types_1.ActivityTypesHelper.verticalSpeedDerivedDataTypesToUseForActivityType(activityType).length) {
|
|
710
|
+
baseUnitStreams.push(stream);
|
|
711
|
+
}
|
|
712
|
+
return;
|
|
707
713
|
}
|
|
708
|
-
|
|
709
|
-
|
|
710
|
-
|
|
711
|
-
|
|
712
|
-
baseUnitStreams = activity_types_1.ActivityTypesHelper.verticalSpeedDerivedDataTypesToUseForActivityType(activityType).length
|
|
713
|
-
? baseUnitStreams.concat(verticalSpeedStream)
|
|
714
|
-
: baseUnitStreams;
|
|
715
|
-
}
|
|
716
|
-
const startWith = baseUnitStreams.filter((baseUnitStream) => unitStreamTypesToCreate.indexOf(baseUnitStream.type) !== -1 && streams.indexOf(baseUnitStream) === -1);
|
|
714
|
+
// For everything else (like Distance), just add the base stream so it can be used for unit generation
|
|
715
|
+
baseUnitStreams.push(stream);
|
|
716
|
+
});
|
|
717
|
+
const startWith = baseUnitStreams.filter(baseUnitStream => unitStreamTypesToCreate.indexOf(baseUnitStream.type) !== -1 && streams.indexOf(baseUnitStream) === -1);
|
|
717
718
|
if (options.includeUnitVariants === false) {
|
|
718
719
|
return startWith;
|
|
719
720
|
}
|
|
@@ -3,6 +3,7 @@ Object.defineProperty(exports, "__esModule", { value: true });
|
|
|
3
3
|
const event_1 = require("../event");
|
|
4
4
|
const data_speed_max_1 = require("../../data/data.speed-max");
|
|
5
5
|
const activity_1 = require("../../activities/activity");
|
|
6
|
+
const activity_parsing_options_1 = require("../../activities/activity-parsing-options");
|
|
6
7
|
const data_heart_rate_1 = require("../../data/data.heart-rate");
|
|
7
8
|
const data_altitude_1 = require("../../data/data.altitude");
|
|
8
9
|
const data_distance_1 = require("../../data/data.distance");
|
|
@@ -20,6 +21,7 @@ const file_type_enum_1 = require("../adapters/file-type.enum");
|
|
|
20
21
|
const importer_json_1 = require("../adapters/importers/json/importer.json");
|
|
21
22
|
const data_pace_1 = require("../../data/data.pace");
|
|
22
23
|
const data_speed_2 = require("../../data/data.speed");
|
|
24
|
+
const data_swim_pace_1 = require("../../data/data.swim-pace");
|
|
23
25
|
describe('Activity Utilities', () => {
|
|
24
26
|
let event;
|
|
25
27
|
beforeEach(() => {
|
|
@@ -272,8 +274,8 @@ describe('Activity Utilities', () => {
|
|
|
272
274
|
});
|
|
273
275
|
});
|
|
274
276
|
describe('generateMissingStreams', () => {
|
|
275
|
-
it('should generate unit streams
|
|
276
|
-
const activity = new activity_1.Activity(new Date(), new Date(), activity_types_1.ActivityTypes.Running, new creator_1.Creator('test'));
|
|
277
|
+
it('should generate unit streams when generateUnitStreams = true', () => {
|
|
278
|
+
const activity = new activity_1.Activity(new Date(), new Date(), activity_types_1.ActivityTypes.Running, new creator_1.Creator('test'), new activity_parsing_options_1.ActivityParsingOptions({ generateUnitStreams: true }));
|
|
277
279
|
// Add a speed stream
|
|
278
280
|
activity.addStream(new stream_1.Stream(data_speed_1.DataSpeed.type, [10, 20]));
|
|
279
281
|
activity_utilities_1.ActivityUtilities.generateMissingStreams(activity);
|
|
@@ -322,5 +324,52 @@ describe('Activity Utilities', () => {
|
|
|
322
324
|
expect(speedMaxKmh).toBeDefined();
|
|
323
325
|
expect(speedMaxKmh === null || speedMaxKmh === void 0 ? void 0 : speedMaxKmh.getValue()).toBe(72);
|
|
324
326
|
});
|
|
327
|
+
it('should generate DataSwimPace when generateUnitStreams = false for swimming', () => {
|
|
328
|
+
const activity = new activity_1.Activity(new Date(), new Date(), activity_types_1.ActivityTypes.Swimming, new creator_1.Creator('test'));
|
|
329
|
+
activity.parseOptions = {
|
|
330
|
+
streams: { smooth: {}, fixAbnormal: {} },
|
|
331
|
+
maxActivityDurationDays: 14,
|
|
332
|
+
generateUnitStreams: false
|
|
333
|
+
};
|
|
334
|
+
activity.addStream(new stream_1.Stream(data_speed_1.DataSpeed.type, [1, 2])); // m/s
|
|
335
|
+
activity_utilities_1.ActivityUtilities.generateMissingStreamsAndStatsForActivity(activity);
|
|
336
|
+
// Verify Unit Streams missing
|
|
337
|
+
expect(activity.hasStreamData(data_speed_2.DataSpeedKilometersPerHour.type)).toBe(false);
|
|
338
|
+
// Verify Derived Base Stream (Swim Pace) IS present
|
|
339
|
+
expect(activity.hasStreamData(data_swim_pace_1.DataSwimPace.type)).toBe(true);
|
|
340
|
+
});
|
|
341
|
+
it('should generate DataDistanceMiles when generateUnitStreams = true', () => {
|
|
342
|
+
const activity = new activity_1.Activity(new Date(), new Date(), activity_types_1.ActivityTypes.Running, new creator_1.Creator('test'), new activity_parsing_options_1.ActivityParsingOptions({ generateUnitStreams: true }));
|
|
343
|
+
activity.addStream(new stream_1.Stream(data_distance_1.DataDistance.type, [1000, 2000]));
|
|
344
|
+
activity_utilities_1.ActivityUtilities.generateMissingStreamsAndStatsForActivity(activity);
|
|
345
|
+
// Should generate miles
|
|
346
|
+
expect(activity.hasStreamData(data_distance_1.DataDistanceMiles.type)).toBe(true);
|
|
347
|
+
});
|
|
348
|
+
it('should NOT generate DataDistanceMiles when generateUnitStreams = false', () => {
|
|
349
|
+
const activity = new activity_1.Activity(new Date(), new Date(), activity_types_1.ActivityTypes.Running, new creator_1.Creator('test'));
|
|
350
|
+
activity.parseOptions = {
|
|
351
|
+
streams: { smooth: {}, fixAbnormal: {} },
|
|
352
|
+
maxActivityDurationDays: 14,
|
|
353
|
+
generateUnitStreams: false
|
|
354
|
+
};
|
|
355
|
+
activity.addStream(new stream_1.Stream(data_distance_1.DataDistance.type, [1000, 2000]));
|
|
356
|
+
activity_utilities_1.ActivityUtilities.generateMissingStreamsAndStatsForActivity(activity);
|
|
357
|
+
// Should NOT generate miles
|
|
358
|
+
expect(activity.hasStreamData(data_distance_1.DataDistanceMiles.type)).toBe(false);
|
|
359
|
+
// But base Distance should still be there (it was added manually)
|
|
360
|
+
expect(activity.hasStreamData(data_distance_1.DataDistance.type)).toBe(true);
|
|
361
|
+
});
|
|
362
|
+
it('should generate unit streams for Mountain Biking using DataSpeed', () => {
|
|
363
|
+
const speedData = [10, 20, 30]; // m/s
|
|
364
|
+
const speedStream = new stream_1.Stream(data_speed_1.DataSpeed.type, speedData);
|
|
365
|
+
// Mountain Biking (defaults to Cycling group)
|
|
366
|
+
const unitStreams = activity_utilities_1.ActivityUtilities.createUnitStreamsFromStreams([speedStream], activity_types_1.ActivityTypes.MountainBiking, undefined, // Auto-detect all known unit types
|
|
367
|
+
{ includeDerivedTypes: true, includeUnitVariants: true });
|
|
368
|
+
const kmhStream = unitStreams.find(s => s.type === 'Speed in kilometers per hour');
|
|
369
|
+
expect(kmhStream).toBeDefined();
|
|
370
|
+
if (kmhStream) {
|
|
371
|
+
expect(kmhStream.getData()[0]).toBeCloseTo(36, 1); // 10 m/s = 36 km/h
|
|
372
|
+
}
|
|
373
|
+
});
|
|
325
374
|
});
|
|
326
375
|
});
|
|
@@ -38,7 +38,8 @@ export class Activity extends DurationClassAbstract {
|
|
|
38
38
|
this.name = name;
|
|
39
39
|
}
|
|
40
40
|
createStream(type) {
|
|
41
|
-
|
|
41
|
+
const stream = new Stream(type, Array(ActivityUtilities.getDataLength(this.startDate, this.endDate)).fill(null));
|
|
42
|
+
return stream;
|
|
42
43
|
}
|
|
43
44
|
addDataToStream(type, date, value) {
|
|
44
45
|
this.getStreamData(type)[this.getDateIndex(date)] = value;
|
|
@@ -46,7 +47,7 @@ export class Activity extends DurationClassAbstract {
|
|
|
46
47
|
}
|
|
47
48
|
addStream(stream) {
|
|
48
49
|
if (this.streams.find(activityStream => activityStream.type === stream.type)) {
|
|
49
|
-
throw new Error(`Duplicate type of stream when adding ${stream.type} to activity ${this.getID()}`);
|
|
50
|
+
throw new Error(`Duplicate type of stream when adding ${stream.type} to activity ${this.getID()} `);
|
|
50
51
|
}
|
|
51
52
|
this.streams.push(stream);
|
|
52
53
|
return this;
|
|
@@ -98,7 +99,7 @@ export class Activity extends DurationClassAbstract {
|
|
|
98
99
|
getStream(streamType) {
|
|
99
100
|
const find = this.streams.find(stream => stream.type === streamType);
|
|
100
101
|
if (!find) {
|
|
101
|
-
throw Error(`No stream found with type ${streamType}`);
|
|
102
|
+
throw Error(`No stream found with type ${streamType} `);
|
|
102
103
|
}
|
|
103
104
|
return find;
|
|
104
105
|
}
|
|
@@ -45,6 +45,7 @@ export class ActivityTypesHelper {
|
|
|
45
45
|
case ActivityTypeGroups.TrailRunning:
|
|
46
46
|
return [DataPaceAvg.type, DataGradeAdjustedPaceAvg.type, DataSpeedAvg.type, DataGradeAdjustedSpeedAvg.type];
|
|
47
47
|
case ActivityTypeGroups.WaterSports:
|
|
48
|
+
case ActivityTypeGroups.Swimming:
|
|
48
49
|
return [DataSpeedAvg.type, DataSwimPaceAvg.type];
|
|
49
50
|
default:
|
|
50
51
|
return [DataSpeedAvg.type];
|
|
@@ -57,6 +58,7 @@ export class ActivityTypesHelper {
|
|
|
57
58
|
case ActivityTypeGroups.TrailRunning:
|
|
58
59
|
return [DataPace.type, DataSpeed.type];
|
|
59
60
|
case ActivityTypeGroups.WaterSports:
|
|
61
|
+
case ActivityTypeGroups.Swimming:
|
|
60
62
|
return [DataSpeed.type, DataSwimPace.type];
|
|
61
63
|
default:
|
|
62
64
|
return [DataSpeed.type];
|
|
@@ -13,6 +13,7 @@ export declare class EventImporterFIT {
|
|
|
13
13
|
* Generate streams samples based on lengths on an activity
|
|
14
14
|
* When based on lengths, an activity do not provides sample under records object
|
|
15
15
|
* @param sessionObject
|
|
16
|
+
* @param options
|
|
16
17
|
* @private
|
|
17
18
|
*/
|
|
18
19
|
private static generateSamplesFromLengths;
|
|
@@ -266,7 +266,7 @@ export class EventImporterFIT {
|
|
|
266
266
|
// Note: this is how Strava generate streams for this kind of activities
|
|
267
267
|
const isLengthsBased = this.isLengthsBased(sessionObject);
|
|
268
268
|
const samples = isLengthsBased
|
|
269
|
-
? this.generateSamplesFromLengths(sessionObject)
|
|
269
|
+
? this.generateSamplesFromLengths(sessionObject, options)
|
|
270
270
|
: fitDataObject.records.filter((record) => {
|
|
271
271
|
return record.timestamp >= activity.startDate && record.timestamp <= activity.endDate;
|
|
272
272
|
});
|
|
@@ -345,9 +345,10 @@ export class EventImporterFIT {
|
|
|
345
345
|
* Generate streams samples based on lengths on an activity
|
|
346
346
|
* When based on lengths, an activity do not provides sample under records object
|
|
347
347
|
* @param sessionObject
|
|
348
|
+
* @param options
|
|
348
349
|
* @private
|
|
349
350
|
*/
|
|
350
|
-
static generateSamplesFromLengths(sessionObject) {
|
|
351
|
+
static generateSamplesFromLengths(sessionObject, options) {
|
|
351
352
|
if (!this.isLengthsBased(sessionObject)) {
|
|
352
353
|
throw new ParsingEventLibError('Trying to get samples from activities lengths, but no lengths is available');
|
|
353
354
|
}
|
|
@@ -364,12 +365,18 @@ export class EventImporterFIT {
|
|
|
364
365
|
lap.lengths.forEach((length) => {
|
|
365
366
|
// Resolve start/end date of current length
|
|
366
367
|
const lengthStartDate = length.start_time;
|
|
367
|
-
const
|
|
368
|
+
const lengthDuration = (length.total_timer_time || length.total_elapsed_time || 0);
|
|
369
|
+
const lengthEndDate = new Date(lengthStartDate.getTime() + lengthDuration * 1000);
|
|
370
|
+
// We check if length is valid comparing to max activity duration
|
|
371
|
+
if (lengthDuration > options.maxActivityDurationDays * 24 * 60 * 60) {
|
|
372
|
+
return;
|
|
373
|
+
}
|
|
368
374
|
if (lengthEndDate.getTime() <= lengthStartDate.getTime()) {
|
|
369
375
|
return;
|
|
370
376
|
}
|
|
371
377
|
// Generate a stream from length start date to end date filled by null values
|
|
372
|
-
|
|
378
|
+
const streamLength = ActivityUtilities.getDataLength(lengthStartDate, lengthEndDate);
|
|
379
|
+
let lengthStream = Array(streamLength).fill(null);
|
|
373
380
|
// Define distance step to be used for distance stream
|
|
374
381
|
const lengthStepSize = lengthMeters / (lengthStream.length - 1);
|
|
375
382
|
// Generate the length stream based on data we have on current length
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) {
|
|
2
|
+
function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); }
|
|
3
|
+
return new (P || (P = Promise))(function (resolve, reject) {
|
|
4
|
+
function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } }
|
|
5
|
+
function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } }
|
|
6
|
+
function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); }
|
|
7
|
+
step((generator = generator.apply(thisArg, _arguments || [])).next());
|
|
8
|
+
});
|
|
9
|
+
};
|
|
10
|
+
import * as fs from 'fs';
|
|
11
|
+
import * as path from 'path';
|
|
12
|
+
import { EventImporterFIT } from './importer.fit';
|
|
13
|
+
describe('EventImporterFIT OOM Reproduction', () => {
|
|
14
|
+
const oomSamplesDir = path.resolve(__dirname, '../../../../../samples/fit/oom');
|
|
15
|
+
it('should parse OOM sample fit files', () => __awaiter(void 0, void 0, void 0, function* () {
|
|
16
|
+
if (!fs.existsSync(oomSamplesDir)) {
|
|
17
|
+
console.warn(`OOM Samples directory not found at ${oomSamplesDir}. Skipping reproduction.`);
|
|
18
|
+
return;
|
|
19
|
+
}
|
|
20
|
+
const files = fs.readdirSync(oomSamplesDir).filter(f => f.endsWith('.fit'));
|
|
21
|
+
for (const file of files) {
|
|
22
|
+
console.log(`>>> START Processing ${file}`);
|
|
23
|
+
const filePath = path.join(oomSamplesDir, file);
|
|
24
|
+
const fileBuffer = fs.readFileSync(filePath);
|
|
25
|
+
const arrayBuffer = fileBuffer.buffer.slice(fileBuffer.byteOffset, fileBuffer.byteOffset + fileBuffer.byteLength);
|
|
26
|
+
console.log(`Testing file: ${file} (${fileBuffer.length} bytes)`);
|
|
27
|
+
try {
|
|
28
|
+
const event = yield EventImporterFIT.getFromArrayBuffer(arrayBuffer, undefined, file);
|
|
29
|
+
expect(event).toBeDefined();
|
|
30
|
+
console.log(`✅ Successfully parsed ${file}`);
|
|
31
|
+
}
|
|
32
|
+
catch (error) {
|
|
33
|
+
console.error(`❌ Failed to parse ${file}:`, error);
|
|
34
|
+
// We don't necessarily want to throw here if we want to test all files
|
|
35
|
+
}
|
|
36
|
+
console.log(`<<< END Processing ${file}`);
|
|
37
|
+
}
|
|
38
|
+
}), 100000); // 100s timeout as these might take time if they are huge
|
|
39
|
+
});
|
|
@@ -685,32 +685,33 @@ export class ActivityUtilities {
|
|
|
685
685
|
...DynamicDataLoader.speedDerivedDataTypes
|
|
686
686
|
];
|
|
687
687
|
let baseUnitStreams = [];
|
|
688
|
-
|
|
689
|
-
|
|
690
|
-
|
|
691
|
-
|
|
688
|
+
// Iterate over all possible base types that can have unit variants
|
|
689
|
+
// This allows us to dynamically include ALL base streams (like Distance, Power, etc.) that need unit conversion
|
|
690
|
+
Object.keys(DynamicDataLoader.dataTypeUnitGroups).forEach(baseDataType => {
|
|
691
|
+
const stream = streams.find(s => s.type === baseDataType);
|
|
692
|
+
if (!stream) {
|
|
693
|
+
return;
|
|
692
694
|
}
|
|
693
|
-
|
|
694
|
-
|
|
695
|
+
// Special handling for derived types (Pace from Speed, etc.)
|
|
696
|
+
if (baseDataType === DataSpeed.type && options.includeDerivedTypes) {
|
|
697
|
+
baseUnitStreams = baseUnitStreams.concat(this.createByActivityTypeSpeedBasedStreams(stream, activityType));
|
|
698
|
+
return;
|
|
695
699
|
}
|
|
696
|
-
|
|
697
|
-
|
|
698
|
-
|
|
699
|
-
if (options.includeDerivedTypes) {
|
|
700
|
-
baseUnitStreams = baseUnitStreams.concat(this.createByActivityTypeAltiDistanceSpeedBasedStreams(gradeAdjustedSpeedStream, activityType));
|
|
700
|
+
if (baseDataType === DataGradeAdjustedSpeed.type && options.includeDerivedTypes) {
|
|
701
|
+
baseUnitStreams = baseUnitStreams.concat(this.createByActivityTypeAltiDistanceSpeedBasedStreams(stream, activityType));
|
|
702
|
+
return;
|
|
701
703
|
}
|
|
702
|
-
|
|
703
|
-
|
|
704
|
+
if (baseDataType === DataVerticalSpeed.type) {
|
|
705
|
+
// Vertical speed handling
|
|
706
|
+
if (ActivityTypesHelper.verticalSpeedDerivedDataTypesToUseForActivityType(activityType).length) {
|
|
707
|
+
baseUnitStreams.push(stream);
|
|
708
|
+
}
|
|
709
|
+
return;
|
|
704
710
|
}
|
|
705
|
-
|
|
706
|
-
|
|
707
|
-
|
|
708
|
-
|
|
709
|
-
baseUnitStreams = ActivityTypesHelper.verticalSpeedDerivedDataTypesToUseForActivityType(activityType).length
|
|
710
|
-
? baseUnitStreams.concat(verticalSpeedStream)
|
|
711
|
-
: baseUnitStreams;
|
|
712
|
-
}
|
|
713
|
-
const startWith = baseUnitStreams.filter((baseUnitStream) => unitStreamTypesToCreate.indexOf(baseUnitStream.type) !== -1 && streams.indexOf(baseUnitStream) === -1);
|
|
711
|
+
// For everything else (like Distance), just add the base stream so it can be used for unit generation
|
|
712
|
+
baseUnitStreams.push(stream);
|
|
713
|
+
});
|
|
714
|
+
const startWith = baseUnitStreams.filter(baseUnitStream => unitStreamTypesToCreate.indexOf(baseUnitStream.type) !== -1 && streams.indexOf(baseUnitStream) === -1);
|
|
714
715
|
if (options.includeUnitVariants === false) {
|
|
715
716
|
return startWith;
|
|
716
717
|
}
|
|
@@ -1,9 +1,10 @@
|
|
|
1
1
|
import { Event } from '../event';
|
|
2
2
|
import { DataSpeedMaxKilometersPerHour } from '../../data/data.speed-max';
|
|
3
3
|
import { Activity } from '../../activities/activity';
|
|
4
|
+
import { ActivityParsingOptions } from '../../activities/activity-parsing-options';
|
|
4
5
|
import { DataHeartRate } from '../../data/data.heart-rate';
|
|
5
6
|
import { DataAltitude } from '../../data/data.altitude';
|
|
6
|
-
import { DataDistance } from '../../data/data.distance';
|
|
7
|
+
import { DataDistance, DataDistanceMiles } from '../../data/data.distance';
|
|
7
8
|
import { DataDuration } from '../../data/data.duration';
|
|
8
9
|
import { Creator } from '../../creators/creator';
|
|
9
10
|
import { ActivityTypes } from '../../activities/activity.types';
|
|
@@ -18,6 +19,7 @@ import { FileType } from '../adapters/file-type.enum';
|
|
|
18
19
|
import { EventImporterJSON } from '../adapters/importers/json/importer.json';
|
|
19
20
|
import { DataPace, DataPaceMinutesPerMile } from '../../data/data.pace';
|
|
20
21
|
import { DataSpeedKilometersPerHour } from '../../data/data.speed';
|
|
22
|
+
import { DataSwimPace } from '../../data/data.swim-pace';
|
|
21
23
|
describe('Activity Utilities', () => {
|
|
22
24
|
let event;
|
|
23
25
|
beforeEach(() => {
|
|
@@ -270,8 +272,8 @@ describe('Activity Utilities', () => {
|
|
|
270
272
|
});
|
|
271
273
|
});
|
|
272
274
|
describe('generateMissingStreams', () => {
|
|
273
|
-
it('should generate unit streams
|
|
274
|
-
const activity = new Activity(new Date(), new Date(), ActivityTypes.Running, new Creator('test'));
|
|
275
|
+
it('should generate unit streams when generateUnitStreams = true', () => {
|
|
276
|
+
const activity = new Activity(new Date(), new Date(), ActivityTypes.Running, new Creator('test'), new ActivityParsingOptions({ generateUnitStreams: true }));
|
|
275
277
|
// Add a speed stream
|
|
276
278
|
activity.addStream(new Stream(DataSpeed.type, [10, 20]));
|
|
277
279
|
ActivityUtilities.generateMissingStreams(activity);
|
|
@@ -320,5 +322,52 @@ describe('Activity Utilities', () => {
|
|
|
320
322
|
expect(speedMaxKmh).toBeDefined();
|
|
321
323
|
expect(speedMaxKmh === null || speedMaxKmh === void 0 ? void 0 : speedMaxKmh.getValue()).toBe(72);
|
|
322
324
|
});
|
|
325
|
+
it('should generate DataSwimPace when generateUnitStreams = false for swimming', () => {
|
|
326
|
+
const activity = new Activity(new Date(), new Date(), ActivityTypes.Swimming, new Creator('test'));
|
|
327
|
+
activity.parseOptions = {
|
|
328
|
+
streams: { smooth: {}, fixAbnormal: {} },
|
|
329
|
+
maxActivityDurationDays: 14,
|
|
330
|
+
generateUnitStreams: false
|
|
331
|
+
};
|
|
332
|
+
activity.addStream(new Stream(DataSpeed.type, [1, 2])); // m/s
|
|
333
|
+
ActivityUtilities.generateMissingStreamsAndStatsForActivity(activity);
|
|
334
|
+
// Verify Unit Streams missing
|
|
335
|
+
expect(activity.hasStreamData(DataSpeedKilometersPerHour.type)).toBe(false);
|
|
336
|
+
// Verify Derived Base Stream (Swim Pace) IS present
|
|
337
|
+
expect(activity.hasStreamData(DataSwimPace.type)).toBe(true);
|
|
338
|
+
});
|
|
339
|
+
it('should generate DataDistanceMiles when generateUnitStreams = true', () => {
|
|
340
|
+
const activity = new Activity(new Date(), new Date(), ActivityTypes.Running, new Creator('test'), new ActivityParsingOptions({ generateUnitStreams: true }));
|
|
341
|
+
activity.addStream(new Stream(DataDistance.type, [1000, 2000]));
|
|
342
|
+
ActivityUtilities.generateMissingStreamsAndStatsForActivity(activity);
|
|
343
|
+
// Should generate miles
|
|
344
|
+
expect(activity.hasStreamData(DataDistanceMiles.type)).toBe(true);
|
|
345
|
+
});
|
|
346
|
+
it('should NOT generate DataDistanceMiles when generateUnitStreams = false', () => {
|
|
347
|
+
const activity = new Activity(new Date(), new Date(), ActivityTypes.Running, new Creator('test'));
|
|
348
|
+
activity.parseOptions = {
|
|
349
|
+
streams: { smooth: {}, fixAbnormal: {} },
|
|
350
|
+
maxActivityDurationDays: 14,
|
|
351
|
+
generateUnitStreams: false
|
|
352
|
+
};
|
|
353
|
+
activity.addStream(new Stream(DataDistance.type, [1000, 2000]));
|
|
354
|
+
ActivityUtilities.generateMissingStreamsAndStatsForActivity(activity);
|
|
355
|
+
// Should NOT generate miles
|
|
356
|
+
expect(activity.hasStreamData(DataDistanceMiles.type)).toBe(false);
|
|
357
|
+
// But base Distance should still be there (it was added manually)
|
|
358
|
+
expect(activity.hasStreamData(DataDistance.type)).toBe(true);
|
|
359
|
+
});
|
|
360
|
+
it('should generate unit streams for Mountain Biking using DataSpeed', () => {
|
|
361
|
+
const speedData = [10, 20, 30]; // m/s
|
|
362
|
+
const speedStream = new Stream(DataSpeed.type, speedData);
|
|
363
|
+
// Mountain Biking (defaults to Cycling group)
|
|
364
|
+
const unitStreams = ActivityUtilities.createUnitStreamsFromStreams([speedStream], ActivityTypes.MountainBiking, undefined, // Auto-detect all known unit types
|
|
365
|
+
{ includeDerivedTypes: true, includeUnitVariants: true });
|
|
366
|
+
const kmhStream = unitStreams.find(s => s.type === 'Speed in kilometers per hour');
|
|
367
|
+
expect(kmhStream).toBeDefined();
|
|
368
|
+
if (kmhStream) {
|
|
369
|
+
expect(kmhStream.getData()[0]).toBeCloseTo(36, 1); // 10 m/s = 36 km/h
|
|
370
|
+
}
|
|
371
|
+
});
|
|
323
372
|
});
|
|
324
373
|
});
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@sports-alliance/sports-lib",
|
|
3
|
-
"version": "6.1.
|
|
3
|
+
"version": "6.1.13",
|
|
4
4
|
"description": "A Library to for importing / exporting and processing GPX, TCX, FIT and JSON files from services such as Strava, Movescount, Garmin, Polar etc",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"gpx",
|
|
@@ -55,7 +55,7 @@
|
|
|
55
55
|
"dependencies": {
|
|
56
56
|
"fast-xml-parser": "^5.3.3",
|
|
57
57
|
"fit-file-parser": "^2.1.0",
|
|
58
|
-
"geolib": "^3.3.
|
|
58
|
+
"geolib": "^3.3.4",
|
|
59
59
|
"gpx-builder": "^3.7.8",
|
|
60
60
|
"kalmanjs": "^1.1.0",
|
|
61
61
|
"lowpassf": "^0.5.0",
|
package/reproduce_oom.ts
ADDED
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
import * as fs from 'fs';
|
|
2
|
+
import * as path from 'path';
|
|
3
|
+
import { EventImporterFIT } from './src/events/adapters/importers/fit/importer.fit';
|
|
4
|
+
|
|
5
|
+
async function reproduce() {
|
|
6
|
+
const oomSamplesDir = path.resolve(__dirname, 'samples/fit/oom');
|
|
7
|
+
|
|
8
|
+
if (!fs.existsSync(oomSamplesDir)) {
|
|
9
|
+
console.warn(`OOM Samples directory not found at ${oomSamplesDir}.`);
|
|
10
|
+
return;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
const files = fs.readdirSync(oomSamplesDir).filter(f => f.endsWith('.fit'));
|
|
14
|
+
|
|
15
|
+
for (const file of files) {
|
|
16
|
+
process.stdout.write(`>>> START Processing ${file}\n`);
|
|
17
|
+
const filePath = path.join(oomSamplesDir, file);
|
|
18
|
+
const fileBuffer = fs.readFileSync(filePath);
|
|
19
|
+
const arrayBuffer = fileBuffer.buffer.slice(fileBuffer.byteOffset, fileBuffer.byteOffset + fileBuffer.byteLength);
|
|
20
|
+
|
|
21
|
+
console.log(`Testing file: ${file} (${fileBuffer.length} bytes)`);
|
|
22
|
+
try {
|
|
23
|
+
const event = await EventImporterFIT.getFromArrayBuffer(arrayBuffer, undefined, file);
|
|
24
|
+
console.log(`✅ Successfully parsed ${file}`);
|
|
25
|
+
} catch (error) {
|
|
26
|
+
console.error(`❌ Failed to parse ${file}:`, error);
|
|
27
|
+
}
|
|
28
|
+
process.stdout.write(`<<< END Processing ${file}\n`);
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
reproduce().catch(err => {
|
|
33
|
+
console.error('FATAL ERROR:', err);
|
|
34
|
+
process.exit(1);
|
|
35
|
+
});
|