incyclist-services 1.7.49-beta → 1.7.49
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/lib/cjs/activities/active-rides/service.js +1 -1
- package/lib/cjs/activities/base/convert/converter.js +1 -1
- package/lib/cjs/activities/base/convert/fit/index.js +1 -0
- package/lib/cjs/activities/base/convert/fit/local-fit.js +208 -0
- package/lib/cjs/ride/route/RLVDisplayService.js +1 -1
- package/lib/cjs/routes/base/parsers/utils.js +1 -1
- package/lib/cjs/utils/formatting.js +1 -1
- package/lib/cjs/utils/geo.js +2 -1
- package/lib/cjs/utils/xml.js +1 -1
- package/lib/cjs/workouts/base/parsers/zwo/zwo.js +1 -1
- package/lib/esm/activities/active-rides/service.js +1 -1
- package/lib/esm/activities/base/convert/converter.js +2 -2
- package/lib/esm/activities/base/convert/fit/index.js +1 -0
- package/lib/esm/activities/base/convert/fit/local-fit.js +205 -0
- package/lib/esm/ride/route/RLVDisplayService.js +1 -1
- package/lib/esm/routes/base/parsers/utils.js +1 -1
- package/lib/esm/utils/formatting.js +1 -1
- package/lib/esm/utils/geo.js +2 -1
- package/lib/esm/utils/xml.js +1 -1
- package/lib/esm/workouts/base/parsers/zwo/zwo.js +1 -1
- package/lib/types/activities/base/convert/fit/index.d.ts +1 -0
- package/lib/types/activities/base/convert/fit/local-fit.d.ts +12 -0
- package/package.json +2 -1
|
@@ -392,7 +392,7 @@ let ActiveRidesService = (() => {
|
|
|
392
392
|
return 'Anonymous';
|
|
393
393
|
const names = ['Alex', 'Bart', 'Cosmas', 'Dirk', 'Ernesto', 'Frank', 'Guido', 'Hans', 'Irene', 'John', 'Kai', 'Lorenzo', 'Martin', 'Naijb', 'Oswaldo', 'Pete', 'Quentin', 'Rachel', 'Sophia', 'Trevor', 'Ute', 'Vivian', 'Wil', 'Xaver', 'Younes', 'Zoe'];
|
|
394
394
|
const fnKey = id.charAt(0).toLowerCase();
|
|
395
|
-
const idx = !Number.isNaN(Number.parseInt(fnKey)) ? Number.parseInt(fnKey) : fnKey.
|
|
395
|
+
const idx = !Number.isNaN(Number.parseInt(fnKey)) ? Number.parseInt(fnKey) : (fnKey.codePointAt(0) ?? 0) - 96;
|
|
396
396
|
const lnKey = id.charAt(0).toUpperCase();
|
|
397
397
|
const ln = !Number.isNaN(Number.parseInt(lnKey)) ? '' : lnKey;
|
|
398
398
|
return `${names[idx]}${ln}`;
|
|
@@ -9,7 +9,7 @@ class ActivityConverter {
|
|
|
9
9
|
static async convert(activity, format) {
|
|
10
10
|
if (!ActivityConverter.factory) {
|
|
11
11
|
ActivityConverter.factory = new factory_1.ActivityConverterFactory();
|
|
12
|
-
ActivityConverter.factory.add('fit', new fit_1.
|
|
12
|
+
ActivityConverter.factory.add('fit', new fit_1.LocalFitConverter());
|
|
13
13
|
ActivityConverter.factory.add('tcx', new tcx_1.TcxConverter());
|
|
14
14
|
}
|
|
15
15
|
return await ActivityConverter.factory.convert(activity, format);
|
|
@@ -0,0 +1,208 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __runInitializers = (this && this.__runInitializers) || function (thisArg, initializers, value) {
|
|
3
|
+
var useValue = arguments.length > 2;
|
|
4
|
+
for (var i = 0; i < initializers.length; i++) {
|
|
5
|
+
value = useValue ? initializers[i].call(thisArg, value) : initializers[i].call(thisArg);
|
|
6
|
+
}
|
|
7
|
+
return useValue ? value : void 0;
|
|
8
|
+
};
|
|
9
|
+
var __esDecorate = (this && this.__esDecorate) || function (ctor, descriptorIn, decorators, contextIn, initializers, extraInitializers) {
|
|
10
|
+
function accept(f) { if (f !== void 0 && typeof f !== "function") throw new TypeError("Function expected"); return f; }
|
|
11
|
+
var kind = contextIn.kind, key = kind === "getter" ? "get" : kind === "setter" ? "set" : "value";
|
|
12
|
+
var target = !descriptorIn && ctor ? contextIn["static"] ? ctor : ctor.prototype : null;
|
|
13
|
+
var descriptor = descriptorIn || (target ? Object.getOwnPropertyDescriptor(target, contextIn.name) : {});
|
|
14
|
+
var _, done = false;
|
|
15
|
+
for (var i = decorators.length - 1; i >= 0; i--) {
|
|
16
|
+
var context = {};
|
|
17
|
+
for (var p in contextIn) context[p] = p === "access" ? {} : contextIn[p];
|
|
18
|
+
for (var p in contextIn.access) context.access[p] = contextIn.access[p];
|
|
19
|
+
context.addInitializer = function (f) { if (done) throw new TypeError("Cannot add initializers after decoration has completed"); extraInitializers.push(accept(f || null)); };
|
|
20
|
+
var result = (0, decorators[i])(kind === "accessor" ? { get: descriptor.get, set: descriptor.set } : descriptor[key], context);
|
|
21
|
+
if (kind === "accessor") {
|
|
22
|
+
if (result === void 0) continue;
|
|
23
|
+
if (result === null || typeof result !== "object") throw new TypeError("Object expected");
|
|
24
|
+
if (_ = accept(result.get)) descriptor.get = _;
|
|
25
|
+
if (_ = accept(result.set)) descriptor.set = _;
|
|
26
|
+
if (_ = accept(result.init)) initializers.unshift(_);
|
|
27
|
+
}
|
|
28
|
+
else if (_ = accept(result)) {
|
|
29
|
+
if (kind === "field") initializers.unshift(_);
|
|
30
|
+
else descriptor[key] = _;
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
if (target) Object.defineProperty(target, contextIn.name, descriptor);
|
|
34
|
+
done = true;
|
|
35
|
+
};
|
|
36
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
37
|
+
exports.LocalFitConverter = void 0;
|
|
38
|
+
const fitsdk_1 = require("@garmin/fitsdk");
|
|
39
|
+
const gd_eventlog_1 = require("gd-eventlog");
|
|
40
|
+
const settings_1 = require("../../../../settings");
|
|
41
|
+
const decorators_1 = require("../../../../base/decorators");
|
|
42
|
+
const DEG_TO_SEMICIRCLES = (2 ** 31) / 180;
|
|
43
|
+
let LocalFitConverter = (() => {
|
|
44
|
+
let _instanceExtraInitializers = [];
|
|
45
|
+
let _getUserSettings_decorators;
|
|
46
|
+
return class LocalFitConverter {
|
|
47
|
+
static {
|
|
48
|
+
const _metadata = typeof Symbol === "function" && Symbol.metadata ? Object.create(null) : void 0;
|
|
49
|
+
_getUserSettings_decorators = [decorators_1.Injectable];
|
|
50
|
+
__esDecorate(this, null, _getUserSettings_decorators, { kind: "method", name: "getUserSettings", static: false, private: false, access: { has: obj => "getUserSettings" in obj, get: obj => obj.getUserSettings }, metadata: _metadata }, null, _instanceExtraInitializers);
|
|
51
|
+
if (_metadata) Object.defineProperty(this, Symbol.metadata, { enumerable: true, configurable: true, writable: true, value: _metadata });
|
|
52
|
+
}
|
|
53
|
+
logger = __runInitializers(this, _instanceExtraInitializers);
|
|
54
|
+
constructor() {
|
|
55
|
+
this.logger = new gd_eventlog_1.EventLogger('LocalFitExporter');
|
|
56
|
+
}
|
|
57
|
+
async convert(activity) {
|
|
58
|
+
try {
|
|
59
|
+
this.logger.logEvent({ message: 'convert start', format: 'FIT' });
|
|
60
|
+
const fitActivity = this.getFitActivity(activity);
|
|
61
|
+
const data = this.encode(fitActivity);
|
|
62
|
+
this.logger.logEvent({ message: 'convert success', format: 'FIT' });
|
|
63
|
+
return data;
|
|
64
|
+
}
|
|
65
|
+
catch (err) {
|
|
66
|
+
this.logger.logEvent({ message: 'convert result', format: 'FIT', result: 'error', reason: err.message });
|
|
67
|
+
throw err;
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
getFitActivity(activity) {
|
|
71
|
+
const { id, title, time, timeTotal, timePause, distance } = activity;
|
|
72
|
+
const status = 'created';
|
|
73
|
+
const startTime = new Date(activity.startTime).toISOString();
|
|
74
|
+
const logs = activity.logs.map(this.mapLogToFit.bind(this));
|
|
75
|
+
const screenshots = [];
|
|
76
|
+
const laps = this.mapLapsToFit(activity.laps ?? [], activity.startTime);
|
|
77
|
+
const user = {
|
|
78
|
+
id: this.getUserSettings().get('uuid', undefined),
|
|
79
|
+
weight: activity.user.weight
|
|
80
|
+
};
|
|
81
|
+
return { id, title, status, logs, laps, startTime, time, timeTotal, timePause, distance, user, screenshots };
|
|
82
|
+
}
|
|
83
|
+
mapLapsToFit(laps, activityStartTime) {
|
|
84
|
+
const activityStartMs = new Date(activityStartTime).getTime();
|
|
85
|
+
return laps.map((lap, i) => {
|
|
86
|
+
const prevDistance = i > 0 ? laps[i - 1].distance : 0;
|
|
87
|
+
return {
|
|
88
|
+
lapNo: lap.num,
|
|
89
|
+
startTime: new Date(lap.startTime).toISOString(),
|
|
90
|
+
stopTime: new Date(lap.startTime + lap.rideTime * 1000).toISOString(),
|
|
91
|
+
lapDistance: lap.distance - prevDistance,
|
|
92
|
+
totalDistance: lap.distance,
|
|
93
|
+
lapTime: lap.rideTime,
|
|
94
|
+
totalTime: (lap.startTime - activityStartMs) / 1000 + lap.rideTime,
|
|
95
|
+
};
|
|
96
|
+
});
|
|
97
|
+
}
|
|
98
|
+
mapLogToFit(log) {
|
|
99
|
+
const { time, speed, slope, cadence: cadenceOrg, heartrate: heartrateOrg, distance, power: powerOrg, lat, lng, elevation } = log;
|
|
100
|
+
const cadence = Math.round(cadenceOrg);
|
|
101
|
+
const heartrate = Math.round(heartrateOrg);
|
|
102
|
+
const power = Math.round(powerOrg);
|
|
103
|
+
return { time, speed, slope, cadence, heartrate, distance, power, lat, lon: lng, elevation };
|
|
104
|
+
}
|
|
105
|
+
encode(activity) {
|
|
106
|
+
const encoder = new fitsdk_1.Encoder();
|
|
107
|
+
const startTime = new Date(activity.startTime);
|
|
108
|
+
encoder.onMesg(fitsdk_1.Profile.MesgNum.FILE_ID, {
|
|
109
|
+
type: 'activity',
|
|
110
|
+
manufacturer: 'development',
|
|
111
|
+
product: 0,
|
|
112
|
+
timeCreated: startTime,
|
|
113
|
+
});
|
|
114
|
+
encoder.onMesg(fitsdk_1.Profile.MesgNum.EVENT, {
|
|
115
|
+
timestamp: startTime,
|
|
116
|
+
event: 'timer',
|
|
117
|
+
eventType: 'start',
|
|
118
|
+
data: 0,
|
|
119
|
+
});
|
|
120
|
+
let lastTimestampMs = startTime.getTime();
|
|
121
|
+
activity.logs.forEach((log) => {
|
|
122
|
+
if (log.time != null) {
|
|
123
|
+
lastTimestampMs = startTime.getTime() + log.time * 1000;
|
|
124
|
+
}
|
|
125
|
+
const record = {
|
|
126
|
+
timestamp: new Date(lastTimestampMs),
|
|
127
|
+
};
|
|
128
|
+
if (log.lat != null && log.lon != null) {
|
|
129
|
+
record.positionLat = Math.round(log.lat * DEG_TO_SEMICIRCLES);
|
|
130
|
+
record.positionLong = Math.round(log.lon * DEG_TO_SEMICIRCLES);
|
|
131
|
+
}
|
|
132
|
+
if (log.elevation != null)
|
|
133
|
+
record.altitude = log.elevation;
|
|
134
|
+
if (log.heartrate != null)
|
|
135
|
+
record.heartRate = log.heartrate;
|
|
136
|
+
if (log.cadence != null)
|
|
137
|
+
record.cadence = log.cadence;
|
|
138
|
+
if (log.distance != null)
|
|
139
|
+
record.distance = log.distance;
|
|
140
|
+
if (log.speed != null)
|
|
141
|
+
record.speed = log.speed / 3.6;
|
|
142
|
+
if (log.power != null)
|
|
143
|
+
record.power = log.power;
|
|
144
|
+
if (log.slope != null)
|
|
145
|
+
record.grade = log.slope;
|
|
146
|
+
encoder.onMesg(fitsdk_1.Profile.MesgNum.RECORD, record);
|
|
147
|
+
});
|
|
148
|
+
const endTime = new Date(lastTimestampMs);
|
|
149
|
+
encoder.onMesg(fitsdk_1.Profile.MesgNum.EVENT, {
|
|
150
|
+
timestamp: endTime,
|
|
151
|
+
event: 'timer',
|
|
152
|
+
eventType: 'stopAll',
|
|
153
|
+
data: 0,
|
|
154
|
+
});
|
|
155
|
+
if (activity.laps.length > 0) {
|
|
156
|
+
activity.laps.forEach(lap => {
|
|
157
|
+
const lapStart = new Date(lap.startTime);
|
|
158
|
+
const lapEnd = new Date(lap.stopTime);
|
|
159
|
+
encoder.onMesg(fitsdk_1.Profile.MesgNum.LAP, {
|
|
160
|
+
timestamp: lapEnd,
|
|
161
|
+
startTime: lapStart,
|
|
162
|
+
totalElapsedTime: lap.lapTime,
|
|
163
|
+
totalTimerTime: lap.lapTime,
|
|
164
|
+
totalDistance: lap.lapDistance,
|
|
165
|
+
event: 'lap',
|
|
166
|
+
eventType: 'stop',
|
|
167
|
+
});
|
|
168
|
+
});
|
|
169
|
+
}
|
|
170
|
+
else {
|
|
171
|
+
encoder.onMesg(fitsdk_1.Profile.MesgNum.LAP, {
|
|
172
|
+
timestamp: endTime,
|
|
173
|
+
startTime,
|
|
174
|
+
totalElapsedTime: activity.timeTotal,
|
|
175
|
+
totalTimerTime: activity.time,
|
|
176
|
+
totalDistance: activity.distance,
|
|
177
|
+
event: 'lap',
|
|
178
|
+
eventType: 'stop',
|
|
179
|
+
});
|
|
180
|
+
}
|
|
181
|
+
encoder.onMesg(fitsdk_1.Profile.MesgNum.SESSION, {
|
|
182
|
+
timestamp: endTime,
|
|
183
|
+
startTime,
|
|
184
|
+
totalElapsedTime: activity.timeTotal,
|
|
185
|
+
totalTimerTime: activity.time,
|
|
186
|
+
totalDistance: activity.distance,
|
|
187
|
+
sport: 'cycling',
|
|
188
|
+
subSport: 'virtualActivity',
|
|
189
|
+
event: 'session',
|
|
190
|
+
eventType: 'stopDisableAll',
|
|
191
|
+
});
|
|
192
|
+
encoder.onMesg(fitsdk_1.Profile.MesgNum.ACTIVITY, {
|
|
193
|
+
timestamp: endTime,
|
|
194
|
+
totalTimerTime: activity.time,
|
|
195
|
+
numSessions: 1,
|
|
196
|
+
type: 'manual',
|
|
197
|
+
event: 'activity',
|
|
198
|
+
eventType: 'stop',
|
|
199
|
+
});
|
|
200
|
+
const uint8Array = encoder.close();
|
|
201
|
+
return uint8Array.buffer;
|
|
202
|
+
}
|
|
203
|
+
getUserSettings() {
|
|
204
|
+
return (0, settings_1.useUserSettings)();
|
|
205
|
+
}
|
|
206
|
+
};
|
|
207
|
+
})();
|
|
208
|
+
exports.LocalFitConverter = LocalFitConverter;
|
|
@@ -6,7 +6,7 @@ const formatDateTime = (date, fstr = '%Y%m%d%H%M%S', utc = false) => {
|
|
|
6
6
|
if (!(0, valid_1.valid)(date) || !(date instanceof Date))
|
|
7
7
|
return;
|
|
8
8
|
const prefix = utc ? 'getUTC' : 'get';
|
|
9
|
-
return fstr.
|
|
9
|
+
return fstr.replaceAll(/%[YmdHMS]/g, (m) => {
|
|
10
10
|
switch (m) {
|
|
11
11
|
case '%Y': return date[prefix + 'FullYear']();
|
|
12
12
|
case '%m':
|
package/lib/cjs/utils/geo.js
CHANGED
|
@@ -17,7 +17,8 @@ const calculateDistance = (lat1, lon1, lat2, lon2, radius = rEarth * 1000) => {
|
|
|
17
17
|
return distance;
|
|
18
18
|
};
|
|
19
19
|
exports.calculateDistance = calculateDistance;
|
|
20
|
-
const
|
|
20
|
+
const DEFAULT_DISTANCE_PROPS = { abs: true, latLng: true };
|
|
21
|
+
const distanceBetween = (p1, p2, props = DEFAULT_DISTANCE_PROPS) => {
|
|
21
22
|
if (p1 === undefined)
|
|
22
23
|
return 0;
|
|
23
24
|
let dist;
|
package/lib/cjs/utils/xml.js
CHANGED
|
@@ -17,7 +17,7 @@ const removeUTFBom = (str) => {
|
|
|
17
17
|
if (!str || typeof (str) !== 'string') {
|
|
18
18
|
return str;
|
|
19
19
|
}
|
|
20
|
-
while (str.
|
|
20
|
+
while (str.codePointAt(0) === 0 || str.codePointAt(0) === 0xFEFF || str.codePointAt(0) === 0xFFFD)
|
|
21
21
|
str = str.substring(1);
|
|
22
22
|
return str;
|
|
23
23
|
};
|
|
@@ -216,7 +216,7 @@ class ZwoParser {
|
|
|
216
216
|
let tag;
|
|
217
217
|
parser.parseString(data, (err, result) => {
|
|
218
218
|
if (err) {
|
|
219
|
-
err.message = 'File contains error(s): ' + err.message.
|
|
219
|
+
err.message = 'File contains error(s): ' + err.message.replaceAll('\n', ' ');
|
|
220
220
|
this.logger.logEvent({ message: 'error', fn: 'parse()', error: err.message, stack: err.stack });
|
|
221
221
|
return reject(err);
|
|
222
222
|
}
|
|
@@ -389,7 +389,7 @@ let ActiveRidesService = (() => {
|
|
|
389
389
|
return 'Anonymous';
|
|
390
390
|
const names = ['Alex', 'Bart', 'Cosmas', 'Dirk', 'Ernesto', 'Frank', 'Guido', 'Hans', 'Irene', 'John', 'Kai', 'Lorenzo', 'Martin', 'Naijb', 'Oswaldo', 'Pete', 'Quentin', 'Rachel', 'Sophia', 'Trevor', 'Ute', 'Vivian', 'Wil', 'Xaver', 'Younes', 'Zoe'];
|
|
391
391
|
const fnKey = id.charAt(0).toLowerCase();
|
|
392
|
-
const idx = !Number.isNaN(Number.parseInt(fnKey)) ? Number.parseInt(fnKey) : fnKey.
|
|
392
|
+
const idx = !Number.isNaN(Number.parseInt(fnKey)) ? Number.parseInt(fnKey) : (fnKey.codePointAt(0) ?? 0) - 96;
|
|
393
393
|
const lnKey = id.charAt(0).toUpperCase();
|
|
394
394
|
const ln = !Number.isNaN(Number.parseInt(lnKey)) ? '' : lnKey;
|
|
395
395
|
return `${names[idx]}${ln}`;
|
|
@@ -1,12 +1,12 @@
|
|
|
1
1
|
import { ActivityConverterFactory } from "./factory";
|
|
2
|
-
import {
|
|
2
|
+
import { LocalFitConverter } from "./fit";
|
|
3
3
|
import { TcxConverter } from "./tcx";
|
|
4
4
|
export class ActivityConverter {
|
|
5
5
|
static factory;
|
|
6
6
|
static async convert(activity, format) {
|
|
7
7
|
if (!ActivityConverter.factory) {
|
|
8
8
|
ActivityConverter.factory = new ActivityConverterFactory();
|
|
9
|
-
ActivityConverter.factory.add('fit', new
|
|
9
|
+
ActivityConverter.factory.add('fit', new LocalFitConverter());
|
|
10
10
|
ActivityConverter.factory.add('tcx', new TcxConverter());
|
|
11
11
|
}
|
|
12
12
|
return await ActivityConverter.factory.convert(activity, format);
|
|
@@ -0,0 +1,205 @@
|
|
|
1
|
+
var __runInitializers = (this && this.__runInitializers) || function (thisArg, initializers, value) {
|
|
2
|
+
var useValue = arguments.length > 2;
|
|
3
|
+
for (var i = 0; i < initializers.length; i++) {
|
|
4
|
+
value = useValue ? initializers[i].call(thisArg, value) : initializers[i].call(thisArg);
|
|
5
|
+
}
|
|
6
|
+
return useValue ? value : void 0;
|
|
7
|
+
};
|
|
8
|
+
var __esDecorate = (this && this.__esDecorate) || function (ctor, descriptorIn, decorators, contextIn, initializers, extraInitializers) {
|
|
9
|
+
function accept(f) { if (f !== void 0 && typeof f !== "function") throw new TypeError("Function expected"); return f; }
|
|
10
|
+
var kind = contextIn.kind, key = kind === "getter" ? "get" : kind === "setter" ? "set" : "value";
|
|
11
|
+
var target = !descriptorIn && ctor ? contextIn["static"] ? ctor : ctor.prototype : null;
|
|
12
|
+
var descriptor = descriptorIn || (target ? Object.getOwnPropertyDescriptor(target, contextIn.name) : {});
|
|
13
|
+
var _, done = false;
|
|
14
|
+
for (var i = decorators.length - 1; i >= 0; i--) {
|
|
15
|
+
var context = {};
|
|
16
|
+
for (var p in contextIn) context[p] = p === "access" ? {} : contextIn[p];
|
|
17
|
+
for (var p in contextIn.access) context.access[p] = contextIn.access[p];
|
|
18
|
+
context.addInitializer = function (f) { if (done) throw new TypeError("Cannot add initializers after decoration has completed"); extraInitializers.push(accept(f || null)); };
|
|
19
|
+
var result = (0, decorators[i])(kind === "accessor" ? { get: descriptor.get, set: descriptor.set } : descriptor[key], context);
|
|
20
|
+
if (kind === "accessor") {
|
|
21
|
+
if (result === void 0) continue;
|
|
22
|
+
if (result === null || typeof result !== "object") throw new TypeError("Object expected");
|
|
23
|
+
if (_ = accept(result.get)) descriptor.get = _;
|
|
24
|
+
if (_ = accept(result.set)) descriptor.set = _;
|
|
25
|
+
if (_ = accept(result.init)) initializers.unshift(_);
|
|
26
|
+
}
|
|
27
|
+
else if (_ = accept(result)) {
|
|
28
|
+
if (kind === "field") initializers.unshift(_);
|
|
29
|
+
else descriptor[key] = _;
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
if (target) Object.defineProperty(target, contextIn.name, descriptor);
|
|
33
|
+
done = true;
|
|
34
|
+
};
|
|
35
|
+
import { Encoder, Profile } from '@garmin/fitsdk';
|
|
36
|
+
import { EventLogger } from 'gd-eventlog';
|
|
37
|
+
import { useUserSettings } from '../../../../settings';
|
|
38
|
+
import { Injectable } from '../../../../base/decorators';
|
|
39
|
+
const DEG_TO_SEMICIRCLES = (2 ** 31) / 180;
|
|
40
|
+
let LocalFitConverter = (() => {
|
|
41
|
+
let _instanceExtraInitializers = [];
|
|
42
|
+
let _getUserSettings_decorators;
|
|
43
|
+
return class LocalFitConverter {
|
|
44
|
+
static {
|
|
45
|
+
const _metadata = typeof Symbol === "function" && Symbol.metadata ? Object.create(null) : void 0;
|
|
46
|
+
_getUserSettings_decorators = [Injectable];
|
|
47
|
+
__esDecorate(this, null, _getUserSettings_decorators, { kind: "method", name: "getUserSettings", static: false, private: false, access: { has: obj => "getUserSettings" in obj, get: obj => obj.getUserSettings }, metadata: _metadata }, null, _instanceExtraInitializers);
|
|
48
|
+
if (_metadata) Object.defineProperty(this, Symbol.metadata, { enumerable: true, configurable: true, writable: true, value: _metadata });
|
|
49
|
+
}
|
|
50
|
+
logger = __runInitializers(this, _instanceExtraInitializers);
|
|
51
|
+
constructor() {
|
|
52
|
+
this.logger = new EventLogger('LocalFitExporter');
|
|
53
|
+
}
|
|
54
|
+
async convert(activity) {
|
|
55
|
+
try {
|
|
56
|
+
this.logger.logEvent({ message: 'convert start', format: 'FIT' });
|
|
57
|
+
const fitActivity = this.getFitActivity(activity);
|
|
58
|
+
const data = this.encode(fitActivity);
|
|
59
|
+
this.logger.logEvent({ message: 'convert success', format: 'FIT' });
|
|
60
|
+
return data;
|
|
61
|
+
}
|
|
62
|
+
catch (err) {
|
|
63
|
+
this.logger.logEvent({ message: 'convert result', format: 'FIT', result: 'error', reason: err.message });
|
|
64
|
+
throw err;
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
getFitActivity(activity) {
|
|
68
|
+
const { id, title, time, timeTotal, timePause, distance } = activity;
|
|
69
|
+
const status = 'created';
|
|
70
|
+
const startTime = new Date(activity.startTime).toISOString();
|
|
71
|
+
const logs = activity.logs.map(this.mapLogToFit.bind(this));
|
|
72
|
+
const screenshots = [];
|
|
73
|
+
const laps = this.mapLapsToFit(activity.laps ?? [], activity.startTime);
|
|
74
|
+
const user = {
|
|
75
|
+
id: this.getUserSettings().get('uuid', undefined),
|
|
76
|
+
weight: activity.user.weight
|
|
77
|
+
};
|
|
78
|
+
return { id, title, status, logs, laps, startTime, time, timeTotal, timePause, distance, user, screenshots };
|
|
79
|
+
}
|
|
80
|
+
mapLapsToFit(laps, activityStartTime) {
|
|
81
|
+
const activityStartMs = new Date(activityStartTime).getTime();
|
|
82
|
+
return laps.map((lap, i) => {
|
|
83
|
+
const prevDistance = i > 0 ? laps[i - 1].distance : 0;
|
|
84
|
+
return {
|
|
85
|
+
lapNo: lap.num,
|
|
86
|
+
startTime: new Date(lap.startTime).toISOString(),
|
|
87
|
+
stopTime: new Date(lap.startTime + lap.rideTime * 1000).toISOString(),
|
|
88
|
+
lapDistance: lap.distance - prevDistance,
|
|
89
|
+
totalDistance: lap.distance,
|
|
90
|
+
lapTime: lap.rideTime,
|
|
91
|
+
totalTime: (lap.startTime - activityStartMs) / 1000 + lap.rideTime,
|
|
92
|
+
};
|
|
93
|
+
});
|
|
94
|
+
}
|
|
95
|
+
mapLogToFit(log) {
|
|
96
|
+
const { time, speed, slope, cadence: cadenceOrg, heartrate: heartrateOrg, distance, power: powerOrg, lat, lng, elevation } = log;
|
|
97
|
+
const cadence = Math.round(cadenceOrg);
|
|
98
|
+
const heartrate = Math.round(heartrateOrg);
|
|
99
|
+
const power = Math.round(powerOrg);
|
|
100
|
+
return { time, speed, slope, cadence, heartrate, distance, power, lat, lon: lng, elevation };
|
|
101
|
+
}
|
|
102
|
+
encode(activity) {
|
|
103
|
+
const encoder = new Encoder();
|
|
104
|
+
const startTime = new Date(activity.startTime);
|
|
105
|
+
encoder.onMesg(Profile.MesgNum.FILE_ID, {
|
|
106
|
+
type: 'activity',
|
|
107
|
+
manufacturer: 'development',
|
|
108
|
+
product: 0,
|
|
109
|
+
timeCreated: startTime,
|
|
110
|
+
});
|
|
111
|
+
encoder.onMesg(Profile.MesgNum.EVENT, {
|
|
112
|
+
timestamp: startTime,
|
|
113
|
+
event: 'timer',
|
|
114
|
+
eventType: 'start',
|
|
115
|
+
data: 0,
|
|
116
|
+
});
|
|
117
|
+
let lastTimestampMs = startTime.getTime();
|
|
118
|
+
activity.logs.forEach((log) => {
|
|
119
|
+
if (log.time != null) {
|
|
120
|
+
lastTimestampMs = startTime.getTime() + log.time * 1000;
|
|
121
|
+
}
|
|
122
|
+
const record = {
|
|
123
|
+
timestamp: new Date(lastTimestampMs),
|
|
124
|
+
};
|
|
125
|
+
if (log.lat != null && log.lon != null) {
|
|
126
|
+
record.positionLat = Math.round(log.lat * DEG_TO_SEMICIRCLES);
|
|
127
|
+
record.positionLong = Math.round(log.lon * DEG_TO_SEMICIRCLES);
|
|
128
|
+
}
|
|
129
|
+
if (log.elevation != null)
|
|
130
|
+
record.altitude = log.elevation;
|
|
131
|
+
if (log.heartrate != null)
|
|
132
|
+
record.heartRate = log.heartrate;
|
|
133
|
+
if (log.cadence != null)
|
|
134
|
+
record.cadence = log.cadence;
|
|
135
|
+
if (log.distance != null)
|
|
136
|
+
record.distance = log.distance;
|
|
137
|
+
if (log.speed != null)
|
|
138
|
+
record.speed = log.speed / 3.6;
|
|
139
|
+
if (log.power != null)
|
|
140
|
+
record.power = log.power;
|
|
141
|
+
if (log.slope != null)
|
|
142
|
+
record.grade = log.slope;
|
|
143
|
+
encoder.onMesg(Profile.MesgNum.RECORD, record);
|
|
144
|
+
});
|
|
145
|
+
const endTime = new Date(lastTimestampMs);
|
|
146
|
+
encoder.onMesg(Profile.MesgNum.EVENT, {
|
|
147
|
+
timestamp: endTime,
|
|
148
|
+
event: 'timer',
|
|
149
|
+
eventType: 'stopAll',
|
|
150
|
+
data: 0,
|
|
151
|
+
});
|
|
152
|
+
if (activity.laps.length > 0) {
|
|
153
|
+
activity.laps.forEach(lap => {
|
|
154
|
+
const lapStart = new Date(lap.startTime);
|
|
155
|
+
const lapEnd = new Date(lap.stopTime);
|
|
156
|
+
encoder.onMesg(Profile.MesgNum.LAP, {
|
|
157
|
+
timestamp: lapEnd,
|
|
158
|
+
startTime: lapStart,
|
|
159
|
+
totalElapsedTime: lap.lapTime,
|
|
160
|
+
totalTimerTime: lap.lapTime,
|
|
161
|
+
totalDistance: lap.lapDistance,
|
|
162
|
+
event: 'lap',
|
|
163
|
+
eventType: 'stop',
|
|
164
|
+
});
|
|
165
|
+
});
|
|
166
|
+
}
|
|
167
|
+
else {
|
|
168
|
+
encoder.onMesg(Profile.MesgNum.LAP, {
|
|
169
|
+
timestamp: endTime,
|
|
170
|
+
startTime,
|
|
171
|
+
totalElapsedTime: activity.timeTotal,
|
|
172
|
+
totalTimerTime: activity.time,
|
|
173
|
+
totalDistance: activity.distance,
|
|
174
|
+
event: 'lap',
|
|
175
|
+
eventType: 'stop',
|
|
176
|
+
});
|
|
177
|
+
}
|
|
178
|
+
encoder.onMesg(Profile.MesgNum.SESSION, {
|
|
179
|
+
timestamp: endTime,
|
|
180
|
+
startTime,
|
|
181
|
+
totalElapsedTime: activity.timeTotal,
|
|
182
|
+
totalTimerTime: activity.time,
|
|
183
|
+
totalDistance: activity.distance,
|
|
184
|
+
sport: 'cycling',
|
|
185
|
+
subSport: 'virtualActivity',
|
|
186
|
+
event: 'session',
|
|
187
|
+
eventType: 'stopDisableAll',
|
|
188
|
+
});
|
|
189
|
+
encoder.onMesg(Profile.MesgNum.ACTIVITY, {
|
|
190
|
+
timestamp: endTime,
|
|
191
|
+
totalTimerTime: activity.time,
|
|
192
|
+
numSessions: 1,
|
|
193
|
+
type: 'manual',
|
|
194
|
+
event: 'activity',
|
|
195
|
+
eventType: 'stop',
|
|
196
|
+
});
|
|
197
|
+
const uint8Array = encoder.close();
|
|
198
|
+
return uint8Array.buffer;
|
|
199
|
+
}
|
|
200
|
+
getUserSettings() {
|
|
201
|
+
return useUserSettings();
|
|
202
|
+
}
|
|
203
|
+
};
|
|
204
|
+
})();
|
|
205
|
+
export { LocalFitConverter };
|
|
@@ -3,7 +3,7 @@ export const formatDateTime = (date, fstr = '%Y%m%d%H%M%S', utc = false) => {
|
|
|
3
3
|
if (!valid(date) || !(date instanceof Date))
|
|
4
4
|
return;
|
|
5
5
|
const prefix = utc ? 'getUTC' : 'get';
|
|
6
|
-
return fstr.
|
|
6
|
+
return fstr.replaceAll(/%[YmdHMS]/g, (m) => {
|
|
7
7
|
switch (m) {
|
|
8
8
|
case '%Y': return date[prefix + 'FullYear']();
|
|
9
9
|
case '%m':
|
package/lib/esm/utils/geo.js
CHANGED
|
@@ -12,7 +12,8 @@ export const calculateDistance = (lat1, lon1, lat2, lon2, radius = rEarth * 1000
|
|
|
12
12
|
const distance = abs(R * c);
|
|
13
13
|
return distance;
|
|
14
14
|
};
|
|
15
|
-
|
|
15
|
+
const DEFAULT_DISTANCE_PROPS = { abs: true, latLng: true };
|
|
16
|
+
export const distanceBetween = (p1, p2, props = DEFAULT_DISTANCE_PROPS) => {
|
|
16
17
|
if (p1 === undefined)
|
|
17
18
|
return 0;
|
|
18
19
|
let dist;
|
package/lib/esm/utils/xml.js
CHANGED
|
@@ -13,7 +13,7 @@ export const removeUTFBom = (str) => {
|
|
|
13
13
|
if (!str || typeof (str) !== 'string') {
|
|
14
14
|
return str;
|
|
15
15
|
}
|
|
16
|
-
while (str.
|
|
16
|
+
while (str.codePointAt(0) === 0 || str.codePointAt(0) === 0xFEFF || str.codePointAt(0) === 0xFFFD)
|
|
17
17
|
str = str.substring(1);
|
|
18
18
|
return str;
|
|
19
19
|
};
|
|
@@ -210,7 +210,7 @@ export class ZwoParser {
|
|
|
210
210
|
let tag;
|
|
211
211
|
parser.parseString(data, (err, result) => {
|
|
212
212
|
if (err) {
|
|
213
|
-
err.message = 'File contains error(s): ' + err.message.
|
|
213
|
+
err.message = 'File contains error(s): ' + err.message.replaceAll('\n', ' ');
|
|
214
214
|
this.logger.logEvent({ message: 'error', fn: 'parse()', error: err.message, stack: err.stack });
|
|
215
215
|
return reject(err);
|
|
216
216
|
}
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import { EventLogger } from 'gd-eventlog';
|
|
2
|
+
import { ActivityDetails, ActivityLogRecord, FitExportActivity, FitLapEntry, FitLogEntry, LapSummary } from '../../model';
|
|
3
|
+
export declare class LocalFitConverter {
|
|
4
|
+
protected logger: EventLogger;
|
|
5
|
+
constructor();
|
|
6
|
+
convert(activity: ActivityDetails): Promise<ArrayBuffer>;
|
|
7
|
+
protected getFitActivity(activity: ActivityDetails): FitExportActivity;
|
|
8
|
+
protected mapLapsToFit(laps: LapSummary[], activityStartTime: string): FitLapEntry[];
|
|
9
|
+
protected mapLogToFit(log: ActivityLogRecord): FitLogEntry;
|
|
10
|
+
protected encode(activity: FitExportActivity): ArrayBuffer;
|
|
11
|
+
protected getUserSettings(): import("../../../../settings").UserSettingsService;
|
|
12
|
+
}
|
package/package.json
CHANGED
|
@@ -1,10 +1,11 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "incyclist-services",
|
|
3
|
-
"version": "1.7.49
|
|
3
|
+
"version": "1.7.49",
|
|
4
4
|
"peerDependencies": {
|
|
5
5
|
"gd-eventlog": "^0.1.27"
|
|
6
6
|
},
|
|
7
7
|
"dependencies": {
|
|
8
|
+
"@garmin/fitsdk": "^21.200.0",
|
|
8
9
|
"axios": "^1.15.0",
|
|
9
10
|
"incyclist-devices": "^3.0.12",
|
|
10
11
|
"promise.any": "^2.0.6",
|