@sports-alliance/sports-lib 7.0.0 → 7.0.2
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/.github/workflows/test.yml +2 -2
- package/lib/cjs/activities/activity.types.d.ts +6 -3
- package/lib/cjs/activities/activity.types.js +9 -4
- package/lib/cjs/data/data.cycling-position.js +1 -1
- package/lib/cjs/data/data.feeling.js +1 -1
- package/lib/cjs/data/data.interface.js +1 -1
- package/lib/cjs/data/data.rpe.js +1 -1
- package/lib/cjs/data/data.store.export.spec.js +28 -14
- package/lib/cjs/data/data.store.js +1 -1
- package/lib/cjs/events/adapters/file-type.enum.js +1 -1
- package/lib/cjs/events/adapters/importers/fit/importer.fit.coros.device.names.js +2 -1
- package/lib/cjs/events/adapters/importers/fit/importer.fit.d.ts +2 -1
- package/lib/cjs/events/adapters/importers/fit/importer.fit.garmin.device.names.js +61 -2
- package/lib/cjs/events/adapters/importers/fit/importer.fit.garmin.profile.data.js +867 -855
- package/lib/cjs/events/adapters/importers/fit/importer.fit.garmin.profile.mapper.spec.d.ts +1 -0
- package/lib/cjs/events/adapters/importers/fit/importer.fit.garmin.profile.mapper.spec.js +34 -0
- package/lib/cjs/events/adapters/importers/fit/importer.fit.integration.spec.js +22 -8
- package/lib/cjs/events/adapters/importers/fit/importer.fit.js +24 -2
- package/lib/cjs/events/adapters/importers/fit/importer.fit.suunto.device.names.js +1 -0
- package/lib/cjs/events/adapters/importers/fit/importer.fit.wahoo.device.names.js +5 -1
- package/lib/cjs/events/adapters/importers/gpx/importer.gpx.integration.spec.js +22 -8
- package/lib/cjs/events/adapters/importers/gpx/importer.gpx.js +1 -1
- package/lib/cjs/events/adapters/importers/gpx/importer.gpx.spec.js +50 -0
- package/lib/cjs/events/adapters/importers/suunto/importer.suunto.activity.ids.js +1 -1
- package/lib/cjs/events/adapters/importers/suunto/importer.suunto.device.names.js +1 -0
- package/lib/cjs/events/adapters/importers/suunto/importer.suunto.integration.spec.js +22 -8
- package/lib/cjs/events/adapters/importers/tcx/importer.tcx.integration.spec.js +22 -8
- package/lib/cjs/events/utilities/activity.utilities.js +7 -7
- package/lib/cjs/events/utilities/grade-calculator/low-pass-filter.js +3 -3
- package/lib/cjs/events/utilities/helpers.d.ts +1 -1
- package/lib/cjs/events/utilities/helpers.js +19 -19
- package/lib/cjs/index.d.ts +1 -1
- package/lib/cjs/index.js +5 -1
- package/lib/cjs/laps/lap.types.js +1 -1
- package/lib/cjs/meta-data/event-meta-data.interface.js +1 -1
- package/lib/cjs/privacy/privacy.class.interface.js +1 -1
- package/lib/cjs/specs/activities-parsing.integration.spec.js +75 -61
- package/lib/cjs/specs/strava-streams-compliance.spec.js +22 -8
- package/lib/cjs/streams/compressed.stream.interface.js +2 -2
- package/lib/cjs/tiles/tile.settings.interface.js +5 -5
- package/lib/cjs/users/settings/dashboard/user.dashboard.settings.interface.js +1 -1
- package/lib/cjs/users/settings/user.app.settings.interface.js +1 -1
- package/lib/cjs/users/settings/user.chart.settings.interface.js +3 -3
- package/lib/cjs/users/settings/user.map.settings.interface.js +2 -2
- package/lib/cjs/users/settings/user.unit.settings.interface.js +9 -9
- package/lib/esm/activities/activity.types.d.ts +6 -3
- package/lib/esm/activities/activity.types.js +7 -2
- package/lib/esm/data/data.store.export.spec.js +6 -6
- package/lib/esm/data/data.store.js +1 -1
- package/lib/esm/events/adapters/importers/fit/importer.fit.coros.device.names.js +2 -1
- package/lib/esm/events/adapters/importers/fit/importer.fit.d.ts +2 -1
- package/lib/esm/events/adapters/importers/fit/importer.fit.garmin.device.names.js +61 -2
- package/lib/esm/events/adapters/importers/fit/importer.fit.garmin.profile.data.js +867 -855
- package/lib/esm/events/adapters/importers/fit/importer.fit.garmin.profile.mapper.spec.d.ts +1 -0
- package/lib/esm/events/adapters/importers/fit/importer.fit.garmin.profile.mapper.spec.js +32 -0
- package/lib/esm/events/adapters/importers/fit/importer.fit.js +24 -2
- package/lib/esm/events/adapters/importers/fit/importer.fit.suunto.device.names.js +1 -0
- package/lib/esm/events/adapters/importers/fit/importer.fit.wahoo.device.names.js +5 -1
- package/lib/esm/events/adapters/importers/gpx/importer.gpx.js +1 -1
- package/lib/esm/events/adapters/importers/gpx/importer.gpx.spec.js +17 -0
- package/lib/esm/events/adapters/importers/suunto/importer.suunto.device.names.js +1 -0
- package/lib/esm/events/utilities/activity.utilities.js +7 -7
- package/lib/esm/events/utilities/grade-calculator/low-pass-filter.js +3 -3
- package/lib/esm/events/utilities/helpers.d.ts +1 -1
- package/lib/esm/index.d.ts +1 -1
- package/lib/esm/specs/activities-parsing.integration.spec.js +53 -53
- package/package.json +21 -21
- package/.editorconfig +0 -14
- package/.eslintignore +0 -4
- package/.eslintrc.js +0 -9
- package/.prettierignore +0 -24
- package/.prettierrc.json +0 -12
- package/.vscode/launch.json +0 -49
- package/.vscode/tasks.json +0 -34
- package/CODE_OF_CONDUCT.md +0 -76
- package/debug-fit-import.js +0 -10
- package/jest.config.js +0 -23
- package/lib/cjs/package.json +0 -1
- package/lib/cjs/specs/fixtures/streams/strava/rides/3171472783.json +0 -52534
- package/lib/cjs/specs/fixtures/streams/strava/rides/3171487458.json +0 -78818
- package/lib/cjs/specs/fixtures/streams/strava/rides/343080886.json +0 -105090
- package/lib/cjs/specs/fixtures/streams/strava/rides/5910143591.json +0 -110711
- package/lib/cjs/specs/fixtures/streams/strava/runs/2451375851.json +0 -74846
- package/lib/cjs/specs/fixtures/streams/strava/runs/2709634581.json +0 -66817
- package/lib/cjs/specs/fixtures/streams/strava/runs/3156040843.json +0 -17594
- package/lib/cjs/specs/fixtures/streams/strava/runs/3182900697.json +0 -17322
- package/lib/cjs/specs/fixtures/streams/strava/runs/3183465494.json +0 -20463
- package/lib/cjs/specs/fixtures/streams/strava/runs/3183490558.json +0 -58202
- package/lib/esm/specs/fixtures/streams/strava/rides/3171472783.json +0 -52534
- package/lib/esm/specs/fixtures/streams/strava/rides/3171487458.json +0 -78818
- package/lib/esm/specs/fixtures/streams/strava/rides/343080886.json +0 -105090
- package/lib/esm/specs/fixtures/streams/strava/rides/5910143591.json +0 -110711
- package/lib/esm/specs/fixtures/streams/strava/runs/2451375851.json +0 -74846
- package/lib/esm/specs/fixtures/streams/strava/runs/2709634581.json +0 -66817
- package/lib/esm/specs/fixtures/streams/strava/runs/3156040843.json +0 -17594
- package/lib/esm/specs/fixtures/streams/strava/runs/3182900697.json +0 -17322
- package/lib/esm/specs/fixtures/streams/strava/runs/3183465494.json +0 -20463
- package/lib/esm/specs/fixtures/streams/strava/runs/3183490558.json +0 -58202
- package/test_output.log +0 -162
- package/test_output_2.log +0 -168
- package/test_output_3.log +0 -168
- package/tsconfig.cjs.json +0 -7
- package/tsconfig.esm.json +0 -8
- package/tsconfig.lib.json +0 -8
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
import { GarminProfileMapper } from './importer.fit.garmin.profile.mapper';
|
|
2
|
+
describe('GarminProfileMapper', () => {
|
|
3
|
+
describe('getSportName', () => {
|
|
4
|
+
it('should translate sport ID', () => {
|
|
5
|
+
// Assuming ID 1 is Running
|
|
6
|
+
expect(GarminProfileMapper.getSportName(1)).toBe('running');
|
|
7
|
+
});
|
|
8
|
+
it('should return null for unknown sport', () => {
|
|
9
|
+
expect(GarminProfileMapper.getSportName(99999)).toBeNull();
|
|
10
|
+
});
|
|
11
|
+
it('should return names in snake_case (regression check for extraction logic)', () => {
|
|
12
|
+
// Checking ID 4 which is 'fitness_equipment' (was 'fitnessEquipment' in raw SDK)
|
|
13
|
+
expect(GarminProfileMapper.getSportName(4)).toBe('fitness_equipment');
|
|
14
|
+
});
|
|
15
|
+
});
|
|
16
|
+
describe('getSubSportName', () => {
|
|
17
|
+
it('should translate sub sport ID', () => {
|
|
18
|
+
// Assuming ID 1 is Treadmill
|
|
19
|
+
expect(GarminProfileMapper.getSubSportName(1)).toBe('treadmill');
|
|
20
|
+
});
|
|
21
|
+
it('should return names in snake_case (regression check for extraction logic)', () => {
|
|
22
|
+
// Checking ID 6 which is 'indoor_cycling' (was 'indoorCycling' in raw SDK)
|
|
23
|
+
expect(GarminProfileMapper.getSubSportName(6)).toBe('indoor_cycling');
|
|
24
|
+
});
|
|
25
|
+
it('should correctly map Enduro and Downhill MTB (IDs 153/154)', () => {
|
|
26
|
+
// ID 153 = mountain_enduro
|
|
27
|
+
expect(GarminProfileMapper.getSubSportName(153)).toBe('mountain_enduro');
|
|
28
|
+
// ID 154 = mountain_downhill
|
|
29
|
+
expect(GarminProfileMapper.getSubSportName(154)).toBe('mountain_downhill');
|
|
30
|
+
});
|
|
31
|
+
});
|
|
32
|
+
});
|
|
@@ -116,8 +116,8 @@ import FitFileParser from 'fit-file-parser';
|
|
|
116
116
|
// Threshold to detect that session.timestamp are not trustable (when exceeding 15% of session.total_elapsed_time)
|
|
117
117
|
const INVALID_DATES_ELAPSED_TIME_RATIO_THRESHOLD = 1.15;
|
|
118
118
|
export class EventImporterFIT {
|
|
119
|
-
static getFromArrayBuffer(
|
|
120
|
-
return __awaiter(this,
|
|
119
|
+
static getFromArrayBuffer(arrayBuffer_1) {
|
|
120
|
+
return __awaiter(this, arguments, void 0, function* (arrayBuffer, options = ActivityParsingOptions.DEFAULT, name = 'New Event') {
|
|
121
121
|
return new Promise((resolve, reject) => {
|
|
122
122
|
const fitFileParser = new FitFileParser({
|
|
123
123
|
force: true,
|
|
@@ -128,6 +128,7 @@ export class EventImporterFIT {
|
|
|
128
128
|
mode: 'both'
|
|
129
129
|
});
|
|
130
130
|
fitFileParser.parse(arrayBuffer, (error, fitDataObject) => {
|
|
131
|
+
var _a;
|
|
131
132
|
if (error) {
|
|
132
133
|
// For now, assume any error from parser on this file means it's broken/empty in a way we treat as EmptyEventLibError
|
|
133
134
|
// to satisfy existing tests. Or ideally we wrap in a generic EventLibError.
|
|
@@ -139,6 +140,27 @@ export class EventImporterFIT {
|
|
|
139
140
|
reject(new EmptyEventLibError());
|
|
140
141
|
return;
|
|
141
142
|
}
|
|
143
|
+
// Check if we have length data at the top level (new parser behavior or missing mapping)
|
|
144
|
+
if (fitDataObject.lengths && fitDataObject.lengths.length > 0) {
|
|
145
|
+
(_a = fitDataObject.sessions) === null || _a === void 0 ? void 0 : _a.forEach((session) => {
|
|
146
|
+
var _a;
|
|
147
|
+
const sessionStartTime = new Date(session.start_time).getTime();
|
|
148
|
+
const sessionEndTime = sessionStartTime + (session.total_elapsed_time || 0) * 1000;
|
|
149
|
+
session.lengths = fitDataObject.lengths.filter((length) => {
|
|
150
|
+
const lengthTime = new Date(length.timestamp || length.start_time).getTime();
|
|
151
|
+
return lengthTime >= sessionStartTime && lengthTime < sessionEndTime;
|
|
152
|
+
});
|
|
153
|
+
// Also distribute to laps
|
|
154
|
+
(_a = session.laps) === null || _a === void 0 ? void 0 : _a.forEach((lap) => {
|
|
155
|
+
const lapStartTime = new Date(lap.start_time).getTime();
|
|
156
|
+
const lapEndTime = lapStartTime + (lap.total_elapsed_time || 0) * 1000;
|
|
157
|
+
lap.lengths = fitDataObject.lengths.filter((length) => {
|
|
158
|
+
const lengthTime = new Date(length.timestamp || length.start_time).getTime();
|
|
159
|
+
return lengthTime >= lapStartTime && lengthTime < lapEndTime;
|
|
160
|
+
});
|
|
161
|
+
});
|
|
162
|
+
});
|
|
163
|
+
}
|
|
142
164
|
// Iterate over the sessions and create their activities
|
|
143
165
|
const activities = fitDataObject.sessions.map((sessionObject) => {
|
|
144
166
|
// Get the activity from the sessionObject
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { ImporterSuuntoDeviceNames } from '../suunto/importer.suunto.device.names';
|
|
2
2
|
export const ImporterFitSuuntoDeviceNames = {
|
|
3
|
+
19: ImporterSuuntoDeviceNames.Ambit2,
|
|
3
4
|
28: ImporterSuuntoDeviceNames.Brighton,
|
|
4
5
|
29: ImporterSuuntoDeviceNames.Amsterdam,
|
|
5
6
|
34: ImporterSuuntoDeviceNames.Ibiza,
|
|
@@ -54,7 +54,7 @@ export class EventImporterGPX {
|
|
|
54
54
|
// @todo for routes add a separate parser
|
|
55
55
|
const endDate = isActivity
|
|
56
56
|
? new Date(samples[samples.length - 1].time[0])
|
|
57
|
-
: new Date(startDate.getTime() + samples.length * 1000);
|
|
57
|
+
: new Date(startDate.getTime() + (samples.length > 0 ? samples.length - 1 : 0) * 1000);
|
|
58
58
|
let activityType = isActivity ? ActivityTypes.unknown : ActivityTypes.route;
|
|
59
59
|
if (trackOrRoute.type && ActivityTypes[trackOrRoute.type]) {
|
|
60
60
|
activityType = ActivityTypes[trackOrRoute.type];
|
|
@@ -51,4 +51,21 @@ describe('importer.gpx', () => {
|
|
|
51
51
|
const result = yield EventImporterGPX.getFromString(gpxString, xmldom.DOMParser);
|
|
52
52
|
expect(result.getFirstActivity().name).toEqual('Meylan Road Cycling');
|
|
53
53
|
}));
|
|
54
|
+
it('parses route.gpx from samples', () => __awaiter(void 0, void 0, void 0, function* () {
|
|
55
|
+
const fs = yield import('fs');
|
|
56
|
+
const path = yield import('path');
|
|
57
|
+
const samplesDir = path.resolve(__dirname, '../../../../../samples/gpx');
|
|
58
|
+
const filePath = path.join(samplesDir, 'route.gpx');
|
|
59
|
+
const fileString = fs.readFileSync(filePath, 'utf-8');
|
|
60
|
+
const result = yield EventImporterGPX.getFromString(fileString, xmldom.DOMParser);
|
|
61
|
+
expect(result.getActivities().length).toBeGreaterThan(0);
|
|
62
|
+
const activity = result.getFirstActivity();
|
|
63
|
+
expect(activity.type).toEqual('Route');
|
|
64
|
+
const distance = activity.getStat('Distance');
|
|
65
|
+
expect(distance).toBeDefined();
|
|
66
|
+
expect(distance.getValue()).toBeGreaterThan(0);
|
|
67
|
+
// Check if the number of samples matches the number of points in the GPX
|
|
68
|
+
const latStream = activity.getStream('Latitude');
|
|
69
|
+
expect(latStream.getData().length).toBe(2987); // I counted 2987 rtept earlier
|
|
70
|
+
}));
|
|
54
71
|
});
|
|
@@ -18,6 +18,7 @@ export const ImporterSuuntoDeviceNames = {
|
|
|
18
18
|
Gdansk: 'Spartan WHR Baro',
|
|
19
19
|
'Spartan Sport Wrist HR Baro': 'Spartan WHR Baro',
|
|
20
20
|
Helsinki: '3 Fitness',
|
|
21
|
+
Ambit2: 'Ambit2',
|
|
21
22
|
'Ambit3 Sport': 'Ambit3 Sport',
|
|
22
23
|
'Ambit3 Peak': 'Ambit3 Peak',
|
|
23
24
|
'Ambit3 Run': 'Ambit3 Run',
|
|
@@ -108,8 +108,8 @@ const ALTITUDE_SPIKES_FILTER_WIN = 3;
|
|
|
108
108
|
// Fix abnormal streams
|
|
109
109
|
const SPEED_STREAM_STD_DEV_THRESHOLD_DEFAULT = 25 / 3.6; // Kph to mps
|
|
110
110
|
const SPEED_STREAM_STD_DEV_THRESHOLD_MAP = new Map([
|
|
111
|
-
[ActivityTypeGroups.Running, 15 / 3.6],
|
|
112
|
-
[ActivityTypeGroups.Cycling, 27 / 3.6],
|
|
111
|
+
[ActivityTypeGroups.Running, 15 / 3.6], // kph to m/s
|
|
112
|
+
[ActivityTypeGroups.Cycling, 27 / 3.6], // kph to m/s
|
|
113
113
|
[ActivityTypeGroups.Swimming, 5 / 3.6] // kph to m/s
|
|
114
114
|
]);
|
|
115
115
|
export class ActivityUtilities {
|
|
@@ -275,7 +275,7 @@ export class ActivityUtilities {
|
|
|
275
275
|
this.shapeStream(DataSpeed.type, activity, squashedSpeedData => {
|
|
276
276
|
// Grade stream
|
|
277
277
|
const SPEED_KALMAN_SMOOTHING = {
|
|
278
|
-
R: 0.01,
|
|
278
|
+
R: 0.01, // Speed model calculation is something stable
|
|
279
279
|
Q: speedStdDev * 2 // We intend to get a measurement error which can be under and over std dev (explaining the double factor)
|
|
280
280
|
};
|
|
281
281
|
// Apply kalman filter
|
|
@@ -291,7 +291,7 @@ export class ActivityUtilities {
|
|
|
291
291
|
// Always include derived base streams (like Pace), but conditionally include unit variants
|
|
292
292
|
const includeUnitVariants = !activity.parseOptions || activity.parseOptions.generateUnitStreams;
|
|
293
293
|
activity.addStreams(this.createUnitStreamsFromStreams(activity.getAllStreams(), activity.type, undefined, {
|
|
294
|
-
includeDerivedTypes: true,
|
|
294
|
+
includeDerivedTypes: true, // Always include derived base types (Pace etc)
|
|
295
295
|
includeUnitVariants
|
|
296
296
|
}));
|
|
297
297
|
}
|
|
@@ -757,9 +757,9 @@ export class ActivityUtilities {
|
|
|
757
757
|
return prevPosition;
|
|
758
758
|
}
|
|
759
759
|
if (prevPosition && position) {
|
|
760
|
-
distance += this.
|
|
760
|
+
distance += this.geoLibAdapter.getDistance([prevPosition, position]);
|
|
761
761
|
}
|
|
762
|
-
streamData[index] = distance;
|
|
762
|
+
streamData[index] = this.round(distance, 2);
|
|
763
763
|
return position;
|
|
764
764
|
});
|
|
765
765
|
if (!activity.hasStreamData(DataDistance.type)) {
|
|
@@ -812,7 +812,7 @@ export class ActivityUtilities {
|
|
|
812
812
|
this.shapeStream(DataGradeSmooth.type, activity, squashedGradeData => {
|
|
813
813
|
// Grade stream
|
|
814
814
|
const GRADE_KALMAN_SMOOTHING = {
|
|
815
|
-
R: 0.01,
|
|
815
|
+
R: 0.01, // Grade model is stable
|
|
816
816
|
Q: 0.5 // Grade measurement error which can be expected
|
|
817
817
|
};
|
|
818
818
|
// Predict proper grade values
|
|
@@ -1,12 +1,12 @@
|
|
|
1
1
|
export class LowPassFilter {
|
|
2
|
+
static smooth(values, smoothing = 0.5) {
|
|
3
|
+
return new LowPassFilter(smoothing).smoothArray(values);
|
|
4
|
+
}
|
|
2
5
|
constructor(smoothing) {
|
|
3
6
|
this._smoothing = smoothing || 0.5; // must be smaller than 1
|
|
4
7
|
this._buffer = []; // FIFO queue
|
|
5
8
|
this._bufferMaxSize = 10;
|
|
6
9
|
}
|
|
7
|
-
static smooth(values, smoothing = 0.5) {
|
|
8
|
-
return new LowPassFilter(smoothing).smoothArray(values);
|
|
9
|
-
}
|
|
10
10
|
/**
|
|
11
11
|
* Init buffer with array of values
|
|
12
12
|
* @param {number[]} values
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
export declare function isNumberOrString(property: any):
|
|
1
|
+
export declare function isNumberOrString(property: any): property is string | number;
|
|
2
2
|
export declare function isNumber(property: any): boolean;
|
|
3
3
|
/**
|
|
4
4
|
* Converts speed from m/s to pace as of seconds per km
|
package/lib/esm/index.d.ts
CHANGED
|
@@ -20,7 +20,7 @@ export declare class SportsLib {
|
|
|
20
20
|
* @param arrayBuffer
|
|
21
21
|
* @param options
|
|
22
22
|
*/
|
|
23
|
-
static importFromFit(arrayBuffer: ArrayBuffer
|
|
23
|
+
static importFromFit(arrayBuffer: ArrayBuffer | Buffer<ArrayBuffer>, options?: ActivityParsingOptions): Promise<EventInterface>;
|
|
24
24
|
/**
|
|
25
25
|
* Parses and returns an event using Suunto format
|
|
26
26
|
* @param jsonString
|