@sports-alliance/sports-lib 6.1.10 → 6.1.12

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/babel.config.js CHANGED
@@ -1,3 +1,3 @@
1
1
  module.exports = {
2
- presets: [['@babel/preset-env', { targets: { node: 'current' } }]],
2
+ presets: [['@babel/preset-env', { targets: { node: 'current' } }]]
3
3
  };
package/jest.config.js CHANGED
@@ -18,5 +18,6 @@ module.exports = {
18
18
  'ts-jest': {
19
19
  babelConfig: true
20
20
  }
21
- }
21
+ },
22
+ setupFilesAfterEnv: ['<rootDir>/jest.setup.ts']
22
23
  };
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.');
@@ -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];
@@ -70,7 +70,9 @@ describe('DataStore', () => {
70
70
  temperatureUnits: [],
71
71
  weightUnits: []
72
72
  };
73
- const result = data_store_1.DynamicDataLoader.getUnitBasedDataTypesFromDataTypes([data_speed_1.DataSpeed.type], settings, { includeDerivedTypes: false });
73
+ const result = data_store_1.DynamicDataLoader.getUnitBasedDataTypesFromDataTypes([data_speed_1.DataSpeed.type], settings, {
74
+ includeDerivedTypes: false
75
+ });
74
76
  expect(result).toContain(data_speed_1.DataSpeedKilometersPerHour.type);
75
77
  expect(result).not.toContain(data_pace_1.DataPaceMinutesPerMile.type);
76
78
  });
