incyclist-services 1.7.131 → 1.7.133
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/base/repo/db.js +7 -0
- package/lib/cjs/activities/base/utils/activity.js +2 -1
- package/lib/cjs/activities/base/utils/helpers.js +2 -2
- package/lib/cjs/activities/ride/service.js +5 -3
- package/lib/cjs/devices/page/statemachine.js +3 -2
- package/lib/cjs/ride/display/service.js +20 -1
- package/lib/cjs/ride/route/RLVDisplayService.js +2 -2
- package/lib/cjs/ride/route/RouteDisplayService.js +2 -2
- package/lib/cjs/routes/base/parsers/gpx.js +1 -0
- package/lib/cjs/routes/base/parsers/incyclist.js +12 -3
- package/lib/cjs/routes/base/utils/index.js +1 -0
- package/lib/cjs/routes/base/utils/smoothing.js +332 -0
- package/lib/cjs/routes/list/cards/RouteCard.js +64 -4
- package/lib/cjs/routes/list/service.js +85 -1
- package/lib/cjs/workouts/ride/service.js +19 -1
- package/lib/esm/activities/base/repo/db.js +7 -0
- package/lib/esm/activities/base/utils/activity.js +2 -1
- package/lib/esm/activities/base/utils/helpers.js +2 -2
- package/lib/esm/activities/ride/service.js +5 -3
- package/lib/esm/devices/page/statemachine.js +3 -2
- package/lib/esm/ride/display/service.js +20 -1
- package/lib/esm/ride/route/RLVDisplayService.js +2 -2
- package/lib/esm/ride/route/RouteDisplayService.js +2 -2
- package/lib/esm/routes/base/parsers/gpx.js +1 -0
- package/lib/esm/routes/base/parsers/incyclist.js +12 -3
- package/lib/esm/routes/base/utils/index.js +1 -0
- package/lib/esm/routes/base/utils/smoothing.js +322 -0
- package/lib/esm/routes/list/cards/RouteCard.js +64 -4
- package/lib/esm/routes/list/service.js +85 -1
- package/lib/esm/workouts/ride/service.js +19 -1
- package/lib/types/activities/base/model/index.d.ts +2 -0
- package/lib/types/activities/base/repo/db.d.ts +1 -0
- package/lib/types/activities/base/repo/types.d.ts +1 -0
- package/lib/types/activities/ride/service.d.ts +1 -1
- package/lib/types/ride/base/types.d.ts +1 -0
- package/lib/types/ride/display/service.d.ts +2 -0
- package/lib/types/routes/base/api/types.d.ts +1 -0
- package/lib/types/routes/base/parsers/incyclist.d.ts +3 -2
- package/lib/types/routes/base/types/index.d.ts +1 -0
- package/lib/types/routes/base/utils/index.d.ts +1 -0
- package/lib/types/routes/base/utils/smoothing.d.ts +26 -0
- package/lib/types/routes/list/cards/RouteCard.d.ts +12 -1
- package/lib/types/routes/list/cards/types.d.ts +13 -1
- package/lib/types/routes/list/service.d.ts +16 -0
- package/lib/types/workouts/ride/service.d.ts +2 -0
- package/lib/types/workouts/ride/types.d.ts +1 -0
- package/package.json +2 -2
|
@@ -240,6 +240,7 @@ let ActivitiesRepository = (() => {
|
|
|
240
240
|
result = this.checkStartPosFilter(result, criteria);
|
|
241
241
|
result = this.checkEndPosFilter(result, criteria);
|
|
242
242
|
result = this.checkRealityFactorFilter(result, criteria);
|
|
243
|
+
result = this.checkSmoothingFilter(result, criteria);
|
|
243
244
|
result = this.checkIsSavedFilter(result, criteria);
|
|
244
245
|
result = this.checkUploadStatusFilter(result, criteria);
|
|
245
246
|
result = this.checkTimeFilter(result, criteria);
|
|
@@ -287,6 +288,12 @@ let ActivitiesRepository = (() => {
|
|
|
287
288
|
}
|
|
288
289
|
return result;
|
|
289
290
|
}
|
|
291
|
+
checkSmoothingFilter(result, criteria) {
|
|
292
|
+
if (result?.length > 0 && criteria?.smoothingLevel !== undefined) {
|
|
293
|
+
result = result.filter(ai => (ai.summary.smoothingLevel ?? 0) === criteria.smoothingLevel);
|
|
294
|
+
}
|
|
295
|
+
return result;
|
|
296
|
+
}
|
|
290
297
|
checkIsSavedFilter(result, criteria) {
|
|
291
298
|
if (result?.length > 0 && criteria?.isSaved !== undefined) {
|
|
292
299
|
result = result.filter(ai => ai.summary.isSaved === criteria.isSaved);
|
|
@@ -291,13 +291,14 @@ let Activity = (() => {
|
|
|
291
291
|
}
|
|
292
292
|
createStartSettings() {
|
|
293
293
|
try {
|
|
294
|
-
const { startPos, realityFactor, segment, endPos } = this.details;
|
|
294
|
+
const { startPos, realityFactor, segment, endPos, smoothingLevel } = this.details;
|
|
295
295
|
const card = this.getRouteCard();
|
|
296
296
|
const settings = {
|
|
297
297
|
startPos,
|
|
298
298
|
endPos,
|
|
299
299
|
segment,
|
|
300
300
|
realityFactor,
|
|
301
|
+
smoothingLevel,
|
|
301
302
|
showPrev: true,
|
|
302
303
|
type: card?.getCardType() ?? 'Route'
|
|
303
304
|
};
|
|
@@ -18,7 +18,7 @@ exports.getActivityTotalElevation = exports.buildSummary = void 0;
|
|
|
18
18
|
__exportStar(require("./activity"), exports);
|
|
19
19
|
const model_1 = require("../model");
|
|
20
20
|
const buildSummary = (activity, proposedName) => {
|
|
21
|
-
const { id, route, screenshots, startTime: startTimeUTC, time: rideTime, distance, startPos, endPos, realityFactor = 100, links, laps, fileName } = activity;
|
|
21
|
+
const { id, route, screenshots, startTime: startTimeUTC, time: rideTime, distance, startPos, endPos, realityFactor = 100, smoothingLevel, links, laps, fileName } = activity;
|
|
22
22
|
let name = proposedName ?? activity.name;
|
|
23
23
|
const routeId = route?.id;
|
|
24
24
|
const routeHash = route?.hash;
|
|
@@ -48,7 +48,7 @@ const buildSummary = (activity, proposedName) => {
|
|
|
48
48
|
uploadStatus.push({ service: 'strava', status: 'success' });
|
|
49
49
|
}
|
|
50
50
|
return {
|
|
51
|
-
id, title, name, routeId, routeHash, previewImage, startTime, rideTime, distance, totalElevation, startPos, endPos, realityFactor, uploadStatus, laps
|
|
51
|
+
id, title, name, routeId, routeHash, previewImage, startTime, rideTime, distance, totalElevation, startPos, endPos, realityFactor, smoothingLevel, uploadStatus, laps
|
|
52
52
|
};
|
|
53
53
|
};
|
|
54
54
|
exports.buildSummary = buildSummary;
|
|
@@ -841,7 +841,8 @@ let ActivityRideService = (() => {
|
|
|
841
841
|
const { startPos, realityFactor } = settings;
|
|
842
842
|
const routeId = this.activity.route.id;
|
|
843
843
|
const routeHash = this.activity.route.hash;
|
|
844
|
-
const
|
|
844
|
+
const smoothingLevel = this.getRouteList().getAppliedSmoothingLevel();
|
|
845
|
+
const filter = { routeId, routeHash, startPos, realityFactor, smoothingLevel, minTime: 30, minDistance: 500 };
|
|
845
846
|
(0, list_1.useActivityList)()
|
|
846
847
|
.getPastActivitiesWithDetails(filter)
|
|
847
848
|
.then(prevRides => {
|
|
@@ -1025,8 +1026,9 @@ let ActivityRideService = (() => {
|
|
|
1025
1026
|
const user = this.getUserSettings().get('user', {});
|
|
1026
1027
|
const { weight, ftp } = user;
|
|
1027
1028
|
const uuid = this.getUserSettings().get('uuid', undefined);
|
|
1028
|
-
let selectedRoute = this.getRouteList().
|
|
1029
|
+
let selectedRoute = this.getRouteList().getRideRoute();
|
|
1029
1030
|
const startSettings = this.getRouteList().getStartSettings();
|
|
1031
|
+
const smoothingLevel = this.getRouteList().getAppliedSmoothingLevel();
|
|
1030
1032
|
this.tsStart = Date.now();
|
|
1031
1033
|
const startTime = new Date(this.tsStart).toISOString();
|
|
1032
1034
|
this.current = { deviceData: {}, dataState: {} };
|
|
@@ -1109,7 +1111,7 @@ let ActivityRideService = (() => {
|
|
|
1109
1111
|
distance: 0,
|
|
1110
1112
|
totalElevation: 0,
|
|
1111
1113
|
logs: [],
|
|
1112
|
-
startPos, endPos, segment, realityFactor,
|
|
1114
|
+
startPos, endPos, segment, realityFactor, smoothingLevel,
|
|
1113
1115
|
fileName,
|
|
1114
1116
|
sport
|
|
1115
1117
|
};
|
|
@@ -321,8 +321,9 @@ let PairingPageStateMachine = (() => {
|
|
|
321
321
|
this.performCheck();
|
|
322
322
|
}
|
|
323
323
|
else if (this.state === 'Pairing') {
|
|
324
|
-
this.getDevicePairing().stopPairing();
|
|
325
|
-
this.
|
|
324
|
+
await this.getDevicePairing().stopPairing();
|
|
325
|
+
this._selectState = 'Active';
|
|
326
|
+
this.performCheck();
|
|
326
327
|
}
|
|
327
328
|
}
|
|
328
329
|
resetTimeouts() {
|
|
@@ -308,6 +308,24 @@ let RideDisplayService = (() => {
|
|
|
308
308
|
toggleRightSideView() {
|
|
309
309
|
this.toggleOverlay('sv-right');
|
|
310
310
|
}
|
|
311
|
+
isOverlayPinned(overlay) {
|
|
312
|
+
try {
|
|
313
|
+
return this.getUserSettings().get(`preferences.rideOverlays.${overlay}.pinned`, false);
|
|
314
|
+
}
|
|
315
|
+
catch (err) {
|
|
316
|
+
this.logError(err, 'isOverlayPinned');
|
|
317
|
+
return false;
|
|
318
|
+
}
|
|
319
|
+
}
|
|
320
|
+
setOverlayPinned(overlay, pinned) {
|
|
321
|
+
try {
|
|
322
|
+
this.getUserSettings().set(`preferences.rideOverlays.${overlay}.pinned`, pinned);
|
|
323
|
+
this.observer?.emit('overlay-update', this.getDisplayProperties());
|
|
324
|
+
}
|
|
325
|
+
catch (err) {
|
|
326
|
+
this.logError(err, 'setOverlayPinned');
|
|
327
|
+
}
|
|
328
|
+
}
|
|
311
329
|
onArrowKey(event) {
|
|
312
330
|
try {
|
|
313
331
|
const { key, shiftKey } = event;
|
|
@@ -371,7 +389,8 @@ let RideDisplayService = (() => {
|
|
|
371
389
|
hideAll: this.hideAll,
|
|
372
390
|
showShiftingButtons,
|
|
373
391
|
showDashboard,
|
|
374
|
-
showWorkout
|
|
392
|
+
showWorkout,
|
|
393
|
+
shiftingPinned: this.isOverlayPinned('shifting')
|
|
375
394
|
};
|
|
376
395
|
const childProps = this.getRideModeService()?.getDisplayProperties(props) ?? {};
|
|
377
396
|
return { ...props, ...childProps };
|
|
@@ -81,7 +81,7 @@ let RLVDisplayService = (() => {
|
|
|
81
81
|
this.currentVideo = undefined;
|
|
82
82
|
this.videos = [];
|
|
83
83
|
delete this.startTime;
|
|
84
|
-
this.addVideo(this.
|
|
84
|
+
this.addVideo(this.getRouteList().getRideRoute(), true).then(() => {
|
|
85
85
|
this.videosInitialized = true;
|
|
86
86
|
this.emit('state-update');
|
|
87
87
|
});
|
|
@@ -559,7 +559,7 @@ let RLVDisplayService = (() => {
|
|
|
559
559
|
}
|
|
560
560
|
}
|
|
561
561
|
const current = this.videos.find(v => v.route?.details?.id === route.details.id);
|
|
562
|
-
const nextRoute = this.getRouteList().
|
|
562
|
+
const nextRoute = this.getRouteList().getRideRoute(nextId);
|
|
563
563
|
if (!nextRoute) {
|
|
564
564
|
return false;
|
|
565
565
|
}
|
|
@@ -97,7 +97,7 @@ let RouteDisplayService = (() => {
|
|
|
97
97
|
}
|
|
98
98
|
getDeviceStartSettings() {
|
|
99
99
|
const startSettings = this.getRouteList().getStartSettings();
|
|
100
|
-
const route = this.getRouteList().
|
|
100
|
+
const route = this.getRouteList().getRideRoute();
|
|
101
101
|
const { realityFactor = 100, startPos = 0 } = startSettings;
|
|
102
102
|
return { realityFactor, startPos, route };
|
|
103
103
|
}
|
|
@@ -253,7 +253,7 @@ let RouteDisplayService = (() => {
|
|
|
253
253
|
}
|
|
254
254
|
}
|
|
255
255
|
initRoute() {
|
|
256
|
-
this.currentRoute = this.getRouteList().
|
|
256
|
+
this.currentRoute = this.getRouteList().getRideRoute();
|
|
257
257
|
}
|
|
258
258
|
initView() { }
|
|
259
259
|
get route() {
|
|
@@ -14,6 +14,11 @@ class IncyclistXMLParser extends xml_1.XMLParser {
|
|
|
14
14
|
super();
|
|
15
15
|
this.logger = new gd_eventlog_1.EventLogger('IncyclistParser');
|
|
16
16
|
}
|
|
17
|
+
async buildInfo(context) {
|
|
18
|
+
const info = await super.buildInfo(context);
|
|
19
|
+
info.pointsSource = 'gpx';
|
|
20
|
+
return info;
|
|
21
|
+
}
|
|
17
22
|
async loadDescription(context) {
|
|
18
23
|
await super.loadDescription(context);
|
|
19
24
|
const title = context.data['title'];
|
|
@@ -159,12 +164,15 @@ class IncyclistXMLParser extends xml_1.XMLParser {
|
|
|
159
164
|
if (data['cuts']) {
|
|
160
165
|
this.processCuts(data, route);
|
|
161
166
|
}
|
|
162
|
-
if (data['elevation-shift'])
|
|
163
|
-
this.processElevationShift(Number(data['elevation-shift']), route.points);
|
|
167
|
+
if (data['elevation-shift']) {
|
|
168
|
+
const applied = this.processElevationShift(Number(data['elevation-shift']), route.points);
|
|
169
|
+
if (applied)
|
|
170
|
+
route.elevationShifted = true;
|
|
171
|
+
}
|
|
164
172
|
}
|
|
165
173
|
processElevationShift(elevationShift, points) {
|
|
166
174
|
if (!elevationShift || !points || points.length < 2 || points.length < elevationShift)
|
|
167
|
-
return;
|
|
175
|
+
return false;
|
|
168
176
|
const cuts = points.filter(p => p.isCut) || [];
|
|
169
177
|
const lastPoints = [];
|
|
170
178
|
cuts.forEach(cut => {
|
|
@@ -189,6 +197,7 @@ class IncyclistXMLParser extends xml_1.XMLParser {
|
|
|
189
197
|
idx++;
|
|
190
198
|
}
|
|
191
199
|
});
|
|
200
|
+
return true;
|
|
192
201
|
}
|
|
193
202
|
}
|
|
194
203
|
exports.IncyclistXMLParser = IncyclistXMLParser;
|
|
@@ -0,0 +1,332 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.analyseElevationNoise = exports.getSmoothingGradient = exports.getSteepestGradient = exports.isSmoothingEligible = exports.applySmoothing = exports.smoothElevation = exports.getSmoothingProfile = exports.MAX_SMOOTHING_LEVEL = void 0;
|
|
4
|
+
const route_1 = require("./route");
|
|
5
|
+
const NOISE_REFERENCE_LEVEL = 3;
|
|
6
|
+
const PROFILES = {
|
|
7
|
+
1: { windowLength: 30, passes: 1, spikeThreshold: 5 },
|
|
8
|
+
2: { windowLength: 60, passes: 1, spikeThreshold: 4 },
|
|
9
|
+
3: { windowLength: 120, passes: 2, spikeThreshold: 3 },
|
|
10
|
+
4: { windowLength: 250, passes: 2, spikeThreshold: 3 },
|
|
11
|
+
5: { windowLength: 500, passes: 2, spikeThreshold: 2 },
|
|
12
|
+
};
|
|
13
|
+
exports.MAX_SMOOTHING_LEVEL = 5;
|
|
14
|
+
const NO_OP_PROFILE = {
|
|
15
|
+
windowLength: 0,
|
|
16
|
+
passes: 1,
|
|
17
|
+
spikeThreshold: Number.POSITIVE_INFINITY,
|
|
18
|
+
};
|
|
19
|
+
const getSmoothingProfile = (level) => {
|
|
20
|
+
if (!Number.isFinite(level))
|
|
21
|
+
return { ...NO_OP_PROFILE };
|
|
22
|
+
const rounded = Math.round(level);
|
|
23
|
+
if (rounded < 1)
|
|
24
|
+
return { ...NO_OP_PROFILE };
|
|
25
|
+
const clamped = Math.min(rounded, exports.MAX_SMOOTHING_LEVEL);
|
|
26
|
+
return { ...PROFILES[clamped] };
|
|
27
|
+
};
|
|
28
|
+
exports.getSmoothingProfile = getSmoothingProfile;
|
|
29
|
+
const median3 = (a, b, c) => Math.max(Math.min(a, b), Math.min(Math.max(a, b), c));
|
|
30
|
+
const getSegmentRanges = (points) => {
|
|
31
|
+
const ranges = [];
|
|
32
|
+
let start = 0;
|
|
33
|
+
for (let i = 1; i < points.length; i++) {
|
|
34
|
+
if (points[i]?.isCut === true) {
|
|
35
|
+
ranges.push([start, i]);
|
|
36
|
+
start = i;
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
ranges.push([start, points.length]);
|
|
40
|
+
return ranges;
|
|
41
|
+
};
|
|
42
|
+
const isUsable = (points, start, end) => {
|
|
43
|
+
for (let i = start; i < end; i++) {
|
|
44
|
+
const p = points[i];
|
|
45
|
+
if (!Number.isFinite(p?.elevation) || !Number.isFinite(p?.routeDistance))
|
|
46
|
+
return false;
|
|
47
|
+
}
|
|
48
|
+
return true;
|
|
49
|
+
};
|
|
50
|
+
const despikeRange = (points, start, end, threshold) => {
|
|
51
|
+
if (end - start < 3 || !Number.isFinite(threshold))
|
|
52
|
+
return;
|
|
53
|
+
const original = new Array(end - start);
|
|
54
|
+
for (let i = start; i < end; i++)
|
|
55
|
+
original[i - start] = points[i].elevation;
|
|
56
|
+
for (let i = start + 1; i < end - 1; i++) {
|
|
57
|
+
const m = median3(original[i - 1 - start], original[i - start], original[i + 1 - start]);
|
|
58
|
+
if (Math.abs(original[i - start] - m) > threshold)
|
|
59
|
+
points[i].elevation = m;
|
|
60
|
+
}
|
|
61
|
+
};
|
|
62
|
+
const boxFilterRange = (points, start, end, windowLength) => {
|
|
63
|
+
const n = end - start;
|
|
64
|
+
if (n < 3 || windowLength <= 0)
|
|
65
|
+
return;
|
|
66
|
+
const d = new Array(n);
|
|
67
|
+
const e = new Array(n);
|
|
68
|
+
for (let i = 0; i < n; i++) {
|
|
69
|
+
d[i] = points[start + i].routeDistance - points[start].routeDistance;
|
|
70
|
+
e[i] = points[start + i].elevation;
|
|
71
|
+
}
|
|
72
|
+
const total = d[n - 1];
|
|
73
|
+
if (total <= 0)
|
|
74
|
+
return;
|
|
75
|
+
const cum = new Array(n);
|
|
76
|
+
cum[0] = 0;
|
|
77
|
+
for (let i = 1; i < n; i++)
|
|
78
|
+
cum[i] = cum[i - 1] + ((e[i - 1] + e[i]) / 2) * (d[i] - d[i - 1]);
|
|
79
|
+
const cursor = { lower: 0, upper: 0 };
|
|
80
|
+
const integralAt = (x, which) => {
|
|
81
|
+
let j = cursor[which];
|
|
82
|
+
while (j < n - 2 && d[j + 1] < x)
|
|
83
|
+
j++;
|
|
84
|
+
cursor[which] = j;
|
|
85
|
+
if (x <= d[j])
|
|
86
|
+
return cum[j];
|
|
87
|
+
if (x >= d[j + 1])
|
|
88
|
+
return cum[j + 1];
|
|
89
|
+
const span = d[j + 1] - d[j];
|
|
90
|
+
const t = span > 0 ? (x - d[j]) / span : 0;
|
|
91
|
+
const ex = e[j] + (e[j + 1] - e[j]) * t;
|
|
92
|
+
return cum[j] + ((e[j] + ex) / 2) * (x - d[j]);
|
|
93
|
+
};
|
|
94
|
+
const smoothed = new Array(n);
|
|
95
|
+
for (let i = 0; i < n; i++) {
|
|
96
|
+
const w = Math.min(windowLength, 2 * d[i], 2 * (total - d[i]));
|
|
97
|
+
if (w <= 0) {
|
|
98
|
+
smoothed[i] = e[i];
|
|
99
|
+
continue;
|
|
100
|
+
}
|
|
101
|
+
const value = (integralAt(d[i] + w / 2, 'upper') - integralAt(d[i] - w / 2, 'lower')) / w;
|
|
102
|
+
smoothed[i] = Number.isFinite(value) ? value : e[i];
|
|
103
|
+
}
|
|
104
|
+
for (let i = 0; i < n; i++)
|
|
105
|
+
points[start + i].elevation = smoothed[i];
|
|
106
|
+
};
|
|
107
|
+
const LAG_SHIFT_RATIO = 0.15;
|
|
108
|
+
const getLagShiftDistance = (opts) => opts.passes === 2 ? opts.windowLength * LAG_SHIFT_RATIO : 0;
|
|
109
|
+
const buildElevationLookup = (d, e) => {
|
|
110
|
+
const n = d.length;
|
|
111
|
+
let cursor = 0;
|
|
112
|
+
return (x) => {
|
|
113
|
+
if (x >= d[n - 1]) {
|
|
114
|
+
const span = d[n - 1] - d[n - 2];
|
|
115
|
+
const slope = span > 0 ? (e[n - 1] - e[n - 2]) / span : 0;
|
|
116
|
+
return e[n - 1] + slope * (x - d[n - 1]);
|
|
117
|
+
}
|
|
118
|
+
while (cursor < n - 2 && d[cursor + 1] < x)
|
|
119
|
+
cursor++;
|
|
120
|
+
if (x <= d[cursor])
|
|
121
|
+
return e[cursor];
|
|
122
|
+
const span = d[cursor + 1] - d[cursor];
|
|
123
|
+
const t = span > 0 ? (x - d[cursor]) / span : 0;
|
|
124
|
+
return e[cursor] + (e[cursor + 1] - e[cursor]) * t;
|
|
125
|
+
};
|
|
126
|
+
};
|
|
127
|
+
const applyLagShift = (points, start, end, shiftDistance) => {
|
|
128
|
+
const n = end - start;
|
|
129
|
+
if (n < 2 || shiftDistance <= 0)
|
|
130
|
+
return;
|
|
131
|
+
const d = new Array(n);
|
|
132
|
+
const e = new Array(n);
|
|
133
|
+
for (let i = 0; i < n; i++) {
|
|
134
|
+
d[i] = points[start + i].routeDistance - points[start].routeDistance;
|
|
135
|
+
e[i] = points[start + i].elevation;
|
|
136
|
+
}
|
|
137
|
+
if (d[n - 1] <= 0)
|
|
138
|
+
return;
|
|
139
|
+
const elevationAt = buildElevationLookup(d, e);
|
|
140
|
+
const reference = elevationAt(shiftDistance);
|
|
141
|
+
for (let i = 0; i < n; i++) {
|
|
142
|
+
points[start + i].elevation = e[0] + elevationAt(d[i] + shiftDistance) - reference;
|
|
143
|
+
}
|
|
144
|
+
};
|
|
145
|
+
const LOOP_PADDING_DISTANCE = 800;
|
|
146
|
+
const padForLoop = (points, padDistance) => {
|
|
147
|
+
const total = points.at(-1)?.routeDistance ?? 0;
|
|
148
|
+
if (total <= 0)
|
|
149
|
+
return { padded: points, headCount: 0 };
|
|
150
|
+
const tail = points.filter((p) => total - p.routeDistance <= padDistance);
|
|
151
|
+
const head = points.filter((p) => p.routeDistance <= padDistance);
|
|
152
|
+
const before = tail.map((p) => ({ ...p, routeDistance: p.routeDistance - total, isCut: false }));
|
|
153
|
+
const after = head.map((p) => ({ ...p, routeDistance: p.routeDistance + total, isCut: false }));
|
|
154
|
+
return { padded: [...before, ...points, ...after], headCount: before.length };
|
|
155
|
+
};
|
|
156
|
+
const smoothElevation = (points, opts) => {
|
|
157
|
+
if (!points?.length)
|
|
158
|
+
return [];
|
|
159
|
+
const result = points.map((p) => ({ ...p }));
|
|
160
|
+
if (!opts)
|
|
161
|
+
return result;
|
|
162
|
+
const passes = opts.passes === 2 ? 2 : 1;
|
|
163
|
+
const lagShiftDistance = getLagShiftDistance(opts);
|
|
164
|
+
getSegmentRanges(result).forEach(([start, end]) => {
|
|
165
|
+
if (!isUsable(result, start, end))
|
|
166
|
+
return;
|
|
167
|
+
despikeRange(result, start, end, opts.spikeThreshold);
|
|
168
|
+
for (let pass = 0; pass < passes; pass++)
|
|
169
|
+
boxFilterRange(result, start, end, opts.windowLength);
|
|
170
|
+
applyLagShift(result, start, end, lagShiftDistance);
|
|
171
|
+
});
|
|
172
|
+
return result;
|
|
173
|
+
};
|
|
174
|
+
exports.smoothElevation = smoothElevation;
|
|
175
|
+
const applySmoothing = (route, level) => {
|
|
176
|
+
const smoothed = route?.clone();
|
|
177
|
+
if (!smoothed)
|
|
178
|
+
return smoothed;
|
|
179
|
+
if (!Number.isFinite(level) || level < 1)
|
|
180
|
+
return smoothed;
|
|
181
|
+
const source = smoothed.details?.points ?? smoothed.description?.points;
|
|
182
|
+
if (!source?.length)
|
|
183
|
+
return smoothed;
|
|
184
|
+
const profile = (0, exports.getSmoothingProfile)(level);
|
|
185
|
+
let points;
|
|
186
|
+
if ((0, route_1.checkIsLoop)(source)) {
|
|
187
|
+
const { padded, headCount } = padForLoop(source, LOOP_PADDING_DISTANCE);
|
|
188
|
+
points = (0, exports.smoothElevation)(padded, profile).slice(headCount, headCount + source.length);
|
|
189
|
+
}
|
|
190
|
+
else {
|
|
191
|
+
points = (0, exports.smoothElevation)(source, profile);
|
|
192
|
+
}
|
|
193
|
+
if (smoothed.details?.points)
|
|
194
|
+
smoothed.details.points = points;
|
|
195
|
+
if (smoothed.description?.points)
|
|
196
|
+
smoothed.description.points = points;
|
|
197
|
+
points.forEach((p) => {
|
|
198
|
+
delete p.slope;
|
|
199
|
+
delete p.elevationGain;
|
|
200
|
+
});
|
|
201
|
+
if (points.some((p) => !Number.isFinite(p.cnt)))
|
|
202
|
+
(0, route_1.updateCnt)(points);
|
|
203
|
+
(0, route_1.updateSlopes)(points, false);
|
|
204
|
+
(0, route_1.updateElevationGain)(points);
|
|
205
|
+
const elevation = points.at(-1)?.elevationGain;
|
|
206
|
+
if (Number.isFinite(elevation)) {
|
|
207
|
+
if (smoothed.description)
|
|
208
|
+
smoothed.description.elevation = elevation;
|
|
209
|
+
if (smoothed.details)
|
|
210
|
+
smoothed.details.elevation = elevation;
|
|
211
|
+
}
|
|
212
|
+
return smoothed;
|
|
213
|
+
};
|
|
214
|
+
exports.applySmoothing = applySmoothing;
|
|
215
|
+
const MIN_ELIGIBLE_POINTS = 8;
|
|
216
|
+
const getRoutePoints = (route) => {
|
|
217
|
+
const points = route?.points ?? route?.details?.points ?? route?.description?.points;
|
|
218
|
+
return Array.isArray(points) ? points : [];
|
|
219
|
+
};
|
|
220
|
+
const isVideoRoute = (route) => route?.description?.hasVideo === true || route?.details?.video !== undefined;
|
|
221
|
+
const isInScopeVideoRoute = (route) => {
|
|
222
|
+
if (route?.description?.pointsSource === 'gpx')
|
|
223
|
+
return true;
|
|
224
|
+
const points = route?.details?.points;
|
|
225
|
+
if (!Array.isArray(points))
|
|
226
|
+
return false;
|
|
227
|
+
return points.some((p) => p?.isCut === true) || points.some((p) => Number.isFinite(p?.time));
|
|
228
|
+
};
|
|
229
|
+
const isSmoothingEligible = (route) => {
|
|
230
|
+
try {
|
|
231
|
+
const points = getRoutePoints(route);
|
|
232
|
+
if (points.length < MIN_ELIGIBLE_POINTS)
|
|
233
|
+
return false;
|
|
234
|
+
if (!points.every((p) => Number.isFinite(p?.elevation)))
|
|
235
|
+
return false;
|
|
236
|
+
if (new Set(points.map((p) => Math.round(p.elevation))).size < 2)
|
|
237
|
+
return false;
|
|
238
|
+
if (!points.some((p) => p?.lat && p?.lng))
|
|
239
|
+
return false;
|
|
240
|
+
if (route?.description?.hasGpx === false)
|
|
241
|
+
return false;
|
|
242
|
+
if (route?.details?.gpxDisabled)
|
|
243
|
+
return false;
|
|
244
|
+
if (route?.details?.elevationShifted)
|
|
245
|
+
return false;
|
|
246
|
+
if (route?.details?.epp)
|
|
247
|
+
return false;
|
|
248
|
+
return !isVideoRoute(route) || isInScopeVideoRoute(route);
|
|
249
|
+
}
|
|
250
|
+
catch {
|
|
251
|
+
return false;
|
|
252
|
+
}
|
|
253
|
+
};
|
|
254
|
+
exports.isSmoothingEligible = isSmoothingEligible;
|
|
255
|
+
const getSteepestGradient = (points) => {
|
|
256
|
+
if (!Array.isArray(points))
|
|
257
|
+
return 0;
|
|
258
|
+
let steepest = 0;
|
|
259
|
+
for (const p of points) {
|
|
260
|
+
const slope = p?.slope;
|
|
261
|
+
if (!Number.isFinite(slope))
|
|
262
|
+
continue;
|
|
263
|
+
const magnitude = Math.abs(slope);
|
|
264
|
+
if (magnitude > steepest)
|
|
265
|
+
steepest = magnitude;
|
|
266
|
+
}
|
|
267
|
+
return steepest;
|
|
268
|
+
};
|
|
269
|
+
exports.getSteepestGradient = getSteepestGradient;
|
|
270
|
+
const MIN_VISIBLE_GRADIENT_DELTA = 0.5;
|
|
271
|
+
const MIN_VISIBLE_GAIN_RATIO = 0.02;
|
|
272
|
+
const getSmoothingGradient = (routePoints, smoothedPoints, routeGain, smoothedGain) => {
|
|
273
|
+
const routeSteepest = (0, exports.getSteepestGradient)(routePoints);
|
|
274
|
+
const smoothedSteepest = (0, exports.getSteepestGradient)(smoothedPoints);
|
|
275
|
+
const gradientDelta = Math.abs(routeSteepest - smoothedSteepest);
|
|
276
|
+
const gainRatio = Number.isFinite(routeGain) && routeGain > 0 && Number.isFinite(smoothedGain)
|
|
277
|
+
? Math.abs(routeGain - smoothedGain) / routeGain
|
|
278
|
+
: 0;
|
|
279
|
+
const hasVisibleEffect = gradientDelta >= MIN_VISIBLE_GRADIENT_DELTA || gainRatio >= MIN_VISIBLE_GAIN_RATIO;
|
|
280
|
+
return { routeSteepest, smoothedSteepest, hasVisibleEffect };
|
|
281
|
+
};
|
|
282
|
+
exports.getSmoothingGradient = getSmoothingGradient;
|
|
283
|
+
const getElevationGain = (points) => {
|
|
284
|
+
let gain = 0;
|
|
285
|
+
for (let i = 1; i < points.length; i++) {
|
|
286
|
+
const delta = points[i].elevation - points[i - 1].elevation;
|
|
287
|
+
if (delta > 0)
|
|
288
|
+
gain += delta;
|
|
289
|
+
}
|
|
290
|
+
return gain;
|
|
291
|
+
};
|
|
292
|
+
const getMedian = (values) => {
|
|
293
|
+
if (!values.length)
|
|
294
|
+
return 0;
|
|
295
|
+
const sorted = [...values].sort((a, b) => a - b);
|
|
296
|
+
const mid = Math.floor(sorted.length / 2);
|
|
297
|
+
return sorted.length % 2 ? sorted[mid] : (sorted[mid - 1] + sorted[mid]) / 2;
|
|
298
|
+
};
|
|
299
|
+
const analyseElevationNoise = (points) => {
|
|
300
|
+
const empty = { gainRatio: 1, reversalDensity: 0, pointCount: 0, medianSpacing: 0 };
|
|
301
|
+
if (!points?.length)
|
|
302
|
+
return empty;
|
|
303
|
+
const usable = points.filter((p) => Number.isFinite(p?.elevation) && Number.isFinite(p?.routeDistance));
|
|
304
|
+
if (usable.length < 2)
|
|
305
|
+
return { ...empty, pointCount: points.length };
|
|
306
|
+
const spacings = [];
|
|
307
|
+
let reversals = 0;
|
|
308
|
+
let prevSign = 0;
|
|
309
|
+
for (let i = 1; i < usable.length; i++) {
|
|
310
|
+
const dd = usable[i].routeDistance - usable[i - 1].routeDistance;
|
|
311
|
+
if (dd > 0)
|
|
312
|
+
spacings.push(dd);
|
|
313
|
+
const de = usable[i].elevation - usable[i - 1].elevation;
|
|
314
|
+
const sign = Math.sign(de);
|
|
315
|
+
if (sign !== 0) {
|
|
316
|
+
if (prevSign !== 0 && sign !== prevSign)
|
|
317
|
+
reversals++;
|
|
318
|
+
prevSign = sign;
|
|
319
|
+
}
|
|
320
|
+
}
|
|
321
|
+
const distanceKm = (usable.at(-1).routeDistance - usable[0].routeDistance) / 1000;
|
|
322
|
+
const rawGain = getElevationGain(usable);
|
|
323
|
+
const smoothedGain = getElevationGain((0, exports.smoothElevation)(usable, (0, exports.getSmoothingProfile)(NOISE_REFERENCE_LEVEL)));
|
|
324
|
+
const gainRatio = smoothedGain > 0 ? rawGain / smoothedGain : 1;
|
|
325
|
+
return {
|
|
326
|
+
gainRatio,
|
|
327
|
+
reversalDensity: distanceKm > 0 ? reversals / distanceKm : 0,
|
|
328
|
+
pointCount: points.length,
|
|
329
|
+
medianSpacing: getMedian(spacings),
|
|
330
|
+
};
|
|
331
|
+
};
|
|
332
|
+
exports.analyseElevationNoise = analyseElevationNoise;
|
|
@@ -47,6 +47,7 @@ const utils_1 = require("../../../utils");
|
|
|
47
47
|
const service_2 = require("../../download/service");
|
|
48
48
|
const gd_eventlog_1 = require("gd-eventlog");
|
|
49
49
|
const route_1 = require("../../base/utils/route");
|
|
50
|
+
const smoothing_1 = require("../../base/utils/smoothing");
|
|
50
51
|
const workouts_1 = require("../../../workouts");
|
|
51
52
|
const utils_2 = require("../utils");
|
|
52
53
|
const monitoring_1 = require("../../../monitoring");
|
|
@@ -103,6 +104,7 @@ let RouteCard = (() => {
|
|
|
103
104
|
ready;
|
|
104
105
|
logger;
|
|
105
106
|
cntActive = 0;
|
|
107
|
+
smoothingPreview;
|
|
106
108
|
constructor(route, props) {
|
|
107
109
|
super();
|
|
108
110
|
const { list } = props || {};
|
|
@@ -412,8 +414,11 @@ let RouteCard = (() => {
|
|
|
412
414
|
const totalElevation = { value: C(this.route.description.elevation, 'elevation', { digits: 0 }), unit: uEl };
|
|
413
415
|
const xScale = { value: C(1, 'distance'), unit: uDist };
|
|
414
416
|
const yScale = { value: C(1, 'elevation'), unit: uEl };
|
|
417
|
+
const smoothingAvailable = this.isSmoothingAvailable();
|
|
418
|
+
const { smoothedPoints, smoothedElevation, smoothedGradient } = this.getSmoothingPreview(uiSettings.smoothingLevel);
|
|
415
419
|
return { settings: uiSettings, totalDistance, totalElevation, showLoopOverwrite, showNextOverwrite, hasWorkout, showWorkoutOption, canStart, videoChecking, videoMissing, detailsAvailable,
|
|
416
420
|
xScale, yScale,
|
|
421
|
+
smoothingAvailable, smoothingMaxLevel: smoothing_1.MAX_SMOOTHING_LEVEL, smoothedPoints, smoothedElevation, smoothedGradient,
|
|
417
422
|
updateStartPos: this.updateStartPos.bind(this),
|
|
418
423
|
updateMarkers: this.updateMarkers.bind(this)
|
|
419
424
|
};
|
|
@@ -446,19 +451,21 @@ let RouteCard = (() => {
|
|
|
446
451
|
delete props.prevRides;
|
|
447
452
|
if (isUI) {
|
|
448
453
|
const uiProps = props;
|
|
449
|
-
const { realityFactor, segment, showPrev, loopOverwrite, nextOverwrite } = uiProps;
|
|
454
|
+
const { realityFactor, segment, showPrev, loopOverwrite, nextOverwrite, smoothingLevel } = uiProps;
|
|
450
455
|
const startPos = C(uiProps.startPos.value, 'distance', { from: U('distance'), to: 'm' }) ?? 0;
|
|
451
456
|
const endPos = uiProps.endPos === undefined ? undefined : C(uiProps.endPos.value, 'distance', { from: U('distance'), to: 'm' });
|
|
452
457
|
this.startSettings = {
|
|
453
458
|
...this.startSettings,
|
|
454
|
-
startPos, endPos, realityFactor, segment, showPrev, loopOverwrite, nextOverwrite
|
|
459
|
+
startPos, endPos, realityFactor, segment, showPrev, loopOverwrite, nextOverwrite,
|
|
460
|
+
smoothingLevel: smoothingLevel ?? this.startSettings?.smoothingLevel
|
|
455
461
|
};
|
|
456
462
|
}
|
|
457
463
|
else {
|
|
458
|
-
const { startPos, endPos, realityFactor, segment, showPrev, loopOverwrite, nextOverwrite } = props;
|
|
464
|
+
const { startPos, endPos, realityFactor, segment, showPrev, loopOverwrite, nextOverwrite, smoothingLevel } = props;
|
|
459
465
|
this.startSettings = {
|
|
460
466
|
...this.startSettings,
|
|
461
|
-
startPos, endPos, realityFactor, segment, showPrev, loopOverwrite, nextOverwrite
|
|
467
|
+
startPos, endPos, realityFactor, segment, showPrev, loopOverwrite, nextOverwrite,
|
|
468
|
+
smoothingLevel: smoothingLevel ?? this.startSettings?.smoothingLevel
|
|
462
469
|
};
|
|
463
470
|
}
|
|
464
471
|
const userSettings = (0, settings_1.useUserSettings)();
|
|
@@ -470,6 +477,59 @@ let RouteCard = (() => {
|
|
|
470
477
|
this.logError(err, 'changeSettings');
|
|
471
478
|
}
|
|
472
479
|
}
|
|
480
|
+
getSmoothingPreview(level) {
|
|
481
|
+
const none = {};
|
|
482
|
+
try {
|
|
483
|
+
if (!Number.isFinite(level) || level < 1)
|
|
484
|
+
return none;
|
|
485
|
+
if (!this.isSmoothingAvailable())
|
|
486
|
+
return none;
|
|
487
|
+
const key = `${this.getRouteDescription()?.id}:${Math.round(level)}`;
|
|
488
|
+
if (this.smoothingPreview?.key !== key) {
|
|
489
|
+
const smoothed = (0, smoothing_1.applySmoothing)(this.route, level);
|
|
490
|
+
const points = smoothed?.points;
|
|
491
|
+
if (!points?.length)
|
|
492
|
+
return none;
|
|
493
|
+
const elevation = smoothed.description?.elevation ?? smoothed.details?.elevation;
|
|
494
|
+
const routeElevation = this.route.description?.elevation ?? this.route.details?.elevation;
|
|
495
|
+
this.smoothingPreview = {
|
|
496
|
+
key,
|
|
497
|
+
points,
|
|
498
|
+
elevation,
|
|
499
|
+
gradient: (0, smoothing_1.getSmoothingGradient)(this.route.points, points, routeElevation, elevation)
|
|
500
|
+
};
|
|
501
|
+
}
|
|
502
|
+
const { points: smoothedPoints, elevation, gradient } = this.smoothingPreview;
|
|
503
|
+
const [C, U] = (0, i18n_1.getUnitConversionShortcuts)();
|
|
504
|
+
return {
|
|
505
|
+
smoothedPoints,
|
|
506
|
+
smoothedElevation: Number.isFinite(elevation)
|
|
507
|
+
? { value: C(elevation, 'elevation', { digits: 0 }), unit: U('elevation') }
|
|
508
|
+
: undefined,
|
|
509
|
+
smoothedGradient: gradient
|
|
510
|
+
};
|
|
511
|
+
}
|
|
512
|
+
catch (err) {
|
|
513
|
+
this.logError(err, 'getSmoothingPreview');
|
|
514
|
+
return none;
|
|
515
|
+
}
|
|
516
|
+
}
|
|
517
|
+
isSmoothingAvailable() {
|
|
518
|
+
return (0, smoothing_1.isSmoothingEligible)(this.route);
|
|
519
|
+
}
|
|
520
|
+
getPrevRidesFilter(settings) {
|
|
521
|
+
const routeHash = this.getRouteDescription()?.routeHash;
|
|
522
|
+
const routeId = routeHash ? undefined : this.getRouteDescription()?.id;
|
|
523
|
+
const smoothingLevel = this.isSmoothingAvailable() ? (settings.smoothingLevel ?? 0) : 0;
|
|
524
|
+
return {
|
|
525
|
+
routeHash,
|
|
526
|
+
routeId,
|
|
527
|
+
startPos: settings.startPos,
|
|
528
|
+
endPos: settings.endPos,
|
|
529
|
+
realityFactor: settings.realityFactor,
|
|
530
|
+
smoothingLevel,
|
|
531
|
+
};
|
|
532
|
+
}
|
|
473
533
|
adjustStartPosAvi(settings) {
|
|
474
534
|
try {
|
|
475
535
|
if (this.route.description.videoFormat === 'avi') {
|