@@ -627,10 +627,20 @@ class EventImporterFIT {
627
627
  // Pause TIME on Object (activity, lap...)
628
628
  const pause = elapsedTime > movingTime && movingTime > 0 ? Math.round((elapsedTime - movingTime) * 100) / 100 : 0;
629
629
  stats.push(new data_pause_1.DataPause(pause));
630
+ const getStatValue = (obj, keys) => {
631
+ for (const key of keys) {
632
+ if ((0, helpers_1.isNumberOrString)(obj[key])) {
633
+ return obj[key];
634
+ }
635
+ }
636
+ return null;
637
+ };
638
+ const avgSpeed = getStatValue(object, ['enhanced_avg_speed', 'EnhancedAvgSpeed', 'avg_speed', 'AvgSpeed']);
639
+ const totalDistance = getStatValue(object, ['total_distance', 'TotalDistance']);
630
640
  // Assign is active lap status
631
- stats.push(new data_active_lap_1.DataActiveLap(!!(object.total_distance || object.avg_speed)));
632
- if ((0, helpers_1.isNumberOrString)(object.total_distance)) {
633
- stats.push(new data_distance_1.DataDistance(object.total_distance));
641
+ stats.push(new data_active_lap_1.DataActiveLap(!!(totalDistance || avgSpeed)));
642
+ if (totalDistance !== null) {
643
+ stats.push(new data_distance_1.DataDistance(totalDistance));
634
644
  }
635
645
  else {
636
646
  stats.push(new data_distance_1.DataDistance(0));
@@ -690,24 +700,16 @@ class EventImporterFIT {
690
700
  stats.push(new data_power_pedal_smoothness_right_1.DataPowerPedalSmoothnessRight(object.avg_right_pedal_smoothness));
691
701
  }
692
702
  // Speed
693
- if ((0, helpers_1.isNumberOrString)(object.avg_speed)) {
694
- stats.push(new data_speed_avg_1.DataSpeedAvg(object.avg_speed));
695
- }
696
- if ((0, helpers_1.isNumberOrString)(object.min_speed)) {
697
- stats.push(new data_speed_min_1.DataSpeedMin(object.min_speed));
698
- }
699
- if ((0, helpers_1.isNumberOrString)(object.max_speed)) {
700
- stats.push(new data_speed_max_1.DataSpeedMax(object.max_speed));
701
- }
702
- // Keep latest , enhanced @todo this can create a bug
703
- if ((0, helpers_1.isNumberOrString)(object.enhanced_avg_speed)) {
704
- stats.push(new data_speed_avg_1.DataSpeedAvg(object.enhanced_avg_speed));
703
+ if (avgSpeed !== null) {
704
+ stats.push(new data_speed_avg_1.DataSpeedAvg(avgSpeed));
705
705
  }
706
- if ((0, helpers_1.isNumberOrString)(object.enhanced_min_speed)) {
707
- stats.push(new data_speed_min_1.DataSpeedMin(object.enhanced_min_speed));
706
+ const minSpeed = getStatValue(object, ['enhanced_min_speed', 'EnhancedMinSpeed', 'min_speed', 'MinSpeed']);
707
+ if (minSpeed !== null) {
708
+ stats.push(new data_speed_min_1.DataSpeedMin(minSpeed));
708
709
  }
709
- if ((0, helpers_1.isNumberOrString)(object.enhanced_max_speed)) {
710
- stats.push(new data_speed_max_1.DataSpeedMax(object.enhanced_max_speed));
710
+ const maxSpeed = getStatValue(object, ['enhanced_max_speed', 'EnhancedMaxSpeed', 'max_speed', 'MaxSpeed']);
711
+ if (maxSpeed !== null) {
712
+ stats.push(new data_speed_max_1.DataSpeedMax(maxSpeed));
711
713
  }
712
714
  // Temperature
713
715
  if ((0, helpers_1.isNumberOrString)(object.avg_temperature)) {
@@ -720,12 +722,14 @@ class EventImporterFIT {
720
722
  stats.push(new data_temperature_max_1.DataTemperatureMax(object.max_temperature));
721
723
  }
722
724
  // Ascent
723
- if ((0, helpers_1.isNumberOrString)(object.total_ascent)) {
724
- stats.push(new data_ascent_1.DataAscent(object.total_ascent));
725
+ const ascent = getStatValue(object, ['total_ascent', 'TotalAscent']);
726
+ if (ascent !== null) {
727
+ stats.push(new data_ascent_1.DataAscent(ascent));
725
728
  }
726
729
  // Descent
727
- if ((0, helpers_1.isNumberOrString)(object.total_descent)) {
728
- stats.push(new data_descent_1.DataDescent(object.total_descent));
730
+ const descent = getStatValue(object, ['total_descent', 'TotalDescent']);
731
+ if (descent !== null) {
732
+ stats.push(new data_descent_1.DataDescent(descent));
729
733
  }
730
734
  // Calories
731
735
  if ((0, helpers_1.isNumberOrString)(object.total_calories)) {
@@ -50,7 +50,7 @@ exports.FITSampleMapper = [
50
50
  {
51
51
  dataType: data_distance_1.DataDistance.type,
52
52
  getSampleValue: (sample) => {
53
- return sample.distance;
53
+ return (0, helpers_1.isNumber)(sample.distance) ? sample.distance : sample.Distance;
54
54
  }
55
55
  },
56
56
  {
@@ -62,13 +62,19 @@ exports.FITSampleMapper = [
62
62
  {
63
63
  dataType: data_altitude_1.DataAltitude.type,
64
64
  getSampleValue: (sample) => {
65
- return (0, helpers_1.isNumber)(sample.enhanced_altitude)
66
- ? Math.round(sample.enhanced_altitude * Math.pow(10, constants_1.ALTITUDE_PRECISION_NUMBER_OF_DECIMAL_PLACES)) /
65
+ const altitude = (0, helpers_1.isNumber)(sample.enhanced_altitude)
66
+ ? sample.enhanced_altitude
67
+ : (0, helpers_1.isNumber)(sample.EnhancedAltitude)
68
+ ? sample.EnhancedAltitude
69
+ : (0, helpers_1.isNumber)(sample.altitude)
70
+ ? sample.altitude
71
+ : (0, helpers_1.isNumber)(sample.Altitude)
72
+ ? sample.Altitude
73
+ : null;
74
+ return (0, helpers_1.isNumber)(altitude)
75
+ ? Math.round(altitude * Math.pow(10, constants_1.ALTITUDE_PRECISION_NUMBER_OF_DECIMAL_PLACES)) /
67
76
  Math.pow(10, constants_1.ALTITUDE_PRECISION_NUMBER_OF_DECIMAL_PLACES)
68
- : (0, helpers_1.isNumber)(sample.altitude)
69
- ? Math.round(sample.altitude * Math.pow(10, constants_1.ALTITUDE_PRECISION_NUMBER_OF_DECIMAL_PLACES)) /
70
- Math.pow(10, constants_1.ALTITUDE_PRECISION_NUMBER_OF_DECIMAL_PLACES)
71
- : sample.altitude;
77
+ : altitude;
72
78
  }
73
79
  },
74
80
  {
@@ -105,12 +111,18 @@ exports.FITSampleMapper = [
105
111
  {
106
112
  dataType: data_speed_1.DataSpeed.type,
107
113
  getSampleValue: (sample) => {
108
- if (Number.isFinite(sample.enhanced_speed)) {
114
+ if ((0, helpers_1.isNumber)(sample.enhanced_speed)) {
109
115
  return sample.enhanced_speed;
110
116
  }
111
- if (Number.isFinite(sample.speed)) {
117
+ if ((0, helpers_1.isNumber)(sample.EnhancedSpeed)) {
118
+ return sample.EnhancedSpeed;
119
+ }
120
+ if ((0, helpers_1.isNumber)(sample.speed)) {
112
121
  return sample.speed;
113
122
  }
123
+ if ((0, helpers_1.isNumber)(sample.Speed)) {
124
+ return sample.Speed;
125
+ }
114
126
  return null;
115
127
  }
116
128
  },
@@ -291,9 +291,12 @@ class ActivityUtilities {
291
291
  static generateMissingStreams(activity) {
292
292
  // Compute missing streams
293
293
  this.generateMissingStreamsForActivity(activity);
294
- if (!activity.parseOptions || activity.parseOptions.generateUnitStreams) {
295
- activity.addStreams(this.createUnitStreamsFromStreams(activity.getAllStreams(), activity.type));
296
- }
294
+ // Always include derived base streams (like Pace), but conditionally include unit variants
295
+ const includeUnitVariants = !activity.parseOptions || activity.parseOptions.generateUnitStreams;
296
+ activity.addStreams(this.createUnitStreamsFromStreams(activity.getAllStreams(), activity.type, undefined, {
297
+ includeDerivedTypes: true,
298
+ includeUnitVariants
299
+ }));
297
300
  }
298
301
  static getSummaryStatsForActivities(activities) {
299
302
  const stats = [];
@@ -680,34 +683,37 @@ class ActivityUtilities {
680
683
  includeUnitVariants: true
681
684
  }) {
682
685
  // @todo perhaps check input to be unitStreamTypesStrictly
683
- const unitStreamTypesToCreate = unitStreamTypes || data_store_1.DynamicDataLoader.allUnitDerivedDataTypes;
686
+ const unitStreamTypesToCreate = unitStreamTypes || [
687
+ ...data_store_1.DynamicDataLoader.allUnitDerivedDataTypes,
688
+ ...data_store_1.DynamicDataLoader.speedDerivedDataTypes
689
+ ];
684
690
  let baseUnitStreams = [];
685
- const speedStream = streams.find(stream => stream.type === data_speed_1.DataSpeed.type);
686
- if (speedStream) {
687
- if (options.includeDerivedTypes) {
688
- baseUnitStreams = baseUnitStreams.concat(this.createByActivityTypeSpeedBasedStreams(speedStream, activityType));
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;
689
697
  }
690
- else {
691
- baseUnitStreams.push(speedStream);
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;
692
702
  }
693
- }
694
- const gradeAdjustedSpeedStream = streams.find(stream => stream.type === data_grade_adjusted_speed_1.DataGradeAdjustedSpeed.type);
695
- if (gradeAdjustedSpeedStream) {
696
- if (options.includeDerivedTypes) {
697
- 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;
698
706
  }
699
- else {
700
- baseUnitStreams.push(gradeAdjustedSpeedStream);
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;
701
713
  }
702
- }
703
- const verticalSpeedStream = streams.find(stream => stream.type === data_vertical_speed_1.DataVerticalSpeed.type);
704
- if (verticalSpeedStream) {
705
- // For vertical speed (yet) we dont need a seperate function so just add the base that is the "derived" one
706
- baseUnitStreams = activity_types_1.ActivityTypesHelper.verticalSpeedDerivedDataTypesToUseForActivityType(activityType).length
707
- ? baseUnitStreams.concat(verticalSpeedStream)
708
- : baseUnitStreams;
709
- }
710
- // @todo add distance ?
714
+ // For everything else (like Distance), just add the base stream so it can be used for unit generation
715
+ baseUnitStreams.push(stream);
716
+ });
711
717
  const startWith = baseUnitStreams.filter(baseUnitStream => unitStreamTypesToCreate.indexOf(baseUnitStream.type) !== -1 && streams.indexOf(baseUnitStream) === -1);
712
718
  if (options.includeUnitVariants === false) {
713
719
  return startWith;
@@ -867,7 +873,8 @@ class ActivityUtilities {
867
873
  activity.addStream(leftPowerStream);
868
874
  }
869
875
  // If left stance time stream available, then add the right balance stream too
870
- if (activity.hasStreamData(data_stance_time_balance_left_1.DataStanceTimeBalanceLeft.type) && !activity.hasStreamData(data_stance_time_balance_right_1.DataStanceTimeBalanceRight.type)) {
876
+ if (activity.hasStreamData(data_stance_time_balance_left_1.DataStanceTimeBalanceLeft.type) &&
877
+ !activity.hasStreamData(data_stance_time_balance_right_1.DataStanceTimeBalanceRight.type)) {
871
878
  const rightStanceBalanceTimeStream = activity.createStream(data_stance_time_balance_right_1.DataStanceTimeBalanceRight.type);
872
879
  const leftStanceBalanceTimeStream = activity.getStreamData(data_stance_time_balance_left_1.DataStanceTimeBalanceLeft.type);
873
880
  const rightStanceBalanceTimeData = leftStanceBalanceTimeStream.map(leftBalance => {
@@ -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(() => {
@@ -238,7 +240,10 @@ describe('Activity Utilities', () => {
238
240
  });
239
241
  it('should exclude derived types when includeDerivedTypes is false', () => {
240
242
  const streams = [new stream_1.Stream(data_speed_1.DataSpeed.type, [10, 20])];
241
- const result = activity_utilities_1.ActivityUtilities.createUnitStreamsFromStreams(streams, activity_types_1.ActivityTypes.Running, undefined, { includeDerivedTypes: false, includeUnitVariants: true });
243
+ const result = activity_utilities_1.ActivityUtilities.createUnitStreamsFromStreams(streams, activity_types_1.ActivityTypes.Running, undefined, {
244
+ includeDerivedTypes: false,
245
+ includeUnitVariants: true
246
+ });
242
247
  const paceStream = result.find(s => s.type === data_pace_1.DataPaceMinutesPerMile.type);
243
248
  const kmhStream = result.find(s => s.type === data_speed_2.DataSpeedKilometersPerHour.type);
244
249
  expect(paceStream).toBeUndefined();
@@ -247,7 +252,10 @@ describe('Activity Utilities', () => {
247
252
  it('should exclude unit variants when includeUnitVariants is false', () => {
248
253
  const streams = [new stream_1.Stream(data_speed_1.DataSpeed.type, [10, 20])];
249
254
  // We pass DataPace.type in unitStreamTypes so we can check if the derived base stream is present
250
- const result = activity_utilities_1.ActivityUtilities.createUnitStreamsFromStreams(streams, activity_types_1.ActivityTypes.Running, [data_pace_1.DataPace.type], { includeDerivedTypes: true, includeUnitVariants: false });
255
+ const result = activity_utilities_1.ActivityUtilities.createUnitStreamsFromStreams(streams, activity_types_1.ActivityTypes.Running, [data_pace_1.DataPace.type], {
256
+ includeDerivedTypes: true,
257
+ includeUnitVariants: false
258
+ });
251
259
  const paceStream = result.find(s => s.type === data_pace_1.DataPace.type);
252
260
  const paceUnitStream = result.find(s => s.type === data_pace_1.DataPaceMinutesPerMile.type);
253
261
  expect(paceStream).toBeDefined(); // Derived type should be there
@@ -255,7 +263,10 @@ describe('Activity Utilities', () => {
255
263
  });
256
264
  it('should exclude both derived types and unit variants', () => {
257
265
  const streams = [new stream_1.Stream(data_speed_1.DataSpeed.type, [10, 20])];
258
- const result = activity_utilities_1.ActivityUtilities.createUnitStreamsFromStreams(streams, activity_types_1.ActivityTypes.Running, [data_pace_1.DataPace.type], { includeDerivedTypes: false, includeUnitVariants: false });
266
+ const result = activity_utilities_1.ActivityUtilities.createUnitStreamsFromStreams(streams, activity_types_1.ActivityTypes.Running, [data_pace_1.DataPace.type], {
267
+ includeDerivedTypes: false,
268
+ includeUnitVariants: false
269
+ });
259
270
  const paceStream = result.find(s => s.type === data_pace_1.DataPace.type);
260
271
  const paceUnitStream = result.find(s => s.type === data_pace_1.DataPaceMinutesPerMile.type);
261
272
  expect(paceStream).toBeUndefined();
@@ -263,8 +274,8 @@ describe('Activity Utilities', () => {
263
274
  });
264
275
  });
265
276
  describe('generateMissingStreams', () => {
266
- it('should generate unit streams by default (generateUnitStreams = true)', () => {
267
- 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 }));
268
279
  // Add a speed stream
269
280
  activity.addStream(new stream_1.Stream(data_speed_1.DataSpeed.type, [10, 20]));
270
281
  activity_utilities_1.ActivityUtilities.generateMissingStreams(activity);
@@ -278,7 +289,7 @@ describe('Activity Utilities', () => {
278
289
  activity.parseOptions = {
279
290
  streams: { smooth: {}, fixAbnormal: {} },
280
291
  maxActivityDurationDays: 14,
281
- generateUnitStreams: false,
292
+ generateUnitStreams: false
282
293
  };
283
294
  // Add a speed stream
284
295
  activity.addStream(new stream_1.Stream(data_speed_1.DataSpeed.type, [10, 20]));
@@ -296,24 +307,69 @@ describe('Activity Utilities', () => {
296
307
  activity.parseOptions = {
297
308
  streams: { smooth: {}, fixAbnormal: {} },
298
309
  maxActivityDurationDays: 14,
299
- generateUnitStreams: false, // DISABLE streams
310
+ generateUnitStreams: false // DISABLE streams
300
311
  };
301
312
  // Add a speed stream [10 m/s, 20 m/s]
302
313
  // Max speed = 20 m/s
303
314
  activity.addStream(new stream_1.Stream(data_speed_1.DataSpeed.type, [10, 20]));
304
315
  activity_utilities_1.ActivityUtilities.generateMissingStreamsAndStatsForActivity(activity);
305
- // 1. Verify Streams are missing (as requested)
306
- expect(activity.hasStreamData(data_speed_2.DataSpeedKilometersPerHour.type)).toBe(false);
307
- // 1. Verify Streams are missing (as requested)
316
+ // 1. Verify Unit Streams are missing (as requested)
308
317
  expect(activity.hasStreamData(data_speed_2.DataSpeedKilometersPerHour.type)).toBe(false);
309
- // 2. Verify Stats are PRESENT (Safety Check)
318
+ // 2. Verify Derived Base Streams are PRESENT (The fix)
319
+ expect(activity.hasStreamData(data_pace_1.DataPace.type)).toBe(true);
320
+ // 3. Verify Stats are PRESENT (Safety Check)
310
321
  // 20 m/s = 72 km/h
311
- // We check for the stat by its string type if we can't import the constant easily, or iterate.
312
- // Based on activity.utilities.ts it uses: DataSpeedMaxKilometersPerHour
313
322
  const allStats = Array.from(activity.getStats().values());
314
323
  const speedMaxKmh = allStats.find(s => s.getType() === data_speed_max_1.DataSpeedMaxKilometersPerHour.type);
315
324
  expect(speedMaxKmh).toBeDefined();
316
325
  expect(speedMaxKmh === null || speedMaxKmh === void 0 ? void 0 : speedMaxKmh.getValue()).toBe(72);
317
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
+ });
318
374
  });
319
375
  });
@@ -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];
@@ -68,7 +68,9 @@ describe('DataStore', () => {
68
68
  temperatureUnits: [],
69
69
  weightUnits: []
70
70
  };
71
- const result = DynamicDataLoader.getUnitBasedDataTypesFromDataTypes([DataSpeed.type], settings, { includeDerivedTypes: false });
71
+ const result = DynamicDataLoader.getUnitBasedDataTypesFromDataTypes([DataSpeed.type], settings, {
72
+ includeDerivedTypes: false
73
+ });
72
74
  expect(result).toContain(DataSpeedKilometersPerHour.type);
73
75
  expect(result).not.toContain(DataPaceMinutesPerMile.type);
74
76
  });
@@ -621,10 +621,20 @@ export class EventImporterFIT {
621
621
  // Pause TIME on Object (activity, lap...)
622
622
  const pause = elapsedTime > movingTime && movingTime > 0 ? Math.round((elapsedTime - movingTime) * 100) / 100 : 0;
623
623
  stats.push(new DataPause(pause));
624
+ const getStatValue = (obj, keys) => {
625
+ for (const key of keys) {
626
+ if (isNumberOrString(obj[key])) {
627
+ return obj[key];
628
+ }
629
+ }
630
+ return null;
631
+ };
632
+ const avgSpeed = getStatValue(object, ['enhanced_avg_speed', 'EnhancedAvgSpeed', 'avg_speed', 'AvgSpeed']);
633
+ const totalDistance = getStatValue(object, ['total_distance', 'TotalDistance']);
624
634
  // Assign is active lap status
625
- stats.push(new DataActiveLap(!!(object.total_distance || object.avg_speed)));
626
- if (isNumberOrString(object.total_distance)) {
627
- stats.push(new DataDistance(object.total_distance));
635
+ stats.push(new DataActiveLap(!!(totalDistance || avgSpeed)));
636
+ if (totalDistance !== null) {
637
+ stats.push(new DataDistance(totalDistance));
628
638
  }
629
639
  else {
630
640
  stats.push(new DataDistance(0));
@@ -684,24 +694,16 @@ export class EventImporterFIT {
684
694
  stats.push(new DataPowerPedalSmoothnessRight(object.avg_right_pedal_smoothness));
685
695
  }
686
696
  // Speed
687
- if (isNumberOrString(object.avg_speed)) {
688
- stats.push(new DataSpeedAvg(object.avg_speed));
689
- }
690
- if (isNumberOrString(object.min_speed)) {
691
- stats.push(new DataSpeedMin(object.min_speed));
692
- }
693
- if (isNumberOrString(object.max_speed)) {
694
- stats.push(new DataSpeedMax(object.max_speed));
695
- }
696
- // Keep latest , enhanced @todo this can create a bug
697
- if (isNumberOrString(object.enhanced_avg_speed)) {
698
- stats.push(new DataSpeedAvg(object.enhanced_avg_speed));
697
+ if (avgSpeed !== null) {
698
+ stats.push(new DataSpeedAvg(avgSpeed));
699
699
  }
700
- if (isNumberOrString(object.enhanced_min_speed)) {
701
- stats.push(new DataSpeedMin(object.enhanced_min_speed));
700
+ const minSpeed = getStatValue(object, ['enhanced_min_speed', 'EnhancedMinSpeed', 'min_speed', 'MinSpeed']);
701
+ if (minSpeed !== null) {
702
+ stats.push(new DataSpeedMin(minSpeed));
702
703
  }
703
- if (isNumberOrString(object.enhanced_max_speed)) {
704
- stats.push(new DataSpeedMax(object.enhanced_max_speed));
704
+ const maxSpeed = getStatValue(object, ['enhanced_max_speed', 'EnhancedMaxSpeed', 'max_speed', 'MaxSpeed']);
705
+ if (maxSpeed !== null) {
706
+ stats.push(new DataSpeedMax(maxSpeed));
705
707
  }
706
708
  // Temperature
707
709
  if (isNumberOrString(object.avg_temperature)) {
@@ -714,12 +716,14 @@ export class EventImporterFIT {
714
716
  stats.push(new DataTemperatureMax(object.max_temperature));
715
717
  }
716
718
  // Ascent
717
- if (isNumberOrString(object.total_ascent)) {
718
- stats.push(new DataAscent(object.total_ascent));
719
+ const ascent = getStatValue(object, ['total_ascent', 'TotalAscent']);
720
+ if (ascent !== null) {
721
+ stats.push(new DataAscent(ascent));
719
722
  }
720
723
  // Descent
721
- if (isNumberOrString(object.total_descent)) {
722
- stats.push(new DataDescent(object.total_descent));
724
+ const descent = getStatValue(object, ['total_descent', 'TotalDescent']);
725
+ if (descent !== null) {
726
+ stats.push(new DataDescent(descent));
723
727
  }
724
728
  // Calories
725
729
  if (isNumberOrString(object.total_calories)) {
@@ -47,7 +47,7 @@ export const FITSampleMapper = [
47
47
  {
48
48
  dataType: DataDistance.type,
49
49
  getSampleValue: (sample) => {
50
- return sample.distance;
50
+ return isNumber(sample.distance) ? sample.distance : sample.Distance;
51
51
  }
52
52
  },
53
53
  {
@@ -59,13 +59,19 @@ export const FITSampleMapper = [
59
59
  {
60
60
  dataType: DataAltitude.type,
61
61
  getSampleValue: (sample) => {
62
- return isNumber(sample.enhanced_altitude)
63
- ? Math.round(sample.enhanced_altitude * Math.pow(10, ALTITUDE_PRECISION_NUMBER_OF_DECIMAL_PLACES)) /
62
+ const altitude = isNumber(sample.enhanced_altitude)
63
+ ? sample.enhanced_altitude
64
+ : isNumber(sample.EnhancedAltitude)
65
+ ? sample.EnhancedAltitude
66
+ : isNumber(sample.altitude)
67
+ ? sample.altitude
68
+ : isNumber(sample.Altitude)
69
+ ? sample.Altitude
70
+ : null;
71
+ return isNumber(altitude)
72
+ ? Math.round(altitude * Math.pow(10, ALTITUDE_PRECISION_NUMBER_OF_DECIMAL_PLACES)) /
64
73
  Math.pow(10, ALTITUDE_PRECISION_NUMBER_OF_DECIMAL_PLACES)
65
- : isNumber(sample.altitude)
66
- ? Math.round(sample.altitude * Math.pow(10, ALTITUDE_PRECISION_NUMBER_OF_DECIMAL_PLACES)) /
67
- Math.pow(10, ALTITUDE_PRECISION_NUMBER_OF_DECIMAL_PLACES)
68
- : sample.altitude;
74
+ : altitude;
69
75
  }
70
76
  },
71
77
  {
@@ -102,12 +108,18 @@ export const FITSampleMapper = [
102
108
  {
103
109
  dataType: DataSpeed.type,
104
110
  getSampleValue: (sample) => {
105
- if (Number.isFinite(sample.enhanced_speed)) {
111
+ if (isNumber(sample.enhanced_speed)) {
106
112
  return sample.enhanced_speed;
107
113
  }
108
- if (Number.isFinite(sample.speed)) {
114
+ if (isNumber(sample.EnhancedSpeed)) {
115
+ return sample.EnhancedSpeed;
116
+ }
117
+ if (isNumber(sample.speed)) {
109
118
  return sample.speed;
110
119
  }
120
+ if (isNumber(sample.Speed)) {
121
+ return sample.Speed;
122
+ }
111
123
  return null;
112
124
  }
113
125
  },
@@ -288,9 +288,12 @@ export class ActivityUtilities {
288
288
  static generateMissingStreams(activity) {
289
289
  // Compute missing streams
290
290
  this.generateMissingStreamsForActivity(activity);
291
- if (!activity.parseOptions || activity.parseOptions.generateUnitStreams) {
292
- activity.addStreams(this.createUnitStreamsFromStreams(activity.getAllStreams(), activity.type));
293
- }
291
+ // Always include derived base streams (like Pace), but conditionally include unit variants
292
+ const includeUnitVariants = !activity.parseOptions || activity.parseOptions.generateUnitStreams;
293
+ activity.addStreams(this.createUnitStreamsFromStreams(activity.getAllStreams(), activity.type, undefined, {
294
+ includeDerivedTypes: true,
295
+ includeUnitVariants
296
+ }));
294
297
  }
295
298
  static getSummaryStatsForActivities(activities) {
296
299
  const stats = [];
@@ -677,34 +680,37 @@ export class ActivityUtilities {
677
680
  includeUnitVariants: true
678
681
  }) {
679
682
  // @todo perhaps check input to be unitStreamTypesStrictly
680
- const unitStreamTypesToCreate = unitStreamTypes || DynamicDataLoader.allUnitDerivedDataTypes;
683
+ const unitStreamTypesToCreate = unitStreamTypes || [
684
+ ...DynamicDataLoader.allUnitDerivedDataTypes,
685
+ ...DynamicDataLoader.speedDerivedDataTypes
686
+ ];
681
687
  let baseUnitStreams = [];
682
- const speedStream = streams.find(stream => stream.type === DataSpeed.type);
683
- if (speedStream) {
684
- if (options.includeDerivedTypes) {
685
- baseUnitStreams = baseUnitStreams.concat(this.createByActivityTypeSpeedBasedStreams(speedStream, activityType));
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;
686
694
  }
687
- else {
688
- baseUnitStreams.push(speedStream);
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;
689
699
  }
690
- }
691
- const gradeAdjustedSpeedStream = streams.find(stream => stream.type === DataGradeAdjustedSpeed.type);
692
- if (gradeAdjustedSpeedStream) {
693
- if (options.includeDerivedTypes) {
694
- baseUnitStreams = baseUnitStreams.concat(this.createByActivityTypeAltiDistanceSpeedBasedStreams(gradeAdjustedSpeedStream, activityType));
700
+ if (baseDataType === DataGradeAdjustedSpeed.type && options.includeDerivedTypes) {
701
+ baseUnitStreams = baseUnitStreams.concat(this.createByActivityTypeAltiDistanceSpeedBasedStreams(stream, activityType));
702
+ return;
695
703
  }
696
- else {
697
- baseUnitStreams.push(gradeAdjustedSpeedStream);
704
+ if (baseDataType === DataVerticalSpeed.type) {
705
+ // Vertical speed handling
706
+ if (ActivityTypesHelper.verticalSpeedDerivedDataTypesToUseForActivityType(activityType).length) {
707
+ baseUnitStreams.push(stream);
708
+ }
709
+ return;
698
710
  }
699
- }
700
- const verticalSpeedStream = streams.find(stream => stream.type === DataVerticalSpeed.type);
701
- if (verticalSpeedStream) {
702
- // For vertical speed (yet) we dont need a seperate function so just add the base that is the "derived" one
703
- baseUnitStreams = ActivityTypesHelper.verticalSpeedDerivedDataTypesToUseForActivityType(activityType).length
704
- ? baseUnitStreams.concat(verticalSpeedStream)
705
- : baseUnitStreams;
706
- }
707
- // @todo add distance ?
711
+ // For everything else (like Distance), just add the base stream so it can be used for unit generation
712
+ baseUnitStreams.push(stream);
713
+ });
708
714
  const startWith = baseUnitStreams.filter(baseUnitStream => unitStreamTypesToCreate.indexOf(baseUnitStream.type) !== -1 && streams.indexOf(baseUnitStream) === -1);
709
715
  if (options.includeUnitVariants === false) {
710
716
  return startWith;
@@ -864,7 +870,8 @@ export class ActivityUtilities {
864
870
  activity.addStream(leftPowerStream);
865
871
  }
866
872
  // If left stance time stream available, then add the right balance stream too
867
- if (activity.hasStreamData(DataStanceTimeBalanceLeft.type) && !activity.hasStreamData(DataStanceTimeBalanceRight.type)) {
873
+ if (activity.hasStreamData(DataStanceTimeBalanceLeft.type) &&
874
+ !activity.hasStreamData(DataStanceTimeBalanceRight.type)) {
868
875
  const rightStanceBalanceTimeStream = activity.createStream(DataStanceTimeBalanceRight.type);
869
876
  const leftStanceBalanceTimeStream = activity.getStreamData(DataStanceTimeBalanceLeft.type);
870
877
  const rightStanceBalanceTimeData = leftStanceBalanceTimeStream.map(leftBalance => {
@@ -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(() => {
@@ -236,7 +238,10 @@ describe('Activity Utilities', () => {
236
238
  });
237
239
  it('should exclude derived types when includeDerivedTypes is false', () => {
238
240
  const streams = [new Stream(DataSpeed.type, [10, 20])];
239
- const result = ActivityUtilities.createUnitStreamsFromStreams(streams, ActivityTypes.Running, undefined, { includeDerivedTypes: false, includeUnitVariants: true });
241
+ const result = ActivityUtilities.createUnitStreamsFromStreams(streams, ActivityTypes.Running, undefined, {
242
+ includeDerivedTypes: false,
243
+ includeUnitVariants: true
244
+ });
240
245
  const paceStream = result.find(s => s.type === DataPaceMinutesPerMile.type);
241
246
  const kmhStream = result.find(s => s.type === DataSpeedKilometersPerHour.type);
242
247
  expect(paceStream).toBeUndefined();
@@ -245,7 +250,10 @@ describe('Activity Utilities', () => {
245
250
  it('should exclude unit variants when includeUnitVariants is false', () => {
246
251
  const streams = [new Stream(DataSpeed.type, [10, 20])];
247
252
  // We pass DataPace.type in unitStreamTypes so we can check if the derived base stream is present
248
- const result = ActivityUtilities.createUnitStreamsFromStreams(streams, ActivityTypes.Running, [DataPace.type], { includeDerivedTypes: true, includeUnitVariants: false });
253
+ const result = ActivityUtilities.createUnitStreamsFromStreams(streams, ActivityTypes.Running, [DataPace.type], {
254
+ includeDerivedTypes: true,
255
+ includeUnitVariants: false
256
+ });
249
257
  const paceStream = result.find(s => s.type === DataPace.type);
250
258
  const paceUnitStream = result.find(s => s.type === DataPaceMinutesPerMile.type);
251
259
  expect(paceStream).toBeDefined(); // Derived type should be there
@@ -253,7 +261,10 @@ describe('Activity Utilities', () => {
253
261
  });
254
262
  it('should exclude both derived types and unit variants', () => {
255
263
  const streams = [new Stream(DataSpeed.type, [10, 20])];
256
- const result = ActivityUtilities.createUnitStreamsFromStreams(streams, ActivityTypes.Running, [DataPace.type], { includeDerivedTypes: false, includeUnitVariants: false });
264
+ const result = ActivityUtilities.createUnitStreamsFromStreams(streams, ActivityTypes.Running, [DataPace.type], {
265
+ includeDerivedTypes: false,
266
+ includeUnitVariants: false
267
+ });
257
268
  const paceStream = result.find(s => s.type === DataPace.type);
258
269
  const paceUnitStream = result.find(s => s.type === DataPaceMinutesPerMile.type);
259
270
  expect(paceStream).toBeUndefined();
@@ -261,8 +272,8 @@ describe('Activity Utilities', () => {
261
272
  });
262
273
  });
263
274
  describe('generateMissingStreams', () => {
264
- it('should generate unit streams by default (generateUnitStreams = true)', () => {
265
- 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 }));
266
277
  // Add a speed stream
267
278
  activity.addStream(new Stream(DataSpeed.type, [10, 20]));
268
279
  ActivityUtilities.generateMissingStreams(activity);
@@ -276,7 +287,7 @@ describe('Activity Utilities', () => {
276
287
  activity.parseOptions = {
277
288
  streams: { smooth: {}, fixAbnormal: {} },
278
289
  maxActivityDurationDays: 14,
279
- generateUnitStreams: false,
290
+ generateUnitStreams: false
280
291
  };
281
292
  // Add a speed stream
282
293
  activity.addStream(new Stream(DataSpeed.type, [10, 20]));
@@ -294,24 +305,69 @@ describe('Activity Utilities', () => {
294
305
  activity.parseOptions = {
295
306
  streams: { smooth: {}, fixAbnormal: {} },
296
307
  maxActivityDurationDays: 14,
297
- generateUnitStreams: false, // DISABLE streams
308
+ generateUnitStreams: false // DISABLE streams
298
309
  };
299
310
  // Add a speed stream [10 m/s, 20 m/s]
300
311
  // Max speed = 20 m/s
301
312
  activity.addStream(new Stream(DataSpeed.type, [10, 20]));
302
313
  ActivityUtilities.generateMissingStreamsAndStatsForActivity(activity);
303
- // 1. Verify Streams are missing (as requested)
304
- expect(activity.hasStreamData(DataSpeedKilometersPerHour.type)).toBe(false);
305
- // 1. Verify Streams are missing (as requested)
314
+ // 1. Verify Unit Streams are missing (as requested)
306
315
  expect(activity.hasStreamData(DataSpeedKilometersPerHour.type)).toBe(false);
307
- // 2. Verify Stats are PRESENT (Safety Check)
316
+ // 2. Verify Derived Base Streams are PRESENT (The fix)
317
+ expect(activity.hasStreamData(DataPace.type)).toBe(true);
318
+ // 3. Verify Stats are PRESENT (Safety Check)
308
319
  // 20 m/s = 72 km/h
309
- // We check for the stat by its string type if we can't import the constant easily, or iterate.
310
- // Based on activity.utilities.ts it uses: DataSpeedMaxKilometersPerHour
311
320
  const allStats = Array.from(activity.getStats().values());
312
321
  const speedMaxKmh = allStats.find(s => s.getType() === DataSpeedMaxKilometersPerHour.type);
313
322
  expect(speedMaxKmh).toBeDefined();
314
323
  expect(speedMaxKmh === null || speedMaxKmh === void 0 ? void 0 : speedMaxKmh.getValue()).toBe(72);
315
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
+ });
316
372
  });
317
373
  });
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@sports-alliance/sports-lib",
3
- "version": "6.1.10",
3
+ "version": "6.1.12",
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.1",
58
+ "geolib": "^3.3.4",
59
59
  "gpx-builder": "^3.7.8",
60
60
  "kalmanjs": "^1.1.0",
61
61
  "lowpassf": "^0.5.0",